Skip to main content

MCP Serverside Code Context

Context is king. Also when coding MCP Campaign Templates. Read about all its undocumented tricks.

Marketing Cloud Personalization offers flexible campaign template creation tooling with multiple properties and imports that help you fulfil business needs while providing a pleasant user experience for the marketer. There is also one more—undocumented—feature that can change your template from good to outstanding: the CampaignComponentContext object.

You Should Know

In this article, I'm covering the main CampaignComponentContext object that is passed as a context argument to the run block in the Serverside Code of every campaign template.

Some other contexts (like GearLifecycleContext passed to search methods) have different structures and are out of the scope of this article.

The CampaignComponentContext object is available in the Serverside Code of the Web, Serverside and Triggered Campaign Templates and provides extensive details about triggering event, user and delivered experience.

Structure of the context object
{
"campaignId": string,
"experienceId": string,
"userGroup": string,
"beaconVersion": number,
"event": Object,
"contentZone": string,
"trigger": Object,
"locale": string,
"services": Object,
"user": Object,
"accountId": string,
"datasetId": string,
"configuration": Object
}

It's straightforward to use once you know what's in there. For example, you can pull product ID stored with Sitemap in a User Attribute and leverage it to pull complete data about that product for personalisation:

Leverage context in the run method of your Serverside Code
    run(context: CampaignComponentContext) {
const lastAddedToCartProductId = context.user.attributes?.lastAddedToCartProduct?.value;
const lastAddedToCartProductDetails = context.services.catalog.findItem('Product', lastAddedToCartProductId);
return { lastATCDetails: lastAddedToCartProductDetails };
}

And that's just a basic usage. The key to unlocking the power of a context object is knowing what is stored there and how to use it. So let's dive in, property by property (hint: the fun part starts at event).

You Should Know

This article is a work in progress. I'm continually extending details about various parts of the context object as I use it in real life.

campaignId & experienceId

The first two string properties of the context object are campaignId and experienceId, and their purpose is very straightforward. They provide the five-character, case-sensitive, alphanumerical IDs for the campaign and experience selected for a user (for example, vALdQ for Campaign ID and f3WpK for Experience ID).

Both those values are passed by default from serverside to clientside and handlebars (as campaign and experience accordingly), so there is not much added value in the two unless you want to append those values as query strings to the links for tracking purposes.

However, for Web Campaigns, you can do it easily within the handlebars tab, and for Serverside and Triggered Campaigns, you can do it on the receiving system side.

userGroup

The userGroup string property should tell you the group assigned to the user. Well, it should. In practice, you will see there one of the two values: Test for users that got an A/B Test or Rule-Based experience and testUserGroup for those that are in the Control group (or in the Template preview pane within MCP UI).

However, you will see better values in the out-of-the-box serverside payload userGroup property that correctly shows values like Test, Default, and Control and only displays testUserGroup during preview.

You Should Know

The control group creates more problems for the context, as it keeps displaying the payload preview version of it. So you will also see only the placeholder values for campaignId and experienceId, beaconVersion and skip other datapoints like event.fields.

In short, don't use context for custom payload dedicated to control group users.

beaconVersion

The beaconVersion number property will display the current Web SDK version (e.g., 16 at the time of writing) or 0 for the preview/control group. Not really useful.

event

The event object property is where the magic of the context object starts. It stores information about the event that triggered the campaign - the data you can see when you leverage the .setLoggingLevel('debug') method in your Sitemap.

You Should Know

The context.event object won't work correctly in the Template Preview sidebar, as there is no valid event in that mode. Use an active campaign directly on the website to preview the actual output during development. Make sure you use the campaign targeting to limit execution just to you.

In Template Preview, you will only be able to use context.event.fields.item - a single property with a stringified object containing details of the Item selected in the top right Simulate section):

"{\"type\":\"ITEM_TYPE\",\"_id\":{\"label\":\"ITEM_LABEL\",\"value\":\"ITEM_ID\"}}"

My MCP Catalog ETL Metadata Viewer provides an example of its usage.

Structure of the context.event object
{
"time": datetime,
"fields": Object,
"ipAddress": (): string,
"itemId": (): string,
"itemType": (): string
}

While the context.event.time is not that useful (unless you want to make some time-dependent changes to the campaign payload), the three available methods are much more practical.

event methods

context.event.ipAddress()

Returns an IP address of the user visiting your website. You might use it to blocklist specific IP ranges (internal or competitors) from seeing your campaigns. It's not a clean solution (as the campaign needs to be executed to get this far), but this might be your best bet, as there is no MCP-level IP blocklist. Of course, a much better solution would be to build such logic on the website side to block IPs already on the Web SDK import step conditionally, but that might not always be possible.

context.event.itemId()

Returns the ID of the Catalog Item the user viewed in the event. It pairs perfectly with the following method: context.event.itemType().

context.event.itemType()

Returns the Catalog of the Item viewed (f.e. 'Product' for Product View).

