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 "@odata.type": "#Sitecore.XConnect.Collection.Model.PageViewEvent",
_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 "Url": "/path-to-page",
_16 "SitecoreRenderingDevice": {
_16 "Id": "fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3",
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
.
I don't recommend this approach:
Whenever you override default behavior in Sitecore, there's always a risk of complications and unexpected consequences, so proceed with caution and test thoroughly.
Create a class that implements the ConvertToXConnectEventProcessorBase<PageData>
interface and override as needed:
ConvertPageDataToPageViewEvent.cs
_52using Sitecore.Analytics.Model;
_52using Sitecore.Framework.Conditions;
_52using SitecoreXConnect = Sitecore.XConnect;
_52using Sitecore.XConnect.Collection.Model;
_52using Sitecore.Analytics.XConnect.DataAccess.Pipelines.ConvertToXConnectEventPipeline;
_52namespace Client.Foundation.DataExchange.Pipelines.ConvertToXConnectEventPipeline
_52 /// This class is automatically called when a session / interaction ends.
_52 public class ConvertPageDataToPageViewEvent : ConvertToXConnectEventProcessorBase<PageData>
_52 protected override SitecoreXConnect.Event ConvertToEvent(PageData pageData)
_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 private PageViewEvent CreatePageViewEvent(PageData pageData)
_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 // Default setting of Url
_52 pageViewEvent.Url = pageData.Url.ToString();
_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 // 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 return pageViewEvent;
_52 private void FillPageViewEvent(PageViewEvent pageViewEvent, PageData pageData)
_52 if (pageData.SitecoreDevice == null)
_52 pageViewEvent.SitecoreRenderingDevice = new SitecoreXConnect.Collection.Model.SitecoreDeviceData(pageData.SitecoreDevice.Id, pageData.SitecoreDevice.Name);
_11<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
_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 </convertToXConnectEvent>
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.
Sitecore Says:
Don't rely on the Data, DataKey, Text, and CustomValues properties on the Event class to store custom data. These properties are only available for legacy purposes.
You may feel an intense urge to forego Sitecore's advice.
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
_81using Sitecore.Analytics;
_81using Sitecore.Analytics.Pipelines.ProcessItem;
_81using Sitecore.Diagnostics;
_81using Sitecore.Analytics.Data;
_81using System.Collections.Generic;
_81namespace Client.Foundation.DataExchange.Pipelines.ProcessItem
_81 public class RegisterCustomPageEvents : ProcessItemProcessor
_81 public override void Process(ProcessItemArgs args)
_81 Assert.ArgumentNotNull((object)args, nameof(args));
_81 foreach (TrackingField trackingParameter in (IEnumerable<TrackingField>)args.TrackingParameters)
_81 TrackingFieldProcessor.ProcessEvents(args.Interaction, trackingParameter);
_81 if (!Tracker.Enabled || Tracker.Current == null || !Tracker.Current.IsActive)
_81 RegisterCustomPageViewEvent(args);
_81 private void RegisterCustomPageViewEvent(ProcessItemArgs args)
_81 if (args.Item == null)
_81 // Log or throw exception
_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 // Log or throw exception
_81 var pageData = new PageEventData(ev.Alias, ev.Id);
_81 pageData.Text = "Custom Page View";
_81 var fullUrl = System.Web.HttpContext.Current.Request.Url.AbsoluteUri;
_81 pageData.DataKey = "Url";
_81 pageData.Data = fullUrl;
_81 pageData.ItemId = args.Item.ID.ToGuid();
_81 Assert.IsNotNull(Tracker.Current, "Tracker.Current");
_81 Assert.IsNotNull(Tracker.Current.Session, "Tracker.Current.Session");
_81 var interaction = Tracker.Current.Session.Interaction;
_81 Assert.IsNotNull(interaction, "Tracker.Current.Session.Interaction");
_81 Assert.IsNotNull(interaction.CurrentPage, "Tracker.Current.Session.Interaction.CurrentPage");
_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 Tracker.Current.CurrentPage.Register(pageData);
_81 Log.Error($"Unable to fire custom page event: {ex.Message}", ex, this);
_9<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
_9 <processor type="Client.Foundation.DataExchange.Pipelines.ProcessItem.RegisterCustomPageEvents, Client.Foundation.DataExchange" />
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
_45using Sitecore.Analytics.Model;
_45using Sitecore.Framework.Conditions;
_45using Sitecore.XConnect;
_45using Sitecore.Analytics.XConnect.DataAccess.Pipelines.ConvertToXConnectEventPipeline;
_45namespace Client.Foundation.DataExchange.Pipelines.ConvertToXConnectEventPipeline
_45 // https://doc.sitecore.com/xp/en/developers/93/sitecore-experience-platform/convert-a-page-event.html
_45 public class ConvertPageEventDataToCustomPageViewEvent : ConvertPageEventDataToEventBase
_45 protected override bool CanProcessPageEventData(PageEventData pageEventData)
_45 Condition.Requires(pageEventData, nameof(pageEventData)).IsNotNull();
_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 /// Create new custom event from pageEventData. All base Event properties are mapped automatically.
_45 /// <param name="pageEventData"></param>
_45 /// <returns></returns>
_45 protected override Event CreateEvent(PageEventData pageEventData)
_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 // 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);
_8<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
_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"/>
For reference, here is what your custom event type might look like:
_27using Sitecore.XConnect;
_27namespace Client.Foundation.DataExchange.Models
_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 [FacetKey(DefaultFacetKey)]
_27 public class MyCustomPageViewEvent: Event
_27 public const string DefaultFacetKey = "MyCustomPageViewEvent";
_27 public string FullUrl { get; set; }
_27 public MyCustomPageViewEvent(DateTime timestamp, Guid itemId, string dataKey, string data) : base(EventDefinitionId, timestamp)
_27 public static Guid EventDefinitionId { get; } = new Guid("7DDE228D-A30A-4670-A885-4EFB4054B561");
For reference, here's a the default PageViewEvent
that ships with Sitecore:
_25namespace Sitecore.XConnect.Collection.Model
_25 public class PageViewEvent : Event
_25 public PageViewEvent(DateTime timestamp, Guid itemId, int itemVersion, string itemLanguage)
_25 : base(PageViewEvent.EventDefinitionId, timestamp)
_25 this.ItemId = itemId;
_25 this.ItemVersion = itemVersion;
_25 this.ItemLanguage = itemLanguage;
_25 public static Guid EventDefinitionId { get; } = new Guid("9326CB1E-CEC8-48F2-9A3E-91C7DBB2166C");
_25 public string ItemLanguage { get; set; }
_25 public int ItemVersion { get; set; }
_25 public string Url { get; set; }
_25 public SitecoreDeviceData SitecoreRenderingDevice { get; set; }
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