xConnect: Storing Full URLs in Page View Events

Cover Image for xConnect: Storing Full URLs in Page View Events

Overview

Data lakes, warehouses, swamps, and graveyards are all the rage these days. Imagine a scenario in which you want to send xConnect page view events to an external data store (perhaps via DEF?).

In doing so, we want to ensure that there's enough detail in the event information to be a self contained unit of value within the external data store. Let's take a look at a typical Page View Event in xConnect:


_16
{
_16
"@odata.type": "#Sitecore.XConnect.Collection.Model.PageViewEvent",
_16
"CustomValues": [],
_16
"DefinitionId": "9326cb1e-cec8-48f2-9a3e-91c7dbb2166c",
_16
"ItemId": "163dcf8c-5191-4e77-90a1-babf352e7b1f",
_16
"Id": "3a2b4f6c-afc4-410b-9a6f-076636633d4f",
_16
"Timestamp": "2022-09-30T16:12:56.7227459Z",
_16
"Duration": "PT9.969S",
_16
"ItemLanguage": "en-US",
_16
"ItemVersion": 1,
_16
"Url": "/path-to-page",
_16
"SitecoreRenderingDevice": {
_16
"Id": "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3",
_16
"Name": "Default"
_16
}
_16
}

Looks like we're missing a full URL. How is an external system supposed to know that /path-to-page corresponds to https://www.yoursite.com/path-to-page? What if you have multiple websites on your Sitecore instance and you need to be able to distinguish page views between them? One way would be to add that information before you send it to the external system, or you could add the full URL in real time. Two potential approaches for adding the full URL in real time are explained below.

1. Override the Default Page View Event Creation Logic

This approach overrides the ConvertPageDataToPageViewEvent class to include the full URL instead of just the path. Once in place, this affects all future PageViewEvents.

Create a class that implements the ConvertToXConnectEventProcessorBase<PageData> interface and override as needed:

ConvertPageDataToPageViewEvent.cs

_52
using Sitecore.Analytics.Model;
_52
using Sitecore.Framework.Conditions;
_52
using SitecoreXConnect = Sitecore.XConnect;
_52
using Sitecore.XConnect.Collection.Model;
_52
using System;
_52
using Sitecore.Analytics.XConnect.DataAccess.Pipelines.ConvertToXConnectEventPipeline;
_52
using Sitecore.Web;
_52
_52
namespace Client.Foundation.DataExchange.Pipelines.ConvertToXConnectEventPipeline
_52
{
_52
/// <summary>
_52
/// This class is automatically called when a session / interaction ends.
_52
/// </summary>
_52
public class ConvertPageDataToPageViewEvent : ConvertToXConnectEventProcessorBase<PageData>
_52
{
_52
protected override SitecoreXConnect.Event ConvertToEvent(PageData pageData)
_52
{
_52
Condition.Requires<DateTime>(pageData.DateTime, nameof(pageData)).IsUtc("{0}.DateTime should be of kind UTC");
_52
PageViewEvent pageViewEvent = CreatePageViewEvent(pageData);
_52
FillPageViewEvent(pageViewEvent, pageData);
_52
return (SitecoreXConnect.Event)pageViewEvent;
_52
}
_52
_52
private PageViewEvent CreatePageViewEvent(PageData pageData)
_52
{
_52
PageViewEvent pageViewEvent = new PageViewEvent(pageData.DateTime, pageData.Item.Id, pageData.Item.Version, pageData.Item.Language);
_52
pageViewEvent.Duration = TimeSpan.FromMilliseconds((double)pageData.Duration);
_52
_52
// Default setting of Url
_52
pageViewEvent.Url = pageData.Url.ToString();
_52
_52
// Do what you need here
_52
// Note that there is no Sitecore context in this pipeline
_52
// The Sitecore.Web.WebUtil class is your friend
_52
pageViewEvent.Url = System.Web.HttpContext.Current.Request.Url.AbsoluteUri;
_52
_52
// If you need to store arbitrary data, create a custom event instead (see the next section of this blog post)
_52
// In other words, don't do this:
_52
// pageViewEvent.CustomValues.Add("FullUrl", "Example Value");
_52
_52
return pageViewEvent;
_52
}
_52
_52
private void FillPageViewEvent(PageViewEvent pageViewEvent, PageData pageData)
_52
{
_52
if (pageData.SitecoreDevice == null)
_52
return;
_52
_52
pageViewEvent.SitecoreRenderingDevice = new SitecoreXConnect.Collection.Model.SitecoreDeviceData(pageData.SitecoreDevice.Id, pageData.SitecoreDevice.Name);
_52
}
_52
}
_52
}

Patch.config

_11
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
_11
<sitecore>
_11
<pipelines>
_11
<convertToXConnectEvent>
_11
<processor type="Sitecore.Analytics.XConnect.DataAccess.Pipelines.ConvertToXConnectEventPipeline.ConvertPageDataToPageViewEvent, Sitecore.Analytics.XConnect">
_11
<patch:attribute name="type" value="Client.Foundation.DataExchange.Pipelines.ConvertToXConnectEventPipeline.ConvertPageDataToPageViewEvent, Client.Foundation.DataExchange" />
_11
</processor>
_11
</convertToXConnectEvent>
_11
</pipelines>
_11
</sitecore>
_11
</configuration>

