DEV Community

Dmitry Bastron for ByteMinds

Posted on • Updated on • Originally published at byteminds.co.uk

Activity logging with Xperience by Kentico

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:

Image description

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>
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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:

Image description

Add the following fields:

Image description

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);
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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; }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

This approach will allow our marketers to accomplish their task of setting up a dynamic contact group with buying prospects from London:

Image description

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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
andreystepanov profile image
Andrey Stepanov

Good stuff, Dmitry! Love to see more articles about Xperience from you!