Back Arrow
From the blog

Activity logging with Xperience by Kentico

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.

Dmitry Bastron

Solution Architect / Kentico MVP

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.

It's easy to start working with us. Just fill the brief or call us.

Find out more
White Arrow
From the blog
Related articles

Mastering advanced tracking with Kentico Xperience

Dmitry Bastron

We will take you on a journey through a real-life scenario of implementing advanced tracking and analytics using Kentico Xperience 13 DXP.

Kentico
Devtools

Sitecore integration with Azure Active Directory B2C

Dmitry Bastron

We would like to share our experience of integrating Sitecore 9.3 with the Azure AD B2C (Azure Active Directory Business to Consumer) user management system.

Sitecore
Azure

Why is Kentico of such significance to us?

Anastasia Medvedeva

Kentico stands as one of our principal development tools, we believe it would be fitting to address why we opt to work with Kentico and why we allocate substantial time to cultivating our experts in this DXP.

Kentico

Interesting features of devtools for QA

Chrome DevTools serves as a developer console, offering an array of in-browser tools for constructing and debugging websites and applications.

Devtools
QA

Where to start learning Sitecore - An interview with Sitecore MVP Anna Gevel

Anna Gevel

As a software development company, we at Byteminds truly believe that learning and sharing knowledge is one of the best ways of growing technical expertise.

Sitecore

Sitecore replatforming and upgrades

Anastasia Medvedeva

Our expertise spans full-scale builds and support to upgrades and replatforming.

Sitecore

Kentico replatforming and upgrades

Anastasia Medvedeva

Since 2015, we've been harnessing Kentico's capabilities well beyond its core CMS functions.

Kentico

Umbraco replatforming and upgrades

Anastasia Medvedeva

Our team boasts several developers experienced in working with Umbraco, specialising in development, upgrading, and replatforming from other CMS to Umbraco.

Umbraco

How we improved page load speed for Next.js ecommerce website by 50%

Sergei Pestov

How to stop declining of the performance indicators of your ecommerce website and perform optimising page load performance.

Next.js

Sitecore Personalize: tips & tricks for decision models and programmable nodes

Anna Gevel

We've collected various findings around decision models and programmable nodes working with Sitecore Personalize.

Sitecore

Fixed Price, Time & Materials, and Retainer: How to Choose the Right Agreement for Your Project with Us

We will explain how these agreements differ from one another and what projects they are suitable for.

Customer success

Enterprise projects: what does a developer need to know?

Let's talk about what enterprise development is, what nuance enterprise projects may have, and which skills you need to acquire to successfully work within the .NET stack.

Development

Headless CMS. Identifying Ideal Use Cases and Speeding Up Time-to-Market

Andrey Stepanov

All you need to know about Headless CMS. We also share the knowledge about benefits of Headless CMS, its pros and cons.

Headless CMS

Dynamic URL routing with Kontent.ai

We'll consider the top-to-bottom approach for modeling content relationships, as it is more user-friendly for content editors working in the Kontent.ai admin interface.

Kontent Ai
This website uses cookies. View Privacy Policy.