Introduction
In our previous post, we explored activity tracking data modeling principles and common pitfalls. If you missed it, catch up here. In this article, we'll dive into practical implementation in your Xperience by Kentico project. We'll guide you through setting up a custom activity type and show you how to log visitor activities effectively.
Setting Up the Example: Selling Property Search
Let's implement one of the custom activity types that was mentioned in the previous post - Selling property search. We aim to log this activity whenever a website visitor searches for a property for sale. If the visitor interacted with any filters, then some additional context information should be captured as well:
- property location: postcode area, city or district
- property type: house, flat, etc.
- price range
- number of bedrooms
- status: available or sold
Creating a custom activity type
First of all, we need to create our custom Activity type via Xperience by Kentico interface. This can be done in Digital Marketing > Contact Management > Activity Types section of Xperience by Kentico admin:
Remember the code name of the custom activity type - SellingPropertySearch - this will be used later in the code.
Logging activity
There are two main methods to log visitor activity in Xperience by Kentico - server-side and client-side.
The main difference is that server-side activity tracking happens as part of the request execution pipeline before the result is returned to the visitor, and client-side happens after the page is returned to the visitor.
Client-side logging
For most activity logging scenarios, we recommend employing client-side logging. This ensures that activities are logged only after the visitor has seen the rendered page, preventing false-positive tracking triggered by crawlers or bots. Additionally, certain user interface interactions, like button clicks, can only be logged on the client side.
Consider this code snippet to log our custom activity when a visitor lands on a search results page for properties on sale:
@using Kentico.Activities.Web.Mvc
@* Registers the script that allows custom activity logging *@
@Html.Kentico().ActivityLoggingAPI()
...
<script>
function trackSalesSearch() {
kxt('customactivity', {
type: 'SellingPropertySearch',
value: '', // Keep it blank for now, we will add some context later
title: 'Property search for sale'
});
}
// Add an event listener to track the activity after the page load
window.addEventListener('load', trackSalesSearch);
</script>
Server-side logging
In some cases, like dynamic Ajax requests, server-side tracking is more suitable. Imagine a visitor landing on a search results page, then applying various filters, and triggering a request to the backend, resulting in new properties matching the filter criteria appearing. Xperience by Kentico provides three options to accomplish this:
Manually insert ActivityInfo object
You can manually create and save an ActivityInfo object into the database, as shown below:
var activity = new ActivityInfo
{
ActivityCreated = DateTime.Now,
ActivityType = "SellingPropertySearch",
ActivitySiteID = siteId,
ActivityContactID = ContactManagementContext.CurrentContactID
};
activity.Insert();
However, this method has its drawbacks, such as always logging the activity irrespective of global settings and cookie consent, and not populating extra fields automatically.
Using standard ICustomActivityLogger
Another option is to utilize Kentico's implementation of the ICustomActivityLogger interface:
using CMS.Activities;
private readonly ICustomActivityLogger customActivityLogger;
...
var salesPropertySearchActivityData = new CustomActivityData() {
ActivityTitle = "Property search for sale",
ActivityValue = ""
};
customActivityLogger.Log("SellingPropertySearch", salesPropertySearchActivityData);
This method respects cookie consent and populates extra activity fields but can become unwieldy if you have numerous custom activity types with unique logic.
Implementing CustomActivityInitializerBase
For more streamlined activity logging, especially when dealing with multiple custom activity types, it's recommended to inherit activity type implementations from the CustomActivityInitializerBase base class:
public class SellingPropertySearchActivityInitializer : CustomActivityInitializerBase
{
private readonly string activityValue;
private readonly int activityItemId;
public SellingPropertySearchActivityInitializer(string activityValue = "", int activityItemId = 0)
{
this.activityValue = activityValue;
this.activityItemId = activityItemId;
}
public override void Initialize(IActivityInfo activity)
{
activity.ActivityTitle = "Property search for sale";
activity.ActivityValue = activityValue;
activity.ActivityItemID = activityItemId;
}
public override string ActivityType => "SellingPropertySearch";
The actual logging part of the code will then look like this:
var service = Service.Resolve<IActivityLogService>(); // or retrieve it from DI container
var activityInitializer = new SellingPropertySearchActivityInitializer("value");
service.Log(activityInitializer);
Beware server-side caching
One critical consideration to keep in mind is server-side caching. If the activity logging code is part of a widget or component with output caching enabled, the activity will only be logged the first time the page is opened. Subsequent requests for the same page will return the cached widget, bypassing the tracking code.
Attaching context data
We've successfully logged a simple property search activity, but now it's time to enhance it with the context data mentioned at the beginning of this article. Specifically, we want to include location, price range, and other relevant details.
For each activity, we have four spare fields to attach context:
- ActivityItemID - integer identifier of whatever "primary" object from DXP database you would like to relate to this activity: location or office is a good example
- ActivityItemDetailID - yet another integer identifier to relate something "secondary", e.g. the property itself
- ActivityValue - free text field where we can put any additional information
- ActivityComment - same as previous, another free text field
Creating a custom class
Initially, you might consider creating a separate container to store this context information, such as a Module Custom Class. Then, you can save an integer reference to this object in the ActivityItemID or ActivityItemDetailID fields.
Start by creating a custom class from Development > Modules > Classes interface:
Add the following fields:
Generate a strongly-typed C# class for your module custom class following the documentation's instructions.
Now, we can modify the activity logging code as follows:
var propertySearchAttributes = new PropertySearchAttributesInfo()
{
PropertyType = "Flat",
Location = "London",
PriceFrom = 350000,
PriceTo = 500000,
BedroomsFrom = 2,
BedroomsTo = 3,
Status = "Available"
};
propertySearchAttributes.Insert();
var service = Service.Resolve<IActivityLogService>();
var activityInitializer = new SellingPropertySearchActivityInitializer("", propertySearchAttributes.PropertySearchAttributesID);
service.Log(activityInitializer);
We can query the joined result of our logging via SQL:
SELECT *
FROM OM_Activity a
INNER JOIN Custom_PropertySearchAttributes psa
on a.ActivityItemID = psa.PropertySearchAttributesID
WHERE a.ActivityType = 'SellingPropertySearch'
From the technical perspective, it looks great because the data connected to the activity is decoupled from the activity itself. It can be processed, amended, or queried separately.
The reality though sounds much more pragmatic: this will never be the case. When marketers require this activity info, they always need the whole thing with all the context information available. For example, based on activities they may want to create a dynamic contact group with those who were searching for properties for sale in London to send out some communication for these people when there are new interesting properties available.
However, evaluating a connected object from the Custom_PropertySearchAttributes table to check the Location field would necessitate an additional SQL query, potentially impacting performance.
In the current version of Xperience by Kentico this method is also unavailable and we can only inspect data stored within ActivityValue field:
Structured XML
The alternative solution is to store serialized XML or JSON in ActivityValue field.
We only need a couple of things to make this work. First is a POCO-model:
[Serializable]
public class SalesSearch
{
public string PropertyType { get; set; }
public string Location { get; set; }
public int PriceFrom { get; set; }
public int PriceTo { get; set; }
public int BedroomsFrom { get; set; }
public int BedroomsTo { get; set; }
public string Status { get; set; }
}
Then a generic method to serialize this object into XML string, like this:
public static string SerializeToXml<T>(T obj)
{
var settings = new XmlWriterSettings
{
OmitXmlDeclaration = true,
Indent = false
};
using (StringWriter stringWriter = new StringWriter())
using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter, settings))
{
var xs = new XmlSerializerNamespaces();
xs.Add("", "");
var serializer = new XmlSerializer(obj.GetType());
serializer.Serialize(xmlWriter, obj, xs);
return stringWriter.ToString();
}
}
And finally our tracking code would be:
var propertySearchAttributes = new SalesSearch
{
PropertyType = "Flat",
Location = "London",
PriceFrom = 350000,
PriceTo = 500000,
BedroomsFrom = 2,
BedroomsTo = 3,
Status = "Available"
};
var service = Service.Resolve<IActivityLogService>();
var activityInitializer = new SellingPropertySearchActivityInitializer(SerializeToXml(propertySearchAttributes));
service.Log(activityInitializer);
As a result, this would be the XML in ActivityValue column in OM_Activity table:
<SalesSearch>
<PropertyType>Flat</PropertyType>
<Location>London</Location>
<PriceFrom>350000</PriceFrom>
<PriceTo>500000</PriceTo>
<BedroomsFrom>2</BedroomsFrom>
<BedroomsTo>3</BedroomsTo>
<Status>Available</Status>
</SalesSearch>
This approach will allow our marketers to accomplish their task of setting up a dynamic contact group with buying prospects from London:
Similar effect can be achieved with client-side tracking as well. We just need to withdraw this XML into the view with our tracking code:
@using Kentico.Activities.Web.Mvc
@* Registers the script that allows custom activity logging *@
@Html.Kentico().ActivityLoggingAPI()
@{
var propertySearchAttributes = new SalesSearch
{
PropertyType = "Flat",
Location = "London",
PriceFrom = 350000,
PriceTo = 500000,
BedroomsFrom = 2,
BedroomsTo = 3,
Status = "Available"
};
}
...
<script>
function trackSalesSearch() {
kxt('customactivity', {
type: 'SellingPropertySearch',
value: '@SerializeToXml(propertySearchAttributes)',
title: 'Property search for sale'
});
}
// Add an event listener to track the activity after the page load
window.addEventListener('load', trackSalesSearch);
</script>
Conclusion
In this article, we've explored the practical implementation of activity tracking in Xperience by Kentico, focusing on the "Selling Property Search" custom activity type as an example.
For most cases, the recommended implementation is client-side tracking, ensuring accuracy after the page rendering. Finally, to enhance logged activities with context data, we suggested using structured XML or JSON in the ActivityValue field.
By following these techniques, you can effectively track and gather insights from visitor activities, enhancing your marketing strategies.
Top comments (1)
Good stuff, Dmitry! Love to see more articles about Xperience from you!