Showing posts with label goal. Show all posts
Showing posts with label goal. Show all posts

Wednesday, February 7, 2018

Displaying extra data on a Sitecore 9 Goal

Goals in Sitecore 9

In our project we had goals set when a whitepaper was downloaded. This works fine, but it would be much nicer if we could show which whitepaper was actually downloaded. All the whitepapers have an identifier code, so the challenge was to add this identifier to the goal data.

Capturing custom goal data

Capturing the data is very well explained in the official documentation of Sitecore. We used the "Data" and "DataKey" properties of the goal:
  • DataKey: a key that identifies the contents of  "Data" - e.g. 'Whitepaper code'
  • Data : any data collected as part of triggering the event - in our case the whitepaper identifier
This all works fine and we can see the data in the database (xDB shards) as part of the events.

Displaying custom goal data

Data is nice, but you need a way to show it. It wasn't immediatly clear how to do this, so I asked it on Sitecore Stack Exchange.
Jarmo Jarvi pointed me in the good direction by mentioning a tremendous blog post by Jonathan Robbins. The blog post was based on Sitecore 8 - the idea behind my post here is to show the differences when doing this in Sitecore 9 and xConnect. The result will be the same:
an extra column in the goals section of the activity tab of a contact in the Experience Profile

ExperienceProfileContactViews

The ExperienceProfileContactViews pipeline is where all the magic happens. Adding the extra column to the results table is identical to the description for Sitecore 8. Fetching the data (in GetGoals) however is quite different as we have to use xConnect now:
public class AddGoalDataColumn : ReportProcessorBase
{
  public override void Process(ReportProcessorArgs args)
  {
    args.ResultTableForView?.Columns.Add(Schema.GoalData.ToColumn());
  }
}

public static class Schema
{
  public static ViewField GoalData = new ViewField("GoalData");
}

public class FillGoalData : ReportProcessorBase
{
  public override void Process(ReportProcessorArgs args)
  {
    var resultTableForView = args.ResultTableForView;
    Assert.IsNotNull(resultTableForView, "Result table for {0} could not be found.", args.ReportParameters.ViewName);
    var i = 0;
    foreach (var row in resultTableForView.AsEnumerable())
    {
      var goalData = args.QueryResult.Rows[i].ItemArray[4];
      if (goalData != null)
      {
        row[Schema.GoalData.Name] = goalData;
      }

      i++;
    }
  }
}
public class GetGoals : ReportProcessorBase
{
  public override void Process(ReportProcessorArgs args)
  {
    var goalsDataXconnect = GetGoalsDataXconnect(args.ReportParameters.ContactId);
    args.QueryResult = goalsDataXconnect;
  }

  private static DataTable GetGoalsDataXconnect(Guid contactId)
  {
    var goalsTableWithSchema = CreateGoalsTableWithSchema();
    var contactExpandOptions = new ContactExpandOptions(Array.Empty<string>())
    {
      Interactions = new RelatedInteractionsExpandOptions("WebVisit")
      {
        StartDateTime = DateTime.MinValue
      }
    };

    FillRawTable(GetContactByOptions(contactId, contactExpandOptions).Interactions.Where(p => p.Events.OfType<Goal>().Any()), goalsTableWithSchema);
    return goalsTableWithSchema;
  }

  private static DataTable CreateGoalsTableWithSchema()
  {
    var dataTable = new DataTable();
    dataTable.Columns.AddRange(new[]
    {
      new DataColumn("_id", typeof(Guid)),
      new DataColumn("ContactId", typeof(Guid)),
      new DataColumn("Pages_PageEvents_PageEventDefinitionId", typeof(Guid)),
      new DataColumn("Pages_PageEvents_DateTime", typeof(DateTime)),
      new DataColumn("Pages_PageEvents_Data", typeof(string)),
      new DataColumn("Pages_Url_Path", typeof(string)),
      new DataColumn("Pages_Url_QueryString", typeof(string)),
      new DataColumn("Pages_PageEvents_Value", typeof(int)),
      new DataColumn("Pages_Item__id", typeof(Guid)),
      new DataColumn("SiteName", typeof(string))
    });

    return dataTable;
  }

  private static void FillRawTable(IEnumerable<Interaction> goalsInteractions, DataTable rawTable)
  {
    foreach (var goalsInteraction in goalsInteractions)
    {
      foreach (var goal in goalsInteraction.Events.OfType<Goal>())
      {
        var currentEvent = goal;
        var row = rawTable.NewRow();
        row["_id"] = goalsInteraction.Id;
        row["ContactId"] = goalsInteraction.Contact.Id;
        row["Pages_PageEvents_PageEventDefinitionId"] = currentEvent.DefinitionId;
        row["Pages_PageEvents_DateTime"] = currentEvent.Timestamp;
        row["Pages_PageEvents_Data"] = currentEvent.Data;
        row["Pages_PageEvents_Value"] = currentEvent.EngagementValue;
        var dataRow = row;
        const string index = "SiteName";
        var webVisit = goalsInteraction.WebVisit();
        var str = webVisit?.SiteName ?? string.Empty;
        dataRow[index] = str;
        if (currentEvent.ParentEventId.HasValue)
        {
          if (goalsInteraction.Events.FirstOrDefault(p => p.Id == currentEvent.ParentEventId.Value) is PageViewEvent pageViewEvent)
          {
            row["Pages_Item__id"] = pageViewEvent.ItemId;
            var urlString = new UrlString(pageViewEvent.Url);
            row["Pages_Url_Path"] = urlString.Path;
            row["Pages_Url_QueryString"] = urlString.Query;
          }
        }

        rawTable.Rows.Add(row);
      }
    }
  }