2. Create a Custom Event That Stores the Full URL

Before we continue, I recommend reading The Friendly Manual™️ on Sitecore events (yes, MORE reading!) because there are many ways you can veer away from best practices at this point; particularly around how you go about storing your custom data.

You may feel an intense urge to forego Sitecore's advice.

Why Shouldn't I

After all, those legacy fields actually work just fine for storing data... But don't do it. It will likely complicate future upgrades and migrations. Instead, let's extend Sitecore like we're supposed to by creating a custom page view event.

Create a class that inherits ProcessItemProcessor. This will register the event data while the user is in session:

RegisterCustomPageEvents.cs

_81
using Sitecore.Analytics;
_81
using System;
_81
using Sitecore.Analytics.Pipelines.ProcessItem;
_81
using Sitecore.Diagnostics;
_81
using Sitecore.Analytics.Data;
_81
using System.Collections.Generic;
_81
using System.Linq;
_81
_81
namespace Client.Foundation.DataExchange.Pipelines.ProcessItem
_81
{
_81
public class RegisterCustomPageEvents : ProcessItemProcessor
_81
{
_81
public override void Process(ProcessItemArgs args)
_81
{
_81
Assert.ArgumentNotNull((object)args, nameof(args));
_81
_81
foreach (TrackingField trackingParameter in (IEnumerable<TrackingField>)args.TrackingParameters)
_81
{
_81
TrackingFieldProcessor.ProcessEvents(args.Interaction, trackingParameter);
_81
}
_81
_81
if (!Tracker.Enabled || Tracker.Current == null || !Tracker.Current.IsActive)
_81
{
_81
return;
_81
}
_81
_81
RegisterCustomPageViewEvent(args);
_81
}
_81
_81
private void RegisterCustomPageViewEvent(ProcessItemArgs args)
_81
{
_81
if (args.Item == null)
_81
{
_81
// Log or throw exception
_81
return;
_81
}
_81
_81
// Create a corresponding item here: /sitecore/system/Settings/Analytics/Page Events/
_81
var customPageEventId = Guid.Parse("{7DDE228D-A30A-4670-A885-4EFB4054B561}");
_81
var ev = Tracker.MarketingDefinitions.PageEvents[customPageEventId];
_81
_81
if (ev == null)
_81
{
_81
// Log or throw exception
_81
return;
_81
}
_81
_81
var pageData = new PageEventData(ev.Alias, ev.Id);
_81
_81
pageData.Text = "Custom Page View";
_81
_81
var fullUrl = System.Web.HttpContext.Current.Request.Url.AbsoluteUri;
_81
_81
pageData.DataKey = "Url";
_81
pageData.Data = fullUrl;
_81
pageData.ItemId = args.Item.ID.ToGuid();
_81
_81
try
_81
{
_81
Assert.IsNotNull(Tracker.Current, "Tracker.Current");
_81
Assert.IsNotNull(Tracker.Current.Session, "Tracker.Current.Session");
_81
_81
var interaction = Tracker.Current.Session.Interaction;
_81
_81
Assert.IsNotNull(interaction, "Tracker.Current.Session.Interaction");
_81
Assert.IsNotNull(interaction.CurrentPage, "Tracker.Current.Session.Interaction.CurrentPage");
_81
_81
// Prevent duplicates since this specific event should only fire once per page
_81
var doesEventExist = interaction.CurrentPage.PageEvents.Any(pe => pe.PageEventDefinitionId == customPageEventId);
_81
if (!doesEventExist)
_81
{
_81
Tracker.Current.CurrentPage.Register(pageData);
_81
}
_81
}
_81
catch (Exception ex)
_81
{
_81
Log.Error($"Unable to fire custom page event: {ex.Message}", ex, this);
_81
}
_81
}
_81
}
_81
}

Patch.config

_9
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
_9
<sitecore>
_9
<pipelines>
_9
<processItem>
_9
<processor type="Client.Foundation.DataExchange.Pipelines.ProcessItem.RegisterCustomPageEvents, Client.Foundation.DataExchange" />
_9
</processItem>
_9
</pipelines>
_9
</sitecore>
_9
</configuration>

We've registered the event, but it won't actually get written to the database until we create a custom event converter which runs when the session ends.

Add a class that inherits ConvertPageEventDataToEventBase:

ConvertPageEventDataToCustomPageViewEvent.cs

