Analytics Processing on DataSource Items

Use Case and Background

I recently encountered a use case where I needed to trigger goals and page events based on a Datasource item.  Sitecore provides a convenient method for assigning goals, page events, and more to any Sitecore item via the Tracking field. You can find this field in the Advanced section of the Standard Fields.

2018-10-02_9-52-07

When Sitecore processes an item as a Page Item (meaning that the URL that maps to an item or more specifically the Context.Item) during a request, it runs a pipeline called  ProcessItem.

<processItem patch:source="Sitecore.Analytics.Tracking.config">
  <processor type="Sitecore.Analytics.Pipelines.ProcessItem.CollectParameters,Sitecore.Analytics"/>
  <processor type="Sitecore.Analytics.Pipelines.ProcessItem.TriggerCampaigns,Sitecore.Analytics"/>
  <processor type="Sitecore.Analytics.Pipelines.ProcessItem.RegisterPageEvents,Sitecore.Analytics"/>
  <processor type="Sitecore.Analytics.Pipelines.ProcessItem.ProcessProfiles, Sitecore.Analytics"/>
</processItem>

This is called on every request for the determined Context.Item.

Now, to accomplish what I want to do, which is trigger a goal when a component renders a specific data source; as a marketer, I see the Tracking field on that item. Nothing stops me from putting Tracking information on that item.

The Root Problem

Unfortunately, Sitecore doesn’t process tracking rules like that out of the box. The ProcessItem pipeline is only executed for the given Context.Item coming out of the StartTracking pipeline.  When Sitecore compiles the renderings and data sources for display, taking into account content testing, personalization, and customizations, the Tracking field is not evaluated.

The Discovery

I wasn’t satisfied with that as a solution. I wanted to know where exactly I needed to plug into, in order to process the tracking field.  Even more important, was the discovery that the ProcessItem pipeline could be executed on ANY given item.  If you take a look at the Sitecore.Analytics.Pipelines.StartTracking.ProcessItem processor, you’ll see exactly how to run the ProcessItem pipeline.

  public class ProcessItem : StartTrackingProcessor
  {
    public override void Process(StartTrackingArgs args)
    {
      Assert.ArgumentNotNull((object) args, "args");
      if (Tracker.Current == null || Tracker.Current.Session == null || Tracker.Current.Session.Interaction == null)
        return;
      Item obj = Context.Item;
      if (obj == null)
        return;
      ProcessItemPipeline.Run(new ProcessItemArgs(Tracker.Current.Session.Interaction, obj));
    }
  }

I can send ANY items into this pipeline.  So, if I could isolate which Datasource Items are actually being rendered, I can call this pipeline with that Datasource item.

But where do I find THAT? (aka going DEEP into Sitecore)

After many hours of staring at the ShowConfig.aspx on my Content Management server, I noticed the pipeline mvc.customizeRendering.  What jumped out at me was the Personalize processor.  But it wasn’t taking into account Content Testing, which whatever I was looking for, needed to account for both.

mvc.customizeRendering Pipeline on Content Management Server Role

<mvc.customizeRendering patch:source="Sitecore.MvcAnalytics.config">
  <processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.Response.CustomizeRendering.SkipIfDesigning, Sitecore.Mvc.ExperienceEditor" patch:source="Sitecore.MVC.ExperienceEditor.config"/>
  <processor type="Sitecore.Mvc.Analytics.Pipelines.Response.CustomizeRendering.Personalize, Sitecore.Mvc.Analytics"/>
</mvc.customizeRendering>

On a whim, I asked myself, “I wonder if the Content Delivery config is different”So I took a look at that.

mvc.customizeRendering Pipeline on Content Management Server Role

