SFMC-Cookbook

How to survive as a developer for Salesforce Marketing Cloud

View the Project on GitHub

Einstein Recommendations

This page aims to make using Einstein recommendations a little easier by adding a few explanations around how to use it in a modern setup.

Collect Code

Official documentation: help.salesforce.com

The default sample codes all assume a page load is happening whenever something needs to be tracked. However, the collect.js library is actually capable of being loaded asynchronously and can be used in a single-page-application (SPA) environment as well, similarly to Google's analytics.js.

Note to developers

While the offical sample code snippets might lead you to believe that you are looking at an Array, that is in fact only half true. The variable _etmc gets rewritten by collect.js to become an object that happens to also expose a method named push(). Their base snippet in fact assumes that collect.js has been loaded synchronously before you execute your first call to push(). Essentially, the first Array-element is the name of an internal method of collect.js that gets executed.

For more details, have a look at the collect.js library itself.

Initialize the library

Collect Code on page load

Quoting from SFMC's documentation:

The Collect Code should be placed just before the closing head tag and before any other Marketing Cloud code.

To initialize the tracking, do not forget to replace the two occurences of "INSERT_MID" with your actual BU's Member ID.

<script src='//INSERT_MID.collect.igodigital.com/collect.js'></script>

This code should be placed directly below the </head> tag following the above tag.

<script>
_etmc.push(['setOrgId', 'INSERT_MID']);


// then execute tracking, e.g.:
// _etmc.push(['trackPageView']);
</script>

Download: Default sample code

Asynchronous Collect Code

The Collect Code tag should be added near the top of the <head> tag and before any other script or CSS tags.

To initialize the tracking, do not forget to replace the two occurences of "INSERT_MID" with your actual BU's Member ID.

<!-- Einstein Collect Code -->
<script>
(function(e,t,c,n,o,s,a){e[o]=e[o]||[],s=t.createElement(c),a=t.getElementsByTagName(c)[0],s.async=1,s.src=n,a.parentNode.insertBefore(s,a)})(window,document,'script','//INSERT_MID.collect.igodigital.com/collect.js','_etmc');

// always run this line once, followed by what you actually want to track; can be run programmatically in single-page-applications
_etmc.push(['setOrgId', 'INSERT_MID']);


// then execute tracking, e.g.:
// _etmc.push(['trackPageView']);
</script>
<!-- End Einstein Collect Code -->

Download: Async sample code

The above code does four main things:

  1. Creates a <script> element that starts asynchronously downloading the collect.js JavaScript library from https://INSERT_MID.collect.igodigital.com/collect.js
  2. Initializes a global _etmc function that allows you to schedule commands to be run once the collect.js library is loaded and ready to go.
  3. Adds a command to the _etmc command queue to set the Org ID to your Business Unit Member ID. This is required to ensure your tracking data ends up in the correct Business Unit.
  4. Adds another command to the _etmc command queue to track a pageview to Einstein for the current page.

Custom implementations may require modifying the last line of the above code snippet (the trackPageView command) or adding additional code to capture more interactions. However, you should not change the code that loads the collect.js library or initializes the _etmc.push() command queue function. Refer to the official docs for details on available options.

Asynchronous Collect Code with preloading

The above Collect Code snippet will ensure things work across all browsers but it has the disadvantage of not allowing modern browsers to preload the script.

The alternative async tag below adds support for preloading, which will provide a small performance boost on modern browsers, but can degrade to synchronous loading and execution on IE 9 and older mobile browsers that do not recognize the async script attribute. Only use this tag configuration if your visitors primarily use modern browsers to access your site.

To initialize the tracking, do not forget to replace the two occurences of "INSERT_MID" with your actual BU's Member ID.

<!-- Einstein Collect Code -->
<script>
window._etmc=window._etmc||[];

_etmc.push(['setOrgId', 'INSERT_MID']);


// then execute tracking, e.g.:
// _etmc.push(['trackPageView']);
</script>
<script async src='//INSERT_MID.collect.igodigital.com/collect.js'></script>
<!-- End Einstein Collect Code -->

Keep the above in the <head> section of your code.

Download: Async preload sample code

Debug your tracking solution

Use the following to activate console.log outputs whenever data is send to the server.

// enable debug output
_etmc.debug = true;

Alternatively, look for loaded images in the Network tab of your browser. The calls go to "files" (endpoints) with the name of your method calls, though rewritten to Snake Case.

Tracking and other Collect Code features

There are a lot of options available from the Collect Code library, available via the _etmc.push() method. Only options starting on "track" and "update" actually result in a callout to the server. The "set" and "doNotTrack" methods are mere settings that need to be executed before those.

Method Description
doNotTrack Deactivate tracking on the current page. More info below.
setFirstParty Allows you to send tracking data to a server other than the default, e.g. to proxy the data through your own server. Use together with one of the track... methods
setInsecure Use together with setFirstParty to track data via a non-secure proxy server. Only works if the current website was not opened securely either; use together with one of the track... methods
setOrgId set your BUs MID; use together with one of the track... methods
setUserInfo allows to send in an object with user data {email:'', custom:'abc'}; use together with one of the track... methods
trackCart Log items added or removed from a contact's cart
trackConversion Log details about a contact’s purchase
trackEvent Undocumented feature: Allows you to track custom events
trackPageView Log content/product page views, in-site search terms and category views. More info below
trackRating Log a user's rating for an item on your website.
trackWishlist Undocumented feature: Allows you to track multiple "shopping carts"-like lists in which users track future wishes
updateItem This allows you to update your product catalog. More info below.

Disable tracking

