WFFM Discreet Analytics

Posted 21 July 2017 12:00 AM by Benjamin Moles, Senior .NET Developer @ ClearPeople

This article describes a Sitecore customisation with the following sections:

  1. Motivation: Describes the reasons to implement the customisation.
  2. Usage: Describes how to use the customisation.
  3. Implementation: Describes the main details of the implementation and provides the relevant source code.
  4. Technical Details: Records the Sitecore version on which the customisation was implemented along with the version of any related module.

Motivation

A WFFM form is required to store analytic data. Some form’s fields are required to be stored and other cannot be stored due to privacy specifications. WFFM on Sitecore 8.0 allows to enable and disable form analytics. However, it is an all-or-none choice. WFFM, OOTB does not allow to specify which fields should be stored and which should be ignored.

Usage

The feature implemented next allows to specify in a configuration file what fields of a specific form should be masked (ignored) while storing the values submitted by the user. Masked fields can be defined by their name or their field Id.

Implementation

The WFFM forms analytic data is stored by the class “Sitecore.WFFM.Analytics.Providers.AnalyticsDataProvider”, implemented in the assembly “Sitecore.WFFM.Analytics” and injected into Sitecore WFFM by the configuration file “Sitecore.WFFM.Analytics.config” as follows:

<wffm>
    <formsDataProvider type="Sitecore.WFFM.Analytics.Providers.AnalyticsDataProvider, Sitecore.WFFM.Analytics" singleInstance="true"/>
    <facetFactory type="Sitecore.WFFM.Analytics.FacetFactory, Sitecore.WFFM.Analytics" singleInstance="true"/>
</wffm> 

The fields masking process can take place during the storage process therefore a new DiscreetAnalyticsDataProvider needs to substitute the original one to implement the required logic. The starting point is the original AnalyticsDataProvider which code can be obtained by decompiling its DLL (included at the end of the article). The method to look at is “InsertForm”. Last line within the method “InsertForm” is responsible for storing the data in MongoDB therefore just before this line the masking process will take place.

public void InsertForm(IFormData form)
{
    object obj = null;
    if (Warn.IsNull(Tracker.Current, "Tracker.Current") || Warn.IsNull(Tracker.Current.Session, "Tracker.Current.Session"))
    {
        return;
    }
    if (Tracker.Current.Session.CustomData.ContainsKey(Sitecore.WFFM.Analytics.Core.Constants.FormsCollectionName) && !Tracker.Current.Session.CustomData.TryGetValue(Sitecore.WFFM.Analytics.Core.Constants.FormsCollectionName, out obj))
    {
        return;
    }
    WffmContext wffmContext = obj as WffmContext;
    if (wffmContext == null)
    {
        wffmContext = new WffmContext();
        Tracker.Current.Session.CustomData.Add(Sitecore.WFFM.Analytics.Core.Constants.FormsCollectionName, wffmContext);
    }
    RemoveUnwantedData(form);
    wffmContext.Forms.Add(form);
}

A new method named “RemoveUnwantedData” will take care of removing the form fields which values are sensitive and should not be stored. Before implementing it, some scaffolding code is required in order to get the masked field definitions from the configuration file.

File: Web\WFFM\Analytics\Providers\FieldMasking.cs

using System;
using System.Collections.Generic;

namespace Web.WFFM.Analytics.Providers
{
    public class FieldMasking: Dictionary<Guid, FieldMaskingForm>
    {

        public void Add(FieldMaskingForm form)
        {
            this.Add(form.FormId, form);
        }
    }
}

File: Web\WFFM\Analytics\Providers\FieldMaskingField.cs

using System;
using System.Collections.Generic;

namespace Web.WFFM.Analytics.Providers
{
    public class FieldMaskingForm
    {
        private List>FieldMaskingField< fields = new List>FieldMaskingField<();
        private HashSet>Guid< maskedFieldIds = new HashSet>Guid<();
        private HashSet>string< maskedFieldNames = new HashSet>string<();

        public Guid FormId { get; set; }

        public HashSet>Guid< MaskedFieldIds { get { return this.maskedFieldIds; } }
        public HashSet>string< MaskedFieldNames { get { return this.maskedFieldNames; } }

       public void Add(FieldMaskingField field)
        {
            if (field.Id == null && field.Name == null)
            {
                throw new ArgumentException("The field needs to have an Id or a Name.");
            }
            if (field.Id != null) maskedFieldIds.Add(field.Id);
            if (field.Name != null) maskedFieldNames.Add(field.Name);
            this.fields.Add(field);
        }
    }
}

File: Web\WFFM\Analytics\Providers\FieldMaskingForm.cs

using System;

namespace Web.WFFM.Analytics.Providers
{
    public class FieldMaskingField
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
    }
}

The classes provide above will hold the data relative to the fields that need to be ignored. They will be exposed to the method “RemoveUnwantedData” through a new property that needs to be added to the new class “DiscreetAnalyticsDataProvider”:

public FieldMasking FieldMasks { get; set; }

The property “FieldMasks” will be automatically populated by WFFM once it is properly configured. Now that all scaffolding code is in place the “RemoveUnwantedData” can be implemented:

private void RemoveUnwantedData(IFormData form)
{
    var masks = this.FieldMasks;
    if (masks == null || masks.Count == 0) return;

    Guid formId = form.FormID;

    if (masks.ContainsKey(formId))
    {
        var maskedForm = masks[formId];
        var maskedFieldIds = maskedForm.MaskedFieldIds;
        var maskedFieldNames = maskedForm.MaskedFieldNames;

        var newFields = new List<IFieldData>();

        foreach (var field in form.Fields)
        {
            var fieldId = field.FieldId;
            var fieldName = field.FieldName;
            if (maskedFieldIds.Any(id => id == fieldId) || maskedFieldNames.Any(n => n == fieldName))
            {
                field.Data = string.Empty;
                field.Value = string.Empty;
            }
            else
            {
                newFields.Add(field);
            }
        }

        form.Fields = newFields;

    }
}

As previously mentioned, for the new code to work Sitecore configuration needs to be changed with two main purposes:

  1. Replace original “AnalyticsDataProvider” class with the new “DiscreetAnalyticsDataProvider” class.
  2. Indicate what fields need to be masked (ignored).

Following best practices, the original Sitecore configuration files will remain untouched and the changes will be done with a new patching configuration file which name can be any. The only relevant thing is that needs to be read after the Sitecore original one (Website\App_Config\Include\Sitecore.WFFM.Analytics.config). A good way to achieve this is by adding a subfolder under “App_Config\Include” with the name of the current feature.

The required configuration file content is:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>

    <wffm>
      <formsDataProvider type="Web.WFFM.Analytics.Providers.DiscreetAnalyticsDataProvider, Website" singleInstance="true" patch:instead="formsDataProvider" >
        <FieldMasks ref="wffm/fieldMasking" />
      </formsDataProvider>

      <fieldMasking type="Web.WFFM.Analytics.Providers.FieldMasking, Website">
        <forms hint="list:Add">

          <form type="Web.WFFM.Analytics.Providers.FieldMaskingForm, Website">
            <FormId>{00001111-2222-AAAA-BBBB-CCCCDDDDEEEE}</FormId>
            <fields hint="list:Add">
              <field type="Web.WFFM.Analytics.Providers.FieldMaskingField, Website"><name>Field1</name></field>
              <field type="Web.WFFM.Analytics.Providers.FieldMaskingField, Website"><name>Field2</name></field>
              <field type="Web.WFFM.Analytics.Providers.FieldMaskingField, Website"><name>Field3</name></field>
            </fields>
          </form>

          <form type="Web.WFFM.Analytics.Providers.FieldMaskingForm, Website">
            <FormId>{33334444-5555-AAAA-BBBB-123412341234}</FormId>
            <fields hint="list:Add">
              <field type="Web.WFFM.Analytics.Providers.FieldMaskingField, Website"><name>FieldA</name></field>
              <field type="Web.WFFM.Analytics.Providers.FieldMaskingField, Website"><name>FieldB</name></field>
              <field type="Web.WFFM.Analytics.Providers.FieldMaskingField, Website"><name>FieldC</name></field>
            </fields>
          </form>

        </forms>
      </fieldMasking>
      
    </wffm>

  </sitecore>
</configuration>

Please notice that the xml element “FormId” and the field names need to be updated with the details of the actual form which fields need to be masked. Below is the entire code of the class “DiscreetAnalyticsDataProvider”.

File: Web\WFFM\Analytics\Providers\DiscreetAnalyticsDataProvider.cs