<mvc.customizeRendering patch:source="Sitecore.MvcAnalytics.config">
  <processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.Response.CustomizeRendering.SkipIfDesigning, Sitecore.Mvc.ExperienceEditor" patch:source="Sitecore.MVC.ExperienceEditor.config"/>
  <processor type="Sitecore.ContentTesting.Mvc.Pipelines.Response.CustomizeRendering.SelectVariation, Sitecore.ContentTesting.Mvc" patch:source="Sitecore.ContentTesting.Mvc.config"/>
  <processor type="Sitecore.Mvc.Analytics.Pipelines.Response.CustomizeRendering.Personalize, Sitecore.Mvc.Analytics"/>
</mvc.customizeRendering>

Woah! This is different! And includes the Content Testing! So what calls mvc.customizeRendering?

Turns out it’s the mvc.getRenderer pipeline thanks to dotPeek!

<mvc.getRenderer patch:source="Sitecore.Mvc.config">
  <processor type="Sitecore.Mvc.Analytics.Pipelines.Response.GetRenderer.CustomizeRendering, Sitecore.Mvc.Analytics" patch:source="Sitecore.MvcAnalytics.config"/>
  <processor type="Sitecore.Mvc.Pipelines.Response.GetRenderer.GetViewRenderer, Sitecore.Mvc"/>
  <processor type="Sitecore.Mvc.Pipelines.Response.GetRenderer.GetItemRenderer, Sitecore.Mvc"/>
  <processor type="Sitecore.Mvc.Pipelines.Response.GetRenderer.GetXsltRenderer, Sitecore.Mvc"/>
  <processor type="Sitecore.Mvc.Pipelines.Response.GetRenderer.GetControllerRenderer, Sitecore.Mvc"/>
  <processor type="Sitecore.Mvc.Pipelines.Response.GetRenderer.GetMethodRenderer, Sitecore.Mvc"/>
  <processor type="Sitecore.Mvc.Pipelines.Response.GetRenderer.GetUrlRenderer, Sitecore.Mvc"/>
  <processor type="Sitecore.Mvc.Pipelines.Response.GetRenderer.GetDefaultRenderer, Sitecore.Mvc"/>
</mvc.getRenderer>

So what calls mvc.getRenderer?

Turns out it’s pretty well buried.  Thanks to dotPeek (again), mvc.getRenderer pipeline is called in the Rendering.Renderer property.

get Render

Following the bouncing ball, this property is utilized in a number of different areas within Sitecore.  But one of the usages stuck out at me.  The ExecuteRenderer processor.

Usages

The ExecuteRenderer processor is part of the mvc.renderRendering pipeline.

<mvc.renderRendering patch:source="Sitecore.Mvc.config">
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.InitializeProfiling, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.StartStatisticRecording, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.ResolveArea, Sitecore.Mvc">
		<param type="Sitecore.Mvc.Pipelines.Response.RenderRendering.ChainedAreaResolveStrategy, Sitecore.Mvc" desc="areaResolver">
			<Resolvers hint="list">
				<resolver type="Sitecore.Mvc.Pipelines.Response.RenderRendering.RenderingDefinitionAreaResolveStrategy, Sitecore.Mvc"/>
				<resolver type="Sitecore.Mvc.Pipelines.Response.RenderRendering.RenderingParametersAreaResolveStrategy, Sitecore.Mvc"/>
				<resolver type="Sitecore.Mvc.Pipelines.Response.RenderRendering.RenderingLayoutAreaResolveStrategy, Sitecore.Mvc"/>
			</Resolvers>
		</param>
		<param type="Sitecore.Mvc.AreaNamespaceRegistry, Sitecore.Mvc" desc="areaNamespaceRegistry"/>
	</processor>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.SetCacheability, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.EnterRenderingContext, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.GenerateCacheKey, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.RenderFromCache, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.StartRecordingOutput, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.Response.RenderRendering.AddWrapper, Sitecore.Mvc.ExperienceEditor" resolve="true" patch:source="Sitecore.MVC.ExperienceEditor.config"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc">
		<param type="Sitecore.Mvc.Pipelines.Response.RenderRendering.HttpExceptionWrappingRendererErrorStrategy, Sitecore.Mvc" desc="rendererErrorHandler">
			<param type="Sitecore.Mvc.Pipelines.Response.RenderRendering.ChainedRendererErrorStrategy, Sitecore.Mvc" desc="rendererErrorHandler">
				<Handlers hint="list">
					<handler type="Sitecore.Mvc.Pipelines.Response.RenderRendering.PageModeRenderingErrorStrategy, Sitecore.Mvc"/>
				</Handlers>
			</param>
		</param>
	</processor>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.AddRecordedHtmlToCache, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.RecordStatistic, Sitecore.Mvc"/>