Contrary to the official docs, just a single line is needed, which then literally deactivates _etmc.push() and therefore any other tracking calls that are issued afterwards on the current page. This should be executed on page load before other push-calls.

// disable tracking for current page
_etmc.push(['doNotTrack']);

Identify Business Unit for Tracking

This is a required configuration step before tracking anything which allows the collect code to send the tracking data to the right business unit.

_etmc.push(['setOrgId','INSERT_MID']);

Identify current user

Contrary to what the official docs state, the only line required to define the user is this:

_etmc.push(['setUserInfo', {'email': 'INSERT_EMAIL_OR_UNIQUE_ID'}]);

// run a generic trackPageView once to set cookies that are necessary for personalized Web Recommendations to show up
_etmc.push(['trackPageView']);

According to a well hidden part of the documentation Einstein Engagement Scoring actually supports for INSERT_EMAIL_OR_UNIQUE_ID:

... as your subscriber identifier in Collect Tracking Code. Based on the source of collect.js, this should always be handed in as a value of 'email'.

Thinking about using Einstein in Journey Builder it makes sense to align with something that Einstein can actually understand and map to existing contacts in SFMC. However, there is the automatically created attribute group that links the PI_* Data Extensions to a Contact using the Email. Based on what I was able to find out, one should simply ignore that Attribute Group altogether.

On the other hand, if all you care about is showing Einstein powered recommendations, you simply have to ensure that you use the same string when you retrieve web/email recommendations that you previously used for tracking via collect code.

Attribute Affinity

This should theoretically boost catalog items that carry the same attribute (detail-field and value) defined as the current user.

Quote: Match a contact attribute to a tagged catalog field to increase the subscriber's affinity for the value of that contact attribute. The amount of increase is less than what results from a purchase but more than the increase from a view.

_etmc.push(['setUserInfo', {
    'email': 'INSERT_EMAIL_OR_UNIQUE_ID',
    'details': {
        'gender': 'female',
        'otherCustomAttribute', 'myValue'
    }
}]);

Track Page Views: trackPageView

The first element you pass in basically represents a method name which takes multiple variables. trackPageView accepts the following parameters alone or combined:

To track the view of a content or product, use this code:

Key Value
'item' Product code (String)
'search' Search term (String)
'category' Catgeory (String)

The most simple versions use one of the following lines

// product page viewed
_etmc.push(['trackPageView', { 'item': 'INSERT_PRODUCT_CODE' }]);

// category viewed
_etmc.push(['trackPageView', { 'category': 'INSERT_CATEGORY' }]);

// search executed
_etmc.push(['trackPageView', { 'search': 'INSERT_SEARCH_TERM' }]);

But of course these can also be combined: If the user came to the page using your search you can optionally use the following extended snippet:

_etmc.push(['trackPageView', { 'item': 'INSERT_PRODUCT_CODE','search': 'INSERT_SEARCH_TERM' }]);

Track Items in Cart: trackCart

This should be run each time a product is added or removed from the cart, the quantity is changed or when the purchase is finalized.

_etmc.push(['trackCart', {
    'cart': [
        {
            'item': 'INSERT_ITEM',
            'quantity':  'INSERT_QUANTITY',
            'price': 'INSERT_PRICE',
            'unique_id': 'INSERT_UNIQUE_ID'
        },
        {
            'item': 'INSERT_ITEM',
            'quantity':  'INSERT_QUANTITY' ,
            'price': 'INSERT_PRICE' ,
            'unique_id': 'INSERT_UNIQUE_ID'
        }
    ]
}]);

Definitions:

Key Definition
item Matches the field mapped to ProductCode in the catalog.
quantity The number of items added for the particular SKU.
price The price at the time of adding an item to the cart.
unique_id Matches the field mapped to the SKUId in the catalog. When these items match it ensures the exact record in the catalog, including all specific attributes like color and size, is tied to the cart rather than just the ProductCode

Important: Always include the entire cart in this call because it will overwrite whatever was stored before. Therefore, in order to remove one row, simply pass in all other rows that were not deleted.

The official docs state that one should use the following to remove all cart line items at once:

_etmc.push(['trackCart', { 'clear_cart': true } ]);

Track Purchases / Conversions: trackConversion

_etmc.push(['trackConversion', {
    'cart': [
        {
            'item': 'INSERT_ITEM',
            'quantity':  'INSERT_QUANTITY',
            'price': 'INSERT_PRICE',
            'unique_id': 'INSERT_UNIQUE_ID'
        },
        {
            'item': 'INSERT_ITEM',
            'quantity':  'INSERT_QUANTITY',
            'price': 'INSERT_PRICE',
            'unique_id': 'INSERT_UNIQUE_ID'
        }
    ],
   // OPTIONAL PARAMETERS
   'details': {
       'AttributeName': 'Value'
    }
   // END OPTIONAL PARAMETERS
}]);
Key Description
cart same as for trackCart; see above for more details
order_number mentioned in official docs but not actually supported
discount mentioned in official docs but not actually supported
shipping mentioned in official docs but not actually supported
details optional: Given the similar format this could have something to do with affinity attributes but the effect remains unclear TBC
Tracking overhead cost

Not actually working!

The official docs state that you can in fact track order_number, discount and shipping as separate fields and that those are then used to calculate the line item cost together with their respective overhead. While looking into collect.js however, those fields seem to get ignored when calling back to the server.

What to do instead: Simply calculate final prices on an order-line-item level before sharing it with the Collect Code. Shipping Cost have no bearing on the recommendation but if you have to track it, send it in as an order-line-item.

