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

Cover Image for How to Run Old Versions of Solr in a Docker Container

How to Run Old Versions of Solr in a Docker Container

> Please don't make me install another version of Solr on my local...

Cover Image for Don't Ignore the HttpRequestValidationException

Don't Ignore the HttpRequestValidationException

> Doing so could be... potentially dangerous

Cover Image for Ideas For Docker up.ps1 Scripts

Ideas For Docker up.ps1 Scripts

> Because Docker can be brittle

Cover Image for JSS: Reducing Bloat in Multilist Field Serialization

JSS: Reducing Bloat in Multilist Field Serialization

> Because: performance, security, and error-avoidance

Cover Image for Troubleshooting 502 Responses in Azure App Services

Troubleshooting 502 Responses in Azure App Services

> App Services don't support all libraries

Cover Image for On Mentorship and Community Contributions

On Mentorship and Community Contributions

> Reflections and what I learned as an MVP mentor

Cover Image for Add TypeScript Type Checks to RouteData fields

Add TypeScript Type Checks to RouteData fields

> Inspired by error: Conversion of type may be a mistake because neither type sufficiently overlaps with the other.

Cover Image for Azure PaaS Cache Optimization

Azure PaaS Cache Optimization

> App Services benefit greatly from proper configuration

Cover Image for Super Fast Project Builds with Visual Studio Publish

Super Fast Project Builds with Visual Studio Publish

> For when solution builds take too long

Cover Image for Early Returns in React Components

Early Returns in React Components

> When and how should you return early in a React component?

Cover Image for Script: Boost SIF Certificate Expiry Days

Script: Boost SIF Certificate Expiry Days

> One simple script that definitely won't delete your system32 folder

Cover Image for Tips for Forms Implementations

Tips for Forms Implementations

> And other pro tips

Cover Image for NextJS: Short URL for Viewing Layout Service Response

NextJS: Short URL for Viewing Layout Service Response

> Because the default URL is 2long4me

Cover Image for Tips for Applying Cumulative Sitecore XM/XP Patches and Hotfixes

Tips for Applying Cumulative Sitecore XM/XP Patches and Hotfixes

> It's probably time to overhaul your processes

Cover Image for Content Editor Search Bar Not Working

Content Editor Search Bar Not Working

> Sometimes it works, sometimes not

Cover Image for Symposium 2022 Reflections

Symposium 2022 Reflections

> Sitecore is making big changes

Cover Image for On Sitecore Development

On Sitecore Development

> Broadly speaking

Cover Image for Hello World

Hello World

> Welcome to the show

Cover Image for JSS + TypeScript Sitecore Project Tips

JSS + TypeScript Sitecore Project Tips

> New tech, new challenges

Cover Image for Critical Security Bulletin SC2024-001-619349 Announced

Critical Security Bulletin SC2024-001-619349 Announced

> And other scintillating commentary

Cover Image for NextJS: Access has been blocked by CORS policy

NextJS: Access has been blocked by CORS policy

> CORS is almost as much of a nuisance as GDPR popups

Cover Image for Tips for New Sitecore Developers

Tips for New Sitecore Developers

> If I had more time, I would have written a shorter letter

Cover Image for SPE Script Performance & Troubleshooting

SPE Script Performance & Troubleshooting

> Script never ends or runs too slow? Get in here.

Cover Image for Security Series: App Service IP Restrictions

Security Series: App Service IP Restrictions

> How to manage IP rules "at scale" using the Azure CLI

Cover Image for Sitecore Symposium 2022

Sitecore Symposium 2022

> What I'm Watching 👀

Cover Image for NextJS: Unable to Verify the First Certificate

NextJS: Unable to Verify the First Certificate

> UNABLE_TO_VERIFY_LEAF_SIGNATURE

Cover Image for Year in Review: 2022

Year in Review: 2022

> Full steam ahead

Cover Image for On Sitecore Stack Exchange (SSE)

On Sitecore Stack Exchange (SSE)

> What I've learned, what I see, what I want to see

Cover Image for NextJS/JSS Edit Frames Before JSS v21.1.0

NextJS/JSS Edit Frames Before JSS v21.1.0

> It is possible. We have the technology.