| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| 'use strict'; |
| |
| var integration = require('@segment/analytics.js-integration'); |
| var extend = require('extend'); |
| |
| var Unomi = (module.exports = integration('Apache Unomi') |
| .global('cxs') |
| .assumesPageview() |
| .readyOnLoad() |
| .option('scope', 'systemscope') |
| .option('url', 'http://localhost:8181') |
| .option('timeoutInMilliseconds', 3000) |
| .option('sessionCookieName', 'unomiSessionId') |
| .option('sessionId')); |
| |
| /** |
| * Initialize. |
| * |
| * @api public |
| */ |
| Unomi.prototype.initialize = function() { |
| var self = this; |
| |
| console.info('[UNOMI] Initializing...', arguments); |
| |
| this.analytics.on('invoke', function(msg) { |
| var action = msg.action(); |
| var listener = 'on' + msg.action(); |
| self.debug('%s %o', action, msg); |
| if (self[listener]) self[listener](msg); |
| }); |
| |
| this.analytics.personalize = function(personalization, callback) { |
| this.emit('invoke', {action:function() {return "personalize"}, personalization:personalization, callback:callback}); |
| }; |
| |
| // Standard to check if cookies are enabled in this browser |
| if (!navigator.cookieEnabled) { |
| this.executeFallback(); |
| return; |
| } |
| |
| // digitalData come from a standard so we can keep the logic around it which can allow complex website to load more complex data |
| window.digitalData = window.digitalData || { |
| scope: self.options.scope |
| }; |
| |
| window.digitalData.page = window.digitalData.page || { |
| path: location.pathname + location.hash, |
| pageInfo: { |
| pageName: document.title, |
| pageID : location.pathname + location.hash, |
| pagePath : location.pathname + location.hash, |
| destinationURL: location.href |
| } |
| } |
| |
| var unomiPage = window.digitalData.page; |
| if (!unomiPage) { |
| unomiPage = window.digitalData.page = { pageInfo:{} } |
| } |
| if (self.options.initialPageProperties) { |
| var props = self.options.initialPageProperties; |
| this.fillPageData(unomiPage, props); |
| } |
| window.digitalData.events = window.digitalData.events || []; |
| window.digitalData.events.push(this.buildEvent('view', this.buildPage(unomiPage), this.buildSource(this.options.scope, 'site'))) |
| |
| if (!self.options.sessionId) { |
| var cookie = require('component-cookie'); |
| |
| self.sessionId = cookie(self.options.sessionCookieName); |
| // so we should not need to implement our own |
| if (!self.sessionId || self.sessionId === '') { |
| self.sessionId = self.generateGuid(); |
| cookie(self.options.sessionCookieName, self.sessionId); |
| } |
| } else { |
| this.sessionId = self.options.sessionId; |
| } |
| |
| setTimeout(self.loadContext.bind(self), 0); |
| }; |
| |
| /** |
| * Loaded. |
| * |
| * @api private |
| * @return {boolean} |
| */ |
| Unomi.prototype.loaded = function() { |
| return !!window.cxs; |
| }; |
| |
| /** |
| * Page. |
| * |
| * @api public |
| * @param {Page} page |
| */ |
| Unomi.prototype.page = function(page) { |
| var unomiPage = { }; |
| this.fillPageData(unomiPage, page.json().properties); |
| |
| this.collectEvent(this.buildEvent('view', this.buildPage(unomiPage), this.buildSource(this.options.scope, 'site'))); |
| }; |
| |
| Unomi.prototype.fillPageData = function(unomiPage, props) { |
| unomiPage.attributes = []; |
| unomiPage.consentTypes = []; |
| unomiPage.interests = props.interests || {}; |
| unomiPage.pageInfo = extend({}, unomiPage.pageInfo, props.pageInfo); |
| unomiPage.pageInfo.pageName = unomiPage.pageInfo.pageName || props.title; |
| unomiPage.pageInfo.pageID = unomiPage.pageInfo.pageID || props.path; |
| unomiPage.pageInfo.pagePath = unomiPage.pageInfo.pagePath || props.path; |
| unomiPage.pageInfo.destinationURL = unomiPage.pageInfo.destinationURL || props.url; |
| unomiPage.pageInfo.referringURL = unomiPage.pageInfo.referringURL || props.referrer; |
| this.processReferrer(); |
| }; |
| |
| Unomi.prototype.processReferrer = function() { |
| var referrerURL = document.referrer; |
| if (referrerURL) { |
| // parse referrer URL |
| var referrer = document.createElement('a'); |
| referrer.href = referrerURL; |
| |
| // only process referrer if it's not coming from the same site as the current page |
| var local = document.createElement('a'); |
| local.href = document.URL; |
| if (referrer.host !== local.host) { |
| // 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, ' '); |
| } |
| |
| // add data to digitalData |
| if (window.digitalData && window.digitalData.page && window.digitalData.page.pageInfo) { |
| window.digitalData.page.pageInfo.referrerHost = referrer.host; |
| window.digitalData.page.pageInfo.referrerQuery = query; |
| } |
| |
| // register referrer event |
| this.registerEvent(this.buildEvent('viewFromReferrer', this.buildTargetPage())); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Identify. |
| * |
| * @api public |
| * @param {Identify} identify |
| */ |
| Unomi.prototype.identify = function(identify) { |
| this.collectEvent(this.buildEvent("identify", |
| this.buildTarget(identify.userId(), "analyticsUser", identify.traits()), |
| this.buildSource(this.options.scope, 'site', identify.context()))); |
| }; |
| |
| /** |
| * ontrack. |
| * |
| * @api private |
| * @param {Track} track |
| */ |
| Unomi.prototype.track = function(track) { |
| // we use the track event name to know that we are submitted a form because Analytics.js trackForm method doesn't give |
| // us another way of knowing that we are processing a form. |
| if (track.event() && track.event().indexOf("form") === 0) { |
| var form = document.forms[track.properties().formName]; |
| var formEvent = this.buildFormEvent(form.name); |
| formEvent.properties = this.extractFormData(form); |
| this.collectEvent(formEvent); |
| } else { |
| this.collectEvent(this.buildEvent(track.event(), |
| this.buildTargetPage(), |
| this.buildSource(this.options.scope, 'site', track.context()), |
| track.properties() |
| )); |
| } |
| }; |
| |
| /** |
| * 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 |
| */ |
| Unomi.prototype.loadContext = function (skipEvents, invalidate) { |
| this.contextLoaded = true; |
| var jsonData = { |
| requiredProfileProperties: ['j:nodename'], |
| source: this.buildPage(window.digitalData.page) |
| }; |
| if (!skipEvents) { |
| jsonData.events = window.digitalData.events |
| } |
| if (window.digitalData.personalizationCallback) { |
| jsonData.personalizations = window.digitalData.personalizationCallback.map(function (x) { |
| return x.personalization |
| }) |
| } |
| |
| jsonData.sessionId = this.sessionId; |
| |
| var contextUrl = this.options.url + '/context.json'; |
| if (invalidate) { |
| contextUrl += '?invalidateSession=true&invalidateProfile=true'; |
| } |
| |
| var self = this; |
| |
| var onSuccess = function (xhr) { |
| |
| window.cxs = JSON.parse(xhr.responseText); |
| |
| self.ready(); |
| |
| if (window.digitalData.loadCallbacks) { |
| console.info('[UNOMI] Found context server load callbacks, calling now...'); |
| for (var i = 0; i < window.digitalData.loadCallbacks.length; i++) { |
| window.digitalData.loadCallbacks[i](digitalData); |
| } |
| } |
| if (window.digitalData.personalizationCallback) { |
| console.info('[UNOMI] Found context server personalization, calling now...'); |
| for (var i = 0; i < window.digitalData.personalizationCallback.length; i++) { |
| window.digitalData.personalizationCallback[i].callback(cxs.personalizations[window.digitalData.personalizationCallback[i].personalization.id]); |
| } |
| } |
| }; |
| |
| this.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: onSuccess, |
| error: this.executeFallback |
| }); |
| |
| console.info('[UNOMI] Context loading...'); |
| }; |
| |
| Unomi.prototype.onpersonalize = function (msg) { |
| if (this.contextLoaded) { |
| console.error('[UNOMI] Already loaded, too late...'); |
| return; |
| } |
| window.digitalData = window.digitalData || { |
| scope: this.options.scope |
| }; |
| window.digitalData.personalizationCallback = window.digitalData.personalizationCallback || []; |
| window.digitalData.personalizationCallback.push({personalization: msg.personalization, callback: msg.callback}); |
| }; |
| |
| /** |
| * 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 this.buildTarget(targetId, targetType, targetProperties) |
| * @param {object} [source] The source object for your event can be build with this.buildSource(sourceId, sourceType, sourceProperties) |
| * @param {object} [properties] a map of properties for the event |
| * @returns {{eventType: *, scope}} |
| */ |
| Unomi.prototype.buildEvent = function (eventType, target, source, properties) { |
| var event = { |
| eventType: eventType, |
| scope: window.digitalData.scope |
| }; |
| |
| if (target) { |
| event.target = target; |
| } |
| |
| if (source) { |
| event.source = source; |
| } |
| |
| if (properties) { |
| event.properties = properties; |
| } |
| |
| 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: {}}}} |
| */ |
| Unomi.prototype.buildFormEvent = function (formName) { |
| return this.buildEvent('form', this.buildTarget(formName, 'form'), this.buildSourcePage()); |
| }; |
| |
| /** |
| * This function return the source object for a source of type page |
| * |
| * @returns {*|{scope, itemId: *, itemType: *}} |
| */ |
| Unomi.prototype.buildTargetPage = function () { |
| return this.buildTarget(window.digitalData.page.pageInfo.pageID, 'page', window.digitalData.page); |
| }; |
| |
| /** |
| * This function return the source object for a source of type page |
| * |
| * @returns {*|{scope, itemId: *, itemType: *}} |
| */ |
| Unomi.prototype.buildSourcePage = function () { |
| return this.buildSource(window.digitalData.page.pageInfo.pageID, 'page', window.digitalData.page); |
| }; |
| |
| |
| /** |
| * This function return the source object for a source of type page |
| * |
| * @returns {*|{scope, itemId: *, itemType: *}} |
| */ |
| Unomi.prototype.buildPage = function (page) { |
| return this.buildSource(page.pageInfo.pageID, 'page', 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: *}} |
| */ |
| Unomi.prototype.buildTarget = function (targetId, targetType, targetProperties) { |
| return this.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: *}} |
| */ |
| Unomi.prototype.buildSource = function (sourceId, sourceType, sourceProperties) { |
| return this.buildObject(sourceId, sourceType, sourceProperties); |
| }; |
| |
| |
| /** |
| * This function will send an event to Apache Unomi |
| * @param {object} event The event object to send, you can build it using this.buildEvent(eventType, target, source) |
| * @param {function} successCallback will be executed in case of success |
| * @param {function} errorCallback will be executed in case of error |
| */ |
| Unomi.prototype.collectEvent = function (event, successCallback, errorCallback) { |
| this.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 |
| */ |
| Unomi.prototype.collectEvents = function (events, successCallback, errorCallback) { |
| events.sessionId = this.sessionId; |
| |
| var data = JSON.stringify(events); |
| this.ajax({ |
| url: this.options.url + '/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 |
| }); |
| }; |
| |
| /*******************************/ |
| /* Private Function under this */ |
| /*******************************/ |
| |
| Unomi.prototype.registerEvent = function (event) { |
| if (window.digitalData) { |
| if (window.cxs) { |
| console.error('[UNOMI] already loaded, too late...'); |
| } else { |
| window.digitalData.events = window.digitalData.events || []; |
| window.digitalData.events.push(event); |
| } |
| } else { |
| window.digitalData = {}; |
| window.digitalData.events = window.digitalData.events || []; |
| window.digitalData.events.push(event); |
| } |
| }; |
| |
| Unomi.prototype.registerCallback = function (onLoadCallback) { |
| if (window.digitalData) { |
| if (window.cxs) { |
| console.info('[UNOMI] digitalData object loaded, calling on load callback immediately and registering update callback...'); |
| if (onLoadCallback) { |
| onLoadCallback(window.digitalData); |
| } |
| } else { |
| console.info('[UNOMI] digitalData object present but not loaded, registering load callback...'); |
| if (onLoadCallback) { |
| window.digitalData.loadCallbacks = window.digitalData.loadCallbacks || []; |
| window.digitalData.loadCallbacks.push(onLoadCallback); |
| } |
| } |
| } else { |
| console.info('[UNOMI] No digital data object found, creating and registering update callback...'); |
| window.digitalData = {}; |
| if (onLoadCallback) { |
| window.digitalData.loadCallbacks = []; |
| window.digitalData.loadCallbacks.push(onLoadCallback); |
| } |
| } |
| }; |
| |
| /** |
| * This is an utility function to generate a new UUID |
| * |
| * @returns {string} |
| */ |
| Unomi.prototype.generateGuid = function () { |
| function s4() { |
| return Math.floor((1 + Math.random()) * 0x10000) |
| .toString(16) |
| .substring(1); |
| } |
| |
| return s4() + s4() + '-' + s4() + '-' + s4() + '-' + |
| s4() + '-' + s4() + s4() + s4(); |
| }; |
| |
| Unomi.prototype.buildObject = function (itemId, itemType, properties) { |
| var object = { |
| scope: window.digitalData.scope, |
| itemId: itemId, |
| itemType: itemType |
| }; |
| |
| if (properties) { |
| object.properties = properties; |
| } |
| |
| return object; |
| }; |
| |
| /** |
| * This is an utility function to execute AJAX call |
| * |
| * @param {object} ajaxOptions |
| */ |
| Unomi.prototype.ajax = function (ajaxOptions) { |
| var xhr = new XMLHttpRequest(); |
| if ('withCredentials' in xhr) { |
| xhr.open(ajaxOptions.type, ajaxOptions.url, ajaxOptions.async); |
| xhr.withCredentials = true; |
| } else if (typeof XDomainRequest != 'undefined') { |
| xhr = new XDomainRequest(); |
| xhr.open(ajaxOptions.type, ajaxOptions.url); |
| } |
| |
| if (ajaxOptions.contentType) { |
| xhr.setRequestHeader('Content-Type', ajaxOptions.contentType); |
| } |
| if (ajaxOptions.dataType) { |
| xhr.setRequestHeader('Accept', ajaxOptions.dataType); |
| } |
| |
| if (ajaxOptions.responseType) { |
| xhr.responseType = ajaxOptions.responseType; |
| } |
| |
| var requestExecuted = false; |
| if (this.options.timeoutInMilliseconds !== -1) { |
| setTimeout(function () { |
| if (!requestExecuted) { |
| console.error('[UNOMI] XML request timeout, url: ' + ajaxOptions.url); |
| requestExecuted = true; |
| if (ajaxOptions.error) { |
| ajaxOptions.error(xhr); |
| } |
| } |
| }, this.options.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 (ajaxOptions.success) { |
| ajaxOptions.success(xhr); |
| } |
| } |
| } else { |
| requestExecuted = true; |
| if (ajaxOptions.error) { |
| ajaxOptions.error(xhr); |
| } |
| console.error('[UNOMI] XML request error: ' + xhr.statusText + ' (' + xhr.status + ')'); |
| } |
| } |
| } |
| }; |
| |
| if (ajaxOptions.jsonData) { |
| xhr.send(JSON.stringify(ajaxOptions.jsonData)); |
| } else if (ajaxOptions.data) { |
| xhr.send(ajaxOptions.data); |
| } else { |
| xhr.send(); |
| } |
| }; |
| |
| Unomi.prototype.executeFallback = function () { |
| console.warn('[UNOMI] execute fallback'); |
| window.cxs = {}; |
| for (var index in window.digitalData.loadCallbacks) { |
| window.digitalData.loadCallbacks[index](); |
| } |
| if (window.digitalData.personalizationCallback) { |
| for (var i = 0; i < window.digitalData.personalizationCallback.length; i++) { |
| window.digitalData.personalizationCallback[i].callback([window.digitalData.personalizationCallback[i].personalization.strategyOptions.fallback]); |
| } |
| } |
| }; |
| |
| Unomi.prototype.extractFormData = function (form) { |
| var params = {}; |
| for (var i = 0; i < form.elements.length; i++) { |
| var e = form.elements[i]; |
| if (typeof(e.name) != 'undefined') { |
| 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; |
| default: |
| console.warn("[UNOMI] " + e.nodeName + " form element type not implemented and will not be tracked."); |
| } |
| } |
| } |
| return params; |
| }; |