// non-working example from official docs
_etmc.push(['trackConversion', {
    'cart': [
        {
            'item': '123',
            'quantity': '2',
            'price': '10.00',
            'unique_id': '123'
        }
    ],
    // OPTIONAL PARAMETERS
    'order_number': '123456', // fact check: not supported by collect.js
    'discount': '2.00', // fact check: not supported by collect.js
    'shipping': '5.00' // fact check: not supported by collect.js
    // END OPTIONAL PARAMETERS
}]);

Track Custom Event: trackEvent

From what collect.js shows, only name and details are actually send to the server. The field details is optional!

_etmc.push(['trackEvent', {
    'name': 'INSERT_CUSTOM_EVENT_NAME',
    'details': {
        'gender': 'female',
        'otherCustomAttribute', 'myValue'
    }
}]);

This is untested code and hence further information will be added later once we've seen it in action.

Track User Wishlist: trackWishList

Allows you to store contact wishlists for items on your website.

_etmc.push(['trackWishlist', {
    'items': ['INSERT_ITEM_1', 'INSERT_ITEM_2', 'INSERT_ITEM_3'],
    'skus': ['INSERT_UNIQUE_ID_1', 'INSERT_UNIQUE_ID_2', 'INSERT_UNIQUE_ID_3']
}]);
Key Definition
item Matches the field mapped to ProductCode in the catalog.
unique_id Matches the field mapped to the SKUId in the catalog.

According to the offical docs, handing in skus is optional but I haven't tested that. Make sure the 2 arrays have the same length as the first item in one list is mapped to the first in the other; the second to the second; and so on and so forth.

The contact’s wishlist data is replaced by each subsequent trackWishlist call. Therefore, make sure to always include the entire current list and remove entries from the call that the user deleted from it.

Update Catalog

There are various ways of updating the catalog in Einstein that come with their own advantages and disadvantages.

Via Collect Code

Source: help.salesforce.com/articleView?id=mc_ctc_streaming_updates.htm

It is actually possible to send in updates to your catalog via the collect code. This approach runs without authentication and is therefore up for being hacked. Treat it with caution. With that said, you cannot deactivate it AFAIK.

Update a single item:

_etmc.push(['updateItem',
    {
    'item': 'INSERT_ITEM',
    'unique_id': 'INSERT_UNIQUE_ITEM_ID',
    'name': 'INSERT_ITEM_NAME_OR_TITLE',
    'url': 'INSERT_ITEM_URL',
    'item_type': 'INSERT_ITEM_TYPE',
    'INSERT_ATTRIBUTE_NAME': 'INSERT_ATTRIBUTE_VALUE'
    }
]);

Update multiple items:

_etmc.push(['updateItem', [
    {
        'item': 'INSERT_ITEM',
        'unique_id': 'INSERT_UNIQUE_ITEM_ID',
        'name': 'INSERT_ITEM_NAME_OR_TITLE',
        'url': 'INSERT_ITEM_URL',
        'item_type': 'INSERT_ITEM_TYPE',
        'INSERT_ATTRIBUTE_NAME': 'INSERT_ATTRIBUTE_VALUE'
    },
    {
        'item': 'INSERT_ITEM',
        'unique_id': 'INSERT_UNIQUE_ITEM_ID',
        'name': 'INSERT_ITEM_NAME_OR_TITLE',
        'url': 'INSERT_ITEM_URL',
        'item_type': 'INSERT_ITEM_TYPE',
        'INSERT_ATTRIBUTE_NAME': 'INSERT_ATTRIBUTE_VALUE'
    }
]]);

Update Catalog via API

Source: help.salesforce.com/articleView?id=mc_ctc_streaming_updates.htm

This is in theory possible, however so far I haven't gotten it to work.

Predictive Intelligence (PI) Data Extensions

PI_ABANDONED_CART_EVENT and PI_ABANDONED_CART_ITEMS will never have any data, according to a Partner webinar from Salesforce. They were "part of an earlier attempt" of the solution but are no longer in use - but nonetheless still created together with your BUs.

Einstein Email Recommendations

Official docs: help.salesforce.com/articleView?id=mc_pb_einstein_email_recommendations.htm TODO: Add more details :-)

Einstein Web Recommendations

Official docs: help.salesforce.com/articleView?id=mc_pb_einstein_web_recommendation.htm

To get recommendations, you have to create "pages" that should mirror the views/pages of your website. You can define per page what exactly shall be recommended and also choose between JSON and JavaScript format.

Embedding Web Recommendations

The JSON has to be retrieved via XHR callout and then parsed by your own code to actually create visible output in your HTML. The JavaScript approach requires you to load the JS file like any other JavaScript resource but also include pre-defined HTML code, provided to you by the "Get Code" tab.

Please note that you select the output format when creating a "page" in Einstein on the "ouput" tab. This will define what code is presented on the "Get Code" tab

selecting output format

You should define what field values you want in your recommendation via the "Output" tab. Here you can add, remove and order the fields that will be available in your recommendation. Ordering has no impact if you choose to embed via JSON.

How the recommender knows who the current user is

The current user can be identified in 2 ways. The first option uses the cookie set by the Collect Code (collect.js). This might lead to other challenges given the browser initiatives to block third-party cookies (see CORS) and explains what setFirstParty is for. You might have to implement a proxy logic to accomodate this. If no cookie was set, generic recommendations are displayed.

Important: Make sure you run one of the trackXXX methods from collect.js before trying to see personalized recommendations. The setXXX methods alone do not set these cookies.

The second option uses the GET parameter ?email=INSERT_EMAIL_OR_UNIQUE_ID (same you string you used for the Collect Code). Simply attach that to your recommend.js or recommend.json and you are good to go.

Example for JSON:

GET https://INSERT_MID.recs.igodigital.com/a/v2/INSERT_MID/INSERT_PAGE_NAME/recommend.json?category=My%20Shoes&email=joern@foobar.com