The pair of itemId and itemType is handy, as those two details are precisely what MCP requires for a context.services.catalog.findItem() call that lets you get complete details about the currently displayed item. It enables use cases where you want to change the campaign payload based on displayed item attributes, related catalog objects, or other information on the item details. For example, access to localized item data.

You Should Know

Event is not only an excellent data point that you can access through context. It is also the basis for the anchor in Einstein Recipes. You can leverage it (in a filthy way) to create fake anchors and deploy recommendations for products related to the one currently viewed.

On top of the above, context.event also contains the context.event.fields subobject, capturing even more details about the triggering event.

event.fields

Structure of the context.event.fields object
{
".anonId": string,
".bv": string,
".pv": boolean,
".scv": number,
".skipProcessing": boolean,
"action": string,
"channel": string,
"clientIp": string,
"contentZones": string[],
"pageType": string,
"url": string,
"urlref": string,
"userAgent": string,
"_anon": boolean,
"_debug": boolean,
"customAttribute1": any,
"customAttribute2": any
}

context.event.fields subobject groups multiple data points, many of which are very technical and not really useful for us. But some can open exciting use cases - let's dive in.

context.event.fields.pageType

Returns the name of the currently viewed page type as configured in the Sitemap (e.g., "Home"). This is useful when you want to adapt the campaign's serverside payload based on page type when the content zone is shared across many sites.

context.event.fields.action

Returns the name of the currently pushed action as configured in the Sitemap (f.e. 'Viewed Home'). It shines for custom action names that can drive different campaign experiences - despite being triggered on the same page type.

context.event.fields.url

Returns the URL where the event originated. The cool part is that it contains the hash and query strings, so you can drive use cases using those elements (f.e. change the experience based on the query string values you set in the email campaign using SFMC data).

context.event.fields.customAttribute

Apart from those always-there properties, you will also see custom attributes you passed along with the event. For example, if in the Sitemap you are passing SFMC Contact Key along with the event: actionEvent.user.attributes.sfmcContactKey = queryParameters.get('sk'); you will have context.event.fields.sfmcContactKey property available with that value. This is huge - it enables us to build campaigns leveraging real-time data. Use cases? Sure! Create an hasAddedInsurance attribute filled out by the Add to Cart event to determine whether you want to promote a cross-sell. Add the hasMetFreeDeliveryThershold boolean field to conditionally trigger recommendations of products that will help the customer get over the line of free delivery. The sky is the limit with those.

You Should Know