_45
using Sitecore.Analytics.Model;
_45
using Sitecore.Framework.Conditions;
_45
using Sitecore.XConnect;
_45
using System;
_45
using Sitecore.Analytics.XConnect.DataAccess.Pipelines.ConvertToXConnectEventPipeline;
_45
_45
namespace Client.Foundation.DataExchange.Pipelines.ConvertToXConnectEventPipeline
_45
{
_45
// https://doc.sitecore.com/xp/en/developers/93/sitecore-experience-platform/convert-a-page-event.html
_45
public class ConvertPageEventDataToCustomPageViewEvent : ConvertPageEventDataToEventBase
_45
{
_45
protected override bool CanProcessPageEventData(PageEventData pageEventData)
_45
{
_45
Condition.Requires(pageEventData, nameof(pageEventData)).IsNotNull();
_45
_45
// Make sure the item you reference here is published and the marketing definitions are deployed
_45
if (pageEventData.PageEventDefinitionId == Guid.Parse("{7DDE228D-A30A-4670-A885-4EFB4054B561}"))
_45
{
_45
return true;
_45
}
_45
_45
return false;
_45
}
_45
_45
/// <summary>
_45
/// Create new custom event from pageEventData. All base Event properties are mapped automatically.
_45
/// </summary>
_45
/// <param name="pageEventData"></param>
_45
/// <returns></returns>
_45
protected override Event CreateEvent(PageEventData pageEventData)
_45
{
_45
// Creating a generic Event will work, but remember what Sitecore said...
_45
// Event customPageViewEvent = new Event(new Guid("7DDE228D-A30A-4670-A885-4EFB4054B561"), pageEventData.DateTime);
_45
// customPageViewEvent.ItemId = pageEventData.ItemId;
_45
// customPageViewEvent.DataKey = pageEventData.DataKey;
_45
// customPageViewEvent.Data = pageEventData.Data;
_45
_45
// Custom model must be generated and deployed, else Sitecore will throw errors
_45
// https://doc.sitecore.com/xp/en/developers/93/sitecore-experience-platform/deploy-a-custom-model.html
_45
MyCustomPageViewEvent ev = new MyCustomPageViewEvent(pageEventData.DateTime, pageEventData.ItemId, pageEventData.DataKey, pageEventData.Data);
_45
_45
return ev;
_45
}
_45
}
_45
}

Patch.config

_8
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
_8
<sitecore>
_8
<pipelines>
_8
<convertToXConnectEvent>
_8
<processor patch:after="processor[@type='Sitecore.Analytics.XConnect.DataAccess.Pipelines.ConvertToXConnectEventPipeline.ConvertPageEventDataToGoal, Sitecore.Analytics.XConnect']" type="Client.Foundation.DataExchange.Pipelines.ConvertToXConnectEventPipeline.ConvertPageEventDataToCustomPageViewEvent, Client.Foundation.DataExchange"/>
_8
</pipelines>
_8
</sitecore>
_8
</configuration>

For reference, here is what your custom event type might look like:

MyCustomPageViewEvent.cs

_27
using System;
_27
using Sitecore.XConnect;
_27
_27
namespace Client.Foundation.DataExchange.Models
_27
{
_27
/// <summary>
_27
/// This event type is intended to be used as a data store which will ultimately be sent to and utilized by D365.
_27
/// This custom event type is required in order to store different data than what is stored by default.
_27
/// </summary>
_27
[Serializable]
_27
[FacetKey(DefaultFacetKey)]
_27
public class MyCustomPageViewEvent: Event
_27
{
_27
public const string DefaultFacetKey = "MyCustomPageViewEvent";
_27
public string FullUrl { get; set; }
_27
_27
public MyCustomPageViewEvent(DateTime timestamp, Guid itemId, string dataKey, string data) : base(EventDefinitionId, timestamp)
_27
{
_27
ItemId = itemId;
_27
DataKey = dataKey;
_27
Data = data;
_27
FullUrl = data;
_27
}
_27
_27
public static Guid EventDefinitionId { get; } = new Guid("7DDE228D-A30A-4670-A885-4EFB4054B561");
_27
}
_27
}

For reference, here's a the default PageViewEvent that ships with Sitecore:


_25
using System;
_25
_25
namespace Sitecore.XConnect.Collection.Model
_25
{
_25
public class PageViewEvent : Event
_25
{
_25
public PageViewEvent(DateTime timestamp, Guid itemId, int itemVersion, string itemLanguage)
_25
: base(PageViewEvent.EventDefinitionId, timestamp)
_25
{
_25
this.ItemId = itemId;
_25
this.ItemVersion = itemVersion;
_25
this.ItemLanguage = itemLanguage;
_25
}
_25
_25
public static Guid EventDefinitionId { get; } = new Guid("9326CB1E-CEC8-48F2-9A3E-91C7DBB2166C");
_25
_25
public string ItemLanguage { get; set; }
_25
_25
public int ItemVersion { get; set; }
_25
_25
public string Url { get; set; }
_25
_25
public SitecoreDeviceData SitecoreRenderingDevice { get; set; }
_25
}
_25
}

In a future post, I'll be covering how you can selectively sync your custom events with DEF/D365 while filtering for specific event types / definition IDs.

More Reading

Keep on BUIDLing,

Marcel


More Stories