Tuesday, February 12, 2013

Tag, You're it: Tracking Tag Management in Sitecore

As web marketers, a great deal of our strategy relies on hunting (we'll leave farming tactics, like social media, nurturing and personalization, for another day). Most of our hunting activities rely heavily on tracking. The wiki definition of tracking (in the hunting context) fits this quite well:
Tracking in hunting and ecology is the science and art of observing animal tracks and other signs, with the goal of gaining understanding of the landscape and the animal being tracked (quarry).
For web marketers, hunting requires an understanding of the web ecosystem and the behavior of our "quarry" within it, so tracking is very important to us. There has been an explosion in tracking technologies of late, most of which are based on injecting tracking tags into our pages. Our pages have more tags hanging off them than the discount rack at Macy's --- so much so that tracking tag management has become a hot service offering. Just Google tracking tag management and you'll find more tools than you can count.

These tools can be appealing. After all, we have so much tag content that we need to manage on our site.

Wait a minute ... "content" ... "manage" ... hmmm --- aren't we already working in a content management environment?

There's no doubt that we could build a superb tracking tag management system into Sitecore. When we do a deep dive into these tagging technologies, we find that the formation and injection of tags into our pages can become quite complicated, but Sitecore is the best system around for building complex content management architectures.


Simple tracking tag management in Sitecore


But let's start off simple. When you first get into tagging, you find that at its simplest, all you have to do is copy a tag snippet (typically some javascript) into your pages. You find  new tracking service you want to use, you paste their snippet into your pages, and you're off.

Simple as that sounds, our marketers can't just do that themselves. They need the developers to deploy the tags to the production servers, typically within layouts. But marketeers want things done now ... so why don't we simplify this process by creating support for simple tag injection?

The solution I'll describe here is pretty rudimentary. It assumes that you are tagging every page on your site with every tag you have. That's fine for a start; over time, we'll examine ways to create dynamic tags and associate tags with content more robustly.

We'll create templates to allow the tag markup to be managed as Sitecore content. Since we want the tags on every page, we'll use a pipeline processor to inject them into every page (rather than relying on layouts).

You can download the source and package for this solution from the Sitecore Marketplace.


The templates


First, a couple of templates. We will create other tag types over time, but for now we'll just create a template for "Html Tracking Tag" that just holds some static tag markup. We'll also create a folder template to hold all of our tag items. This just a template with no fields, but with Standard Values set with insert options for our tag template.


The HtmlTrackingTag template is simple; it just contains a memo field to hold the tag markup.


We'll create a TrackingTagFolder in our content, wherever we manage "globals".


I've added a couple of tag items, which contain simple tag markup copied from that service's site.



The code


First, we'll need a configuration file to register up our pipeline and manage our settings:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <settings>
            <setting name="TrackingTags.GlobalTagFolder" value="/sitecore/content/Global/TrackingTags" />
        </settings>
        <pipelines>
            <insertRenderings>
                <processor
                    type="Arke.SharedSource.TrackingTags.Pipelines.InsertRenderings.InsertTrackingTags, Arke.SharedSource.TrackingTags"
                    patch:after="processor[@type='Sitecore.Pipelines.InsertRenderings.Processors.AddRenderings, Sitecore.Kernel']"
                    />
            </insertRenderings>
        </pipelines>
    </sitecore>
</configuration>

And we'll create an associated settings class to make it simpler to access our settings (we only have one for now, but it's good practice).

namespace Arke.SharedSource.TrackingTags
{
 public class Settings
 {
  public const string DEFAULT_GLOBAL_TAG_FOLDER = "/sitecore/content/Global/TrackingTags";
  public static string GlobalTagFolder
  {
   get
   {
    return Sitecore.Configuration.Settings.GetSetting("TrackingTags.GlobalTagFolder", DEFAULT_GLOBAL_TAG_FOLDER);
   }
  }
 
 }
}



To render the tags to the page, we'll create a simple web control that grabs the markup from the tag definition item and spits it to the page.

namespace Arke.SharedSource.TrackingTags
{
 public class HtmlTrackingTag : Sitecore.Web.UI.WebControl
 {
  System.Web.UI.WebControls.Literal container;
 
  public string TagItem { getset; }
 
  protected override void OnInit(EventArgs e)
  {
   base.OnInit(e);
   container = new System.Web.UI.WebControls.Literal();
  }
 
  protected override void CreateChildControls()
  {
   Assert.IsNotNullOrEmpty(TagItem, "tag item");
   Item item = Sitecore.Context.Database.GetItem(TagItem);
   Assert.IsNotNull(item.Fields["Markup"], "Markup");
   string markup = item.Fields["Markup"].Value;
   container.Text = markup;
  }
 
  protected override void DoRender(HtmlTextWriter output)
  {
   EnsureChildControls();
   container.RenderControl(output);
  }
 
  protected override string GetCachingID() 
  { 
   return this.GetType().FullName; 
  }
 }
}

Finally, we'll use a pipeline processor to iterate over the global tags and inject a HtmlTrackingTag control into the page for each.

namespace Arke.SharedSource.TrackingTags.Pipelines.InsertRenderings
{
 public class InsertTrackingTags
 {
  public void Process(InsertRenderingsArgs args)
  {
   Assert.ArgumentNotNull(args, "args");
 
   if (Sitecore.Context.Site.Name == "shell")
   {
    return;
   }
 
   Item globalTagFolder = Sitecore.Context.Database.GetItem(Settings.GlobalTagFolder);
 
   Profiler.StartOperation("Adding Tracking Tags.");
 
   foreach (Item globalTagItem in globalTagFolder.Children)
   {
    Arke.SharedSource.TrackingTags.HtmlTrackingTag control = 
     new Arke.SharedSource.TrackingTags.HtmlTrackingTag();
    if (control != null)
    {
     control.TagItem = globalTagItem.ID.ToGuid().ToString();
     control.Cacheable = true;
     control.VaryByData = true;
     RenderingReference reference = new RenderingReference(control);
     reference.AddToFormIfUnused = true;
     args.Renderings.Add(reference);
     Tracer.Info(string.Concat("Added tracking tag '", globalTagItem.Name, "'"));
    }
   }
 
   Profiler.EndOperation();
  }
 }
}

Note from the config file that we're putting our processor before Sitecore's AddRenderings processor.This puts our renderings at the top of the body part of the page.

If you look at the page in debug mode, you can see the script tag renderings.




Shortcomings of this solution

Of course, this is a bit of a dangerous solution. We've enabled content managers to manually inject script into the page. Either by accident or intent, a content owner could wreak havoc on the site. The tag definitions in globals should have good security to limit access only to trusted power users.

As I said before, this is a pretty simple solution. The obvious next step is to create a place to store "ad hoc" tags, and a rendering parameter template for the HtmlTrackingTag control. This would allow content manager to insert specific tag into specific pages.

Beyond this, a much more robust solution would include templates for the well-known tags so they could be managed more easily (by pasting in specific parameters into specific fields, rather than the entire tag). Dynamic tags that modify their behavior depending on context, template or rendering parameters could be built as well.

This solution allows the easy things to be easy, and the hard things will just have to wait till later. At least now, when we identify a new tracking service we want to implement, its as simple as "tag, you're it".





The source code and package for this solution are available at the Sitecore Marketplace.