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"