If you need some data only for the specific event purpose and don't want to store it in an attribute (be it due to limits or because Sitemap cannot remove the attribute value later), you can still use the actionEvent.user.attributes.customAttribute = 'value' approach to pass that information. It won't be stored on the user attribute (if there isn't one matching the name) but will still be available on the Event Stream and in the context object as context.event.fields.customAttribute. Works also for pushing custom events:

Pushing custom event with a custom attribute that does not exist as a user attribute
Evergage.sendEvent({
action: 'Custom Event',
user: {
attributes: {
customAttribute: 'value', // You can make up any property name you want
},
},
source: {
contentZones: [{ name: 'virtual_for_global_control' }],
}
})

contentZone

The contentZone string property returns the Content Zone selected for the Campaign. It might be helpful if your campaign supports multiple content zones and you want to alter some payload elements based on the one selected (f.e. change the number of returned recommendations):

Leverage context.contentZone to change the serverside payload
// Limit the number of recommendations to the first four for smaller placements
if (['search_see-more', 'listing_see-more'].includes(context.contentZone)) {
recommendations = recommendations.slice(0,4);
}

trigger

The trigger object property is filled only for the Triggered Campaign Templates.

🚧 Work in progress 🚧

locale

If you have switched on Locale support in your Marketing Cloud Personalization, the locale string will return a five-character long combination of ISO language code and ISO country code (language_COUNTRY, for example: en_US for American English).

You can use it to return the campaign content based on the most recent user locale (be it based on manually entered variations in the Campaign configuration or by pulling directly from the localized Catalog):

Leverage context.locale to pull localized product details
const recommendedIds = recommendIdsOnly(context, recipeConfig);
// Return localized recommendations with key data points needed for the campaign
let localisedRecommendations = context.services.catalog
.findItems('Product', recommendedIds)
.map(product => product.toFlatJSON(
['id', 'name,' 'imageUrl', 'url', 'price'],
context.locale || ''
))

services

context.services is the most potent part of the context object - packed to the brim with methods that give you access to Marketing Cloud Personalization data or let you create new recommendations.

Structure of the context.services object
{
"catalog": Object,
"recommendations": Object,
"smartTrends": Object,
"surveys": Object,
"decisions": Object,
"corvus": Object,
"promotionCatalog": Object
}

It's a nested object, so let's go through it property by property to discuss each group of methods.

services.catalog

The services.catalog object contains a set of lookup methods that help you find Items in your Marketing Cloud Personalization Catalogs. It opens a wide range of use cases for using related Items for cross-sell and up-sell purposes.

Structure of the context.services.catalog object
{
"dimensionFilter": (dimension: string): ItemFilter<any>,
"findClosestItems": (request: ClosestItemsRequest): Item[],
"findItem": (type: string, id: string): Item,
"findItems": (type: string, ids: string[]): Item[] || (type: ItemFilter<any>, ids: ItemSort<any>): Item[] || (type: ItemFilter<any>): Item[]
}

Most of those methods return an Item object - the content of it will differ depending on what type of item it is. By default, it will contain an id string and an attributes object with a name, imageUrl, description, promotable and archived properties. Optionally, an Item can also have a dimensions object with RCO's data and a location object. Finally, some properties are related to a specific type of an Item - like categories, skus, modifiedTime or subtitle.

Example Item structure for a Product Catalog
{
"id": "2050055",
"location": null,
"attributes": {
"imageUrl": {
"value": "https://www.northerntrailoutfitters.com/on/demandware.static/-/Sites-nto-apparel/default/dwf53f2476/images/large/2050055BEY-0.jpg"
},
"promotable": {
"value": true
},
"price": {
"value": 120
},
"inventoryCount": {
"value": 1
},
"name": {
"value": "Women's Hedgehog Agile NTO-tech"
},
"url": {
"value": "https://www.northerntrailoutfitters.com/default/women%27s-hedgehog-agile-nto-tech-2050055BEY.html"
},
"archived": {
"value": false
},
"customAttribute": {
"value": "customValue"
}
},
"dimensions": {
"Color": [
"AZT",
"APD",
"ANZ",
"AO5",
"BEY"
],
"Feature": [
"Waterproof",
"Lightweight",
"Breathable"
],
"Gender": [
"WOMEN"
]
},
"categories": [
"WOMEN|FOOTWEAR|HIKING"
],
"skus": {
"2050055BEY": {
"id": "2050055BEY"
}
}
}

context.services.catalog.findItem()

The findItem() is the most straightforward Catalog lookup method. It requires a Catalog Type (e.g., 'Product', 'Category', or your custom Catalog) and an Item ID (as a string) to return the full Item detail.

Return Item details
const itemDetails = context.services.catalog.findItem('Product', '19542');

It pairs perfectly with the context.event methods if you want to get additional details of the currently viewed product (extremely useful when you are using ETL and strict catalog security for catalog data ingestion):

Return Item details dynamically
const itemType = context.event.itemType();
const itemId = context.event.itemId();
const itemDetails = context.services.catalog.findItem(itemType, itemId);

This pairing lets us implement complex use cases like recommending direct cross-sells or up-sells using IDs passed to custom attributes on the product:

Return cross and up-sell details dynamically
// Get details of the currently viewed item
const itemId = context.event.itemId();
const viewedItemDetails = context.services.catalog.findItem('Product', itemId);
// Leverage those details to pull IDs of the hardcoded up-sell and cross-sell products
const upSellableItemId = viewedItemDetails.attributes?.upSellableItemId?.value;
const crossSellableItemId = viewedItemDetails.attributes?.crossSellableItemId?.value;
// Pass those IDs to yet another findItem method to get full details and fill in the handlebars with data
const upSellableItemDetails = context.services.catalog.findItem('Product', upSellableItemId);
const crossSellableItemDetails = context.services.catalog.findItem('Product', crossSellableItemId);

Of course, making so many findItem() calls is not optimal, and MCP offers another method as a solution.

context.services.catalog.findItems()

The findItems() method allows you to request details for multiple Items in a single call, which is cleaner and more performant. The baseline way of calling this function works just as findItem(), but it requires an array of IDs instead of a single ID and returns an array of Item detail objects instead of a single Item object.

Return multiple Items details
const itemsDetails = context.services.catalog.findItem('Product', ['19542', '12524', '91324']);

A neat use case for that method is pulling currently viewed Item details with findItem() to get values from a custom attribute containing alternative product IDs or dedicated accessories for it. Then, pass all those found product IDs into findItems() to get the full detail required for a personalization showcasing those related items.

The findItems() method offers more. You are not limited to just pushing an array of IDs like mentioned above - you can also pass an ItemFilter (created by dimensionFilter() method) to get the IDs of items available in a custom (and only custom) Catalog.

Return multiple Items details
const catalogFilter = context.services.catalog.dimensionFilter('Color');
const itemsDetails = context.services.catalog.findItems(catalogFilter);

It can be further extended that custom-catalog-based approach by passing an ItemSort as a second argument. However, despite trying my luck with the sortByActivity and sortByPublishedDate properties, I haven't been able to make it work. That's too bad, as sorting by activity could have been interesting when working with custom Catalog Items.

context.services.catalog.findClosestItems()

If you are using location details for your Items, the findClosestItems() method will work wonders for you as it allows you to get items based on the provided longitude and latitude within a defined distance:

Return multiple Items details
const closestDefinition = {itemType: 'Product', latitude: 52.23, longitude: 21.0118, maxDistance: 10, maxResults: 5};
const itemsDetails = context.services.catalog.findClosestItems(closestDefinition);

As you can see, you need to provide an object with itemType, longitude and latitude, maxDistance (in miles) and maxResults as an argument. All fields are required. Of course, you don't have to hardcode it. There are two neat use cases around dynamically provided longitude and latitude:

  1. You can try to get longitude and latitude from context.user.location.geographicPoint. It allows you to make magic-like recommendations even for anonymous, first-time visits. However, confidence might be limited depending on the market specifics.
  2. You can capture user-provided location (based on the shipping details or the city they are filtering for on your website) and pass calculated long/lat to a user attribute. Then, use that user attribute as the source of data for the findClosestItems() call to provide location-aware recommendations for your customers.

context.services.catalog.dimensionFilter()

As mentioned in findItems(), the dimensionFilter() method lets us create an ItemFilter that can be leveraged to find Items from a given custom catalog (which used to be called a Dimension, hence the method name). Just pass the Catalog name as a string there. That's it.

Create an ItemFilter for Color Catalog
const catalogFilter = context.services.catalog.dimensionFilter('Color');

services.recommendations

The services.recommendations object contains four methods related to Item recommendations. Two of them are absolutely great for delivering highly customized cross-selling use cases, while the other two don't seem to work. It's a nice mixed bag. Let's dive in.

Structure of the context.services.recommendations object
{
"recommend": (request: RecommendationsRequest): Item[],
"recommendIdsOnly": (request: RecommendationsRequest): Item[],
"smartSearch": (request: SmartSearchRequest): Item[],
"smartSort": (request: SmartSort): Item[]
}

context.services.recommendations.recommend()

The recommend() method is an excellent solution for generating Item recommendations in a programmatic and highly customized way. It requires currentItemId and currentItemType as an anchor for the Recipe, maxResults to limit the number of outcomes, recipeId to apply the correct recommendation model and userId to pull the correct affinity data.

Those data points can be hard coded, pulled from marketer-filled fields or captured from other context properties like context.user.id, context.event.itemId() and context.event.itemType() or context.services.catalog outputs.

Return details of items recommended from a recipe
const userId = context.user.id;
const recommendationsRequest = {
currentItemId: '1017847',
currentItemType: 'Product',
maxResults: 12,
recipeId: '7H5OK',
userId: userId
}
const recommendedItems = context.services.recommendations.recommend(recommendationsRequest);

It's a great feature for deploying recommendations that shouldn't be changed during campaign configuration, cross-selling offerings based on the currently viewed item (using its custom attributes), or filtering items based on negative recipes.

You Should Know

The recommend() method works just as the recommend that you can import in the template along with RecommendationsConfig from the recs module. The imported one is better suited for the user-friendly module that the marketer can set up according to the business needs during the campaign configuration phase. The context method is better for deploying hardcoded logic programmatically without any input from the marketer—the key value being the possibility of providing a custom Item anchor that might be different from the currently viewed one.

However, forcing a custom anchor Item is also possible with the imported recommend thanks to the overrideOnPageAnchor() method (requiring itemId and itemType string arguments) that can be deployed both along with RecommendationsConfig as well as within the run block:

Changing Item anchor for recommendation
import { RecommendationsConfig, recommend } from "recs";

export class customAnchorRecommendation implements CampaignTemplateComponent {
// Renders interface for configuring the recommendation
recsConfig: RecommendationsConfig = new RecommendationsConfig().restrictItemType("Product");

run(context: CampaignComponentContext) {
// Replaces currently viewed Item anchor context with a custom one
this.recsConfig.overrideOnPageAnchor('251422', 'Product');

return {
products: recommend(context, this.recsConfig),
};
}
}

context.services.recommendations.recommendIdsOnly()

The recommendIdsOnly() method works like recommend(), but instead of returning an array of objects with Item details, it returns an array of Item IDs as strings. Just as recommendIdsOnly you can import with RecommendationsConfig from the recs module.

Return IDs of items recommended from a recipe
const recommendationsRequest = {
currentItemId: '1017847',
currentItemType: 'Product',
maxResults: 12,
recipeId: '7H5OK',
userId: context.user.id
}
const recommendedItemIds = context.services.recommendations.recommendIdsOnly(recommendationsRequest);

It can be helpful if you only need to pass IDs to the e-commerce platform for it to render the campaign experience or when you want to filter out specific IDs returned by a recipe from another recommendation you deploy.

context.services.recommendations.smartSearch()

The smartSearch() method allows you to get recommendations based on search query string (at least 3 characters long) and a recipe (for example, to recommend items when user interacts with a search box on the page):

Return Items recommended using smart search
const smartSearchRequest = {
query: "jacket",
maxResults: 1,
recipeId: "7H5OK",
userId: context.user.id
}
const recommendedItems = context.services.recommendations.smartSearch(smartSearchRequest);

The query parameter is looking for a match in the name and description (only), then filters found Items using the provided Recipe ID. Keep in mind that the Recipe cannot be using Ingredients requiring an Item anchor. It’s best to use the Any Items ingredient and then optionally sort it using smartSort().

You Should Know

smartSearch() can return more than 12 recommendations (it can bring back thousand - and frequently it will need to so that there pool big enough for proper smartSort() results), but as it returns whole Item details, it can impact performance. It might be helpful to filter the outcome just to the information required for your purpose for further processing.

Return only IDs of items recommended using smart search
const recommendedItemsIds = context.services.recommendations.smartSearch(smartSearchRequest).map(item => item.id);

context.services.recommendations.smartSort()

The smartSort() method requires an object with an itemIds array, recipeId, and userId. It is a cool feature for the context.services.catalog set of functions as it can filter and order Item IDs pulled from a custom attribute or a catalog retrieve based on current user interest using a provided Recipe.

Return items sorted using a Recipe
const itemsToBeSorted = ['1018913', '60365', '1017847', '1105'];
const smartSortRequest = {
itemIds: itemsToBeSorted,
recipeId: '7H5OK',
userId: context.user.id
}
const smartSortedItems = context.services.recommendations.smartSort(smartSortRequest);

services.smartTrends

The smartTrends() method lets you leverage the MCP Trends feature directly in the Serverside code of the Campaign. It requires a special SmartTrendsRequest argument structure ({itemIds: ['1018913'], itemType: "Product", lookbackMinutes: 9999 }) and returns an array of objects with product engagement details within the provided lookback ([{itemId: '1018913', visitViews: 1234, purchases: 98}]).

Return item engagement details
const smartTrendsRequest: SmartTrendsRequest = {
itemIds: ['1018913'],
itemType: 'Product',
lookbackMinutes: 1656
}
const smartTrendItemDetails = context.services.smartTrends.smartTrends(smartTrendsRequest);

It’s a nice and more powerful alternative to the client-side code API approach based on the Trends Global Template.

services.surveys

Structure of the context.services.surveys object
{
"getSurey": (surveyId: string): Survey
}

🚧 Work in progress 🚧

services.decisions

Structure of the context.services.decisions object
{
"decide": (request: ContextualBanditRequest): Item[]
}

🚧 Work in progress 🚧

services.corvus

Structure of the context.services.corvus object
{
"contextualBandit": {
"decide": (request: ContextualBanditRequest, filter: PromotionFilter): Item[]
}
}

🚧 Work in progress 🚧

services.promotionCatalog

Structure of the context.services.promotionCatalog object
{
"findPromotions": (filter: ItemFilter<any>, context: CampaignComponentContext): Promotion: [],
"promotionFilter": (contentZone: string): PromotionFilter
}

🚧 Work in progress 🚧

user

context.user is the most significant property, covering tons of information about the user that triggered the campaign. It contains multiple subobjects and methods perfect for statistics-based use cases within the serverside code.

Structure of the context.user object
{
"attributes": Object,
"profileObjects": Object,
"visits": [Object],
"orderHistory": [Object],
"location": Object,
"currentCart": Object,
"anonymous": boolean,
"segmentMembership": [Object],
"id": string,
actionCount: (request: ActionStatsRequest): number,
actionCountPerItem: (request: ActionStatsRequest): Object,
getDimensionActivity: (dimension: string, start: Date, end: Date): {
[itemId: string]: ItemActionStats
},
getDimensionActivityByDay: (dimension: string, start: Date, end: Date): {
[date: string] : ItemActionStats
},
getEmailSendHistory: (start: Date, end: Date): EmailSendActivity[] || (): EmailSendActivity[],
getLatestOrderByStatus: (status: 'Open' | 'Purchased' | 'Cancelled'): Order,
getSegmentJoinDate: (segmentId: string): Date,
itemStatTotal: (request: ItemStatsRequest): number,
itemStatTotalPerItem: (request: ItemStatsRequest): ItemStat[],
pageViewCount: (request: StatsRequest): number,
visitCount: (request: StatsRequest): number,
visitDurationMillis: (request: StatsRequest): number,
}

Let's start our discovery of context.user with the methods.

user methods

context.user.actionCount()

Requires an ActionStatsRequest ({actionName: 'Name of the action'}) and returns the total of provided action triggers for the current user. You can also extend ActionStatsRequest with start or end (but not both) date properties to limit the timeframe of the action count.

Check how many times customer viewed cart within the last day
const today = new Date();
const yesterday = new Date(today.setDate(today.getDate() - 1));
const homeViewCount = context.user.actionCount({actionName: 'Viewed Cart', start: yesterday}); // Returns: 4

context.user.actionCountPerItem()

In theory, it should be able to return the action count per item (after passing a 'Viewed Product' action in ActionStatsRequest, it should show the counts per each product where that action triggered). But it doesn't. It returns the same information as the actionCount() method, but instead of doing it directly as a number, it does it as an object with an action name. Unless I'm missing something, it's useless.

Full on promise, null on delivery actionCountPerItem method
const homeViewCount = context.user.actionCountPerItem({actionName: 'Viewed Product'}); // Returns: {'Viewed Product': 5}

context.user.getDimensionActivity()

Requires a dimension (a Catalog, like 'Product', 'Category' or 'CustomCatalog') and start + end date boundaries. This time, you must always provide both in that exact order. The significant difference with this method is that you pass direct arguments, not a grouping Stat object. It returns an object with Item IDs and related activity data from the selected Dimension with which the user interacted during the timeframe.

Find the activity in a specified catalog
const today = new Date();
const yesterday = new Date(today.setDate(today.getDate() - 1));
const brandActivity = context.user.getDimensionActivity('Brand', yesterday, today); // Returns:
// {
// "Apple": {
// "view": 2,
// "viewOutOfStock": 0,
// "viewDetail": 0,
// "viewTime": 43339,
// "cart": 1,
// "cartValue": 215,
// "purchase": 1,
// "purchaseValue": 215,
// "review": 0,
// "share": 0,
// "comment": 0,
// "favorite": 0
// }
// }

That's an excellent set of data to calculate the most viewed Category, longest viewed Product, or most purchased Brand by that specific user. Unfortunately, it's still just a proxy for the actual affinity data, which is unavailable.

You Should Know

While you can work on the returned object, you cannot directly pass it to the serverside payload. You can output the final value (f.e. brandActivity.Apple.view), but both brandActivity and brandActivity.Apple will break it.

You can perform calculations in serverside code on any level, but if you need to output it directly in the payload, there is a trick: JSON.parse(JSON.strinify(brandActivity)).

context.user.getDimensionActivityByDay()

The getDimensionActivityByDay method works nearly the same as getDimensionActivity. There are two key differences:

  1. It requires additional argument - right after selecting the Dimension, you must also pass the specific Item ID for which you want to see the activity.
  2. The returned Object will have epoch properties for each activity day within the selected period.
Find the activity in a specified catalog
const today = new Date();
const yesterday = new Date(today.setDate(today.getDate() - 1));
const brandActivity = context.user.getDimensionActivityByDay('Brand', 'Apple', yesterday, today); // Returns:
// {
// "1707782400000": {
// "view": 2,
// "viewOutOfStock": 0,
// "viewDetail": 0,
// "viewTime": 43339,
// "cart": 1,
// "cartValue": 215,
// "purchase": 1,
// "purchaseValue": 215,
// "review": 0,
// "share": 0,
// "comment": 0,
// "favorite": 0
// }
// }

It also has the same payload limitation as getDimensionActivity, and the same workaround is available.

context.user.getEmailSendHistory()

Requires either nothing or start/end data boundary and returns... nothing. At least I couldn't get it to work with the OTE Campaign data. It may leverage the barely working External Email Campaign ETL.

context.user.getLatestOrderByStatus()

Requires an order status ('Open', 'Purchased' or 'Cancelled') and returns the most recent Order object in the selected state. The data structure and content are the same as in the user.orderHistory.

context.user.getSegmentJoinDate()

Requires Segment ID (you can view it in User Segments after you add the ID column or by opening a specific segment and copying five alphanumerical characters from URL: .../segment/{SegmentID}/members...) and returns an epoch with join date. It's a fantastic way to capture additional context for the user (f.e. how many days ago he joined the Gold Tier segment).

Segment join epoch fun
const segmentJoinEpoch = context.user.getSegmentJoinDate('qWeR1'); // Returns: 1695796858287

If the Segment ID is incorrect or the user has not joined the provided segment, it will return null.

You Should Know

You can easily convert epoch to date to simplify date calculations:

Perform date comparisons on epoch
const segmentJoinEpoch = context.user.getSegmentJoinDate('qWeR1'); // Returns: 1695796858287
const segmentJoinDate = new Date(segmentJoinEpoch);

const today = new Date();
const lastWeek = new Date(today.setDate(today.getDate() - 7));

const hasJoinedLastWeek = segmentJoinDate > lastWeek;

context.user.itemStatTotal()

Requires an ItemStatsRequest ({itemType: 'CatalogName', statType: 'StatTypeName'}) and returns the count for that stat for a given Catalog. You can also extend ItemStatsRequest with another optional property itemId: 'id' to limit the result to a specific Item within a given Catalog (itemType). Finally, as with actionCount(), you can also use start or end timeframe boundaries - but not both.

Time spent by the user on Laptop Category within the last day in milliseconds
const today = new Date();
const yesterday = new Date(today.setDate(today.getDate() - 1));
const itemViewTime = context.user.itemStatTotal({
itemId: 'Laptop',
itemType: 'Category',
statType: 'ViewTime',
start: yesterday
}); // Returns: 98663

Available statTypes: 'View', 'ViewOutOfStock', 'ViewValue', 'ViewDetail', 'QuickView', 'ViewTime', 'Cart', 'CartValue', 'Purchase', 'Visit', 'PurchaseValue', 'Review', 'Share', 'Comment', 'Favorite', 'Searches', 'SearchClicks', 'ClickThrough', 'RemoveFromCart', 'RemoveFromCartValue', 'RecommendedCount', 'PageLoadTime', 'PageLoadTimeCount', 'DomLoadTime', 'DomLoadTimeCount', 'TwReceiverTime', 'TwReceiverTimeCount', 'NumErrorEvents', 'TriggeredCount', 'RequestedForServing', 'EligibleForServing', 'Served'.

Remember that the meaning of the returned value will differ depending on the selected statType - it can be count, milliseconds or money.

You Should Know

This method works perfectly with the context.event.itemId() and context.event.itemType() as with those, you can pull relevant stats for a currently viewed Item and adapt payload for it (f.e. adapt Exit Intent incentive based on the number of visits or time spent on currently viewed product).

context.user.itemStatTotalPerItem()

itemStatsPerItem() works in a very similar manner to itemStatsTotal() and accepts the same ItemStatsRequest. The key difference is that instead of a single value, it will return an array of objects, each containing an itemId and value specific to that item.

Time spent by the user on Products within the last day in milliseconds
const today = new Date();
const yesterday = new Date(today.setDate(today.getDate() - 1));
const itemViewTime = context.user.itemStatPerItem({
itemType: 'Product',
statType: 'ViewTime',
start: yesterday
}); // Returns: [{itemId: '123', value: 9238}, {itemId: '456', value: 26651}]

While you can pass itemId: 'id' in the ItemStatsRequest, it will limit the outputted array to a single object for that item, making it not useful vs itemStatsTotal().

context.user.pageViewCount()

Requires StatsRequest ({start: Date, end: Date} - use either start or end; using both will always return 0) and returns the count of pages viewed in that timeframe. Both timeframe bounds are required.

Pages viewed within the last day
const today = new Date();
const yesterday = new Date(today.setDate(today.getDate() - 1));
const itemViewTime = context.user.pageViewCount({
start: yesterday
}); // Returns: 9

context.user.visitCount()

Similar to pageViewCount(), it requires StatsRequest ({start: Date, end: Date} - use either start or end; using both will always return 0) but returns the count of visits instead of specific pages.

You Should Know

Visit for Marketing Cloud Personalization starts from the first page view and ends after the user reaches 30 minutes of inactivity on the site. So if a user goes to your website to view a few pages, leaves and then returns after 40 minutes - it will be counted as a separate visit.

Visits within the last day
const today = new Date();
const yesterday = new Date(today.setDate(today.getDate() - 1));
const itemViewTime = context.user.visitCount({
start: yesterday
}); // Returns: 2

context.user.visitDurationMilis()

Similar to pageViewCount(), it requires StatsRequest ({start: Date, end: Date} - use either start or end, using both will always return 0) but returns the number of milliseconds the user spent on your website in a specified timeframe.

Visits within the last day
const today = new Date();
const yesterday = new Date(today.setDate(today.getDate() - 1));
const itemViewTime = context.user.visitDurationMilis({
start: yesterday,
end: today
}); // Returns: 98663

user.attributes

context.user.attributes object contains out-of-the-box, custom and hidden attributes with respective values for the user. It's an instrumental part of the context as it allows you to pull user-specific data not only from the triggering event (like it is also possible with context.event.fields.customAttribute) but also from past events. This enables fun use cases like saving in custom attributes the last viewed Product and Category with Sitemap and then leveraging that information when the user is on the non-product page of your website to bring them back onto the funnel. It's also great to personalize your campaign (f.e. with the first name in the info banner or overlay).

Structure of the context.user.attributes object
{
"created": {
"value": number // epoch
},
"customAttribute": {
"value": any
},
"originatingReferrer": {
"value": "{\"medium\":\"Direct\",\"source\":null,\"terms\":null,\"domain\":null,\"subdomainReversed\":null,\"url\":null,\"landingUrl\":\"https://www.mateuszdabrowski.pl/\"}"
},
"firstName": {
"value": string
},
"lastViewedCartAt": {
"value": number // epoch
},
"firstActivity": {
"value": number // epoch
}
}

user.profileObjects

🚧 Work in progress 🚧

user.visits

context.user.visits is an Array with user visits. Remember that the Marketing Cloud Personalization visit starts from the first page view and ends after the user reaches 30 minutes of inactivity on the site. So if a user goes to your website to view a few pages, leaves and then returns after 40 minutes - it will be counted as a separate visit. It is critical - there is no way to access the history of each page the user visits. You can only see the visit (session start data) with a pageViewIndex with a count of page views during that visit.

Structure of the context.user.visits array
[
{
"start": number, // epoch
"lastEventTime": number, // epoch
"timeSinceLastVisit": number, // milliseconds
"referrer": { // || null
"medium": "Direct",
"source": null,
"terms": null,
"domain": null,
"subdomainReversed": null,
"url": null,
"landingUrl": "https://www.mateuszdabrowski.pl/"
},
"deviceType": "Computer",
"browser": "Chrome",
"platform": "Web",
"operatingSystem": "Windows",
"weather": { // || null
"temperature": 71,
"humidity": 67,
"windSpeed": 7,
"rain3h": 0,
"snow3h": 0,
"cloudCoverage": 0,
"condition": {
"id": 800,
"name": "clear sky",
"icon": "01d",
"category": "Clear"
}
},
"pageViewIndex": 9
}
]

While there are a few attributes here, I would like to focus on two that enable exciting use cases:

  1. referrer can contain data of the website that led the user to you. If this is the case, you can create a dedicated campaign focusing on the source (f.e. small vouchers to convert people coming from voucher-gathering websites) or terms (f.e. changing the experience based on social ad terms passed).
  2. weather can provide you with details about temperature, rain and snow, unlocking like-magic use cases, f.e. if it is cold and showers for your customer, display a campaign with a dedicated message promoting a sunny and hot travel destination.

user.orderHistory

context.user.orderHistory is an array with past orders in any status (open, purchased or cancelled) for the user. It contains everything - timeframes, order value and currency and even a list of all products in that order.

Structure of the context.user.orderHistory array
[
{
"id": null,
"created": number, // epoch
"updated": number, // epoch
"purchaseDate": null,
"visitAgeAtPurchase": number, // milliseconds
"totalValue": number,
"totalValueCurrency": null,
"status": "Open",
"metadata": null,
"lineItems": [
{
"quantity": number,
"price": number,
"itemId": string,
"attributes": {}
},
],
"attributes": {}
}
]

user.location

context.user.location can be magic or trash - depending on the Internet Service Providers of your audience. The rule of thumb is good data for B2B and mixed data for B2C. It's worth checking because if you can trust/clean this data, you can do astounding things with it.

Structure of the context.user.location object
{
"geographicPoint": {
"latitude": number,
"longitude": number
},
"timeZoneId": "Europe/Warsaw",
"continentKey": "EU",
"countryCode": "PL",
"countryNumericCode": 616,
"stateProvinceCode": "14",
"city": "Warsaw",
"postalCode": "00-633",
"organization": "Pwc Polska Sp. Z O.o.",
"naicsCode": "517311"
}

Firstly, context.user.location.geographicPoint contains latitude and longitude that can be perfect for context.services.catalog.findClosestItems() call.

Secondly, if you target B2B customers and get organisation details (the example above is real - it returned all those details when I checked it from the PwC Poland office), it can help you get precious information about your known and anonymous visitors! However, a (big) grain of salt is needed - the NAICS Code (2017 NAICS Definition) returned for me Wired Telecommunications Carriers, which looks like code for Internet Services Provider, not PwC. For B2C, the organisation field will return the Internet Service Provider name in most cases.

user.currentCart

context.user.currentCart is a single object with the same structure as each of the orders stored in context.user.orderHistory. But because it is still a cart, not an order, most fields will be null/0.

Structure of the context.user.currentCart object
{
"id": null,
"created": number, // epoch
"updated": number, // epoch
"purchaseDate": null,
"visitAgeAtPurchase": 0,
"totalValue": 0,
"totalValueCurrency": null,
"status": "Open",
"metadata": null,
"lineItems": [
{
"quantity": number,
"price": number,
"itemId": string,
"attributes": {}
},
],
"attributes": {}
}

The fun use case here is checking when the user updated the cart and, if enough time has passed, leveraging the lineItems to deploy an abandoned basket Web Campaign.

user.segmentMembership

context.user.segmentMembership is an array with segments the user is a member of. With a good segmentation naming convention, the segmentName and joined can capture valuable additional context for the user (f.e. how many days ago he joined the Gold Tier segment or became an at-risk customer).

Structure of the context.user.segmentMembership array
[
{
"segmentId": string,
"segmentName": string,
"joined": number, // epoch
"createIfMissing": boolean,
"removal": boolean,
"userId": string,
"customerId": string,
"customerType": "User"
}
]

accountId & datasetId

accountId and datasetId are string properties that contain information about the Marketing Cloud Personalization account and dataset that generated the event. It is only handy if you want environment-aware debug log visibility logic.

configuration

The configuration object property contains information about the campaign properties (fields you expect the marketer to fill in when configuring the campaign) in the experience for a given user. Not really useful, as in the serverside code, you can access the same information using the this keyword (f.e. this.campaignPropertyName).