Enhancing recommendation results

You can append parameters to the recommend.json / recommend.js as URL parameters to get more focused results. Please make sure you URL-Encode the values! Depending on the page name, these parameters are even shown to you on the "Get Code" tab.

Page Name Default GET Parameter
category ?category=INSERT_CATEGORY_NAME
product ?item=INSERT_SKU
cart ?cart=INSERT_SKU1,INSERT_SKU2,INSERT_SKU3,...
(separate SKUs with ,)
search ?search=INSERT_SEARCH_TERM
home no parameter
custom name no parameter needed but optionally usable

List of available GET Parameters (docs):

Get Parameter Definition
item This unique identifier for your product or content must match the unique key in the catalog and the value sent in the trackPageView collect item variable. This parameter is required to make a product- or content-based recommendation on any page.
search This pipe-delimited list contains search terms from your search page and matches the value sent in the trackPageView collect search variable. Example: search=foo|bar
category This pipe-delimited list of categories matches both the values sent in the catalog feed and the value sent in the trackPageView collect category variable. Example: search=shoes|adults|men
cart This pipe-delimited list of products in the cart matches the trackCart collect item variable.
wishlist This pipe-delimited list of products in the customer's wishlist must match the trackWishlist collect items variable array.
email Use this parameter for faux-server-side, CloudPages, MobilePush, FaceBook tab, or some mobile apps. Pass the email address of the profile recommendation you want to access. The email value is the same value passed to Collect. If this value is a SubscriberKey, pass the SubscriberKey value.
user_id Not verified: This parameter is used in testing or an advanced setup. It returns the cookie value from the profile containing the recommendations you want to access.
item_count Use this parameter to set the number of returned products or override the returned products setting per area. If you have multiple areas, define the numbers pipe-delimited (exampe of 4 areas: item_count=1|1|1|1 ensures only one item is returned for each)
locale This five-character value (e.g. fr-FR, en-US) indicates which localized content to display. How to set up your Product Catalog for this.

You may also combine two or more GET parameters (with an & sign, the questionmark is only used up front):

Example for JSON:

GET https://INSERT_MID.recs.igodigital.com/a/v2/INSERT_MID/INSERT_PAGE_NAME/recommend.json?category=My%20Shoes&search=red%20female

Example for HTML/JavaScript:

<!-- Copy this code right before the closing </body> of your INSERT_PAGE_NAME page-->
<script src="https://INSERT_MID.recs.igodigital.com/a/v2/INSERT_MID/INSERT_PAGE_NAME/recommend.js?category=My%20Shoes&search=red%20female"></script>

Embed via JSON

This requires you to do all the styling and processing yourself but also comes with greatest amount of flexibility. Call the below URL to get the JSON for your BU-Page combo:

GET https://INSERT_MID.recs.igodigital.com/a/v2/INSERT_MID/INSERT_PAGE_NAME/recommend.json

Example:

// example for web recommendation on 'product' page with SKU=12345 for BU=67890
GET https://67890.recs.igodigital.com/a/v2/67890/product/recommend.json?item=12345

Handling CORS via JSONP - Cross-Origin-Ressource-Sharing the old way:

If, naturally, you assumed that the JSON would come with proper CORS headers allowing to use this anywhere or that could be configured in some way to be limited to your website then, well, tough luck: It does not and you can't. Someone even decided it's a good idea to set x-frame-options: SAMEORIGIN response headers...

However, you can tell the JSON-version of the API to return its payload as a parameter to the callback instead to circumvent the issue. Bit old-fashioned if you ask me but effective.

<script>
// define callback method; ensure it is defined in the GLOBAL scope!
window.myJsFunctionName = function(json) {
    // parse JSON response with your code here
}
</script>

Now, load the JavasScript-ified JSON. One way is to simply use another script tag:

<script src="https://INSERT_MID.recs.igodigital.com/a/v2/INSERT_MID/INSERT_PAGE_NAME/recommend.json?callback=myJsFunctionName"></script>

If your framework has a good wrapper for JSONP then feel free to use that instead of static script-nodes like shown below. The following example uses jQuery:

$.ajax({
    type: 'GET',
    url: 'https://INSERT_MID.recs.igodigital.com/a/v2/INSERT_MID/INSERT_PAGE_NAME/recommend.json',
    dataType: 'jsonp',
    jsonpCallback: 'myJsFunctionName',
    crossDomain: true
});

Either way, the content of recommend.json will be transformed to something like the following:

// the content of recommend.json will turn into javascript
myJsFunctionName([{"name": "igdrec_1","empty": true}]);

Important: The Web Recommendations' "Get Code" tab was apparently only written with the "html" / JavaScript embed code in mind and falsely asks you to load the JSON via script-tag. It then continues to also ask you to include certain HTML. You need to ignore that and simply copy the url out of that snippet instead!

The URL per page (without parameter value) is provided on the "Get Code" tab, however that page is misleading in other ways:

Bad embed code for JSON approach

JSON Example responses

The following assumes only one recommendation area was defined and the default name "igdrec_1" was kept. Furthermore, the list of included fields was defined as "ProductLink", "ImageLink", "ProductName", "RegularPrice".

As long as recommendations are not ready, the API will return ab empty:true attribute per defined recommendation area:

// no recommendation available yet
[
    {
        "name": "igdrec_1",
        "empty": true
    }
]

The reponse will look something like this once recommendations are actually available. Note that you define the area-name (e.g. by default "igdreg_x") and the number of items on the "Areas" tab. What scenarios are displayed first (if possible based on available data) is defined on the "Scenario" tab. If one or more scenarios are not possible to use yet, the system skips to the next one in your order or even falls back to "System Scenarios" (unless you specifically disabled that checkbox for the current page). Finally, the item-attributes (e.g. link, regular_price, ...) are defined for all areas of a page at once on the "Output" tab.

