Campaign Design API > Cross-domain visitor ID transfer

Overview

Problem description

The problem lies in maintaining consistent visitor identification information for Maxymiser platform in case of cross-domain user journey.

A good cross-domain journey example would be visitor's transfer from test.com to secure.com. Travelling between sub-domains (test.com and secure.test.com) does not lead to the issue described in this topic.

Cookies are visible on the host they were set to. So, whenever visitor travels from one domain to another inside one campaign, he or she receives new identification information on the domain he travels to. In result, this visitor also receives new generation on the CG, as all the visitor's history remained inside the identification cookie on the previous domain.

Solution

To maintain visitor identification data we have developed 3 different sets of mmapi.js extensions that save the data into a common place, so that the data can be then shared across domains.

Solution #1: window.name

We are getting more and more messages that window.name data transfer may be limited in the upcoming versions of the popular browsers. If you are not going to invest time into maintaining the applicability of the solution over browser releases, you may be better suited with the Solution #2.

This code snippet will work only in cases when the user journey does not use separate browser windows (tabs) on a cross-domain visitor transfer.

In case the journey is a one-end transfer (from domain #A to domain #B), cookies will not get in sync on the domain #A (i.e., will maintain the previous version of the cookie). So the experiences a visitor has been generated into on the domain #B, will not automatically be synced with domain #A.

Please insert the following source code (this is a one-time integration for the whole site) into mmapi.js extensions:

beforeInit: function( getParam, setParam ) {
/* Cross-domain data restore from window.name */
function restoreVisitorIdFromWindow() {
var key, crossDomainData;

if (window.JSON && window.JSON.stringify && window.JSON.parse) {
window.name = window.name.replace(/\|\*mm(.*)mm\*\|/, function(matchedString, capturedData) {
crossDomainData = JSON.parse(capturedData);
for (key in crossDomainData) {
if (crossDomainData.hasOwnProperty(key)) {
setParam(key, crossDomainData[key], 0);
}
}
return '';
});
}
}

restoreVisitorIdFromWindow();
},

afterResponse: function( getParam, setParam, genInfo ) {
/* Cross-domain data capture to window.name */
function captureVisitorIdToWindow(crossDomainParams) {
var i, cgParamName, cgParamValue,
crossDomainData = {},
hasCrossDomainParams = false;

if (window.JSON && window.JSON.stringify && window.JSON.parse) {
for (i = crossDomainParams.length; i--;) {
cgParamName = crossDomainParams[i];
cgParamValue = getParam(cgParamName, 0);

if (typeof cgParamValue === 'undefined' || cgParamValue === 'undefined') {
// nothing to save
} else {
hasCrossDomainParams = true;
crossDomainData[cgParamName] = cgParamValue;
}
}

if (hasCrossDomainParams) {
window.name = window.name.replace(/\|\*mm(.*)mm\*\|/, '') + ('|*mm' + JSON.stringify(crossDomainData) + 'mm*|');
}
}
}

captureVisitorIdToWindow(['pd', 'mmid', 'srv']);
},



Solution #2: iFrame

Note that this solution requires Maxymiser tag v.1.16 (with a "-secure" storageType setting value, applicable only to HTTPS sites), so that it works in line with the secure-by-default cookie policy rolled out with Chrome 80. More information on the tag's setup is available in the Setting up the tag article, where a short description for the new policy can be accessed via the "SameSite=Lax cookie attribute" left-hand side menu item.

This code snippet works asynchronously, which means that any kind of campaign or action tracking logic running on the 2nd domain would need to be wrapped into a handler that runs right after the visitor id has been synchronized.

In case the journey involves going back and forth between the two domains, data syncing should be done on both of them.

Step 1. Patch mmapi.js extensions

Please insert the following source code (this is a one-time integration for the whole site) into mmapi.js beforeInit extension:

// Query string parameter for the iframe - to identify if scripts run in the slave window // as there may be more than one nesting level and simple iframe check is not sufficient. var FRAME_QUERY = 'mmcrossdomainsolution=yzOaGH52SiHbe4M'; // The list of CD API storage parameters to transfer across domains. var PARAMS_TO_TRANSFER = ['pd', 'srv', 'mmid']; var iframeUrl; var callbacks = {}; var initCallback; var transport; var paramsToReceive; var initialized = false; /** * Parse hostname and get root domain. * @param {string} hostname - location hostname * @return {string | *} */ function getRootDomain(hostname) { return hostname.match(/^[\d.]+$|/)[0] || ((hostname.match(/[^.]+\.(\w{2,3}\.\w{2}|\w{2,})$/) || [hostname])[0]); } /** * Transfer CD API params specified in the 'PARAMS_TO_TRANSFER' list. */ function transferParams() { var i; for (i = 0; i < PARAMS_TO_TRANSFER.length; i += 1) { window.top.postMessage('mm-write-param&!&' + PARAMS_TO_TRANSFER[i] + '&!&' + getParam(PARAMS_TO_TRANSFER[i], 0), '*'); } }; /** * Insert an iframe. */ function insertIframe() { // Create dynamic source of an iframe. var url = iframeUrl.replace(/^(https?)?:?(\/\/)?/, ''); var concatenator = (url.indexOf('?') > -1) ? '&' : '?'; var frameSrc = '//' + url + concatenator + FRAME_QUERY; // Creating iframe element. var frameEl = document.createElement('iframe'); // Prevent iframe insertion when root domains are the same. if (getRootDomain(window.location.hostname) === getRootDomain(iframeUrl)) { return; } frameEl.setAttribute('src', frameSrc); frameEl.setAttribute('class', 'mm-cross-domain'); frameEl.setAttribute('style', 'opacity:0; position:absolute; top:-100000px; left:-100000px; width:0; height:0;'); // Add iframe to the page 'body'. document.body.appendChild(frameEl); }; /** * Mutation Observer based check waiting for the 'body' availability. * @param {function} callback */ function bodyReady(callback) { var observer = new MutationObserver(function(mutations) { var i; var j; i = mutations.length; while (i--) { j = mutations[i].addedNodes.length; while (j--) { if (( (mutations[i].addedNodes[j].nodeName === 'BODY') || document.body) // IE11 fix && !initialized // needed to avoid duplicate init ) { initialized = true; observer.disconnect(); callback(); } } } }); observer.observe(document.querySelector('html'), { childList: true, subtree: false, attributes: false, characterData: false, }); } /** * Slave iframe operations */ function iFrameOperations() { // Prevent page to continue loading. if (window.stop) { window.stop(); } else { document.execCommand('Stop'); } // Send params to the root window. transferParams(); // Setup post message listener data getter. window.addEventListener('message', function(event) { var paramList; var data; if ((typeof event.data === 'string') && (event.data.indexOf('mm-get-data') > -1)) { paramList = event.data.split('&!&'); data = localStorage.getItem(paramList[1]); window.top.postMessage('mm-write-data&!&' + paramList[2] + '&!&' + data, '*'); } }, false); // Cancels request to Content Generator. Testing becomes disabled for the current page. loader.disable(); } /** * Master window operations */ function masterOperations() { paramsToReceive = PARAMS_TO_TRANSFER.slice(0); bodyReady(function load() { // Create iframe. if (iframeUrl) { insertIframe(); } }); // Add post message listener for the root window. window.addEventListener('message', function(event) { var paramList; var index; if ((typeof event.data === 'string') && ((event.data.indexOf('mm-write-param') > -1) || (event.data.indexOf('mm-write-data') > -1) )) { paramList = event.data.split('&!&'); // Received params if (event.data.indexOf('mm-write-param') > -1) { if (!transport) { transport = event.source; } setParam(paramList[1], paramList[2], 0); // Remove param from the "expect" list index = paramsToReceive.indexOf(paramList[1]); if (index !== -1) { paramsToReceive.splice(index, 1); } // If all parameters set, notify configurating script if (paramsToReceive.length < 1) { initCallback(); } } // Received data if (event.data.indexOf('mm-write-data') > -1) { callbacks[paramList[1]](paramList[2]) delete callbacks[paramList[1]]; } } }, false); } /** * Set public methods */ function addPublicHooks() { /** * Initialization of cross domain solution. * It will open an iframe to pull cross-domain data from it. * @public * @param {string} address - url of the page to open in iframe */ function setIframe(url, callback) { // Validate domain object if (typeof url !== 'string') { return; } if (!iframeUrl) { initCallback = callback; iframeUrl = url; } else { // Notify developer that the config has been already specified - developer should resolve the conflict console.warn('CD-API: Tried to set another url for the cross-domain solution. It can use only one. Please resolve duplicated requests.') } }; /** * Transfer custom data between domains. * Data will be sent with the help of localStorage. * @public * @param {string} key - localStorage key * @param {string} value - localStorage value */ function getData(key, callback) { var id; if (iframeUrl) { if (!transport) { setTimeout(function() { getData(key, callback); }, 100); } else { do { id = ('000000' + Math.floor(Math.random() * 100000)).substr(-6); } while (callbacks[id]); if (callback) { callbacks[id] = callback; } transport.postMessage('mm-get-data&!&' + key + '&!&' + id, '*'); } } }; // Public object initialization. window.mmCrossDomain = { setIframe: setIframe, getData: getData }; } /** * Fallback for not supported browsers */ function addEmptyHooks() { window.mmCrossDomain = { setIframe: function() {}, getData: function() {}, }; } // Run logic in iframe. if (window.location.search.indexOf(FRAME_QUERY) !== -1) { iFrameOperations(); } else if (typeof MutationObserver === 'function') { // Main window masterOperations(); addPublicHooks(); } else { // the browser does not support MutationObserver - most likelly IE < 11 - degrade gracefully addEmptyHooks(); }

Step 2. Pull visitor id with help of a site/campaign script

This step is done individually for every cross-domain travel to manually pull the visitor information from the other domain.

Create a script on either site or campaign level in the Maxymiser UI with the following contents:

window.mmCrossDomain.setIframe('http_ref_to_a_page_on_the_other_domain.html', function() { // asynchronous callback, executed on the visitor id arrival });

Make sue you customize the arguments of the function call above as described below. Then map the script to the URL visitor will arrive to on domain B. The callback you pass as a second argument may track actions, request campaigns, etc.

And here is the method's syntax:

window.mmCrossDomain.setIframe( frameSrc, callback )

Makes a request to a HTML page to retrieve Maxymiser cookie data from it with the help of postMessage().

Parameters

Name Description Type
frameSrc Page URL used as a source for the visitor id import.

Note: It is recommended to use an empty page with only mmapi.js added to its source, so that the call doesn't affect the other page assets (e.g., tracking pixels).
String
callback Asynchronous callback run after cookie data is imported. Function

Code examples

window.mmCrossDomain.setIframe( 'https://test.com/blank.html', function() { actions.send( 'PurchaseConf', '1' ); } );