using Sitecore.Analytics;
using Sitecore.Analytics.Reporting;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.WFFM.Analytics;
using Sitecore.WFFM.Analytics.Core;
using Sitecore.WFFM.Analytics.Model;
using Sitecore.WFFM.Analytics.Providers;
using Sitecore.WFFM.Analytics.Providers.Common;
using Sitecore.WFFM.Analytics.Queries;
using Sitecore.WFFM.Core.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Web.WFFM.Analytics.Providers
{
    public class DiscreetAnalyticsDataProvider : IWfmDataProvider
    {
        public ReportDataProviderBase ReportsProvider
        {
            get
            {
                return Assert.ResultNotNull<ReportDataProvider>(Factory.CreateObject("reporting/dataProvider", true) as ReportDataProvider, "ReportDataProvider is null.");
            }
        }
        public IEnumerable<IFormData> GetFormData(Guid formId)
        {
            ID formDataReportQuery = IDs.FormDataReportQuery;
            FormDataReportQuery formDataReportQuery2 = new FormDataReportQuery(formId, formDataReportQuery, this.ReportsProvider, null);
            formDataReportQuery2.Execute();
            return formDataReportQuery2.Data;
        }
        public void InsertForm(IFormData form)
        {
            object obj = null;
            if (Warn.IsNull(Tracker.Current, "Tracker.Current") || Warn.IsNull(Tracker.Current.Session, "Tracker.Current.Session"))
            {
                return;
            }
            if (Tracker.Current.Session.CustomData.ContainsKey(Sitecore.WFFM.Analytics.Core.Constants.FormsCollectionName) && !Tracker.Current.Session.CustomData.TryGetValue(Sitecore.WFFM.Analytics.Core.Constants.FormsCollectionName, out obj))
            {
                return;
            }
            WffmContext wffmContext = obj as WffmContext;
            if (wffmContext == null)
            {
                wffmContext = new WffmContext();
                Tracker.Current.Session.CustomData.Add(Sitecore.WFFM.Analytics.Core.Constants.FormsCollectionName, wffmContext);
            }
            RemoveUnwantedData(form);
            wffmContext.Forms.Add(form);
        }

        public FieldMasking FieldMasks { get; set; }

        private void RemoveUnwantedData(IFormData form)
        {
            var masks = this.FieldMasks;
            if (masks == null || masks.Count == 0) return;

            Guid formId = form.FormID;

            if (masks.ContainsKey(formId))
            {
                var maskedForm = masks[formId];
                var maskedFieldIds = maskedForm.MaskedFieldIds;
                var maskedFieldNames = maskedForm.MaskedFieldNames;

                var newFields = new List<IFieldData>();

                foreach (var field in form.Fields)
                {
                    var fieldId = field.FieldId;
                    var fieldName = field.FieldName;
                    if (maskedFieldIds.Any(id => id == fieldId) || maskedFieldNames.Any(n => n == fieldName))
                    {
                        field.Data = string.Empty;
                        field.Value = string.Empty;
                    }
                    else
                    {
                        newFields.Add(field);
                    }
                }

                form.Fields = newFields;

            }
        }

        public IEnumerable<IFormContactsResult> GetFormsStatisticsByContact(Guid formId, PageCriteria pageCriteria)
        {
            ID formStatisticsByContactsReportQuery = IDs.FormStatisticsByContactsReportQuery;
            FormStatisticsByContactReportQuery formStatisticsByContactReportQuery = new FormStatisticsByContactReportQuery(formId, formStatisticsByContactsReportQuery, this.ReportsProvider, null, null);
            formStatisticsByContactReportQuery.Execute();
            return formStatisticsByContactReportQuery.Data.Skip(pageCriteria.PageIndex).Take(pageCriteria.PageSize);
        }
        public IFormStatistics GetFormStatistics(Guid formId)
        {
            ID formSubmitStatisticsReportQuery = IDs.FormSubmitStatisticsReportQuery;
            FormSummaryReportQuery formSummaryReportQuery = new FormSummaryReportQuery(formId, formSubmitStatisticsReportQuery, this.ReportsProvider, null);
            formSummaryReportQuery.Execute();
            return new FormStatistics
            {
                Dropouts = formSummaryReportQuery.Dropouts,
                SubmitsCount = formSummaryReportQuery.SubmitsCount,
               Visits = formSummaryReportQuery.Visits,
                SuccessSubmits = formSummaryReportQuery.Success
            };
        }
        public IEnumerable<IFormFieldStatistics> GetFormFieldsStatistics(Guid formId)
        {
            ID formFieldsStatisticsReportQuery = IDs.FormFieldsStatisticsReportQuery;
            FormFieldsStatisticsReportQuery formFieldsStatisticsReportQuery2 = new FormFieldsStatisticsReportQuery(formId, formFieldsStatisticsReportQuery, this.ReportsProvider, null);
            formFieldsStatisticsReportQuery2.Execute();
            return formFieldsStatisticsReportQuery2.Data;
        }
    }

}

Technical Details

The provided code has been tested for the following Sitecore versions: • Sitecore: 8.0 rev. 150812 • WFFM: 8.0 rev. 150625

Share:

Add your comment

 
 

 

Archive

Tagcloud

Digital Transformation employee engagement staff satisfaction productivity Microsoft Teams Office 365 Yammer cms content management system agile GDPR Microsoft Graph collaboration Microsoft sharepoint 2016 upgrade migration SharePoint Online 2016 Tech Trends Digital Disruption Context marketing marketing SharePoint 2010 SharePoint 2013 TFS Git security kentico Analytics intranet jquery QA Quality Assurance testing digital workspace content management websites Sitecore sitecore marketplace sitecore module cloud Microsoft Cloud Storage digital strategy technical consulting sitecore modules Experience database Sitecore 7 Sitecore 8 support account management customer experience Data Storage windows azure cms integration front end front end development prototype Cloud Storage StorSimple Front-end Development Layout SharePoint 2013 colour palette UI design website design log viewer sitecore cms website Azure big data business-critical sharepoint accessibility android apple chrome clear people clearpeople debug emulator ios mobile testing opera resize adobe desktop flash ie10 internet explorer 10 metro windows 8 bcsp SharePoint Advanced System Reporter reporting framework ControlMode form control master page placeholder publishing console SharePoint 2007 SharePoint error search search results search values software testing testing scenario audit content information architecture retention schedules PowerShell QuickLaunch scripts SharePoint server 2010 business solutions metalogix replication replicator storagepoint stena technet UK Technet picture library slideshow web part RTM released to manufacturing caml caml query MOSS 2007 query infopath