// (some) recommendation returned for a page with 3 defined areas

[ // list of areas, corresponding to scenario ordering
    {
        "name": "igdrec_1", // Name of Area #1
        "title": "Popular Items Today", // title of 1st available scenario
        "priority": 1,
        "items": [
            {
                "link": "Link",
                "image_link": "Image link",
                "name": "Name",
                "regular_price": 112.0
            },
            {
                "link": "Link",
                "image_link": "Image link",
                "name": "Name",
                "regular_price": 412.0
            }
        ]
    },
    {
        "name": "igdrec_2", // Name of Area #2
        "title": "Most Viewed Items", // title of 2nd available scenario
        "priority": 2,
        "items": [
            {
                "link": "Link",
                "image_link": "Image link",
                "name": "Name",
                "regular_price": 212.0
            },
            {
                "link": "Link",
                "image_link": "Image link",
                "name": "Name",
                "regular_price": 312.0
            }
        ]
    },
    {
        "name": "igdrec_3", // Name of Area #3
        "empty": true // no further recommendation available yet
    }
]

Embed via JavaScript ("HTML")

You will be asked to load a JavaScript file like this:

<!-- Copy this code right before the closing </body> of your INSERT_PAGE_NAME page-->
<script src='https://INSERT_MID.recs.igodigital.com/a/v2/INSERT_MID/INSERT_PAGE_NAME/recommend.js'></script>

As well as position some HTML where you want the final recommendation to be inserted like this:

<!-- Copy this code to where you want to show web recommendations on your INSERT_PAGE_NAME page-->
<div id='igdrec_1'></div>

This second part will vary depending on your choices made on the "Build">"Areas" tab.

JavaScript/HTML example code

As long as recommendations are not ready the code of recommend.js will be missing the crucial part that actually fills in the recommendation.

// recommend.js if recommendations are not ready yet and only one area named "idgrec_1" was defined for this page

function display_INSERT_PAGE_NAME(zone, id) {
    if (id === 'igdrec_1') {
          zone.innerHTML = '';
    }
}

function addLoadEvent(func) {
    var oldonload = window.onload;
    if (typeof window.onload != 'function') {
        window.onload = func;
    } else {
        window.onload = function() {
            if (oldonload) {
                oldonload();
            }
            func();
        }
    }
}

function callREC() {
    var pageZone = document.getElementById('igdrec_1');
    if ( undefined != pageZone) {
        display_INSERT_PAGE_NAME(pageZone, 'igdrec_1');
    }
}

callREC();

Note: the method display_INSERT_PAGE_NAME() will be named according to your page: If you named the page "MyTest" the method will be named display_MyTest().

If you defined more than one recommendation area, the code will be auto-extended to match that:

// recommend.js if recommendations are not ready yet and two areas named "idgrec_1" and "idgrec_2" were defined for this page

function display_INSERT_PAGE_NAME(zone, id) {
    if (id === 'igdrec_1') {
        zone.innerHTML = '';
    }
    if (id === 'igdrec_2') {
        zone.innerHTML = '';
    }
}

function addLoadEvent(func) {
    var oldonload = window.onload;
    if (typeof window.onload != 'function') {
        window.onload = func;
    } else {
        window.onload = function() {
            if (oldonload) {
                oldonload();
            }
            func();
        }
    }
}

function callREC() {
    var pageZone = document.getElementById('igdrec_1');
    if ( undefined != pageZone) {
        display_INSERT_PAGE_NAME(pageZone, 'igdrec_1');
    }
    var pageZone = document.getElementById('igdrec_2');
    if ( undefined != pageZone) {
        display_INSERT_PAGE_NAME(pageZone, 'igdrec_2');
    }
}

callREC();

The reponse will look something like this once recommendations are actually available. The only difference is that the zone.innerHTML-line now gets the actual HTML set, pre-rendered on the server without further callouts:

// recommend.js if recommendations are finally ready and only one area named "idgrec_1" was defined for this page with 2 items returned

function display_INSERT_PAGE_NAME(zone, id) {
    if (id === 'igdrec_1') {
        // NOTE: main difference is that zone.innerHTML actually gets a value
        zone.innerHTML = "  <div class='igo_boxhead'><h2>Most Viewed Items</h2></div>  <div class='igo_boxbody'><div class='igo_product'><a href='https://INSERT_MID.collect.igodigital.com/redirect/v3Q_SOME_BASE64_ENCODED_AND_ENRRYPTED_STRING_HERE_ZGYyNw=='>banana</a><a href='https://INSERT_MID.collect.igodigital.com/redirect/v3Qk_SOME_BASE64_ENCODED_AND_ENRRYPTED_STRING_HERE_ZGYyNw=='><img class='igo_product_image' src='https://your.own.image-server.com/2332264' /></a><div class='igo_product_product_name'><span class='igo_product_product_name_label'>Product Name:</span><span class='igo_product_product_name_value'>banana</span></div><div class='igo_product_regular_price'><span class='igo_product_regular_price_label'></span><span class='igo_product_regular_price_value'>$12.30</span></div></div><div class='igo_product last_rec'><a href='https://INSERT_MID.collect.igodigital.com/redirect/v3Q_SOME_BASE64_ENCODED_AND_ENRRYPTED_STRING_HERE_iYjNhOQ=='>banana</a><a href='https://INSERT_MID.collect.igodigital.com/redirect/v3Q_SOME_BASE64_ENCODED_AND_ENRRYPTED_STRING_HERE_iYjNhOQ=='><img class='igo_product_image' src='https://your.own.image-server.com/2324009' /></a><div class='igo_product_product_name'><span class='igo_product_product_name_label'>Product Name:</span><span class='igo_product_product_name_value'>banana</span></div><div class='igo_product_regular_price'><span class='igo_product_regular_price_label'></span><span class='igo_product_regular_price_value'>$12.30</span></div></div>  </div>";
    }
}

