xDB: Tracking the Untrackable – Part 2

In part 1 I showed you how you can make untrackable MVC and Web API endpoints trackable. Let’s see what we now can do with it.

Attribution

A little bit of xDB glossary first. A visit is an interaction. A hit is a page. If we just start tracking our MVC and Web API routes and do nothing else then every AJAX request will be recorded as a page. Sitecore won’t have the item details, of course, but it will record the URL:

mongo pages

I would much rather my AJAX requests were tracked as page events on the previous page. Or rather the current page that the user is still on. The technique I am about to show you is nothing new. I found mentions of it as early as 2011 (the OMS days) here and later here. The recipe is simple:

// #1. Start tracking (if needed)
Tracker.StartTracking();

// #2. Attribute page event(s) to the previous page
Tracker.Current.Interaction.PreviousPage.Register(new PageEventData("name"));

// #3. Cancel current page tracking
Tracker.Current.CurrentPage.Cancel();

Attribute Tracking

We have attribute routing. Let’s do attribute tracking. Here’s how I would like my MVC and Web API actions to look like:

[Route("product/search", Name=RouteNames.Product.Search)]
[MvcPageEvent("Product Search")]
public JsonResult FindByQuery(string q, int limit)
{
    // ...
}
[HttpPost]
[ActionName("GetAll")]
[WebApiPageEvent("Products")]
public TableResult<ProductDto> GetAll([FromBody] TableSettings settings)
{
    // ...
}

It’s not enough to record that a user touched an AJAX endpoint. I would also like to record what exactly they did. In other words, I would like my page events to record all action arguments. The MvcPageEvent and WebApiPageEvent are both action filters – one for MVC and one for Web API. We are not on ASP.NET 5 yet but we can definitely do two simple wrappers over a common implementation:

public class MvcPageEventAttribute: System.Web.Mvc.ActionFilterAttribute
{
    private readonly PageEventAttribute _attribute;

    public MvcPageEventAttribute(string name)
    {
        _attribute = new PageEventAttribute(name);
    }
  
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        _attribute.RecordPageEvent(filterContext.ActionParameter);
    }
}
public class WebApiPageEventAttribute: System.Web.Http.Filters.ActionFilterAttribute
{
    private readonly PageEventAttribute _attribute;

    public WebApiPageEventAttribute(string name)
    {
        _attribute = new PageEventAttribute(name);
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        _attribute.RecordPageEvent(actionContext.ActionArguments);
    }
}

And here’s the implementation of the PageEventAttribtue. Please note that the code was simplified for the blog post and stripped out of logging and error handling logic:

public class PageEventAttribute
{
    public string Event { get; private set; }

    public PageEventAttribute(string name)
    {
        Event = name;
    }

    public virtual void RecordPageEvent(IDictionary<string, object> data)
    {
        try
        {
            if (!Tracker.IsActive)
            {
                Tracker.StartTracking();
            }

            var pageEvent = new Sitecore.Analytics.Data.PageEventData(Event)
            {
                DataKey = Event,
                Data = JsonConvert.SerializeObject(data)
            });

            Tracker.Current.Interaction.PreviousPage.Register(pageEvent);
        }
        finally
        {
            Tracker.Current.CurrentPage.Cancel();
        }
    }
}

Page Events

I like Sitecore. I like xDB. I like MVC. I like Web API. Page Events… let’s see.

First, you need to create items for each page event. Your Mongo might be schema-less but your Sitecore is not. You will probably like to refer to your page events by their names (like I did) even though you can pass in a GUID. You can segregate them into subfolders but Sitecore still reads them all recursively as one big list. This is very prone to name collisions in a multi-tenant setup so you will probably want to prefix your events with something special. I wish we could namespace page events per site in a more cleaner way. Or even record arbitrary page events.

Next, Page Events are stringly typed. I borrowed the term from this RubyConf talk. You will probably use Data or Text to store the details of what happened. These were strings back in OMS days. They are still strings in the modern xDB world. Why am I not happy? Well, we have Mongo now and an embedded document would be a lot better than this:

stringly typed

Page events have backing items so they need to be published. And first they need to be deployed – approved in the analytics workflow. An attempt to register a page event that is not there will result … in a page event about page event not found. Sitecore records certain things as page events – long running requests and exceptions, for example – and will happily record a page event about a page event.

The Good Parts

On the bright side, though, we can change a lot of what xDB records. We already attributed an event to a previous page. We can also:

  • Change the item details of the Page record which will help your analytics dashboard – no more [unknown page] for those dynamic pages
  • Change the URL path. I, for example, like those /product/{id} routes recorded under the same /product/details URL with a page event attached to it that has the {id}.

It’s as simple as:

var page = Tracker.Current.CurrentPage;

page.SetItemProperties(...);
page.SetUrl(...);
page.Register(...);
Pavel Veller

Add a Comment

Your email address will not be published. Required fields are marked *

Or request call back