  private static Contact GetContactByOptions(Guid contactId, ExpandOptions options = null)
  {
    using (var client = SitecoreXConnectClientConfiguration.GetClient())
    {
      if (options == null)
      {
        options = new ContactExpandOptions(Array.Empty<string>())
        {
          Interactions = new RelatedInteractionsExpandOptions("IpInfo", "WebVisit")
        };
      }

      var contactReference = new ContactReference(contactId);
      var contact = client.Get(contactReference, options);
      if (contact == null)
      {
        throw new ContactNotFoundException($"No Contact with id [{contactId}] found");
      }

      return contact;
    }
  }
}

One more change is the configuration. It is almost the same as in Sitecore 8, but we don't need to change the goals-query anymore - instead we have to patch the GetGoals in the same ExperienceProfileContactViews pipeline.
<sitecore>
  <pipelines>
    <group groupName="ExperienceProfileContactViews">
      <pipelines>
        <goals>
          <processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.ConstructGoalsDataTable, Sitecore.Cintel']"
             type="MyNamespace.AddGoalDataColumn, MyProject" />
          <processor patch:after="*[@type='Sitecore.Cintel.Reporting.Contact.Goal.Processors.PopulateGoalsWithXdbData, Sitecore.Cintel']"
             type="MyNamespace.FillGoalData, MyProject" />
          <processor patch:instead="*[@type='Sitecore.Cintel.Reporting.ReportingServerDatasource.Goals.GetGoals, Sitecore.Cintel']"
             type="MyNamespace.GetGoals, MyProject" />
        </goals>
      </pipelines>
    </group>
  </pipelines>
</sitecore>

Final step - core database

As Jonathan mentioned, we do need to add a new item in the core database to display our new column in the list of Goals. Create this item under the path /sitecore/client/Applications/ExperienceProfile/Contact/PageSettings/Tabs/Activity/Activity Subtabs/Goals/GoalsPanel/Goals and set a descriptive HeaderText and a DataField value that matches the schema name.

Your result should look like this:



I woud like to thank Jonathan Robbins for his original blog post and Jarmo Jarvi for his answer on SSE.

Sunday, February 14, 2016

Integrating AddThis with Sitecore goals

Social buttons


We faced the requirement to add social buttons to a Sitecore 8.1 site and track the interaction with a goal in xDB. Oh yes, and do it fast.

Social Connected

Our first thought was using the social buttons provided by Sitecore (in 'social connected'). But, we bumped into some issues (well, actually just not enough options) and had to write quite some button-code ourselves.

AddThis

So we started looking for alternatives and got back to a tool which we had used before and was known to our customer : AddThis. Problem was that it is not integrated within Sitecore and therefor did not create a goal. After discussing pros and cons we went for the flexibility of AddThis and started writing the code to trigger a goal.

Trigger a goal in a MVC controller

Some people have already blogged about not being able to get to the current Tracker from within WebApi ..  With SSC (Sitecore.Services.Client) it should be possible - as Mike Robbins has shown:


but as we did not have that information yet and we could not afford to take any risks, we went for the solution that seemed easy: create a mvc controller and a HttpPost action.

The controller action


[HttpPost]
public ActionResult TriggerGoal(string goal)
{
 if (!Tracker.IsActive)
 {
  return Json(new { Success = false, Error = "Tracker not active" });
 }

 if (string.IsNullOrEmpty(goal))
 {
  return Json(new { Success = false, Error = "Goal not set" });
 }

 var goalItem = ... // get goal item from Sitecore based on goal string
 if (goalItem == null)
 {
  return Json(new { Success = false, Error = "Goal not found" });
 }

 var visit = Tracker.Current;
 if (visit == null)
 {
  return Json(new { Success = false, Error = "Current tracker is null" });
 }

 var page = Tracker.Current.Session.Interaction.PreviousPage;
 if (page == null)
 {
  return Json(new { Success = false, Error = "Page is null" });
 }

 var registerTheGoal = new PageEventItem(goalItem);
 var eventData = page.Register(registerTheGoal);
 eventData.Data = goalItem["Description"];
 eventData.ItemId = goalItem.ID.Guid;
 eventData.DataKey = goalItem.Paths.Path;
 Tracker.Current.Interaction.AcceptModifications();

 Tracker.Current.CurrentPage.Cancel(); // Abandon current request so that it doesn't appear in reports

 return Json(new { Success = true });
}

We try to find the goal (Sitecore item) based on the post parameter. If found, we use that data to register a PageEvent to the PreviousPage of the current Tracker. We end by 'saving' the changes and cancelling the current request. The return values are an indication of what happened.

Remember:  Do not forget to register the route to your controller... ;)


The javascript

(function(global){

 var jQuery = global.jQuery;
 var addthisComp = {
  $instance: null,
  
  init: function(){
   addthisComp.$instance = jQuery('.addthis_sharing_toolbox');
   if(addthisComp.$instance.length){
    
    // Listen for the ready event
    if(global.addthis){
     if(global.addthis.addEventListener) 
          global.addthis.addEventListener('addthis.menu.share', addthisComp._share);
    }

   }
  },

  // Dispatched when the user shares
  _share: function(evt) {
      jQuery.ajax({
    type: "POST",
    url: '...', // url - route to your controller action
    data: {"goal": evt.data.service},
    success: function(obj){ 
    },
    dataType: 'json'
   });
  }
 };

 global.addthisComp = addthisComp;
   jQuery(document).ready(function(){
     global.addthisComp.init();
 });

})(window);

This is just attaching an ajax post request to out controller when the share functions of addthis are used. Of course, the post action could be attached to any event you want..