You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@unomi.apache.org by jk...@apache.org on 2022/09/05 16:43:29 UTC
[unomi-tracker] 01/01: UNOMI-610: base tracker first draft
This is an automated email from the ASF dual-hosted git repository.
jkevan pushed a commit to branch UNOMI-610-new-tracker
in repository https://gitbox.apache.org/repos/asf/unomi-tracker.git
commit 22d125111f84f09e7d072c38e4cc3deabd45f895
Author: Kevan <ke...@jahia.com>
AuthorDate: Mon Sep 5 18:42:59 2022 +0200
UNOMI-610: base tracker first draft
---
package.json | 3 +-
src/index.js | 6 +-
src/tracker/tracker.js | 1109 ++++++++++++++++++++++++++++++++++++++++++++++++
test/spec.js | 6 +-
yarn.lock | 5 +
5 files changed, 1124 insertions(+), 5 deletions(-)
diff --git a/package.json b/package.json
index 46c352a..dbb8c6b 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,8 @@
"rollup": "^2.79.0"
},
"dependencies": {
- "@babel/runtime": "^7.18.9"
+ "@babel/runtime": "^7.18.9",
+ "es6-crawler-detect": "^3.3.0"
},
"license": "Apache-2.0"
}
diff --git a/src/index.js b/src/index.js
index 5a7f0b3..6bbd7cf 100644
--- a/src/index.js
+++ b/src/index.js
@@ -15,4 +15,8 @@
* limitations under the License.
*/
-export const hello = () => "Hello world!";
\ No newline at end of file
+import {newTracker} from "./tracker/tracker";
+
+export const useTracker = () => {
+ return newTracker();
+};
\ No newline at end of file
diff --git a/src/tracker/tracker.js b/src/tracker/tracker.js
new file mode 100644
index 0000000..62a148e
--- /dev/null
+++ b/src/tracker/tracker.js
@@ -0,0 +1,1109 @@
+import { Crawler } from 'es6-crawler-detect';
+
+export const newTracker = () => {
+ const 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 {object} digitalData config of the tracker
+ */
+ init: function (digitalData) {
+ // added for external tracker
+ // store digitalData in tracker instance instead of window.
+ wem.digitalData = digitalData;
+ // new conf:
+ wem.trackerProfileIdCookieName = wem.digitalData.wemInitConfig.trackerProfileIdCookieName ? wem.digitalData.wemInitConfig.trackerProfileIdCookieName : "wem-profile-id";
+ wem.trackerSessionIdCookieName = wem.digitalData.wemInitConfig.trackerSessionIdCookieName ? wem.digitalData.wemInitConfig.trackerSessionIdCookieName : "wem-session-id";
+ wem.activateWem = wem.digitalData.wemInitConfig.activateWem;
+
+ const { contextServerUrl, isPreview, timeoutInMilliseconds, contextServerCookieName } = wem.digitalData.wemInitConfig;
+ wem.contextServerCookieName = contextServerCookieName;
+ wem.contextServerUrl = contextServerUrl;
+ wem.timeoutInMilliseconds = timeoutInMilliseconds;
+ wem.formNamesToWatch = [];
+ wem.eventsPrevented = [];
+ wem.sessionID = wem.getCookie(wem.trackerSessionIdCookieName);
+ 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;
+ }
+
+ wem._registerCallback(function () {
+ if (wem.cxs.profileId) {
+ wem.setCookie(wem.trackerProfileIdCookieName, wem.cxs.profileId);
+ }
+ if (!wem.cxs.profileId) {
+ wem.removeCookie(wem.trackerProfileIdCookieName);
+ }
+ // process tracked events
+ var videoNamesToWatch = [];
+ var clickToWatch = [];
+
+ if (wem.cxs.trackedConditions && wem.cxs.trackedConditions.length > 0) {
+ for (var i = 0; i < wem.cxs.trackedConditions.length; i++) {
+ switch (wem.cxs.trackedConditions[i].type) {
+ case 'formEventCondition':
+ if (wem.cxs.trackedConditions[i].parameterValues && wem.cxs.trackedConditions[i].parameterValues.formId) {
+ wem.formNamesToWatch.push(wem.cxs.trackedConditions[i].parameterValues.formId);
+ }
+ break;
+ case 'videoViewEventCondition':
+ if (wem.cxs.trackedConditions[i].parameterValues && wem.cxs.trackedConditions[i].parameterValues.videoId) {
+ videoNamesToWatch.push(wem.cxs.trackedConditions[i].parameterValues.videoId);
+ }
+ break;
+ case 'clickOnLinkEventCondition':
+ if (wem.cxs.trackedConditions[i].parameterValues && wem.cxs.trackedConditions[i].parameterValues.itemId) {
+ clickToWatch.push(wem.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');
+ }
+ }
+ });
+
+ // Load the context once document is ready
+ document.addEventListener('DOMContentLoaded', function () {
+ wem.DOMLoaded = true;
+
+ // 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(wem.digitalData.site.siteInfo.siteID, 'site'));
+ viewEvent.flattenedProperties = {};
+
+ // Add URLParameters
+ if (location.search) {
+ viewEvent.flattenedProperties['URLParameters'] = wem.convertUrlParametersToObj(location.search);
+ }
+ // Add interests
+ if (wem.digitalData.interests) {
+ viewEvent.flattenedProperties['interests'] = wem.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 (wem.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 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 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 = '';
+ }
+ },
+
+ /**
+ * 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: wem.digitalData.wemInitConfig.requiredProfileProperties,
+ requiredSessionProperties: wem.digitalData.wemInitConfig.requiredSessionProperties,
+ requireSegments: wem.digitalData.wemInitConfig.requireSegments,
+ requireScores: wem.digitalData.wemInitConfig.requireScores,
+ source: wem.buildSourcePage()
+ };
+ if (!skipEvents) {
+ jsonData.events = wem.digitalData.events;
+ }
+ if (wem.digitalData.personalizationCallback) {
+ jsonData.personalizations = wem.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 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 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 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: wem.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(wem.digitalData.page.pageInfo.pageID, 'page', wem.digitalData.page);
+ },
+
+ /**
+ * This function return the source object for a source of type page
+ *
+ * @returns {*|{scope, itemId: *, itemType: *}}
+ */
+ buildSourcePage: function () {
+ return wem.buildSource(wem.digitalData.page.pageInfo.pageID, 'page', wem.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 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 */
+ /*************************************/
+ _checkUncompleteRegisteredEvents: function () {
+ if (wem.digitalData && wem.digitalData.events) {
+ for (const event of wem.digitalData.events) {
+ wem._completeEvent(event);
+ }
+ }
+ },
+
+ _dispatchJSExperienceDisplayedEvents: () => {
+ if (wem.digitalData && wem.digitalData.events) {
+ for (const event of wem.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 (wem.digitalData && wem.digitalData.events) {
+ wem.digitalData.events = wem.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 = wem.digitalData.scope;
+ }
+ if (event.target && !event.target.scope) {
+ event.target.scope = wem.digitalData.scope;
+ }
+ return event;
+ },
+
+ _registerEvent: function (event, unshift) {
+ if (wem.digitalData) {
+ if (wem.cxs) {
+ console.error('[WEM] already loaded, too late...');
+ return;
+ }
+ } else {
+ wem.digitalData = {};
+ }
+
+ wem.digitalData.events = wem.digitalData.events || [];
+ if (unshift) {
+ wem.digitalData.events.unshift(event);
+ } else {
+ wem.digitalData.events.push(event);
+ }
+ },
+
+ _registerCallback: function (onLoadCallback) {
+ if (wem.digitalData) {
+ if (wem.cxs) {
+ console.info('[WEM] digitalData object loaded, calling on load callback immediately and registering update callback...');
+ if (onLoadCallback) {
+ onLoadCallback(wem.digitalData);
+ }
+ } else {
+ console.info('[WEM] digitalData object present but not loaded, registering load callback...');
+ if (onLoadCallback) {
+ wem.digitalData.loadCallbacks = wem.digitalData.loadCallbacks || [];
+ wem.digitalData.loadCallbacks.push(onLoadCallback);
+ }
+ }
+ } else {
+ console.info('[WEM] No digital data object found, creating and registering update callback...');
+ wem.digitalData = {};
+ if (onLoadCallback) {
+ wem.digitalData.loadCallbacks = [];
+ wem.digitalData.loadCallbacks.push(onLoadCallback);
+ }
+ }
+ },
+
+ _registerPersonalizationCallback: function (personalization, callback) {
+ if (wem.digitalData) {
+ if (wem.cxs) {
+ console.error('[WEM] already loaded, too late...');
+ } else {
+ console.info('[WEM] digitalData object present but not loaded, registering sort callback...');
+ wem.digitalData.personalizationCallback = wem.digitalData.personalizationCallback || [];
+ wem.digitalData.personalizationCallback.push({ personalization: personalization, callback: callback });
+ }
+ } else {
+ wem.digitalData = {};
+ wem.digitalData.personalizationCallback = wem.digitalData.personalizationCallback || [];
+ wem.digitalData.personalizationCallback.push({ personalization: personalization, callback: callback });
+ }
+ },
+
+ _buildObject: function (itemId, itemType, properties) {
+ var object = {
+ scope: wem.digitalData.scope,
+ itemId: itemId,
+ itemType: itemType
+ };
+
+ if (properties) {
+ object.properties = properties;
+ }
+
+ return object;
+ },
+
+ _onSuccess: function (xhr) {
+ wem.cxs = JSON.parse(xhr.responseText);
+
+ if (wem.digitalData.loadCallbacks && wem.digitalData.loadCallbacks.length > 0) {
+ console.info('[WEM] Found context server load callbacks, calling now...');
+ if (wem.digitalData.loadCallbacks) {
+ for (var i = 0; i < wem.digitalData.loadCallbacks.length; i++) {
+ wem.digitalData.loadCallbacks[i](wem.digitalData);
+ }
+ }
+ if (wem.digitalData.personalizationCallback) {
+ for (var j = 0; j < wem.digitalData.personalizationCallback.length; j++) {
+ wem.digitalData.personalizationCallback[j].callback(wem.cxs.personalizations[wem.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;
+ wem.cxs = {};
+ for (var index in wem.digitalData.loadCallbacks) {
+ wem.digitalData.loadCallbacks[index]();
+ }
+ if (wem.digitalData.personalizationCallback) {
+ for (var i = 0; i < wem.digitalData.personalizationCallback.length; i++) {
+ wem.digitalData.personalizationCallback[i].callback([wem.digitalData.personalizationCallback[i].personalization.strategyOptions.fallback]);
+ }
+ }
+ },
+
+ _processReferrer: function () {
+ var referrerURL = wem.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 wem.digitalData.page and add data to pageInfo sub object
+ if (wem.digitalData && wem.digitalData.page && wem.digitalData.page.pageInfo) {
+ wem.digitalData.page.pageInfo.referrerHost = referrer.host;
+ wem.digitalData.page.pageInfo.referrerQuery = query;
+ }
+ }
+ }
+ wem.digitalData.page.pageInfo.sameDomainReferrer = sameDomainReferrer;
+ },
+
+ _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;
+ },
+
+ _resolveId: function (id) {
+ if (wem.digitalData.sourceLocalIdentifierMap){
+ var source = Object.keys(wem.digitalData.sourceLocalIdentifierMap).filter(function (source) {
+ return id.indexOf(source) > 0;
+ });
+ return source ? id.replace(source, wem.digitalData.sourceLocalIdentifierMap[source]) : id;
+ }
+ return id;
+ },
+
+ _enableWem: (enable, callback) => {
+ // display fallback if wem is not enable
+ wem.fallback = !enable;
+ // remove cookies, reset cxs
+ if (!enable) {
+ wem.cxs = {};
+ document.cookie = wem.trackerProfileIdCookieName + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
+ document.cookie = wem.contextServerCookieName + '=; 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.
+ wem.activateWem = true;
+ }
+ }
+
+ if (callback) {
+ callback(enable)
+ }
+ console.log(`Wem ${enable ? 'enabled' : 'disabled'}`);
+ },
+
+ _isInControlGroup: function (id) {
+ if (wem.cxs.profileProperties && wem.cxs.profileProperties.unomiControlGroups) {
+ let controlGroup = wem.cxs.profileProperties.unomiControlGroups.find(controlGroup => controlGroup.id === id);
+ if (controlGroup) {
+ return true;
+ }
+ }
+ if (wem.cxs.sessionProperties && wem.cxs.sessionProperties.unomiControlGroups) {
+ let controlGroup = wem.cxs.sessionProperties.unomiControlGroups.find(controlGroup => controlGroup.id === id);
+ if (controlGroup) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ return wem;
+};
\ No newline at end of file
diff --git a/test/spec.js b/test/spec.js
index 938ea50..760fc99 100644
--- a/test/spec.js
+++ b/test/spec.js
@@ -15,9 +15,9 @@
* limitations under the License.
*/
-const tracker = require('..');
const assert = require('assert')
+const unomi = require('..');
+const tracker = unomi.useTracker();
-assert.strictEqual(tracker.hello(), 'Hello world!');
-
+assert(tracker !== null)
console.log(`Tests passed`);
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index acaa886..595aa58 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1330,6 +1330,11 @@ electron-to-chromium@^1.4.202:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.239.tgz#5b04acb39c16b897a508980d1be95ba5f0201771"
integrity sha512-XbhfzxPIFzMjJm17T7yUGZEyYh5XuUjrA/FQ7JUy2bEd4qQ7MvFTaKpZ6zXZog1cfVttESo2Lx0ctnf7eQOaAQ==
+es6-crawler-detect@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/es6-crawler-detect/-/es6-crawler-detect-3.3.0.tgz#3a05cd3f2739099145bf40b012a6ad472cbbfb49"
+ integrity sha512-ptGU13H76+HNr5n0kvi5aO+RuqRHaIET/60Srv4+BgVWsuVVf3x9seDhz/IEcmuQMXJvrU2g+DbrKVrliUkTJQ==
+
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"