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.
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.
{
"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:
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
).
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.
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.
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.
{
"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.
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
{
".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.
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:
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):
// 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):
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.
{
"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.
{
"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
.
{
"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.
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):
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:
// 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.
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.
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:
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:
- 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. - 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.
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.
{
"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.
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.
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:
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.
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):
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()
.
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.
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.
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}]
).
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
{
"getSurey": (surveyId: string): Survey
}
🚧 Work in progress 🚧
services.decisions
{
"decide": (request: ContextualBanditRequest): Item[]
}
🚧 Work in progress 🚧
services.corvus
{
"contextualBandit": {
"decide": (request: ContextualBanditRequest, filter: PromotionFilter): Item[]
}
}
🚧 Work in progress 🚧
services.promotionCatalog
{
"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.
{
"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.
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.
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.
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.
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:
- 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.
- The returned Object will have epoch properties for each activity day within the selected period.
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).
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 can easily convert epoch to date to simplify date calculations:
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.
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.
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.
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.
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.
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.
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.
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).
{
"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.
[
{
"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:
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).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.
[
{
"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.
{
"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
.
{
"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).
[
{
"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
).