function addLoadEvent(func) {
    var oldonload = window.onload;
    if (typeof window.onload != 'function') {
        window.onload = func;
    } else {
        window.onload = function() {
            if (oldonload) {
                oldonload();
            }
            func();
        }
    }
}

function callREC() {
    var pageZone = document.getElementById('igdrec_1');
    if ( undefined != pageZone) {
        display_INSERT_PAGE_NAME(pageZone, 'igdrec_1');
    }
}

callREC();

The HTML that will be created for you will look something like the following:

<div class="igo_boxhead"><h2>Most Viewed Items</h2></div>
<div class="igo_boxbody">
    <div class="igo_product">
        <a
            href="https://INSERT_MID.collect.igodigital.com/redirect/v3Q_SOME_BASE64_ENCODED_AND_ENRRYPTED_STRING_HERE_ZGYyNw=="
            >banana</a
        ><a
            href="https://INSERT_MID.collect.igodigital.com/redirect/v3Qk_SOME_BASE64_ENCODED_AND_ENRRYPTED_STRING_HERE_ZGYyNw=="
            ><img class="igo_product_image" src="https://your.own.image-server.com/2332264"
        /></a>
        <div class="igo_product_product_name">
            <span class="igo_product_product_name_label">Product Name:</span
            ><span class="igo_product_product_name_value">banana</span>
        </div>
        <div class="igo_product_regular_price">
            <span class="igo_product_regular_price_label"></span
            ><span class="igo_product_regular_price_value">$12.30</span>
        </div>
    </div>
    <div class="igo_product last_rec">
        <a
            href="https://INSERT_MID.collect.igodigital.com/redirect/v3Q_SOME_BASE64_ENCODED_AND_ENRRYPTED_STRING_HERE_iYjNhOQ=="
            >banana</a
        ><a
            href="https://INSERT_MID.collect.igodigital.com/redirect/v3Q_SOME_BASE64_ENCODED_AND_ENRRYPTED_STRING_HERE_iYjNhOQ=="
            ><img class="igo_product_image" src="https://your.own.image-server.com/2324009"
        /></a>
        <div class="igo_product_product_name">
            <span class="igo_product_product_name_label">Product Name:</span
            ><span class="igo_product_product_name_value">banana</span>
        </div>
        <div class="igo_product_regular_price">
            <span class="igo_product_regular_price_label"></span
            ><span class="igo_product_regular_price_value">$12.30</span>
        </div>
    </div>
</div>

Debugging Web Recommendations

Once you have created a page and start seeing recommendations come in you might wonder why things are displayed the way they are. An easy way to dig deeper is to the recommend.js or recommend.json URL that the Get Code tab shows you and change the ending to recommend.explain. That shows you a whole lot more output all the sudden and lets you analyze whats happening.

Example call:

GET https://INSERT_MID.recs.igodigital.com/a/v2/INSERT_MID/INSERT_PAGE_NAME/recommend.explain

Example response:

{
    "scenarios": [
        {
            "name": "topenjoyed",
            "code": "Home_TopEnjoyed",
            "score": 990,
            "getmores": 1,
            "for_target": "OurMostBoughtProducts",
            "items": [
                "ABC-93710600001",
                "ABC-86594971",
                "ABC-94212000003"
            ],
            "filters": [],
            "status": "returned"
        },
        {
            "name": "topenjoyed",
            "code": "Home_TopEnjoyed",
            "score": 900,
            "getmores": 1,
            "for_target": "InSeason",
            "items": ["ABC-87847461", "ABC-87843491", "ABC-87847471"],
            "filters": [],
            "status": "returned"
        },
        {
            "name": "topenjoyed",
            "code": "Home_TopEnjoyed",
            "score": 800,
            "getmores": 1,
            "for_target": "NewProducts",
            "items": [
                "ABC-19025001",
                "ABC-87906121",
                "ABC-24690301"
            ],
            "filters": [],
            "status": "returned"
        },
        {
            "name": "topenjoyed",
            "code": "Home_TopEnjoyed",
            "score": 700,
            "getmores": 2,
            "for_target": "MostRelevantPromotedProducts",
            "items": [
                "ABC-35926501",
                "ABC-30965601",
                "ABC-22645201"
            ],
            "filters": [],
            "status": "returned"
        },
        {
            "name": "topsellers",
            "code": "Home_MostPopular",
            "score": 1000,
            "getmores": 2,
            "for_target": "OurMostBoughtProducts",
            "items": [],
            "filters": [],
            "status": "rejected"
        },
        {
            "name": "topsellers",
            "code": "Home_MostPopular",
            "score": 920,
            "getmores": 2,
            "for_target": "InSeason",
            "items": [],
            "filters": [],
            "status": "rejected"
        },
        {
            "name": "topviews",
            "code": "Home_MostViewed",
            "score": 910,
            "getmores": 2,
            "for_target": "InSeason",
            "items": [
                "ABC-89673861",
                "ABC-88148871",
                "ABC-99403401050"
            ],
            "filters": [],
            "status": "rejected"
        },
        {
            "name": "topsellers",
            "code": "Home_MostPopular",
            "score": 820,
            "getmores": 2,
            "for_target": "NewProducts",
            "items": [],
            "filters": [],
            "status": "rejected"
        },
        {
            "name": "topviews",
            "code": "Home_MostViewed",
            "score": 810,
            "getmores": 2,
            "for_target": "NewProducts",
            "items": [
                "ABC-89673861",
                "ABC-88148871",
                "ABC-99403401050"
            ],
            "filters": [],
            "status": "rejected"
        },
        {
            "name": "topsellers",
            "code": "Home_MostPopular",
            "score": 720,
            "getmores": 2,
            "for_target": "MostRelevantPromotedProducts",
            "items": [],
            "filters": [],
            "status": "rejected"
        },
        {
            "name": "topviews",
            "code": "Home_MostViewed",
            "score": 710,
            "getmores": 2,
            "for_target": "MostRelevantPromotedProducts",
            "items": [
                "ABC-89673861",
                "ABC-88148871",
                "ABC-99403401050"
            ],
            "filters": [],
            "status": "rejected"
        },
        {
            "name": "topgrossing",
            "code": "Home_TopGrossing",
            "score": 1,
            "getmores": 0,
            "for_target": null,
            "items": null,
            "filters": [],
            "status": "available"
        },
        {
            "name": "topviews",
            "code": "Home_MostViewed",
            "score": 1,
            "getmores": 0,
            "for_target": null,
            "items": null,
            "filters": [],
            "status": "available"
        },
        {
            "name": "topsellers",
            "code": "Home_MostPopular",
            "score": 1,
            "getmores": 0,
            "for_target": null,
            "items": null,
            "filters": [],
            "status": "available"
        }
    ],
    "filters": [
        {
            "type": "emphasize_tags",
            "params": [{}],
            "block_given?": false,
            "priority": 0,
            "waslocal": true,
            "excluded_skus": null
        },
        {
            "type": "exclude",
            "params": [{ "ct": ["asset", "content", "banner"] }],
            "block_given?": false,
            "priority": 0,
            "waslocal": true,
            "excluded_skus": []
        }
    ],
    "divs": [
        { "size": "1..16", "target": "OurMostBoughtProducts" },
        {
            "size": "1..16",
            "target": "InSeason",
            "filters": [
                {
                    "type": "include",
                    "params": [{ "st": "Christmas" }, null],
                    "block_given?": false,
                    "priority": 0,
                    "waslocal": true,
                    "excluded_skus": [
                        "ABC-94212300001",
                        "ABC-46054901",
                        "ABC-32529101",
                        "ABC-87906081",
                        "ABC-54878201"
                    ]
                }
            ]
        },
        {
            "size": "1..16",
            "target": "NewProducts",
            "filters": [
                {
                    "type": "include",
                    "params": [{ "ne": "Y" }, null],
                    "block_given?": false,
                    "priority": 0,
                    "waslocal": true,
                    "excluded_skus": [
                        "ABC-89673861",
                        "ABC-88148871",
                        "ABC-99403401050",
                        "ABC-87879261",
                        "ABC-87842451"
                    ]
                }
            ]
        },
        {
            "size": "1..16",
            "target": "MostRelevantPromotedProducts",
            "filters": [
                {
                    "type": "include",
                    "params": [{ "ioo": "Y" }, null],
                    "block_given?": false,
                    "priority": 0,
                    "waslocal": true,
                    "excluded_skus": [
                        "ABC-87842441",
                        "ABC-87884171",
                        "ABC-93710500001",
                        "ABC-87905821",
                        "ABC-87842491",
                        "ABC-87842451"
                    ]
                }
            ]
        }
    ],
    "errors": [],
    "weights": {},
    "page": "general",
    "referer": null
}

Embedding Collect Code via Google Tag Manager (GTM)

There are multiple ways of achieving an integration, but given that you are looking at a tag manager, you are likely including multiple trackers in your page. In this scenario, you will want to dive deep into Google's Ecommerce (GA4) Developer Guide. There is also the deprecated Enhanced Ecommerce (UA) Developer Guide - please disregard this document in favor of the newer "GA4" version.

Optional read: You may want to understand the Enhanced Ecommerce GA Developer Guide which describes how to enable the measurement of user interactions with products on ecommerce websites across the user's shopping experience in Google Analytics (GA). While you of course do not need to use GA together with Einstein, it does explain the underlying concepts.

While we are looking at prerequisites, please also pay attention to Google's definition of "triggers" and their defintion of tags.

For SFMC's Collect Code, you will need to understand Custom Tags.

Loading the GTM library

First off, let's make sure GTM is loaded on your website. You will need to go to Google Tag Manager and select your Account (1), or alternatively create a new Account (2).

Create or select Account

Either way, you end up with the Container ID (starting with "GTM-"). Now, we switch over to GTM's own Quick Start Guide which at the time of writing, asks you to complete the following 2 steps in your website:

  1. Copy the following JavaScript and paste it as close to the opening tag as possible on every page of your website, replacing GTM-XXXX with your container ID:
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXX');</script>
<!-- End Google Tag Manager -->
  1. Copy the following snippet and paste it immediately after the opening tag on every page of your website, replacing GTM-XXXX with your container ID:
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->

Loading collect.js via GTM

After selecting/creating a GTM account in the previous step, find "Tags" in the navigation and then click on Create by clicking on the upper tile, ignoring the lower "Triggering" tile.

Create new GTM Tag

The "Choose tag type" dialogue pops up. Scroll down until you find the Custom section and click on "Custom HTML":

Create Custom HTML Tag

Now you can configure all relevant details of ur library-loading tag:

Create Custom HTML Tag

  1. Change the name from "Untitled Tag" to "Collect.js loader" (or whatever suits you).
  2. Go fetch the code from Initialize the library > Asynchronous Collect Code section above. and paste it into the "HTML" textarea.

    Important: make sure you replace INSERT_MID with your BU's MID in 2 places in this snippet!

  3. click on "Advanced Settings" which pops up the below additional interface and ensure "Tag firing options" is set to "Once per page". Advanced Tag settings
  4. Ignore the "Triggering" section for this tag. We will load it through the actual events.

When you are done the tag should look like this:

Library loader tag

While saving the system might ask you to add a trigger - Do not add a trigger here but instead simply save the tag. We will ensure it's loaded later.

Library loader tag w/o trigger

GTM: Developer Playground

For early testing and especially if you don't have access to it on your SFMC instance just yet, you may replace the URL of the loader from '//1234567.collect.igodigital.com/collect.js' to 'https://joernberkefeld.github.io/SFMC-Cookbook/einstein/recommendation/collect-code/default/collect.js'. This probably won't actually log anything but you can check in your browser DevTools's network tab if the right type of callouts are made without actually having access to Marketing Cloud.

Logging events via GTM

With collect.js loading prepared, you may now start creating more custom tags, one for each event you want to be able to log for Einstein. Make sure you understood how to actually log events of all kinds via GTM and then simply hook up your new custom tags to those triggers.

Identify current user via GTM

In you website you want to trigger an event that is then caught by the triggers you specified in GTM for a certain tag. First, we need to define the variable that we want to log - unless it's one of the default ones of course. For our user login, let us create a custom Data Layer variable called userId:

This variable can then be used when writing into the data layer on the website and referenced in our Custom HTML Tag:

Website Code:

<script>
dataLayer.push({
  'userId': 'my.personal@email.com',
  'event': 'identifyUser'
});
</script>

Your Custom HTML Tag:

Please remove the 4 backslashes in "{{userid}}" below - those are there to work around a weird templating issue I have here in GitHub. Do compare the following code snippet with the below screenshot if your are unsure about how this should look.

<script>
_etmc.push(['setUserInfo', {'email': \{\{userId\}\} }]);

// run a generic trackPageView once to set cookies that are necessary for personalized Web Recommendations to show up
_etmc.push(['trackPageView']);
</script>

Putting that all together it will look something like this for the tag:

GTM "Identify user" Tag

And like this for the trigger that you will need to create as a "Custom Event", based on the event name you used in the website code. In this example that event is called identifyUser:

GTM "Identify User" Trigger

Now, finally, we need to ensure our collect.js library is actually loaded. That is done in the Advanced Settings of the Custom HTML Tag that we just created (named "Collect.js - Identify current user").

GTM Ensure that collect.js is loaded when event occurs

Ensure that for the option "Fire a tag before" you select the first tag we create earlier. That way, our library is really only loaded if events occur. And since it supports queueing events, loading it only now won't have a negative effect on whats loaded, nor on performance.

Debugging / Previewing your GTM setup

Make sure to go through Loading the GTM library for the page you intend to test this on (unless you are just updating a previous setup). If the GTM library is loading fine, you can now try the white Preview button in the top right corner. This will open up Tag Assistant which should ask you to provide the URL of your page. Once given, Tag Assistant will open a new window (or new tab) with that link and try to "connect". That way, whatever changes you made will be usable in a save environment while normal users continue to use the last released ("submitted") version.

Publishing your changes

When things look good, do make sure you actually publish your changes! You can do so by hitting the blue Submit button which should be in the top right corner of every GTM page. Unless you do this, none of your changes will be live!

Events mapping: Collect Code to GA4 Retail/Ecommerce

The most current list of GA Events can be found in the Analytics Help.

The following table aims to show how events are tracked in comparison to each other. Please note that SFMC can track additional events (marked with '-' below) using trackEvent method, however, this would not have an impact on Einstein Recommendations.

Also, Google's add_to_cart and remove_from_cart only take the items actually added/removed, SFMC's Collect code, however, requires you to use trackCart for both events and to pass in all items that remain in the cart after the event.

Events missing in GA:

Also don't forget about setOrgId that needs to run on page load and doNotTrack after login / as soon as we know.

SFMC Event GA4 Event Trigger GA Parameters
trackEvent add_payment_info when a user submits their payment information coupon, currency, items, payment_type, value
trackEvent add_shipping_info when a user submits their shipping information coupon, currency, items, shipping_tier, value
trackCart add_to_cart when a user adds items to cart currency, items, value
trackWishlist add_to_wishlist when a user adds items to a wishlist currency, items, value
trackEvent begin_checkout when a user begins checkout coupon, currency, items, value
trackEvent generate_lead when a user submits a form or request for information value, currency
trackConversion
trackCart.clear_cart
purchase when a user completes a purchase affiliation, coupon, currency, items, transaction_id, shipping, tax, value
trackEvent refund when a refund is issued affiliation, coupon, currency, items, transaction_id, shipping, tax, value
trackCart
trackCart.clear_cart
remove_from_cart when a user removes items from a cart currency, items, value
trackEvent select_item when an item is selected from a list items, item_list_name, item_list_id
trackEvent select_promotion when a user selects a promotion items, promotion_id, promotion_name, creative_name, creative_slot, location_id
trackPageView view_cart when a user views their cart currency, items, value
trackPageView.item view_item when a user views an item currency, items, value
trackPageView.category view_item_list when a user sees a list of items/offerings items, item_list_name, item_list_id
trackPageView view_promotion when a promotion is shown to a user items, promotion_id, promotion_name, creative_name, creative_slot, location_id
doNotTrack probably best to handle in application layer and avoid loading collect.js at all - -
setUserInfo see Identify current user via GTM above - -