# developer Marketing Cloud Personalization (personalization) - Full Documentation # Account Export API The Marketing Cloud Personalization Account Export API helps you retrieve account-level data from datasets such as segments, engagement scores, change in engagement scores, and customer activity for use in other systems. Your Personalization contract defines the volume of records you can export using the Account Export API. For more information, refer to your contract or contact your account representative. **important:** Account Profiles was retired in August 2024. For more information, see [Personalization Account Profiles Retirement](https://help.salesforce.com/s/articleView?id=001744617&type=1). ## Exporting Account Data via API Export account data from a Personalization dataset. ### Method and Endpoint `GET` `/api/dataset//accounts.< json | csv >` ### Request URL `https://..evergage.com/api/dataset//accounts.< json | csv >` ### Request Parameters | URL Parameter | Required? | Description | | ---------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `accountName` | Yes | The Personalization account name. You can find the `accountName` by accessing **Gears** from the Personalization UI and reviewing the URL. For example, if the Gears URL is `https://demo.us-1.evergage.com/`, then the account name is `demo`. | | `instance` | Yes | The Personalization instance identifier. You can find this value by accessing **Gears** from the Personalization UI and reviewing the URL. For example, if the Gears URL is `https://demo.us-1.evergage.com/`, then the instance identifier is `us-1`. | | `datasetId` | Yes | The name or identifier of the Personalization dataset you’re exporting account data from. | | `filter` | No | Optional Segment name or ID | | `start` | No | Optional start time in epoch milliseconds used for engagement statistics | | `end` | No | Optional end time in epoch milliseconds used for engagement statistics | | `segmentationId` | No | Optional segmentation identifier | | `segmentId` | No | Optional segment identifier | | `pageSize` | No | Number of records to return. Returns all records unless specified. | | `page` | No | Page number of records to show, starting at zero for the first page. Starts at zero unless specified. | ### Authorization `Authorization: Basic ` The Account Export API uses API tokens to authenticate requests. Your API token must be a strict Base64 encoding of your API Key ID and its API Secret Key, separated by a colon. You can use an existing API token if it has the required permissions. Alternatively, you can ask an account administrator to generate a new one from the Personalization UI. For more information on API tokens, see [Personalization API Tokens](https://help.salesforce.com/s/articleView?id=sf.mc_pers_api_tokens.htm) on Salesforce Help. Alternatively, if you use [cURL](https://curl.se/docs/manpage.html) to interact with the API, you can send your API Key ID and its API Key Secret using the `-u "apiKeyId:apiKeySecret"` command-line option. cURL automatically converts the `apiKeyId:apiKeySecret` pair into a Base64-encoded string and adds the `Authorization: Basic ` to the request header. For an example of how to pass your API credentials in a cURL request, refer to the [Example](#example) section in this article. ### Example The following is an example GET request sent using cURL that retrieves account data in the JSON format from the specified Personalization dataset. **note:** The following example request passes API credentials using the `-u` command-line option. ```bash curl -u "apiKeyId:apiKeySecret" \ "https://..evergage.com/api/dataset//accounts.json" ``` The preceding example cURL request generates the following example response in the JSON format. ```json [ { "name": "Acme, Inc.", "engagementScore": 0.546, "engagementPercentChange": -0.138, "firstActivity": 1358640000000, "lastActivity": 1360195200000, "totalActions": 51, "accountStatus": "premium", "attributes": {}, "accountName": "Acme, Inc.", "activeUsers": 3, "segments": [] } ] ``` ## See Also - _Salesforce Help_: [Personalization API Tokens](https://help.salesforce.com/s/articleView?id=sf.mc_pers_api_tokens.htm) # Add Third Party Tracking Parameters to Open Time Email Campaigns Urchin Tracking Module (UTM) parameters are tags that you can add to the end of a URL to track its performance and know its traffic sources. Adding UTM parameters to URLs in your Open Time Email Campaigns can give you more detailed insights into your campaign's performance. While Marketing Cloud Personalization provides analytics and full campaign performance reporting, you can still use your existing web analytics system to monitor clickthrough traffic with your Open Time Email campaigns. To learn more about how you can combine third-party tracking data with data from Personalization, talk to your Salesforce account representative. This article describes how to use UTM parameters with Open Time Email Campaigns. To know more about Open Time Email Campaigns, or how to create one, see [Open Time Email Campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign.htm) on Salesforce Help. Before reading the instructions in this article, make sure you've created or cloned an Open Time Email campaign, tested it, and have its HTML code open in front of you. ## Add UTM Parameters to Your Emails To add UTM parameters to your emails, do the following. 1. Access the Open Time Email campaign within the Campaign Edit screen in the Personalization UI. For detailed instructions on accessing the Campaign Edit screen, see [Create an Open Time Email Campaign](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign_create.htm) on Salesforce Help. 2. Click **SIMULATE** > **Show HTML**. 3. Identify the URLs to track in your email campaign. We recommend tracking URLs for item recommendations included in your email template along with any other URLs of importance. 4. Append UTM parameters at the end of each identified URL, starting with and separated by the `&` symbol. Excluding the `&` symbol breaks the URL and leads to improper tracking. For example, if the URL of an item in your email is `https://demo.evergage.com/api/dataset/engage/campaign/YnFdJ/N7pF1/1/detail?userId=@@USER_ID@@`, add the UTM parameters at the end of the URL as shown in the following example. ``` https://demo.evergage.com/api/dataset/engage/campaign/YnFdJ/N7pF1/1/detail?userId=%40%40USER_ID&utm_source=newsletter&utm_medium=email&utm_campaign=lead_generation ``` After you've added UTM parameters to all relevant URLs, use the updated campaign HTML with your email marketing solution to send your campaign to recipients. With the UTM parameters you added, you can track your campaign's clickthrough rates directly in your web analytics system. ## URL Encoding in UTM Parameters To ensure that URLs resolve correctly, you must encode all non-ASCII characters and the following reserved ASCII characters when using them in UTM parameters. | ASCII Character | URL Encoding | | --------------- | ------------ | | `!` | `%21` | | `#` | `%23` | | `$` | `%24` | | `'` | `%27` | | `(` | `%28` | | `)` | `%29` | | `*` | `%2A` | | `+` | `%2B` | | `,` | `%2C` | | `/` | `%2F` | | `:` | `%3A` | | `;` | `%3B` | | `=` | `%3D` | | `?` | `%3F` | | `@` | `%40` | | `[` | `%5B` | | `\|` | `%5C` | | `]` | `%5D` | ## Example The following code sample contains HTML for a row of three item recommendations in an Open Time Email campaign. This sample HTML is for illustrative purposes only and includes an illustrative dynamic variable `@@USER_ID@@` for `userId`. Your email marketing solution could use a different dynamic variable format. **note:** Depending on your email marketing solution, you may have to modify the item and item image URLs in your Open Time Email campaign. For more information on capturing and modifying campaign URLs, refer to [Capture an Open Time Email Campaign URL](https://developer.salesforce.com/docs/marketing/personalization/guide/capture-email-campaign-url.md). ```html
``` The goal of this example is to track the traffic source of item URLs in the campaign. To do so, we'll append the UTM parameters `utm_source=newsletter`, `utm_medium=email`, and `utm_campaign=lead_generation` at the end of each item URL, after `detail?userId=@@USER_ID@@"`. The sample campaign HTML after adding UTM parameters to each item URL is as shown in the following example. ```html
``` ## See Also - _Salesforce Help_: [Open Time Email Campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign.htm) - _Salesforce Help_: [Create an Open Time Email Campaign](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign_create.htm) # Additional APIs Marketing Cloud Personalization provides the following additional APIs that let you interact with and retrieve data from your Personalization instance, and also perform housekeeping tasks on your Personalization instance as needed. - [Account Export API](https://developer.salesforce.com/docs/marketing/personalization/guide/account-export-api.md) - Export account data from Personalization datasets. - [Audit Log API](https://developer.salesforce.com/docs/marketing/personalization/guide/audit-log-api.md) - Retrieve access and activity logs from your Personalization instance. - [Principals API](https://developer.salesforce.com/docs/marketing/personalization/guide/principals-api.md) - Retrieve list of Personalization Principals. - [User Export API](https://developer.salesforce.com/docs/marketing/personalization/guide/user-export-api.md) - Export user data from Personalization datasets. - [User Look-Up & Deletion API](https://developer.salesforce.com/docs/marketing/personalization/guide/user-look-up-deletion-api.md) - Service _Right to Access_ and _Right to be Forgotten_ requests received from customers. # Advanced Dynamic Content Cheatsheet This section details the variables used for advanced dynamic messages. ## Users | Variable | Type | Description | | -------------------------------- | ------ | ------------------------------- | | `${user.id}` | String | User ID | | `${user.userName}` | String | Name | | `${user.userNameOrAnonLocation}` | String | Name or "user from City, State" | | `${user.accountType}` | String | Account type | | `${user.emailAddress}` | String | Email address | ### User Location | Variable | Type | Description | | ------------------------------------ | ------- | -------------------------------------------------- | | `${user.location.postalCode}` | String | ZIP code | | `${user.location.city}` | String | City | | `${user.location.metro}` | String | Metro area | | `${user.location.metroCode}` | Integer | Metro code | | `${user.location.region}` | String | State/region | | `${user.location.stateProvinceCode}` | String | State/region code (shorthand) | | `${user.location.country}` | String | Country | | `${user.location.countryCode}` | String | Country code (shorthand) | | `${user.location.organization}` | String | Organization | | `${user.location.industry}` | String | Industry | | `${user.location.naicsCode}` | Integer | North American Industry Classification System code | ### User Custom | Variable | Description | | ------------------------------ | --------------------------------------------------------- | | `${user.attributes._________}` | Custom Attribute (Use dropdown in editor for specific ID) | ## Items Things that count as items: - Products - Articles - Blogs - Catalog Objects - Categories ### Basic Must have intended item query selected in message settings | Variable | Type | Description | | --------------------------------- | ----------------- | ------------------------------------------------------ | | `${item}` | Object | Item object | | `${items}` | Array | Array of item objects | | `${items[0]}...${items[9]}` | Object | Item in array | | `${page.item}` | Object | Current page item object | | `${page.category}` | Object | Current page category or category of current page item | | `${item.name}` | String | Item name | | `${item.id}` | String or Integer | Item ID | | `${item.url}` | String | Item URL | | `${item.imageUrl}` | String | Image URL | | `${item.description}` | String | Item description | | `#field(${item.rating}, '0')` | Integer | Average rating | | `#field(${item.numRatings}, '0')` | Integer | Number of ratings | | `${item.categories}` | Array | Categories objects | | `${item.dimensions}` | Array | All related Catalog Objects | | Custom Variables | Description | | -------------------------------- | ------------------------------------------------ | | `${item.attributes.xxxxx.value}` | Value of the attribute named "xxxxx" on the item | ### Products Only | Variable | Type | Description | | -------------------------- | ------- | ------------------------------------------------------------------------------------- | | `${item.price}` | Integer | Price. **NOTE**: Unformatted. For more information, see [Utilities](#utilities). | | `${item.priceDescription}` | String | Price description | | `${item.listPrice}` | Float | List price. **NOTE**: Unformatted. For more information, see [Utilities](#utilities). | | `${item.inventoryCount}` | Integer | Inventory count | | `${item.brands}` | Array | Brand tag objects | | `${item.classes}` | Array | Class tag objects | | `${item.styles}` | Array | Style tag objects | | `${item.genders}` | Array | Gender tag objects | ## Build Item Query in Message (Advanced) ### Query Type: Global/User Interchangeable | Variable | Description | | ------------------------------- | ----------------------- | | `${tools.global...}` | Query Global Statistics | | `${tools.user...}` | Query User Statistics | | `${tools.global.products...}` | Query Products | | `${tools.global.articles...}` | Query Articles | | `${tools.global.blogs...}` | Query Blogs | | `${tools.global.categories...}` | Query Categories | | `${tools.global.brands...}` | Query Brands | | `${tools.global.styles...}` | Query Styles | | `${tools.global.authors...}` | Query Authors | | `${tools.global.keywords...}` | Query Keywords | ### Query: Default Array Length Min 1 Max 5 | Variable | Type | Description | | ------------------------------------------ | ----- | --------------------------------------- | | `${tools.global.products.viewCount()}` | Array | Top viewed products by count | | `${tools.global.products.viewTime()}` | Array | Top viewed products by time | | `${tools.global.products.cartCount()}` | Array | Products most added to cart by count | | `${tools.global.products.cartValue()}` | Array | Products most added to cart by currency | | `${tools.global.products.purchaseCount()}` | Array | Top purchased products by count | Ensure that you either iterate over every product in an array ### Global Only | Variable | Type | Description | | ------------------------------------------ | ----- | ------------------------------- | | `${tools.global.products.publishedDate()}` | Array | Most recently published product | ### User Only | Variable | Type | Description | | ----------------------------------------- | ----- | ---------------------------------------------------------------------------------- | | `${tools.user.products.viewed()}` | Array | Most Recently Viewed | | `${tools.user.orders.currentItems()}` | Array | Products in cart. **Note**: Personalization doesn’t currently track cart removals. | | `${tools.user.orders.currentValue()}` | Float | Total cart value. **Note**: Unformatted, see utilities. | | `${tools.user.get('Engagement', 'none')}` | | User's Engagement Score | ## Item Options You can further refine results from query functions by passing them a set of item options, as shown in the following example. ```html $tools.user.products.viewCount(${itemOptions...}) ``` You can specify item options by calling multiple functions on the `itemOptions` object, as shown in the following example. ```html ${itemOptions.optionOne().optionTwo().optionThree()} ``` ### Sizes By default, a message containing advanced dynamic queries renders only if there's at least 1 result. To increase the minimum number of items returned, use `${itemOptions.minItems()}`. To increase the maximum number of items returned, use `${itemOptions.maxItems()}`. **note:** You can’t increase the maximum number of items returned beyond 10. ### Filters You can further filter query results using the following filters. | Filter | Description | | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `${itemOptions.withCategory()}` | Filter results by the category the visitor is currently viewing. | | `${itemOptions.withBrand()}` | Filter results to contain only items with a specific brand. | | `${itemOptions.withBrandAnyOf([,,...])}` | Filter results to contain items from any of the specified brands. | | `${itemOptions.withGender()}` | Filter results by gender. | | `${itemOptions.withGenderAnyOf([,,...])}` | Filter results by any of the specified gender IDs. | | `${itemOptions.withKeyword(,,...])}` | Filter results by any of the specified keywords. | | `${itemOptions.withAuthor(,,...)}` | Filter results by any of the specified authors. | | `${itemOptions.withContentClass()}` | Filter results by `ContentClassID`. | | `${itemOptions.withContentClassAnyOf([,,...])}` | Filter results by any of the specified content classes. | | `${itemOptions.withStyle()}` | Filter results by style. | | `${itemOptions.withStyleAnyOf([,,...])}` | Filter results by any of the specified styles. | | `${itemOptions.withItemClass()}` | Filter results by item class. | | `${itemOptions.withItemClassAnyOf([,,...])}` | Filter results by any of the specified item classes. | | `${itemOptions.whereCreated()}` | Filter results by date or time range of creation. To know more about the available `TimeObject` functions, see [Lookback Period](#lookback-period). | | `${itemOptions.wherePublished()}` | Filter results by published or time range. To know more about the available `TimeObject` functions, see [Lookback Period](#lookback-period). | ### Lookback Period To specify a lookback period apart from the default lookback period of the past week, use: ```handlebars ${itemOptions.time()} ``` The following functions are provided by the `$time` singleton object: | Function | Description | | -------------- | -------------------------------------------------------------------------------- | | `today` | Today | | `thisWeek` | Current calendar week (with Monday as first day of week) | | `thisMonth` | Current calendar month | | `since(date)` | Since the specified date; date format is “mm/dd/yyyy” (for example “07/07/2017”) | | `lastNDays(n)` | Last n days, including today; lastNDays(1) would mean today and yesterday | | `allTime` | Don’t define a specific date range | ### Conditional Options The `itemOptions` object provides several conditional options or methods that you can call to filter or manipulate its properties based on certain conditions. | Conditional Option | Description | | ------------------------------------------- | -------------------------------------------- | | `${itemOptions.whereName()...}` | Filter data based on item name. | | `${itemOptions.whereDescription()...}` | Filter data based on item description. | | `${itemOptions.whereUrl()...}` | Filter data based on item URL. | | `${itemOptions.whereImageUrl()...}` | Filter data based on an item's image URL. | | `${itemOptions.wherePriceDescription()...}` | Filter data based on item price description. | #### Conditionals You can use conditionals with conditional options to further refine the data being filtered. ```handlebars ${itemOptions.whereName().exists()} ``` The following conditionals are available for use. | Conditional | Description | | -------------------------- | -------------------------------------------------------------------------------- | | `exists()` | Checks whether the property being filtered exists. | | `doesNotExist()` | Checks whether the property being filtered doesn’t exist. | | `contains()` | Checks whether the property being filtered contains the specified string. | | `doesNotContain()` | Checks whether the property being filtered doesn’t contain the specified string. | ### Sorting To sort the results for display, use the `displaySort` function. ```handlebars ${itemOptions.displaySort()} ``` To sort results for display in the descending order, use the `descending` function. ```handlebars ${itemOptions.descending()} ``` The `displaySort` function provides the following sort options. | Sort Option | Description | | -------------------------------- | -------------------------------- | | `${productSort.name()}` | Sort products by name. | | `${productSort.price()}` | Sort products by price. | | `${articleSort.name()}` | Sort articles by name. | | `${articleSort.publishedDate()}` | Sort articles by published date. | | `${blogSort.name()}` | Sort blogs by name. | | `${blogSort.publishedDate()}` | Sort blogs by published date. | To sort results for display in the descending order, use the `descending` function. ```handlebars ${itemOptions.descending()} ``` ### Inclusions To include items a visitor is currently viewing in query results, use: ```handlebars ${itemOptions.includeItemsBeingViewed()} ``` To include items in a visitor's cart in query results, use: ```handlebars ${itemOptions.includeItemsInCart()} ``` ### Exclusions To exclude items a visitor has already purchased from query results, use: ```handlebars ${itemOptions.excludePurchasedItems()} ``` To exclude items a visitor has already viewed from query results, use: ```handlebars ${itemOptions.excludeViewedItems()} ``` ## Utilities ### If/Else Statements ```html
Hello $user.attributes.firstName
You don't have a first name at all
``` ### Set Variables ```handlebars #set (${variableName} = $...) ``` ### Loops ```handlebars #foreach (${variableName} in ) ... #end ``` ### Format Price There are two ways to format prices. - By passing the price as a floating-point number to the `formatPrice` method. The `formatPrice` method formats the given number as a price, with the default dataset currency symbol and appropriate decimal formatting, and returns the formatted string. ```handlebars $tools.formatPrice() ``` **important:** In Open Time Email Templates, only the `$currencyTools.formatPrice()` method is available. This method is used instead of is used instead of the `$tools.formatPrice()` and `$tools.formatNumber(, )` methods. - By passing the price as a floating-point number and a fixed number of digits after the decimal point to the `formatNumber` method. ```handlebars $tools.formatNumber(, ) ``` ### Field The `#field` utility enables you to provide a fallback value for an attribute. ```html
#field($item.attributes.testString, "Item does not have a testString attribute")
``` # Advanced Dynamic Message Content Reference Dynamic Message Content (DMC) allows you to insert personalized content into your Marketing Cloud Personalization Open Time Email and Mobile Data campaign messages in real time. When you enable and use DMC, you can insert placeholders into a message that are replaced with the corresponding values for the receiving audience. However, there are cases where DMC placeholders aren’t configurable enough to cater to specific or advanced requirements. In such cases, you can use Advanced Dynamic Message Content (ADMC) syntax to gain greater flexibility and control over the dynamic message content. ADMC offers variables for additional configurability so you can address specific or complex campaign requirements. You can use ADMC to query Personalization to return a specific set of items or content that matches certain criteria, and to further refine your query results. Overall, ADMC helps create engaging messages for campaign audiences, whether providing product recommendations, promoting related content, or offering targeted promotions. **important:** We suggest enlisting the help of experienced developers while using ADMC in your campaigns. If you’re unsure of any requirements or need further assistance with Advanced Dynamic Message Content, [create a support case](https://help.salesforce.com/s/articleView?language=en_US&id=sf.workcom_contact_support.htm&type=5&_ga=2.21226995.990954368.1679247489-1261322914.1674192271). To know more about using ADMC in your campaigns, see [Use Advanced Dynamic Message Content](https://developer.salesforce.com/docs/marketing/personalization/guide/use-advanced-dynamic-message-content.md). ## See Also - [Advanced Dynamic Message Content Cheatsheet](https://developer.salesforce.com/docs/marketing/personalization/guide/advanced-dynamic-message-content-cheatsheet.md) - _Salesforce Help_: [Dynamic Message Content](https://help.salesforce.com/s/articleView?id=sf.mc_pers_mobile_campaign_dynamic_message_content.htm) - _Salesforce Help_: [Open Time Email Campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign.htm) # Advanced Use-Case APIs - [Marketing Cloud Personalization Trends](https://developer.salesforce.com/docs/marketing/personalization/guide/advanced-use-case-apis/is-trends.md) - [Web Surveys](https://developer.salesforce.com/docs/marketing/personalization/guide/advanced-use-case-apis/web-surveys.md) # Arrays Certain properties can be declared as an array, allowing the input to be multi-select. Doing so returns all selected values within an array. | Property Type | Supported As Array | | -------------- | ------------------ | | boolean | No | | string | Yes | | number | Yes | | Color | No | | Complex Object | Yes | | Select | Yes | | Typed List | No | --- ## Simple Types Simple types can be put into multi-value arrays by simply making them arrays. ```typescript names: string[]; ``` ![Array Input](https://developer.salesforce.com/media/interface-template-configuration/ArrayInput.png) String literal types can also be used as multi-value to provide the end user with a set of options of which multiple can be selected. ```typescript names: ("Alpha"|"Beta"|"Gamma"|"Delta")[]; ``` ![Array Select](https://developer.salesforce.com/media/interface-template-configuration/ArraySelect.png) --- ## Arrays of Complex Objects Configurations can also be arrays of complex objects. You can use these arrays to permit the user to create a list of configured objects. When used in combination with the `@tabular` decorator, it is possible to create table structured inputs. ```typescript export class LTVBands { label: string; minimumLtv: number; } export class LifecyclesLifecycle implements GearLifecycle { @tabular() valueBands: LTVBands[]; } ``` ![Array Complex](https://developer.salesforce.com/media/interface-template-configuration/ArrayComplex.png) # Attributes Attributes are custom-configured, type-safe fields that can store additional custom data at various levels in the platform. You can configure attributes for users, accounts, catalog objects, and profile objects. ## Supported Value Types The following table details the value types supported by Attributes. | Value Type | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | String | Text information | | Integer | Whole numbers | | Decimal | Floating point numbers | | Date | Date and time, specifically the JavaScript [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object | | Boolean | `true` or `false` | | null | Attribute present but without an object value | ## Details Attribute collections are keyed off the attribute name/id, as depicted in the following example. ```typescript attributes: { [attributeName: string]: Attribute }; ``` Each `Attribute` contains a `value` and optional `metadata` about the value. ```typescript interface Attribute { readonly value: AttributeValue; readonly metadata?: Readonly; } ``` A value can be of type `string`, `number`, `boolean`, `null`, `Date`, or a (potentially nested) collection containing them. ```typescript type AttributeValue = string | number | boolean | Date | null | AttributeValueArray; interface AttributeValueArray extends ReadonlyArray {} ``` The optional `metadata` can contain detailed information about the value, as depicted in the following example. ```typescript /** * Source: https://github.com/usnistgov/NISTIR-8112/ * * Glossary: * * Attribute Provider (AP) Manages and provides assertions of identity attributes to other relying and federated * parties. * * Relying Party (RP) An entity that relies upon a subject’s authenticator(s) and credentials or an IDP's assertion of a * subject’s identity, typically to process a transaction or to grant access to information or a system. In this case, * the RP is the Personalization app. */ interface ValueMetadata { /** * The name of the entity that issues or creates the initial attribute value. * * The Origin element conveys the name of the entity that established the initial attribute value. This may or * may not be an authoritative entity, or the provider; if, for example, the AP generates the attribute value * through a derivation process, then the AP would be the origin. The key distinction between the origin and the * provider is the act of initially generating, capturing, or provisioning the attribute's value, rather than * just asserting the attribute's value to an RP. * * e.g. CRM, PointOfSaleSystem, FulfillmentSystem, or LoyaltySystem */ origin?: string; /** * The name of the entity that is providing the attribute. * * This specifies the name of the entity that supplies the attribute value to the RP. This does not have to be * the AP itself. This element enables RPs to understand and evaluate the source of the individual attribute * values that may be included in a bundle of attributes. For example, if a full service credential provider * generates an assertion with several identity attributes provided by multiple APs, the {@code provider} * element enables the RP to understand, at a granular level, where each has come from and determine whether or * not that value can be used for access to specific resources. In instances where a single attribute is * asserted directly to the RP, this element may be omitted since the assertion itself will carry the provider * information as well as a certificate or digital signature. * * e.g. CsvUserEtlJob:user-20190528.csv.gz */ provider?: string; /** * the ID of the Personalization gear that updated the attribute, or null if it was not updated by a gear */ gearId?: string; /** * The date and time when the attribute value was last updated */ lastUpdated?: Date; /** * The date and time when the attribute value was last verified as being true and belonging to the specified * individual */ lastVerified?: Date; /** * Metadata relevant or pertaining to the security classification of a given attribute's value */ classification?: Classification; } type Classification = "Sensitive" | "PersonallyIdentifiable" | "NonSensitive"; ``` **important:** The `publishedDate` attribute must be exclusively used for articles and blogs. For other use cases, create a new custom attribute. ## Immutability Attribute TypeScript declarations can almost always contain a `read-only` modifier. Attribute collections are mutable, which means you can add, replace, and remove attributes. However, each attribute is immutable after it is created, ensuring that an attribute value and its optional associated metadata remain in sync. This immutability also means that you must define a value and its optional associated metadata together. Currently, you cannot replace or remove an entire collection of attributes. Instead, you can mutate the existing collection. ## Example Usages ### Creating Attributes ```typescript function mutatingUserAttributes(user: User): void { // Removing delete user.attributes.nameToDelete; delete user.attributes["bracketStyle"]; // Adding user.attributes.someString = { value: "hi" }; user.attributes["someInt"] = { value: 123, metadata: { lastUpdated: new Date(), classification: "NonSensitive" }, }; user.attributes.someDecimal = { value: 123.45 }; user.attributes["someDate"] = { value: new Date(322376400000) }; user.attributes.someBool = { value: false }; user.attributes.someArray = { value: ["one", 2, null, new Date()] }; // Building up complex array to add val = []; val.push("Abigail"); val.push({ cell: "555-1234" }); val.push([2, 4, 6]); val.push({ a: ["b"], c: { d: ["e", ["f"]] } }); val.push([{ one: 2, three: { four: [5, [6, 7]] } }, [8, { nine: 10 }]]); meta = {}; meta.origin = "someOrigin"; meta.lastUpdated = new Date(); user.attributes.complexArray = { value: val, metadata: meta }; } ``` ### Reading Attributes ```typescript // Not fully safe examples: function readingUserAttributeValuesUnsafely(user: User): void { // Assuming attribute present and correct value type, simply casting: let str: string = user.attributes.someString.value as string; // Checking attribute present, then assuming correct value type and casting. // Using undefined to represent not-present: let num: number | undefined = !user.attributes.someNum ? undefined : (user.attributes.someNum.value as number); } // Fully safe examples: function readingUserAttributeValuesSafely(user: User): void { // Handling all cases: if (!user.attributes.someString) { // Attribute not present } else if (user.attributes.someString.value === null) { // Attribute value null } else if (typeof user.attributes.someString.value !== "string") { // Attribute value not a string } else { const str: string = user.attributes.someString.value as string; } // Checking attribute present and value type. // Using undefined to represent not-present or undesired-type: let bool: boolean | undefined = user.attributes.someBool && typeof user.attributes.someBool.value === "boolean" ? (user.attributes.someBool.value as boolean) : undefined; // Similar but as an if statement: if (user.attributes.someBool && typeof user.attributes.someBool.value === "boolean") { const bool: boolean = user.attributes.someBool.value as boolean; } // Similar but using guard functions: if (user.attributes.someBool && isBoolean(user.attributes.someBool.value)) { const bool: boolean = user.attributes.someBool.value; } if (user.attributes.someDate && isDate(user.attributes.someDate.value)) { const date: Date = user.attributes.someDate.value; } if (user.attributes.complexArray && isArray(user.attributes.complexArray.value)) { const arr: AttributeValueArray = user.attributes.complexArray.value; if (arr.length > 0 && isNumber(arr[0])) { const num: number = arr[0] as number; } } } // Guard functions: // Currently the platform may provide a Date-like proxy, with getTime() etc. So avoid `instanceof`. function isDate(value: AttributeValue): value is Date { return typeof value === "object" && typeof (value as Date).getTime === "function"; } function isArray(value: AttributeValue): value is AttributeValueArray { return Array.isArray(value); } function isBoolean(value: AttributeValue): value is boolean { return typeof value === "boolean"; } function isString(value: AttributeValue): value is string { return typeof value === "string"; } function isNumber(value: AttributeValue): value is number { return typeof value === "number"; } function isNull(value: AttributeValue): value is null { return value === null; } ``` # Audit Log API The Marketing Cloud Personalization Audit Log API enables you to programmatically retrieve access and activity logs from your Personalization instance. You can use the Audit Log API to retrieve logs for specific date ranges up to 50 days in the past. This API provides paginated responses with up to 2000 records per page. Each log record includes information about authentication, API token and user management, campaigns, templates, catalog configurations, segments, and other related events. You can use these logs for security and investigation purposes, and to demonstrate compliance with laws, regulations, and policies. In addition, using a specified timeframe, the API enables you to export audit trail information into your internal systems for easy storage, search, and analysis. ## Method and Endpoint `GET` `/api/audit/list` ## Request URL `https://..evergage.com/api/audit/list` ### URL Parameters | URL Parameter | Required? | Description | | ------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `accountName` | Yes | The Personalization account name. You can find the `accountName` by accessing **Gears** from the Personalization UI and reviewing the URL. For example, if the Gears URL is `https://demo.us-1.evergage.com/`, then the account name is `demo`. | | `instance` | Yes | The Personalization instance identifier. You can find this value by accessing **Gears** from the Personalization UI and reviewing the URL. For example, if the Gears URL is `https://demo.us-1.evergage.com/`, then the instance identifier is `us-1`. | | `page` | No | The specific page of data to return from a paginated response. The `page` parameter is typically used in conjunction with the `pageSize` parameter to control the number of records returned per page. The default value is `1`. To iterate through an entire response, start by setting page to `1`, and continue making requests until you get zero results. | | `pageSize` | No | The number of records to be returned per page in a paginated response. The `pageSize` parameter is typically used in conjunction with the `page` parameter to navigate through a paginated response. The default value is `100`. | | `startDate` | No | The start date of the date range for the audit logs to be returned. If unspecified, the default value is seven days before the current date. The `startDate` must precede the `endDate` and be sent as a string represented in the ISO-8601 format, for example, `2023-03-01T22:00:00.000Z`. | | `endDate` | No | The end date of the audit log date range that you want to retrieve. If unspecified, the default value is the current date. The `endDate` value must follow the `startDate` value, and be sent as a string represented in the ISO-8601 format, for example,`2023-03-01T22:00:00.000Z`. **Note**: You can retrieve logs for a maximum range of 50 days. If the range specified exceeds 50 days, the platform automatically adjusts the range and outputs logs from 50 days before the specified `endDate`. | ## Response Content Type - `application/json` ## Authorization `Authorization: Basic ` The Audit Log API uses API tokens to authenticate requests. Your API token must be a strict Base64 encoding of your API Key ID and its API Secret Key, separated by a colon. For the API token to have access to the Audit Log API, you must enable the **Can access Audit logs** permission when creating or updating a token. For security purposes, we recommend using a dedicated token to access Audit Log API with only this permission enabled. **important:** A token with the **Can access Audit logs** permission doesn’t use dataset restrictions for the Audit Log data. Instead, the token provides access to Audit Log data across all datasets. For more information on API tokens, see [Personalization API Tokens](https://help.salesforce.com/s/articleView?id=sf.mc_pers_api_tokens.htm) on Salesforce Help. When using [cURL](https://curl.se/docs/manpage.html) to interact with the API, you can use the `-u "apiKeyId:apiKeySecret"` command-line option to send your API Key ID and API Key Secret. cURL automatically converts the `apiKeyId:apiKeySecret` pair into a Base64-encoded string and adds `Authorization: Basic ` to the request header. ## Example Request The following is an example API request sent using cURL. ```bash curl -H 'Content-Type: application/json' \ -u ":" \ "https://..evergage.com/api/audit/list?page=1&pageSize=50&startDate=2023-03-01T00:00:00.000Z&endDate=2023-03-10T00:00:00.000Z" ``` ## Example Response ```json { "page": 1, "pageSize": 9, "count": 706, "startDate": "2023-03-10T00:00:00.000Z", "endDate": "2023-03-23T00:00:00.000Z" "items": [ { "date": "2023-03-22T13:01:54.451Z", "tenantId": "my_tenant_id", "employee": { "ipAddress": "2.2.2.2" }, "type": "Authentication", "operation": { "name": "Auth", "detail": [ "LoginSuccess" ] }, "object": { "id": "test@salesforce.com" } }, { "date": "2023-03-22T12:01:52.917Z", "tenantId": "my_tenant_id", "datasetId": "my_dataset_id", "employee": { "id": "test@salesforce.com", "emailAddress": "test@salesforce.com" }, "type": "Campaign Template", "operation": { "name": "Update", "detail": [ "draft-serverSideTypeScript" ] }, "object": { "id": "TFz6G" } }, { "date": "2023-03-22T11:46:28.255Z", "tenantId": "my_tenant_id", "datasetId": "my_dataset_id", "employee": { "id": "test@salesforce.com", "emailAddress": "test@salesforce.com" }, "type": "Campaign Template", "operation": { "name": "Create" }, "object": { "id": "TFz6G" } }, { "date": "2023-03-22T11:12:30.821Z", "tenantId": "my_tenant_id", "datasetId": "my_dataset_id", "employee": { "id": "test@salesforce.com", "emailAddress": "test@salesforce.com" }, "type": "Segment", "operation": { "name": "Update" }, "object": { "id": "EBljH" } }, { "date": "2023-03-22T11:12:04.440Z", "tenantId": "my_tenant_id", "datasetId": "my_dataset_id", "employee": { "id": "test@salesforce.com", "emailAddress": "test@salesforce.com" }, "type": "Segment", "operation": { "name": "Create" }, "object": { "id": "EBljH" } }, { "date": "2023-03-22T11:09:57.150Z", "tenantId": "my_tenant_id", "datasetId": "my_dataset_id", "employee": { "id": "test@salesforce.com", "emailAddress": "test@salesforce.com" }, "type": "Segment", "operation": { "name": "Update" }, "object": { "id": "1zDIZ" } } ] } ``` | Property | Optional | Data Type | Description | | ------------------------------ | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `page` | No | Number | The requested page number. The value of the `page` query parameter you specified in the request, or the default value if no page number was specified. | | `pageSize` | No | Number | The requested page size. The value of the `pageSize` query parameter you specified in the request, or the default value if no page number was specified. | | `count` | No | Number | Number of records for the specified date range. | | `startDate` | No | String | The start date in the ISO-8601 format used when filtering the results. For example: `2023-03-01T22:00:00.000Z`. | | `endDate` | No | String | The end date in the ISO-8601 format used when filtering the results. For example: `2023-03-01T22:00:00.000Z`. | | `items` | No | Array | An array of Audit Log events. | | `.date` | No | String | The event date for a record in the ISO-8601 format. For example: 2023-03-01T22:00:00.000Z.A string representation of the event date in the ISO-8601 format. For example: `2023-03-01T22:00:00.000Z`. | | `.tenantId` | No | String | Your Personalization `accountName`. | | `.datasetId` | Yes | String | The dataset where an event occurred. This property doesn’t exist for global events like authentication. | | `.employee.ipAddress` | Yes | String | The IP address of the user associated with the audit event, if available. | | `.employee.id` | Yes | String | The Marketing Cloud user ID associated with the audit event, if available. | | `.employee.emailAddress` | Yes | String | The Marketing Cloud user email address associated with the audit event, if available. | | `.type` | No | String | Audit Log event type. For example: `API Token`, `Authentication`, `Segment`, `Campaign`, and so on. | | `.operation.name` | No | String | The type of operation performed. Examples of available options: `Create`, `Read`, `Update`, `Delete`, `Auth`, `Execute`, `TestExecute`. | | `.operation.detail` | Yes | Array | A list of optional additional details about the operation performed by the user. | | `.object.id` | Yes | String | A reference to the object that was affected by the logged operation. | ## See Also - _Salesforce Help_: [View the Personalization Audit Log](https://help.salesforce.com/s/articleView?id=sf.mc_pers_audit_log_viewing.htm) - _Salesforce Help_: [Personalization API Tokens](https://help.salesforce.com/s/articleView?language=en_US&id=sf.mc_pers_api_tokens.htm) # B2B Using [Tableau](https://www.tableau.com) as our "client", the following document describes and presents code for an example implementation. The sample code in this document is not exhaustive and maps only key areas of the site that are valuable for demonstration purposes. The primary focus is depicting how to approach designing a Catalog for B2B websites. While each implementation is unique, this sitemap contends with many issues, particularly mapping B2B sites and websites without purchasable products in general. **important:** The sitemap code examples in this article are only for demonstration purposes. Avoid copying sample code into your Sitemap Editor as it is built around example scenarios that can differ from your implementation's goals and use cases. ## Client Requirements This section outlines the functionality that our imaginary client requires from their implementation. Since each client or customer always has a unique set of requirements and use cases in mind for Marketing Cloud Personalization, experience is the real teacher when it comes to knowing what questions to ask clients to help them establish their goals and requirements for their use of Personalization. ### Primary Goal - Understand the content a user is most interested in based on their prior interactions with the site. ### Home page type - Recommend Blogs similar to recently viewed Blogs, fallback to promote trending Blogs. ### Solutions, Learning, and Product Landing page types - Recommend Articles of the same Category (solutions, learn, or products) as the currently viewed landing page. - Personalize banner based on past interest in Articles under the currently viewed Category ### Solutions, Learning, and Product page types - Capture `Industry`, `JobRole`, `PageBundle`, and `Department` values as related catalog objects. ### Product page type - Capture `Keyword` values as a related catalog object. - Recommend related product Articles by `Keyword`. - Distinguish between `Add-On` and `Software` products in the Catalog. ### Blog Landing page type - Recommend trending `Blogs`. ### Blog page type - Recommend related `Blog` posts based on category. ## Discovery - Since it is the only identity consistently available on the web and used as a username when users log in to the site, `emailAddress` is used as the default Web SDK identity attribute used for this implementation. The site already uses email addresses to identify its users, so we are configuring Personalization to piggyback on that identity attribute. > _**Aside**: Since an email address is Personally Identifiable Information (PII), make sure you have the appropriate approval from within your organization before using it as a Personalization Identity. Additionally, using emails captured from web forms, other than a login form, as an Identity could potentially cause privacy issues where more than one user is tied to a Personalization user. In the following code, email is only being captured from the login form when a user successfully logs in. The code capturing user id is designed to work on [Tableau Public](https://public.tableau.com/) as this portion of the Tableau site is accessible by everyone with a free public account. For more information on Identity, read [Identity System Setup for the Web SDK](https://help.salesforce.com/s/articleView?id=sf.mc_pers_identity_web_sdk.htm)._ - While this site does promote products, there is no ability to purchase products or attribute revenue to them from other channels, matching with user activity from the web. Since we do not need the special capabilities of the `Product` item type for this implementation, we are instead using the `Article` item type, using `Category` to tell types of `Article` items apart. > _**Aside**: If the website had a more robust, hierarchical category system in which it could be possible to share `Category` information between the `Article` types, we would instead configure a catalog object specifically to house the type of `Article`, such as `ArticleType`. This way, if solutions and products shared `Category` data, they would be relatable to each other, instead of belonging to siloed `Category` hierarchies, each starting with their respective `Article` type (solutions, products, and learning)._ - There is a categorization system already natively built into the site's blog section, so our code scrapes the categories already present in the `dataLayer` as `Category` in the Personalization Catalog. Collecting `Category` data for the `Blogs` help build user affinity based on those categories. These categories are used to also power the customer's use case to recommend related `Blogs` on blog pages based on the currently viewed `Category`. - In addition to `Category` data, the `dataLayer` contains other useful information, such as `Department` and `Job Role`. The values for `Department` and `Job Role` are collected as `relatedCatalogObjects` to tie them to each of the collected `Article` types. As these values are collected, they are used to build a user's affinity towards them, which can then be used for segmentation and potentially for recommending `Articles` of any `Category` based on one of these related catalog objects, instead of only recommending `Articles` of one type at a time. - To distinguish between `Add-On` and `Software` products in the Catalog, as stated in the "Client Requirements", we are mapping a related catalog object called `ProductType` on the "product" page type. Remember, we are using the `Article` item type to house Catalog data for the `product`, `solutions`, and `learning` page types. As a result, our `ProductType` catalog object is attached to the `Article` Catalog item type when configuring the Catalog, even though it is mapped only on one page type containing `Article` Catalog data. The `ProductType` related catalog object helps to more granularly distinguish which type of product `Article` is being viewed by the user and create highly targeted recipes based on that activity, if desired. **note:** The following examples assume that `emailAddress` has been configured as the default Web SDK Identity. ## Example Sitemap in the SalesforceInteractions Namespace ```javascript SalesforceInteractions.init({ cookieDomain: "tableau.com", consents: [ { purpose: SalesforceInteractions.mcis.ConsentPurpose.Personalization, provider: "Example Consent Manager", status: SalesforceInteractions.ConsentStatus.OptIn, }, ], }).then(() => { const findInDataLayer = (targetAttribute) => { if (!window.dataLayer) { return; } for (let i = 0; i < window.dataLayer.length; i++) { const result = SalesforceInteractions.mcis.getValueFromNestedObject( "window.dataLayer[" + i + "]", ); if (result && result[targetAttribute]) { return result; } } return; }; // We set a value for this variable only once per page load and then reference it several times for the data found in the dataLayer needed for each page. let pageDetails; const setPageDetailsFromDataLayer = () => { pageDetails = pageDetails || findInDataLayer("entityId"); }; /* Here we will first check session storage for the islogin item containing the user's email that is associated with their account. This value is set in the global event listener in the site config when a submission event occurs. On this website, both successful and invalid form submissions emit a submission event, the page also is quickly reloaded after a successful login. In this unusual situation, we will first save the submitted data in session storage. Then when the page loads, we check for the islogin value in session storage and then look for a DOM element which indicates a successful login before retrieving the stored user email and sending our login event. */ let loginEvent = sessionStorage.getItem("islogin"); // if there is a stored login event... if (loginEvent) { // wait for the logged in user dropdown to appear in the nav, signifying a successful login SalesforceInteractions.DisplayUtils.pageElementLoaded( "#block-public-sitewide-ui-author-profile-dropdown", ).then(() => { // remove the stored email from session storage sessionStorage.removeItem("islogin"); // construct and send the Login Success event to Personalization SalesforceInteractions.sendEvent({ interaction: { name: "Login Success" }, user: { /* The users email address provided in the login form is sent to Personalization as user.identities.emailAddress because in this example, emailAddress is configured to be the default identity for the web channel. */ identities: { emailAddress: loginEvent, }, }, }); }); } const config = { global: { contentZones: [ { name: "global_popup" }, { name: "global_infobar" }, { name: "global_exit_intent" }, /* Since the website does not have consistent selectors and structure within the Article pages, this content zone will be used to add recs to the bottom of Article pages. */ { name: "global_footer", selector: "footer.global-footer" }, ], listeners: [ /* Here we are listening for all submission events that happen within this document. This pattern can be used to create generic form submission event handlers, or even just to consolidate them to one place in the sitemap code. */ SalesforceInteractions.listener("submit", "body", (event) => { // Check the id of the event target to check for the login form we want to scrape data from if (event.target.id === "login-form") { // loop through login form fields... for (i = 0; i < event.target.length; i++) { // Find the email field by id within the event object. if (event.target[i].id === "login-email") { // save user email in session storage sessionStorage.setItem("islogin", event.target[i].value); } } } }), ], }, pageTypeDefault: { name: "default", interaction: { name: "Default Page", }, }, pageTypes: [ { name: "home", isMatch: () => /^\/$/.test(window.location.pathname), interaction: { name: "Homepage", }, contentZones: [ { name: "home_recs_1", selector: "section.datalocation-audience-segment-links" }, { name: "home_recs_2", selector: "section.datalocation-customer-stories" }, ], }, { name: "products_landing", isMatch: () => /^\/products\/?$/.test(window.location.pathname), interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { /* Remember, Catalog IDs are case sensitive. Transforming collected values as all upper case or all lower case is a common method for ensuring consistency throughout the site when creating Catalog IDs with the sitemap which do not need to correspond to data in external systems. */ type: "Category", id: () => SalesforceInteractions.mcis .getLastPathComponentWithoutExtension(window.location.pathname) .toLowerCase(), attributes: { url: SalesforceInteractions.resolvers.fromCanonical(), name: SalesforceInteractions.resolvers.fromMeta("og:title"), }, }, }, contentZones: [{ name: "products_landing_hero_banner", selector: "#hero" }], }, { name: "solutions_landing", isMatch: () => /^\/solutions\/?(customers|industries|departments|technologies)?$/.test( window.location.pathname, ), interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { /* The last parameter of each resolver function accepts a function which returns whatever the desired final value is. This is very useful when you need to transform or sanitize a scraped value before sending it to Interaction studio. You can learn more about resolver functions here: https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.html#resolvers Below, the pathname portion of the URL is transformed into a hierarchal Category id. */ type: "Category", id: SalesforceInteractions.resolvers.fromWindow("location.pathname", (path) => path.split("/").slice(1).join("|").toLowerCase(), ), attributes: { url: SalesforceInteractions.resolvers.fromCanonical(), name: SalesforceInteractions.resolvers.fromMeta("og:title"), }, }, }, contentZones: [{ name: "solutions_landing_hero_banner", selector: "#hero" }], }, { name: "learning_landing", isMatch: () => /^\/learn\/?$/.test(window.location.pathname), interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Category", id: () => SalesforceInteractions.mcis .getLastPathComponentWithoutExtension(window.location.pathname) .toLowerCase(), attributes: { url: SalesforceInteractions.resolvers.fromCanonical(), name: SalesforceInteractions.resolvers.fromMeta("og:title"), }, }, }, contentZones: [ { name: "learning_landing_gray_recs", selector: ".entity-paragraphs-item.paragraph--type--cross-reference", }, { name: "learning_landing_hero_banner", selector: "#hero" }, ], }, { name: "blog_landing", isMatch: () => /^\/about\/blog\/?$/.test(window.location.pathname), interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Category", id: () => SalesforceInteractions.mcis .getLastPathComponentWithoutExtension(window.location.pathname) .toLowerCase(), attributes: { url: SalesforceInteractions.resolvers.fromCanonical(), name: SalesforceInteractions.resolvers.fromMeta("og:title"), }, }, }, contentZones: [ { name: "blog_landing_card_wall", selector: ".card-wall" }, { name: "blog_landing_hero_banner", selector: "#hero" }, ], }, { name: "product", action: "Product", isMatch: () => { if (/^\/products\//.test(window.location.pathname)) { /* We only want to call setPageDetailsFromDataLayer() when pages which rely on scraping information from the dataLayer are actually matched. Adding a conditional statement prevents this function from being called on every page on the site as Personalization resolves every isMatch function to see which page type matches the current one. */ setPageDetailsFromDataLayer(); return true; } return false; }, interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { /* Remember, while this site does promote products, there is no ability to purchase products or attribute revenue to them from other channels which would match with user activity from the web. Since the special capabilities of the Product item type will not be needed for this implementation, we will instead use the Article item type, utilizing Category to tell types of Article apart. */ type: "Article", /* We are using SalesforceInteractions.mcis.getValueFromNestedObject() to more easily reference data from the pageDetails object after the value is set when the page matches. */ id: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityId", pageDetails), attributes: { name: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityLabel", pageDetails), url: SalesforceInteractions.resolvers.fromCanonical(), imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute( "div.feature-highlight__image img", "src", ), }, relatedCatalogObjects: { Category: SalesforceInteractions.resolvers.fromWindow("location.pathname", (path) => { const categories = path.split("/").slice(1); categories.pop(); if (categories.length === 0) { const dataLayerCategories = SalesforceInteractions.mcis.getValueFromNestedObject( "page.category1", pageDetails, ); return dataLayerCategories ? [dataLayerCategories.toLowerCase()] : []; } return [categories.join("|").toLowerCase()]; }), ProductType: () => window.location.pathname.indexOf("add-ons") > -1 ? ["Add-On"] : ["Software"], Industry: () => SalesforceInteractions.mcis.getValueFromNestedObject( "dataModelFields.field_dmo_industries", pageDetails, ) || null, Department: () => SalesforceInteractions.mcis.getValueFromNestedObject( "dataModelFields.field_departments", pageDetails, ) || null, JobRole: () => SalesforceInteractions.mcis.getValueFromNestedObject( "dataModelFields.taxonomy_vocabulary_23", pageDetails, ) || null, PageBundle: () => { const pageBundle = SalesforceInteractions.mcis.getValueFromNestedObject( "entityBundleNice", pageDetails, ); return pageBundle ? [pageBundle] : null; }, Keyword: SalesforceInteractions.resolvers.fromMeta("keywords", (ele) => { return ele ? ele.split(/\,\s*/) : null; }), }, }, }, listeners: [ SalesforceInteractions.listener( "submit", "#webform-submission-email-embeddable-1-add-form", (event) => { SalesforceInteractions.sendEvent({ interaction: { name: "Free Trial Download" }, /* A content zone is provided in this event in order to allow a campaign targeting the "global_infobar" content zone to be returned in the response sent back from Personalization with this request. */ source: { contentZones: ["global_infobar"], }, }); }, ), ], }, { name: "solutions", isMatch: () => { if ( /\/solutions\/(?!customers|industries|departments|technologies)/.test( window.location.pathname, ) ) { setPageDetailsFromDataLayer(); return true; } return false; }, interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Article", id: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityId", pageDetails), attributes: { name: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityLabel", pageDetails), url: SalesforceInteractions.resolvers.fromCanonical(), imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute( "div.feature-highlight__image img", "src", ), }, relatedCatalogObjects: { Category: SalesforceInteractions.resolvers.fromWindow("location.pathname", (path) => { const categories = path.split("/").slice(1); categories.pop(); return [categories.join("|").toLowerCase()]; }), Industry: () => SalesforceInteractions.mcis.getValueFromNestedObject( "dataModelFields.field_dmo_industries", pageDetails, ) || null, Department: () => SalesforceInteractions.mcis.getValueFromNestedObject( "dataModelFields.field_departments", pageDetails, ) || null, JobRole: () => SalesforceInteractions.mcis.getValueFromNestedObject( "dataModelFields.taxonomy_vocabulary_23", pageDetails, ) || null, PageBundle: () => { const pageBundle = SalesforceInteractions.mcis.getValueFromNestedObject( "entityBundleNice", pageDetails, ); return pageBundle ? [pageBundle] : null; }, }, }, }, }, { name: "learning", isMatch: () => { if (/^\/learn\//.test(window.location.pathname)) { setPageDetailsFromDataLayer(); return true; } return false; }, interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Article", id: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityId", pageDetails), attributes: { name: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityLabel", pageDetails), url: SalesforceInteractions.resolvers.fromCanonical(), imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute( "div.feature-highlight__image img", "src", ), }, relatedCatalogObjects: { Category: SalesforceInteractions.resolvers.fromWindow("location.pathname", (path) => { const categories = path.split("/").slice(1); categories.pop(); return [categories.join("|").toLowerCase()]; }), Industry: () => SalesforceInteractions.mcis.getValueFromNestedObject( "dataModelFields.field_dmo_industries", pageDetails, ) || null, Department: () => SalesforceInteractions.mcis.getValueFromNestedObject( "dataModelFields.field_departments", pageDetails, ) || null, JobRole: () => SalesforceInteractions.mcis.getValueFromNestedObject( "dataModelFields.taxonomy_vocabulary_23", pageDetails, ) || null, PageBundle: () => { const pageBundle = SalesforceInteractions.mcis.getValueFromNestedObject( "entityBundleNice", pageDetails, ); return pageBundle ? [pageBundle] : null; }, }, }, }, }, { name: "blog", isMatch: () => { if (/\/about\/blog\/\d+\/\d+\/.+/.test(window.location.pathname)) { setPageDetailsFromDataLayer(); return true; } return false; }, interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Blog", id: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityId", pageDetails), attributes: { name: () => SalesforceInteractions.mcis.getValueFromNestedObject("entityLabel", pageDetails), url: SalesforceInteractions.resolvers.fromCanonical(), imageUrl: SalesforceInteractions.resolvers.fromMeta("og:image"), }, relatedCatalogObjects: { Category: () => [ SalesforceInteractions.mcis.getValueFromNestedObject( "flatTaxonomy.blog_categories", pageDetails, ), ], }, }, }, listeners: [ SalesforceInteractions.listener("submit", ".premium-access-ajax", () => { const eloquaId = findInDataLayer("EloquaGuid"); if (eloquaId) { SalesforceInteractions.sendEvent({ interaction: { name: "Tableau Blog Sign-up" }, user: { attributes: { eloquaId: eloquaId } }, }); } }), ], contentZones: [ /* As in this case below, content zones do not necessarily have to denote content to be replaced, they can be useful for inserting template content before or after the the DOM node with the provided selector. */ { name: "blog_text_content", selector: ".field--name-field-page-sections" }, ], }, ], }; SalesforceInteractions.initSitemap(config); }); ``` ## Example Sitemap in the Evergage Namespace ```javascript Evergage.init({ cookieDomain: "tableau.com", }).then(() => { const findInDataLayer = (targetAttribute) => { if (!window.dataLayer) { return; } for (let i = 0; i < window.dataLayer.length; i++) { const result = Evergage.util.getValueFromNestedObject("window.dataLayer[" + i + "]"); if (result && result[targetAttribute]) { return result; } } return; }; // We set a value for this variable only once per page load and then reference it several times for the data found in the dataLayer needed for each page. let pageDetails; const setPageDetailsFromDataLayer = () => { pageDetails = pageDetails || findInDataLayer("entityId"); }; /* Here we will first check session storage for the islogin item containing the user's email that is associated with their account. This value is set in the global event listener in the site config when a submission event occurs. On this website, both successful and invalid form submissions emit a submission event, the page also is quickly reloaded after a successful login. In this unusual situation, we will first save the submitted data in session storage. Then when the page loads, we check for the islogin value in session storage and then look for a DOM element which indicates a successful login before retrieving the stored user email and sending our login event. */ let loginEvent = sessionStorage.getItem("islogin"); // if there is a stored login event... if (loginEvent) { // wait for the logged in user dropdown to appear in the nav, signifying a successful login Evergage.DisplayUtils.pageElementLoaded( "#block-public-sitewide-ui-author-profile-dropdown", ).then(() => { // remove the stored email from session storage sessionStorage.removeItem("islogin"); // construct and send the Login Success event to Personalization Evergage.sendEvent({ action: "Login Success", user: { /* The users email address provided in the login form is sent to Personalization as user.id because in this example, emailAddress is configured to be the default identity for the web channel. */ id: loginEvent, }, }); }); } const config = { global: { contentZones: [ { name: "global_popup" }, { name: "global_infobar" }, { name: "global_exit_intent" }, /* Since the website does not have consistent selectors and structure within the Article pages, this content zone will be used to add recs to the bottom of Article pages. */ { name: "global_footer", selector: "footer.global-footer" }, ], listeners: [ /* Here we are listening for all submission events that happen within this document. This pattern can be used to create generic form submission event handlers, or even just to consolidate them to one place in the sitemap code. */ Evergage.listener("submit", document, (event) => { // Check the id of the event target to check for the login form we want to scrape data from if (event.target.id === "login-form") { // loop through login form fields... for (i = 0; i < event.target.length; i++) { // Find the email field by id within the event object. if (event.target[i].id === "login-email") { // save user email in session storage sessionStorage.setItem("islogin", event.target[i].value); } } } }), ], }, pageTypeDefault: { name: "default", }, pageTypes: [ { name: "home", action: "Homepage", isMatch: () => /^\/$/.test(window.location.pathname), contentZones: [ { name: "home_recs_1", selector: "section.datalocation-audience-segment-links" }, { name: "home_recs_2", selector: "section.datalocation-customer-stories" }, ], }, { name: "products_landing", action: "Products Landing", isMatch: () => /^\/products\/?$/.test(window.location.pathname), catalog: { Category: { /* Remember, Catalog IDs are case sensitive. Transforming collected values as all upper case or all lower case is a common method for ensuring consistency throughout the site when creating Catalog IDs with the sitemap which do not need to correspond to data in external systems. */ _id: () => Evergage.util .getLastPathComponentWithoutExtension(window.location.pathname) .toLowerCase(), url: Evergage.resolvers.fromCanonical(), name: Evergage.resolvers.fromMeta("og:title"), }, }, contentZones: [{ name: "products_landing_hero_banner", selector: "#hero" }], }, { name: "solutions_landing", action: "Solutions Landing", isMatch: () => /^\/solutions\/?(customers|industries|departments|technologies)?$/.test( window.location.pathname, ), catalog: { Category: { /* The last parameter of each resolver function accepts a function which returns whatever the desired final value is. This is very useful when you need to transform or sanitize a scraped value before sending it to Interaction studio. You can learn more about resolver functions here: https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.html#resolvers Below, the pathname portion of the URL is transformed into a hierarchal Category id. */ _id: Evergage.resolvers.fromWindow("location.pathname", (path) => path.split("/").slice(1).join("|").toLowerCase(), ), url: Evergage.resolvers.fromCanonical(), name: Evergage.resolvers.fromMeta("og:title"), }, }, contentZones: [{ name: "solutions_landing_hero_banner", selector: "#hero" }], }, { name: "learning_landing", action: "Learning Landing", isMatch: () => /^\/learn\/?$/.test(window.location.pathname), catalog: { Category: { _id: () => Evergage.util .getLastPathComponentWithoutExtension(window.location.pathname) .toLowerCase(), url: Evergage.resolvers.fromCanonical(), name: Evergage.resolvers.fromMeta("og:title"), }, }, contentZones: [ { name: "learning_landing_gray_recs", selector: ".entity-paragraphs-item.paragraph--type--cross-reference", }, { name: "learning_landing_hero_banner", selector: "#hero" }, ], }, { name: "blog_landing", action: "Blog Landing", isMatch: () => /^\/about\/blog\/?$/.test(window.location.pathname), catalog: { Category: { _id: () => Evergage.util .getLastPathComponentWithoutExtension(window.location.pathname) .toLowerCase(), url: Evergage.resolvers.fromCanonical(), name: Evergage.resolvers.fromMeta("og:title"), }, }, contentZones: [ { name: "blog_landing_card_wall", selector: ".card-wall" }, { name: "blog_landing_hero_banner", selector: "#hero" }, ], }, { name: "product", action: "Product", isMatch: () => { if (/^\/products\//.test(window.location.pathname)) { /* We only want to call setPageDetailsFromDataLayer() when pages which rely on scraping information from the dataLayer are actually matched. Adding a conditional statement prevents this function from being called on every page on the site as Personalization resolves every isMatch function to see which page type matches the current one. */ setPageDetailsFromDataLayer(); return true; } return false; }, catalog: { /* Remember, while this site does promote products, there is no ability to purchase products or attribute revenue to them from other channels which would match with user activity from the web. Since the special capabilities of the Product item type will not be needed for this implementation, we will instead use the Article item type, utilizing Category to tell types of Article apart. */ Article: { /* We are using Evergage.util.getValueFromNestedObject() to more easily reference data from the pageDetails object after the value is set when the page matches. */ _id: () => Evergage.util.getValueFromNestedObject("entityId", pageDetails), name: () => Evergage.util.getValueFromNestedObject("entityLabel", pageDetails), url: Evergage.resolvers.fromCanonical(), imageUrl: Evergage.resolvers.fromSelectorAttribute( "div.feature-highlight__image img", "src", ), categories: Evergage.resolvers.fromWindow("location.pathname", (path) => { const categories = path.split("/").slice(1); categories.pop(); if (categories.length === 0) { const dataLayerCategories = Evergage.util.getValueFromNestedObject( "page.category1", pageDetails, ); return dataLayerCategories ? [dataLayerCategories.toLowerCase()] : []; } return [categories.join("|").toLowerCase()]; }), relatedCatalogObjects: { // In case you're using 'dimensions' instead of 'relatedCatalogObjects', you can continue to do so as they both function the same way. ProductType: () => window.location.pathname.indexOf("add-ons") > -1 ? ["Add-On"] : ["Software"], Industry: () => Evergage.util.getValueFromNestedObject( "dataModelFields.field_dmo_industries", pageDetails, ) || null, Department: () => Evergage.util.getValueFromNestedObject( "dataModelFields.field_departments", pageDetails, ) || null, JobRole: () => Evergage.util.getValueFromNestedObject( "dataModelFields.taxonomy_vocabulary_23", pageDetails, ) || null, PageBundle: () => { const pageBundle = Evergage.util.getValueFromNestedObject( "entityBundleNice", pageDetails, ); return pageBundle ? [pageBundle] : null; }, Keyword: Evergage.resolvers.fromMeta("keywords", (ele) => { return ele ? ele.split(/\,\s*/) : null; }), }, }, }, listeners: [ Evergage.listener( "submit", "#webform-submission-email-embeddable-1-add-form", (event) => { Evergage.sendEvent({ action: "Free Trial Download", /* A content zone is provided in this event in order to allow a campaign targeting the "global_infobar" content zone to be returned in the response sent back from Personalization with this request. */ source: { contentZones: ["global_infobar"], }, }); }, ), ], }, { name: "solutions", action: "Solutions", isMatch: () => { if ( /\/solutions\/(?!customers|industries|departments|technologies)/.test( window.location.pathname, ) ) { setPageDetailsFromDataLayer(); return true; } return false; }, catalog: { Article: { _id: () => Evergage.util.getValueFromNestedObject("entityId", pageDetails), name: () => Evergage.util.getValueFromNestedObject("entityLabel", pageDetails), url: Evergage.resolvers.fromCanonical(), imageUrl: Evergage.resolvers.fromSelectorAttribute( "div.feature-highlight__image img", "src", ), categories: Evergage.resolvers.fromWindow("location.pathname", (path) => { const categories = path.split("/").slice(1); categories.pop(); return [categories.join("|").toLowerCase()]; }), relatedCatalogObjects: { Industry: () => Evergage.util.getValueFromNestedObject( "dataModelFields.field_dmo_industries", pageDetails, ) || null, Department: () => Evergage.util.getValueFromNestedObject( "dataModelFields.field_departments", pageDetails, ) || null, JobRole: () => Evergage.util.getValueFromNestedObject( "dataModelFields.taxonomy_vocabulary_23", pageDetails, ) || null, PageBundle: () => { const pageBundle = Evergage.util.getValueFromNestedObject( "entityBundleNice", pageDetails, ); return pageBundle ? [pageBundle] : null; }, }, }, }, }, { name: "learning", action: "Learning", isMatch: () => { if (/^\/learn\//.test(window.location.pathname)) { setPageDetailsFromDataLayer(); return true; } return false; }, catalog: { Article: { _id: () => Evergage.util.getValueFromNestedObject("entityId", pageDetails), name: () => Evergage.util.getValueFromNestedObject("entityLabel", pageDetails), url: Evergage.resolvers.fromCanonical(), imageUrl: Evergage.resolvers.fromSelectorAttribute( "div.feature-highlight__image img", "src", ), categories: Evergage.resolvers.fromWindow("location.pathname", (path) => { const categories = path.split("/").slice(1); categories.pop(); return [categories.join("|").toLowerCase()]; }), relatedCatalogObjects: { Industry: () => Evergage.util.getValueFromNestedObject( "dataModelFields.field_dmo_industries", pageDetails, ) || null, Department: () => Evergage.util.getValueFromNestedObject( "dataModelFields.field_departments", pageDetails, ) || null, JobRole: () => Evergage.util.getValueFromNestedObject( "dataModelFields.taxonomy_vocabulary_23", pageDetails, ) || null, PageBundle: () => { const pageBundle = Evergage.util.getValueFromNestedObject( "entityBundleNice", pageDetails, ); return pageBundle ? [pageBundle] : null; }, }, }, }, }, { name: "blog", isMatch: () => { if (/\/about\/blog\/\d+\/\d+\/.+/.test(window.location.pathname)) { setPageDetailsFromDataLayer(); return true; } return false; }, catalog: { Blog: { _id: () => Evergage.util.getValueFromNestedObject("entityId", pageDetails), name: () => Evergage.util.getValueFromNestedObject("entityLabel", pageDetails), url: Evergage.resolvers.fromCanonical(), imageUrl: Evergage.resolvers.fromMeta("og:image"), categories: () => [ Evergage.util.getValueFromNestedObject("flatTaxonomy.blog_categories", pageDetails), ], }, }, listeners: [ Evergage.listener("submit", ".premium-access-ajax", () => { const eloquaId = findInDataLayer("EloquaGuid"); if (eloquaId) { Evergage.sendEvent({ action: "Tableau Blog Sign-up", user: { attributes: { eloquaId: eloquaId } }, }); } }), ], contentZones: [ /* As in this case below, content zones do not necessarily have to denote content to be replaced, they can be useful for inserting template content before or after the the DOM node with the provided selector. */ { name: "blog_text_content", selector: ".field--name-field-page-sections" }, ], }, ], }; Evergage.initSitemap(config); }); ``` ## See Also - [_Salesforce Help_: Identity System Setup for the Web SDK](https://help.salesforce.com/s/articleView?id=sf.mc_pers_identity_web_sdk.htm) # Best Practices for Data Capture Using Site Mapping After reviewing all relevant sitemap documentation, move your sitemap from your test dataset to your production environment only after thoroughly testing and validating it. Additionally, the information in this document can prove helpful to reference throughout each phase of implementation. As a final validation for working on a sitemap, we suggest working through the following steps. The checklists laid out in this document are by no means comprehensive. Instead, they call out common areas where implementors run into trouble while writing sitemap code. --- ## In this article - [Validate the JavaScript](#validate-the-javascript) - [Checklist](#checklist) - [Use the browser extension to test the sitemap code](#use-the-browser-extension-to-test-the-sitemap-code) - [Checklist](#checklist-1) - [sendEvent function usage](#sendevent-function-usage) - [Single Page App (SPA) sites](#single-page-app-spa-sites) - [Timing issues](#timing-issues) - [Data Capture Sources](#data-capture-sources) - [Window Objects](#window-objects) - [Meta Tags](#meta-tags) - [itemprop](#itemprop) - [JSON-LD](#json-ld) - [URLs](#urls) - [Page Source Code](#page-source-code) - [Example](#example) - [DisplayUtils](#displayutils) - [util.resolveWhenTrue](#utilresolvewhentrue) - [Additional Best Practices](#additional-best-practices) --- ## Validate the JavaScript ### Checklist - Check for common JavaScript errors by reviewing the sitemap code in the sitemap code editor, found in the Personalization Visual Editor. The sitemap code editor helps identify and highlight errors in your code. You can also validate your code by visually spot-checking it for errors. - Depending on your chosen SDK namespace, use `SalesforceInteractions.cashDom` or `Evergage.cashDom` for each implementation instead of using jQuery or another library that the Personalization platform does not include (such as Knockout). Since the Personalization Beacon includes CashDom (the [`cash`](https://github.com/fabiospampinato/cash#documentation) library), code written using CashDom remains unaffected by updates made to JS libraries found on the client site, thus allowing for greater stability over time. - Ensure that the sitemap does not manipulate the DOM in any way; that is the function of the Campaigns and Templates system. For more information on using these features on your Web integration, see [About Web Campaigns and Templates](https://help.salesforce.com/s/articleView?id=sf.mc_pers_web_campaign.htm) on Salesforce Help. - When using custom code to supply dates to the backend, ensure the custom code instantiates dates using `new Date(Date.UTC(YYYY, MM, DD))` instead of `new Date(YYYY, MM, DD)`. Using `new Date(YYYY, MM, DD)` can introduce errors due to differences between an end user's timezone and the way dates are stored as UTC. - Whenever possible, avoid using resolvers in situations where a direct value needs to be ingested since resolvers are designed to return a function. For example, the following examples depict improper use of a resolver. ```javascript var someValue = SalesforceInteractions.resolvers.fromSelector(".someCssSelector"); ``` ```javascript var someValue = Evergage.resolvers.fromSelector(".someCssSelector"); ``` ## Use the browser extension to test the sitemap code - Injecting or forcing the SDK URL to test the sitemap code. - [Install the Salesforce Interactions SDK Launcher](https://help.salesforce.com/s/articleView?id=sf.mc_pers_web_campaign_interactions_sdk_launcher.htm) - If you followed the best practices detailed in the [Plan a Web Integration](https://developer.salesforce.com/docs/marketing/personalization/guide/web-integration-planning.md) document, you can validate your sitemap code against an available test dataset. - After your test dataset runs on the site, click through the mapped pages and check to see that the events are sending as expected. - Refer to our documentation on [Event Validation Using Chrome Browser Developer Tools](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-event-validation.md#event-validation-using-chrome-browser-developer-tools). ### Checklist - No errors in the **Network** tab for each event. - Mapped attributes are appearing as expected in the events. - Ensure that the [Sitemap Implementation](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.md) and any [Event API Request](https://developer.salesforce.com/docs/marketing/personalization/event-api/event-options-salesforceinteractions.md) objects in your code are appropriately structured and collect values of the correct type for each option, as described in the documentation for [Attributes](https://developer.salesforce.com/docs/marketing/personalization/data-modeling/attributes.md). - When collecting `Category` data associated with another item type (`Article`, `Product`, or `Blog`), ensure that the structure and case of category IDs are _exactly_ consistent on all catalog items mapped with categories. - Carefully validate category ID structures. When mapping the `Category` item type directly, ensure that the value of `id` (or `_id` in the `Evergage` namespace) in the `pageType` is _always_ supplied as a string. Additionally, you can construct a hierarchy of Categories by using `|` separated values to denote each level of the `Category` hierarchy. Ideally, these values are normalized in some way with your sitemap as they are collected. Normalizing these values helps ensure consistency within your catalog. For example, the Personalization platform considers a `Category` with a "Green Hats" ID different from a Category with a "Green hats" ID. In the following example `Category` ID, all the principles and best practices concerning `Category ID` structure have been used: `main-category|sub-cat-1|sub-cat-2`. - Check whether listeners are binding correctly on each page and are successfully sending events without errors or incorrectly formatted data. For example, if a sitemap has a click listener that listens for a button click to send an `AddToCart` event, ensure that clicking that button sends an `AddToCart` event with all the appropriate `lineItem` data successfully. - Validate that `onActionEvent` is being used correctly. While it is possible, we strongly discourage the use of event listeners and/or the `sendEvent` function in `onActionEvent`. However, if you do decide that you must use `sendEvent` here, take extra care to make sure that your code includes an exit case. Otherwise, you risk the possibility of triggering an infinite loop. Ensure that `onActionEvent` is not being used to overwrite the contents of the event, instead only to manipulate it and/or append additional data. Additionally, make sure that `onActionEvent` always returns the `ActionEvent`, even if no transformations and/or modifications are made. You can find examples of properly using `onActionEvent` to ingest user attributes in the [Capturing User and Account Attributes](https://developer.salesforce.com/docs/marketing/personalization/guide/capturing-user-account-attributes.md) page. - Validate that your code is using supported and correctly formatted fields when mapping Page Types. The supported fields are outlined in the [Page Types](https://developer.salesforce.com/docs/marketing/personalization/guide/page-types.md) documentation. - Ensure that your mapped content zones can serve the customer's use-cases. Refer to the [Content Zones documentation that outlines many commonly mapped content zones to get some ideas. ## sendEvent function usage Depending on the SDK namespace you use, the following `sendEvent` function is used to send an Event to Personalization. Ensure that events sent using this function strictly follow the structure of the Event API requests, as outlined in the [Event API](https://developer.salesforce.com/docs/marketing/personalization/event-api/event-api.md) documentation. ```javascript SalesforceInteractions.sendEvent; ``` ```javascript Evergage.sendEvent; ``` **note:** While very similar, the Event API and page types in the sitemap _do_ have different structures. ## Single Page App (SPA) sites For more information about handling SPA page changes, refer to the [Single Page App (SPA) Handling](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.md#single-page-app-spa-handling) documentation. While using an interval powers the documented example on that page, you can use a different mechanism to monitor the site for page changes at your discretion. ## Timing issues - Do selectors work in the console or editor, but the sitemap is still unsuccessful in scraping those values when sending the event? - The location of the data you are scraping probably does not exist at the time the page matches. - Solution: - Write a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) for the `isMatch` function that matches the page type, which relies on data not available precisely at page load. The page is matched after the `Promise` resolves, that resolution being delayed until the source of your data exists in the DOM or `window` object. **note:** If you do decide to use a Promise, ensure that you include a mechanism that resolves the Promise after a set period, regardless of whether the match condition has been met. - Does the catalog configuration work in the editor, but the sitemap is still unsuccessful in scraping those values when sending the event? - The catalog configuration values are being evaluated when the sitemap is parsed and not when the `pageType` is matched. - Solution: - Be sure to use custom functions or [`resolvers`](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.md#resolvers) for catalog configuration values. If you do not use functions or resolvers, these fields are evaluated when the browser parses the sitemap object, not when the `pageType` is evaluated. - For example: ```javascript description: () => window.product.description; ``` ## Data Capture Sources When deciding on what to capture, first **consider all possible locations where the data is available on the page for that `pageType`**. While testing a new data source, ensure that the method of obtaining the data persists throughout the Page Type and that the data is available when the sitemap executes. For synchronous integrations, this could mean using a `Promise` to resolve the `isMatch` function when data is available or using `Promises` for the catalog ID field until the available data is available to capture. ### Window Objects Data found within window objects (that is, `window.dataLayer`) tends to be more static and reliable, though be careful about using a third-party vendor. Sites correctly implemented with Shopify, Magento, and Drupal tend to be more reliable than others. Taken from the [ecommerce example](https://developer.salesforce.com/docs/marketing/personalization/guide/example-sitemaps/ecommerce.md), we are able to extract the product details found inside the ecommerce data object in the following examples. ```javascript const getProductsFromDataLayer = () => { if (window.dataLayer) { for (let i = 0; i < window.dataLayer.length; i++) { if ( ((window.dataLayer[i].ecommerce && window.dataLayer[i].ecommerce.detail) || {}).products ) { return window.dataLayer[i].ecommerce.detail.products; } } } }; ``` ```javascript const getProductsFromDataLayer = () => { if (window.dataLayer) { for (let i = 0; i < window.dataLayer.length; i++) { if ( ((window.dataLayer[i].ecommerce && window.dataLayer[i].ecommerce.detail) || {}).products ) { return window.dataLayer[i].ecommerce.detail.products; } } } }; ``` You can use the `SalesforceInteractions.resolvers.fromWindow` and `SalesforceInteractions.mcis.getValueFromNestedObject` functions in the `SalesforceInteractions` namespace and `Evergage.resolvers.fromWindow` and `Evergage.util.getValueFromNestedObject` functions in the `Evergage` namespace to return a value from a nested object safely. For example, when capturing data from `window.dataLayer[0].ecommerce.detail.products`, you could encounter an error if one part of the path is not defined. The `SalesforceInteractions.resolvers.fromWindow` function ( the `Evergage.resolvers.fromWindow` function in the `Evergage` namespace) safely navigates the given path to return a value if it exists or `null` if the nested object, or some object in the path, does not exist. ### Meta Tags If the data exists within the meta tag and the DOM, meta tag data is a reliable data format. Personalization has a built-in resolver function in the `SalesforceInteractions` and `Evergage` namespaces to retrieve data from meta tags: ```javascript SalesforceInteractions.resolvers.fromMeta(); ``` ```javascript Evergage.resolvers.fromMeta(); ``` As an example, say a hypothetical website selling widgets contains the following meta tag for description. ```html ``` In a catalog configuration, you can retrieve the description content of that tag with the following code. ```javascript description: SalesforceInteractions.resolvers.fromMeta("description"); ``` ```javascript description: Evergage.resolvers.fromMeta("description"); ``` `description` now has the string value: `The premier place for all your widget needs.` ### itemprop The `itemprop` attribute is used to add properties to an element on the page, describing the content in the element. To learn more about the itemprop attribute, refer to the [itemprop](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/itemprop) documentation on the [Mozilla Developer Network](https://developer.mozilla.org/en-US/). Depending on the SDK namespace you're using, Personalization has the following built-in resolver function to retrieve data from elements with a specified `itemprop`. This resolver function returns the content of the first matching element with the given `itemprop`. ```javascript SalesforceInteractions.resolvers.fromItemProp; ``` ```javascript Evergage.resolvers.fromItemProp; ``` For example, when trying to capture the rating on a product, you could come across a page that contains the following element: ```html ``` In a catalog configuration, you can retrieve the rating value of that element with the following code. ```javascript rating: SalesforceInteractions.resolvers.fromItemProp("ratingValue"); ``` ```javascript rating: Evergage.resolvers.fromItemProp("ratingValue"); ``` `rating` now has the value `"5"` ### JSON-LD JSON-LD is a popular way to annotate and describe elements on a page with structured data. Personalization has the following built-in resolver function to parse and retrieve data from a JSON-LD element. This resolver function parses the first JSON-LD script on the page. If given a `path`, it returns the value of the path from the parsed JSON-LD object. ```javascript SalesforceInteractions.resolvers.fromJsonLd; ``` ```javascript Evergage.resolvers.fromJsonLd; ``` For example, if a site had the following element: ```html ``` In the catalog configuration, you can retrieve the entire object using the preceding resolver function. Alternatively, if you only wanted a single element such as `name`, use the resolver function as shown in the following examples to retrieve the entire object. ```javascript SalesforceInteractions.resolvers.fromJsonLd("name"); ``` ```javascript Evergage.resolvers.fromJsonLd("name"); ``` ### URLs A page's URL can contain helpful information that you'd like to capture. For example, you can capture a Product URL directly from the URL the user is visiting using the following resolver function. ```javascript SalesforceInteractions.resolvers.fromHref; ``` ```javascript Evergage.resolvers.fromHref; ``` Some pages can also contain a canonical link element that is the preferred URL of the page. You can capture canonical URLs using the following resolver function. ```javascript SalesforceInteractions.resolvers.fromCanonical; ``` ```javascript SalesforceInteractions.resolvers.fromCanonical; ``` ### Page Source Code Sometimes the only source of data is the DOM itself. Personalization has several helper functions that can extract the data you need. The following table lists the helper functions available in the `SalesforceInteractions` and `Evergage` SDK namespaces that you can use to extract text and attributes from elements in the DOM. | `SalesforceInteractions` | `Evergage` | Description | | ---------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------- | | `SalesforceInteractions.resolvers.fromSelector` | `Evergage.resolvers.fromSelector` | Retrieve data from an element. | | `SalesforceInteractions.resolvers.fromSelectorMultiple` | `Evergage.resolvers.fromSelectorMultiple` | Retrieve data from multiple elements. | | `SalesforceInteractions.resolvers.fromSelectorAttribute` | `Evergage.resolvers.fromSelectorAttribute` | Retrieve attributes from an element. | | `SalesforceInteractions.resolvers.fromSelectorAttributeMultiple` | `Evergage.resolvers.fromSelectorAttributeMultiple` | Retrieve attributes from multiple elements. | #### Example Consider your website's sitemap code has the following element. ```html Worlds Greatest Widget ``` In the catalog configuration, you can retrieve the product ID with the following code. ```javascript id: SalesforceInteractions.resolvers.fromSelectorAttribute("#product", "data-productId"); ``` ```javascript _id: Evergage.resolvers.fromSelectorAttribute("#product", "data-productId"); ``` You can also retrieve the product's name with the following code. ```javascript name: SalesforceInteractions.resolvers.fromSelector("#product"); ``` ```javascript name: Evergage.resolvers.fromSelector("#product"); ``` If you're familiar with [cash](https://kenwheeler.github.io/cash/), you can use `SalesforceInteractions.cashDom` (or `Evergage.cashDom` in the `Evergage` namespace) to retrieve data on the DOM. You can view the list of available utility functions in Evergage.js in the source menu inside the dev tool. ### DisplayUtils When you do have to use a Promise to wait for data to be available, the [web template display utilities](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/web-campaigns-templates/web-template-display-utilities.md) provide methods that are helpful in waiting for specific elements to be on the page. These methods trigger only when the specified element is loaded on the page. For example, if you need to wait for the `#productId` element to be on the page in order to capture the ID of a product, you can use `pageElementLoaded` utility as shown in the following examples. ```javascript id: () => { return SalesforceInteractions.DisplayUtil.pageElementLoaded("#productId", "html").then( (ele) => { return SalesforceInteractions.resolvers.fromSelector("#productId"); }, ); }; ``` ```javascript _id: () => { return Evergage.DisplayUtil.pageElementLoaded("#productId", "html").then((ele) => { return Evergage.resolvers.fromSelector("#productId"); }); }; ``` ### util.resolveWhenTrue When you have to wait for data to be available on the page, you can use the following [utility function](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.md#utility-functions) to bind and unbind processes that return a Promise that resolves when the provided function returns a "truthy" value. ```javascript SalesforceInteractions.util.resolveWhenTrue; ``` ```javascript Evergage.util.resolveWhenTrue; ``` The preceding function also supports a timeout that rejects the Promise if the provided function does not return a "truthy" value within the specified time. For example, if you need to wait for the `window.dataLayer[0].productId` to be available on the page to capture the product ID, you can use the following code. ```javascript id: () => { return SalesforceInteractions.util.resolveWhenTrue.bind(() => { let productId = SalesforceInteractions.mcis.getValueFromNestedObject( "window.dataLayer[0].productId", ); if (productId) { return productId; } else { return false; } }, undefined, 1000, 50); }; ``` ```javascript _id: () => { return Evergage.util.resolveWhenTrue.bind(() => { let productId = Evergage.util.getValueFromNestedObject("window.dataLayer[0].productId"); if (productId) { return productId; } else { return false; } }, undefined, 1000, 50); }; ``` ### Additional Best Practices - **Avoid undefined data** - If your data field depends on a resolver, include it as a return inside a function to avoid sending undefined data. - **Practice console logging** - Get into a good habit of console logging the data in the sitemap. Just because it was available in Chrome Developer Tools doesn’t guarantee its availability at the precise moment that the Personalization sitemap code is executing. Consider the timing when the data is available. - Does it load only after a specific action? - Is it only available when the user is logged in? - Only use console logging when developing a sitemap. Avoid including these statements in a live sitemap. - **Test your data source thoroughly** - Navigate to the page from a different link - Click back and forth on the browser button to ensure the data persists through the change. - Some pages can display differently depending on the referring source. This behavior is especially true for single-page applications. - **Consider backup plans** - If the data is available in two separate areas, consider using the second data source as a backup. - When creating custom helper functions, always assume the value is not available and null check everything. - **Ensure clean data capture** - Trim and clean the values, especially when it comes to IDs. Errant empty spaces can produce duplicate items in the catalog. - **Keep your data capture as simple as possible** - When using selectors, prefer IDs and unique classes over relative selectors if possible. Long chains of child selectors or selectors using `nth-child` may not always return the same element on pages of the same type. - When using selectors on the DOM, reduce the number of selectors to the minimum required to obtain the data. - If the selector is too specific, it could capture data incorrectly and differently from the way it does on other Page Types. - **Double-check data capture from meta tags** - Be careful of meta tags; some sites do not always update the meta tags appropriately. ## See Also - [_Salesforce Help_: Install the Salesforce Interactions SDK Launcher](https://help.salesforce.com/s/articleView?id=sf.mc_pers_web_campaign_interactions_sdk_launcher.htm) - [_Salesforce Help_: About Web Campaigns and Templates](https://help.salesforce.com/s/articleView?id=sf.mc_pers_web_campaign.htm) - [_External Link_: Cash Documentation](https://github.com/fabiospampinato/cash#documentation) # Boolean Property ## Simple Checkbox A basic checkbox input. ```typescript simpleProperty: boolean = true; ``` ![Boolean](https://developer.salesforce.com/media/interface-template-configuration/Boolean.png) In the preceding example, `true` is the value. `false` or an undefined value is also valid. # Campaign Debugger The **Campaign Debugger** tool gives you the details of all Marketing Cloud Personalization activity and data on a page, including: - Actions - Campaigns, both enabled and disabled - Item Data Before you can enable and use the **Campaign Debugger**, your dataset needs the following: - Personalization Tools Gear installed - Beacon Version 16 - Salesforce Interactions SDK Launcher Chrome Extension - A completed Sitemap ## Usage To use the **Campaign Debugger**, do the following. 1. Navigate to the page you want to test 2. Click the **Salesforce Interactions SDK Launcher Chrome Extension** icon to open the menu 3. Select **Campaign Debugger** 4. If the Personalization JavaScript beacon is not installed on the page, you must enable **Inject SDK** to use the Campaign Debugger. 5. If the Personalization JavaScript beacon is installed, and you would like to test another dataset, you should force override the SDK. The URL structure should be: `https://ACCOUNT_NAME.evergage.com/beacon/ACCOUNT_NAME/DATASET_NAME/scripts/evergage.js` 6. After enabling the **Campaign Debugger**, refresh the page and click the blue Personalization tab on the right side of the page. The rest of this article details the specific functionalities of the **Campaign Debugger** including: 1. Campaign Debugging - Allows you to test campaigns and experiences 2. Events - Provides details for the current page event 3. Campaign Responses - Details for the campaigns that did return 4. Campaign Explanations - Explanations for the campaigns that did not return ### Campaign Debugging At the top of the debugger tool you'll see information regarding the account, dataset, beacon version, as well as an input box and toggle for testing campaigns and experiences. If you're interested in testing a particular campaign, type a partial campaign name in this box to see it in the dropdown, even if it's disabled. After it is selected, you're free to begin testing. #### Campaign & Experience Testing There are two modes you can test in using the **Campaign Debugger**: - **Campaign Testing Mode** - Use to simulate unpublished campaign behavior and appearance on your site - **Experience Testing Mode** - Use to test a specific campaign experience **Campaign Testing Mode** If you want to test a campaign to see how would behave on your site for your user account without publishing it: 1. Ensure the **Campaign Debugger** is enabled 2. Select the campaign 3. Enable **Testing Mode** using the toggle switch 4. Click **Apply** 5. You will be redirected to the same page with the query parameter `evergageTestMessages=[CAMPAIGN_ID]` in the URL. This will cause the campaign to act like it's published so you can test changes outside of the campaign editor. You will see whichever campaign experience your user account would qualify for. This testing mode adheres to all of the rules configured for the campaign and experiences. This means that there is a chance your user account is ineligible to see the campaign so you won't see anything displayed on the screen. This does not mean that the campaign is not working, since it may be simply that you didn't qualify to see it either due to campaign or experience rules, or being added to the campaign or experience control group. **Experience Testing Mode** If you want to test a campaign's specific experience: 1. Ensure the **Campaign Debugger** is enabled 2. Select the experience you want to test from the dropdown 3. Enable **Testing Mode** using the toggle switch 4. Click **Apply** 5. This will redirect you to the same page with the query parameter `evergageTestMessages=[EXPERIENCE_ID]` in the URL. Experience Testing Mode behavior will be different from **Campaign Testing Mode** behavior because this mode forces your user account to see the specific experience you selected in step 2 even though you may not naturally have qualified to see it. This is because **Experience Testing Mode** ignores any rules at the campaign and experience levels and forces you into the default test group (i.e. you won't see the control). While this mode may not simulate the exact behavior of the published campaign, it will give you the opportunity to preview aesthetic changes outside of the Visual Editor. **note:** If you don't see the experience with `evergageTestMessages=[EXPERIENCE_ID]` in the URL, there may be some other issue with the campaign. For example, recommendations aren't returning from the server and there's nothing to render. If this happens, the **Campaign Explanations** panel may help elucidate further information. See details below. ### Events This section shows the raw event JSON going to the Personalization server. This includes details like: - User agent - Content zones included on the page - userId of the campaign viewer If there is a mapped item on the page, item data will be shown. This data includes: - name - description - image URL - a link to the item in the Personalization Catalog. ### Campaign Responses This section shows a list of campaigns returned from the server for this event. These are campaigns that were returned as part of the response and (assuming the template client-side code handled the response properly) rendered onto the page. The details included are: - Campaign name - Campaign ID - Specific Experience ID - Number of templates rendered - Whether your user is currently viewing the control or not (Impression Type). ### Campaign Explanations The last section of the **Campaign Debugger** shows which campaigns did not make it back from the server for this event. This can be due to several common issues, including: - The campaign is disabled and not being actively tested using the **Campaign Debugger**. - The server-side template code had an error. - The Content Zone for the campaign is not on this page. - The user doesn't meet the rule criteria to see the campaign. - The items or promotions the campaign intends to return are not be available for this particular configuration. # Campaign Development in Marketing Cloud Personalization After implementing Marketing Cloud Personalization, developers need to shift focus from data ingestion to cross-channel use case execution and enablement. Along with being business user-friendly, Personalization's [Campaign & Templates system](https://developer.salesforce.com/docs/marketing/personalization/guide/campaign-development/web-campaigns-templates.md) also empowers developers to determine what data and what level of flexibility a business user has access to. ## Personalization Campaign Types Personalization campaigns can be broken down into the following four types. - **Web Campaigns:** To support web campaigns, developers are responsible for integrating Personalization with the client's website (or websites) and developing web templates that business users can leverage to build out campaigns, including A/B/n and rules-based tests. **note:** - For information on integrating Personalization with your website, refer to the [Personalization Web Integration documentation](https://developer.salesforce.com/docs/marketing/personalization/guide/web-integration.md). - For information on how to leverage campaign templates, refer to the [Web Templates documentation](https://developer.salesforce.com/docs/marketing/personalization/guide/campaign-development/web-campaigns-templates.md). - **Server-Side Campaigns:** Developers can configure templates specifically for server-side campaigns by leveraging a similar approach as web campaigns. A business user can still control test configuration and setup but collaborates more closely with their development team to leverage Personalization's [Event API](https://developer.salesforce.com/docs/marketing/personalization/guide/event-api/event-api.md) correctly for their campaign execution. **note:** For information on building a Server-Side Campaign from a template, refer to the [Server-Side Templates documentation](https://developer.salesforce.com/docs/marketing/personalization/guide/campaign-development/server-side-campaigns-templates.md) - **Email Campaigns:** Although email campaigns are developed through the Personalization UI, they require some basic knowledge of HTML. Depending on the business users' capabilities, developers could have limited or no involvement in open-time email campaign development. **note:** For information on configuring Open-time Email campaigns, see [Open Time Email Campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign.htm) on the Salesforce Help site. - **Mobile Campaigns:** Personalization can support campaigns for display on mobile devices by using responsive HTML (HTML5) in its standard web templates feature or through integration with native iOS and Android mobile apps. **note:** For information on the Personalization Mobile SDK, refer to the [Personalization Mobile Integrations documentation](https://developer.salesforce.com/docs/marketing/personalization/guide/mobile-integration.md). ## See Also - [_Salesforce Help_: Open Time Email Campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign.htm) # Campaign Template APIs - [Recommendations](https://developer.salesforce.com/docs/marketing/personalization/guide/campaign-template-apis/recommendations.md) - [Corvus AI](https://developer.salesforce.com/docs/marketing/personalization/guide/campaign-template-apis/corvus-ai.md) # Capture an Open Time Email Campaign URL Most (if not all) email marketing solutions allow you to embed URLs within content blocks in email templates to either import images and rich media or link to catalog items or item pages. You can use URLs generated by your Marketing Cloud Personalization OTE campaigns to import item images or link to catalog items within your externally managed email campaigns. You can still [create open time email campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign_create.htm) using the Personalization UI. However, you do not have to copy over all the generated HTML into your email template. Instead, you can cherry-pick item and item image URLs from your OTE campaign and embed them in your email template. This article describes how and where to find URLs within a Personalization OTE campaign to copy into your email marketing system. This article assumes that you have created or cloned an OTE campaign, thoroughly tested it, and have the HTML code for the campaign open in front of you. To know more about viewing the HTML for your OTE campaign, see [Capture Open Time Email Campaign URLs](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign_url.htm). ## Capture URLs from Campaign HTML The following code sample depicts HTML generated by an example Personalization OTE campaign. **note:** - In messages that contain more than one content block (such as this sample campaign), the URL for each block differs by one number only. - The sample code includes the combined code for all the item blocks in the campaign. You can combine all HTML blocks in your campaign by clicking **COMBINE ALL HTML BLOCKS** when viewing the generated HTML in the UI. - The characters used after `userId` in the sample code are for simulation purposes only. Do not copy these characters into your email marketing solution. ```html
``` To copy over URLs from the preceding sample campaign HTML, do the following. 1. Locate the `` and `` tags for each content block in the message. 2. Copy the URL from within each tag and do the following. - Replace the number in the middle of the URL with the corresponding item number in the email template. For example, if you've created a 2x2 grid layout to display four items in your OTE campaign, each item and item image URL is allocated a number based on their position in the grid, starting from 1 to 4. If your email template uses different numbering to indicate the location of items within the layout, replace the number in your OTE campaign URL with the corresponding number in your email template. - Replace the value after `?userId=` in the URL with the macro or tag your email marketing solution provides for dynamically inserting your recipient's email address. 3. Insert the modified URLs into your email marketing solution wherever necessary. ## Example Campaign URLs The following are examples of item (`` tag) and item image (`` tag) URLs captured from the preceding sample campaign HTML. `` in these examples indicates parts of the URL you need to modify before embedding the URL into your email marketing solution. **`` tag or item URL** ```html https://demo.evergage.com/api/dataset/retail/campaign/mhegt/jsv81//detail?userId= ``` **`` tag or item image URL** ```html https://demo.evergage.com/api/dataset/retail/campaign/mhegt/jsv81//summary.png?userId= ``` ## See Also - [_Salesforce Help_: Create an Open Time Email Campaign](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign_create.htm) - [_Salesforce Help_: Capture Open Time Email Campaign URLs](https://help.salesforce.com/s/articleView?id=sf.mc_pers_email_campaign_url.htm) # Capture User and Account Attributes The `onActionEvent` method captures user and account attributes to add user data to an event before sending it to Personalization. For more information on the `user` and `account` objects, see the [user object](https://developer.salesforce.com/docs/marketing/personalization/event-api/event-options-salesforceinteractions.md#the-user-object) and the [account object](https://developer.salesforce.com/docs/marketing/personalization/event-api/event-options-salesforceinteractions.md#the-account-object) documentation. **important:** Account Profiles was retired in August 2024. For more information, see [Personalization Account Profiles Retirement](https://help.salesforce.com/s/articleView?id=001744617&type=1). ## Use onActionEvent in the global Configuration The following are examples of using the `onActionEvent` method in the `global` configuration. ```javascript global: { onActionEvent: (actionEvent) => { const email = SalesforceInteractions.cashDom("header.logged-in-user-email").text().trim(); if (email) { actionEvent.user = actionEvent.user || {}; actionEvent.user.attributes = actionEvent.user.attributes || {}; actionEvent.user.attributes.emailAddress = email; } return actionEvent; }; } ``` ```javascript global: { onActionEvent: (actionEvent) => { const email = Evergage.cashDom("header.logged-in-user-email").text().trim(); if (email) { actionEvent.user = actionEvent.user || {}; actionEvent.user.attributes = actionEvent.user.attributes || {}; actionEvent.user.attributes.emailAddress = email; } return actionEvent; }; } ``` ## Use onActionEvent in the pageTypes Configuration The following are examples of using the `onActionEvent` method in the `pageTypes` configuration. ```javascript pageTypes: [ { name: "search", isMatch: () => /\/search/.test(window.location.pathname), onActionEvent: (actionEvent) => { const searchTerm = SalesforceInteractions.cashDom(".content.searchTerm").text().trim(); if (searchTerm) { actionEvent.user = actionEvent.user || {}; actionEvent.user.attributes = actionEvent.user.attributes || {}; actionEvent.user.attributes.lastSearchTerm = searchTerm; }; return actionEvent; } }, ], ``` ```javascript pageTypes: [ { name: "search", isMatch: () => /\/search/.test(window.location.pathname), onActionEvent: (actionEvent) => { const searchTerm = Evergage.cashDom(".content.searchTerm").text().trim(); if (searchTerm) { actionEvent.user = actionEvent.user || {}; actionEvent.user.attributes = actionEvent.user.attributes || {}; actionEvent.user.attributes.lastSearchTerm = searchTerm; }; return actionEvent; } }, ], ``` ## Setting User and Account Attributes Using Event Listeners In the following code samples, the `onActionEvent` method is used in the `global` configuration. It listens on every page for a form in the footer to be submitted. It then sends an event containing the user's ID and captures the user's email address, which you can then configure for use as an identity. To know more about using captured data as an ID, see the [Event API](https://developer.salesforce.com/docs/marketing/personalization/event-api/event-options-salesforceinteractions.md#the-user-object) documentation. ```javascript global: { listeners: [ SalesforceInteractions.listener("submit", "form.footer", () => { const email = SalesforceInteractions.cashDom("form.footer input.email").val(); const name = SalesforceInteractions.cashDom("form.footer input.name").val(); SalesforceInteractions.sendEvent({ interaction: { name: "Form Submission | Footer", }, user: { identities: { emailAddress: email }, attributes: { name: name } } }); }) ] }, ``` ```javascript global: { listeners: [ Evergage.listener("submit", "form.footer", () => { const email = Evergage.cashDom("form.footer input.email").val(); const name = SalesforceInteractions.cashDom("form.footer input.name").val(); Evergage.sendEvent({ action: "Form Submission | Footer", user: { attributes: { emailAddress: email, name: name } } }); }) ] }, ``` The following examples depict a listener in the `pageTypes` configuration. You can see that the event's structure is the same as the structure of the event sent by the listener in the `global` configuration in the previous namespace-specific examples. ```javascript pageTypes: [ { name: "preferences", isMatch: () => /\/preferences/.test(window.location.pathname), interaction: { name: "Preferences" }, listeners: [ SalesforceInteractions.listener("submit", "form.user-prefs", () => { const email = SalesforceInteractions.cashDom("form.user-prefs input.email").val(); const name = SalesforceInteractions.cashDom("form.user-prefs input.name").val(); SalesforceInteractions.sendEvent({ interaction: { name: "Form Submission | Preferences", }, user: { identities: { emailAddress: email }, attributes: { name: name } } }); }) ] }, ], ``` ```javascript pageTypes: [ { name: "preferences", isMatch: () => /\/preferences/.test(window.location.pathname), listeners: [ Evergage.listener("submit", "form.user-prefs", () => { const email = Evergage.cashDom("form.user-prefs input.email").val(); const name = SalesforceInteractions.cashDom("form.user-prefs input.name").val(); Evergage.sendEvent({ action: "Form Submission | Preferences", user: { attributes: { emailAddress: email, name: name } } }); }) ] }, ], ``` Capturing `account` attributes works the same way as capturing `user` attributes. For the full structure of the `account` object, see the [account object](https://developer.salesforce.com/docs/marketing/personalization/event-api/event-options-salesforceinteractions.md#the-account-object) documentation. ```javascript global: { listeners: [ SalesforceInteractions.listener("submit", "form.survey", () => { const industry = SalesforceInteractions.cashDom("form.survey input.industry").val(); const employeeSize = SalesforceInteractions.cashDom("form.survey input.employeeSize").val(); SalesforceInteractions.sendEvent({ interaction: { name: "Form Submission", }, account: { attributes: { industry: industry, employeeSize: employeeSize } } }); }) ] }, ``` ```javascript global: { listeners: [ Evergage.listener("submit", "form.survey", () => { const industry = Evergage.cashDom("form.survey input.industry").val(); const employeeSize = Evergage.cashDom("form.survey input.employeeSize").val(); Evergage.sendEvent({ action: "Form Submission", account: { attributes: { industry: industry, employeeSize: employeeSize } } }); }) ] }, ``` ## See Also - _Salesforce Help_: [Create a User Profile Object](https://help.salesforce.com/s/articleView?id=sf.mc_pers_user_profile_object_create.htm) # Catalog The Marketing Cloud Personalization platform can model key business objects as objects within a shadow catalog. This configuration happens within the Personalization [catalog setup](https://help.salesforce.com/s/articleView?id=sf.mc_pers_catalog_profile.htm) screens. ## Catalog Objects Catalog objects, such as products or articles, represent key business objects with which a user can interact. You can configure these objects with a name, labels, [custom attributes](https://developer.salesforce.com/docs/marketing/personalization/guide/attributes.md), and related catalog objects. User behavior is tracked against objects as well as against their related catalog objects. ## Attributes Catalog objects and object types can also have attributes that you can customize based on business requirements. You can configure custom attributes for objects in addition to objects that are already available to reflect supplementary information about the object such as price, date added, or image alternate text for products or author, subject, or keyword for blogs and articles. A catalog object's attributes must satisfy the following conditions to be promoted and appear in Personalization Recommendations: - The value of the `archived` attribute must be `false`. - The value of the `promotionState` attribute must not be `Excluded`. - The `expiration` date (if any) must not be before the current date. - The `published` date (if any) must not be after the current date. In addition to satisfying the conditions listed above, all catalog objects must have values for their `name` and `url` attributes. Additionally, objects of the type `Product` must also have a `price` greater than zero, an `inventoryCount` greater than zero, and a value for their `imageUrl` attribute. **important:** The `publishedDate` attribute must be exclusively used for articles and blogs. For other use cases, create a new custom attribute. ## Catalog and Profile Objects Personalization uses Catalog and Profile Objects as foundational components to establish a thorough understanding of a customer's business context and a user's relations to that business context. Catalog Objects help build out an understanding of a customer's business. Additionally, catalog objects can be assigned to other catalog objects as related catalog objects to further describe those objects or define relationships between catalog objects. Examples of catalog object types include products, articles, blogs, promotions/offers, brands, styles, categories, and so on. Profile Objects improve data dimensionality by allowing you to configure and manage customizable objects on a profile in addition to the [profile attributes](https://help.salesforce.com/s/articleView?id=sf.mc_pers_account_profile_attributes_create.htm) already available. You can set up profile objects the same way you configure catalog objects. You can also assign related catalog objects to profile objects. Data stored within a profile object is accessible via a set of out-of-the-box rules, allowing you to leverage this data for insights and cross-channel experience activation. Real-world examples of Profile Object types include Product Registrations, Vehicle Lease, Home Mortgage, Service Cases, and so on. ## Interactions Interactions model a user's interaction with an object. Examples include: - View - Add to Cart - Purchase ## See Also - [_Salesforce Help_: Catalogs and Profiles](https://help.salesforce.com/s/articleView?id=sf.mc_pers_catalog_profile.htm) - [_Salesforce Help_: Create an Account Profile Attribute](https://help.salesforce.com/s/articleView?language=en_US&id=sf.mc_pers_account_profile_attributes_create.htm) # Color Property ## Color Picker A basic color picker input. ```typescript colorProperty: Color; ``` ![Color Picker](https://developer.salesforce.com/media/interface-template-configuration/Color.png) The value returned from the selection contains both the hex value and the RGB value. The object structure is as follows: ```typescript { hex: string; a: number; b: number; g: number; r: number; } ``` # Comparison Shopping Comparison Shopping is a specific type of action that occurs whenever a visitor copies a product name onto their clipboard and then leaves the page. By tracking this action, you can identify visitors who look up a product's price on your website and then compares it with prices of the same or similar products across other websites and online suppliers. While there are a number of ways to implement tracking for comparison shopping behavior for business users of Marketing Cloud Personalization, we highly recommend doing so in the Sitemap for a Personalization dataset. After it is implemented, business users can then track comparison shopping actions either for segmentation or to render campaigns triggered by comparison shopping actions (as long as those actions contain the necessary content zone used by the Template in that Campaign). **note:** To know more about the business uses of Comparison Shopping, see [Comparison Shopping](https://help.salesforce.com/s/articleView?id=mc_pers_web_campaign_comparison_shopping) on Salesforce Help. This article explains how you can implement the tracking of Comparison Shopping actions in a Personalization Sitemap. ## Prerequisites Before implementing Comparison Shopping tracking using the following example as reference, ensure that your Personalization Sitemap is functional and has at least one working `pageType`. For more information, refer to the [Personalization Web Integration](https://developer.salesforce.com/docs/marketing/personalization/web-integration.md) documentation. ### Comparison Shopping as a Listener Although there are other ways to set up a Comparison Shopping action, we advise using a [Listener](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemapping-getting-started.md#listeners). The following are namespace-specific examples of setting up a listener that targets the element containing the product name. ```javascript // definition of SalesforceInteractions.ComparisonShopping SalesforceInteractions.ComparisonShopping = (() => { let productNameCopied = false; const bindComparisonShoppingCopy = (event) => { SalesforceInteractions.cashDom(event.currentTarget).on("copy.evgComparisonShopping", () => { try { productNameCopied = true; } catch (e) { SalesforceInteractions.sendException(e, "bindComparisonShopping: copy event"); } }); }; const bindComparisonShoppingBlur = () => { try { SalesforceInteractions.cashDom(window).on("blur.evgComparisonShopping", () => { if (productNameCopied) { SalesforceInteractions.sendEvent({ interaction: { name: "Comparison Shopping", }, source: { contentZones: SalesforceInteractions.mcis.getSitemapResult().currentPage.source.contentZones, }, }); productNameCopied = false; } }); } catch (e) { SalesforceInteractions.sendException(e, "handleComparisonShopping"); } }; return { handleComparisonShopping: (event) => { SalesforceInteractions.cashDom(window).off(".evgComparisonShopping"); bindComparisonShoppingCopy(event); bindComparisonShoppingBlur(); }, }; })(); SalesforceInteractions.init().then(() => { // sitemap const config = { pageTypes: [ { // ... listeners: [ SalesforceInteractions.listener( "mouseup.initComparisonShopping touchend.initComparisonShopping", ".pdp-primary-info .product-name", SalesforceInteractions.ComparisonShopping.handleComparisonShopping, ), ], }, ], }; // ... }); ``` ```javascript // definition of Evergage.ComparisonShopping Evergage.ComparisonShopping = (() => { let productNameCopied = false; const bindComparisonShoppingCopy = (event) => { Evergage.cashDom(event.currentTarget).on("copy.evgComparisonShopping", () => { try { productNameCopied = true; } catch (e) { Evergage.sendException(e, "bindComparisonShopping: copy event"); } }); }; const bindComparisonShoppingBlur = () => { try { Evergage.cashDom(window).on("blur.evgComparisonShopping", () => { if (productNameCopied) { Evergage.sendEvent({ action: "Comparison Shopping", source: { contentZones: Evergage.getCurrentPage().source.contentZones, }, }); productNameCopied = false; } }); } catch (e) { Evergage.sendException(e, "handleComparisonShopping"); } }; return { handleComparisonShopping: (event) => { Evergage.cashDom(window).off(".evgComparisonShopping"); bindComparisonShoppingCopy(event); bindComparisonShoppingBlur(); }, }; })(); Evergage.init().then(() => { // sitemap const config = { pageTypes: [ { // ... listeners: [ Evergage.listener( "mouseup.initComparisonShopping touchend.initComparisonShopping", ".pdp-primary-info .product-name", Evergage.ComparisonShopping.handleComparisonShopping, ), ], }, ], }; // ... }); ``` **important:** When leveraging a comparison shopping action to trigger a Campaign, the event must include the content zone used by the Template in that Campaign for it to be included in the response from the `sendEvent` call. Otherwise, events that do not include the content zones targeted by the Campaign do not trigger it. ## See Also - [_Salesforce Help_: Comparison Shopping Web Campaigns](https://help.salesforce.com/s/articleView?id=mc_pers_web_campaign_comparison_shopping) # Complex Object ## Complex Types Configurations can be more complicated groups of fields represented by another TypeScript class. Those classes can have multiple properties and use all the same features of the properties in the parent configuration class. ```typescript export class ComplexType { firstProperty: string; secondProperty: number; } export class SimpleTemplate implements CampaignTemplateComponent { myProperty: ComplexType; } ``` ![Complex](https://developer.salesforce.com/media/interface-template-configuration/Complex.png) Complex types defined within gears can be used by Templates. ## See Also - [Select Property](https://developer.salesforce.com/docs/marketing/personalization/guide/select.md) - [Arrays](https://developer.salesforce.com/docs/marketing/personalization/arrays.md) # Content Security Policies and Personalization JavaScript Beacon Directives The Personalization JavaScript beacon currently uses the [`unsafe-eval`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#unsafe_keyword_values) directive, which allows the use of dynamic code evaluation functions such as `eval()`. As part of our ongoing efforts to enhance security standards, we're working on removing the dependency on `unsafe-eval` in the JavaScript beacon. In the meantime, if your organization's security scanner flags the presence of the `unsafe-eval` directive in the JavaScript beacon, include the [`unsafe-inline`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#unsafe_keyword_values) directive in your [Content Security Policy](https://developer.salesforce.com/docs/marketing/personalization/guide/managing-cors-with-sdk-launcher.md##content-security-policy) as a workaround. Add the [`unsafe-inline`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#unsafe_keyword_values) directive to your Content Security Policy, especially if: - your site has a pre-existing Content Security Policy - you're using web templates and have the Handlebars Gear enabled - you haven't already included the `unsafe-inline` directive in your Content Security Policy. **note:** - If you're already using `unsafe-eval` elsewhere, you don't have to immediately stop using it. However, you must include the `unsafe-inline` directive in your Content Security Policy. If your Content Security Policy does not currently have `unsafe-eval` but includes `unsafe-inline`, you should not encounter any issues. - The changes we're making to the JavaScript beacon are designed with backward compatibility in mind. This approach ensures that you have ample time for conducting security reviews and obtaining necessary approvals before making adjustments to your Content Security Policy. ## See Also - _Salesforce Developers_: [Managing CORS with the Salesforce Interactions SDK Launcher](https://developer.salesforce.com/docs/marketing/personalization/guide/managing-cors-with-sdk-launcher.md) - _Salesforce Developers_: [Web Template Handlebars](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/templates/web-template-handlebars.md) - _External Link_: [MDN Web Docs: `Content-Security-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) # Content Zones Content zones are areas throughout a website that you can define to render Personalization campaigns. The web and server-side campaign templates that you create using the Personalization UI reference these content zones within campaigns. You can define content zones globally in the `globalConfig` or for a specific page type in the `pageType` configuration. ## Content Zone Fields This table summarizes the fields that a content zones accepts. | Field Name | Expected Value Type | Required | Description | | ---------- | ------------------- | -------- | ---------------------------------------------------------------------------------------- | | `name` | String | **Yes** | The name of the content zones. Example: `global_popup`, `product_detail_recommendations` | | `selector` | String | No | The CSS selector for the content zones target. | **note:** All content zones require a `name`, while `selector` is optional. You can still use a content zone without a selector in templates that target the glass pane, such as a popup. ## Common Content Zones Depending on the type of website, we recommend defining certain Content Zones over others to use specific global templates. This table outlines content zones that help you use specific global templates without additional content zone configuration if mapped via the sitemap. | Content Zone | Global Template | Description | | ------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------- | | `home_hero` | Banner with Call-to-Action | Content zone for a hero banner on the home page | | `home_recs` | Einstein Content Recommendations | Content zone for recommendations on home page | | `product_detail_recs_row_1` | Einstein Product Recommendations | Content zone for recommendations on a `product_detail` page | | `product_detail_recs_row_2` | Einstein Product Recommendations | Content zone for recommendations on a `product_detail` page | | `category_recs` | Einstein Product Recommendations | Content zone for recommendations on a category page | | `cart_recs_row_1` | Einstein Product Recommendations | Content zone for recommendations on the cart page | | `cart_recs_row_2` | Einstein Product Recommendations | Content zone for recommendations on the cart page | | `search_recs` | Einstein Content Recommendations, Einstein Product Recommendations | Content zone for recommendations on the search page | | `order_confirmation_recs_row_1` | Einstein Product Recommendations | Content zone for recommendations on the order confirmation page | | `order_confirmation_recs_row_2` | Einstein Product Recommendations | Content zone for recommendations on the order confirmation page | | `error_page_recs` | Einstein Product Recommendations | Content zone for recommendations on an error page | | `add_to_cart_modal_recs` | Einstein Product Recommendations | Content zone for recommendations on an Add to Cart Modal | | `global_popup` | Exit Intent Popup with Email Capture, Exit Intent Popup | Content zone for a global popup | | `global_infobar_top_of_page` | Infobar With Call-To-Action, Infobar With User Attribute and Call-To-Action | Content zone for an info bar at the beginning of the page | | `global_infobar_bottom_of_page` | Infobar With Call-To-Action, Infobar With User Attribute and Call-To-Action | Content zone for an info bar at the end of the page | | `global_slide_in` | Slide-In with Call-To-Action | Content zone for slide-in messages with call-to-action | ## See Also For code samples and example content zone implementations, see: - [Get Started with Site Mapping](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemapping-getting-started.md) - [Server-Side Campaigns and Content Zones](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/server-side-campaigns-templates/server-side-campaigns-content-zones.md) - [Modify Global Templates](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/templates/modifying-global-templates.md) - [Example Sitemaps](https://developer.salesforce.com/docs/marketing/personalization/guide/example-sitemaps.md) # Corvus AI The Corvus AI is a service responsible for training the Contextual Bandit leveraged in Einstein Decisions. That service is made available through the Corvus gear, which allows the fetching of Contextual Bandit arms from within the context of a campaign template. ## Usage Import `ContextualBanditConfig` into any template and define a configuration, declaring the content zone, maximum number of promotions returned (up to 12), and explicit fallback promotions to use. Optionally, include a filter on which promotions are eligible for the bandit. Import the `decide` function to be used to request contextual bandit promotions from within the template's `run` method in the server-side TypeScript. The `decide` function adheres to the following type signature. ```typescript decide(campaignComponentContext, contextualBanditConfig, promotionFilter) => Promotion[] ``` ## Example Template The following is an example template that leverages `ContextualBanditConfig`. ```typescript import { ContextualBanditConfig, decide } from "corvus"; export class CorvusTemplate implements CampaignTemplateComponent { run(context: CampaignComponentContext) { const banditConfig: ContextualBanditConfig = { contentZone: context.contentZone, maxResults: 3, fallbackArms: ["arm01"], } as ContextualBanditConfig; const geoPoint: GeographicPoint = { latitude: context.user.location.geographicPoint.latitude, longitude: context.user.location.geographicPoint.longitude, } as GeographicPoint; const geoFence: GeoFence = { radius: 1000, point: geoPoint } as GeoFence; const promotionFilter: PromotionFilter = context.services.promotionCatalog .promotionFilter(banditConfig.contentZone) .withGeoFence(geoFence) as PromotionFilter; return { promotions: decide(context, banditConfig, promotionFilter), }; } } ``` ## Click Tracking To properly train the contextual bandit, it is essential to ensure click log generation. The Einstein Decisions global web template automatically tracks click logs and view logs in web campaigns. However, only view logs are tracked automatically in server-side Einstein Decisions campaigns, while clicks require manual tracking. For instructions on ensuring clicks are correctly tracked, refer to the [Einstein Decisions - Server-Side Template](https://developer.salesforce.com/docs/marketing/personalization/server-side-campaigns-templates/einstein-decisions-server-side-template.md#tracking-click-logs) documentation. # Custom Events The Salesforce Interactions SDK and the Personalization module feature several built-in [CustomEvents](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) that you can use to bind listeners specific to the Web SDK. You can find the data passed by these CustomEvents in the `detail` property of the event object passed to a listener's callback function. ## List of Custom Events The Salesforce Interactions SDK and Personalization module provide the following built-in custom CustomEvents in each of the namespaces. ```javascript SalesforceInteractions.mcis.CustomEvents = { OnEventResponse: "mcis:onEventResponse", OnBeforeEventSend: "mcis:onBeforeEventSend", OnStatSend: "mcis:onStatSend", OnTemplateDisplayEnd: "mcis:onTemplateDisplayEnd", OnInit: "mcis:onInit", }; SalesforceInteractions.CustomEvents = { OnBeforeEventSend: "interactions:onBeforeEventSend", OnClearPersistedIdentities: "interactions:onClearPersistedIdentities", OnConsentRevoke: "interactions:onConsentRevoke", OnException: "interactions:onException", OnInit: "interactions:onInit", OnInitSitemap: "interactions:onInitSitemap", OnPageMatchStatusUpdated: "interactions:onPageMatchStatusUpdated", OnResetAnonymousId: "interactions:onResetAnonymousId", OnSetAnonymousId: "interactions:onSetAnonymousId", OnShutDown: "interactions:onShutDown", OnClearCookie: "interactions:onClearCookie", }; ``` ```javascript Evergage.CustomEvents = { OnEventResponse: "evergage:onEventResponse", OnEventSend: "evergage:onEventSend", OnStatSend: "evergage:onStatSend", OnException: "evergage:onException", OnTemplateDisplayEnd: "evergage:onTemplateDisplayEnd", OnPageMatchStatusUpdated: "evergage:onPageMatchStatusUpdated", OnInit: "evergage:onInit", OnInitSitemap: "evergage:onInitSitemap", OnShutDown: "evergage:onShutDown", OnConsentRevoke: "evergage:OnConsentRevoke", }; ``` ## Example Usage The following code sample depicts how you can use an `mcis:onStatSend` or `evergage:onStatSend` CustomEvent to dispatch a CustomEvent whenever a campaign stat is logged and sent via `Evergage.sendStat`. ```javascript document.addEventListener(SalesforceInteractions.mcis.CustomEvents.OnStatSend, (domEvent) => { console.log(domEvent.detail.campaignStat.stat); // will log "Impression" after the `sendStat` call below }); const stat = { experienceId: "test", control: false, stat: "Impression", }; SalesforceInteractions.mcis.sendStat({ campaignStats: [stat] }); ``` ```javascript document.addEventListener(Evergage.CustomEvents.OnStatSend, (domEvent) => { console.log(domEvent.detail.campaignStat.stat); // will log "Impression" after the `sendStat` call below }); const stat = { experienceId: "test", control: false, stat: "Impression", }; Evergage.sendStat({ campaignStats: [stat] }); ``` --- ## Specifications ### OnEventResponse The `mcis:onEventResponse` or `evergage:onEventResponse` CustomEvent is dispatched whenever an [Event API](https://developer.salesforce.com/docs/marketing/personalization/event-api/event-api.md) request returns a response. This CustomEvent contains the `actionEvent` payload sent to the Event API and any returned campaign data, such as the names and IDs of campaigns and experiences. The `OnEventResponse` CustomEvent contains the following `detail`. ```typescript detail: { response: ActionEventResponse, actionEvent: ActionEvent | undefined } ``` #### ActionEventResponse The `ActionEventResponse` object has the following structure. ```typescript { campaignResponses: CampaignResponse[], compiledCampaignTemplates: any, errorCode: number } ``` #### ActionEvent The `ActionEvent` object has the following structure. ```typescript { action?: string; itemAction?: ItemAction; interaction?: Interaction; user: UserPayload; debug?: DebugPayload; account?: AccountPayload; consents?: Consent[]; flags?: FlagsPayload; source?: SourcePayload; catalog?: CatalogPayload; order?: OrderPayload; cart?: CartPayload; performance?: PerformancePayload; explain?: boolean; pageView?: boolean; time?: number; } ``` --- ### OnEventSend The `mcis:onBeforeEventSend` or `evergage:onEventSend` CustomEvent is dispatched before an event is sent to Personalization. This CustomEvent has the following structure and contains the `actionEvent` payload to be processed in the event pipeline. Both CustomEvents can be canceled to prevent the event from being sent to Personalization. ```typescript detail: { actionEvent: ActionEvent; } ``` #### ActionEvent The `ActionEvent` object has the following structure. ```typescript { action?: string; itemAction?: ItemAction; interaction?: Interaction; user: UserPayload; debug?: DebugPayload; account?: AccountPayload; consents?: Consent[]; flags?: FlagsPayload; source?: SourcePayload; catalog?: CatalogPayload; order?: OrderPayload; cart?: CartPayload; performance?: PerformancePayload; explain?: boolean; pageView?: boolean; time?: number; } ``` The following code example depicts canceling the `OnEventSend` CustomEvent to prevent the event from being sent to Personalization. ```javascript document.addEventListener(SalesforceInteractions.CustomEvents.mcis.OnBeforeEventSend, (event) => { event.preventDefault(); }); ``` ```javascript document.addEventListener(Evergage.CustomEvents.OnEventSend, (event) => { event.preventDefault(); }); ``` --- ### OnException The `interactions:onException` or `evergage:onException` CustomEvent is dispatched when an operation fails and contains information about the error and its context. You can use the `OnException` CustomEvent for debugging and logging runtime errors. The `OnException` CustomEvent contains the following `detail`. ```typescript detail: { error: Error, context: string } ``` #### Error The `Error` object has the following structure. ```typescript { stack?: string; } ``` --- ### OnInit The `mcis:onInit` or `evergage:onInit` CustomEvent is dispatched when the Personalization module of the Salesforce Interactions SDK is initialized. You can use the `OnInit` CustomEvent for debugging [single-page applications (SPA)](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.md#single-page-app-handling) where the beacon and the sitemap usually have to reinitialize without a page load event. The `OnInit` CustomEvent contains the following `detail`. ```typescript detail: { beaconConfig?: Partial } ``` **note:** The `BeaconConfig` object contains internal configuration in order for the Web SDK to run properly. Do not reference these fields as they are subject to change. --- ### OnShutDown The `interactions:onShutDown` or `evergage:onShutDown` CustomEvent is dispatched whenever the Salesforce Interactions Web SDK is shut down and contains the shutdown message with the following `detail`. ```typescript detail: { message: string; } ``` --- ### OnInitSitemap The `interactions:onInitSitemap` or `evergage:onInitSitemap` CustomEvent is dispatched whenever a sitemap is initialized via the `SalesforceInteractions.initSitemap` or `Evergage.initSitemap` function and contains the following `detail`. ```typescript detail: { sitemapConfig: SiteMapConfig; } ``` #### SiteMapConfig The `SiteMapConfig` object has the following structure. ```typescript { currentKey?: string, global: GlobalConfig, pageTypeDefault?: DefaultPageConfig pageTypes: pageTypeConfig[], settings?: object } ``` --- ### OnPageMatchStatusUpdated The `interactions:onPageMatchStatusUpdated` or `evergage:onPageMatchStatusUpdated` CustomEvent is dispatched whenever the match status of a sitemap's page type is updated. The `matchStatus` array accumulates every time an `OnPageMatchStatusUpdated` CustomEvent is dispatched, in the order the match statuses are updated. If the status is `running`, the `PageMatchStatus` includes the start time. Conversely, if the status is `selected`, `rejected`, or `matched`, the `PageMatchStatus` includes the end time. The `selected` page type is the first page type to be matched. The sitemap does not evaluate other `matched` page types. The `OnPageMatchStatusUpdated` CustomEvent contains the following `detail`. ```typescript detail: { matchStatus: PageMatchStatus[] } ``` #### PageMatchStatus The `PageMatchStatus` object has the following structure. ```typescript { pageName: string; status: MatchStatus; startTime?: number; endTime?: number; _reject?: any; } ``` --- ### OnStatSend The `mcis:onStatSend` or `evergage:onStatSend` CustomEvent is dispatched when a stat is tracked and can be used to [send Personalization campaign stats to third-party tracking tools](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/templates/campaign-stats-tracking.md#sending-campaign-stats-to-third-parties), such as Google Analytics and Adobe Analytics. **note:** The `OnStatSend` CustomEvent is dispatched with or without the Campaign Stats Gear. For more information on sending campaign stats with or without the Campaign Stats Gear, refer to the [Campaign Stats Tracking](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/templates/campaign-stats-tracking.md) documentation. The `OnStatSend` CustomEvent contains the following `detail`. ```typescript detail: { campaignStat: CampaignStat, campaignResponse: CampaignResponse } ``` #### CampaignStat The `CampaignStat` object has the following structure. ```typescript { experienceId: string; stat: 'Send' | 'Impression' | 'Click' | 'Dismissal'; control: boolean; catalog?: { [itemTypeKey: string]: string[] }; } ``` #### CampaignResponse The `CampaignResponse` object has the following structure. ```typescript { campaignId: string; campaignJavascriptContent: string; campaignName: string; campaignType: string; experienceId: string; experienceName: string; experienceSourceCode: string; payload: object state: string; templateNames: string[]; type: string; userGroup: string; } ``` --- ### OnTemplateDisplayEnd When enabled, the Handlebars Gear dispatches the `mcis:onTemplateDisplayEnd` or `evergage:onTemplateDisplayEnd` CustomEvent on rendering templates. The Campaign Stats Gear listens for this CustomEvent to track campaign stats. Server-side campaigns require the `OnTemplateDisplayEnd` CustomEvent to be manually dispatched. For instructions on how to dispatch an `OnTemplateDisplayEnd` CustomEvent manually without the Handlebars Gear, refer to the [Web Campaign Stats](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/templates/campaign-stats-tracking.md#use-without-handlebars-and-client-side-code) documentation. The `OnTemplateDisplayEnd` CustomEvent contains the following `detail`. ```typescript detail: { payload: any; } ``` --- ### OnConsentRevoke The `interactions:onConsentRevoke` or `evergage:onConsentRevoke` CustomEvent is dispatched when a Consents status is changed from `Opt In` to `Opt Out`. This CustomEvent contains information about the Consent that was revoked with the following detail. ```typescript detail: { consent: Consent, lastUpdateTime: number, lastSentTime?: number } ``` #### Consent The `Consent` object included in the `OnConsentRevoke` CustomEvent detail is the consent that was updated and has the following structure. ```typescript { purpose: string, provider: string, status: "Opt Out" } ``` --- ## SalesforceInteractions CustomEvents The following CustomEvents are only dispatched on the `SalesforceInteractions` namespace. ### OnBeforeEventSend The `interactions:onBeforeEventSend` CustomEvent is dispatched by the Salesforce Interactions Web SDK before an event is sent to any modules. This CustomEvent contains the event from the Salesforce Interactions Web SDK and the following detail. ```typescript { actionEvent: ActionEvent; } ``` #### ActionEvent The `ActionEvent` object has the following structure. ```typescript { interaction: Interaction; user?: User; account?: Account; source?: Source; consents?: Consent[]; explain?: boolean; pageView?: boolean; time?: number; performance?: PerformanceMetrics; } ``` ### OnSetAnonymousId The `interactions:onSetAnonymousId` CustomEvent is dispatched when an anonymousId is set and contains the following detail. ```typescript { newAnonymousId: string; } ``` --- ### OnResetAnonymousId The `interactions:onResetAnonymousId` CustomEvent is dispatched when the anonymousId is reset through a call to `SalesforceInteractions.resetAnonymousId`. --- ### OnClearCookie The `interactions:onClearCookie` CustomEvent is not dispatched by the Salesforce Interactions Web SDK but can be manually dispatched to clear any cookies set by the Web SDK. This CustomEvent can be used to instruct the Salesforce Interactions Web SDK to remove any cookies it has set. The `interactions:onClearCookie` CustomEvent optionally allows for the following detail where `domain` is the `cookieDomain` used for setting cookies in the Web SDK. ```typescript { options: { domain: string; } } ``` The following code sample depicts dispatching this CustomEvent with a specified domain. ```javascript document.dispatchEvent(new CustomEvent(SalesforceInteractions.CustomEvents.OnClearCookie), { detail: { options: { domain: domain, }, }, }); ``` ## See Also - [_External Link_: CustomEvent Documentation](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) # Custom Interface and Template Configuration The Gears configuration system enables the development of custom interface configurations to users of the Marketing Cloud Personalization UI. With this system, a Gear developer can tailor the setup interface for business requirements. These custom interface configurations can be used in the development of Personalization Campaign Templates as well as configuration screens within the Gears system. ## Configurable types The following Gear types support configuration: - GearLifecycle - SegmentExporter - UserSegmentRule - AccountSegmentRule - ContextualRule - CampaignTemplate The specification of configuration inputs is done by adding properties to the configurable component. Configuration types can also be shared with CampaignTemplate gears so that templates can use the configuration objects from gears providing campaign services. For example, the `RecommendationConfiguration` class provides the user interface for users to configure the use of calls to the recommendation service. ## Configuration Properties Configurable types have child properties that are rendered into the UI in the order they display in the page. Property types define the inputs that are rendered. The following example defines a single text input box with the Title "Simple Property". Titles of form fields are automatically converted from camel case to title case. ```typescript export class SimpleTemplate implements CampaignTemplate { simpleProperty: string; } ``` ![Simple Property](https://developer.salesforce.com/docs/media/interface-template-configuration/SimpleProperty.png) # Data Dictionary ## In This Article - [Table campaign](#table-campaign) - [Table category](#table-category) - [Table country (deprecated)](#table-country-deprecated) - [Table dimension](#table-dimension) - [Table experience](#table-experience) - [Table goal_completion_attribution](#table-goal_completion_attribution) - [Table goal_completion_attribution_promoted_item](#table-goal_completion_attribution_promoted_item) - [Table goal_completion_line_item](#table-goal_completion_line_item) - [Table location (deprecated)](#table-location-deprecated) - [Table order_line_item](#table-order_line_item) - [Table order_line_item_attribute](#table-order_line_item_attribute) - [Table product](#table-product) - [Table product_attribute](#table-product_attribute) - [Table product_category](#table-product_category) - [Table product_dimension](#table-product_dimension) - [Table segment](#table-segment) - [Table segment_membership](#table-segment_membership) - [Table survey_question (deprecated)](#table-survey_question-deprecated) - [Table survey_response (deprecated)](#table-survey_response-deprecated) - [Table user](#table-user) - [Table user_alias](#table-user_alias) - [Table user_attribute](#table-user_attribute) - [Table user_campaign_state](#table-user_campaign_state) - [Table user_click](#table-user_click) - [Table user_daily_stat](#table-user_daily_stat) - [Table user_daily_stat_action](#table-user_daily_stat_action) - [Table user_daily_stat_item_stat_category](#table-user_daily_stat_item_stat_category) - [Table user_daily_stat_item_stat_dimension](#table-user_daily_stat_item_stat_dimension) - [Table user_daily_stat_item_stat_product](#table-user_daily_stat_item_stat_product) - [Table user_daily_stat_item_stat_promotion](#table-user_daily_stat_item_stat_promotion) - [Table user_dismissal](#table-user_dismissal) - [Table user_engagement_history](#table-user_engagement_history) - [Table user_goal_completion](#table-user_goal_completion) - [Table user_impression](#table-user_impression) - [Table user_order](#table-user_order) - [Table user_order_attribute](#table-user_order_attribute) - [Table user_search](#table-user_search) - [Table user_user_visit_lookup](#table-user_user_visit_lookup) - [Table user_visit](#table-user_visit) - [Table user_visit_detail](#table-user_visit_detail) ## Table campaign Campaigns contain experiences you design to personalize the interaction a visitor has with your website or application. | Field Name | Data Type | Description | Example values | | ------------------- | ----------------------------- | ---------------------------------------------------------- | ------------------------------------- | | `id` | `Char(5)` | Unique identifier for a campaign | | | `"type"` | `Varchar(32)` | Campaign type implies marketing channel | `Web`, `MobileData`, `TriggeredEmail` | | `created_ts` | `Timestamp` without time zone | UTC date and time the campaign was created | | | `updated_ts` | `Timestamp` without time zone | UTC date and time the campaign was most recently updated | | | `last_published_ts` | `Timestamp` without time zone | UTC date and time the campaign was most recently published | | | `name` | `Varchar(100)` | Name of the campaign | | | `state` | `Varchar(100)` | Campaign state | `Published`, `Testing`, `Disabled` | ### Indexes - `PRIMARY KEY campaign_pkey ON campaign USING btree (id)` ### Referenced by - `TABLE "experience" CONSTRAINT "experience_campaign_id_fkey" FOREIGN KEY (campaign_id) REFERENCES campaign(id)` - `TABLE "user_campaign_state" CONSTRAINT "user_campaign_state_campaign_id_fkey" FOREIGN KEY (campaign_id) REFERENCES campaign(id)` ### Example Query ```sql /* Find campaigns published in the recent 7 days */ SELECT id, name FROM campaign WHERE last_published_ts >= CURRENT_DATE - 7; ``` [Back to top](#in-this-article) --- ## Table category In a hierarchical catalog, items belong to a category. This table stores the category IDs and names. In Personalization, there’s more than one way to represent a hierarchy. First, categories can be fully specified by the category ID itself (for example, `womensapparel|denim|highrise`). A hierarchy can be derived from the path. Second, you can use the `parent_id` field in the category. Products can belong to many categories. How you build categories depends on your integration, but the data structure supports both approaches. Encoding the hierarchy in the ID is simpler. You can use the parent ID in cases where the hierarchy maybe unavailable, but the short, non-hierarchical ID remains consistent across channels and systems that reference categories. | Field Name | Data Type | Description | Example values | | --------------- | ----------------------------- | ------------------------------------------------------------------------------------------------ | -------------- | | `id` | `Varchar(200)` | Unique identifier for the category | | | `name` | `Varchar(200)` | Name of the category | | | `is_department` | `Boolean` | `true` when the category is a department (a higher-level entity in the category hierarchy) | | | `parent_id` | `Varchar(200)` | Parent category ID of this category | | | `created_ts` | `Timestamp` without time zone | Date and time the category was created in the operational store, UTC | | | `updated_ts` | `Timestamp` without time zone | Date and time of the most recent update in the operation store, UTC (as of the recent DW import) | | ### Indexes - `PRIMARY KEY category_pkey ON category USING btree (id)` ### Referenced by - `TABLE "product_category" CONSTRAINT "product_category_category_id_fkey" FOREIGN KEY (category_id) REFERENCES category(id)` - `TABLE "user_daily_stat_item_stat_category" CONSTRAINT "user_daily_stat_item_stat_category_category_id_fkey" FOREIGN KEY (category_id) REFERENCES category(id)` ### Example Query ```sql /* Get the number of products in each category */ SELECT c.name, count(1) AS num_products FROM product_category pc LEFT OUTER JOIN category c ON c.id = pc.category_id GROUP BY 1 ORDER BY count(1) DESC ``` [Back to top](#in-this-article) --- ## Table country (deprecated) **important:** The `country` dimension table is now deprecated and no longer in use. Country lookup table. Provides `iso_2` and `iso_3` codes and continent information. For more information, refer to [IBAN's list of country ISO codes](https://www.iban.com/country-codes). | Field Name | Data Type | Description | Example values | | ---------------------- | ----------------------------- | ---------------------------------------------------- | -------------------------------------- | | `id` | `Varchar(200)` | Unique identifier for the country | | | `name` | `Varchar(100)` | Name of the country | | | `updated_ts` | `Timestamp` without time zone | Date and time of the most recent update to the table | | | `iso_2_code` | `Char(2)` | 2-character country code | `AW`, `GB`, `US` | | `iso_3_code` | `Char(3)` | 3-character country code | `ABW`, `GBR` | | `has_regions` | `Boolean` | True if the country has regions | | | `continent_code` | `Varchar(100)` | Numeric continent code | `0`, `1`, `2`, `3`, `4`, `5`, `6`, `7` | | `continent_iso_2_code` | `Char(2)` | ISO-2 standard code for a continent | `NA`, `AS`, `EU` | ### Indexes - `PRIMARY KEY country_pkey ON country USING btree (id)` ### Example Query ```sql /* count the number of visits by country */ SELECT c.name, count(1) as num_visits FROM country c LEFT OUTER JOIN user_visit_detail v ON c.id = v.country_code GROUP BY 1 ORDER BY count(1) DESC; ``` [Back to top](#in-this-article) --- ## Table dimension Dimensions are name-value pairs associated with items (usually products). Typical defaults are `brand`, `color`, `style`, and `gender`. Custom dimensions are also available. The combination of (`id`, `dimension_type`) is unique. **note:** As of September 7, 2021, there’s no distinction between items and dimensions. They’re both now known and treated as catalog objects. For more information on catalog objects, refer to the [Catalog Objects documentation](https://developer.salesforce.com/docs/marketing/personalization/data-modeling/catalog.md#catalog-and-profile-objects). | Field Name | Data Type | Description | Example values | | ---------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------- | | `id` | `Varchar(200)` | Identifier for this dimension. The combination of `id` and `dimension_type` uniquely identifies a dimension. | | | `dimension_type` | `Varchar(200)` | Built-in and custom types are supported. Dimension types are capitalized. | `Brand`, `Gender`, `Color`, `Style` | | `name` | `Varchar(200)` | The label used when displaying the dimension | `Gucci`, `Mens` | | `created_ts` | `Timestamp` without time zone | Date and time the dimension was created | | | `updated_ts` | `Timestamp` without time zone | Date and time this dimension was most recently updated | | ### Indexes - `UNIQUE INDEX dimension_id_key ON dimension USING btree (id, dimension_type)` ### Referenced by - `TABLE "product_dimension" CONSTRAINT "product_dimension_dimension_id_fkey" FOREIGN KEY (dimension_id, dimension_type) REFERENCES dimension(id, dimension_type)` ### Example Query ```sql /* Get frequency and recency for stored dimension values */ SELECT dimension_type, count(1) AS num_names, max(updated_ts) AS most_recent_update FROM dimension GROUP BY 1; ``` [Back to top](#in-this-article) --- ## Table experience Campaigns contain experiences. You can create different personalization results within the same campaign using experiences. Suppose you want to create a campaign for first-time visitors to your site. However, you want to show a different message to visitors from Boston and San Francisco where you have upcoming events. In this scenario, you would create 3 experiences in the same campaign: one campaign each for visitors from Boston and San Francisco, and one for everyone else. You can then use rules to restrict each experience's visibility to its respective target group. | Field Name | Data Type | Description | Example values | | -------------- | -------------- | ----------------------------------------- | ------------------------- | | `id` | `Char(5)` | Unique identifier for this experience | | | `name` | `Varchar(100)` | Name of the experience | | | `display_mode` | `Varchar(100)` | Presentation detail about the experience. | `Personalize`, `Redirect` | | `campaign_id` | `Char(5)` | Associated Campaign ID | | ### Indexes - `PRIMARY KEY experience_pkey ON experience USING btree (id)` ### Referenced by - `TABLE "goal_completion_attribution" CONSTRAINT "goal_completion_attribution_experience_id_fkey" FOREIGN KEY (experience_id) REFERENCES experience(id)` - `TABLE "goal_completion_attribution_promoted_item" CONSTRAINT "goal_completion_attribution_promoted_item_experience_id_fkey" FOREIGN KEY (experience_id) REFERENCES experience(id)` - `TABLE "user_click" CONSTRAINT "user_click_experience_id_fkey" FOREIGN KEY (experience_id) REFERENCES experience(id)` - `TABLE "user_dismissal" CONSTRAINT "user_dismissal_experience_id_fkey" FOREIGN KEY (experience_id) REFERENCES experience(id)` - `TABLE "user_impression" CONSTRAINT "user_impression_experience_id_fkey" FOREIGN KEY (experience_id) REFERENCES experience(id)` ### Example Query ```sql /* Get a list of campaigns and experiences that are published */ SELECT c.name AS campaign_name, e.name AS experience_name FROM campaign c INNER JOIN experience e ON c.id = e.campaign_id WHERE c.state = 'Published'; ``` [Back to top](#in-this-article) --- ## Table goal_completion_attribution Stores the timestamp for the campaign goal in UTC. This table is important when reconstructing or analyzing campaign performance. Use the `last_age_seconds` field to decide whether to attribute a goal completion to a campaign. | Field Name | Data Type | Description | Example values | | ------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | | `user_id` | `Varchar(200`) | User who satisfied the goal criteria | | | `goal_ts` | `Timestamp` without time zone | Timestamp of the goal completion in UTC | | | `goal_id` | `Varchar(256)` | One of: segment id or purchase (p) | `p` | | `experience_id` | `Varchar(256)` | The experience associated with the goal completion | | | `user_group` | `Char(1)` | Per experience test (D) and control (C) | `C`, `D` | | `promotion_id` | `Varchar(200)` | The promotion associated with the goal completion | | | `direct_revenue` | `Numeric(18,4`) | Revenue associated with the promoted item of the goal | | | `event_type` | `Varchar(50)` | The event type | `click`, `send`, `impression` | | `first_age_seconds` | `Integer` | First age is the time between the first attributable event and the goal time. | | | `first_time_ts` | `Timestamp` without time zone | Timestamp for the first attributable event, UTC | | | `last_age_seconds` | `Integer` | Last age is the time between the most recent attributable event and the goal time. | | | `last_time_ts` | `Timestamp` without time zone | Timestamp for the most recent attributable event, UTC | | | `browser` | `Varchar(256)` | Client browser application | `Safari`, `Chrome`, `Firefox`, `IE` | | `device` | `Varchar(256)` | Client hardware device | `mobile`, computer, tablet, other | | `engagement` | `Varchar(256)` | Engagement for the associated user | `low`, `medium`, `high` | | `has_purchased` | Boolean | Set when the user makes a purchase | | | `is_anonymous` | Boolean | `true` when the user isn’t named (identified) | | | `is_firsttime` | Boolean | `true` when the completion represents a first encounter with a visitor | | | `os` | `Varchar(256)` | Operating system - x is "macOS X" | `x`, `windows`, `linux`, `ios`, `android`, `other` | | `platform` | `Varchar(256)` | Platform used in the goal completion (email, web) | `Email`, `Web` | | `source` | `Varchar(256)` | For web events, source is `Web`. For customers who use the mobile SDK, each mobile app would be a separate source. | | ### Indexes - `UNIQUE INDEX goal_completion_attribution_user_id_key ON goal_completion_attribution USING btree (user_id, goal_id, goal_ts, experience_id, user_group, event_type)` ### Example Query ```sql /* count the users eligible for goal completion with a 1-day window */ SELECT count(DISTINCT user_id) FROM goal_completion_attribution WHERE experience_id = '{EXPERIENCE_ID}' AND event_type = 'impression' AND last_age_seconds <= 60 * 60 * 24; ``` [Back to top](#in-this-article) --- ## Table goal_completion_attribution_promoted_item If a goal completion has an associated promoted item, you can find it in this table. Use `(user_id, goal_id, goal_ts)` to join back to the other goal tables. | Field Name | Data Type | Description | Example values | | --------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | | `user_id` | `Varchar(200)` | User associated with the goal completion | | | `goal_ts` | `Timestamp` without time zone | Timestamp of the goal completion in UTC | | | `goal_id` | `Varchar(256)` | The goal id stores different value types; for segment goals, the goal id is the segment id; for purchase goals, the goal id is the character `p`. | `p` | | `experience_id` | `Varchar(256)` | Experience associated with the goal | | | `user_group` | `Char(1)` | Per experience control (`C`) and test (`D`) | `C`, `D` | | `item_id` | `Varchar(256`) | Unique identifier for the promoted item | | | `item_type` | `Varchar(256)` | For example, `p` to indicate product | `p` | | `attributes` | `Varchar(256)` | A list of attributes for the promoted item. The underlying data is JSON, stored here as a string. | | | `updated_ts` | `Timestamp` without time zone | UTC Date and time of the most recent update of this promoted item attribution | | | `revenue` | `Numeric(18,4)` | Revenue resulting from the goal completion | | ### Indexes - `UNIQUE INDEX goal_completion_attribution_promoted_item_user_id_key ON goal_completion_attribution_promoted_item USING btree (user_id, goal_id, goal_ts, experience_id, user_group, item_type, item_id)` ### Example Query ```sql /* Get total revenue, users, and products associated with goal completions for an experience */ SELECT sum(revenue) AS total_revenue, count(DISTINCT user_id) AS num_users, count(DISTINCT item_id) AS num_products FROM goal_completion_attribution_promoted_item WHERE experience_id = '{EXPERIENCE_ID}'; ``` [Back to top](#in-this-article) --- ## Table goal_completion_line_item For goal completions that are associated with a purchase, this table stores the line items associated with that purchase. | Field Name | Data Type | Description | Example values | | --------------- | ----------------------------- | ------------------------------------------------------------------------------------- | -------------- | | `user_id` | `Varchar(200)` | User associated with the order | | | `goal_ts` | `Timestamp` without time zone | Timestamp of the goal completion in UTC | | | `goal_id` | `Varchar(256)` | `p` for purchase or a segment id | `p` | | `line_item_num` | `Smallint` | Data warehouse internal enumeration (doesn’t have an analog in the operational store) | | | `item_id` | `Varchar(256)` | Line item | | | `item_type` | `Varchar(256)` | Typically `p` for product | `p` | | `price` | `Numeric(18,4)` | Item price | | | `quantity` | `Smallint` | Item quantity | | ### Indexes - `UNIQUE INDEX goal_completion_line_item_user_id_key ON goal_completion_line_item USING btree (user_id, goal_id, goal_ts, line_item_num)` ### Example Query ```sql /* average revenue per line item in goals */ SELECT avg(price * quantity) FROM goal_completion_line_item; ``` [Back to top](#in-this-article) --- ## Table location (deprecated) **important:** The `location` dimension table is now deprecated and no longer in use. Use this table to de-reference geographical information stored at the visit or user level (for example, `city_code`, `metro_code`, `postal_code`, `latitude`, or `longitude`) and convert it to human-readable labels. | Field Name | Data Type | Description | Example values | | ---------------- | ----------------------------- | --------------------------------------------------------------------------------- | ---------------------- | | `id` | `Varchar(200)` | Unique identifier for the location | | | `latitude` | `Numeric(9,6)` | Latitude associated with the location | `40.7134`, `37.0547` | | `longitude` | `Numeric(9,6)` | Longitude associated with the location | `-111.889`, `-91.5281` | | `postal_code` | `Varchar(10)` | Postal code associated with the location | `02143` | | `city_code` | `Integer` | Code for the city associated with the location | | | `city_name` | `Varchar(100)` | Name of the city associated with the location | | | `metro_code` | `Integer` | Metro code associated with the location. Metro codes for US are Nielsen DMA codes | | | `region_code` | `Varchar(10)` | Region code associated with the location | `UT`, `WI` | | `country_code` | `Varchar(100)` | 2-letter Country code associated with the location | `US`, `IT`, `KR` | | `continent_code` | `Varchar(100)` | 2-letter continent code associated with the location | `NA`, `SA` | | `updated_ts` | `Timestamp` without time zone | UTC date and time of the most recent update to the location | | ### Indexes - `PRIMARY KEY location_pkey ON "location" USING btree (id)` [Back to top](#in-this-article) --- ## Table order_line_item Stores the line items for a given order. Use `(user_id, order_id)` when joining back to order. | Field Name | Data Type | Description | Example values | | ------------------ | ----------------------------- | ------------------------------------------------------------------------------------- | -------------- | | `user_id` | `Varchar(200)` | User id associated with the order | | | `order_id` | `Varchar(256)` | Unique identifier for the order associated with the line item | | | `line_item_num` | `Smallint` | Data warehouse internal enumeration (doesn’t have an analog in the operational store) | | | `item_type` | `Varchar(256)` | Type of item. Usually product. | | | `item_id` | `Varchar(256)` | The item (usually product) associated with the order | | | `price` | `Numeric(18,4)` | Unit price | | | `quantity` | `Smallint` | Item quantity for this line item | | | `currency` | `Varchar(256)` | Currency associated with the order | | | `sku` | `Varchar(256)` | Stock Keeping Unit associated with the line item | | | `order_created_ts` | `Timestamp` without time zone | UTC date and time when the order was created | | ### Indexes - `PRIMARY KEY order_line_item_pkey ON order_line_item USING btree (user_id, order_id, item_id, order_created_ts, line_item_num)` [Back to top](#in-this-article) --- ## Table order_line_item_attribute Provides attribute name-value pairs associated with line items, as well as provenance. | Field Name | Data Type | Description | Example values | | --------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `user_id` | `Varchar(200)` | User associated with the order | | | `order_id` | `Varchar(256)` | Unique order identifier | | | `line_item_num` | `Smallint` | Generated in DW, a 1-based sequence value for this line item | | | `item_type` | `Varchar(256)` | Type type of item | | | `item_id` | `Varchar(256)` | Unique identifier for the associated item | | | `attribute_name` | `Varchar(256)` | The name of the attribute | `quantity_shipped` | | `attribute_value` | `Varchar(256)` | The value of the attribute | | | `value_metadata_origin` | `Varchar(256`) | The name of the entity that issues or creates the initial attribute value. | `CRM`, `PointOfSaleSystem`, `FulfillmentSystem`, `LoyaltySystem` | | `value_metadata_provider` | `Varchar(256)` | The name of the entity that is providing the attribute. | `CsvUserEtlJob:user-20190528.csv.gz` | | `value_metadata_gear_id` | `Varchar(256)` | ID of the Personalization gear that updated the attribute, or null if it wasn’t updated by a gear | | | `value_metadata_last_updated_ts` | `Varchar(256)` | UTC date and time when the attribute was last updated | | | `value_metadata_last_verified_ts` | `Varchar(256)` | UTC date and time when the attribute value was last verified as being true and belonging to the specified individual | | | `value_metadata_classification` | `Varchar(256)` | Metadata relevant or pertaining to the security classification of a given attribute's value | | ### Indexes - `PRIMARY KEY order_line_item_attribute_pkey ON order_line_item_attribute USING btree (user_id, order_id, item_type, item_id, line_item_num, attribute_name)` [Back to top](#in-this-article) --- ## Table product Stores product name and price. | Field Name | Data Type | Description | Example values | | ----------------- | ----------------------------- | ------------------------------------------------------------------------------------- | ------------------------- | | `id` | `Varchar(200)` | Unique identifier for the product | | | `name` | `Varchar(200)` | Name of the product | | | `description` | `Varchar(500)` | Description of the product | | | `list_price` | `Real` | The list price of the product | | | `price` | `Real` | Unit price displayed to an anonymous user when promoting the product | | | `created_ts` | `Timestamp` without time zone | UTC date and time the product created in Personalization system | | | `updated_ts` | `Timestamp` without time zone | UTC date and time the product was most recently updated in the Personalization system | | | `promotion_state` | `Varchar(50)` | Metadata concerning product eligibility and prioritization | `Excluded`, `Prioritized` | | `url` | `Varchar(2048)` | URL for the product | | | `image_url` | `Varchar(2048)` | URL for the product image | | ### Indexes - `PRIMARY KEY product_pkey ON product USING btree (id)` ### Referenced by - `TABLE "product_attribute" CONSTRAINT "product_attribute_product_id_fkey" FOREIGN KEY (product_id) REFERENCES product(id)` - `TABLE "product_category" CONSTRAINT "product_category_product_id_fkey" FOREIGN KEY (product_id) REFERENCES product(id)` - `TABLE "product_dimension" CONSTRAINT "product_dimension_product_id_fkey" FOREIGN KEY (product_id) REFERENCES product(id)` - `TABLE "user_daily_stat_item_stat_product" CONSTRAINT "user_daily_stat_item_stat_product_product_id_fkey" FOREIGN KEY (product_id) REFERENCES product(id)` [Back to top](#in-this-article) --- ## Table product_attribute Stores [attribute data](https://developer.salesforce.com/docs/marketing/personalization/data-modeling/attributes.md) about products, as well as provenance for the attributed value. | Field Name | Data Type | Description | Example values | | --------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `product_id` | `Varchar(200)` | Unique identifier for the product | | | `attribute_name` | `Varchar(256)` | The name of the attribute | | | `attribute_value` | `Varchar(256)` | The value of the attribute | | | `value_metadata_origin` | `Varchar(256)` | The name of the entity that issues or creates the initial attribute value | `CRM`, `PointOfSaleSystem`, `FulfillmentSystem`, `LoyaltySystem` | | `value_metadata_provider` | `Varchar(256)` | The name of the entity that is providing the attribute | `CsvUserEtlJob:user-20190528.csv.gz` | | `value_metadata_gear_id` | `Varchar(256)` | ID of the Personalization gear that updated the attribute, or `null` if it wasn’t updated by a gear | | | `value_metadata_last_updated_ts` | `Varchar(256`) | UTC date and time when the attribute was last updated | | | `value_metadata_last_verified_ts` | `Varchar(256)` | UTC date and time when the attribute value was last verified as being true and belonging to the specified individual | | | `value_metadata_classification` | `Varchar(256)` | Metadata relevant or pertaining to the security classification of a given attribute's value | | ### Indexes - `PRIMARY KEY product_attribute_pkey ON product_attribute USING btree (product_id, attribute_name)` [Back to top](#in-this-article) --- ## Table product_category A product can belong to more than one (fully qualified) category. This table stores product-category relationships. | Field Name | Data Type | Description | Example values | | ------------- | -------------- | ---------------------------------- | -------------- | | `product_id` | `Varchar(256)` | Unique identifier for the product | | | `category_id` | `Varchar(256)` | Unique identifier for the category | | ### Indexes - `UNIQUE INDEX product_category_product_id_key ON product_category USING btree (product_id, category_id)` [Back to top](#in-this-article) --- ## Table product_dimension Logically the (`dimension_type`, `dimension_id`) are a name-value pair associated with a product. Common dimensions include gender, brand, and style. Custom dimensions are also available. | Field Name | Data Type | Description | Example values | | ---------------- | -------------- | -------------------------------------------------------- | -------------------------- | | `product_id` | `Varchar(200)` | Unique identifier for a product | | | `dimension_id` | `Varchar(200)` | The value of a dimension | `Gucci`, `Red` | | `dimension_type` | `Varchar(200)` | The name of a dimension. Dimension names are capitalized | `Brand`, `Gender`, `Style` | ### Indexes - `UNIQUE INDEX product_dimension_product_id_key ON product_dimension USING btree (product_id, dimension_type, dimension_id)` [Back to top](#in-this-article) --- ## Table segment A Personalization segment is a real-time grouping of accounts or individuals based on a set of criteria you define. Segment updates in the operational store happen in real time, so any membership changes occur immediately, even during the same visit. Exports to this table, if done at all, are done one time per day. Segment export is enabled for individual segments by request, on the condition that the segment is small enough to export in this manner. | Field Name | Data Type | Description | Example values | | ------------- | ----------------------------- | ----------------------------------------------- | ----------------- | | `id` | `Varchar(10)` | Unique identifier for the segment | | | `name` | `Varchar(200)` | Name of the segment | | | `is_goal` | `Boolean` | `true` when segment is associated with a goal. | | | `entity_type` | `Varchar(200)` | The type of entity associated with this segment | `Account`, `User` | | `created_ts` | `Timestamp` without time zone | UTC date and time of creation | | | `updated_ts` | `Timestamp` without time zone | UTC date and time of recent update | | ### Indexes - `PRIMARY KEY segment_pkey ON segment USING btree (id)` ### Referenced by - `TABLE "segment_membership" CONSTRAINT "segment_membership_segment_id_fkey" FOREIGN KEY (segment_id) REFERENCES segment(id)` [Back to top](#in-this-article) --- ## Table segment_membership User-segment associations. For B2C, segments are associated with users. | Field Name | Data Type | Description | Example values | | ------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------- | | `user_id` | `Varchar(200)` | Segment member | | | `segment_id` | `Varchar(200)` | Unique identifier for the segment | | | `joined_ts` | `Timestamp` without time zone | UTC date and time of segment join event | | | `left_ts` | `Timestamp` without time zone | UTC date and time of segment leave event. **Note:** This field is never populated and its value is always set to `NULL`. | | | `is_active` | `Boolean` | True if user is an active member of the segment | | | `updated_dt` | `Timestamp` without time zone | UTC date and time of most recent update | | ### Indexes - `UNIQUE INDEX segment_membership_user_id_key ON segment_membership USING btree (user_id, segment_id, joined_ts)` [Back to top](#in-this-article) --- ## Table survey_question (deprecated) **important:** The `survey_question` dimension table is now deprecated and no longer in use. Survey questions and associated metadata | Field Name | Data Type | Description | Example values | | ------------------- | ----------------------------- | ---------------------------------------------------------------------------- | -------------- | | `survey_id` | `Varchar(6)` | Unique identifier for the survey | | | `survey_name` | `Varchar(200)` | Name of the survey | | | `survey_updated_ts` | `Timestamp` without time zone | UTC date and time of the most recent update to the question | | | `page_name` | `Varchar(200)` | Page name on which the question appears | | | `page_num` | `Integer` | Internal sequence number for the page | | | `element_name` | `Varchar(200)` | Name of the element in which the question appears. Pages have many elements. | | | `element_title` | `Varchar(200)` | Title of the element in which the question appears | | | `element_type` | `Varchar(200)` | The type of element - text, radio button, and so on. | | | `element_id` | `Varchar(200)` | Unique identifier for the element in which the question appears | | | `element_num` | `Integer` | Internal sequence enumerating the elements on a page | | | `question_id` | `Varchar(200)` | Unique identifier for the question | | ### Indexes - `UNIQUE INDEX survey_question_survey_id_key ON survey_question USING btree (survey_id, question_id, element_id, page_num, element_num)` [Back to top](#in-this-article) --- ## Table survey_response (deprecated) **important:** The `survey_response` dimension table is now deprecated and no longer in use. | Field Name | Data Type | Description | Example values | | ---------------- | ----------------------------- | ------------------------------------------------------------------------------------------- | -------------- | | `survey_id` | `Varchar(6)` | Unique identifier for the survey | | | `started_ts` | `Timestamp` without time zone | UTC date and time the survey was started | | | `user_id` | `Varchar(200)` | Unique identifier for the user taking the survey | | | `question_id` | `Varchar(200)` | Unique identifier for the survey question | | | `survey_session` | `Varchar(200)` | hash of `survey_id`, `started_ts`, `user_id`, which can help group questions into a session | | | `response_ts` | `Timestamp` without time zone | UTC date and time the response was made | | | `answer` | `Varchar(2048)` | Long text field that captures the survey answer | | ### Indexes - `UNIQUE INDEX survey_response_user_id_key ON survey_response USING btree (user_id, survey_id, question_id, response_ts)` [Back to top](#in-this-article) --- ## Table user User is the center of the Personalization data model. Much of the data about users is stored in tables other than this one; this table stores top-level information associated with a user. Fields prefixed with `loc_` refer to location information. Location codes can be looked up in the Location table. Fields prefixed with `or_` refer to the original referring event for this user. | Field Name | Data Type | Description | Example values | | ------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | `id` | `Varchar(200)` | Unique id for the user | | | `id_mod_100` | `Integer` | An offset from 0 to 99 used to create random partitions in the audience | | | `first_activity_ts` | `Timestamp` without time zone | UTC date and time the user was first observed | | | `last_activity_ts` | `Timestamp` without time zone | UTC date and time the user was most recently observed | | | `engagement_score` | `Integer` | The engagement score is an index that tracks how engaged your site visitors and application users are based on several different factors including visit information, actions, key performance indicators (KPIs), and segments. Higher values indicate deeper engagement. | | | `engagement_dt` | `Date` | The time the engagement score was computed. | | | `created_ts` | `Timestamp` without time zone | UTC date and time this record was created | | | `loc_device_provided` | `Boolean` | `true` if the location (`loc_` fields) was provided by a device (for example, obtained via a GPS retrieval), or `false` if this location is an approximate determined via geocoding of an IP address | | | `loc_ip_address` | `Varchar(100)` | IP Address associated with the user location | | | `loc_continent_key` | `Varchar(100)` | 2-letter continent code associated with the user location | `NA`, `AS` | | `loc_country_code` | `Char(2)` | An [ISO 3166-1 alpha-2](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code (for example, `US`) | `US` | | `loc_country_numeric_code` | `Smallint` | ISO-3166-1 numeric country code (for example, `840` for United States) | | | `loc_region_code` | `Integer` | Location's region code. Note - doesn’t correspond to `location(region_code)` | | | `loc_metro_code` | `Integer` | Location's metro code, corresponds to `location(metro_code)`. For the US, the metro code is the Nielsen Designated Market Area (DMA) code | | | `loc_city_code` | `Integer` | Location's city code, corresponds to `location(city_code)` | | | `loc_postal_code` | `Varchar(10)` | Location's postal code | | | `loc_isp` | `Varchar(100)` | Internet Service Provider associated with a location | `Telstra Internet`, `Comcast`, `Verizon` | | `loc_organization` | `Varchar(100)` | Organization associated with a location (can be `null`) | | | `loc_naics_code` | `Integer` | NAICS industry code | | | `or_medium` | `Varchar(50)` | Original referrer medium - how the user first arrived | `SEARCH`, `EMAIL`, `SOCIAL`, `DIRECT` | | `or_referrer_source` | `Varchar(50)` | Source of the original referrer | `Google`, `Bing`, `Yahoo!`, `Facebook`, `Pinterest` | | `or_query_terms` | `Varchar(512)` | Search terms for user who first arrived via search | | | `or_referrer_domain` | `Varchar(100)` | Domain of the original referrer | `google.com`, `bing.com`, `google.de`, `facebook.com` | | `or_referrer_reverse_subdomain` | `Varchar(200)` | Full host associated with the original referrer, reversed Used operationally | `mx.com.google.www` | | `is_anonymous` | `Boolean` | `true` when this user isn’t identifiable | | | `lifetime_value` | `Real` | Total amount this user has spent | | | `loc_city` | `Varchar(256)` | City name associated with the user's most recent location | | | `loc_latitude` | `Real` | Latitude associated with the user's most recent location | | | `loc_longitude` | `Real` | Longitude associated with the user's most recent location | | | `loc_timezone` | `Varchar(256)` | Timezone associated with the user's most recent location | | | `updated_ts` | `Timestamp` without time zone | UTC date and time the user was most recently updated | | | `loc_state_province_code` | `Varchar(256)` | Alphanumeric code associated with the user's most recent location | `KY`, `PAC` | | `or_referring_url` | `Varchar(2048)` | URL associated with the original referrer | `https://www.google.com/` | | `or_landing_url` | `Varchar(2048)` | URL of the first page the user saw | | | `email_address` | `Varchar(100)` | Email address associated with the user, if available | | ### Indexes - `PRIMARY KEY user_pkey ON "user" USING btree (id)` ### Referenced by - `TABLE "goal_completion_attribution_promoted_item" CONSTRAINT "goal_completion_attribution_promoted_item_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "goal_completion_attribution" CONSTRAINT "goal_completion_attribution_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "goal_completion_line_item" CONSTRAINT "goal_completion_line_item_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "order_line_item_attribute" CONSTRAINT "order_line_item_attribute_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "order_line_item" CONSTRAINT "order_line_item_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "segment_membership" CONSTRAINT "segment_membership_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_alias" CONSTRAINT "user_alias_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_attribute" CONSTRAINT "user_attribute_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_campaign_state" CONSTRAINT "user_campaign_state_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_click" CONSTRAINT "user_click_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_daily_stat_action" CONSTRAINT "user_daily_stat_action_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_daily_stat_item_stat_category" CONSTRAINT "user_daily_stat_item_stat_category_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_daily_stat_item_stat_dimension" CONSTRAINT "user_daily_stat_item_stat_dimension_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_daily_stat_item_stat_product" CONSTRAINT "user_daily_stat_item_stat_product_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_daily_stat_item_stat_promotion" CONSTRAINT "user_daily_stat_item_stat_promotion_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_daily_stat" CONSTRAINT "user_daily_stat_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_dismissal" CONSTRAINT "user_dismissal_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_engagement_history" CONSTRAINT "user_engagement_history_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_goal_completion" CONSTRAINT "user_goal_completion_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_impression" CONSTRAINT "user_impression_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_order_attribute" CONSTRAINT "user_order_attribute_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_order" CONSTRAINT "user_order_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_search" CONSTRAINT "user_search_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` - `TABLE "user_visit" CONSTRAINT "user_visit_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)` [Back to top](#in-this-article) --- ## Table user_alias This table contains the IDs of any anonymous users that have been merged into a given user. It also contains any non-anonymous IDs they used to have. This occurs when userIDs are changed to a new format, or simply a new ID. | Field Name | Data Type | Description | Example values | | ------------ | -------------- | --------------------------------- | -------------- | | `user_id` | `Varchar(200)` | Unique identifier for a user | | | `anon_alias` | `Varchar(200)` | User Id associated with this user | | ### Indexes - `UNIQUE INDEX user_alias_user_id_key ON user_alias USING btree (user_id, anon_alias)` [Back to top](#in-this-article) --- ## Table user_attribute Name-value pairs associated with a user (many different use cases - ids, segment support, and so on) | Field Name | Data Type | Description | Example values | | ------------ | ----------------------------- | ---------------------------------------------------------- | -------------- | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `updated_ts` | `Timestamp` without time zone | UTC date and time this attribute was most recently updated | | | `name` | `Varchar(256)` | Name of the attribute | | | `value` | `Varchar(256)` | Value of the attribute | | ### Indexes - `PRIMARY KEY user_attribute_pkey ON user_attribute USING btree (user_id, name, updated_ts)` [Back to top](#in-this-article) --- ## Table user_campaign_state This table provides summary information about a user or campaign. It mirrors the current state of an operational table for real-time reporting or other application. Although this table is less useful in a warehouse context, it does provide the state at batch time. You can use this table to quickly confirm membership or perform other on-demand tasks that can later be migrated to fully fleshed-out queries. | Field Name | Data Type | Description | Example values | | --------------------- | ----------------------------- | ----------------------------------------------------------------------- | -------------- | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `campaign_id` | `Varchar(256)` | Unique identifier for the campaign | | | `last_reset_time_ts` | `Timestamp` without time zone | UTC date and time of the most recent campaign reset | | | `experience_id` | `Varchar(256)` | Unique identifier for the experience | | | `user_group` | `Varchar(256)` | The user group (control or test) associated with this user's experience | | | `num_clicks` | `Integer` | Total number of clicks | | | `first_click_ts` | `Timestamp` without time zone | UTC date and time of the least recent click | | | `last_click_ts` | `Timestamp` without time zone | UTC date and time of the most recent click | | | `num_impressions` | `Integer` | Total number of impressions | | | `first_impression_ts` | `Timestamp` without time zone | UTC date and time of the least recent impression | | | `last_impression_ts` | `Timestamp` without time zone | UTC date and time of the most recent impression | | ### Indexes - `UNIQUE INDEX user_campaign_state_user_id_key ON user_campaign_state USING btree (user_id, campaign_id, experience_id, user_group)` [Back to top](#in-this-article) --- ## Table user_click Stores click events associated with a campaign at the visit level. `Event_ts` is associated with the most recent event. | Field Name | Data Type | Description | Example values | | --------------- | ----------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------- | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `experience_id` | `Varchar(256)` | Unique identifier for the experience | | | `event_ts` | `Timestamp` without time zone | UTC date and time of the most recent click in a session | | | `user_group` | `Char(1)` | The user group associated with the user and experience - control (`C`) or test (`D`) | | | `browser` | `Varchar(256)` | Client browser application | `Safari`, `Chrome`, `Firefox`, `IE`, `other` | | `device` | `Varchar(256)` | Client hardware device | `mobile`, `computer`, `tablet`, `other` | | `engagement` | `Varchar(256)` | Engagement for the associated user | `low`, `medium`, `high` | | `has_purchased` | `Boolean` | Set when the user makes a purchase | | | `click_count` | `Smallint` | Total number of clicks for the session | | | `is_anonymous` | `Boolean` | `true` when the user isn’t named (identified) | | | `is_firsttime` | `Boolean` | `true` when this visit is a user's first | | | `os` | `Varchar(256)` | Operating system - `x` is "macOS X" | `x`, `windows`, `linux`, `ios`, `android`, `other` | | `platform` | `Varchar(256)` | Platform associated with the clicks | `Email`, `Web` | | `source` | `Varchar(256)` | For customers who use the mobile SDK, each mobile app would be listed as a separate source | | ### Indexes - `PRIMARY KEY user_click_pkey ON user_click USING btree (user_id, experience_id, event_ts)` [Back to top](#in-this-article) --- ## Table user_daily_stat Aggregation of events at the (user, day) grain | Field Name | Data Type | Description | Example values | | --------------- | -------------- | ----------------------------------------------- | -------------- | | `stat_date` | `Date` | Date of the stat aggregation for this user | | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `visit_count` | `Integer` | Number of times user visited on the `stat_date` | | | `visit_millis` | `Bigint` | Total visit duration, in milliseconds | | | `total_actions` | `Integer` | Number of actions performed by the user | | | `num_pages` | `Integer` | Number of pages the user viewed | | ### Indexes - `UNIQUE INDEX user_daily_stat_stat_date_key ON user_daily_stat USING btree (stat_date, user_id)` [Back to top](#in-this-article) --- ## Table user_daily_stat_action Aggregation of events at the (user, day, action) grain | Field Name | Data Type | Description | Example values | | -------------- | -------------- | ------------------------------------------------------------------------- | ------------------------------------------------- | | `stat_date` | `Date` | Date of the stat aggregation for this user | | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `action_id` | `Varchar(200)` | Identifier for an action performed by the user | `Home`, `Search`, `View Category`, `View Product` | | `action_count` | `Integer` | Total count of actions for this action on this day by the associated user | | ### Indexes - `UNIQUE INDEX user_daily_stat_action_stat_date_key ON user_daily_stat_action USING btree (stat_date, user_id, action_id)` [Back to top](#in-this-article) --- ## Table user_daily_stat_item_stat_category Aggregation of events at the (user, day, product category) grain | Field Name | Data Type | Description | Example values | | -------------------- | --------------- | ------------------------------------------------------------- | -------------- | | `stat_date` | `Date` | Date associated with the stat | | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `category_id` | `Varchar(200)` | Unique identifier for the category | | | `cart_value` | `Numeric(18,4)` | Total value of items in the cart for this day, category, user | | | `num_cart_items` | `Integer` | Count of items in the cart | | | `num_purchases` | `Integer` | Count of purchases | | | `out_of_stock_views` | `Integer` | Count of views of products that were out of stock | | | `purchase_value` | `Numeric(18,4)` | Total value of purchased made for this user, category, date | | | `recommended_count` | `Integer` | Number of items recommended to the user for this category | | | `view_time_millis` | `Integer` | Total view time for the category in milliseconds | | | `view_value` | `Numeric(18,4)` | The sum of the value of items viewed in this category | | | `views` | `Integer` | Number of views for this category, day, user | | ### Indexes - `UNIQUE INDEX user_daily_stat_item_stat_category_stat_date_key ON user_daily_stat_item_stat_category USING btree (stat_date, user_id, category_id)` [Back to top](#in-this-article) --- ## Table user_daily_stat_item_stat_dimension Aggregation of events at the (user, day, product dimension) grain | Field Name | Data Type | Description | Example values | | -------------------- | --------------- | -------------------------------------------------------------- | --------------------------------------- | | `stat_date` | `Date` | Date associated with the stat | | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `dimension_type` | `Varchar(256)` | The name of a dimension. Dimension names are capitalized. | `Brand`, `Gender`, `Style`, `ItemClass` | | `dimension_id` | `Varchar(256)` | Unique identifier for the dimension | | | `cart_value` | `Numeric(18,4)` | Total value of items in the cart for this day, dimension, user | | | `num_cart_items` | `Integer` | Count of items in the cart | | | `num_purchases` | `Integer` | Count of purchases | | | `out_of_stock_views` | `Integer` | Count of views of products that were out of stock | | | `purchase_value` | `Numeric(18,4)` | Total value of purchased made for this user, dimension, date | | | `recommended_count` | `Integer` | Number of items recommended to the user for this dimension | | | `view_time_millis` | `Integer` | Total view time for the dimension in milliseconds | | | `view_value` | `Numeric(18,4)` | The sum of the value of items viewed in this dimension | | | `views` | `Integer` | Number of views for this dimension, day, user | | ### Indexes - `UNIQUE INDEX user_daily_stat_item_stat_dimension_stat_date_key ON user_daily_stat_item_stat_dimension USING btree (stat_date, user_id, dimension_type, dimension_id)` [Back to top](#in-this-article) --- ## Table user_daily_stat_item_stat_product Aggregation of events at the (user, day, product) grain | Field Name | Data Type | Description | Example values | | -------------------- | --------------- | ------------------------------------------------------------ | -------------- | | `stat_date` | `Date` | Date associated with the stat | | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `product_id` | `Varchar(200)` | Unique identifier for the product | | | `cart_value` | `Numeric(18,4)` | Total value of items in the cart for this day, product, user | | | `num_cart_items` | `Integer` | Count of items in the cart | | | `num_purchases` | `Integer` | Count of purchases | | | `out_of_stock_views` | `Integer` | Count of views of products that were out of stock | | | `purchase_value` | `Numeric(18,4)` | Total value of purchased made for this user, product, date | | | `recommended_count` | `Integer` | Number of items recommended to the user for this product | | | `view_time_millis` | `Integer` | Total view time for the product in milliseconds | | | `view_value` | `Numeric(18,4)` | The sum of the value of items viewed in this product | | | `views` | `Integer` | Number of views for this product, day, user | | ### Indexes - `UNIQUE INDEX user_daily_stat_item_stat_product_stat_date_key ON user_daily_stat_item_stat_product USING btree (stat_date, user_id, product_id)` ### Example Query ```sql /* top products using daily stats */ SELECT CASE WHEN pct_total > 1 THEN a.name ELSE 'ALL OTHERS' END AS name, to_char(sum(revenue), 'FM$999,999,999,990D00') AS revenue, to_char(sum(pct_total), 'FM99.99%') AS pct_total, sum(rnk) rnk FROM ( SELECT p.name, sum(a.num_purchases * purchase_value) revenue, ratio_to_report(sum(a.num_purchases * purchase_value)) OVER () * 100 AS pct_total, row_number() OVER ( ORDER BY revenue DESC ) AS rnk FROM product p, user_daily_stat_item_stat_product a WHERE p.id = a.product_id GROUP BY 1 ORDER BY sum(a.num_purchases * purchase_value) DESC ) a GROUP BY 1 ORDER BY rnk ``` [Back to top](#in-this-article) --- ## Table user_daily_stat_item_stat_promotion Aggregation of events at the (user, day, promotion) grain | Field Name | Data Type | Description | Example values | | ----------------------- | --------------- | -------------------------------------------------------------------- | -------------- | | `stat_date` | `Date` | Date for the stat | | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `promotion_id` | `Varchar(200)` | Unique identifier for the promotion | | | `cart_value` | `Numeric(18,4)` | Total value of items in the cart for this day, promotion, user | | | `num_cart_items` | `Integer` | Count of items in the cart | | | `num_purchases` | `Integer` | Count of purchases | | | `out_of_stock_views` | `Integer` | Count of views of promotions associated with an out of stock product | | | `purchase_value` | `Numeric(18,4)` | Total value of purchases made for this user, promotion, date | | | `recommended_count` | `Integer` | Number of items recommended to the user for this promotion | | | `view_time_millis` | `Integer` | Total view time for the promotion in milliseconds | | | `view_value` | `Numeric(18,4)` | The sum of the value of promotions viewed | | | `views` | `Integer` | Number of views by this user for this promotion and day | | | `eligible_for_serving` | `Integer` | Number of times this promotion was eligible to be served | | | `requested_for_serving` | `Integer` | Number of times this promotion was requested to be served | | | `served` | `Integer` | Number of times this promotion was served | | ### Indexes - `UNIQUE INDEX user_daily_stat_item_stat_promotion_stat_date_key ON user_daily_stat_item_stat_promotion USING btree (stat_date, user_id, promotion_id)` [Back to top](#in-this-article) --- ## Table user_dismissal Stores dismissal events associated with a campaign at the visit level. Event_ts is associated with the most recent event. | Field Name | Data Type | Description | Example values | | ----------------- | ----------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------- | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `experience_id` | `Varchar(256)` | Unique identifier for the experience | | | `event_ts` | `Timestamp` without time zone | UTC date and time of the most recent dismissal in a session. | | | `user_group` | `Char(1)` | The user group associated with the user and experience - control (`C`) or (`D`) | | | `browser` | `Varchar(256)` | Client browser application | `Safari`, `Chrome`, `Firefox`, `IE`, `other` | | `device` | `Varchar(256)` | Client hardware device | `mobile`, `computer`, `tablet`, `other` | | `engagement` | `Varchar(256)` | Engagement for the associated user | `low`, `medium`, `high` | | `has_purchased` | `Boolean` | Set when the user makes a purchase | | | `dismissal_count` | `Smallint` | Total number of dismissals for the session | | | `is_anonymous` | `Boolean` | `true` when the user isn’t named (identified) | | | `is_firsttime` | `Boolean` | `true` when this visit is a user's first | | | `os` | `Varchar(256)` | Operating system - x is "macOS X" | `x`, `windows`, `linux`, `ios`, `android`, `other` | | `platform` | `Varchar(256)` | Platform associated with the dismissals | `Email`, `Web` | | `source` | `Varchar(256)` | For customers who use the mobile SDK, each mobile app would be a separate source. | | ### Indexes - `PRIMARY KEY user_dismissal_pkey ON user_dismissal USING btree (user_id, experience_id, event_ts)` [Back to top](#in-this-article) --- ## Table user_engagement_history Stores historical engagement values at the user grain | Field Name | Data Type | Description | Example values | | ---------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | | `user_id` | `Varchar(200)` | Unique identifier for a user | | | `"day"` | `Date` | Date of the day tracked for engagement | | | `score` | `Integer` | Engagement score is an integer reflecting a percentage from 0 to 125 and the value depends on account configuration. For more information, see [Engagement Score](https://help.salesforce.com/s/articleView?id=sf.mc_pers_engagement_score.htm). | | ### Indexes - `UNIQUE INDEX user_engagement_history_user_id_key ON user_engagement_history USING btree (user_id, "day")` [Back to top](#in-this-article) --- ## Table user_goal_completion Since goal completions often involve a purchase, purchase data is stored at the top level in this table and is empty for non-purchase goal completions | Field Name | Data Type | Description | Example values | | ------------------------- | ----------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------- | | `user_id` | `Varchar(200)` | User who satisfied the goal criteria | | | `goal_ts` | `Timestamp` without time zone | Timestamp of the goal completion in UTC | | | `goal_id` | `Varchar(256)` | One of: segment id or purchase (`p`) | `p` | | `order_id` | `Varchar(256)` | Unique identifier for the order. This ID comes from the customer. | | | `order_currency` | `Varchar(256)` | Currency used to purchase | | | `order_line_item_count` | `Smallint` | Count of line items in the order | | | `order_total_value` | `Numeric(18,4)` | Sum of quantity \* price for all line items | | | `order_units_count` | `Smallint` | Count of units in the order | | | `order_visit_age_minutes` | Double precision | Age of the visit at the time of the purchase goal completion | | | `order_visit_time` | `Timestamp` without time zone | UTC date and time of the visit during which this order was made | | | `browser` | `Varchar(256)` | Client browser application | `Safari`, `Chrome`, `Firefox`, `IE`, `other` | | `device` | `Varchar(256)` | Client hardware device | `mobile`, `computer`, `tablet`, `other` | | `engagement` | `Varchar(256)` | Engagement for the associated user | `low`, `medium`, `high` | | `has_purchased` | `Boolean` | Set when the user makes a purchase | | | `is_anonymous` | `Boolean` | `true` when the user isn’t named (identified) | | | `is_first_time` | `Boolean` | `true` when the completion represents a first encounter with a visitor | | | `os` | `Varchar(256)` | Operating system - x is "macOS X" | `x`, `windows`, `linux`, `ios`, `android`, `other` | | `platform` | `Varchar(256)` | Platform used in the goal completion (email, web) | `Email`, `Web` | | `source` | `Varchar(256)` | For customers who use the mobile SDK, each mobile app would be a separate source. | | ### Indexes - `UNIQUE INDEX user_goal_completion_user_id_key ON user_goal_completion USING btree (user_id, goal_id, goal_ts)` [Back to top](#in-this-article) --- ## Table user_impression Stores impression events associated with a campaign at the visit level, with the most recent event associated with `event_ts`. | Field Name | Data Type | Description | Example values | | ------------------ | ----------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------- | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `experience_id` | `Varchar(256)` | Unique identifier for the experience | | | `event_ts` | `Timestamp` without time zone | UTC date and time of the most recent impression in a session. | | | `user_group` | `Char(1)` | The user group associated with the user and experience - control (C) or test (D) | | | `browser` | `Varchar(256)` | Client browser application | `Safari`, `Chrome`, `Firefox`, `IE`, `other` | | `device` | `Varchar(256)` | Client hardware device | `mobile`, `computer`, `tablet`, `other` | | `engagement` | `Varchar(256)` | Engagement for the associated user | `low`, `medium`, `high` | | `has_purchased` | `Boolean` | Set when the user makes a purchase | | | `impression_count` | `Smallint` | Total number of impressions for the session | | | `is_anonymous` | `Boolean` | `true` when the user isn’t named (identified) | | | `is_firsttime` | `Boolean` | `true` when this visit is a user's first | | | `os` | `Varchar(256)` | Operating system - x is "macOS X" | `x`, `windows`, `linux`, `ios`, `android`, `other` | | `platform` | `Varchar(256)` | Platform associated with the impressions | `Email`, `Web` | | `source` | `Varchar(256)` | For customers who use the mobile SDK, each mobile app would be a separate source. | | ### Indexes - `PRIMARY KEY user_impression_pkey ON user_impression USING btree (user_id, experience_id, event_ts)` [Back to top](#in-this-article) --- ## Table user_order Stores purchases. Only purchased orders are kept in the Data Warehouse, nothing open | Field Name | Data Type | Description | Example values | | -------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------- | | `id` | `Varchar(100)` | Unique identifier for the order. This ID comes from the customer. | | | `user_id` | `Varchar(200)` | Unique identifier for the user | | | `order_num` | `Smallint` | 1-based sequence of orders for this user. Based on the array order in the operational store. (Not set by the customer) | | | `visit_start_ts` | `Timestamp` without time zone | Start time of the visit in which this order was placed | | | `visit_num` | `Integer` | Visit in which order create time falls within the visit start or end. | | | `created_ts` | `Timestamp` without time zone | UTC date and time at which the order was created | | | `updated_ts` | `Timestamp` without time zone | UTC date and time the order was most recently updated | | | `purchased_ts` | `Timestamp` without time zone | UTC date and time the purchase was made | | | `visit_age_at_purchase_ms` | `Integer` | Age of the session, in milliseconds, at purchase time | | | `num_items` | `Integer` | Number of items in the order | | | `num_units` | `Integer` | Number of units (distinct items) in the order | | | `total_value` | `Numeric(18,4)` | Total value of the purchase | | | `currency` | `Varchar(200)` | Currency used to make the purchase | | | `status` | `Varchar(200)` | Purchased only, other states not stored in the warehouse. (`Open`, `Purchased`, `Canceled`). | | ### Indexes - `PRIMARY KEY user_order_pkey ON user_order USING btree (id)` - `UNIQUE INDEX user_order_user_id_key ON user_order USING btree (user_id, created_ts)` ### Referenced by - `TABLE "order_line_item_attribute" CONSTRAINT "order_line_item_attribute_order_id_fkey" FOREIGN KEY (order_id) REFERENCES user_order(id)` - `TABLE "order_line_item" CONSTRAINT "order_line_item_order_id_fkey" FOREIGN KEY (order_id) REFERENCES user_order(id)` - `TABLE "user_order_attribute" CONSTRAINT "user_order_attribute_order_id_fkey" FOREIGN KEY (order_id) REFERENCES user_order(id)` [Back to top](#in-this-article) --- ## Table user_order_attribute Name-value pairs associated with an order | Field Name | Data Type | Description | Example values | | --------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `order_id` | `Varchar(100)` | Unique identifier for the order | | | `user_id` | `Varchar(200)` | User who made the purchase | | | `order_num` | `Smallint` | Internally generated 1-based sequence indicating the order number | | | `attribute_name` | `Varchar(256)` | The name of the attribute | | | `attribute_value` | `Varchar(256)` | The value of the attribute | | | `value_metadata_origin` | `Varchar(256)` | The name of the entity that issues or creates the initial attribute value | `CRM`, `PointOfSaleSystem`, `FulfillmentSystem`, `LoyaltySystem` | | `value_metadata_provider` | `Varchar(256)` | The name of the entity that is providing the attribute | `CsvUserEtlJob:user-20190528.csv.gz` | | `value_metadata_gear_id` | `Varchar(256)` | ID of the Personalization gear that updated the attribute, or `null` if it wasn’t updated by a gear | | | `value_metadata_last_updated_ts` | `Varchar(256)` | UTC date and time when the attribute was last updated | | | `value_metadata_last_verified_ts` | `Varchar(256)` | UTC date and time when the attribute value was last verified as being true and belonging to the specified individual | | | `value_metadata_classification` | `Varchar(256)` | Metadata relevant or pertaining to the security classification of a given attribute's value | | ### Indexes - `PRIMARY KEY user_order_attribute_pkey ON user_order_attribute USING btree (user_id, order_id, attribute_name)` [Back to top](#in-this-article) --- ## Table user_search User search terms (some normalization and tokenization is performed) | Field Name | Data Type | Description | Example values | | -------------------- | ----------------------------- | ---------------------------------------------------------- | -------------- | | `user_id` | `Varchar(200)` | User who performed the search | | | `term` | `Varchar(200)` | Search term (tokenized) | | | `most_recent_search` | `Timestamp` without time zone | UTC date and time of the most recent search for this term | | | `product_clicks` | `Integer` | Count of product clicks associated with the search | | | `purchase_value` | `Numeric(18,4)` | Total purchase value of orders associated with this search | | | `search_count` | `Integer` | Total number of searches for this term | | ### Indexes - `PRIMARY KEY user_search_pkey ON user_search USING btree (user_id, term)` [Back to top](#in-this-article) --- ## Table user_user_visit_lookup Use this table to join the `user_visit_detail` with other tables containing a `user_id`. The `user_id` in this table corresponds to the general notion of `user_id` in the database, and `user(id)`. The `user_visit_key` is the `user_id` in the `user_visit_detail` table. | Field Name | Data Type | Description | Example values | | ---------------- | -------------- | ----------------------------------------------- | -------------- | | `user_id` | `Varchar(200)` | User id from the user table | | | `user_id_hashed` | `Varchar(200)` | Hashed user id from the user_visit_detail table | | ### Indexes - `UNIQUE INDEX user_user_visit_lookup_user_id_key ON user_user_visit_lookup USING btree (user_id, user_id_hashed)` [Back to top](#in-this-article) --- ## Table user_visit Stores information about the user visit Fields that only appear in user visit: `referring_campaign_id`, `weather`, `previous_path`, `temperature`, `referrer`, `num_pageviews`, `num_events`, `last_event_ts`, `visit_num` Fields that appear in both user visit and user visit detail: `is_anonymous`, `browser`, `engagement`, `source`, `utm_parameters`, `device`, `is_firsttime`, `has_purchased`, `platform`, `start_ts`, `_dw_created_ts`, `os`, `user_id` **note:** The `user_id` `user_visit_detail` is a hash value of the user ID in this table; use `user_user_lookup` to join | Field Name | Data Type | Description | Example values | | ----------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | | `user_id` | `Varchar(200)` | User id associated with the visit | | | `visit_num` | `Smallint` | Internally generated sequence value in the visit history - the visit number. | | | `num_events` | `Integer` | Count of events for the visit | | | `previous_path` | `Varchar(100)` | The most recent page view action for this visit (operation field) | | | `num_pageviews` | `Integer` | Count of pages viewed in the visit | | | `referring_campaign_id` | Varchar(32) | External campaign that drove the visit | | | `device` | `Varchar(100)` | Client hardware device | `mobile`, `computer`, `tablet`, `other` | | `engagement` | `Varchar(100)` | Engagement for the associated user | `low`, `medium`, `high` | | `os` | `Varchar(100)` | Operating system - x is "macOS X" | `x`, `windows`, `linux`, `ios`, `android`, `other` | | `browser` | `Varchar(100)` | Client browser application | `Safari`, `Chrome`, `Firefox`, `IE`, `other` | | `platform` | `Varchar(100)` | Platform used in the goal completion (email, web) | Email, Web | | `temperature` | `Varchar(100)` | Recorded temperature at the physical location of the user during the visit time | | | `weather` | `Varchar(100)` | Recorded weather at the physical location of the user during the visit time | | | `referrer` | `Varchar(2048)` | HTTP referrer for the visit | | | `start_ts` | `Timestamp` without time zone | UTC date and time the visit began | | | `last_event_ts` | `Timestamp` without time zone | UTC date and time of the most recent event in the visit | | | `utm_parameters` | `Varchar(100)` | Urchin Tracking Module (UTM) parameters identify the marketing campaign that refers traffic to a specific website and are associated with the visit's referrer | | | `has_purchased` | `Boolean` | Set when the user makes a purchase | | | `is_anonymous` | `Boolean` | `true` when the user isn’t named (identified) | | | `is_first_time` | `Boolean` | `true` when this visit is the user's first | | | `source` | `Varchar(10)` | For customers who use the mobile SDK, each mobile app would be a separate source | | ### Indexes - `PRIMARY KEY user_visit_pkey ON user_visit USING btree (user_id, start_ts)` - `UNIQUE INDEX user_visit_user_id_key ON user_visit USING btree (user_id, visit_num)` ### Referenced by - `TABLE "user_order" CONSTRAINT "user_order_user_id_visit_num_fkey" FOREIGN KEY (user_id, visit_num) REFERENCES user_visit(user_id, visit_num)` - `TABLE "user_order" CONSTRAINT "user_order_user_id_visit_start_ts_fkey" FOREIGN KEY (user_id, visit_start_ts) REFERENCES user_visit(user_id, start_ts)` [Back to top](#in-this-article) --- ## Table user_visit_detail Stores additional information about the user visit - location and referrer are usually of interest. The following fields appear only in the visit detail, not the visit: `region_code`, `referrer_domain`, `revenue`, `city_code`, `metro_code`, `longitude`, `industry_code`, `referring_campaign`, `country_code`, `action_count`, `page_view_count`, `referrer_subdomain`, `referrer_url`, `referrer_terms`, `referrer_source`, `latitude`, `duration_minutes`, `referrer_landing_url`, `referrer_medium` Fields that appear in both this table and `user_visit`: `is_anonymous`, `browser`, `engagement`, `source`, `utm_parameters`, `device`, `is_firsttime`, `has_purchased`, `platform`, `start_ts`, `_dw_created_ts`, `os`, `user_id` **note:** The `user_id` in this table is a hash value on the `user_id` in `user_visit` | Field Name | Data Type | Description | Example values | | ---------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | | `user_id_hashed` | `Varchar(200)` | Hash code of the user_id (id from the user table) associated with this visit | | | `start_ts` | `Timestamp` without time zone | UTC date and time the visit started | | | `referrer_medium` | `Varchar(100)` | How the user first arrived | `SEARCH`, `EMAIL`, `SOCIAL`, `DIRECT` | | `referrer_source` | `Varchar(100)` | Source of the visit | `Google`, `Bing`, `Yahoo!`, `Facebook`, `Pinterest` | | `referrer_terms` | `Varchar(255)` | Search terms for associated with the referral | | | `referrer_domain` | `Varchar(100)` | Domain of the visit referrer | `google.com`, `bing.com`, `google.de`, `facebook.com` | | `referrer_subdomain` | `Varchar(100)` | Referrer host with subdomain | `www.google.com.mx` | | `referrer_url` | `Varchar(2048)` | URL associated with the visit referrer | | | `referrer_landing_url` | `Varchar(2048)` | URL of the first page the user saw | | | `utm_parameters` | `Varchar(100)` | Urchin Tracking Module (UTM) parameters identify the marketing campaign that refers traffic to a specific website and are associated with the visit's referrer | | | `latitude` | `Numeric(9,6)` | Latitude associated with the visitor's physical location | | | `longitude` | `Numeric(9,6)` | Longitude associated with the visitor's physical location | | | `country_code` | `Varchar(100)` | 2-letter Country code associated with the visit | | | `region_code` | `Varchar(100)` | Region code associated with the visit | | | `metro_code` | `Integer` | Metro code associated with the visit. Metro codes for US are Nielsen DMA codes | | | `city_code` | `Integer` | Code for the city associated with the visit | | | `industry_code` | `Integer` | NAICS industry code | | | `referring_campaign` | `Varchar(32)` | External campaign that drove the visit | | | `device` | `Varchar(100)` | Client hardware device | `mobile`, `computer`, `tablet`, `other` | | `engagement` | `Varchar(100)` | Engagement for the associated user | `low`, `medium`, `high` | | `os` | `Varchar(100)` | Operating system - x is "macOS X" | `x`, `Windows`, `Linux`, `iOS`, `Android`, `other` | | `browser` | `Varchar(100)` | Client browser application | `Safari`, `Chrome`, `Firefox`, `IE`, `other` | | `platform` | `Varchar(100)` | Platform (email, web) | `Email`, `Web`, `Mobile` | | `has_purchased` | `Boolean` | Set when the user makes a purchase | | | `is_anonymous` | `Boolean` | `true` when the user isn’t named (identified) | | | `is_first_time` | `Boolean` | `true` when this visit is the user's first | | | `source` | `Varchar(10)` | For customers who use the mobile SDK, each mobile app would be a separate source | | | `duration_minutes` | Double precision | Time spent in the visit, in minutes | | | `revenue` | `Numeric(18,4)` | Total revenue associated with purchases made during the visit | | | `action_count` | `Integer` | Count of actions performed this visit | | | `page_view_count` | `Integer` | Count of pages viewed this visit | | ### Indexes - `PRIMARY KEY user_visit_detail_pkey ON user_visit_detail USING btree (user_id_hashed, start_ts)` [Back to top](#in-this-article) ## See Also - [_Salesforce Help_: Engagement Score](https://help.salesforce.com/s/articleView?id=sf.mc_pers_engagement_score.htm) - [_External Link_: IBAN Alpha-2 and Alpha-3 Country Codes](https://www.iban.com/country-codes) # Data Modeling Data modeling in Marketing Cloud Personalization involves the configuration of the platform to represent the key business information for your company. Data in Personalization represents three primary areas of key information: user behaviors, tracked at the user, account, business, or household level; shadow catalog information, representing key business objects; and statistical tracking of key performance indicators, such as purchase revenue. The configuration of these features is accomplished in the Personalization platform by administrators during initial setup. - User - Identity - [Attributes](https://developer.salesforce.com/docs/marketing/personalization/guide/data-modeling/attributes.md) - Segments - Visits - Behaviors - Campaign Interactions - Orders & Goal Completions - Accounts / Households - Identity - [Attributes](https://developer.salesforce.com/docs/marketing/personalization/guide/data-modeling/attributes.md) - Segments - Aggregate behaviors - Shadow Catalog - Items (For example, Products.) - [Attributes](https://developer.salesforce.com/docs/marketing/personalization/guide/data-modeling/attributes.md) - Related catalog objects # Data Warehouse The Marketing Cloud Personalization Data Warehouse enables business analysts to connect BI tools to Personalization, and data scientists to analyze and enhance Personalization data as well as use the power of machine learning. **note:** The Data Warehouse is available only to customers who have already purchased the Marketing Cloud Personalization - Data Warehouse add-on product. ## In This Article This article discusses the following topics related to the Personalization Data Warehouse: - What data is accessible? - Physical Data Model Entity Relationship Diagram (ERD) - Data Warehouse Dimensions - Data Warehouse Activity (Fact) Tables ## What Data Is Accessible? For every prospect, customer, and account who engages with your organization, Personalization maintains a unified profile that stores engagement details and a contextual understanding of the individual’s behavior. Profiles also include attribute data ingested from external sources or captured with each web visit. This attribute data includes things like referring sources, geolocation, and browser. Personalization tracks detailed engagement statistics at the product level, as well as transactional data such as registrations, downloads, purchases, adds-to-cart. Using the Data Warehouse, business analysts can access this information to identify trends and patterns related to their product or content catalogs. **important:** The Personalization Data Warehouse currently doesn’t support data related to User Owned Objects and Article, Blog, and Promotion catalog objects. ## Physical Data Model ERD The following ERD shows the relationships between the **Dimension** and **Activity** (Fact) tables of the Personalization Data Warehouse. The [Data Dictionary](https://developer.salesforce.com/docs/marketing/personalization/guide/data-dictionary.md) article provides the complete data specifications on these tables in alphabetical order. **important:** The `_dw_created_ts` field present in all Personalization Data Warehouse tables is a system field for tracking when a record was created. We advise against building dependencies on it as its name or the logic behind it can change without notice. Instead, we recommend using the `created_ts` or `update_ts` fields, or other fields documented in the [Data Dictionary](https://developer.salesforce.com/docs/marketing/personalization/guide/data-dictionary.md) that are more suited to your use case. ### ERD Details Due to the size of the full data warehouse model, we've provided **ERD Detail** sections at the end of each dimension and activity table sections for clarity. These sections present just the portion of the data warehouse ERD discussed. These sections show the following information for the tables in the ERD: - Table name - Field names - Field data types (d=date, #=number, t=text/string, b=boolean, c=code) - Primary Keys (indicated by key icon) - Foreign Keys (indicated by "up-and-out" arrow) - Fields used as Foreign Key by other tables (indicated by "down-and-in" arrow) ![ER Diagram](https://developer.salesforce.com/media/data-analytics/data-warehouse-erd.png) ## Data Warehouse Dimensions The dimension tables in the default Personalization Data Warehouse provide the context for data analysis in the following dimensions: - User - Product - Campaign - Location (deprecated) - User Segment - Customer Survey (deprecated) ### Tables by Dimension The following sections present the data warehouse tables used to store data for each dimension. #### User The dimension tables that store data associated with the User dimension include the following: - `user` - `user_attribute` - `user_alias` #### Product The dimension tables that store data associated with the Product dimension include the following: - `product` - `product_category` - `category` - `product_attribute` - `product_dimension` - `dimension` #### Campaign The dimension tables that store data associated with the Campaign dimension include the following: - `campaign` - `experience` #### Location (deprecated) The dimension tables that store data associated with the Location (affiliate) dimension include the following: - `location` (deprecated) - `country` (deprecated) **important:** The `location` and `country` dimension tables are now deprecated and no longer in use. #### User Segment The `segment` dimension table stores data associated with the specific segment for which a user has qualified. #### Customer Survey (deprecated) The `survey_question` dimension table stores data associated with the customer survey questions that exist in the current system. **important:** The `survey_question` dimension table is now deprecated and no longer in use. ### ERD Detail - Dimensions The following sections present the tables in the **Dimension** side of the data warehouse ERD broken into top and bottom sections. #### ERD Detail - Dimension Top ![Dimension Top](https://developer.salesforce.com/media/data-analytics/data-warehouse-erd-dimension-top.png) #### ERD Detail - Dimension Bottom ![Dimension Bottom](https://developer.salesforce.com/media/data-analytics/data-warehouse-erd-dimension-bottom.png) ## Data Warehouse Activity (Fact) Tables The following sections list the activity (fact) tables in the default Personalization data warehouse that collect and store the values used with dimension tables analysis. The tables are presented in the order in which they appear in the **Activity** side of the ERD shown in this article (left-to-right, top-to-bottom). The **ERD Detail** section at the end of each subsection presents corresponding table details for the listed tables. ### Activity Tables - ERD Top The activity tables in the top portion of the ERD Activity side include: - `user_visit_detail` - `user_visit_detail_utm_parameters` - `path` (affiliate) - `user_search` - `user_send` - `user_send_activity` #### ERD Detail - Activity Top ![Activity Top](https://developer.salesforce.com/media/data-analytics/data-warehouse-erd-activity-top.png) ### Activity Tables - ERD Middle-Left The activity tables in the middle-left portion of the ERD Activity side include: - `segment_membership` - `user_campaign_state` - `user_click` - `user_order_attribute` - `order_line_item_attribute` - `user_order` - `user_user_visit_lookup` - `user_impression` #### ERD Detail - Activity Middle-Left ![Activity Middle-Left](https://developer.salesforce.com/media/data-analytics/data-warehouse-erd-activity-middle-left.png) ### Activity Tables - ERD Middle-Right The activity tables in the middle-right portion of the ERD Activity side include: - `user_daily_stat_item_stat_promotion` - `user_goal_completion` #### ERD Detail - Activity Middle-Right ![Activity Middle-Right](https://developer.salesforce.com/media/data-analytics/data-warehouse-erd-activity-middle-right.png) ### Activity Tables - ERD Bottom-Left The activity tables in the bottom-left portion of the ERD Activity side include: - `order_line_item` - `goal_completion_attribution` - `user_visit` - `user_daily_stat_item_stat_category` - `user_daily_stat_item_stat_product` - `goal_completion_line_item` - `user_dismissal` - `goal_completion_attribution_promoted_item` - `survey_response` (depracated) **important:** The `survey_response` dimension table is now deprecated and no longer in use. #### ERD Detail - Activity Bottom-Left ![Activity Bottom-Left](https://developer.salesforce.com/media/data-analytics/data-warehouse-erd-activity-bottom-left.png) ### Activity Tables - ERD Bottom-Right The activity tables in the bottom-right portion of the ERD Activity side include: - `user_daily_stat_item_stat_product` - `user_daily_stat` - `user_daily_stat_action` - `user_engagement_history` - `user_daily_stat_item_stat_dimension` #### ERD Detail - Activity Bottom-Right ![Activity Bottom-Left](https://developer.salesforce.com/media/data-analytics/data-warehouse-erd-activity-bottom-right.png) # Datasets Datasets represent an instance of Marketing Cloud Personalization for use in personalization. User profiles, campaign configurations, and other features of Personalization are all scoped to a dataset. This scoping strategy enables you to use different datasets on different sites, geographies, or deployment environments. For example, while splitting datasets between your QA and production environments is common, it is also helpful to maintain separate datasets for your French and German sites. Datasets are placed within Identity Groups to coordinate user identities between environments, ensuring that you're not emailing the same user from the French and German environments while still letting each dataset coordinate that user's personalization. # DateTime Property ## Date Picker A date picker input. ```typescript dateProperty: DateTime; ``` ![Date Picker](https://developer.salesforce.com/media/interface-template-configuration/DateTime.png) The value returned from the selection is structured as an object with a single `dateTime` property. The property is an array of strings representing the date and time in ISO 8601 format (for example, 2019-10-08T16:00:00.000Z). The object structure is as follows: ```typescript { dateTime: string[] } ``` **note:** Even if the `DateTime` picker only intends to return a single date, it still returns an array. --- ## Date Range The picker can be configured to return a range using the `@range` decorator. ```typescript @range(true) dateProperty: DateTime; ``` In this case, the returned array value has the start of the range in the 0 index of the array, and the end of the range in the 1 index. # Decorators ## Title The label for the input, to be used instead of the default title case conversion of the property name. ```typescript @title("A Custom Title") simpleProperty : string; ``` ![Title](https://developer.salesforce.com/media/interface-template-configuration/Title.png) --- ## Subtitle A subtitle for the input. ```typescript @subtitle("Some information about what to put here") simpleProperty : string; ``` ![Subtitle](https://developer.salesforce.com/media/interface-template-configuration/Subtitle.png) --- ## Units For information on the `@units` decorator, see [Number Property](https://developer.salesforce.com/docs/marketing/personalization/guide/properties/number.md). --- ## Header Adds a header before the decorated property, indicating all properties below this decorator (until the next header) are intended to be a section on configurables. ```typescript simpleProperty : string; @header('Section Header') simpleProperty2: string; simpleProperty3: string; ``` ![Header](https://developer.salesforce.com/media/interface-template-configuration/Header.png) In the preceding example, `simpleProperty2` and `simpleProperty3` fall under the header "Section Header". --- ## Header Subtitle Additional text to describe a section, can be used to provide the end user with instructions. ```typescript simpleProperty : string; @header('Section Header') @headerSubtitle('A brief explanation of this section.') simpleProperty2: string; simpleProperty3: string; ``` ![HeaderSubtitle](https://developer.salesforce.com/media/interface-template-configuration/HeaderSubtitle.png) ## Markdown Basic Markdown rendered before the decorated property. Syntaxes are limited to the following examples. ```typescript @markdown(` # H1 ## H2 ### H3 #### H4 ##### H5 ###### H6 **bold text** *italicized text* Ordered Lists 1. Ordered List Item 1 2. Ordered List Item 2 3. Ordered List Item 3 Unordered Lists - Unordered List Item 1 - Unordered List Item 2 Horizontal Rule --- [Link](https://www.salesforce.com/) `) simpleProperty : string; ``` ![Markdown](https://developer.salesforce.com/media/interface-template-configuration/Markdown.png) --- ## Optional The `@optional` decorator enables you to flag properties as either optional or required. ```typescript @optional(true) simpleProperty : string; ``` **note:** Personalization doesn’t prevent you from saving a campaign with empty or incomplete template properties that are flagged as required. For example, even if a `simpleText: string` property is tagged as required by using the `@optional(false)` decorator, a campaign developer can still save a campaign with the `simpleText` property field left empty or incomplete. --- ## Hidden Hides the input from the user. ```typescript @hidden(true) simpleProperty : string; ``` This decorator can also accept a function in order to hide configurables conditionally. The arguments for the decorator are `this`, and a function whose first argument is `self`, the instance of this class. The function MUST return a boolean `true` or `false`. Returning a non-boolean `truthy` or `falsy` value (for example, 0, 1, 'some string') throws an error. ```typescript simpleBoolean : boolean; @hidden(this, (self) => self.simpleBoolean) conditionalField: string; ``` --- ## Shown If The inverse of the `@hidden` decorator, displays the field if true, hides the field if false. ```typescript @shownIf(true) simpleProperty : string; ``` ```typescript simpleBoolean : boolean; @shownIf(this, (self) => self.simpleBoolean) conditionalField: string; ``` --- ## Rich Text For information on the `@richText` decorator, see [String Property](https://developer.salesforce.com/docs/marketing/personalization/guide/properties/string.md). --- ## Options For information on the `@options` decorator, see [Select Property](https://developer.salesforce.com/docs/marketing/personalization/guide/properties/select.md). --- ## Button Group Renders a static list of options as a button group. ```typescript @buttonGroup(true) simpleOption : "option1" | "option2" | "option3"; ``` --- ## Range For information on the `@range` decorator, see [DateTime Property](https://developer.salesforce.com/docs/marketing/personalization/guide/properties/datetime.md). --- ## Tabular Displays the fields of a complex object horizontally. ```typescript @tabular() complexObject : SomeComplexType; ``` Arrays of complex objects are rendered like a table. Include `headersPerRow: true` to show field titles on each row. Optionally render a line to visually divide each row by including `dividerLine: true`. ```typescript @tabular({dividerLine: true, headersPerRow:false}) complexObjectList : SomeComplexType[]; ``` # ecommerce The following code sample depicts sitemap code for an imaginary implementation of Marketing Cloud Personalization on the Northern Trail Outfitters website `https://www.northerntrailoutfitters.com`. This sitemap code is not exhaustive and maps only key areas valuable for demonstrating an example implementation of Personalization. The primary focus of this sitemap code is to depict how to approach designing a Catalog for ecommerce websites. **important:** This example sitemap is for reference purposes only. Avoid copying this code into your Sitemap Editor as it does not produce the results you intended. This example includes an SKU implementation, however most sitemaps don't require them. For more information about Marketing Cloud Personalization SKU implementation, see [Stock Keeping Units](https://help.salesforce.com/s/articleView?id=mktg.mc_pers_catalog_object_built_in.htm&type=5#:~:text=currency%2C%20and%20rating.-,Stock%20Keeping%20Units,-Stock%20Keeping%20Units). ## Example Sitemap in the SalesforceInteractions Namespace ```javascript SalesforceInteractions.init({ cookieDomain: "northerntrailoutfitters.com", consents: [ { purpose: SalesforceInteractions.mcis.ConsentPurpose.Personalization, provider: "Example Consent Manager", status: SalesforceInteractions.ConsentStatus.OptIn, }, ], }).then(() => { const sitemapConfig = { global: { onActionEvent: (actionEvent) => { const email = SalesforceInteractions.mcis.getValueFromNestedObject( "window._etmc.user_info.email", ); if (email) { actionEvent.user = actionEvent.user || {}; actionEvent.user.identities = actionEvent.user.identities || {}; actionEvent.user.identities.emailAddress = email; } return actionEvent; }, contentZones: [ { name: "global_infobar_top_of_page", selector: "header.site-header" }, { name: "global_infobar_bottom_of_page", selector: "footer.site-footer" }, ], listeners: [ SalesforceInteractions.listener("submit", ".email-signup", () => { const email = SalesforceInteractions.cashDom("#dwfrm_mcsubscribe_email").val(); if (email) { SalesforceInteractions.sendEvent({ interaction: { name: "Email Sign Up - Footer" }, user: { identities: { emailAddress: email } }, }); } }), ], }, pageTypeDefault: { name: "default", interaction: { name: "Default Page", }, }, pageTypes: [ { name: "product_detail", /* The best practice for isMatch is to match as quickly as possible. If matching immediately is not an option, you can use a Promise. The Promise should resolve true or false and not pend indefinitely. This Promise example uses a setTimeout to prevent the isMatch from pending indefinitely if the match condition is not met fast enough. In this scenario, we know that the match condition will be met within 50 milliseconds or not at all. Note that using a timeout like this might not be sufficient in all cases and if you are using a Promise it should be tailored to your specific use-case. */ isMatch: () => new Promise((resolve, reject) => { let isMatchPDP = setTimeout(() => { resolve(false); }, 50); return SalesforceInteractions.DisplayUtils.pageElementLoaded( "div.page[data-action='Product-Show']", "html", ).then(() => { clearTimeout(isMatchPDP); resolve(true); }); }), interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Product", id: () => { return SalesforceInteractions.util.resolveWhenTrue.bind(() => { const productId = SalesforceInteractions.cashDom(".product-id").first().text(); const products = getProductsFromDataLayer(); if (products && products.length > 0) { return products[0].id; } else if (productId) { return productId; } else { return false; } }); }, attributes: { sku: { id: SalesforceInteractions.cashDom(".product-detail[data-pid]").attr("data-pid"), }, name: SalesforceInteractions.resolvers.fromJsonLd("name"), description: SalesforceInteractions.resolvers.fromSelector( ".short-description", (desc) => desc.trim(), ), url: SalesforceInteractions.resolvers.fromHref(), imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute( ".product-carousel .carousel-item[data-slick-index='0'] img", "src", (url) => window.location.origin + url, ), inventoryCount: 1, price: SalesforceInteractions.resolvers.fromSelector( ".prices .price .value", (price) => parseFloat(price.replace(/[^0-9\.]+/g, "")), ), rating: () => { return SalesforceInteractions.mcis.extractFirstGroup( /([.\w]+) out of/, SalesforceInteractions.cashDom(".ratings .sr-only").text(), ); }, }, relatedCatalogObjects: { Category: SalesforceInteractions.DisplayUtils.pageElementLoaded( ".container .product-breadcrumb .breadcrumb a", "html", ).then((ele) => { return SalesforceInteractions.resolvers.buildCategoryId( ".container .product-breadcrumb .breadcrumb a", null, null, (categoryId) => [categoryId.toUpperCase()], ); }), Gender: SalesforceInteractions.DisplayUtils.pageElementLoaded( ".product-breadcrumb .breadcrumb a, h1.product-name", "html", ).then((ele) => { if ( SalesforceInteractions.cashDom(".product-breadcrumb .breadcrumb a") .first() .text() .toLowerCase() === "women" || SalesforceInteractions.cashDom("h1.product-name").text().indexOf("Women") >= 0 ) { return ["WOMEN"]; } else if ( SalesforceInteractions.cashDom(".product-breadcrumb .breadcrumb a") .first() .text() .toLowerCase() === "men" || SalesforceInteractions.cashDom("h1.product-name").text().indexOf("Men") >= 0 ) { return ["MEN"]; } else { return; } }), Color: SalesforceInteractions.DisplayUtils.pageElementLoaded( ".attributes", "html", ).then((ele) => { return SalesforceInteractions.resolvers.fromSelectorAttributeMultiple( ".color-value", "data-attr-value", ); }), Feature: SalesforceInteractions.DisplayUtils.pageElementLoaded( ".features", "html", ).then((ele) => { return SalesforceInteractions.resolvers.fromSelectorMultiple( ".long-description li", (features) => { return features.map((feature) => { return feature.trim().toUpperCase(); }); }, ); }), }, }, }, contentZones: [ { name: "product_detail_recs_row_1", selector: ".row.recommendations div[id*='cq']:nth-of-type(1)", }, { name: "product_detail_recs_row_2", selector: ".row.recommendations div[id*='cq']:nth-of-type(2)", }, { name: "testHeader", selector: ".site-header" }, ], listeners: [ SalesforceInteractions.listener("click", ".add-to-cart", () => { let lineItem = SalesforceInteractions.mcis.buildLineItemFromPageState( "select.quantity-select option:checked", ); SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.CartInteractionName.AddToCart, lineItem: lineItem, }, }); }), SalesforceInteractions.listener("click", ".attribute", (event) => { let classList = event.target.classList.value.split(" "); if (classList.includes("color-value") || classList.includes("size-value")) { SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObjectDetail, catalogObject: { type: "Product", id: SalesforceInteractions.getSitemapResult().currentPage.interaction .catalogObject.id, attributes: { sku: { id: SalesforceInteractions.cashDom(".product-detail[data-pid]").attr( "data-pid", ), }, }, relatedCatalogObjects: { Color: [ SalesforceInteractions.cashDom(".color-value.selected").attr( "data-attr-value", ), ], }, }, }, }); } }), ], }, { name: "Category", isMatch: () => new Promise((resolve, reject) => { let isMatchCat = setTimeout(() => { resolve(false); }, 50); return SalesforceInteractions.DisplayUtils.pageElementLoaded( "#product-search-results", "html", ).then(() => { clearTimeout(isMatchCat); resolve(SalesforceInteractions.cashDom(".breadcrumb").length > 0); }); }), interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Category", id: SalesforceInteractions.DisplayUtils.pageElementLoaded( ".breadcrumb .breadcrumb-item a", "html", ).then((ele) => { return SalesforceInteractions.resolvers.buildCategoryId( ".breadcrumb .breadcrumb-item a", 1, null, (categoryId) => categoryId.toUpperCase(), ); }), }, }, listeners: [ SalesforceInteractions.listener("click", ".quickview", (e) => { const pid = SalesforceInteractions.cashDom(e.target).attr("href").split("pid=")[1]; if (!pid) { return; } SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.QuickViewCatalogObject, catalogObject: { type: "Product", id: pid, }, }, }); }), SalesforceInteractions.listener("click", "body", (e) => { if ( SalesforceInteractions.cashDom(e.target).closest("button[data-dismiss='modal']") .length > 0 ) { SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.mcis.CatalogObjectInteractionName .StopQuickViewCatalogObject, }, }); } else if ( SalesforceInteractions.cashDom(e.target).closest("#quickViewModal").length > 0 && SalesforceInteractions.cashDom(e.target).find("#quickViewModal .modal-dialog") .length > 0 ) { SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.mcis.CatalogObjectInteractionName .StopQuickViewCatalogObject, }, }); } }), ], }, { name: "department", isMatch: () => new Promise((resolve, reject) => { let isMatchDept = setTimeout(() => { resolve(false); }, 50); return SalesforceInteractions.DisplayUtils.pageElementLoaded( "div.category-tile", "html", ).then(() => { clearTimeout(isMatchDept); resolve(!/\/homepage/.test(window.location.href)); }); }), }, { name: "search_results", isMatch: () => /\/default\/search$/.test(window.location.pathname), }, { name: "cart", isMatch: () => /\/cart/.test(window.location.href), interaction: { name: SalesforceInteractions.CartInteractionName.ReplaceCart, lineItems: SalesforceInteractions.DisplayUtils.pageElementLoaded( ".cart-empty, .checkout-btn", "html", ).then(() => { let cartLineItems = []; SalesforceInteractions.cashDom(".product-info .product-details").each((index, ele) => { let itemQuantity = parseInt( SalesforceInteractions.cashDom(ele).find(".qty-card-quantity-count").text().trim(), ); if (itemQuantity && itemQuantity > 0) { let lineItem = { catalogObjectType: "Product", catalogObjectId: SalesforceInteractions.cashDom(ele) .find(".line-item-quanity-info") .attr("data-pid"), price: SalesforceInteractions.cashDom(ele) .find(".pricing") .text() .replace(/[^0-9\.]+/g, "") / itemQuantity, quantity: itemQuantity, }; cartLineItems.push(lineItem); } }); return cartLineItems; }), }, }, { name: "order_confirmation", isMatch: () => /\/confirmation/.test(window.location.href), interaction: { name: SalesforceInteractions.OrderInteractionName.Purchase, order: { id: SalesforceInteractions.DisplayUtils.pageElementLoaded(".order-number", "html").then( (ele) => { return SalesforceInteractions.resolvers.fromSelector(".order-number"); }, ), lineItems: SalesforceInteractions.DisplayUtils.pageElementLoaded( ".product-line-item", "html", ).then(() => { let purchaseLineItems = []; SalesforceInteractions.cashDom(".product-line-item").each((index, ele) => { let itemQuantity = parseInt( SalesforceInteractions.cashDom(ele) .find(".qty-card-quantity-count") .text() .trim(), ); if (itemQuantity && itemQuantity > 0) { let lineItem = { catalogObjectType: "Product", catalogObjectId: SalesforceInteractions.cashDom(ele) .find(".line-item-quanity-info") .attr("data-pid") .trim(), price: SalesforceInteractions.cashDom(ele) .find(".pricing") .text() .trim() .replace(/[^0-9\.]+/g, "") / itemQuantity, quantity: itemQuantity, }; purchaseLineItems.push(lineItem); } }); return purchaseLineItems; }), }, }, }, { name: "login", isMatch: () => /\/login/.test(window.location.href) && window.location.hostname !== "community.northerntrailoutfitters.com", interaction: { name: "Login", }, listeners: [ SalesforceInteractions.listener("click", "form[name='login-form'] button", () => { const email = SalesforceInteractions.cashDom("#login-form-email").val(); if (email) { SalesforceInteractions.sendEvent({ interaction: { name: "Logged In" }, user: { identities: { emailAddress: email } }, }); } }), ], }, { name: "home", isMatch: () => /\/homepage/.test(window.location.href), interaction: { name: "Homepage", }, contentZones: [ { name: "home_hero", selector: ".experience-carousel-bannerCarousel" }, { name: "home_sub_hero", selector: ".experience-carousel-bannerCarousel + .experience-component", }, { name: "home_popup" }, ], }, { name: "community_login", isMatch: () => window.location.hostname === "community.northerntrailoutfitters.com" && /\/s\/login/.test(window.location.href), interaction: { name: "Community Login", }, listeners: [ SalesforceInteractions.listener("click", ".loginButton", (event) => { const email = SalesforceInteractions.cashDom("input[placeholder='Email']").val(); if (email) { SalesforceInteractions.sendEvent({ interaction: { name: "Community Log In" }, user: { identities: { emailAddress: email } }, }); } }), ], }, { name: "community_home", isMatch: () => { return ( window.location.hostname === "community.northerntrailoutfitters.com" && /^\/s\/?$/.test(window.location.pathname) ); }, interaction: { name: "Community Homepage", }, listeners: [ SalesforceInteractions.listener("click", "li.topicItem", (event) => { const topicLabel = SalesforceInteractions.cashDom(event.currentTarget) .find(".topicLabel") .text() .trim(); if (topicLabel) { SalesforceInteractions.sendEvent({ interaction: { name: "Community Homepage - " + topicLabel }, }); } }), ], }, ], }; const getProductsFromDataLayer = () => { if (window.dataLayer) { for (let i = 0; i < window.dataLayer.length; i++) { if ( ((window.dataLayer[i].ecommerce && window.dataLayer[i].ecommerce.detail) || {}).products ) { return window.dataLayer[i].ecommerce.detail.products; } } } }; SalesforceInteractions.initSitemap(sitemapConfig); }); ``` ## Example Sitemap in the Evergage Namespace ```javascript Evergage.init({ cookieDomain: "northerntrailoutfitters.com", }).then(() => { const config = { global: { onActionEvent: (actionEvent) => { const email = Evergage.util.getValueFromNestedObject("window._etmc.user_info.email"); if (email) { actionEvent.user = actionEvent.user || {}; actionEvent.user.attributes = actionEvent.user.attributes || {}; actionEvent.user.attributes.emailAddress = email; } return actionEvent; }, contentZones: [ { name: "global_infobar_top_of_page", selector: "header.site-header" }, { name: "global_infobar_bottom_of_page", selector: "footer.site-footer" }, ], listeners: [ Evergage.listener("submit", ".email-signup", () => { const email = Evergage.cashDom("#dwfrm_mcsubscribe_email").val(); if (email) { Evergage.sendEvent({ action: "Email Sign Up - Footer", user: { attributes: { emailAddress: email } }, }); } }), ], }, pageTypeDefault: { name: "default", }, pageTypes: [ { name: "product_detail", isMatch: () => { return Evergage.DisplayUtils.pageElementLoaded("div.page", "html").then((ele) => { const pageType = Evergage.cashDom(ele).attr("data-action"); return pageType === "Product-Show"; }); }, catalog: { Product: { _id: () => { return Evergage.util.resolveWhenTrue.bind(() => { const productId = Evergage.cashDom(".product-id").first().text(); const products = getProductsFromDataLayer(); if (products && products.length > 0) { return products[0].id; } else if (productId) { return productId; } else { return false; } }); }, sku: { _id: Evergage.cashDom(".product-detail[data-pid]").attr("data-pid") }, name: Evergage.resolvers.fromJsonLd("name"), description: Evergage.resolvers.fromSelector(".short-description"), url: Evergage.resolvers.fromHref(), imageUrl: Evergage.resolvers.fromSelectorAttribute( ".product-carousel .carousel-item[data-slick-index='0'] img", "src", ), inventoryCount: 1, price: Evergage.resolvers.fromSelector(".prices .price .value"), rating: () => { return Evergage.util.extractFirstGroup( /([.\w]+) out of/, Evergage.cashDom(".ratings .sr-only").text(), ); }, categories: () => { return Evergage.DisplayUtils.pageElementLoaded( ".container .product-breadcrumb .breadcrumb a", "html", ).then((ele) => { return Evergage.resolvers.buildCategoryId( ".container .product-breadcrumb .breadcrumb a", null, null, (categoryId) => [categoryId.toUpperCase()], ); }); }, relatedCatalogObjects: { // In case you're using 'dimensions' instead of 'relatedCatalogObjects', you can continue to do so as they both function the same way. Gender: () => { return Evergage.DisplayUtils.pageElementLoaded( ".product-breadcrumb .breadcrumb a, h1.product-name", "html", ).then((ele) => { if ( Evergage.cashDom(".product-breadcrumb .breadcrumb a") .first() .text() .toLowerCase() === "women" || Evergage.cashDom("h1.product-name").text().indexOf("Women") >= 0 ) { return ["WOMEN"]; } else if ( Evergage.cashDom(".product-breadcrumb .breadcrumb a") .first() .text() .toLowerCase() === "men" || Evergage.cashDom("h1.product-name").text().indexOf("Men") >= 0 ) { return ["MEN"]; } else { return; } }); }, Color: () => { return Evergage.DisplayUtils.pageElementLoaded(".attributes", "html").then( (ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple( ".color-value", "data-attr-value", ); }, ); }, Feature: () => { return Evergage.DisplayUtils.pageElementLoaded(".features", "html").then((ele) => { return Evergage.resolvers.fromSelectorMultiple( ".features .feature", (features) => { return features.map((feature) => { return feature.trim(); }); }, ); }); }, }, }, }, contentZones: [ { name: "product_detail_recs_row_1", selector: ".row.recommendations div[id*='cq']:nth-of-type(1)", }, { name: "product_detail_recs_row_2", selector: ".row.recommendations div[id*='cq']:nth-of-type(2)", }, { name: "testHeader", selector: ".site-header" }, ], listeners: [ Evergage.listener("click", ".add-to-cart", () => { const lineItem = Evergage.util.buildLineItemFromPageState("select[id*=quantity]"); lineItem.sku = { _id: Evergage.cashDom(".product-detail[data-pid]").attr("data-pid") }; Evergage.sendEvent({ itemAction: Evergage.ItemAction.AddToCart, cart: { singleLine: { Product: lineItem, }, }, }); }), Evergage.listener("click", ".attribute", (event) => { let classList = event.target.classList.value.split(" "); if (classList.includes("color-value") || classList.includes("size-value")) { Evergage.sendEvent({ itemAction: Evergage.ItemAction.ViewItemDetail, catalog: { Product: { _id: Evergage.util.buildLineItemFromPageState("select[id*=quantity]")._id, sku: { _id: Evergage.cashDom(".product-detail[data-pid]").attr("data-pid") }, relatedCatalogObjects: { Color: [Evergage.cashDom(".color-value.selected").attr("data-attr-value")], }, }, }, }); } }), ], }, { name: "Category", action: "Viewed Category", isMatch: () => { return Evergage.DisplayUtils.pageElementLoaded(".page", "html").then((ele) => { const pageType = Evergage.cashDom(ele).attr("data-action"); return pageType === "Search-Show" && Evergage.cashDom(".breadcrumb").length > 0; }); }, catalog: { Category: { _id: () => { return Evergage.DisplayUtils.pageElementLoaded( ".breadcrumb .breadcrumb-item a", "html", ).then((ele) => { return Evergage.resolvers.buildCategoryId( ".breadcrumb .breadcrumb-item a", 1, null, (categoryId) => categoryId.toUpperCase(), ); }); }, }, }, listeners: [ Evergage.listener("click", ".quickview", (e) => { const pid = Evergage.cashDom(e.target).attr("href").split("pid=")[1]; if (!pid) { return; } Evergage.sendEvent({ action: "Category Page Quick View", itemAction: Evergage.ItemAction.QuickViewItem, catalog: { Product: { _id: pid, }, }, }); }), Evergage.listener("click", "body", (e) => { if (Evergage.cashDom(e.target).closest("button[data-dismiss='modal']").length > 0) { Evergage.sendEvent({ action: "Close Quick View", itemAction: Evergage.ItemAction.StopQuickViewItem, }); } else if ( Evergage.cashDom(e.target).closest("#quickViewModal").length > 0 && Evergage.cashDom(e.target).find("#quickViewModal .modal-dialog").length > 0 ) { Evergage.sendEvent({ action: "Close Quick View", itemAction: Evergage.ItemAction.StopQuickViewItem, }); } }), ], }, { name: "department", isMatch: () => { return Evergage.DisplayUtils.pageElementLoaded("div.category-tile", "html").then( (ele) => { return !/\/homepage/.test(window.location.href); }, ); }, }, { name: "search_results", isMatch: () => /\/default\/search$/.test(window.location.pathname), }, { name: "cart", isMatch: () => /\/cart/.test(window.location.href), itemAction: Evergage.ItemAction.ViewCart, catalog: { Product: { lineItems: { _id: () => { return Evergage.DisplayUtils.pageElementLoaded( ".cart-empty, .checkout-btn", "html", ).then((ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple( ".product-info .product-details .line-item-quantity-info", "data-pid", ); }); }, price: () => { return Evergage.DisplayUtils.pageElementLoaded( ".cart-empty, .checkout-btn", "html", ).then((ele) => { return Evergage.resolvers.fromSelectorMultiple( ".product-info .product-details .pricing", ); }); }, quantity: () => { return Evergage.DisplayUtils.pageElementLoaded( ".cart-empty, .checkout-btn", "html", ).then((ele) => { return Evergage.resolvers.fromSelectorMultiple( ".product-info .product-details .qty-card-quantity-count", ); }); }, }, }, }, }, { name: "order_confirmation", isMatch: () => /\/confirmation/.test(window.location.href), itemAction: Evergage.ItemAction.Purchase, catalog: { Product: { orderId: () => { return Evergage.DisplayUtils.pageElementLoaded(".order-number", "html").then( (ele) => { return Evergage.resolvers.fromSelector(".order-number"); }, ); }, lineItems: { _id: () => { return Evergage.DisplayUtils.pageElementLoaded( ".product-line-item line-item-quantity-info", "html", ).then((ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple( ".product-line-item .line-item-quantity-info", "data-pid", ); }); }, price: () => { return Evergage.DisplayUtils.pageElementLoaded( ".product-line-item .pricing", "html", ).then((ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple( ".product-line-item .pricing", "data-pid", ); }); }, quantity: () => { return Evergage.DisplayUtils.pageElementLoaded( ".product-line-item .qty-card-quantity-count", "html", ).then((ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple( ".product-line-item .qty-card-quantity-count", "data-pid", ); }); }, }, }, }, }, { name: "login", action: "Login", isMatch: () => /\/login/.test(window.location.href) && window.location.hostname !== "community.northerntrailoutfitters.com", listeners: [ Evergage.listener("click", "form[name='login-form'] button", () => { const email = Evergage.cashDom("#login-form-email").val(); if (email) { Evergage.sendEvent({ action: "Logged In", user: { attributes: { emailAddress: email } }, }); } }), ], }, { name: "home", action: "Homepage", isMatch: () => /\/homepage/.test(window.location.href), contentZones: [ { name: "home_hero", selector: ".experience-carousel-bannerCarousel" }, { name: "home_sub_hero", selector: ".experience-carousel-bannerCarousel + .experience-component", }, { name: "home_popup" }, ], }, { name: "community_login", action: "Community Login", isMatch: () => window.location.hostname === "community.northerntrailoutfitters.com" && /\/s\/login/.test(window.location.href), listeners: [ Evergage.listener("click", ".loginButton", (event) => { const email = Evergage.cashDom("input[placeholder='Email']").val(); if (email) { Evergage.sendEvent({ action: "Community Log In", user: { attributes: { emailAddress: email } }, }); } }), ], }, { name: "community_home", action: "Community Homepage", isMatch: () => { return ( window.location.hostname === "community.northerntrailoutfitters.com" && /^\/s\/?$/.test(window.location.pathname) ); }, listeners: [ Evergage.listener("click", "li.topicItem", (event) => { const topicLabel = Evergage.cashDom(event.currentTarget) .find(".topicLabel") .text() .trim(); if (topicLabel) { Evergage.sendEvent({ action: "Community Homepage - " + topicLabel }); } }), ], }, ], }; const getProductsFromDataLayer = () => { if (window.dataLayer) { for (let i = 0; i < window.dataLayer.length; i++) { if ( ((window.dataLayer[i].ecommerce && window.dataLayer[i].ecommerce.detail) || {}).products ) { return window.dataLayer[i].ecommerce.detail.products; } } } }; Evergage.initSitemap(config); }); ``` # Einstein Decisions - Server-Side Template When using Einstein Decisions in a template, a slightly different server-side code configuration is necessary for a server-side template than a Web template. This article describes the fundamental differences between the two types of templates concerning the following two critical parts of the template. - What content zone or tag the bandit leverages for decision-making - What data is returned on the promotion ## Global Web Template ### Content Zone or Tag Selection: context.contentZone The Global Web Template leverages `context.contentZone` to determine which promotion content zone is to be evaluated for a decision by Einstein Decisions. The `context.contentZone` value maps to the targeted web content zone of the template, meaning the targeted web content zone serves the following two purposes. - Determining where the Einstein Decisions template is allowed to render on the site - Filtering which promotions are eligible to be returned by the template (only promotions with an asset that has a content zone or tag that maps to the targeted web content zone are eligible for decision-making) To configure the targeted web content zone, you can use the **Content Zones in Template** selector in the Web Template Editor to select a single value or define a set of values for business users to pick from via a dropdown menu. ![Einstein Decisions Web contentZone](https://developer.salesforce.com/media/campaign-development/einstein-decisions-content-zone-web.png) By leveraging `context.contentZone` in the template, you effectively restrict which promotions can be returned. Since the **Content Zones in Template** selector in the Web Template Editor only displays web content zones defined in the sitemap, only promotions with an asset tied to a web content zone are eligible to return. ### Campaign Response: Image URL and Promotion URL The Global Web Template returns the primary data required for client-side rendering. In this case, the template is set to return just the image URL for the selected asset and the promotion URL. You can easily configure additional promotion attributes/information to return via the web template. ## Server-Side Template ### Content Zone or Tag Selection: ContentZoneLookup The server-side iteration of the template gives you a little more flexibility. Since the **Content Zones in Template** selector leveraged in web templates is not available in the server-side template interface, use `ContentZoneLookup` instead of referencing `context.contentZone`. Unlike the web content zone selector, `ContentZoneLookup` is not restricted to only content zones defined in the sitemap. Instead, it enables a business user to select any content zone or tag value stored against a promotion asset or defined in the sitemap. The value that a business user selects will reflect the promotion content zone or tag that Einstein Decisions then leverages for real-time evaluation and decision-making. ### Campaign Response: Full Promotion Since Marketing Cloud Personalization is not responsible for rendering the decision passed back via a server-side campaign, the Server-Side template is designed to return all promotion information for the selected promotion by default. You can easily restrict what is returned in the response if the complete promotion data is not required. Additionally, since the complete promotion data is being returned, as opposed to asset-specific data like an image URL, a business user only needs to define a fallback promotion as opposed to a fallback promotion along with a fallback asset. ## Template Selection Based on Click Log Tracking To properly train the contextual bandit, it is essential to ensure click log generation. The Einstein Decisions global web template automatically tracks click logs and view logs in web campaigns. However, only view logs are tracked automatically in server-side Einstein Decisions campaigns, while clicks require manual tracking. This section outlines two approaches for ensuring the proper capture of click logs. ### Template Option 1: Return Selected Content Zone or Tag in the Template run() Function This option requires `contentZone` to be set on the experience. You can set `contentZone` on the experience in server-side campaigns by returning the `contentZone` explicitly through the server-side template's `run` function. The following code depicts an example template for this approach. ```typescript import { ContextualBanditConfig, decide } from "corvus"; import { ItemReference, ContentZoneLookup } from "common"; export class PromotionSearchOptions implements Search { search(context: GearLifecycleContext, searchString: string): ItemReference[] { if (!searchString) { return []; } const promos: Promotion[] = context.services.catalog.findByName( "Promotion", searchString, ) as Promotion[]; return promos.reduce((allPromos: ItemReference[], promo: Promotion) => { const promoItem = { itemType: "Promotion", id: promo.id, label: promo.attributes["name"] != null ? promo.attributes["name"].value : promo.id, } as ItemReference; allPromos.push(promoItem); return allPromos; }, []); } } export class EinsteinDecisionsTemplate implements CampaignTemplateComponent { @lookupOptions(() => new ContentZoneLookup()) assetContentZoneOrTag: string; @searchOptions((self) => new PromotionSearchOptions()) @title("Optional Fallback Promotion Selector") @subtitle(`Search for a fallback promotion to return if there are no eligible promotions to return for the end user. If no fallback is selected, no promotion will be returned. (NOTE: This field is case-sensitive.)`) fallbackArm: ItemReference; run(context: CampaignComponentContext) { const banditConfig: ContextualBanditConfig = { maxResults: 1, contentZone: this.assetContentZoneOrTag, fallbackArms: this.fallbackArm ? [this.fallbackArm.id] : [], } as ContextualBanditConfig; const promotions: Promotion[] = decide(context, banditConfig, null) as Promotion[]; return { promotions, contentZone: this.assetContentZoneOrTag }; } } ``` In the preceding template configuration, note that the selected `contentZone` in the template configuration screen gets applied to the experience. Therefore, for this campaign/experience to return, the inbound event request to Personalization must include a matching `contentZone` value on the `source` object. The campaign does not return if there is no matching `contentZone` value on the inbound event. If the desired `contentZone` is unknown when sending an event request to Personalization, we recommend leveraging [Option 2](#template-option-2-modified-stat-event-leveraging-click-through-item-action), which allows you to avoid setting a `contentZone` on the campaign/experience to ensure that the campaign can return without a `contentZone` being specified on the `source` object on the inbound event. The following code depicts the formatting of an example `stat` event using the preceding template. ```json { "source": { "channel": "Server", "pageType": "Homepage" }, "user": { "id": "sample user" }, "campaignStats": [ { "stat": "Click", "experienceId": "experience Id", "control": false, "catalog": { "Promotion": ["promotion Id"] } } ] } ``` ### Template Option 2: Modified Stat Event Leveraging Click Through Item Action As mentioned in Option 1, using the template configuration that sets the `contentZone` via the `run` function means that the experience only returns when `contentZone` is present in the original event. If the `contentZone` is unknown at the campaign request time, you can use the following template configuration and ensure click log generation using a modified `stat` event that includes the `contentZone` leveraged in the campaign decision. The only difference between the following template and the preceding one is the `return` object of the `run` function. ```typescript import { ContextualBanditConfig, decide } from "corvus"; import { ItemReference, ContentZoneLookup } from "common"; export class PromotionSearchOptions implements Search { search(context: GearLifecycleContext, searchString: string): ItemReference[] { if (!searchString) { return []; } const promos: Promotion[] = context.services.catalog.findByName( "Promotion", searchString, ) as Promotion[]; return promos.reduce((allPromos: ItemReference[], promo: Promotion) => { const promoItem = { itemType: "Promotion", id: promo.id, label: promo.attributes["name"] != null ? promo.attributes["name"].value : promo.id, } as ItemReference; allPromos.push(promoItem); return allPromos; }, []); } } export class EinsteinDecisionsTemplate implements CampaignTemplateComponent { @lookupOptions(() => new ContentZoneLookup()) assetContentZoneOrTag: string; @searchOptions((self) => new PromotionSearchOptions()) @title("Optional Fallback Promotion Selector") @subtitle(`Search for a fallback promotion to return if there are no eligible promotions to return for the end user. If no fallback is selected, no promotion will be returned. (NOTE: This field is case-sensitive.)`) fallbackArm: ItemReference; run(context: CampaignComponentContext) { const banditConfig: ContextualBanditConfig = { maxResults: 1, contentZone: this.assetContentZoneOrTag, fallbackArms: this.fallbackArm ? [this.fallbackArm.id] : [], } as ContextualBanditConfig; const promotions: Promotion[] = decide(context, banditConfig, null) as Promotion[]; return { promotions }; } } ``` The following code depicts the formatting of an example `stat` event to ensure proper click log generation using the preceding template. ```json { "source": { "channel": "Server", "pageType": "Homepage", "contentZones": ["content zone"] }, "user": { "id": "sample user" }, "itemAction": "Click Through", "catalog": { "Promotion": { "id": "promo" } }, "campaignStats": [ { "stat": "Click", "experienceId": "experience Id", "control": false, "catalog": { "Promotion": ["promotion Id"] } } ] } ``` The preceding `stat` event includes the standard click campaign `stat` object that populates the clicks against the campaign and the promotion on the promoted item detail stat screen. This `stat` event is different from the `stat` event outlined in Option 1 in the following key ways. - **`contentZone` included on the `source` object**: In [Option 1](#template-option-1-return-selected-content-zone-or-tag-in-the-template-run-function), the `contentZone` is set on the campaign/experience and is therefore not required in the `stat` event. In this option, since the `contentZone` is not set on the campaign/experience, it needs to be included on the `stat` event to allow for click log generation to support model training. The `contentZone` or tag selected in the template configuration for decisioning purposes is available on the campaign response object as the `assetContentZoneOrTag` value. The web developer configuring the `stat` event can easily reference that value to populate the `contentZone` value on the `stat` event. - **`Click Through` Item Action**: This item action is only leveraged in this specific instance to track clicks on the promotion object. Paired with the `contentZone` on the `source` object, this item action causes click logs to be generated for model training. When sent to Personalization, a single event has to include the campaign stat object and the `Click Through` item action object. The `return` object in the first template sets the selected asset content zone or tag as the `contentZone` on the experience and campaign response object, while this template does not. Instead, it leaves the `contentZone` value blank. # Event API Requests The Personalization Event API uses standard HTTP methods and returns JSON-formatted responses to requests generated by the Sitemap and other Personalization event sources. The following sections describe the various Event API endpoints that enable you to interact with the Personalization platform. ## Send Event Data Send event data to the Personalization dataset specified in the request. ### Method and Endpoint `POST` `/api2/event/` ### Request URL `https://..evergage.com/api2/event/` ### URL Parameters | URL Parameter | Required? | Description | | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `accountName` | Yes | The Personalization account name. You can find the `accountName` by accessing **Gears** from the Personalization UI and reviewing the URL. For example, if the Gears URL is `https://demo.us-1.evergage.com/`, then the account name is `demo`. | | `instance` | Yes | The Personalization instance identifier. You can find this value by accessing **Gears** from the Personalization UI and reviewing the URL. For example, if the Gears URL is `https://demo.us-1.evergage.com/`, then the instance identifier is `us-1`. | | `dataset` | Yes | Name or identifier of the Personalization dataset you're sending data to. | ### Supported Content Types - `application/json` - `application/x-www-form-urlencoded` ### Examples Consider the following as sample event data that you would like to send to Personalization. ```json { "action": "hello world", "user": { "id": "testuser" } } ``` You can send this event data to Personalization as a JSON payload by [minifying](https://www.npmjs.com/package/json-minify) the event data to remove whitespaces, including it in the body of your API request, and setting the `Content-Type` to `application/json`. ```bash curl -X POST \ -H 'Content-Type: application/json' \ -d '{"action":"hello world","user":{"id":"testuser"}}' \ "https://..evergage.com/api2/event/" ``` **important:** Don’t make `POST` requests with `Content-Type: application/json` from an environment that checks for Cross-Origin Resource Sharing (CORS), such as a web browser. When using `Content-Type: application/x-www-form-urlencoded`, the event data must be minified and then base64-encoded. Minifying the preceding example event data JSON gives you the following single-line JSON string. ```json { "action": "hello world", "user": { "id": "testuser" } } ``` Base64-encoding the above JSON string gives you the value `eyJhY3Rpb24iOiJoZWxsbyB3b3JsZCIsInVzZXIiOnsiaWQiOiJ0ZXN0dXNlciJ9fQ==`. You can then pass this value via the `event` key, as shown in the following example request. ```bash curl -X POST \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'event=eyJhY3Rpb24iOiJoZWxsbyB3b3JsZCIsInVzZXIiOnsiaWQiOiJ0ZXN0dXNlciJ9fQ==' \ "https://..evergage.com/api2/event/" ``` ## Send Event Data Using a GET Request Send event data to the Personalization dataset specified in the request. ### Method and Endpoint `GET` `/api2/event/` The event data you send in `GET` requests must be minified, base64-encoded, and passed via the `event` query parameter in the request URL. ### Request URL `https://..evergage.com/api2/event/?event=` ### URL Parameters | URL Parameter | Required? | Description | | ------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `accountName` | Yes | The Personalization account name. You can find the `accountName` by accessing **Gears** from the Personalization UI and reviewing the URL. For example, if the Gears URL is `https://demo.us-1.evergage.com/`, then the account name is `demo`. | | `instance` | Yes | The Personalization instance identifier. You can find this value by accessing **Gears** from the Personalization UI and reviewing the URL. For example, if the Gears URL is `https://demo.us-1.evergage.com/`, then the instance identifier is `us-1`. | | `dataset` | Yes | Name or identifier of the Personalization dataset you're sending data to. | | `base64-encoded-minified-event` | Yes | The minified and base64-encoded event data string. | ### Example Consider the following as sample event data that you would like to send to Personalization. ```json { "action": "hello world", "user": { "id": "testuser" } } ``` To send this event, minify the JSON event data, base64-encode the resulting string, and include it in the event parameter of a `GET` request, as shown in the following example. ```bash curl -X GET 'https://..evergage.com/api2/event//event=eyJhY3Rpb24iOiJoZWxsbyB3b3JsZCIsInVzZXIiOnsiaWQiOiJ0ZXN0dXNlciJ9fQ==' ``` You can also send the `GET` request via a web browser by accessing the request URL directly in your browser. **note:** You can use `GET` requests as an alternative to using `POST` requests for sending event data to your Personalization datasets, if necessary. ## Send Event Data via Trusted Channel The Personalization Event API provides a dedicated endpoint sending events that use _trusted_ `Server` channels. The API call must be authenticated with a valid API token that is permitted to send events. The API token must be provided via the `Authorization` header according to the HTTP basic authentication scheme. For more information about creating and configuring an API token in Personalization, see [Personalization API Tokens](https://help.salesforce.com/s/articleView?id=sf.mc_pers_api_tokens.htm). **note:** - For datasets created after January 28, 2021, the campaign targeting rule is set to `channel: Server` by default. To return server-side campaigns using the Event API, events must be sent via a trusted server channel. For more information on the implications of using or removing the `channel: Server` rule, refer to the [Server-Side Templates](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/server-side-campaigns-templates.md#determine-campaign-identity--authentication-requirements) documentation. - To add authentication to server-side campaigns for datasets created before January 28, 2021, [Create a Support Case](https://help.salesforce.com/s/articleView?language=en_US&id=sf.workcom_contact_support.htm&type=5). ### Supported Content Types - `application/json` ### Method and Endpoint `POST` `/api2/authevent/` ### Request URL `https://..evergage.com/api2/authevent/` ### URL Parameters | URL Parameter | Required? | Description | | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `accountName` | Yes | The Personalization account name. You can find the `accountName` by accessing **Gears** from the Personalization UI and reviewing the URL. For example, if the Gears URL is `https://demo.us-1.evergage.com/`, then the account name is `demo`. | | `instance` | Yes | The Personalization instance identifier. You can find this value by accessing **Gears** from the Personalization UI and reviewing the URL. For example, if the Gears URL is `https://demo.us-1.evergage.com/`, then the instance identifier is `us-1`. | | `dataset` | Yes | Name or identifier of the Personalization dataset you're sending data to. | ### Supported Content Types `application/json` ### Authorization `Authorization: Basic ` The Event API uses API tokens to authenticate requests. Your API token must be a strict Base64 encoding of your API Key ID and its API Secret Key, separated by a colon. You can use an existing API token with required permissions or request an administrator to create one via the Personalization UI at **Security** > **API Tokens** > **Create Token**. For more information about API tokens, see [Personalization API Tokens](https://help.salesforce.com/s/articleView?id=sf.mc_pers_api_tokens.htm). Alternatively, if you use [cURL](https://curl.se/docs/manpage.html) to interact with the API, you can send your API Key ID and its API Key Secret using the `-u "apiKeyId:apiKeySecret"` command-line option. cURL automatically converts the `apiKeyId:apiKeySecret` pair into a Base64-encoded string and adds `Authorization: Basic ` to the request header. ### Example Consider the following as sample event data that you would like to send to Personalization. ```json { "action": "hello world", "source": { "channel": "Server" }, "user": { "id": "testuser" } } ``` You can send this event data via the trusted channel via a `POST` request to the dedicated endpoint, as shown in the following example request. Ensure that you minify the event JSON data into a single line by removing all whitespaces before sending it via the `POST` request. ```bash curl -X POST \ -H 'Content-Type: application/json' \ -u "apiKeyId:apiKeySecret" \ -d '{"action":"hello world","source":{"channel":"Server"},"user":{"id":"testuser"}}' \ "https://..evergage.com/api2/authevent/" ``` ## See Also - [_Salesforce Help_: Personalization API Tokens](https://help.salesforce.com/s/articleView?id=sf.mc_pers_api_tokens.htm) - [_Salesforce Help_: Create a Support Case](https://help.salesforce.com/s/articleView?id=sf.workcom_contact_support.htm) - [_External Link_: npm: Minify JSON](https://www.npmjs.com/package/json-minify) - [_External Link_: cURL Man Page](https://curl.se/docs/manpage.html) # Event API Responses Requests sent to the Event API return JSON that contains information about any campaigns that are returned. A response from the Event API has the following structure. ```json { "campaignResponses": CampaignResponse[], "compiledCampaignTemplates": CompiledCampaignTemplate[], "campaignExplanations": CampaignExplanation[], "errorCode": number, "resolvedUserId": string, "persistedUserId": object, "eventDetails": eventDetails } ``` | Key | Value | Description | | --------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `campaignResponses` | `CampaignResponse[]` | An array of all campaigns returned in the event. For more information, see [The CampaignResponse Object](#the-campaignresponse-object). | | `compiledCampaignTemplates` | `CompiledCampaignTemplate[]` | An array of all campaign templates returned in the event. For more information, see [The CompiledCampaignTemplates Object](#the-compiledcampaigntemplates-object). | | `errorCode` | Number | The error code. Possible values are:
  • 0 : Success
  • 1 : Missing Encrypted `UserId` Parameter
  • 2 : Invalid Encrypted `UserId` Parameter
  • 3 : Busy
  • 4: Anonymous ID Correction
  • 5: Invalid Event
| | `resolvedUserId` | String | The `primaryId` of the resolved User. The `primaryId` is only returned if the **Pipeline Identity Resolution** feature is enabled for the tenant. For more information, see [User Identity Mapping](https://developer.salesforce.com/docs/marketing/personalization/web-integration/user-identity-mapping.md). | | `persistedUserId` | Object | An object containing the encrypted user ID of the user. The Event API returns the `persistedUserId` only if **Pipeline Identity Resolution** isn’t enabled for the tenant and a primary user ID is specified in the event. For more information, see [User Identity Mapping](https://developer.salesforce.com/docs/marketing/personalization/web-integration/user-identity-mapping.md). | | `eventDetails` | `eventDetails` | An object containing the event received by the server. This object is only returned if `debug.explanations` was sent with the event and the event was authenticated. | ## The CampaignResponse Object The `CampaignResponse` object contains data associated with the campaigns returned from an event. ```json { "type": string, "campaignId": string, "campaignName": string, "campaignType": string, "experienceId": string, "experienceName": string, "experienceSourceCode": string, "state": string, "displayMode": string, "redirectUrl": null, "saveParameters": boolean, "hidePageBeforeRedirect": boolean, "campaignJavascriptContent": null, "javascriptContent": null, "userGroup": string, "googleAnalyticsConfig": null, "messages": [], "pageChanges": [], "templateNames": string[], "serverSideMessages": ServerSideMessage[], "payload": payload } ``` | Key | Value | Description | | --------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | String | The type of campaign returned. The value of `type` is `"c"` for server-side campaigns and `"ng"` for web campaigns. | | `campaignId` | String | The ID of the campaign returned. | | `campaignName` | String | The name of the campaign returned. | | `campaignType` | String | The type of the campaign returned. The value of `campaignType` is `"ServerSide"` for server-side campaigns and `"Web"` for web campaigns. | | `experienceId` | String | The ID of the experience returned. | | `experienceName` | String | The name of the experience returned. | | `state` | String | The state of the campaign returned. The value of `state` can be `"Published"`, `"Disabled"`, or `"Testing"` depending on the campaign state. | | `displayMode` | String | **Deprecated**. This object is only returned for server-side campaigns and its value is always `"Personalized"`. | | `redirectUrl` | Null | **Deprecated**. This object is only returned for server-side campaigns. | | `saveParameters` | Boolean | **Deprecated**. This object is only returned for server-side campaigns and is always set to `false`. | | `hidePageBeforeRedirect` | Boolean | **Deprecated**. This object is only returned for server-side campaigns and is always set to `false`. | | `campaignJavascriptContent` | Null | **Deprecated**. The `campaignJavascriptContent` object is always set to `null`. | | `javascriptContent` | Null | **Deprecated**. This object is only returned for server-side campaigns and is always set to `null`. | | `userGroup` | String | The user group of the campaign experience returned. The value of `userGroup` can be either `"Default"` or `"Control"`. | | `googleAnalyticsConfig` | Null | **Deprecated**. This object is only returned for server-side campaigns and is always set to `null`. | | `messages` | Empty array | **Deprecated**. This object is only returned for server-side campaigns and is always set to `[]`. | | `pageChanges` | Empty array | **Deprecated**. This object is only returned for server-side campaigns and is always set to `[]`. | | `templateNames` | String array | An array of the template names used in the experience. This object is only returned for web campaigns. | | `serverSideMessages` | `ServerSideMessage[]` | An array of messages returned for a server-side campaign. This object is only returned for server-side campaigns. The event **must** be sent with `source.channel` set to `"Server"` in order to return `serverSideMessages`. For more information, see [ServerSideMessage object](#the-serversidemessage-object). | | `payload` | `payload` | The payload object that the campaign returns. The `payload` object always includes the `campaign`, `experience`, and `userGroup` fields, along with other user defined fields. For more information, see [Template Server Typescript](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/templates/template-server-typescript.md). **Note:** The value of `campaignResponse.payload.userGroup` can either be `"Test"` or `"Control"`. | ## The ServerSideMessage Object The `ServerSideMessage` object contains data associated with `serverSideMessages` returned in a `campaignResponse`. For server-side campaigns, an array of `ServerSideMessage` objects is returned that contains information about the data returned for the server-side campaign message. The event **must** be sent with `source.channel` set to `"Server"` in order to return `serverSideMessages`. For more information on configuring server-side campaigns, see [Server-Side Campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_server_side_campaign.htm). ```javascript { "type": string, "id": string, "data": string, "targetName": string, "containsDynamicContent": boolean, "promotedItemKeys": PromotedItem[] | null, "campaignState": string, "dataMap": dataMap } ``` | Key | Value | Description | | ------------------------ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | String | The type of message that is returned. For a `ServerSideMessage`, the `type` is always `"evg_md"`. | | `id` | String | The ID of the message that is returned. | | `data` | String | **Deprecated**. Its value is always `"{}"`. | | `targetName` | String | The name of the target for the `ServerSideMessage`. | | `containsDynamicContent` | Boolean | The value returned is `true` if the message contains any dynamic content, and `false` if the message doesn’t contain dynamic content. | | `promotedItemKeys` | `PromotedItem[] \| null` | If the message contains promoted items, `promotedItemKeys` is an array of promoted items. Otherwise its value is `null`. Each `PromotedItem` returned contains the type of the item returned and the `_id` of the item returned. | | `campaignState` | `"Published"`, `"Disabled"`, or `"Testing"` | The state of the campaign returned. | | `dataMap` | `dataMap` | An object containing the key value pairs defined in the campaign. | ### Example Values The following is an example of a value returned in the `promotedItemKeys` object: ```javascript { type: "p", _id: "123" } ``` The following is an example of a value returned in the `dataMap` object: ```javascript dataMap: { "myText": "helloWorld", "myNumber": "1", "myPromotedContent": [ { "type": "p", "_id": "123" }, { "type": "p", "_id": "456" } ] } ``` For keys with type "Promoted Content", the full value of each item is returned unless the "Item IDs only" option is enabled in the message settings. For more information on configuring server-side campaigns and the returned `dataMap` object, see [Server-Side Campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_server_side_campaign_create.htm). ## The CompiledCampaignTemplates Object The `CompiledCampaignTemplates` object contains data associated with `campaignTemplates` returned in an event. An event response returns an array of any `compiledCampaignTemplates` used in a `campaignResponse`. The `compiledCampaignTemplates` object contains information about the campaign templates. ```javascript { "id": string, "name": string | null, "bundle" string } ``` | Key | Value | Description | | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- | | `id` | String | The ID of the template. | | `name` | String or `null` | The name of the template. | | `bundle` | String | The stringified [client-side code](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/web-campaigns-templates.md) used to render the template. | ## The CampaignExplanations Object The `CampaignExplanations` Object contains data associated with `campaignExplanations` returned in an event. The `campaignExplanations` object returns in an event only if `debug.explanations` is `true` in the event payload. It contains additional information about why campaigns did or didn’t return in the event response. It’s only returned if the event request is authenticated. ```javascript { "class": string, "campaignId": string, "campaignName": string, "experienceId": string, "experienceName": string, "messageId": string, "explanationMessage": string, "context": string } ``` | Key | Value | Description | | -------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `class` | String | Information of the type of explanation that is returned. | | `campaignId` | String | The ID of the campaign that the explanation is for. | | `campaignName` | String | The name of the campaign that the explanation is for. | | `experienceId` | String | The ID of the experience that the explanation is for. | | `experienceName` | String | The name of the experience that the explanation is for. | | `messageId` | String | The ID of the message that the explanation is for. | | `explanationMessage` | String | A message containing the reason for not returning the campaign in the response. | | `context` | String | A string containing the campaign name, campaign ID, experience name, and experience ID for the message that isn't returned in the response. | ## See Also - [_Salesforce Help_: Server-Side Campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_server_side_campaign.htm) # Event Options in the Evergage Namespace The following code sample details the event options available in the `Evergage` namespace in the Personalization module of the Salesforce Interactions SDK. ```javascript { action: string, itemAction: string, consents: Consent[], debug: { explanations: boolean, testMessages: string }, flags: { noCampaigns: boolean, doNotTrack: boolean, pageView: boolean, nonInteractive: boolean }, source: { channel: string, pageType: string, url: string, urlReferrer: string, locale: string, contentZones: string[], time: number, userAgent?: string, clientIp?: string, operatingSystem?: string operatingSystemVersion?: string, device?: string, surveyId?: string, surveyStartTime?: string }, user: { id: string, attributes: { [key: string]: string | number | boolean }, profileObjects: { [profileObjectType: string]: ProfileObject[] }, }, account: { id: string, domain: string, attributes: { [key: string]: string | number | boolean }, }, catalog: { : { [key: string]: string | number | boolean | object } }, order: { : { orderId: string, totalValue: number, currency: string lineItems: LineItem[] } }, cart: { singleLine: { : LineItem }, complete: { : LineItem[] } }, campaignStats: CampaignStat[], performance: { sdkLoadTime: number, pageLoadTime: number, sdkParseTime: number, networkTime: number, sdkDnsTime: number, eventDnsTime: number, downloadTime: number } } ``` | Event Property | Value Type | Description | | -------------- | ---------- | ------------------------------------------------------------------- | | `action` | String | Name of the event. Used for segmentation, targeting, and reporting. | | `itemAction` | String | Defines the action taken on a catalog item. | ## The consent Object The `Consent` field accepts an array of consent objects for the user. The following table describes the various properties available in the `consent` object. | Property | Value Type | Description | | ---------- | ---------- | ------------------------------------------------------------ | | `purpose` | String | The purpose for the consent. For example, `Personalization`. | | `provider` | String | The consent provider | | `status` | String | The consent status. For example, `OptIn` or `OptOut` | ## The debug Object The `debug` object contains fields that help investigate issues that could arise when developing campaigns. The following table describes the various properties available in the `debug` object. | Property | Value Type | Description | | -------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `explanations` | Boolean | If `true`, then return additional information in the response about why campaigns did or didn’t render. Requires authentication. | | `testMessages` | String | A comma-separated list of campaign experience IDs to be forced to return in the event, ignoring rules that would otherwise prevent the campaign from returning. Alternatively, you can use the string value `true` to return all campaigns in testing mode but all rules are respected. | ## The flags Object The `flags` object contains properties that alter default event processing. By default, all flags are `false` if not present on the event. The following table describes the various properties available in the `flags` object. | Property | Value Type | Description | | ---------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `noCampaigns` | Boolean | If `true`, don’t return campaigns in the response. | | `pageView` | Boolean | If `true`, indicates that the event was triggered from a page load. | | `nonInteractive` | Boolean | If `true`, a visit isn’t created (or updated) for the given user in the event. Additionally, no visit referrer nor originating referrer is created for the user. | | `doNotTrack` | Boolean | If `true`, the event data is sent to the server but not processed. | ## The source Object The `source` object contains properties that help describe where an event is coming from. The following table describes the various properties available in the `source` object. | Property | Value Type | Description | | ------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `channel` | String | The originating source of the event (For example, `Web`, `MobileApp`, `CallCenter`). | | `application` | String | The originating application level source of the event (For example, `ReactApp`, `3rdParty`, `ReactNative`) | | `pageType` | String | The type of page from which you’re sending the event (For example, `PDP`, `Blog`, `Pricing`). | | `url` | String | The URL of the page from which you’re sending the event. | | `urlReferrer` | String | The previous URL visited by the user. `urlReferrer` is populated from `document.referrer` by default on the web. It can be overwritten in the sitemap to any other value if necessary. | | `locale` | String | The locale of the current page, as defined by ISO 639 alpha-2 language codes and ISO 3166 alpha-2 country codes (For example, `en_US`, `de_DE`). | | `contentZones` | `string[]` | An array of content zones on the current page. | | `configVersion` | Number | Version number of the configuration for the SDK. | | `time` | Number | The date and time of the event, in milliseconds elapsed since the UNIX epoch. This field is optional and is auto-populated with the current time if null. | | `userAgent` | String | The user agent for the event. `userAgent` is populated automatically but can be overwritten if necessary | | `clientIp` | String | The IP Address sending the event. `clientIp` is populated automatically but can be overwritten if necessary | | `operatingSystem` | String | Only applicable for mobile events. | | `operatingSystemVersion` | String | Only applicable for mobile events. | | `device` | String | Only applicable for mobile events. | | `surveyId` | String | The ID of the survey being submitted. `surveyId` is populated automatically for events sent through the Survey Gear | | `surveyStartTime` | String | The start time of the survey being submitted. `surveyStartTime` is populated automatically for events sent through the Survey Gear | ## The user Object The `user` object describes the user associated with an event. The following table describes the various properties available in the `user` object. | Property | Value Type | Description | | ---------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | String | The ID of a known user. | | `encryptedId` | String | The encrypted ID returned from an event that contains the ID field. Encrypted IDs are returned in the response to events that provide a `user.id`. | | `attributes` | `{ [key: string]: string \| number \| boolean }` | Key-value pairs that are stored as metadata on the user. These attributes must be defined in the platform. | | `profileObjects` | `{ [profileObjectType: string]: ProfileObject[] }` | Key-value pairs of profile objects that are stored as metadata on the user. Profile objects, their attributes, and related catalog objects must be defined in the platform. | ## The account Object The `account` object describes the account associated with an event. The following table describes the various properties available in the `account` object. | Property | Value Type | Description | | ------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | | `id` | String | The ID of an account. | | `attributes` | `{ [key: string]: string \| number \| boolean }` | Key-value pairs that are stored as metadata on the account. These attributes must be defined in the platform. | ## The catalog Object The `catalog` object describes item data associated with the given `itemAction` and is used for actions pertaining to items in the catalog. For instance, the following `catalog` object provides the item being viewed when sending the item action "View Item". ```javascript { itemAction: Evergage.ItemAction.ViewItem, catalog: { : { [key: string]: string | number | boolean | object } }, ... } ``` ### The location Object You can use the `location` object to send location data on a `catalog` object via the Event API or using the Sitemap. The following is an example of how you to structure a `catalog` object to include location data. ```json "catalog": { "Product": { "_id": "test_product_1", "name": "test product name 1 ", "location": { "city": "Boston", "postalCode": "02215", "countryNumericCode": "840", "stateProvinceCode": "MA", "longlat": [-71.0871357, 42.3610235] } } } ``` The following examples depict sending data for locations outside the US. ```json "catalog": { "Product": { "_id": "test_product_2", "name": "test product name 2", "location": { "city": "Melbourne", "postalCode": "3005", "countryNumericCode": "036", "stateProvinceCode": "VIC", "longlat": [144.963058, -37.813629] } } } ``` ```json "catalog": { "Product": { "_id": "test_product_3", "name": "test product name 3", "location": { "city": "Tokyo", "postalCode": "100-0004", "countryNumericCode": "392", "stateProvinceCode": "13", "longlat": [139.691711, 35.689487] } } } ``` ## The order Object The `order` object describes order data associated with the given `itemAction` and is used for Purchase actions. The `order` object has the following structure. ```javascript { ... order: { : { orderId: string, totalValue: number, currency: string lineItems: LineItem[] } }, ... } ``` The following table describes the properties that an `order` object accepts. | Property | Value Type | Description | | ------------ | ------------ | -------------------------------------------------------------------------------------------- | | `orderId` | String | ID of the purchased order. | | `currency` | String | Optional currency code of purchase. Defaults to the dataset's configured currency if `null`. | | `totalValue` | Number | Optional total value for purchase. Defaults to the sum of line items quantity \* price. | | `lineItems` | `LineItem[]` | List of purchased line items. | ### Line Item A `LineItem` represents a single item in a cart and/or transaction. A `LineItem` object has the following structure. ```javascript { _id: string, price: number, quantity: number } ``` ## The cart Object The `cart` object describes cart data associated with the given `itemAction` and has the following structure. ```javascript { ... cart: { singleLine: { : LineItem }, complete: { : LineItem[] } } ... } ``` The following table describes the properties a `cart` object accepts. | Property | Value Type | Description | | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `singleLine` | `LineItem` | Used for `Add To Cart` and `Update Line Item` actions. `Add To Cart` adds the quantity to any existing matching items. `Update Line Item` overwrites the quantity of any existing matching items. | | `complete` | `LineItem[]` | Used to update the state of the cart in Personalization, setting its content to the provided line items. Previous cart contents are replaced. | Additionally, a user's cart can be emptied by sending an event with an empty array of `LineItems`. ```javascript { cart: { complete: { Product: []; } } } ``` For more information, refer to the [LineItem](#line-item) documentation. ## The campaignStats Object You can use the `campaignStats` object to track campaign statistics for an associated event. For more information on sending campaign statistics on the web, refer to the [Campaign Stats Tracking](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/templates/campaign-stats-tracking.md) documentation. The `campaignStats` object has the following structure. ```javascript { ... { campaignStats: CampaignStat[] } ... } ``` ### CampaignStat The `CampaignStat` object has the following structure. ```javascript { experienceId: string, stat: "Impression" | "Click" | "Dismissal", control: boolean, catalog: { : string[] } } ``` The following table describes the properties a `CampaignStat` object accepts. | Property | Value Type | Description | | -------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `experienceId` | String | The experience ID of the campaign on which the given statistic is being tracked | | `stat` | `"Impression" \| "Click" \| "Dismissal"` | The type of statistic being tracked. | | `control` | Boolean | If `true`, the statistic is tracked for the control group of the given experience. | | `catalog` | `{ : string[] }` | A mapping of catalog item types to a list of corresponding item IDs on which to attribute the given statistic. | The following is an example of a mapping of catalog item types. ```javascript ... catalog: { Product: ["product1", "product2"] } ... ``` ## The performance Object The `performance` object has properties that measure loading, parsing, and network performance. The following table describes the various properties available in the `performance` object. | Property | Value Type | Description | | -------------- | ---------- | ------------------------------------------------------------------------------ | | `sdkLoadTime` | Number | (Web SDK) Time, in milliseconds, for the network to load the web SDK. | | `pageLoadTime` | Number | (Web SDK) Time, in milliseconds, for the DOM to load. | | `sdkParseTime` | Number | (Web SDK) Time, in milliseconds, for the beacon to be parsed during page load. | | `networkTime` | Number | (Web SDK) Time, in milliseconds, for the previous request to return. | | `sdkDnsTime` | Number | (Web SDK) Time, in milliseconds, to perform resolution of SDK's CDN domain. | | `eventDnsTime` | Number | (Web SDK) Time, in milliseconds, to resolve account-specific domain. | | `domLoadTime` | Number | (Web SDK) Time, in milliseconds, for the DOM to finish loading. | # Example Sitemaps The following is a list of example Personalization Sitemaps we've created for different types of business websites that depict how you could approach designing a Sitemap for your Personalization implementation. The JavaScript and HTML code in these example Sitemaps are for reference purposes only but can give you a head start in developing the Sitemap for your implementation. **important:** The sitemap code examples in these articles are only for demonstration purposes. Avoid copying sample code into your Sitemap Editor as it is built around example scenarios that can differ from your implementation's goals and use cases. - [B2B](https://developer.salesforce.com/docs/marketing/personalization/guide/example-sitemaps/b2b.md) - [Ecommerce](https://developer.salesforce.com/docs/marketing/personalization/guide/example-sitemaps/ecommerce.md) - [Financial Services](https://developer.salesforce.com/docs/marketing/personalization/guide/example-sitemaps/financial-services.md) # Financial Services The following code sample depicts sitemap code for an imaginary implementation of Marketing Cloud Personalization on the Cumulus Financial Services website `https://www.cumulusfinserv.com/`. This sitemap code isn’t exhaustive and maps only key areas valuable for demonstrating an example implementation of Personalization. The primary focus of this sitemap code is to depict how to approach designing a Catalog for websites offering financial services. For financial services implementations, we recommend using the Product catalog type as showcased in this example. In order to use this catalog type, we add a default price and quantity for each Product when creating line items as that data is not typically present on financial services Product Detail Pages (PDPs). Then on the forms associated with PDPs, we create a checkout flow and fire a `Purchase` interaction when the last step of a form is clicked. We don't recommend using a custom catalog object type as it doesn't as easily allow for conversion tracking. It's also harder to illustrate ownership when using a non-Product catalog type. **important:** The sitemap code examples in this article are only for demonstration purposes. Avoid copying sample code into your Sitemap Editor as it is built around example scenarios that can differ from your implementation's goals and use cases. ## Example Sitemap for the SalesforceInteractions Namespace ```javascript // This helper function is used help with the Purchase flow and returns an array of lineItems function handleLineItems() { let itemId; if (window.localStorage.getItem('lastAddToCart')) { itemId = window.localStorage.getItem('lastAddToCart'); } let purchaseLineItems = []; if (itemId != null) { let lineItem = { catalogObjectType: "Product", catalogObjectId: itemId, price: 1, quantity: 1 }; purchaseLineItems.push(lineItem); } return purchaseLineItems; } /* * This helper function handles adds to cart, adds the last carted item to local storage, and sets a user attribute * called "lastAddToCart". This attribute can used for campaigns or recipe purposes. */ function handleAddToCart(){ let cartedItem = SalesforceInteractions.mcis.getLastPathComponentWithoutExtension(window.location.href).toUpperCase(); window.localStorage.setItem("lastAddToCart", cartedItem); SalesforceInteractions.sendEvent({ user: { attributes: { lastAddToCart: cartedItem } }, interaction: { name: SalesforceInteractions.CartInteractionName.AddToCart, lineItem: { catalogObjectType: "Product", catalogObjectId: cartedItem, price: 1, quantity: 1 } } }) } /* This helper function passes in data from a form interaction and converts it into a proper actionEvent. * It also pulls all the possible selectors for form inputs to make sure they get scraped * to the correct user attributes. */ function handleFormInteraction({ interactionName, user = {}, order = {} }) { const actionEvent = { user: { ...user }, interaction: { name: interactionName, order: { ...order } } }; actionEvent.user.attributes = { firstName: SalesforceInteractions.cashDom("#firstName, input[name='firstName'], #form-appointment-first-name, #form-advisor-first-name, #form-advisor-first-name").val(), lastName: SalesforceInteractions.cashDom("#lastName, input[name='lastName'], #form-appointment-last-name, #form-advisor-last-name").val(), emailAddress: SalesforceInteractions.cashDom("#email, input[name='email'], #form-appointment-email, #form-advisor-email").val(), zipcode: SalesforceInteractions.cashDom("#zip, input[name='postal'], #form-appointment-zip, #form-advisor-zip, #form-application-zip").val(), existingCustomer: SalesforceInteractions.cashDom("select.form-control option[selected], #form-appointment-existing, #form-advisor-existing").val(), phone: SalesforceInteractions.cashDom("input[name='phone'], #form-application-phone").val(), companyName: SalesforceInteractions.cashDom("#form-appointment-company-name, #form-advisor-company-name").val(), over18: SalesforceInteractions.cashDom("#inlineAge:checked").val(), usCitizen: SalesforceInteractions.cashDom("#inlineCitizen:checked").val(), birthday: SalesforceInteractions.cashDom("#datepicker, input[name='birthday']").val(), addressLine1: SalesforceInteractions.cashDom("input[name='addr1'], #form-application-address-01").val(), } SalesforceInteractions.sendEvent(actionEvent); } /* This helper function helps with the form_complete pageTypes so the correct interaction name * is fired based on the form completed. */ function handleCompleteInteraction() { if (/^\/account\-application\-complete$/.test(window.location.pathname)) { return "Open an Account Form - Complete"; } else if (/^\/quote\-application\-complete$/.test(window.location.pathname)) { return "Get A Quote Form - Complete"; } else if (/loan\-application\-complete/.test(window.location.pathname)) { return "Pre-Approved Form - Complete"; } else { return "Schedule Apppointment Form - Complete"; } } SalesforceInteractions.init({ cookieDomain: "cumulusfinserv.com", consents: [{ purpose: SalesforceInteractions.mcis.ConsentPurpose.Personalization, provider: "Example Consent Manager", status: SalesforceInteractions.ConsentStatus.OptIn }] }).then(() => { const sitemapConfig = { global: { contentZones: [ { name: "global_popup"} ] }, pageTypeDefault: { name: "default" }, pageTypes: [ { name: "personal_home", isMatch: () => /^\/$/.test(window.location.pathname), interaction: { name: "Personal - Homepage" }, contentZones: [ { name: "personal_home_hero", selector: ".hero-inner" }, { name: "personal_home_recommendations", selector: ".intro-content" }, { name: "personal_home_nav", selector: "body > div.category-menu" }, ] }, { name: "corporate_home", isMatch: () => /^\/corporate$/.test(window.location.pathname), interaction: { name: "Corporate - Homepage" }, contentZones: [ { name: "corporate_home_hero", selector: ".hero-inner" }, { name: "corporate_home_recommendations", selector: ".intro-content" }, { name: "corporate_home_nav", selector: "body > div.category-menu" }, ] }, { name: "login", isMatch: () => /^\/login$/.test(window.location.pathname), interaction: { name: "Login Page" }, listeners: [ SalesforceInteractions.listener("submit", ".form-signin", () => { handleFormInteraction({ user: { identities: { emailAddress: SalesforceInteractions.cashDom("inputEmail").val(), } }, interactionName: "Contact Form Submit" }); }) ] }, { name: "contact_us", isMatch: () => /^\/company\/contact\-us$/.test(window.location.pathname), interaction: { name: "Contact Us" }, listeners: [ SalesforceInteractions.listener("submit", ".form-signin", () => { handleFormInteraction({ user: { attributes: { message: SalesforceInteractions.cashDom(".form-control.contact").val() } }, interactionName: "Contact Form Submit" }); }) ] }, { name: "pdp", isMatch: () => SalesforceInteractions.cashDom("div.container.product-intro").length > 0, interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Product", id: SalesforceInteractions.resolvers.fromHref((url) => url.split("/").splice(-1)[0].toUpperCase()), attributes: { name: SalesforceInteractions.resolvers.fromSelector("h1"), description: SalesforceInteractions.resolvers.fromSelector(".product-intro h3"), price: 1, url: SalesforceInteractions.resolvers.fromHref(), imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute(".img-responsive", "src"), inventoryCount: 1 }, relatedCatalogObjects: { Category: SalesforceInteractions.resolvers.buildCategoryId(".nav a.current span", null, null, (id) => { let topLevel = SalesforceInteractions.cashDom("li.current a").text().trim().toUpperCase(); return [topLevel+"|"+id.toUpperCase()]; }), ItemClass: SalesforceInteractions.resolvers.fromSelectorMultiple("li.current a"), Keyword: SalesforceInteractions.resolvers.fromMeta("keywords", (keyword) => { return [keyword]; }), Benefits: SalesforceInteractions.resolvers.fromSelectorMultiple(".product-details .col-md-6:nth-of-type(1) li"), Requirements: SalesforceInteractions.resolvers.fromSelectorMultiple(".product-details .col-md-6:nth-of-type(2) li") } } }, listeners: [ SalesforceInteractions.listener("click", ".product-intro .btn.green-btn.btn-med", handleAddToCart) ], contentZones: [ { name: "pdp_cta", selector: ".btn.green-btn.btn-med" }, { name: "pdp_image", selector: ".pdp-img-responsive"} ] }, { name: "Category", isMatch: () => SalesforceInteractions.cashDom("div.offerings").length === 1, interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Category", id: SalesforceInteractions.resolvers.buildCategoryId(".nav a.current span", null, null, (id) => { let topLevel = SalesforceInteractions.cashDom("li.current a").text().trim().toUpperCase(); if (/.*\/investing\/retirement\-planning$/.test(window.location.href)) { return "PERSONAL|INVESTING|RETIREMENT PLANNING"; } else { return topLevel+"|"+id.toUpperCase(); } }), attributes: { name: SalesforceInteractions.resolvers.fromSelector("h1"), url: SalesforceInteractions.resolvers.fromHref(), imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute(".img-responsive", "src"), } } }, contentZones: [ { name: "category_hero_img", selector: ".svg-hero-container img" } ] }, { name: "pre_approved", isMatch: () => /\/get-preapproved/.test(window.location.href), interaction: { name: "Pre-Approved" }, listeners: [ SalesforceInteractions.listener("click", "#form-application-01-next", () => { handleFormInteraction({ interactionName: "Pre-Approved Form - Step 1 Submit", }); }), SalesforceInteractions.listener("click", "#form-application-02-next", () => { handleFormInteraction({ user: { attributes: { loanAmount: SalesforceInteractions.cashDom("#form-application-loan-amount").val(), purchasePrice: SalesforceInteractions.cashDom("#form-application-purchase-price").val(), downPayment: SalesforceInteractions.cashDom("#form-application-down-payment").val(), propertyType: SalesforceInteractions.cashDom("#form-application-property-type").val(), realtorPhone: SalesforceInteractions.cashDom("#form-application-phone-number").val() } }, interactionName: "Pre-Approved Form - Step 2 Submit", }); }), SalesforceInteractions.listener("click", "#startJourney", () => { handleFormInteraction({ interactionName: SalesforceInteractions.OrderInteractionName.Purchase, user: { attributes: { income: SalesforceInteractions.cashDom("input[name='income']").val(), assets: SalesforceInteractions.cashDom("input[name='assets']").val() } }, order: { lineItems: handleLineItems() } }) }), SalesforceInteractions.listener("click", "#msform", (event) => { if (SalesforceInteractions.cashDom(event.target).closest(".save").length > 0) { const step = SalesforceInteractions.cashDom("#progressbar .active").length; if (step > 0) { handleFormInteraction({ user: { attributes: { accountLifecycleState: "Pre-approved Open" } }, interactionName: "Pre-approved Form Step " + step + " Save For Later", }); } } }) ] }, { name: "form_complete", isMatch: () => /(appointment\-confirmed)|(loan\-application\-complete)|(^\/quote\-application\-complete$) |(^\/account\-application\-complete$)]/.test(window.location.pathname), interaction: { name: handleCompleteInteraction() }, contentZones: [ { name: "form_complete_app_wrapper", selector: ".application-wrapper" }, { name: "schedule_appointment_complete_app_wrapper", selector: ".ty-white-wrapper" } ] }, { name: "get_quote", isMatch: () => /\/get\-a\-quote/.test(window.location.href), interaction: { name: "Get A Quote" }, listeners: [ SalesforceInteractions.listener("click", "#form-application-01-next", () => { handleFormInteraction({ interactionName: "Get A Quote Form - Step 1 Submit", }); }), SalesforceInteractions.listener("click", "#form-application-02-next", () => { handleFormInteraction({ user: { attributes: { lastInsuranceType: SalesforceInteractions.cashDom("#insuranceType").val().toUpperCase(), } }, interactionName: "Get A Quote Form - Step 2 Submit", }); }), SalesforceInteractions.listener("click", "#startJourney", () => { handleFormInteraction({ interactionName: SalesforceInteractions.OrderInteractionName.Purchase, order: { lineItems: handleLineItems() } }) }), SalesforceInteractions.listener("click", "#msform", (event) => { if (SalesforceInteractions.cashDom(event.target).closest(".save").length > 0) { const step = SalesforceInteractions.cashDom("#progressbar .active").length; if (step > 0) { handleFormInteraction({ user: { attributes: { accountLifecycleState: "Get A Quote Open" } }, interactionName: "Get a Quote Form Step " + step + " Save For Later", }); } } }) ] }, { name: "apply_creditcard", isMatch: () => /\/apply\-for\-a\-card/.test(window.location.href), interaction: { name: "Apply For Credit Card" }, listeners: [ SalesforceInteractions.listener("click", "fieldset:nth-of-type(1) input.next.btn.green-btn.btn-med", (event) => { handleFormInteraction({ interactionName: "Apply for Credit Card Form - Step 1 Submit", }); }), SalesforceInteractions.listener("click", "fieldset:nth-of-type(2) input.next.btn.green-btn.btn-med", (event) => { handleFormInteraction({ user: { attributes: { lastCCType: SalesforceInteractions.cashDom("#card option").val().toUpperCase(), } }, interactionName: "Apply for Credit Card Form - Step 2 Submit", }); }), SalesforceInteractions.listener("click", "fieldset:nth-of-type(3) input.next.btn.green-btn.btn-med", (event) => { handleFormInteraction({ user: { attributes: { city: SalesforceInteractions.cashDom("input[name='city']").val(), state: SalesforceInteractions.cashDom("input[name='state']").val() } }, interactionName: "Apply for Credit Card Form - Step 3 Submit", }); }), SalesforceInteractions.listener("click", "#startJourney", () => { handleFormInteraction({ interactionName: SalesforceInteractions.OrderInteractionName.Purchase, order: { lineItems: handleLineItems() } }) }), SalesforceInteractions.listener("click", "#msform", (event) => { if (SalesforceInteractions.cashDom(event.target).closest(".save").length > 0) { const step = SalesforceInteractions.cashDom("#progressbar .active").length; if (step > 0) { handleFormInteraction({ user: { attributes: { accountLifecycleState: "Apply for Credit Card Open" } }, interactionName: "Apply for Credit Card Form Step " + step + " Save For Later", }); } } }) ] }, { name: "open_account", isMatch: () => /\/open\-an\-account/.test(window.location.href), interaction: { name: "Open an Account" }, listeners: [ SalesforceInteractions.listener("click", "#form-application-01-next", () => { handleFormInteraction({ interactionName: "Open an Account Form - Step 1 Submit", }); }), SalesforceInteractions.listener("click", "#form-application-02-next", (event) => { handleFormInteraction({ user: { attributes: { lastAccountType: SalesforceInteractions.cashDom("#form-application-account-type").val().toUpperCase(), } }, interactionName: "Open an Account Form - Step 2 Submit", }); }), SalesforceInteractions.listener("click", "#form-application-03-next", () => { handleFormInteraction({ user: { attributes: { addressLine2: SalesforceInteractions.cashDom("#form-application-address-02").val(), city: SalesforceInteractions.cashDom("#form-application-city").val(), state: SalesforceInteractions.cashDom("#form-application-state").val(), } }, interactionName: "Open an Account Form - Step 3 Submit", }); }), SalesforceInteractions.listener("click", "#startJourney", () => { handleFormInteraction({ interactionName: SalesforceInteractions.OrderInteractionName.Purchase, order: { lineItems: handleLineItems() } }) }), SalesforceInteractions.listener("click", "#msform", (event) => { if (SalesforceInteractions.cashDom(event.target).closest(".save").length > 0) { const step = SalesforceInteractions.cashDom("#progressbar .active").length; if (step > 0) { handleFormInteraction({ user: { attributes: { accountLifecycleState: "Account Application Open" } }, interactionName: "Open an Account Form Step " + step + " Save For Later", }); } } }) ] }, { name: "schedule_appointment", isMatch: () => /\/corporate\/schedule\-appointment$/.test(window.location.href), interaction: { name: "Schedule Appointment" }, listeners: [ SalesforceInteractions.listener("click", "#form-appointment-schedule-btn", () => { handleFormInteraction({ interactionName: SalesforceInteractions.OrderInteractionName.Purchase, order: { lineItems: handleLineItems() }, user: { attributes: { appointmentReason: SalesforceInteractions.cashDom("#form-appointment-reason").val(), branch: SalesforceInteractions.cashDom("#form-appointment-branch").val(), lastApptDate: SalesforceInteractions.cashDom("#form-appointment-date").val(), lastApptTime: SalesforceInteractions.cashDom("#form-appointment-time").val() } } }) }) ] }, { name: "learn_more", isMatch: () => /\/corporate\/learn\-more/.test(window.location.href), interaction: { name: "Learn More" }, listeners: [ SalesforceInteractions.listener("click", "#form-advisor-submit-btn", () => { handleFormInteraction({ interactionName: "Learn More Form - Submit", user: { attributes: { companySize: SalesforceInteractions.cashDom("#form-advisor-company-size").val(), industry: SalesforceInteractions.cashDom("#form-advisor-industry").val(), } } }) }) ] } ] }; SalesforceInteractions.initSitemap(sitemapConfig); }); ``` ## Example Sitemap for the Evergage Namespace ```javascript Evergage.init().then(() => { const config = { global: {}, pageTypes: [ { name: "home", action: "Homepage", isMatch: () => /^\/$/.test(window.location.pathname), contentZones: [ { name: "home_hero", selector: ".hero-inner" }, { name: "home_recommendations", selector: ".intro-content" }, { name: "home_nav", selector: "body > div.category-menu" }, ], }, { name: "product_detail", isMatch: () => Evergage.cashDom("div.container.product-intro").length > 0, action: "View Product", catalog: { Product: { _id: Evergage.resolvers.fromHref((url) => url.split("/").splice(-1)[0].toUpperCase()), name: Evergage.resolvers.fromSelector("h1"), price: 1, url: Evergage.resolvers.fromHref(), imageUrl: Evergage.resolvers.fromSelectorAttribute(".img-responsive", "src"), inventoryCount: 1, categories: Evergage.resolvers.buildCategoryId( ".nav a.current span", null, null, (id) => { return [id]; }, ), relatedCatalogObjects: { // In case you're using 'dimensions' instead of 'relatedCatalogObjects', you can continue to do so as they both function the same way. ItemClass: Evergage.resolvers.fromSelectorMultiple("li.current a"), }, }, }, listeners: [ Evergage.listener("click", ".product-intro .btn.green-btn.btn-med", (event) => { Evergage.sendEvent({ itemAction: Evergage.ItemAction.AddToCart, cart: { singleLine: { Product: { _id: Evergage.util .getPathname(window.location.href) .split("/") .splice(-1)[0] .toUpperCase(), price: 1, quantity: 1, }, }, }, }); }), ], contentZones: [{ name: "product_detail_cta", selector: ".btn.green-btn.btn-med" }], }, { name: "pre_approved", isMatch: () => /\/get-preapproved/.test(window.location.href), action: "Pre-Approved", listeners: [ Evergage.listener("click", "#msform", (event) => { if (Evergage.cashDom(event.target).closest(".next").length > 0) { const step = Evergage.cashDom("#progressbar .active").length - 1; if (step > 0) { const email = Evergage.cashDom("#form-application-email").val(); let actionEvent = { user: {}, action: "Get Pre-Approved Form Step " + step + " Submit", }; if (email) { actionEvent.user.id = email; } Evergage.sendEvent(actionEvent); } } else if (Evergage.cashDom(event.target).closest(".save").length > 0) { const step = Evergage.cashDom("#progressbar .active").length; if (step > 0) { const email = Evergage.cashDom("#form-application-email").val(); let actionEvent = { user: { attributes: { LifecycleState: "Mortgage Application Open" }, }, action: "Get Pre-Approved Form Step " + step + " Save For Later", }; if (email) { actionEvent.user.id = email; } Evergage.sendEvent(actionEvent); } } }), ], }, ], }; Evergage.initSitemap(config); }); ``` # Flicker Defender The Flicker Defender gear can help prevent flicker caused by the rendering of personalized content (Templates/Campaigns) served by the Marketing Cloud Personalization platform. Flicker Defender helps prevent flicker by doing the following. - Initially hides every content zone that has a `selector` by temporarily applying the `visibility: hidden !important` style to elements targeted by that `selector` - Each hidden content zone is then redisplayed depending on: - If it is not targeted by a Template/Campaign for which the user has qualified - Upon resolution of the Template/Campaign targeting it - When the `redisplayTimeout` is hit - If a page is not matched or if the Sitemap errors, all hidden content zones are redisplayed following the `pageMatchTimeout` While there is no limit for either of the timeout values, it is advised to stay under 10000 ms (10 seconds). Additionally, ensure that `pageMatchTimeout` is less than or equal to the `redisplayTimeout`, since `redisplayTimeout` ultimately decides when to reshow all hidden content. --- ## Requirements For Flicker Defender to work correctly, ensure you meet the following requirements. - A synchronously integrated Beacon in the `head` of the site is a strict requirement for Flicker Defender to work correctly. - A working sitemap is necessary for Flicker Defender to work correctly and efficiently. For information on writing a Sitemap, see the [Web Integration](https://developer.salesforce.com/docs/marketing/web-integration.md) documentation. - At least one Content Zone with a `selector` attribute. _(Note that it is the developer's responsibility to ensure that the CSS selector used is valid.)_ --- ## Timeout Configuration The following Flicker Defense timeout options are configurable from within Site-Wide JavaScript. ```typescript setPageMatchTimeout(pageMatchTimeout: number = 1000): void ``` `pageMatchTimeout` controls the maximum length of time to wait for a page to be successfully matched before reshowing all hidden content ```javascript // Example: SalesforceInteractions.mcis.FlickerDefender.setPageMatchTimeout(2500); ``` ```javascript // Example: Evergage.FlickerDefender.setPageMatchTimeout(2500); ``` `redisplayTimeout` controls the maximum length of time that content should ever be hidden. ```typescript setRedisplayTimeout(redisplayTimeout: number = 2500): void ``` ```javascript // Example: SalesforceInteractions.mcis.FlickerDefender.setRedisplayTimeout(5000); ``` ```javascript // Example: Evergage.FlickerDefender.setRedisplayTimeout(5000); ``` **note:** Depending on the namespace you're using, calls to `setPageMatchTimeout` and/or `setRedisplayTimeout` must be done before the call to either `SalesforceInteractions.init()` or `Evergage.init()` in Site-Wide JavaScript. ```javascript // Example: if ( typeof (SalesforceInteractions.mcis.FlickerDefender || {}).setPageMatchTimeout === "function" ) { SalesforceInteractions.mcis.FlickerDefender.setPageMatchTimeout(2500); } if ( typeof (SalesforceInteractions.mcis.FlickerDefender || {}).setRedisplayTimeout === "function" ) { SalesforceInteractions.mcis.FlickerDefender.setRedisplayTimeout(5000); } SalesforceInteractions.init().then(() => { // sitemap }); ``` ```javascript // Example: if (typeof (Evergage.FlickerDefender || {}).setPageMatchTimeout === "function") { Evergage.FlickerDefender.setPageMatchTimeout(2500); } if (typeof (Evergage.FlickerDefender || {}).setRedisplayTimeout === "function") { Evergage.FlickerDefender.setRedisplayTimeout(5000); } Evergage.init().then(() => { // sitemap }); ``` **important:** - By design, Flicker Defender does not run in the Template or Campaign Editors. - Depending on the SDK namespace you're using, ensure that you call `SalesforceInteractions.init` and `SalesforceInteractions.initSitemap` or `Evergage.init` and `Evergage.initSitemap` as soon as possible in Site-Wide JavaScript, since Flicker Defender relies on those calls to determine when to begin hiding content zones. - When injecting or forcing the Web SDK URL using the Salesforce Interactions SDK Launcher browser extension, you are simulating an asynchronous implementation of the product. Therefore, at least a small amount of Flicker always appears when injecting or forcing a Web SDK onto the page. ## Disabling Flicker Defender To effectively disable the Flicker Defender gear, add the following to the top of your sitewide JavaScript. ```javascript if ( typeof (SalesforceInteractions.mcis.FlickerDefender || {}).setPageMatchTimeout === "function" ) { SalesforceInteractions.mcis.FlickerDefender.setPageMatchTimeout(0); } if ( typeof (SalesforceInteractions.mcis.FlickerDefender || {}).setRedisplayTimeout === "function" ) { SalesforceInteractions.mcis.FlickerDefender.setRedisplayTimeout(0); } ``` ```javascript if (typeof (Evergage.FlickerDefender || {}).setPageMatchTimeout === "function") { Evergage.FlickerDefender.setPageMatchTimeout(0); } if (typeof (Evergage.FlickerDefender || {}).setRedisplayTimeout === "function") { Evergage.FlickerDefender.setRedisplayTimeout(0); } ``` # Get Started with Global Web Templates The simplest way to develop a Marketing Cloud Personalization web template is to use one of our global templates and customize it for your business use case. To use a global template, do the following. 1. Clone the global template that most closely represents your web channel use case. 2. Customize the template based on business user requirements. The following sections present the steps for cloning a global template in Personalization and the standards for customizing global template code. For information on the most current list of available global templates and their recommended web channel use cases, see [Web Campaign Template Properties](https://help.salesforce.com/s/articleView?id=sf.mc_pers_web_campaign_template_properties.htm) on Salesforce Help. ## Clone a Global Template To clone a global template, do the following: 1. Open the [Salesforce Interactions SDK Launcher](https://help.salesforce.com/s/articleView?id=sf.mc_pers_web_campaign_interactions_sdk_launcher.htm) Chrome extension tool, and click the **View List** section of the template button: ![Launcher View Template List](https://developer.salesforce.com/media/campaign-development/launcher-view-list.png) 2. To find a list of all currently available global templates, click the **Global Templates** tab. 3. To clone a global template for customization, click the **Clone Global Template** button associated with the global template you want to customize. ![Global Templates](https://developer.salesforce.com/media/campaign-development/global-templates.png) 4. Cloning a global template opens a new copy of the template, with the text "[CLONE]" prepended to the template name. **note:** Cloning and customizing a global template doesn’t alter the original version of the global template in the **Global Templates** tab. You can make as many clones of a global template as you require for different use cases. ![Clone Global Template](https://developer.salesforce.com/media/campaign-development/global-templates-clone.png) **note:** In global templates, comments are provided at the top of the code tabs to help template developers effectively customize the template. For an example of comments within global templates, see the comments in the **Handlebars** tab in the above **[CLONE] Einstein Product Recommendations** figure. 5. Click **Save** to save the cloned version of the global template to your dataset. This cloned version appears in the **Templates** tab along with any other templates you’ve created or cloned. ![Template List](https://developer.salesforce.com/media/campaign-development/templates-list-with-cloned-global-template.png) ## Customize the Template After you’ve cloned a global template as described in the preceding section, customize the template components to meet the requirements of the business use case. As a best practice, we recommend you modify the template component code in the following order: 1. Modify the server-side code in the **Serverside Code** tab. See [Web Template Server TypeScript](https://developer.salesforce.com/docs/marketing/personalization/guide/template-server-typescript.md) for more detail. 2. Modify the client-side code in the **Clientside Code** tab. See [Web Template Client JavaScript](https://developer.salesforce.com/docs/marketing/personalization/guide/template-client-javascript.md) for more detail. 3. Develop the Handlebars templated HTML code in the **Handlebars** tab. See [Web Template Handlebars](https://developer.salesforce.com/docs/marketing/personalization/guide/web-template-handlebars.md) for more detail. 4. Develop the CSS style code in the **CSS** tab. See [Web Template CSS](https://developer.salesforce.com/docs/marketing/personalization/guide/web-template-css.md) for more detail. ## Template Developer Best Practices When customizing global templates, follow the recommendations outlined in the following articles. - [Web Template Building Best Practices](https://developer.salesforce.com/docs/marketing/personalization/web-campaigns-templates/web-template-building-best-practices.md) provide an outline of standards for building templates that run optimally on your website. - [Web Templates Style Guide and Coding Conventions](https://developer.salesforce.com/docs/marketing/personalization/web-campaigns-templates/web-templates-style-guide-and-coding-conventions.md) provide a guide to writing consistent template code. ## See Also - _Salesforce Help_: [Web Campaign Template Properties](https://help.salesforce.com/s/articleView?id=sf.mc_pers_web_campaign_template_properties.htm) - _Salesforce Help_: [Install the Salesforce Interactions SDK Launcher](https://help.salesforce.com/s/articleView?id=sf.mc_pers_web_campaign_interactions_sdk_launcher.htm) # Get Started with Marketing Cloud Personalization You can configure Personalization to support customer engagement campaigns across many different digital communication channels, including Web, Email, and Mobile. It also supports campaigns through integrations with other systems such as CRM and other external data systems. You can perform some of these tasks in the Personalization UI, while other require writing code specific to the requirements of the company and its business users. Developer tasks refer to those tasks that require some kind of code development, while business user tasks refer to tasks carried out by users in the Personalization UI. This documentation explains the tasks for developers. To learn how businesses use channels and develop campaigns in the Personalization UI, see [Channels and Campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_channel_campaign.htm) on Salesforce Help. ## Integrate Web Sites [Web channel campaigns](https://developer.salesforce.com/docs/marketing/personalization/guide/campaign-development.md) and the [web templates](https://developer.salesforce.com/docs/marketing/personalization/guide/campaign-development/personalization-template-system.md) that support them require integration with the company website. This integration is achieved through the Salesforce Interactions SDK and the [Sitemap](https://developer.salesforce.com/docs/marketing/personalization/guide/web-integration/personalization-sitemap.md), a part of the Web SDK, that recognizes, processes and sends user interactivity data from the website to Personalization. The individual or team responsible for managing scripts on the company website must deploy the Salesforce Interactions SDK to the designated company website pages. Then, a developer must validate the deployment and configure the Sitemap based on the data processing requirements provided by the business. The articles in the [Web Integration](https://developer.salesforce.com/docs/marketing/personalization/guide/web-integration.md) section of this site provide website integration planning considerations, Salesforce Interactions SDK specifications, and Sitemap configuration requirements. ## Develop Web Templates Before business users can develop web campaigns in the Personalization UI, Personalization developers must create the framework for displaying those web campaigns by developing reusable web templates. The [Campaign Development](https://developer.salesforce.com/docs/marketing/personalization/guide/campaign-development.md) section of this site explains the web campaign development process from the developer perspective and contains the articles explaining the technical requirements for web template development. ## Integrate Real-Time Event Data Use the [Event API](https://developer.salesforce.com/docs/marketing/personalization/guide/event-api/event-api.md) to process real-time data. The Event API is a REST API that processes and responds to event data sent from the Sitemap or from external systems. The articles in the [Event API](https://developer.salesforce.com/docs/marketing/personalization/guide/event-api/event-api.md) section of this site cover the purpose, capabilities and requirements for using the Event API, HTTP request and response specifications, and HTTP request and response examples for common Personalization use cases. ## Integrate Data Ingestion Systems Personalization supports batch data ingestion and processing from external data system sources through ETL or other batch data processing technologies. The complete requirements for integrating external data systems for batch data ingestion into Personalization are documented in the [About ETL Data Feeds](https://help.salesforce.com/s/articleView?id=sf.mc_pers_etl_integration_about.htm) article on Salesforce Help. See [Data Ingestion](https://developer.salesforce.com/docs/marketing/personalization/guide/data-ingestion.md) in this developer site for a brief overview. ## Integrate Mobile Apps and Responsive Templates Personalization can support campaigns for display on mobile devices by using responsive HTML (HTML5) in its normal [web templates](https://developer.salesforce.com/docs/marketing/personalization/guide/campaign-development/personalization-template-system.md) feature or through integration with native iOS and Android mobile apps. See [Personalization Mobile Integrations](https://developer.salesforce.com/docs/marketing/personalization/guide/mobile-integration.md) for an introduction to the capabilities of the Personalization iOS and Android mobile app integrations and links to the separate native mobile app developer documentation websites. ## See Also - [_Salesforce Help_: Channels and Campaigns](https://help.salesforce.com/s/articleView?id=sf.mc_pers_channel_campaign.htm) - [_Salesforce Help_: About ETL Data Feeds](https://help.salesforce.com/s/articleView?id=sf.mc_pers_etl_integration_about.htm) # Get Started with Site Mapping A Marketing Cloud Personalization Sitemap is a configuration-driven system deployed by and executed within the Personalization module of the Salesforce Interactions SDK. The Sitemap configuration data enables other parts of the Personalization platform to register and receive events from the client context. This article provides guidance for configuring the JavaScript components of a Sitemap required to implement the Personalization web channel solution architecture. For a more comprehensive introduction to the Sitemap, see [Sitemap Overview](https://developer.salesforce.com/docs/marketing/personalization/guide/personalization-sitemap.md). ## Sitemap Development Process To develop a Sitemap, do the following. 1. Navigate to the site that contains the Personalization JavaScript Web SDK. 2. Launch the Personalization **Visual Editor** and log in. For guidance on installing and starting the Personalization Visual Editor, refer to the [Install the Salesforce Interactions SDK Launcher](https://help.salesforce.com/s/articleView?id=sf.mc_pers_web_campaign_interactions_sdk_launcher.htm). ![Launch Salesforce Interactions SDK Launcher](https://developer.salesforce.com/media/Interaction-Studio-Launcher.png) 3. Open the Sitemap Editor by clicking **Site Map**, as highlighted in the following image. ![Launch the Site Map](https://developer.salesforce.com/media/site-map-launch.png) 4. In the **Sitemap JS** tab of the Sitemap Editor, configure the JavaScript components required for your Sitemap as described in the remaining sections of this article. ![Sitemap Editor](https://developer.salesforce.com/media/sitemap-editor.png) ## Sitemap JavaScript Configuration Overview A Sitemap is built and initialized after the Personalization Web SDK is initialized. Developing a Sitemap consists of writing the JavaScript required to do the following. 1. Initializing the Web SDK 2. Configuring the Sitemap JavaScript objects 3. Initializing the Sitemap ### Sitemap JavaScript Code Structure The following code samples depict the general JavaScript code structure for a Sitemap in each of the SDK namespaces. ```javascript SalesforceInteractions.init({ // Initializes the Personalization module of the Salesforce Interactions SDK cookieDomain: "company.com", // Optional tracking cookie domain configuration (overrides default) }).then(() => { const sitemapConfig = { // Sitemap configuration object global: {}, // Object used to contain Global site object configuration pageTypes: [], // Array used to contain the page type object configurations }; SalesforceInteractions.initSitemap(sitemapConfig); // Initializes the Sitemap }); ``` ```javascript Evergage.init({ // Initializes the Personalization module of the Salesforce Interactions SDK cookieDomain: "company.com", // Optional tracking cookie domain configuration (overrides default) }).then(() => { const sitemapConfig = { // Sitemap configuration object global: {}, // Object used to contain Global site object configuration pageTypes: [], // Array used to contain the page type object configurations }; Evergage.initSitemap(sitemapConfig); // Initializes the Sitemap }); ``` The following sections present the considerations and guidance for developing each functional component of a Sitemap. For examples of complete Sitemap configurations, see [Example Sitemaps](https://developer.salesforce.com/docs/marketing/personalization/guide/example-sitemaps.md). ## Initialize the Personalization module of the Salesforce Interactions SDK At the beginning of the Sitemap JavaScript, you must initialize the Personalization module of the Salesforce Interactions SDK with the following function. ```javascript SalesforceInteractions.init({ }).then(() => { } ---Sitemap Configuration--- ``` ```javascript Evergage.init({ }).then(() => { } ---Sitemap Configuration--- ``` The preceding function initializes the Personalization module of the Web SDK and allows it to begin tracking. The Web SDK starts tracking only if `SalesforceInteractions.init()` is called. ### Specify an Optional Configuration of the Web SDK You can specify an optional configuration of the Web SDK by setting `config` object parameters for the `SalesforceInteractions.init` function. These parameters are strictly optional and aren’t required for the Sitemap to function correctly. The following table lists the optional parameters accepted by the `config` object. | Field Name | Expected Value Type | Required? | Description | | -------------- | ------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `trackerUrl` | String | No | Overrides the default URL where the tracker sends events. The `trackerUrl` is of the format `https://..evergage.com`, where `accountName` is your Personalization account name and `instance` is your Personalization instance identifier. You can retrieve these details by accessing Gears from the Personalization UI and reviewing the URL. | | `dataset` | String | No | Name or identifier of your Personalization dataset | | `cookieDomain` | String | No | Overrides the default domain used for setting the Personalization tracking cookie | **important:** Ensure that you use the `account`, `dataset`, and `trackerUrl` parameters exclusively in your staging environment for testing purposes and seldom in your production environment. You can use these parameters to configure Sitemap operations to occur in your sandbox or learning environment without disrupting your production environment. Although there are instances where you have to use the `trackerUrl` parameter in your production environment, such instances are incredibly rare. You can use the `cookieDomain` parameter in the production environment in scenarios where you must share the tracking cookie across all possible subdomains of the default site host. For example, if the default site host is `something.company.com`, the tracking cookie is set only on that host by default. You can override this domain to share the cookie across all subdomains of `company.com` with the following configuration. ```javascript SalesforceInteractions.init({ cookieDomain: "company.com", }); ``` ```javascript Evergage.init({ cookieDomain: "company.com", }); ``` #### Optional Web SDK `config` Parameter Example The following code samples depict an optional configuration of the Web SDK using `config` object parameters. ```javascript SalesforceInteractions.init({ trackerUrl: "https://..evergage.com", cookieDomain: "staging.company.com", }); ``` ```javascript Evergage.init({ trackerUrl: "https://..evergage.com", cookieDomain: "staging.company.com", }); ``` **note:** As mentioned in the previous section, use the `trackerUrl` parameter only in your staging or learning environment and never in your production environment. However, you can use the `cookieDomain` parameter in your production or staging environment as needed. ## Configure Sitemap Objects The `const sitemapConfig` variable is used to configure the JavaScript objects that capture site visitor actions. You can use the optional `global` object to define the user actions to be captured on every page that is matched as a page type. The pageTypes array is used to contain the configurations of one or more specific page types in a site. A Sitemap developer must configure one `global` object, and the `pageTypes` array must have a page type object configured for each page type on the site. ### Common Object Configuration Examples A Sitemap can accept any valid JavaScript. Some JavaScript objects from the Personalization Web SDK are required to support the Personalization data collection and transmission functionality. Some are required in the `pageTypes` array configuration but not used in the `global` object. The following table describes the JavaScript objects from the Web SDK commonly used in the Sitemap `global` and `pageTypes` configurations. | Object | Purpose | Usage | Configuration | Namespace | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | ------------------------------------- | ------------------------ | | `contentZones` | The array of objects used to indicate areas of a page that are eligible for customization | Optional | Global object or configured page type | Both | | `listeners` | The array of `listener()` functions used to listen for user actions that trigger events to send to Personalization | Optional | Global object or configured page type | Both | | `isMatch` | The function required (along with the page name) to match a specific page type | **Required** | Configured page type only | Both | | `catalog` | The object used to collect and send data on catalog items. Used only with the `Evergage` namespace. | Optional | Configured page type only | `Evergage` | | `order` | The object used to collect and send order data to Personalization, including line items, order IDs, total order value, and currency. | Context-dependent | Configured page type only | `Evergage` | | `action` | The property of `ActionEvent` that is used to identify user actions sent to Personalization | Context-dependent | Global object or configured page type | `Evergage` | | `itemAction` | The property of `ActionEvent` used to identify user actions taken on catalog items such as "Purchase" or "AddToCart". | Context-dependent | Configured page type only | `Evergage` | | `interaction` | The property of `ActionEvent` used to identify user actions taken on catalog items such as "Purchase" or "AddToCart". | Context-dependent | Configured page type only | `SalesforceInteractions` | | `onActionEvent` | The function used with `ActionEvent` that sends events to Personalization based on user actions | Optional | Global object or configured page type | Both | **important:** If you're using the `Evergage` namespace within your Sitemap code, you must continue to use the `itemAction` property instead of `interaction` to identify user actions. For more information on Item Actions, refer to the [Item Actions](https://developer.salesforce.com/docs/marketing/personalization/guide/item-actions.md) documentation. #### Resolvers Resolvers are a set of properties provided by the Web SDK that help you return the values of page elements and attributes. You can use resolvers with some objects in the preceding table, such as `catalog`, to more easily return item information. Resolvers return functions so that they can be invoked _after_ a page has been matched. When used in a page type configuration, the returned functions don’t need to be invoked to get the final value as the underlying Sitemap code handles that logic. The following code samples show resolvers being used to return information on line items on an ecommerce site. ```javascript attributes: { name: SalesforceInteractions.resolvers.fromJsonLd("name"), description: SalesforceInteractions.resolvers.fromSelector(".short-description", (desc) => desc.trim()), url: SalesforceInteractions.resolvers.fromHref(), imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute(".product-carousel .carousel-item[data-slick-index='0'] img", "src", (url) => window.location.origin + url) } ``` ```javascript attributes: { name: Evergage.resolvers.fromJsonLd("name"), description: Evergage.resolvers.fromSelector(".short-description"), url: Evergage.resolvers.fromHref(), imageUrl: Evergage.resolvers.fromSelectorAttribute(".product-carousel .carousel-item[data-slick-index='0'] img", "src"), } ``` **note:** For detailed guidance on implementing resolvers and a complete list of the available resolvers, see [Sitemap Implementation Notes](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.md). #### Common Object Configuration Examples The following sections describe and present example configurations of the common Sitemap objects described in the preceding table. The example configurations shown here are taken from the complete [ecommerce Example Sitemap](https://developer.salesforce.com/docs/marketing/personalization/guide/example-sitemaps/ecommerce.md). **Content Zones** Content Zones are used to designate areas of a page that are potential targets for rendering personalized content from customer engagement campaigns, with each content zone targeting a single zone. This functionality is implemented using a `contentZones` array. Individual Content Zones consist of a `name` defined by you indicating the Content Zone's location on a page and a `selector` to designate the HTML element eligible for personalization. Content Zones can be defined without a `selector`, meaning no designated HTML element is associated with the Content Zone. This usage supports abandonment popups that display when a user begins to browse away from the page. The following are examples of Content Zone configurations. ```javascript // Example: "Header" and "Footer" Content Zones defined in the global object configuration contentZones: [ { name: "global_infobar_top_of_page", selector: "header.site-header" }, { name: "global_infobar_bottom_of_page", selector: "footer.site-footer" } ], // Example: Content Zones defined for product recommendation rows in product detail page type contentZones: [ { name: "product_detail_recs_row_1", selector: ".row.recommendations div[id*='cq']:nth-of-type(1)" }, { name: "product_detail_recs_row_2", selector: ".row.recommendations div[id*='cq']:nth-of-type(2)" }, ], // Example: "Logical" Content Zone defined with no selector for popup in the global object configuration contentZones: [ { name: "global_popup" } ], ``` ```javascript // Example: "Header" and "Footer" Content Zones defined in the global object configuration contentZones: [ { name: "global_infobar_top_of_page", selector: "header.site-header" }, { name: "global_infobar_bottom_of_page", selector: "footer.site-footer" } ], // Example: Content Zones defined for product recommendation rows in product detail page type contentZones: [ { name: "product_detail_recs_row_1", selector: ".row.recommendations div[id*='cq']:nth-of-type(1)" }, { name: "product_detail_recs_row_2", selector: ".row.recommendations div[id*='cq']:nth-of-type(2)" }, ], // Example: "Logical" Content Zone defined with no selector for popup in the global object configuration contentZones: [ { name: "global_popup" } ], ``` **note:** For more information and examples of commonly used Content Zones, see [Content Zones](https://developer.salesforce.com/docs/marketing/personalization/guide/content-zones.md). **Listeners** Sitemap listeners are used to listen for user actions on a page, capture information from the action, and send that action and information to Personalization. Listeners could be used in the global object configuration and specific page types. The following are examples of listeners configured in the `global` config and `product_detail` page type. ```javascript // Example: Listener on the global object for email sign-up that captures user email listeners: [ SalesforceInteractions.listener("submit", ".email-signup", () => { const email = SalesforceInteractions.cashDom("#dwfrm_mcsubscribe_email").val(); if (email) { SalesforceInteractions.sendEvent({ interaction: { name: "Email Sign Up - Footer" }, user: { identities: { emailAddress: email } } }); } }), ], // Example: Listener on a product detail page that listens for an add-to-cart action and captures the product line item added listeners: [ SalesforceInteractions.listener("click", ".add-to-cart", () => { let lineItem = SalesforceInteractions.mcis.buildLineItemFromPageState("select.quantity-select option:checked"); SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.CartInteractionName.AddToCart, lineItem: lineItem } }); }), ], ``` ```javascript // Example: Listener on the global object for email sign-up that captures user email listeners: [ Evergage.listener("submit", ".email-signup", () => { const email = Evergage.cashDom("#dwfrm_mcsubscribe_email").val(); if (email) { Evergage.sendEvent({action: "Email Sign Up - Footer", user: {id: email} }); } }), ], // Example: Listener on a product detail page that listens for an add-to-cart action and captures the product line item added listeners: [ Evergage.listener("click", ".add-to-cart", () => { const lineItem = Evergage.util.buildLineItemFromPageState("select[id*=quantity]"); lineItem.sku = { _id: Evergage.cashDom(".product-detail[data-pid]").attr("data-pid") }; Evergage.sendEvent({ itemAction: Evergage.ItemAction.AddToCart, cart: { singleLine: { Product: lineItem } } }); }), ], ``` **note:** For more information on implementing Sitemap listeners, see [Sitemap Implementation Notes](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.md#listeners). **isMatch** The `isMatch` property is a required component of each page type configuration in the `pageTypes` array. The `isMatch` property must declare a function that tells Personalization how to determine what type of page the user is viewing. An `isMatch` function can return a boolean, or a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves a boolean. The following are example configurations of the `isMatch` property. ```javascript // Example: isMatch property using the Web SDK cashDom function to return the "Product-Show" DOM element which // would indicate the user is on the Product Detail ("product_detail") page type. name: "product_detail", isMatch: () => { return SalesforceInteractions.cashDom("div.page[data-action='Product-Show']").length > 0; } // Example: isMatch property using a regex expression to return the window href "/cart" which would indicate // the user is on the shopping cart ("cart") page type. name: "cart", isMatch: () => /\/cart/.test(window.location.href), // Example: isMatch property that returns a Promise name: "cart", isMatch: () => { return SalesforceInteractions.DisplayUtils.pageElementLoaded(".cart-empty, .cart", "html").then(() => true); } ``` ```javascript // Example: isMatch property using the Web SDK cashDom function to return the "Product-Show" DOM element which // would indicate the user is on the Product Detail ("product_detail") page type. name: "product_detail", isMatch: () => { return Evergage.cashDom("div.page[data-action='Product-Show']").length > 0; } // Example: isMatch property using a regex expression to return the window href "/cart" which would indicate // the user is on the shopping cart ("cart") page type. name: "cart", isMatch: () => /\/cart/.test(window.location.href), // Example: isMatch property that returns a Promise name: "cart", isMatch: () => { return Evergage.DisplayUtils.pageElementLoaded(".cart-empty, .cart", "html").then(() => true); } ``` **Capture Catalog Information** You can capture information on Catalog Objects being viewed through the Sitemap. When using the `SalesforceInteractions` namespace, this information is captured from `interaction.catalogObject` of the matching `pageType` for `ViewCatalogObject` interactions. When using the `Evergage` namespace, this information is captured from the `catalog` object of the matching `pageType`. The `id` field (or `_id` when using the `Evergage` namespace) is required when tracking a Catalog Object. In the `SalesforceInteractions` namespace, a Promise can be returned for every field of the `catalogObject`. In the `Evergage` namespace, a Promise can be returned for the `_id`, `categories`, and `relatedCatalogObjects` fields. When returning Promises for these fields, the event is tracked only after all Promises resolve. **important:** Catalog items and attributes must be defined and enabled through the Personalization UI before you can use them in a Sitemap. Some item attributes, like `id`, `name`, and `description`, are built into every catalog item by default. You can add additional attributes for catalog items within the Personalization UI as needed. For more information on defining catalog items and attributes, see [Create a Custom Catalog Object](https://help.salesforce.com/s/articleView?id=sf.mc_pers_catalog_object_custom.htm). The following are examples of capturing catalog information on a product detail page type. ```javascript interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Product", id: () => { return SalesforceInteractions.util.resolveWhenTrue.bind(() => { const productId = SalesforceInteractions.cashDom(".product-id").first().text(); const products = getProductsFromDataLayer(); if (products && products.length > 0) { return products[0].id; } else if (productId) { return productId; } else { return false; } }) }, attributes: { sku: { id: SalesforceInteractions.cashDom(".product-detail[data-pid]").attr("data-pid") }, name: SalesforceInteractions.resolvers.fromJsonLd("name"), description: SalesforceInteractions.resolvers.fromSelector(".short-description", (desc) => desc.trim()), url: SalesforceInteractions.resolvers.fromHref(), imageUrl: SalesforceInteractions.resolvers.fromSelectorAttribute(".product-carousel .carousel-item[data-slick-index='0'] img", "src", (url) => window.location.origin + url), inventoryCount: 1, price: SalesforceInteractions.resolvers.fromSelector(".prices .price .value", (price) => parseFloat(price.replace(/[^0-9\.]+/g, ""))), rating: () => { return SalesforceInteractions.mcis.extractFirstGroup(/([.\w]+) out of/, SalesforceInteractions.cashDom(".ratings .sr-only").text()); }, }, relatedCatalogObjects: { Category: SalesforceInteractions.DisplayUtils.pageElementLoaded(".container .product-breadcrumb .breadcrumb a", "html").then((ele) => { return SalesforceInteractions.resolvers.buildCategoryId(".container .product-breadcrumb .breadcrumb a", null, null, (categoryId) => [categoryId.toUpperCase()]); }), Gender: SalesforceInteractions.DisplayUtils.pageElementLoaded(".product-breadcrumb .breadcrumb a, h1.product-name", "html").then((ele) => { if (SalesforceInteractions.cashDom(".product-breadcrumb .breadcrumb a").first().text().toLowerCase() === "women" || SalesforceInteractions.cashDom("h1.product-name").text().indexOf("Women") >= 0) { return ["WOMEN"]; } else if (SalesforceInteractions.cashDom(".product-breadcrumb .breadcrumb a").first().text().toLowerCase() === "men" || SalesforceInteractions.cashDom("h1.product-name").text().indexOf("Men") >= 0) { return ["MEN"]; } else { return; } }), Color: SalesforceInteractions.DisplayUtils.pageElementLoaded(".attributes", "html").then((ele) => { return SalesforceInteractions.resolvers.fromSelectorAttributeMultiple(".color-value", "data-attr-value"); }), Feature: SalesforceInteractions.DisplayUtils.pageElementLoaded(".features", "html").then((ele) => { return SalesforceInteractions.resolvers.fromSelectorMultiple(".long-description li", (features) => { return features.map((feature) => { return feature.trim().toUpperCase(); }) }) }) } } } ``` ```javascript catalog: { Product: { _id: () => { return Evergage.util.resolveWhenTrue.bind(() => { const productId = Evergage.cashDom(".product-id").first().text(); const products = getProductsFromDataLayer(); if (products && products.length > 0) { return products[0].id; } else if (productId) { return productId; } else { return false; } }) }, sku: { _id: Evergage.cashDom(".product-detail[data-pid]").attr("data-pid") }, name: Evergage.resolvers.fromJsonLd("name"), description: Evergage.resolvers.fromSelector(".short-description"), url: Evergage.resolvers.fromHref(), imageUrl: Evergage.resolvers.fromSelectorAttribute(".product-carousel .carousel-item[data-slick-index='0'] img", "src"), inventoryCount: 1, price: Evergage.resolvers.fromSelector(".prices .price .value"), rating: () => { return Evergage.util.extractFirstGroup(/([.\w]+) out of/, Evergage.cashDom(".ratings .sr-only").text()); }, categories: () => { return Evergage.DisplayUtils.pageElementLoaded(".container .product-breadcrumb .breadcrumb a", "html").then((ele) => { return Evergage.resolvers.buildCategoryId(".container .product-breadcrumb .breadcrumb a", null, null, (categoryId) => [categoryId.toUpperCase()]); }); }, relatedCatalogObjects: { // In case you're using 'dimensions' instead of 'relatedCatalogObjects', you can continue to do so as they both function the same way. Gender: () => { return Evergage.DisplayUtils.pageElementLoaded(".product-breadcrumb .breadcrumb a, h1.product-name", "html").then((ele) => { if (Evergage.cashDom(".product-breadcrumb .breadcrumb a").first().text().toLowerCase() === "women" || Evergage.cashDom("h1.product-name").text().indexOf("Women") >= 0) { return ["WOMEN"]; } else if (Evergage.cashDom(".product-breadcrumb .breadcrumb a").first().text().toLowerCase() === "men" || Evergage.cashDom("h1.product-name").text().indexOf("Men") >= 0) { return ["MEN"]; } else { return; } }); }, Color: () => { return Evergage.DisplayUtils.pageElementLoaded(".attributes", "html").then((ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple(".color-value", "data-attr-value"); }); }, Feature: () => { return Evergage.DisplayUtils.pageElementLoaded(".features", "html").then((ele) => { return Evergage.resolvers.fromSelectorMultiple(".features .feature", (features) => { return features.map((feature) => { return feature.trim(); }); }); }); } } } } ``` **Capture Cart Information** You can capture information on the user's `cart` through the Sitemap. When using the `SalesforceInteractions` namespace, this information is captured from `interaction.lineItems` of the matching pageType for `ReplaceCart` interactions. When using the `Evergage` namespace, this information is captured from the `cart` object. Capturing information about the user's `cart` is typically used in the shopping cart pages of ecommerce sites to collect information about line items, including the item ID (or SKU), price, and quantity. When using the `Evergage` namespace, the `_id`, `price`, and `quantity` fields of a cart object can return a Promise. When returning Promises for these fields, an event is sent only if all Promises resolve. The following are examples of capturing cart information from a cart page type. **note:** In the `SalesforceInteractions` namespace, you can use the `ReplaceCart` interaction to update the state of the cart in Personalization by setting its content to the provided line items while replacing the previous cart contents. ```javascript // Example: Without SKU interaction: { name: SalesforceInteractions.CartInteractionName.ReplaceCart, lineItems: SalesforceInteractions.DisplayUtils.pageElementLoaded(".cart-empty, .checkout-btn", "html").then(() => { let cartLineItems = []; SalesforceInteractions.cashDom(".product-info .product-details").each((index, ele) => { let itemQuantity = parseInt(SalesforceInteractions.cashDom(ele).find(".qty-card-quantity-count").text().trim()); if (itemQuantity && itemQuantity > 0) { let lineItem = { catalogObjectType: "Product", catalogObjectId: SalesforceInteractions.cashDom(ele).find(".line-item-quantity-info").attr("data-pid").trim(), price: SalesforceInteractions.cashDom(ele).find(".pricing").text().trim().replace(/[^0-9\.]+/g, "") / itemQuantity, quantity: itemQuantity }; cartLineItems.push(lineItem); } }) return cartLineItems; }) } // Example: With SKU interaction: { name: SalesforceInteractions.CartInteractionName.ReplaceCart, lineItems: SalesforceInteractions.DisplayUtils.pageElementLoaded(".cart-empty, .checkout-btn", "html").then(() => { let cartLineItems = []; SalesforceInteractions.cashDom(".product-info .product-details").each((index, ele) => { let itemQuantity = parseInt(SalesforceInteractions.cashDom(ele).find(".qty-card-quantity-count").text().trim()); if (itemQuantity && itemQuantity > 0) { let lineItem = { catalogObjectType: "Product", price: SalesforceInteractions.cashDom(ele).find(".pricing").text().trim().replace(/[^0-9\.]+/g, "") / itemQuantity, quantity: itemQuantity, attributes: { sku: { id: SalesforceInteractions.cashDom(ele).find(".line-item-quantity-info").attr("data-sku").trim() } } }; cartLineItems.push(lineItem); } }) return cartLineItems; }) } ``` **note:** In the `Evergage` namespace, you can use the `cart.complete` object to update the state of the cart in Personalization by setting its content to the provided line items while replacing previous cart contents. ```javascript cart: { complete: { Product: { _id: () => { return Evergage.DisplayUtils.pageElementLoaded(".cart-empty, .checkout-btn", "html").then((ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple(".product-info .product-details .line-item-quantity-info", "data-pid") }) }, price: () => { return Evergage.DisplayUtils.pageElementLoaded(".cart-empty, .checkout-btn", "html").then((ele) => { return Evergage.resolvers.fromSelectorMultiple(".product-info .product-details .pricing"); }) }, quantity: () => { return Evergage.DisplayUtils.pageElementLoaded('.cart-empty, .checkout-btn', "html").then((ele) => { return Evergage.resolvers.fromSelectorMultiple(".product-info .product-details .qty-card-quantity-count"); }); } } } } ``` **Capture Order Information** Orders are typically used in the order confirmation pages of ecommerce sites to collect information about the line items in a transaction, including the item ID (or SKU), price, and quantity. When using the `SalesforceInteractions` namespace, this information is captured from `interaction.order` of the matching pageType for `Purchase` interactions. When using the `Evergage` namespace, this information is captured from the `order` object. In the `Evergage` namespace, an `order` object can return Promises for all fields. When returning Promises, an event is sent only after all Promises resolve. **note:** - The `resolver` functions in the Personalization Web SDK are especially useful in simplifying the retrieval of this data. - In the `Evergage` namespace, you must define an `_id` field under item type. However, if they’re properly attributed to a product, you can pass SKU values here instead of the `_id` field. The following are examples of capturing order information from an order confirmation page type. ```javascript // Example: Without SKU interaction: { name: SalesforceInteractions.OrderInteractionName.Purchase, order: { id: SalesforceInteractions.DisplayUtils.pageElementLoaded(".order-number", "html").then((ele) => { return SalesforceInteractions.resolvers.fromSelector(".order-number"); }), lineItems: SalesforceInteractions.DisplayUtils.pageElementLoaded(".product-line-item", "html").then(() => { let purchaseLineItems = []; SalesforceInteractions.cashDom(".product-line-item").each((index, ele) => { let itemQuantity = parseInt(SalesforceInteractions.cashDom(ele).find(".qty-card-quantity-count").text().trim()); if (itemQuantity && itemQuantity > 0) { let lineItem = { catalogObjectType: "Product", catalogObjectId: SalesforceInteractions.cashDom(ele).find(".line-item-quantity-info").attr("data-pid").trim(), price: SalesforceInteractions.cashDom(ele).find(".pricing").text().trim().replace(/[^0-9\.]+/g, "") / itemQuantity, quantity: itemQuantity }; purchaseLineItems.push(lineItem); } }) return purchaseLineItems; }) } } // Example: With SKU interaction: { name: SalesforceInteractions.OrderInteractionName.Purchase, order: { id: SalesforceInteractions.DisplayUtils.pageElementLoaded(".order-number", "html").then((ele) => { return SalesforceInteractions.resolvers.fromSelector(".order-number"); }), lineItems: SalesforceInteractions.DisplayUtils.pageElementLoaded(".product-line-item", "html").then(() => { let purchaseLineItems = []; SalesforceInteractions.cashDom(".product-line-item").each((index, ele) => { let itemQuantity = parseInt(SalesforceInteractions.cashDom(ele).find(".qty-card-quantity-count").text().trim()); if (itemQuantity && itemQuantity > 0) { let lineItem = { catalogObjectType: "Product", price: SalesforceInteractions.cashDom(ele).find(".pricing").text().trim().replace(/[^0-9\.]+/g, "") / itemQuantity, quantity: itemQuantity, attributes: { sku: { id: SalesforceInteractions.cashDom(ele).find(".line-item-quantity-info").attr("data-sku").trim() } } }; purchaseLineItems.push(lineItem); } }) return purchaseLineItems; }) } } ``` ```javascript order: { Product: { orderId: () => { return Evergage.DisplayUtils.pageElementLoaded(".order-number", "html").then((ele) => { return Evergage.resolvers.fromSelector(".order-number"); }); }, lineItems: { _id: () => { return Evergage.DisplayUtils.pageElementLoaded(".product-line-item line-item-quantity-info", "html").then((ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple(".product-line-item .line-item-quantity-info", "data-pid"); }); }, price: () => { return Evergage.DisplayUtils.pageElementLoaded(".product-line-item .pricing", "html").then((ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple(".product-line-item .pricing", "data-pid"); }); }, quantity: () => { return Evergage.DisplayUtils.pageElementLoaded(".product-line-item .qty-card-quantity-count", "html").then((ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple(".product-line-item .qty-card-quantity-count", "data-pid"); }); } } } } ``` **Action** The `action` property provides a way for Personalization to name user actions such as viewing the page or signing up for an email subscription. An `action` property is often included after the `name` in the page type specification. The following example code sends a "Login" action to Personalization every time a user accesses the `login` page type. **important:** The `action` property is available exclusively in the `Evergage` namespace. ```javascript { name: "login", action: "Login", isMatch: () => /\/login/.test(window.location.href) } ``` **Interaction** An `interaction` property is used to identify specific actions users take. Examples of `interaction` values include `AddToCart`, `Purchase`, and `Comment`. For a complete list of `interaction` values, refer to [Interaction Definitions](https://developer.salesforce.com/docs/marketing/personalization/guide/interaction-definitions.md). **important:** The `interaction` property is available exclusively in the `SalesforceInteractions` namespace. If you're using the `Evergage` namespace within your Sitemap code, you must continue to use the `itemAction` property instead of `interaction` to identify user actions. For more information on Item Actions, refer to the [Item Actions](https://developer.salesforce.com/docs/marketing/personalization/guide/item-actions.md) documentation. The following is an example of using the `interaction` property in the `SalesforceInteractions` namespace in Sitemap code. ```javascript // Example: Send Add-to-cart Interaction SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.CartInteractionName.AddToCart, lineItem: lineItem } }); // Example: Replace Cart Interaction on a shopping cart page name: "cart", isMatch: () => /\/cart/.test(window.location.href), interaction: { name: SalesforceInteractions.CartInteractionName.ReplaceCart, lineItems: lineItems } // Example: Purchase Interaction on an order confirmation page name: "order_confirmation", isMatch: () => /\/confirmation/.test(window.location.href), interaction: { name: SalesforceInteractions.OrderInteractionName.Purchase, order: { //order configuration } } ``` The following is an example of using the `itemAction` property within in the `Evergage` namespace in Sitemap code. ```javascript // Example: Send Add-to-cart item action as an event Evergage.sendEvent({ itemAction: Evergage.ItemAction.AddToCart cart: { singleLine: { Product: lineItem } } }); // Example: View-cart item action on a shopping cart page name: "cart", isMatch: () => /\/cart/.test(window.location.href), itemAction: Evergage.ItemAction.ViewCart cart: { //cart config } // Example: Purchase item action on an order confirmation page name: "order_confirmation", isMatch: () => /\/confirmation/.test(window.location.href), itemAction: Evergage.ItemAction.Purchase, order: { //order configuration } ``` **onActionEvent** The `onActionEvent` function is used with the `ActionEvent` property to send events to Personalization based on user actions. The following code samples depict the usage of the `onActionEvent` function in the `pageType` configuration in each of the SDK namespaces. ```javascript pageTypes: [ { name: "search", isMatch: () => /\/search/.test(window.location.pathname), onActionEvent: (actionEvent) => { const searchTerm = SalesforceInteractions.cashDom(".content.searchTerm").text().trim(); if (searchTerm) { actionEvent.user = actionEvent.user || {}; actionEvent.user.attributes = actionEvent.user.attributes || {}; actionEvent.user.attributes.lastSearchTerm = searchTerm; } return actionEvent; }, }, ]; ``` ```javascript pageTypes: [ { name: "search", isMatch: () => /\/search/.test(window.location.pathname), onActionEvent: (actionEvent) => { const searchTerm = Evergage.cashDom(".content.searchTerm").text().trim(); if (searchTerm) { actionEvent.user = actionEvent.user || {}; actionEvent.user.attributes = actionEvent.user.attributes || {}; actionEvent.user.attributes.lastSearchTerm = searchTerm; } return actionEvent; }, }, ]; ``` ### Configure the Global Sitemap Object The `global` Sitemap configuration object allows you to define what to capture on every page that is matched as a page type. The `global` Sitemap object enables you to define Content Zones, listeners, locale, and `onActionEvent` functions for every page that is matched instead of only on individually matched pages. The following shows an example of a `global` Sitemap configuration. ```javascript SalesforceInteractions.init({ cookieDomain: "northerntrailoutfitters.com" }).then(() => { const sitemapConfig = { global: { contentZones: [ {name: "global_infobar_top_of_page", selector: "header.site-header"}, {name: "global_infobar_bottom_of_page", selector: "footer.site-footer"}, {name: "global_popup" } ], listeners: [ SalesforceInteractions.listener("submit", ".email-signup", () => { const email = SalesforceInteractions.cashDom("#dwfrm_mcsubscribe_email").val(); if (email) { SalesforceInteractions.sendEvent({ interaction: { name: "Email Sign Up - Footer" }, user: { identities: { emailAddress: email } } }); } }), ], }, pageTypes: [ // ...page type configurations...] } SalesforceInteractions.initSitemap(sitemapConfig); }); ``` ```javascript Evergage.init({ cookieDomain: "northerntrailoutfitters.com" }).then(() => { const sitemapConfig = { global: { contentZones: [ {name: "global_infobar_top_of_page", selector: "header.site-header"}, {name: "global_infobar_bottom_of_page", selector: "footer.site-footer"}, {name: "global_popup" } ], listeners: [ Evergage.listener("submit", ".email-signup", () => { const email = Evergage.cashDom("#dwfrm_mcsubscribe_email").val(); if (email) { Evergage.sendEvent({action: "Email Sign Up - Footer", user: {id: email} }); } }), ], }, pageTypes: [ // ...page type configurations...] } Evergage.initSitemap(sitemapConfig); }); ``` ## Configure Page Type Array Objects A Sitemap configuration consists of the [`global` configuration](#configure-the-global-sitemap-object), which runs for all page types, and an array of individual page type configurations. At a minimum, a page type requires configuring a `name` and an `isMatch` function. The `name` defines the name of the page type, while the `isMatch` function output is evaluated to determine if the page the user is on matches the page configuration. After `name` and `isMatch`, you can use any of the `sitemapConfig` objects mentioned in this article in a page type configuration object. The following code sample depicts a sample page type configuration. ```javascript { name: string isMatch: () => boolean | () => Promise } ``` The `isMatch` function accepts a function that returns a boolean or a Promise. The sitemap evaluates only the first page type whose `isMatch` resolves to `true`. If no page types match, no event is sent to Personalization. The following are examples of different page type configurations in each SDK namespace. ```javascript SalesforceInteractions.init({ cookieDomain: "northerntrailoutfitters.com", }).then(() => { const config = { global: { onActionEvent: (actionEvent) => { const email = SalesforceInteractions.mcis.getValueFromNestedObject( "window._etmc.user_info.email", ); if (email) { actionEvent.user = actionEvent.user || {}; actionEvent.user.identities = actionEvent.user.identities || {}; actionEvent.user.identities.emailAddress = email; } return actionEvent; }, contentZones: [ { name: "global_infobar_top_of_page", selector: "header.site-header" }, { name: "global_infobar_bottom_of_page", selector: "footer.site-footer" }, ], listeners: [ SalesforceInteractions.listener("submit", ".email-signup", () => { const email = SalesforceInteractions.cashDom("#dwfrm_mcsubscribe_email").val(); if (email) { SalesforceInteractions.sendEvent({ interaction: { name: "Email Sign Up - Footer Test" }, user: { identities: { emailAddress: email } }, }); } }), ], }, pageTypes: [ { name: "home", isMatch: () => /\/homepage/.test(window.location.href), interaction: { name: "Homepage", }, contentZones: [ { name: "home_hero", selector: ".experience-carousel-bannerCarousel" }, { name: "home_sub_hero", selector: ".experience-carousel-bannerCarousel + .experience-component", }, { name: "home_popup" }, ], }, { name: "login", action: "Login", isMatch: () => /\/login/.test(window.location.href) && window.location.hostname !== "community.northerntrailoutfitters.com", listeners: [ SalesforceInteractions.listener("click", "form[name='login-form'] button", () => { const email = SalesforceInteractions.cashDom("#login-form-email").val(); if (email) { SalesforceInteractions.sendEvent({ action: "Logged In", user: { attributes: { emailAddress: email } }, }); } }), ], }, { name: "cart", isMatch: () => /\/cart/.test(window.location.href), interaction: { name: SalesforceInteractions.CartInteractionName.ReplaceCart, lineItems: SalesforceInteractions.DisplayUtils.pageElementLoaded( ".cart-empty, .checkout-btn", "html", ).then(() => { let cartLineItems = []; SalesforceInteractions.cashDom(".product-info .product-details").each( (index, ele) => { let itemQuantity = parseInt( SalesforceInteractions.cashDom(ele) .find(".qty-card-quantity-count") .text() .trim(), ); if (itemQuantity && itemQuantity > 0) { let lineItem = { catalogObjectType: "Product", catalogObjectId: SalesforceInteractions.cashDom(ele) .find(".line-item-quantity-info") .attr("data-pid") .trim(), price: SalesforceInteractions.cashDom(ele) .find(".pricing") .text() .trim() .replace(/[^0-9\.]+/g, "") / itemQuantity, quantity: itemQuantity, }; cartLineItems.push(lineItem); } }, ); return cartLineItems; }), }, }, ], }; SalesforceInteractions.initSitemap(config); }); ``` ```javascript Evergage.init({ cookieDomain: "northerntrailoutfitters.com", }).then(() => { const config = { global: { onActionEvent: (actionEvent) => { const email = Evergage.util.getValueFromNestedObject("window._etmc.user_info.email"); if (email) { actionEvent.user = actionEvent.user || {}; actionEvent.user.attributes = actionEvent.user.attributes || {}; actionEvent.user.attributes.emailAddress = email; } return actionEvent; }, contentZones: [ { name: "global_infobar_top_of_page", selector: "header.site-header" }, { name: "global_infobar_bottom_of_page", selector: "footer.site-footer" }, ], listeners: [ Evergage.listener("submit", ".email-signup", () => { const email = Evergage.cashDom("#dwfrm_mcsubscribe_email").val(); if (email) { Evergage.sendEvent({ action: "Email Sign Up - Footer", user: { attributes: { emailAddress: email } }, }); } }), ], }, pageTypes: [ { name: "home", action: "Homepage", isMatch: () => /\/homepage/.test(window.location.href), contentZones: [ { name: "home_hero", selector: ".experience-carousel-bannerCarousel" }, { name: "home_sub_hero", selector: ".experience-carousel-bannerCarousel + .experience-component", }, { name: "home_popup" }, ], }, { name: "login", action: "Login", isMatch: () => /\/login/.test(window.location.href) && window.location.hostname !== "community.northerntrailoutfitters.com", listeners: [ Evergage.listener("click", "form[name='login-form'] button", () => { const email = Evergage.cashDom("#login-form-email").val(); if (email) { Evergage.sendEvent({ action: "Logged In", user: { attributes: { emailAddress: email } }, }); } }), ], }, { name: "cart", isMatch: () => /\/cart/.test(window.location.href), itemAction: Evergage.ItemAction.ViewCart, catalog: { Product: { lineItems: { _id: () => { return Evergage.DisplayUtils.pageElementLoaded( ".cart-empty, .checkout-btn", "html", ).then((ele) => { return Evergage.resolvers.fromSelectorAttributeMultiple( ".product-info .product-details .line-item-quantity-info", "data-pid", ); }); }, price: () => { return Evergage.DisplayUtils.pageElementLoaded( ".cart-empty, .checkout-btn", "html", ).then((ele) => { return Evergage.resolvers.fromSelectorMultiple( ".product-info .product-details .pricing", ); }); }, quantity: () => { return Evergage.DisplayUtils.pageElementLoaded( ".cart-empty, .checkout-btn", "html", ).then((ele) => { return Evergage.resolvers.fromSelectorMultiple( ".product-info .product-details .qty-card-quantity-count", ); }); }, }, }, }, }, ], }; Evergage.initSitemap(config); }); ``` **note:** For additional examples of page type configurations and complete Sitemaps, see [Example Sitemaps](https://developer.salesforce.com/docs/marketing/personalization/guide/example-sitemaps.md). ## Initialize the Sitemap A Sitemap is initialized at the end of the JavaScript code by calling `SalesforceInteractions.initSitemap(sitemapConfig)` in the `SalesforceInteractions` namespace or `Evergage.initSitemap(sitemapConfig)` in the `Evergage` namespace. Sitemap events aren’t tracked unless the sitemap is initialized by calling either of those functions _and_ until a matching page type is found. If your sitemap has errors or if no matching page types are found, you can encounter errors. The following code sample depicts the initialization of a Sitemap using `SalesforceInteractions.initSitemap(sitemapConfig)` in the `SalesforceInteractions` namespace. ```javascript SalesforceInteractions.init({ cookieDomain: "developer.evergage.com" }).then(() => { const sitemapConfig = { global: {...global object configuration...}, pageTypes: [...page type array configuration...] } SalesforceInteractions.initSitemap(sitemapConfig); }); ``` The following code sample depicts the initialization of a Sitemap using `Evergage.initSitemap(sitemapConfig)` in the `Evergage` namespace. ```javascript Evergage.init({ cookieDomain: "developer.evergage.com" }).then(() => { const sitemapConfig = { global: {...global object configuration...}, pageTypes: [...page type array configuration...] } Evergage.initSitemap(sitemapConfig); }); ``` ## See Also - [_Salesforce Help_: Install the Salesforce Interactions SDK Launcher](https://help.salesforce.com/s/articleView?id=sf.mc_pers_web_campaign_interactions_sdk_launcher.htm) - [_Salesforce Help_: Create a Custom Catalog Object](https://help.salesforce.com/s/articleView?id=sf.mc_pers_catalog_object_custom.htm) # Image Generation in Open Time Email Campaigns Open Time Email (OTE) campaigns and templates use image generation as a background process. When you publish an OTE template, the image generation process kicks off and determines whether eligible products and associated OTE Item Templates need images. It then creates an initial set of images for all items that match the item type of the template and stores them on the Marketing Cloud Personalization CDN. The images generated by this process appear in the OTE Simulator and the emails you provision via OTE campaigns. ## Common Problems & Causes The following table describes common problems that can occur with images in OTE campaigns and their causes. | Problem | Causes | | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Missing or invisible images | Occurs because there was an error in processing an image generation, which could be due misconfiguration, such as when using locales, or customer CDN throttling limits. | | Blurry images | Adding style, height, and width tags can cause blurriness. These tags aren’t necessary in order to generate images. In fact, images without style tags appear crisper. | | Differences between OTE Simulator and test emails | Occurs due to an error with the `customerID` in the test email or because the recipe returns a different result when the test email is opened. | | Differences between OTE Simulator and Recipe Simulator or OTE Item Template preview | Occurs because the OTE Simulator uses the images in the CDN, whereas the other features providing previews or simulation don’t. | | Differences between images and current product data | Occurs because the product and item template pair hasn’t been identified as needing image generation. Products are tagged for image generation only if they meet eligibility requirements and have properties referenced in a published item template that have changed values. Factors that determine image generation eligibility:
  • Products must be eligible, NOT archived, NOT expired, have a name, URL, `imageURL`, and be in stock (that is, inventory > 0) in order to be considered.
  • If the `imageURL` doesn't change but the image at the URL is modified, the CDN image doesn't get updated.
  • If a product's values not referenced in the OTE item template are altered, the CDN image for that product and item template pair will not be updated.
  • If locales are enabled, there may be issues if the default locale is not properly set.
  • | ## Limitations Image generation errors that occur due to customer throttling limits are outside of the control of Marketing Cloud Personalization. These errors usually appear as default transparent pixel images, or erroneous images with missing data. You can find additional error information in internal logs. ## Troubleshooting There are a few things you can do to troubleshoot and fix issues related to images in OTE campaigns and templates. ### Throttling Issues To fix throttling issues, see if adding the following user agent and HTTP headers to an allowlist to either avoid throttling or have a higher threshold: | Where | Add to Allowlist | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | HTTP Header | `'X-Salesforce-Agent': 'Interaction Studio Open-Time Email Image Generator` | | User Agent | `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36 Salesforce (InteractionStudio)` | ### Blurry Images You can improve the clarity of images in your templates by making a few changes to your Personalization OTE and Salesforce Marketing Cloud templates. #### Personalization OTE Template Changes 1. Clone the Personalization OTE template with blurry images. 2. Add the exact HTML comment `` to any valid HTML position in the template. For example, you can prepend the HTML comment at the beginning of the OTE template. **important:** This comment is case-sensitive. #### Marketing Cloud Template Changes 1. Specify width constraints for the Personalization OTE template's image slots. For example, if the image slots on your Personalization OTE template are 190 px wide: - Add the attribute `width="190"` (without `px`) to the `` tag that uses the OTE image as its `src`. - Modify the `style` attribute to include `width: 100%; max-width: 190px`. **note:** The width and height of image containers could require adjustments based on the HTML structure and desired final appearance. Keep in mind that standard CSS rules apply when using OTE images in HTML documents. 2. Use the template in a new campaign that uses the cloned HiDPI version of the original Personalization OTE template. **important:** If you've already deployed the original campaign with blurry images, you must carry out this step in production. # Ingesting Data into Marketing Cloud Personalization Marketing Cloud Personalization Feeds is a built-in data-feed processing capability that enables the ingestion of data from external sources. Feeds are a specific type of Personalization Gear extension that provides the pathways to ingest external data files and translate their contents into the Personalization platform. With Feeds, you can update the company catalog, import and export market segments, load historical data, and perform other tasks manually or automatically. After you configure a Feed, Personalization business users can use the **Feeds Dashboard** to view, monitor, and manage Feed data file processing. Companies are responsible for creating, structuring, and providing data files that meet Personalization specifications. However, Personalization Gear developers are responsible for configuring the Feed pathways that support these data files. ## Deploying Personalization Data Feeds To review implementation details and learn how to use Personalization data feeds, see [About ETL Data Feeds](https://help.salesforce.com/s/articleView?id=sf.mc_pers_etl_integration_about.htm) on Salesforce Help. ## See Also - [_Salesforce Help_: About ETL Data Feeds](https://help.salesforce.com/s/articleView?id=sf.mc_pers_etl_integration_about.htm) # Integrate the JavaScript Beacon The Marketing Cloud Personalization module of the Salesforce Interactions SDK uses a JavaScript beacon to monitor and track the online behavior of your organization’s web users. **note:** Before you integrate the JavaScript beacon, determine whether you want to use synchronous or asynchronous integration. For more information, see [Synchronous or Asynchronous Integration](https://developer.salesforce.com/docs/marketing/personalization/guide/web-integration-considerations.md#synchronous-or-asynchronous-integration). This integration process uses the Personalization content delivery network (CDN) to host the JavaScript beacon. To assume responsibility for hosting the JavaScript beacon on your organization’s CDN, see [Self-Host the Server-Side JavaScript Beacon](#self-host-the-server-side-javascript-beacon). 1. Log in to Personalization as an administrator. 2. From the **Channels & Campaigns** section of the main navigation, select **Web** > **JavaScript Integration**. 3. If you have more than one dataset, select the one you want from the dropdown at the top. 4. Click the tab for the type of integration you want: **Synchronous** or **Asynchronous**. 5. Copy the integration code for that type and place it into the HTML head element of your website to ensure that the beacon loads on every page. 6. Repeat this process for any other datasets you have. 7. After integration, if necessary, you can disable the JavaScript beacon by selecting **Web | Web SDK Settings** from the **Channels & Campaigns** section of the main navigation, selecting the **Disabled** toggle, and clicking **Save**. Selecting **Disabled** replaces the existing JavaScript beacon with an empty JavaScript resource. All event receipt, analytics, and messaging stop immediately, and propagates in minutes as viewers' browser caches expire. ## Self-Host the Server-Side JavaScript Beacon Before proceeding with self-hosting the JavaScript beacon, consider the following critical aspects of doing so: **important:** You must automate updating your SDK from the source SDK that Personalization hosts. The contents of the SDK are dynamic, so if you change various settings in Personalization, change installed gears, or make updates to your site map, the contents of the SDK can also change. If you don't update your hosted version of the SDK in tandem, your campaigns and tracking can break. At a minimum, configure the automated update to check for changes every hour. To self-host the JavaScript beacon, you must first obtain the JavaScript beacon code and then deploy it to your organization's CDN. 1. Log in to Personalization as an administrator. 2. From the **Channels & Campaigns** section of the main navigation, select **Web** > **JavaScript Integration**. 3. If you have more than one dataset, select the one you want from the dropdown at the top. 4. Click the **Synchronous** tab. 5. Copy the source URL, which begins after `src="https://developer.salesforce.com/docs/marketing/personalization/guide/%60,%20from%20the%20integration%20code.%20For%20example,%20%60//cdn.evgnet.com/beacon/myaccount/mydataset/scripts/evergage.min.js%60.%20This%20link%20provides%20access%20to%20the%20JavaScript%20beacon%20code,%20which%20you%20can%20then%20deploy%20on%20your%20organization's CDN to self-host the JavaScript beacon. # Interaction Definitions The Salesforce Interactions SDK provides interactions names used to identify certain user interactions related to `Catalog`, `cart`, and `order` objects. Interactions contain a reserved `name` property and value that Personalization uses to determine how to treat event data. **important:** The Interactions documented in this article are available exclusively in the `SalesforceInteractions` namespace. In the `Evergage` namespace, Interactions are called Item Actions. For more information, refer to the [Item Actions](https://developer.salesforce.com/docs/marketing/personalization/guide/item-actions.md) documentation. ## Interactions in the Event API An example of an event sent using the API within the Sitemap can be found in our [Example ecommerce Sitemap](https://developer.salesforce.com/docs/marketing/personalization/guide/example-sitemaps/ecommerce.md) within the `"product_detail"` pageType configuration. The following code sample includes the relevant code block. In this example, a listener has been mapped containing an `AddToCart` event that fires when the user clicks the DOM element referenced by the provided selector, `".add-to-cart"`. ```javascript listeners: [ SalesforceInteractions.listener("click", ".add-to-cart", () => { let lineItem = SalesforceInteractions.mcis.buildLineItemFromPageState("select.quantity-select option:checked"); SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.CartInteractionName.AddToCart, lineItem: lineItem } }); }) ], ``` ## Reserved Interaction Values When tracking Catalog Object Interactions, Cart Interactions, or Order Interactions, you have to reference all values assigned to the `interaction.name` property from the `SalesforceInteractions` object. The `SalesforceInteractions` namespace provides the following objects. - `SalesforceInteractions.CatalogObjectInteractionName` - `SalesforceInteractions.CartInteractionName` - `SalesforceInteractions.OrderInteractionName` ## Special Interaction Values - `SalesforceInteractions.CatalogObjectInteractionName.QuickViewCatalogObject` - Similar to `CatalogObjectInteractionName.ViewCatalogObject` but stops the timer for the "background" item and starts a timer for the "foreground" item. - `SalesforceInteractions.mcis.CatalogObjectInteractionName.StopQuickViewCatalogObject` - Stops the timer initiated by the "Quick View Item" interaction for the "foreground" item and restarts the timer for the "background" item. `QuickViewCatalogObject` and `StopQuickViewCatalogObject` are used to accurately track view time in cases where a user "views" an item while already viewing a different item. An ecommerce category page that allows users to view individual products in a window can take advantage of these interactions. In the following example, the "background" item is the category and the "foreground" item is the product. A new page load stops both timers. ```javascript { name: "Category", isMatch: () => new Promise((resolve, reject) => { let isMatchCat = setTimeout(() => { resolve(false); }, 50); return SalesforceInteractions.DisplayUtils.pageElementLoaded("#product-search-results", "html").then(() => { clearTimeout(isMatchCat); resolve(SalesforceInteractions.cashDom(".breadcrumb").length > 0); }); }), interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: { type: "Category", id: SalesforceInteractions.DisplayUtils.pageElementLoaded(".breadcrumb .breadcrumb-item a", "html").then((ele) => { return SalesforceInteractions.resolvers.buildCategoryId(".breadcrumb .breadcrumb-item a", 1, null, (categoryId) => categoryId.toUpperCase()); }) } } }, listeners: [ SalesforceInteractions.listener("click", ".quickview", (e) => { const pid = SalesforceInteractions.cashDom(e.target).attr("href").split("pid=")[1]; if (!pid) { return; } SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.QuickViewCatalogObject, catalogObject: { type: "Product", id: pid } } }); }), SalesforceInteractions.listener("click", "body", (e) => { if (SalesforceInteractions.cashDom(e.target).closest("button[data-dismiss='modal']").length > 0) { SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.mcis.CatalogObjectInteractionName.StopQuickViewCatalogObject, } }); } else if (SalesforceInteractions.cashDom(e.target).closest("#quickViewModal").length > 0 && SalesforceInteractions.cashDom(e.target).find("#quickViewModal .modal-dialog").length > 0) { SalesforceInteractions.sendEvent({ interaction: { name: SalesforceInteractions.mcis.CatalogObjectInteractionName.StopQuickViewCatalogObject, } }); } }) ] } ``` ## Currently Supported Interactions Only the `interactions` documented here are currently supported. Unless otherwise indicated, the following definitions apply to both the page config and Event API. ### CatalogObjectInteraction #### View Catalog Object ```javascript { interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObject, catalogObject: CatalogObject } } ``` #### View Catalog Object Detail ```javascript { interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ViewCatalogObjectDetail, catalogObject: CatalogObject } } ``` #### Share ```javascript { interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.ShareCatalogObject, catalogObject: CatalogObject } } ``` #### Comment ```javascript { interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.CommentCatalogObject, catalogObject: CatalogObject } } ``` #### Favorite ```javascript { interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.FavoriteCatalogObject, catalogObject: CatalogObject } } ``` #### Quick View Catalog Object ```javascript { interaction: { name: SalesforceInteractions.CatalogObjectInteractionName.QuickViewCatalogObject, catalogObject: CatalogObject } } ``` ### CartInteraction #### Add To Cart ```javascript { interaction: { name: SalesforceInteractions.CartInteractionName.AddToCart, lineItem: LineItem } } ``` #### Replace Cart ```javascript { interaction: { name: SalesforceInteractions.CartInteractionName.ReplaceCart, lineItems: LineItem[] } } ``` #### Remove From Cart ```javascript { interaction: { name: SalesforceInteractions.CartInteractionName.RemoveFromCart, lineItem: LineItem } } ``` ### OrderInteraction **note:** Personalization accepts OrderInteractions other than the Purchase OrderInteraction but only the Purchase OrderInteraction updates a user's order in Personalization. #### Purchase ```javascript { interaction: { name: SalesforceInteractions.OrderInteractionName.Purchase, order: Order } } ``` #### Preorder ```javascript { interaction: { name: SalesforceInteractions.OrderInteractionName.Preorder, order: Order } } ``` #### Cancel ```javascript { interaction: { name: SalesforceInteractions.OrderInteractionName.Cancel, order: Order } } ``` #### Ship ```javascript { interaction: { name: SalesforceInteractions.OrderInteractionName.Ship, order: Order } } ``` #### Deliver ```javascript { interaction: { name: SalesforceInteractions.OrderInteractionName.Deliver, order: Order } } ``` #### Return ```javascript { interaction: { name: SalesforceInteractions.OrderInteractionName.Return, order: Order } } ``` #### Exchange ```javascript { interaction: { name: SalesforceInteractions.OrderInteractionName.Exchange, order: Order } } ``` ### Other Interactions #### Stop Quick View Catalog Object ```javascript { interaction: { name: SalesforceInteractions.mcis.CatalogObjectInteractionName.StopQuickViewCatalogObject, } } ``` ## Best Practices For Using Interactions When using interactions, it’s important to keep the following best practices in mind: - Only track interactions if they're necessary for personalization and targeting. Think about the personalization use cases you wish to execute and only map the interactions required for those specific use cases. - Map no more than a few thousand unique interactions per dataset. Mapping any more leads to challenges when using the Personalization UI, including difficulties in selecting interactions from dropdown menus and slower report generation. - Ensure interaction names have values you can reuse across users and sessions. - When defining interaction names, avoid dynamically populating them to prevent inflated names with unexpected prefixes and suffixes. - If you dynamically populate interaction names, ensure they stay consistent across sessions for any user. - Refrain from automatically sending all events from other site analytics tools to Personalization, as it can result in a higher volume of events than necessary for personalization. - Incorporating dynamic user information in interaction names can cause issues and must be avoided whenever possible. # Interaction Event Options in the SalesforceInteractions Namespace The following code sample details the event options available in the `SalesforceInteractions` namespace in the Personalization module of the Salesforce Interactions SDK. ```javascript interface ActionEvent { interaction: InteractionType; debug?: boolean; flags?: boolean; user?: User; account?: Account; source?: Source; consents?: Consent[]; explain?: boolean; // If true, then return additional information in the response about why campaigns did or did not render. Requires authentication. pageView?: boolean; // If true, indicates that the event was triggered from a page load. time?: number; // The date and time of the event (in milliseconds) elapsed since the UNIX epoch. This field is optional and will be auto-populated with the current time if null. performance?: PerformanceMetrics; campaignStats?: CampaignStat[], } ``` ## The interaction Object The `interaction` object has the following structure. ```javascript { interaction: Interaction, ... } ``` ### Interaction You can use the `Interaction` field to capture user engagement data. The `Interaction` field has the following structure. ```javascript interface Interaction { name: string; } ``` ### CatalogObjectInteraction This type of `Interaction` is used to capture engagement data about catalog objects. ```javascript interface CatalogObjectInteraction extends Interaction { name: CatalogObjectInteractionName; catalogObject: CatalogObject; } interface CatalogObject { type: string; id: string; attributes?: { [key: string]: any }; relatedCatalogObjects?: { [catalogObjectType: string]: string[] }; } enum CatalogObjectInteractionName { ViewCatalogObject = 'View Catalog Object', ViewCatalogObjectDetail = 'View Catalog Object Detail', QuickViewCatalogObject = 'Quick View Catalog Object', ShareCatalogObject = 'Share Catalog Object', ReviewCatalogObject = 'Review Catalog Object', // This Interaction is not supported in Marketing Cloud Personalization CommentCatalogObject = 'Comment Catalog Object', FavoriteCatalogObject = 'Favorite Catalog Object' } ``` #### The location Object You can use the `location` object to send location data on a `catalogObject` via the Event API or using the Sitemap. The following is an example of how to structure a `catalogObject` to include location data. ```json "catalogObject": { "id": "test_product_one", "type": "Product", "attributes": { "location": { "longlat": [-71.1502415, 42.3571222], "city": "Boston", "stateProvinceCode": "MA", "postalCode": "02134", "countryNumericCode": "840" } } } ``` The following examples depict sending data for locations outside the US. ```json "catalogObject": { "id": "test_product_two", "type": "Product", "attributes": { "location": { "longlat": [144.963058, -37.813629], "city": "Melbourne", "stateProvinceCode": "VIC", "postalCode": "3005", "countryNumericCode": "036" } } } ``` ```json "catalogObject": { "id": "test_product_three", "type": "Product", "attributes": { "location": { "longlat": [139.691711, 35.689487], "city": "Tokyo", "stateProvinceCode": "13", "postalCode": "100-0004", "countryNumericCode": "392" } } } ``` ### CartInteraction This type of `Interaction` is used to capture engagement data about the contents of a user's cart. ```javascript enum CartInteractionName { AddToCart = 'Add To Cart', RemoveFromCart = 'Remove From Cart', ReplaceCart = 'Replace Cart' } interface CartInteraction extends Interaction { name: CartInteractionName; } interface AddToCartInteraction extends CartInteraction { name: CartInteractionName.AddToCart; lineItem: LineItem; } interface RemoveFromCartInteraction extends CartInteraction { name: CartInteractionName.RemoveFromCart; lineItem: LineItem; } interface ReplaceCartInteraction extends CartInteraction { name: CartInteractionName.ReplaceCart; lineItems: LineItem[]; } ``` **note:** At this time, the [Personalization Event Stream report](https://developer.salesforce.com/docs/marketing/personalization/web-integration/sitemap-event-validation.md#event-validation-using-the-event-stream-report) doesn't display `ReplaceCart` interactions. Instead, if you use `SalesforceInteractions.CartInteractionName.ReplaceCart`, the associated `ReplaceCart` interactions appear in the Event Stream report as "View Cart" interactions. ### OrderInteraction This type of `Interaction` is used to capture engagement data about the items in a user's order. Only the `PurchaseOrderInteraction` affects and updates a user's current order. ```javascript interface Order { id: string; lineItems?: LineItem[]; totalValue?: number; currency?: string; attributes?: { [key: string]: any }; } enum OrderInteractionName { Purchase = 'Purchase', Preorder = 'Preorder', Cancel = 'Cancel', Ship = 'Ship', Deliver = 'Deliver', Return = 'Return', Exchange = 'Exchange' } interface OrderInteraction extends Interaction { name: OrderInteractionName; order: Order; } interface PurchaseOrderInteraction extends OrderInteraction { name: OrderInteractionName.Purchase; order: Order; } interface PreorderOrderInteraction extends OrderInteraction { name: OrderInteractionName.Preorder; order: Order } interface CancelOrderInteraction extends OrderInteraction { name: OrderInteractionName.Cancel; order: Order } interface ShipOrderInteraction extends OrderInteraction { name: OrderInteractionName.Ship; order: Order } interface DeliverOrderInteraction extends OrderInteraction { name: OrderInteractionName.Deliver; order: Order } interface ReturnOrderInteraction extends OrderInteraction { name: OrderInteractionName.Return; order: Order } interface ExchangeOrderInteraction extends OrderInteraction { name: OrderInteractionName.Exchange; order: Order } ``` ### Line Item A `LineItem` represents a single item in a cart or transaction. A `LineItem` object has the following structure. ```javascript interface LineItem { catalogObjectType: string; catalogObjectId: string; quantity: number; price: number; currency?: string; attributes?: { [key: string]: any }; // Include sku under attributes } ``` The following table describes the properties that a `LineItem` object accepts. | Property | Value Type | Description | | ------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `catalogObjectType` | String | The type representing the catalog object. | | `catalogObjectId` | String | A unique ID representing the catalog object. | | `quantity` | Number | The number of catalog objects in the line item. | | `price` | Number | The price of the catalog object in the line item. **Important:** `price` is mandatory only when using `LineItem` in a purchase or cart interaction, and optional for other interactions. | | `currency` | String | Optional. Currency code of purchase. If currency is unspecified or `null`, it defaults to the dataset's configured currency. | | `attributes` | `{[key: string]: string \| number \| boolean }` | Optional. Key-value pairs that are stored as metadata on the `LineItem`. You can include `sku` within `attributes`. | The following is an example of a line item used within an `AddToCart` interaction. ```javascript // Example: Without SKU { interaction: { name: "Add To Cart", lineItem: { catalogObjectType: "Product", catalogObjectId: "product-1", quantity: 1, price: 9.99, currency: "USD", attributes: { giftWrapping: 1 } } } } // Example: With SKU { interaction: { name: "Add To Cart", lineItem: { catalogObjectType: "Product", quantity: 1, price: 9.99, currency: "USD", attributes: { sku: { id: "sku12345" } } } } } ``` **note:** At this time, the Personalization Event Stream doesn't display `price` for `AddToCart` events. ## The consent Object The `consent` field accepts an array of consent objects for the user. The following table describes the various properties available in the `consent` object. | Property | Value Type | Description | | ---------- | ---------- | ------------------------------------------------------------ | | `purpose` | String | The purpose for the consent. For example, `Personalization`. | | `provider` | String | The consent provider | | `status` | String | The consent status. For example, `OptIn` or `OptOut` | ## The debug Object The `debug` object contains fields that help investigate issues that could arise when developing campaigns. The following table describes the properties available in the `debug` object. | Property | Value Type | Description | | -------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `testMessages` | String | A comma-separated list of campaign experience IDs to be forced to return in the event, ignoring rules that otherwise prevent the campaign from returning. Alternatively, you can use the string value `true` to return all campaigns in testing mode but all rules are respected. | ## The flags Object The `flags` object contains properties that alter default event processing. By default, all flags are `false` if not present on the event. The following table describes the various properties available in the `flags` object. | Property | Value Type | Description | | ---------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `noCampaigns` | Boolean | If `true`, don’t return campaigns in the response. | | `nonInteractive` | Boolean | If `true`, a visit isn’t created (or updated) for the given user in the event. Additionally, no visit referrer nor originating referrer is created for the user. | ## Source The `Source` object contains properties that help describe where an event is coming from. This object has the following structure. ```javascript interface Source { channel?: string, application?: string, pageType?: string, url?: string, urlReferrer?: string, locale?: string, contentZones?: string, configVersion?: number, userAgent?: string, clientIp?: string, operatingSystem?: string // only applicable for mobile events operatingSystemVersion?: string // only applicable for mobile events device?: string // only applicable for mobile events surveyId?: string // set through the Survey Gear surveyStartTime?: string // set through the Survey Gear } ``` The following table describes the various properties available in the `Source` object. | Property | Value Type | Description | | ------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `channel` | String | The originating source of the event (For example, `Web`, `MobileApp`, `CallCenter`). | | `application` | String | The originating application level source of the event (For example, `ReactApp`, `3rdParty`, `ReactNative`) | | `pageType` | String | The type of page from which you’re sending the event (For example, `PDP`, `Blog`, `Pricing`). | | `url` | String | The URL of the page from which you’re sending the event. | | `urlReferrer` | String | The previous URL visited by the user. `urlReferrer` is populated from `document.referrer` by default on the web. It can be overwritten in the sitemap to any other value if necessary. | | `locale` | String | The locale of the current page, as defined by ISO 639 alpha-2 language codes and ISO 3166 alpha-2 country codes (For example, `en_US`, `de_DE`). | | `contentZones` | `string[]` | An array of content zones on the current page. | | `configVersion` | Number | Version number of the configuration for the SDK. | | `userAgent` | String | The user agent for the event. `userAgent` is populated automatically but can be overwritten if necessary | | `clientIp` | String | The IP Address sending the event. `clientIp` is populated automatically but can be overwritten if necessary | | `operatingSystem` | String | Only applicable for mobile events. | | `operatingSystemVersion` | String | Only applicable for mobile events. | | `device` | String | Only applicable for mobile events. | | `surveyId` | String | The ID of the survey being submitted. Set automatically for events sent through the Survey Gear | | `surveyStartTime` | String | The start time of the survey being submitted. Set automatically for events sent through the Survey Gear | ## The User Object The `User` object describes the user associated with an event. ```javascript interface User { anonymousId?: string; identities?: { [key: string]: string, }; attributes?: { [key: string]: string | number | boolean, }; profileObjects?: { [profileObjectType: string]: ProfileObject[], }; } ``` The following table describes the various properties available in the `user` object. | Property | Value Type | Description | | ---------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `anonymousId` | String | The ID of an anonymous user. | | `identities` | `[key: string]: string` | Key-value pairs that are stored as identity information of a user. | | `encryptedId` | String | The encrypted ID returned from an event that contains the ID field. Encrypted IDs are returned in the response to events that provide identities. | | `attributes` | `{ [key: string]: string \| number \| boolean }` | Key-value pairs that are stored as metadata on the user. These attributes must be defined in the platform. | | `profileObjects` | `{ [profileObjectType: string]: ProfileObject[] }` | Key-value pairs of profile objects that are stored as metadata on the user. Profile objects, their attributes, and related catalog objects must be defined in the platform. | ### Profile Object You can use the `profileObjects` field to send new or update existing profile objects via the Event API or using the Web SDK. You can send multiple profile objects of one type or multiple types in a single event. ```javascript interface ProfileObject { id: string; relatedCatalogObjects?: { [catalogObjectType: string]: string[] }; attributes?: { [key: string]: string | number | boolean }; } ``` **note:** To enable **Strict Profile Object Security**, log on to the Personalization UI and navigate to **Settings** > **Catalog and Profile Objects** > **User Profile Object Settings**. Enabling this setting restricts profile object updates to authenticated event sources such as ETL and Event API calls that use `channel: Server` and the `/authevent` endpoint. After it’s enabled, updates to profile objects from unauthenticated channels such as `Web` are ignored. **important:** - If an event causes a user to exceed 100 profile objects of a specific type, the 100 most recent objects are retained based on their creation date. - You can’t selectively update the fields of a profile object. You can only replace each instance of a profile object entirely. If you're updating an existing profile object, fields in the incoming event entirely replace the user's existing profile object fields. - You can't entirely replace a set of profile objects of a given type with another set or an empty set. For example, an event with `profileObjects: { Lease: [] }` doesn't clear out all `Leases` on that given user. - You can only create new or replace existing profile objects with Event API calls and not remove them. The following table describes the properties that a `ProfileObject` accepts. | Property | Value Type | Description | | ----------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `id` | String | The ID of a Profile Object. | | `relatedCatalogObjects` | `{ [catalogObjectType: string]: string[] }` | Related catalog objects key-value pairs. These related catalog objects must be defined in the platform. | | `attributes` | `{ [key: string]: string \| number \| boolean }` | Attribute key-value pairs that are stored as metadata on the profile object record. These attributes must be defined in the platform. | The following example depicts sending a single “Lease” profile object record with attributes and related catalog objects. ```json "profileObjects": { "Lease": [ { "id": "55X1V44", "relatedCatalogObjects": { "Color": ["red", "blue"], "Make": ["car company"] }, "attributes": { "duration": 365, "startDate": 1641502977863 } } ] } ``` ## The Account Object The `Account` object describes the account associated with an event. ```javascript interface Account { id?: string; attributes?: { [key: string]: string | number | boolean, }; } ``` The following table describes the various properties available in the `account` object. | Property | Value Type | Description | | ------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | | `id` | String | The ID of an account. | | `attributes` | `{ [key: string]: string \| number \| boolean }` | Key-value pairs that are stored as metadata on the account. These attributes must be defined in the platform. | ## The PerformanceMetrics Object The `PerformanceMetrics` object has properties that measure loading, parsing, and network performance. ```javascript interface Performance { sdkLoadTimeMs?: number; sdkParseTimeMs?: number; pageLoadTimeMs?: number; networkTimeMs?: number; } ``` The following table describes the various properties available in the `PerformanceMetrics` object. | Property | Value Type | Description | | ---------------- | ---------- | ------------------------------------------------------------------------------ | | `sdkLoadTimeMs` | Number | (Web SDK) Time, in milliseconds, for the network to load the web SDK. | | `sdkParseTimeMs` | Number | (Web SDK) Time, in milliseconds, for the beacon to be parsed during page load. | | `pageLoadTimeMs` | Number | (Web SDK) Time, in milliseconds, for the DOM to load. | | `networkTimeMs` | Number | (Web SDK) Time, in milliseconds, for the previous request to return. | ## The campaignStats Object You can use the `campaignStats` object to track campaign statistics for an associated event. For more information on sending campaign statistics on the web, refer to the [Campaign Stats Tracking](https://developer.salesforce.com/docs/marketing/personalization/campaign-development/templates/campaign-stats-tracking.md) documentation. The `campaignStats` object has the following structure. ```javascript { ... { campaignStats: CampaignStat[] } ... } ``` ### The CampaignStat Object The `CampaignStat` object has the following structure. ```javascript { experienceId: string, stat: "Impression" | "Click" | "Dismissal", control: boolean, catalog: { : string[] } } ``` The following table describes the properties a `CampaignStat` object accepts. | Property | Value Type | Description | | -------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `experienceId` | String | The experience ID of the campaign on which the given statistic is being tracked | | `stat` | `"Impression" \| "Click" \| "Dismissal"` | The type of statistic being tracked. | | `control` | Boolean | If `true`, the statistic is tracked for the control group of the given experience. | | `catalog` | `{ : string[] }` | A mapping of catalog item types to a list of corresponding item IDs on which to attribute the given statistic. | The following is an example of a mapping of catalog item types. ```javascript ... catalog: { Product: ["product1", "product2"] } ... ``` # Item Actions The `itemAction` property of the Salesforce Interactions SDK is used to identify actions users take on catalog items such as "Purchase" or "AddToCart". Item actions are a separate event property from regular actions (`action`). Item actions contain reserved action names used by Marketing Cloud Personalization to determine how to handle event data. Personalization automatically inserts the appropriate `itemAction` value when sending events containing catalog data originating from matching a `pageType` mapped in the Sitemap code. Therefore, avoid manually mapping the `itemAction` property in your `global` or `pageType` configurations, except in the following scenarios. When mapping pages with `cart` or `order`, you must specify `itemAction` in your `pageType` configuration. Typically the `cart` object is mapped only on the cart page itself with the `itemAction` value of "View Cart" (`Evergage.ItemAction.ViewCart`). Similarly, `order` is only mapped on the order confirmation page with the `itemAction` value of "Purchase" (`Evergage.ItemAction.Purchase`). If no `lineItem` is provided with the Purchase ItemAction, the SDK defaults to using the current state of the cart as line items. **important:** The Item Actions documented in this article are available exclusively in the `Evergage` namespace. In the `SalesforceInteractions` namespace, Interactions replace Item Actions. For more information, refer to the [Interaction Definitions](https://developer.salesforce.com/docs/marketing/personalization/guide/interaction-definitions.md) documentation. ## Actions The `action` property is also used to track user interactions but isn’t specific to actions carried out on catalog items. The purpose of `action` is to provide a high-level overview of user behavior and to help identify trends or patterns in how users interact with a website or application. For example, you can use actions to track when a user views the home page or clicks a call to action. You can also use actions for the following purposes: - Segmentation: You can use actions as criteria to create user or audience segments. You can then target these segments to deliver personalized experiences, offers, or content based on their actions. - Campaigns: By using actions for campaign targeting, you can qualify users who have either performed or not performed a specific set of actions. - Actions Report: The Actions Report provides insights into user actions and user behavior over time within a given dataset, allowing marketers to identify key activities that are valuable for campaign targeting. - Funnels: You can also use actions to track user progression through funnels. By mapping actions to each funnel step, you can track and analyze user behavior and identify steps where users drop off or encounter obstacles. Such insight can help optimize user journeys and improve conversion rates. ### Best Practices For Using Actions When using actions, it’s important to keep the following in mind: - Only track actions if they're necessary for personalization and targeting. Think about the personalization use cases you wish to execute and only map the actions required for those specific use cases. - Map no more than a few thousand unique actions per dataset. Mapping any more leads to challenges when using the Personalization UI, including difficulties in selecting actions from dropdown menus and slower report generation. - Ensure action names have values you can reuse across users and sessions. - When defining action names, avoid dynamically populating them to prevent inflating names with unexpected prefixes and suffixes. - If you dynamically populate action names, ensure they stay consistent across sessions for any user. - Refrain from automatically sending all events from other site analytics tools to Personalization, as it can result in a higher volume of events than necessary for personalization. - Incorporating dynamic user information in action names can cause issues and must be avoided whenever possible. ## Item Actions in the Event API The item action property value must be manually set only when sending events to the Personalization platform using the Event API. Only the reserved item action values within `Evergage.ItemAction` can be used. Within the context of the Sitemap, the Event API is used to send an event independently of the page matching event using `Evergage.sendEvent()`. An example of an event sent using the API within the Sitemap can be found in our [Example ecommerce Sitemap](https://developer.salesforce.com/docs/marketing/personalization/guide/example-sitemaps/ecommerce.md) within the `"product_detail"` pageType configuration. The following code sample includes the relevant code block. In this example, a listener has been mapped containing an `AddToCart` event that fires when the user clicks the DOM element referenced by the provided selector, `".add-to-cart"`. ```javascript listeners: [ Evergage.listener("click", ".add-to-cart", () => { const lineItem = Evergage.util.buildLineItemFromPageState("select[id*=quantity]"); lineItem.sku = { _id: Evergage.cashDom(".product-detail[data-pid]").attr("data-pid")}; Evergage.sendEvent({ itemAction: Evergage.ItemAction.AddToCart, cart: { singleLine: { Product: lineItem } } }); }), ], ``` ## Reserved Item Action Values The possible values for item action are all contained within the `Evergage.ItemAction` object. Be sure to reference all values assigned to the `itemAction` property from the `Evergage.ItemAction` object in the same way as the `AddToCart` event detailed in the previous section. ## Special Item Actions - `Quick View Item` - Similar to `View Item` but stops the timer for the "background" item and starts a timer for the "foreground" item. - `Stop Quick View Item` - Stops the timer initiated by the "Quick View Item" item action for the "foreground" item and restarts the timer for the "background" item. You can use the `Quick View Item` and `Stop Quick View Item` item actions to accurately track view time in cases where a user "views" an item while already viewing a different item. An ecommerce category page that allows users to view individual products in a window can take advantage of these item actions. In the following example, the "background" item is the category and the "foreground" item is the product. A new page load stops both timers. ```javascript config.pageTypes.push({ name: "Category", action: "View Category", isMatch: () => { return ( Evergage.cashDom(".page[data-action='Search-Show']").length > 0 && Evergage.cashDom(".breadcrumb").length > 0 ); }, catalog: { Category: { _id: Evergage.resolvers.buildCategoryId(".breadcrumb .breadcrumb-item a", 1), }, }, listeners: [ Evergage.listener("click", ".quickview", (e) => { const pid = Evergage.cashDom(e.target).attr("href").split("pid=")[1]; if (!pid) { return; } Evergage.sendEvent({ action: "Category Page Quick View", itemAction: Evergage.ItemAction.QuickViewItem, catalog: { Product: { _id: pid, }, }, }); }), Evergage.listener("click", "body", (e) => { if (Evergage.cashDom(e.target).closest("button[data-dismiss='modal']").length > 0) { Evergage.sendEvent({ action: "Close Quick View", itemAction: Evergage.ItemAction.StopQuickViewItem, }); } }), ], }); ``` ## Currently Supported Item Actions While other Item Actions are present in the `Evergage.ItemAction` object, only the Item Actions documented here are currently supported. Unless otherwise indicated, the following definitions apply to both the page config and Event API. Many item actions mentioned in the following definitions rely on events that contain line items. To learn more about the structure of the `lineItem` object, see the [LineItem](https://developer.salesforce.com/docs/marketing/personalization/guide/sitemap-implementation.md#lineitem) documentation. ### View Item ```javascript { itemAction: Evergage.ItemAction.ViewItem, catalog: { : { [key: string]: string | number | boolean | object } } } ``` ### View Item Detail ```javascript { itemAction: Evergage.ItemAction.ViewItemDetail, catalog: { : { [key: string]: string | number | boolean | object } } } ``` ### View Item Out Of Stock ```javascript { itemAction: Evergage.ItemAction.ViewItemOutOfStock, catalog: { Product: { [key: string]: string | number | boolean | object } } } ``` ### Quick View Item ```javascript { itemAction: Evergage.ItemAction.QuickViewItem, catalog: { : { [key: string]: string | number | boolean | object } } } ``` ### Stop Quick View Item ```javascript { itemAction: Evergage.ItemAction.StopQuickViewItem, catalog: { : { [key: string]: string | number | boolean | object } } } ``` ### View Category ```javascript { itemAction: Evergage.ItemAction.ViewCategory, catalog: { Category: { [key: string]: string | number | boolean | object } } } ``` ### Add To Cart When using this specific action, only one item is updated at a time. A single `AddToCart` event doesn’t accept an array of items. If you want to add multiple items to the cart, the items must be sent in multiple events. ```javascript { itemAction: Evergage.ItemAction.AddToCart, cart: { singleLine: { Product: LineItem } } } ``` ### View Cart A `viewCart` event fully updates the items that a user has in their cart. For example, sending a `viewCart` event via the Event API overwrites any items currently saved in the user's cart. **Sitemap PageType Definition** ```javascript { itemAction: Evergage.ItemAction.ViewCart, catalog: { Product: { lineItems: { _id: string[], price: number[], quantity: number[] } } } } ``` **Event API** ```javascript { itemAction: 'View Cart', cart: { complete: { Product: LineItem[] } } } ``` ### Purchase **Sitemap PageType Definition** ```javascript { itemAction: Evergage.ItemAction.Purchase, catalog: { Product: { orderId: string, totalValue ? : number, currency ? : string, lineItems: { _id: string[] | () => string[], price: number[] | () => number[], quantity: number[] | () => number[]. } } } } ``` **Event API** ```javascript { itemAction: 'Purchase', order: { Product: { orderId: string, totalValue?: number, currency?: string, lineItems: LineItem[] } } } ``` ### Share ```javascript { itemAction: Evergage.ItemAction.Share, catalog: { : { [key: string]: string | number | boolean | object } } } ``` ### Comment ```javascript { itemAction: Evergage.ItemAction.Comment, catalog: { : { [key: string]: string | number | boolean | object } } } ``` ### Favorite ```javascript { itemAction: Evergage.ItemAction.Favorite, catalog: { : { [key: string]: string | number | boolean | object } } } ``` ### Update Line Item When using this specific action, only one item is updated at a time. A single `UpdateLineItem` event doesn’t accept an array of items. If you want to update multiple cart items, you must send in multiple events. ```javascript { itemAction: Evergage.ItemAction.UpdateLineItem, cart: { singleLine: { Product: LineItem } } } ``` ### Remove From Cart The `RemoveFromCart` item action only removes one line item from the cart at a time. A single `RemoveFromCart` event doesn't accept an array of line items. To remove multiple cart items, you must send in multiple events. ```javascript { itemAction: Evergage.ItemAction.RemoveFromCart, cart: { singleLine: { Product: LineItem } } } ``` The following example depicts how you can use the `RemoveFromCart` item action to remove a line item from a shopping cart. ```javascript { "itemAction": "Evergage.ItemAction.RemoveFromCart", "cart": { "singleLine": { "Product": { "_id": "123", "price": 10.99, "quantity": 3 } } } } ``` # Manage CORS with the Salesforce Interactions SDK Launcher The Salesforce Interactions SDK Launcher is a Google Chrome extension that supports launching the Marketing Cloud Personalization Visual Editor on any domain. You can install this extension from the [Chrome Web Store](https://chromewebstore.google.com/detail/salesforce-interactions-s/mhmpepeohaddbhkhecaldflljggicedf). **warning:** If you have the legacy Evergage Visual Editor Chrome extension installed, you must **disable** it for the Salesforce Interactions SDK Launcher to work correctly. ## Required HTTP Header Configuration ### Content-Security-Policy - The value of the [`default-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src) directive must include `'https://cdn.evergage.com'`. - If you're using [`media-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/media-src), [`script-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src), [`img-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src), or [`style-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src) directives in place of [`default-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src), then each directive must include `'https://cdn.evergage.com'` in their values. - If the policy includes the [`frame-ancestors`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors) directive, the value must include `'self'`. (Generally, if this directive is in use, its value is set to `none`, which prevents the Visual Editor from framing in your page.) ### X-Frame-Options `X-Frame-Options` must be set to `SAMEORIGIN` which allows the page to be displayed within an `