blob: a3cbf734620c690cbb66b4c313c9c233376ea36a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Crawler} from "./crawler";
export const initializeTracker = () => {
window.wem = {
enableWem: () => {
wem._enableWem(true);
},
disableWem: () => {
wem._enableWem(false);
},
/**
* This function initialize the context in the page it is called internally and should not be called twice in the same page
*
* @param {string} contextServerUrl
* @param {string} proxyServletUrl
* @param {boolean} isPreview
* @param {number} timeoutInMilliseconds
* @param {string} dxUsername
*/
init: function () {
const {
contextServerUrl,
proxyServletUrl,
isPreview,
timeoutInMilliseconds,
dxUsername,
contextServerCookieName,
activateWem
} = window.digitalData.wemInitConfig;
wem.contextServerCookieName = contextServerCookieName;
wem.contextServerUrl = contextServerUrl;
wem.proxyServletUrl = proxyServletUrl;
wem.dxUsername = dxUsername;
wem.timeoutInMilliseconds = timeoutInMilliseconds;
wem.formNamesToWatch = [];
wem.eventsPrevented = [];
wem.sessionID = wem.getCookie('wem-session-id');
wem.fallback = false;
if (wem.sessionID === null) {
console.warn('[WEM] sessionID is null !');
} else if (!wem.sessionID || wem.sessionID === '') {
console.warn('[WEM] empty sessionID, setting to null !');
wem.sessionID = null;
}
if (isPreview) {
// do not execute fallback for preview!
return;
}
let cookieDisabled = !navigator.cookieEnabled;
let noSessionID = !wem.sessionID || wem.sessionID === '';
let crawlerDetected = navigator.userAgent;
if (crawlerDetected) {
const browserDetector = new Crawler();
crawlerDetected = browserDetector.isCrawler(navigator.userAgent);
}
if (cookieDisabled || noSessionID || crawlerDetected) {
document.addEventListener('DOMContentLoaded', function () {
wem._executeFallback('navigator cookie disabled: ' + cookieDisabled + ', no sessionID: ' + noSessionID + ', web crawler detected: ' + crawlerDetected);
});
return;
}
// this is the event to get form factory data when submitting
document.addEventListener('ffFormReady', wem._formFactorySubmitEventListener);
wem._registerCallback(function () {
if (cxs.profileId) {
wem.setCookie('wem-profile-id', cxs.profileId);
}
if (!cxs.profileId) {
wem.removeCookie('wem-profile-id');
}
// process tracked events
var videoNamesToWatch = [];
var clickToWatch = [];
if (cxs.trackedConditions && cxs.trackedConditions.length > 0) {
for (var i = 0; i < cxs.trackedConditions.length; i++) {
switch (cxs.trackedConditions[i].type) {
case 'formEventCondition':
if (cxs.trackedConditions[i].parameterValues && cxs.trackedConditions[i].parameterValues.formId) {
wem.formNamesToWatch.push(cxs.trackedConditions[i].parameterValues.formId);
}
break;
case 'videoViewEventCondition':
if (cxs.trackedConditions[i].parameterValues && cxs.trackedConditions[i].parameterValues.videoId) {
videoNamesToWatch.push(cxs.trackedConditions[i].parameterValues.videoId);
}
break;
case 'clickOnLinkEventCondition':
if (cxs.trackedConditions[i].parameterValues && cxs.trackedConditions[i].parameterValues.itemId) {
clickToWatch.push(cxs.trackedConditions[i].parameterValues.itemId);
}
break;
}
}
}
var forms = document.querySelectorAll('form');
for (var formIndex = 0; formIndex < forms.length; formIndex++) {
var form = forms.item(formIndex);
var formName = form.getAttribute('name') ? form.getAttribute('name') : form.getAttribute('id');
// test attribute data-form-id to not add a listener on FF form
if (formName && wem.formNamesToWatch.indexOf(formName) > -1 && form.getAttribute('data-form-id') == null) {
// add submit listener on form that we need to watch only
console.info('[WEM] watching form ' + formName);
form.addEventListener('submit', wem._formSubmitEventListener, true);
}
}
for (var videoIndex = 0; videoIndex < videoNamesToWatch.length; videoIndex++) {
var videoName = videoNamesToWatch[videoIndex];
var video = document.getElementById(videoName) || document.getElementById(wem._resolveId(videoName));
if (video) {
video.addEventListener('play', wem.sendVideoEvent);
video.addEventListener('ended', wem.sendVideoEvent);
console.info('[WEM] watching video ' + videoName);
} else {
console.warn('[WEM] unable to watch video ' + videoName + ', video not found in the page');
}
}
for (var clickIndex = 0; clickIndex < clickToWatch.length; clickIndex++) {
var clickIdName = clickToWatch[clickIndex];
var click = (document.getElementById(clickIdName) || document.getElementById(wem._resolveId(clickIdName)))
? (document.getElementById(clickIdName) || document.getElementById(wem._resolveId(clickIdName)))
: document.getElementsByName(clickIdName)[0];
if (click) {
click.addEventListener('click', wem.sendClickEvent);
console.info('[WEM] watching click ' + clickIdName);
} else {
console.warn('[WEM] unable to watch click ' + clickIdName + ', element not found in the page');
}
}
wem.checkProfileValidity();
});
// Load the context once document is ready
document.addEventListener('DOMContentLoaded', function () {
wem.DOMLoaded = true;
// enrich digital data considering extensions
wem._handleDigitalDataOverrides();
// complete already registered events
wem._checkUncompleteRegisteredEvents();
// Dispatch javascript events for the experience (perso/opti displayed from SSR, based on unomi events)
wem._dispatchJSExperienceDisplayedEvents();
// Some event may not need to be send to unomi, check for them and filter them out.
wem._filterUnomiEvents();
// Add referrer info into digitalData.page object.
wem._processReferrer();
// Build view event
const viewEvent = wem.buildEvent('view', wem.buildTargetPage(), wem.buildSource(window.digitalData.site.siteInfo.siteID, 'site'));
viewEvent.flattenedProperties = {};
// Add URLParameters
if (location.search) {
viewEvent.flattenedProperties['URLParameters'] = wem.convertUrlParametersToObj(location.search);
}
// Add interests
if (window.digitalData.interests) {
viewEvent.flattenedProperties['interests'] = window.digitalData.interests;
}
// Register the page view event, it's unshift because it should be the first event, this is just for logical purpose. (page view comes before perso displayed event for example)
wem._registerEvent(viewEvent, true);
if (activateWem || window.activateWem) {
wem.loadContext();
} else {
wem._executeFallback('wem is not activated on current page');
}
});
},
convertUrlParametersToObj: function (searchString) {
if (!searchString) {
return null;
}
return searchString
.replace(/^\?/, '') // Only trim off a single leading interrobang.
.split('&')
.reduce((result, next) => {
if (next === '') {
return result;
}
let pair = next.split('=');
let key = decodeURIComponent(pair[0]);
let value = typeof pair[1] !== 'undefined' && decodeURIComponent(pair[1]) || undefined;
if (Object.prototype.hasOwnProperty.call(result, key)) { // Check to see if this property has been met before.
if (Array.isArray(result[key])) { // Is it already an array?
result[key].push(value);
} else { // Make it an array.
result[key] = [result[key], value];
}
} else { // First time seen, just add it.
result[key] = value;
}
return result;
}, {}
);
},
/**
* This function will get the targets for the filter
*
* @returns {Array}
*/
getFilterTargets: function () {
var targets = [];
if (window.digitalData.filterCallback) {
for (var i = 0; i < window.digitalData.filterCallback.length; i++) {
var currentNodeFilters = window.digitalData.filterCallback[i].filter;
for (var j = 0; j < currentNodeFilters.filters.length; j++) {
var currentNodeFilter = currentNodeFilters.filters[j];
for (var k = 0; k < currentNodeFilter.appliesOn.length; k++) {
var applyOnEntry = currentNodeFilter.appliesOn[k];
targets.push(applyOnEntry);
}
}
}
}
return targets;
},
/**
* This function will register a personalization
*
* @param {object} personalization
* @param {object} variants
* @param {boolean} [ajax] Deprecated: Ajax rendering is not supported anymore
* @param {function} [resultCallback]
*/
registerPersonalizationObject: function (personalization, variants, ajax, resultCallback) {
var target = personalization.id;
wem._registerPersonalizationCallback(personalization, function (result) {
var successfulFilters = [];
for (var i = 0; i < result.length; i++) {
successfulFilters.push(variants[result[i]]);
}
var selectedFilter = null;
if (successfulFilters.length > 0) {
selectedFilter = successfulFilters[0];
var minPos = successfulFilters[0].position;
if (minPos >= 0) {
for (var j = 1; j < successfulFilters.length; j++) {
if (successfulFilters[j].position < minPos) {
selectedFilter = successfulFilters[j];
}
}
}
}
if (resultCallback) {
// execute callback
resultCallback(successfulFilters, selectedFilter);
} else {
if (selectedFilter) {
var targetFilters = document.getElementById(target).children;
for (var fIndex in targetFilters) {
var filter = targetFilters.item(fIndex);
if (filter) {
filter.style.display = (filter.id === selectedFilter.content) ? '' : 'none';
}
}
// we now add control group information to event if the user is in the control group.
if (wem._isInControlGroup(target)) {
console.info('[WEM] Profile is in control group for target: ' + target + ', adding to personalization event...');
selectedFilter.event.target.properties.inControlGroup = true;
if (selectedFilter.event.target.properties.variants) {
selectedFilter.event.target.properties.variants.forEach(variant => variant.inControlGroup = true);
}
}
// send event to unomi
wem.collectEvent(wem._completeEvent(selectedFilter.event), function () {
console.info('[WEM] Personalization event successfully collected.');
}, function () {
console.error('[WEM] Could not send personalization event.');
});
//Trigger variant display event for personalization
wem._dispatchJSExperienceDisplayedEvent(selectedFilter.event);
} else {
var elements = document.getElementById(target).children;
for (var eIndex in elements) {
var el = elements.item(eIndex);
el.style.display = 'none';
}
}
}
});
},
/**
* This function will anonymize the current profile
*
* @param {function} successCallback will be executed if case of success
* @param {function} errorCallback will be executed if case of error
*/
anonymizeProfile: function (successCallback, errorCallback) {
wem.ajax({
url: wem.proxyServletUrl + '/cxs/privacy/profiles/' + cxs.profileId + '/anonymize?scope=' + window.digitalData.scope,
type: 'POST',
async: true,
contentType: 'application/x-www-form-urlencoded',
dataType: 'application/json',
data: '',
success: successCallback,
error: errorCallback
});
},
/**
* This function will change the location of the current window to display the current profile information
*/
downloadMyProfile: function () {
window.location = wem.contextServerUrl + '/client/myprofile.text';
},
/**
* This function will toggle the private browsing functionality
*
* @param {function} successCallback will be executed if case of success
* @param {function} errorCallback will be executed if case of error
*/
togglePrivateBrowsing: function (successCallback, errorCallback) {
if (cxs.anonymousBrowsing) {
wem.ajax({
url: wem.proxyServletUrl + '/cxs/privacy/profiles/' + cxs.profileId + '/anonymousBrowsing?scope=' + window.digitalData.scope,
type: 'DELETE',
async: true,
contentType: 'application/x-www-form-urlencoded',
dataType: 'application/json',
success: successCallback,
error: errorCallback
});
} else {
wem.ajax({
url: wem.proxyServletUrl + '/cxs/privacy/profiles/' + cxs.profileId + '/anonymousBrowsing?anonymizePastBrowsing=true&scope=' + window.digitalData.scope,
type: 'POST',
async: true,
contentType: 'application/x-www-form-urlencoded',
dataType: 'application/json',
data: '',
success: successCallback,
error: errorCallback
});
}
},
/**
* This function will register an optimization test or A/B test
*
* @param {string} optimizationTestNodeId
* @param {string} goalId
* @param {string} containerId
* @param {object} variants
* @param {boolean} [ajax] Deprecated: Ajax rendering is not supported anymore
* @param {object} [variantsTraffic]
*/
registerOptimizationTest: function (optimizationTestNodeId, goalId, containerId, variants, ajax, variantsTraffic) {
// check persona panel forced variant
var selectedVariantId = wem.getUrlParameter('wemSelectedVariantId-' + optimizationTestNodeId);
// check already resolved variant stored in local
if (selectedVariantId === null) {
if (wem.storageAvailable('sessionStorage')) {
selectedVariantId = sessionStorage.getItem(optimizationTestNodeId);
} else {
selectedVariantId = wem.getCookie('selectedVariantId');
if (selectedVariantId != null && selectedVariantId === '') {
selectedVariantId = null;
}
}
}
// select random variant and call unomi
if (!(selectedVariantId && variants[selectedVariantId])) {
var keys = Object.keys(variants);
if (variantsTraffic) {
var rand = 100 * Math.random() << 0;
for (var nodeIdentifier in variantsTraffic) {
if ((rand -= variantsTraffic[nodeIdentifier]) < 0 && selectedVariantId == null) {
selectedVariantId = nodeIdentifier;
}
}
} else {
selectedVariantId = keys[keys.length * Math.random() << 0];
}
if (wem.storageAvailable('sessionStorage')) {
sessionStorage.setItem(optimizationTestNodeId, selectedVariantId);
} else {
wem.setCookie('selectedVariantId', selectedVariantId, 1);
}
// spread event to unomi
wem._registerEvent(wem._completeEvent(variants[selectedVariantId].event));
}
//Trigger variant display event for optimization
// (Wrapped in DOMContentLoaded because opti are resulted synchronously at page load, so we dispatch the JS even after page load, to be sure that listeners are ready)
window.addEventListener('DOMContentLoaded', () => {
wem._dispatchJSExperienceDisplayedEvent(variants[selectedVariantId].event);
});
if (selectedVariantId) {
// update persona panel selected variant
if (window.optimizedContentAreas && window.optimizedContentAreas[optimizationTestNodeId]) {
window.optimizedContentAreas[optimizationTestNodeId].selectedVariant = selectedVariantId;
}
// display the good variant
document.getElementById(variants[selectedVariantId].content).style.display = '';
}
},
/**
* @deprecated the variant JS event is now sent automatically from the unomi event.
*/
dispatchVariantJSEvent: function (variantData, experienceType) {
// do nothing
},
/**
* This function is used to load the current context in the page
*
* @param {boolean} [skipEvents=false] Should we send the events
* @param {boolean} [invalidate=false] Should we invalidate the current context
*/
loadContext: function (skipEvents, invalidate) {
if (wem.contextLoaded) {
console.log('Context already requested by', wem.contextLoaded);
return;
}
var jsonData = {
requiredProfileProperties: window.digitalData.wemInitConfig.requiredProfileProperties,
requiredSessionProperties: window.digitalData.wemInitConfig.requiredSessionProperties,
requireSegments: window.digitalData.wemInitConfig.requireSegments,
requireScores: window.digitalData.wemInitConfig.requireScores,
source: wem.buildSourcePage()
};
if (!skipEvents) {
jsonData.events = window.digitalData.events;
}
if (window.digitalData.personalizationCallback) {
jsonData.personalizations = window.digitalData.personalizationCallback.map(function (x) {
return x.personalization;
});
}
jsonData.sessionId = wem.sessionID;
var contextUrl = wem.contextServerUrl + '/context.json';
if (invalidate) {
contextUrl += '?invalidateSession=true&invalidateProfile=true';
}
wem.ajax({
url: contextUrl,
type: 'POST',
async: true,
contentType: 'text/plain;charset=UTF-8', // Use text/plain to avoid CORS preflight
jsonData: jsonData,
dataType: 'application/json',
invalidate: invalidate,
success: wem._onSuccess,
error: function () {
wem._executeFallback('error during context loading');
}
});
wem.contextLoaded = Error().stack;
console.info('[WEM] context loading...');
},
/**
* This function is used internally to load a modified context, for preview testing mainly
* and should not be used outside of this context unless you really know what you are doing.
*
* @param {string} personaId
* @param {object} personaOverrides
*/
loadPersonaContext: function (personaId, personaOverrides) {
var profileOverrides = {
itemId: personaId,
itemType: 'persona',
properties: personaOverrides.properties ? personaOverrides.properties : null,
segments: personaOverrides.segments ? personaOverrides.segments : null,
scores: personaOverrides.scores ? personaOverrides.scores : null
};
var jsonData = {
source: wem.buildSourcePage(),
requireSegments: true,
requiredProfileProperties: ['*'],
requiredSessionProperties: ['*'],
events: window.digitalData.events,
profileOverrides: profileOverrides,
sessionPropertiesOverrides: personaOverrides.sessionProperties
};
if (window.digitalData.personalizationCallback) {
jsonData.personalizations = window.digitalData.personalizationCallback.map(function (x) {
return x.personalization;
});
}
var url = wem.contextServerUrl + '/context.json';
if (personaId) {
url += '?personaId=' + personaId;
}
if (wem.sessionID) {
jsonData.sessionId = wem.sessionID;
}
wem.ajax({
url: url,
type: 'POST',
async: true,
contentType: 'text/plain;charset=UTF-8', // Use text/plain to avoid CORS preflight
jsonData: jsonData,
dataType: 'application/json',
success: wem._onSuccess,
error: function () {
wem._executeFallback('error during persona context loading');
}
});
console.info('[WEM] persona context loading...');
},
/**
* This function will send an event to Apache Unomi
* @param {object} event The event object to send, you can build it using wem.buildEvent(eventType, target, source)
* @param {function} successCallback will be executed in case of success
* @param {function} errorCallback will be executed in case of error
*/
collectEvent: function (event, successCallback, errorCallback) {
wem.collectEvents({events: [event]}, successCallback, errorCallback);
},
/**
* This function will send the events to Apache Unomi
*
* @param {object} events Javascript object { events: [event1, event2] }
* @param {function} successCallback will be executed in case of success
* @param {function} errorCallback will be executed in case of error
*/
collectEvents: function (events, successCallback, errorCallback) {
if (wem.fallback) {
// in case of fallback we dont want to collect any events
return;
}
events.sessionId = wem.sessionID ? wem.sessionID : '';
var data = JSON.stringify(events);
wem.ajax({
url: wem.contextServerUrl + '/eventcollector',
type: 'POST',
async: true,
contentType: 'text/plain;charset=UTF-8', // Use text/plain to avoid CORS preflight
data: data,
dataType: 'application/json',
success: successCallback,
error: errorCallback
});
},
/**
* This function will send an event of type form, but only if there is a mapping associate to the form, it should be used when submitting a form with AJAX/Javascript
*
* @param {object} form element get using document.getElementBy...
* @param {function} successCallback will be executed in case of success
* @param {function} errorCallback will be executed in case of error
*/
sendAjaxFormEvent: function (form, successCallback, errorCallback) {
var formName = form.getAttribute('name') ? form.getAttribute('name') : form.getAttribute('id');
if (formName && wem.formNamesToWatch.indexOf(formName) > -1) {
console.info('[WEM] catching form ' + formName);
var formEvent = wem.buildFormEvent(formName);
// merge form properties with event properties
formEvent.flattenedProperties = {
fields: wem._extractFormData(form)
};
wem.collectEvent(formEvent, successCallback, errorCallback);
} else {
console.info('[WEM] There is no associated form mapping with this form');
if (successCallback) {
successCallback();
}
}
},
/**
* This function will build an event of type click and send it to Apache Unomi
*
* @param {object} event javascript
* @param {function} [successCallback] will be executed if case of success
* @param {function} [errorCallback] will be executed if case of error
*/
sendClickEvent: function (event, successCallback, errorCallback) {
if (event.target.id || event.target.name) {
console.info('[WEM] Send click event');
var targetId = event.target.id ? event.target.id : event.target.name;
var clickEvent = wem.buildEvent('click',
wem.buildTarget(targetId, event.target.localName),
wem.buildSourcePage());
var eventIndex = wem.eventsPrevented.indexOf(targetId);
if (eventIndex !== -1) {
wem.eventsPrevented.splice(eventIndex, 0);
} else {
wem.eventsPrevented.push(targetId);
event.preventDefault();
var target = event.target;
wem.collectEvent(clickEvent, function (xhr) {
console.info('[WEM] Click event successfully collected.');
if (successCallback) {
successCallback(xhr);
} else {
target.click();
}
}, function (xhr) {
console.error('[WEM] Could not send click event.');
if (errorCallback) {
errorCallback(xhr);
} else {
target.click();
}
});
}
}
},
/**
* This function will build an event of type video and send it to Apache Unomi
*
* @param {object} event javascript
* @param {function} [successCallback] will be executed if case of success
* @param {function} [errorCallback] will be executed if case of error
*/
sendVideoEvent: function (event, successCallback, errorCallback) {
console.info('[WEM] catching video event');
var videoEvent = wem.buildEvent('video', wem.buildTarget(event.target.id, 'video', {action: event.type}), wem.buildSourcePage());
wem.collectEvent(videoEvent, function (xhr) {
console.info('[WEM] Video event successfully collected.');
if (successCallback) {
successCallback(xhr);
}
}, function (xhr) {
console.error('[WEM] Could not send video event.');
if (errorCallback) {
errorCallback(xhr);
}
});
},
/**
* This function will invalidate the Apache Unomi session and profile,
* by removing the associated cookies, set the loaded context to undefined
* and set the wem-session-id cookie with a newly generated ID
*/
invalidateSessionAndProfile: function () {
'use strict';
wem.sessionID = wem.generateGuid() + '-browser-generated';
wem.setCookie('wem-session-id', wem.sessionID, 1);
wem.removeCookie(wem.contextServerCookieName);
wem.removeCookie('wem-profile-id');
window.cxs = undefined;
},
/**
* This function will check if the currently logged user in DX is equal to the username stored in the Apache Unomi profile.
* If not we will invalidate both the session and the profile and reload the context. This is related
* to https://jira.jahia.org/browse/DMF-1468 and other reported issues about errors in profile switching,
*/
checkProfileValidity: function () {
'use strict';
if (window.cxs) {
if (cxs.profileProperties && cxs.profileProperties['j:nodename']) {
var cxsDXNodeName = cxs.profileProperties['j:nodename'];
if (cxsDXNodeName && wem.dxUsername !== 'guest' && cxsDXNodeName !== wem.dxUsername) {
console.warn('[WEM] Logged in DX with username (' + wem.dxUsername + ') does not correspond to Apache Unomi profile DX username (' + cxsDXNodeName + '). Invalidating session and profiles and reloading context.');
wem.invalidateSessionAndProfile();
wem._registerEvent(wem.buildEvent('invalidProfileForDXUser', wem.buildTarget(cxsDXNodeName + '-' + wem.dxUsername, 'invalidProfileError', {
unomiNodeName: cxsDXNodeName,
dxUsername: wem.dxUsername
})));
wem.loadContext();
}
}
}
},
/**
* This function return the basic structure for an event, it must be adapted to your need
*
* @param {string} eventType The name of your event
* @param {object} [target] The target object for your event can be build with wem.buildTarget(targetId, targetType, targetProperties)
* @param {object} [source] The source object for your event can be build with wem.buildSource(sourceId, sourceType, sourceProperties)
* @returns {{eventType: *, scope}}
*/
buildEvent: function (eventType, target, source) {
var event = {
eventType: eventType,
scope: window.digitalData.scope
};
if (target) {
event.target = target;
}
if (source) {
event.source = source;
}
return event;
},
/**
* This function return an event of type form
*
* @param {string} formName The HTML name of id of the form to use in the target of the event
* @returns {*|{eventType: *, scope, source: {scope, itemId: string, itemType: string, properties: {}}, target: {scope, itemId: string, itemType: string, properties: {}}}}
*/
buildFormEvent: function (formName) {
return wem.buildEvent('form', wem.buildTarget(formName, 'form'), wem.buildSourcePage());
},
/**
* This function return the source object for a source of type page
*
* @returns {*|{scope, itemId: *, itemType: *}}
*/
buildTargetPage: function () {
return wem.buildTarget(window.digitalData.page.pageInfo.pageID, 'page', window.digitalData.page);
},
/**
* This function return the source object for a source of type page
*
* @returns {*|{scope, itemId: *, itemType: *}}
*/
buildSourcePage: function () {
return wem.buildSource(window.digitalData.page.pageInfo.pageID, 'page', window.digitalData.page);
},
/**
* This function return the basic structure for the target of your event
*
* @param {string} targetId The ID of the target
* @param {string} targetType The type of the target
* @param {object} [targetProperties] The optional properties of the target
* @returns {{scope, itemId: *, itemType: *}}
*/
buildTarget: function (targetId, targetType, targetProperties) {
return wem._buildObject(targetId, targetType, targetProperties);
},
/**
* This function return the basic structure for the source of your event
*
* @param {string} sourceId The ID of the source
* @param {string} sourceType The type of the source
* @param {object} [sourceProperties] The optional properties of the source
* @returns {{scope, itemId: *, itemType: *}}
*/
buildSource: function (sourceId, sourceType, sourceProperties) {
return wem._buildObject(sourceId, sourceType, sourceProperties);
},
/*************************************/
/* Utility functions under this line */
/*************************************/
/**
* This is an utility function to set a cookie
*
* @param {string} cookieName name of the cookie
* @param {string} cookieValue value of the cookie
* @param {number} [expireDays] number of days to set the expire date
*/
setCookie: function (cookieName, cookieValue, expireDays) {
var expires = '';
if (expireDays) {
var d = new Date();
d.setTime(d.getTime() + (expireDays * 24 * 60 * 60 * 1000));
expires = '; expires=' + d.toUTCString();
}
document.cookie = cookieName + '=' + cookieValue + expires + '; path=/; SameSite=Strict';
},
/**
* This is an utility function to get a cookie
*
* @param {string} cookieName name of the cookie to get
* @returns {*} the value of the first cookie with the corresponding name or null if not found
*/
getCookie: function (cookieName) {
var name = cookieName + '=';
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return null;
},
/**
* This is an utility function to remove a cookie
*
* @param {string} cookieName the name of the cookie to rename
*/
removeCookie: function (cookieName) {
'use strict';
wem.setCookie(cookieName, '', -1);
},
/**
* This is an utility function to execute AJAX call
*
* @param {object} options
*/
ajax: function (options) {
var xhr = new XMLHttpRequest();
if ('withCredentials' in xhr) {
xhr.open(options.type, options.url, options.async);
xhr.withCredentials = true;
} else if (typeof XDomainRequest != 'undefined') {
/* global XDomainRequest */
xhr = new XDomainRequest();
xhr.open(options.type, options.url);
}
if (options.contentType) {
xhr.setRequestHeader('Content-Type', options.contentType);
}
if (options.dataType) {
xhr.setRequestHeader('Accept', options.dataType);
}
if (options.responseType) {
xhr.responseType = options.responseType;
}
var requestExecuted = false;
if (wem.timeoutInMilliseconds !== -1) {
setTimeout(function () {
if (!requestExecuted) {
console.error('[WEM] XML request timeout, url: ' + options.url);
requestExecuted = true;
if (options.error) {
options.error(xhr);
}
}
}, wem.timeoutInMilliseconds);
}
xhr.onreadystatechange = function () {
if (!requestExecuted) {
if (xhr.readyState === 4) {
if (xhr.status === 200 || xhr.status === 204 || xhr.status === 304) {
if (xhr.responseText != null) {
requestExecuted = true;
if (options.success) {
options.success(xhr);
}
}
} else {
requestExecuted = true;
if (options.error) {
options.error(xhr);
}
console.error('[WEM] XML request error: ' + xhr.statusText + ' (' + xhr.status + ')');
}
}
}
};
if (options.jsonData) {
xhr.send(JSON.stringify(options.jsonData));
} else if (options.data) {
xhr.send(options.data);
} else {
xhr.send();
}
},
/**
* This is an utility function to load HTML content from the URL into the container,
* The content of the container will be replaced by the loaded content
*
* @param {string} url to load the content
* @param {string} parentSelector HTML ID of the container
*/
loadContent: function (url, parentSelector) {
var xhr = new XMLHttpRequest();
var finished = false;
xhr.onabort = xhr.onerror = function xhrError() {
finished = true;
};
xhr.onreadystatechange = function xhrStateChange() {
if (xhr.readyState === 4 && !finished) {
finished = true;
try {
var fragment = xhr.responseXML;
var documentHead = document.getElementsByTagName('head')[0];
var fragmentHeadChildNodes = fragment.getElementsByTagName('head')[0].childNodes;
for (var cIndex = 0; cIndex < fragmentHeadChildNodes.length; cIndex++) {
var childNode = fragmentHeadChildNodes[cIndex];
// Some browser don't use upper case so let's make sure everything match
if (childNode.nodeName.toUpperCase() === 'LINK'
&& childNode.nodeType === 1) {
documentHead.appendChild(childNode);
} else if (childNode.nodeName.toUpperCase() === 'SCRIPT'
&& childNode.nodeType === 1
&& childNode.src) {
documentHead.appendChild(childNode);
// here we need to load the script asynchronously to ensure the fragment is working
// because they won't be loaded otherwise
wem._loadScript(childNode.src);
}
}
var parent = document.getElementById(parentSelector);
parent.innerHTML = fragment.getElementsByTagName('body')[0].innerHTML;
var scripts = parent.getElementsByTagName('script');
for (var sIndex = 0; sIndex < scripts.length; sIndex++) {
var script = scripts[sIndex];
if (script.src) {
// in case there is a script with src that is not in the head
wem._loadScript(script.src);
} else {
window['eval'].call(window, script.text || script.textContent || script.innerHTML || '');
}
}
} catch (e) {
console.error('[WEM] ' + e);
}
}
};
xhr.open('GET', url + '?includeJavascripts=true&mainResource=' + window.digitalData.page.pageInfo.pageID);
// The responseType must be set here after the open otherwise it won't work on IE and old version of Firefox
xhr.responseType = 'document';
xhr.send();
},
/**
* This is an utility function to extend a JS object
*
* @returns {{}}
*/
extend: function () {
// Variables
var extended = {};
var deep = false;
var i = 0;
var length = arguments.length;
// Check if a deep merge
if (Object.prototype.toString.call(arguments[0]) === '[object Boolean]') {
deep = arguments[0];
i++;
}
// Merge the object into the extended object
var merge = function (obj) {
for (var prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
// If deep merge and property is an object, merge properties
if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') {
extended[prop] = wem.extend(true, extended[prop], obj[prop]);
} else {
extended[prop] = obj[prop];
}
}
}
};
// Loop through each object and conduct a merge
for (; i < length; i++) {
var obj = arguments[i];
merge(obj);
}
return extended;
},
/**
* This is an utility function to generate a new UUID
*
* @returns {string}
*/
generateGuid: function () {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
},
/**
* This is an utility function to check if the local storage is available or not
* @param type
* @returns {boolean}
*/
storageAvailable: function (type) {
try {
var storage = window[type],
x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
return false;
}
},
dispatchJSEvent: function (name, canBubble, cancelable, detail) {
var event = document.createEvent('CustomEvent');
event.initCustomEvent(name, canBubble, cancelable, detail);
document.dispatchEvent(event);
},
/**
* This is an utility function to get current url parameter value
* @param name, the name of the parameter
* @returns {string}
*/
getUrlParameter: function (name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
var results = regex.exec(window.location.search);
return results === null ? null : decodeURIComponent(results[1].replace(/\+/g, ' '));
},
/*************************************/
/* Private functions under this line */
/*************************************/
_handleDigitalDataOverrides: function () {
if (window.digitalDataOverrides && window.digitalDataOverrides.length > 0) {
for (const digitalDataOverride of window.digitalDataOverrides) {
window.digitalData = wem._deepMergeObjects(digitalDataOverride, window.digitalData);
}
}
},
_checkUncompleteRegisteredEvents: function () {
if (window.digitalData && window.digitalData.events) {
for (const event of window.digitalData.events) {
wem._completeEvent(event);
}
}
},
_dispatchJSExperienceDisplayedEvents: () => {
if (window.digitalData && window.digitalData.events) {
for (const event of window.digitalData.events) {
if (event.eventType === 'optimizationTestEvent' || event.eventType === 'personalizationEvent') {
wem._dispatchJSExperienceDisplayedEvent(event);
}
}
}
},
_dispatchJSExperienceDisplayedEvent: experienceUnomiEvent => {
if (!wem.fallback &&
experienceUnomiEvent &&
experienceUnomiEvent.target &&
experienceUnomiEvent.target.properties &&
experienceUnomiEvent.target.properties.variants &&
experienceUnomiEvent.target.properties.variants.length > 0) {
let typeMapper = {
optimizationTestEvent: 'optimization',
personalizationEvent: 'personalization'
};
for (const variant of experienceUnomiEvent.target.properties.variants) {
let jsEventDetail = {
id: variant.id,
name: variant.systemName,
displayableName: variant.displayableName,
path: variant.path,
type: typeMapper[experienceUnomiEvent.eventType],
variantType: experienceUnomiEvent.target.properties.type,
tags: variant.tags,
nodeType: variant.nodeType,
wrapper: {
id: experienceUnomiEvent.target.itemId,
name: experienceUnomiEvent.target.properties.systemName,
displayableName: experienceUnomiEvent.target.properties.displayableName,
path: experienceUnomiEvent.target.properties.path,
tags: experienceUnomiEvent.target.properties.tags,
nodeType: experienceUnomiEvent.target.properties.nodeType
}
};
wem.dispatchJSEvent('displayWemVariant', false, false, jsEventDetail);
}
}
},
_filterUnomiEvents: () => {
if (window.digitalData && window.digitalData.events) {
window.digitalData.events = window.digitalData.events
.filter(event => !event.properties || !event.properties.doNotSendToUnomi)
.map(event => {
if (event.properties) {
delete event.properties.doNotSendToUnomi;
}
return event;
});
}
},
_completeEvent: function (event) {
if (!event.source) {
event.source = wem.buildSourcePage();
}
if (!event.scope) {
event.scope = window.digitalData.scope;
}
if (event.target && !event.target.scope) {
event.target.scope = window.digitalData.scope;
}
return event;
},
_registerEvent: function (event, unshift) {
if (window.digitalData) {
if (window.cxs) {
console.error('[WEM] already loaded, too late...');
return;
}
} else {
window.digitalData = {};
}
window.digitalData.events = window.digitalData.events || [];
if (unshift) {
window.digitalData.events.unshift(event);
} else {
window.digitalData.events.push(event);
}
},
_registerCallback: function (onLoadCallback) {
if (window.digitalData) {
if (window.cxs) {
console.info('[WEM] digitalData object loaded, calling on load callback immediately and registering update callback...');
if (onLoadCallback) {
onLoadCallback(window.digitalData);
}
} else {
console.info('[WEM] digitalData object present but not loaded, registering load callback...');
if (onLoadCallback) {
window.digitalData.loadCallbacks = window.digitalData.loadCallbacks || [];
window.digitalData.loadCallbacks.push(onLoadCallback);
}
}
} else {
console.info('[WEM] No digital data object found, creating and registering update callback...');
window.digitalData = {};
if (onLoadCallback) {
window.digitalData.loadCallbacks = [];
window.digitalData.loadCallbacks.push(onLoadCallback);
}
}
},
_registerPersonalizationCallback: function (personalization, callback) {
if (window.digitalData) {
if (window.cxs) {
console.error('[WEM] already loaded, too late...');
} else {
console.info('[WEM] digitalData object present but not loaded, registering sort callback...');
window.digitalData.personalizationCallback = window.digitalData.personalizationCallback || [];
window.digitalData.personalizationCallback.push({personalization: personalization, callback: callback});
}
} else {
window.digitalData = {};
window.digitalData.personalizationCallback = window.digitalData.personalizationCallback || [];
window.digitalData.personalizationCallback.push({personalization: personalization, callback: callback});
}
},
_buildObject: function (itemId, itemType, properties) {
var object = {
scope: window.digitalData.scope,
itemId: itemId,
itemType: itemType
};
if (properties) {
object.properties = properties;
}
return object;
},
_onSuccess: function (xhr) {
window.cxs = JSON.parse(xhr.responseText);
if (window.digitalData.loadCallbacks && window.digitalData.loadCallbacks.length > 0) {
console.info('[WEM] Found context server load callbacks, calling now...');
if (window.digitalData.loadCallbacks) {
for (var i = 0; i < window.digitalData.loadCallbacks.length; i++) {
window.digitalData.loadCallbacks[i](digitalData);
}
}
if (window.digitalData.personalizationCallback) {
for (var j = 0; j < window.digitalData.personalizationCallback.length; j++) {
window.digitalData.personalizationCallback[j].callback(cxs.personalizations[window.digitalData.personalizationCallback[j].personalization.id]);
}
}
}
// Put a marker to be able to know when wem is full loaded, context is loaded, and callbacks have been executed.
window.wemLoaded = true;
},
_executeFallback: function (logMessage) {
console.warn('[WEM] execute fallback' + (logMessage ? (': ' + logMessage) : ''));
wem.fallback = true;
window.cxs = {};
for (var index in window.digitalData.loadCallbacks) {
window.digitalData.loadCallbacks[index]();
}
if (window.digitalData.personalizationCallback) {
for (var i = 0; i < window.digitalData.personalizationCallback.length; i++) {
window.digitalData.personalizationCallback[i].callback([window.digitalData.personalizationCallback[i].personalization.strategyOptions.fallback]);
}
}
},
_processReferrer: function () {
var referrerURL = digitalData.page.pageInfo.referringURL || document.referrer;
var sameDomainReferrer = false;
if (referrerURL) {
// parse referrer URL
var referrer = new URL(referrerURL);
// Set sameDomainReferrer property
sameDomainReferrer = referrer.host === window.location.host;
// only process referrer if it's not coming from the same site as the current page
if (!sameDomainReferrer) {
// get search element if it exists and extract search query if available
var search = referrer.search;
var query = undefined;
if (search && search != '') {
// parse parameters
var queryParams = [], param;
var queryParamPairs = search.slice(1).split('&');
for (var i = 0; i < queryParamPairs.length; i++) {
param = queryParamPairs[i].split('=');
queryParams.push(param[0]);
queryParams[param[0]] = param[1];
}
// try to extract query: q is Google-like (most search engines), p is Yahoo
query = queryParams.q || queryParams.p;
query = decodeURIComponent(query).replace(/\+/g, ' ');
}
// register referrer event
// Create deep copy of window.digitalData.page and add data to pageInfo sub object
if (window.digitalData && window.digitalData.page && window.digitalData.page.pageInfo) {
window.digitalData.page.pageInfo.referrerHost = referrer.host;
window.digitalData.page.pageInfo.referrerQuery = query;
}
}
}
window.digitalData.page.pageInfo.sameDomainReferrer = sameDomainReferrer;
},
_formFactorySubmitEventListener: function (event) {
console.info('[WEM] Registring Form Factory event callback');
window.ffCallbacks.registerCallback(event.detail, function (formData, formInfo) {
if (wem.formNamesToWatch.indexOf(formInfo.formName) > -1) {
console.info('[WEM] catching FF form ' + formInfo.formName);
var formEvent = wem.buildFormEvent(formInfo.formName);
formEvent.flattenedProperties = {
fields: transformer(formData.resultData)
};
var events = [];
events.push(formEvent);
if (window.consentCallbacks) {
// create array to keep track of consent callback that have been executed to avoid duplication
var executed = [];
for (var cIndex in window.consentCallbacks) {
var consentCallback = window.consentCallbacks[cIndex];
var consentIdentifier = consentCallback.formName + '_' + consentCallback.inputName;
// check form name of consent type to make sure we resolve current form consent only
if (consentCallback.formName === formInfo.formName && executed.indexOf(consentIdentifier) === -1) {
executed.push(consentIdentifier);
var consentTypeEvent = consentCallback.getConsentTypeEvent(formData, formInfo,
consentCallback.inputName, consentCallback.consentTypeId);
events.push(consentTypeEvent);
}
}
}
wem.collectEvents({events: events},
function () {
console.info('[WEM] Form Factory event successfully submitted.');
formInfo.notifyFormFactoryOfCompletion('MFcallback');
},
function (xhr) {
console.error('[WEM] Error while collecting Form Factory event, XHR status: ' + xhr.status + ' ' + xhr.statusText);
xhr.abort();
formInfo.notifyFormFactoryOfCompletion('MFcallback');
}
);
}
function transformer(data) {
for (var key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
var obj = data[key];
if (obj.rendererName && obj.rendererName === 'country') {
// Grab country name only!!!
data[key] = obj.country.key;
}
if (obj.rendererName && obj.value) {
// Generic object with value key (rating etc.)
data[key] = obj.value;
}
}
}
return data;
}
});
},
_formSubmitEventListener: function (event) {
console.info('[WEM] Registering form event callback');
var form = event.target;
var formName = form.getAttribute('name') ? form.getAttribute('name') : form.getAttribute('id');
if (formName && wem.formNamesToWatch.indexOf(formName) > -1) {
console.info('[WEM] catching form ' + formName);
var eventCopy = document.createEvent('Event');
// Define that the event name is 'build'.
eventCopy.initEvent('submit', event.bubbles, event.cancelable);
event.stopImmediatePropagation();
event.preventDefault();
var formEvent = wem.buildFormEvent(formName);
// merge form properties with event properties
formEvent.flattenedProperties = {
fields: wem._extractFormData(form)
};
wem.collectEvent(formEvent,
function () {
form.removeEventListener('submit', wem._formSubmitEventListener, true);
form.dispatchEvent(eventCopy);
if (!eventCopy.defaultPrevented && !eventCopy.cancelBubble) {
form.submit();
}
form.addEventListener('submit', wem._formSubmitEventListener, true);
},
function (xhr) {
console.error('[WEM] Error while collecting form event: ' + xhr.status + ' ' + xhr.statusText);
xhr.abort();
form.removeEventListener('submit', wem._formSubmitEventListener, true);
form.dispatchEvent(eventCopy);
if (!eventCopy.defaultPrevented && !eventCopy.cancelBubble) {
form.submit();
}
form.addEventListener('submit', wem._formSubmitEventListener, true);
}
);
}
},
_extractFormData: function (form) {
var params = {};
for (var i = 0; i < form.elements.length; i++) {
var e = form.elements[i];
// ignore empty and undefined key (e.name)
if (e.name) {
switch (e.nodeName) {
case 'TEXTAREA':
case 'INPUT':
switch (e.type) {
case 'checkbox':
var checkboxes = document.querySelectorAll('input[name="' + e.name + '"]');
if (checkboxes.length > 1) {
if (!params[e.name]) {
params[e.name] = [];
}
if (e.checked) {
params[e.name].push(e.value);
}
}
break;
case 'radio':
if (e.checked) {
params[e.name] = e.value;
}
break;
default:
if (!e.value || e.value == '') {
// ignore element if no value is provided
break;
}
params[e.name] = e.value;
}
break;
case 'SELECT':
if (e.options && e.options[e.selectedIndex]) {
if (e.multiple) {
params[e.name] = [];
for (var j = 0; j < e.options.length; j++) {
if (e.options[j].selected) {
params[e.name].push(e.options[j].value);
}
}
} else {
params[e.name] = e.options[e.selectedIndex].value;
}
}
break;
}
}
}
return params;
},
_createElementFromHTML: function (htmlString) {
var div = document.createElement('div');
div.innerHTML = htmlString.trim();
return div;
},
_loadScript: function (url) {
wem.ajax({
url: url,
type: 'GET',
async: false,
dataType: 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01',
success: function (response) {
// We need to call eval like this otherwise the object won't be link to the window
window['eval'].call(window, response.responseText);
}
});
},
_resolveId: function (id) {
var source = Object.keys(digitalData.sourceLocalIdentifierMap).filter(function (source) {
return id.indexOf(source) > 0;
});
return source ? id.replace(source, digitalData.sourceLocalIdentifierMap[source]) : id;
},
_enableWem: enable => {
// display fallback if wem is not enable
wem.fallback = !enable;
// remove cookies, reset cxs
if (!enable) {
wem.cxs = {};
document.cookie = 'wem-profile-id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
document.cookie = 'context-profile-id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
delete wem.contextLoaded;
} else {
if (wem.DOMLoaded) {
wem.loadContext();
} else {
// As Dom loaded listener not triggered, enable global value.
window.activateWem = true;
}
}
wem.ajax({
url: `${window.digitalData.wemInitConfig.enableWemActionUrl}?wemEnabled=${enable}`,
type: 'POST',
async: true,
success: () => {
console.log(`Wem SSR ${enable ? 'enabled' : 'disabled'}`);
},
error: () => {
console.error(`Error when ${enable ? 'enabling' : 'disabling'} Wem SSR`);
}
});
console.log(`Wem ${enable ? 'enabled' : 'disabled'}`);
},
_deepMergeObjects: function (source, target) {
if (!wem._isObject(target) || !wem._isObject(source)) {
return source;
}
Object.keys(source).forEach(key => {
const targetValue = target[key];
const sourceValue = source[key];
// concat arrays || merge objects || add new props
if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
target[key] = targetValue.concat(sourceValue);
} else if (wem._isObject(targetValue) && wem._isObject(sourceValue)) {
target[key] = wem._deepMergeObjects(sourceValue, Object.assign({}, targetValue));
} else {
target[key] = sourceValue;
}
});
return target;
},
_isObject: function (obj) {
return obj && typeof obj === 'object';
},
_isInControlGroup: function (id) {
if (window.cxs.profileProperties && window.cxs.profileProperties.unomiControlGroups) {
let controlGroup = window.cxs.profileProperties.unomiControlGroups.find(controlGroup => controlGroup.id === id);
if (controlGroup) {
return true;
}
}
if (window.cxs.sessionProperties && window.cxs.sessionProperties.unomiControlGroups) {
let controlGroup = window.cxs.sessionProperties.unomiControlGroups.find(controlGroup => controlGroup.id === id);
if (controlGroup) {
return true;
}
}
return false;
}
}
}