</mvc.renderRendering>

This seems promising! I performed some initial debugging by creating a test processor to see just what data was passing in and out of this pipeline.

The Simple Solution

Turns out, the mvc.renderRendering pipeline is exactly the pipeline I was looking for.

The simple solution is adding a custom processor at the end of this pipeline that looks like this.

using Sitecore.Analytics;
using Sitecore.Analytics.Pipelines.ProcessItem;
using Sitecore.Mvc.Pipelines.Response.RenderRendering;
using Sitecore.StringExtensions;

namespace SitecoreHacker.Foundation.Pipelines.RenderRendering
{
    public class ProcessRenderingAnalytics : RenderRenderingProcessor
    {

        public override void Process(RenderRenderingArgs args)
        {

            var renderingItem = args.Rendering.Item;
            var datasource = args.Rendering.DataSource;

            if (renderingItem == null)
                return;

            if (datasource.IsNullOrEmpty())
                return;

            if (Tracker.Current == null || Tracker.Current.Session == null ||
                Tracker.Current.Session.Interaction == null)
                return;

            ProcessItemPipeline.Run(new ProcessItemArgs(Tracker.Current.Session.Interaction, renderingItem));
        }
    }
}

What I found is that mvc.renderRendering is called for EVERY SINGLE rendering element on the layout presentation details. And the ExecuteRenderer determines what datasource/rendering is to be sent to the browser after all criteria are met (Content Testing, Personalization, etc.)

By the time it gets to my processor, the args.Rendering.Item is the Sitecore Item of the Datasource being rendered. Additionally, if it IS a data source, it puts the ID of that Item into the args.Rendering.DataSource property.

What this means is that even the Page Item is sent through this pipeline. But the DataSource argument property will be empty.

I then check for nulls and make sure Tracker is working properly.

Last but not least I execute the ProcessItem pipeline.

mindblown

I patch it in like this:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:eds="http://www.sitecore.net/xmlconfig/eds/">
  <sitecore role:require="Standalone or ContentDelivery">
	  <pipelines>
		  <mvc.renderRendering>
			  <processor type="SitecoreHacker.Berlin.Foundation.Pipelines.RenderRendering.ProcessRenderingAnalytics, SitecoreHacker.Berlin"
			             patch:after="processor[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.AddRecordedHtmlToCache, Sitecore.Mvc']" />
		  </mvc.renderRendering>
	  </pipelines>
  </sitecore>
</configuration>

I patch it in after the processor AddRecordedHtmlToCache so that I know all rendering processing has been completed.

Summary

So far in all of my testing, this is working without any issues. All of my testing was done in a Sitecore 9 environment, but I believe this might also apply to Sitecore 8.2 as well.

Additionally, this is only solved for MVC renderings as that is the standard practice now with Sitecore. I have not done discovery on how you might use this for WebForms or XmlRendering’s. I’m sure it’s similar in approach if you can find the right places to hook in.

Last but not least I leave with a parting cautionary note:

  • Be careful not to reuse Datasource Items that have Tracking set when you DON’T want that tracking to fire.
    • Instead, consider keeping a separate place where you keep DataSources that have Tracking information in them so that you know what you are adding.

I hope you found this blog post helpful in your journeys with Sitecore.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s