UNPKG

@shopgate/tracking-core

Version:

Tracking core library for the Shopgate Connect PWA.

138 lines • 12.4 kB
function _extends(){_extends=Object.assign||function(target){for(var i=1;i<arguments.length;i++){var source=arguments[i];for(var key in source){if(Object.prototype.hasOwnProperty.call(source,key)){target[key]=source[key];}}}return target;};return _extends.apply(this,arguments);}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function");}}function _defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor);}}function _createClass(Constructor,protoProps,staticProps){if(protoProps)_defineProperties(Constructor.prototype,protoProps);if(staticProps)_defineProperties(Constructor,staticProps);return Constructor;}function _defineProperty(obj,key,value){if(key in obj){Object.defineProperty(obj,key,{value:value,enumerable:true,configurable:true,writable:true});}else{obj[key]=value;}return obj;}import{logger}from'@shopgate/pwa-core/helpers';import errorManager from'@shopgate/pwa-core/classes/ErrorManager';import{SOURCE_TRACKING,CODE_TRACKING}from'@shopgate/pwa-core/constants/ErrorManager';import{optOut as _optOut,isOptOut}from"../helpers/optOut";import trackingEvents,{REMOVE_TRACKER,ADD_TRACKER,scannerEvents}from"../helpers/events";/** * Core for our tracking system. Plugins can use the core to register for events. Pub/sub pattern. */var Core=/*#__PURE__*/function(){/** * Constructor */function Core(){var _this=this;_classCallCheck(this,Core);/** * Check if the opt-out state is set * * @returns {boolean} Information about the opt out state */_defineProperty(this,"isOptOut",function(){return isOptOut();});/** * Returns scanner (adscanner and QR) event action constants. * * @returns {Object} Scanner events */_defineProperty(this,"getScannerEvents",function(){return scannerEvents;});/** * Helper function to create ad scanner opt_label from pageTitle and pageId * * @param {string} pageTitle Name of the Page * @param {string} id ID of the ad * @returns {string} String from pageTitle and pageId */_defineProperty(this,"buildAdImageIdentifierName",function(pageTitle,id){var name=pageTitle?"".concat(pageTitle," "):'';return"".concat(name,"(id: ").concat(id,")");});/** * This function will handle the cross domain tracking, depending on which sdks are there * @param {string} originalUrl url of the link * @param {HTMLFormElement} [formElement] Form element if we do a POST * @returns {boolean|string} Tells if the function executed the steps that are necessary * for domain transitions via tracking plugins. In a sg cloud app it returns the new url. */_defineProperty(this,"crossDomainTracking",function(originalUrl,formElement){if(window.sgData&&window.sgData.device.access==='App'){return false;}var newUrl=originalUrl;// Add econda params if(typeof window.getEmosCrossUrlParams==='function'){newUrl+=(!newUrl.includes('?')?'?':'&')+window.getEmosCrossUrlParams();}// If there is no sgData, we are in sg cloud app and have to return the new url if(!window.sgData){return newUrl;}// Universal try{window.ga(function(){var tracker=window.ga.getByName('shopgate');var linker=new window.gaplugins.Linker(tracker);// Add ?_ga parameter to the url newUrl=linker.decorate(newUrl);});}catch(e){}// Ignore errors // Check if there is the classic sdk if(typeof _gaq==='undefined'){// No classic sdk if(formElement){/** * The no-param-reassign rule is deactivated on purpose, * since the form action has to be replaced in this situation */ // eslint-disable-next-line no-param-reassign formElement.action=newUrl;}else{window.location.href=newUrl;}}else if(formElement){// eslint-disable-next-line no-underscore-dangle window._gaq.push(['merchant_._linkByPost',formElement]);}else{// eslint-disable-next-line no-underscore-dangle window._gaq.push(['merchant_._link',newUrl]);}return true;});// Store for the registered events this.events={};// Store for all triggered events this.triggeredEvents=[];this.registerFinish=false;/** * Storage for functions to register tracking event callbacks * @type {Object} */this.register={};// Create the register functions for every available event trackingEvents.forEach(function(event){/** * Register for event * @param {Function} callback Function that is called if the event occurs * @param {Object} options Additional options * @returns {RemoveListener} Function to remove the listener */_this.register[event]=function(callback,options){return _this.registerHelper(callback,event,options);};});/** * Storage for functions that trigger tracking events * @type {Object} */this.track={};// Create the track functions for every available event trackingEvents.forEach(function(event){// Don't create track functions for those events if([REMOVE_TRACKER,ADD_TRACKER].indexOf(event)!==-1){return;}/** * Track event * @param {Object} rawData Raw data from sgData Object * @param {string} [page] Identifier of the page * @param {Object} [scope] Scope for the event * @param {Object} [state] The redux state * @returns {Core} Instance of Core */_this.track[event]=function(rawData,page,scope,state){return _this.notifyPlugins(_extends({},rawData),event,page,scope,state);};});}/** * Returns and creates event store if needed * * @param {string} eventName Name of the event * @param {string} [page] Identifier of the page * @returns {Object} The event store for the given eventName and page */return _createClass(Core,[{key:"getEventStore",value:function getEventStore(eventName,page){if(!this.events.hasOwnProperty(eventName)){// Create a new event store entry for the current event name, if no one is already present this.events[eventName]={// Events for every page global:[],// Events only for specific pages specific:{}};}// Get the global events for the current event name var eventStore=this.events[eventName].global;if(page){// A page name was provided, which means that we have to use the page specific sub store eventStore=this.events[eventName].specific;if(!eventStore.hasOwnProperty(page)){// There is no entry for the current page, so we initialize it eventStore[page]=[];}// Prepare the specific event store for the provided page as return value eventStore=eventStore[page];}return eventStore;}/** * Register a tracking event callback for a plugin * * @param {Function} callback Function that is called if the event occurs * @param {string} eventName Name of the event * @param {Object} options Additional options * @returns {RemoveListener} Function to remove the listener */},{key:"registerHelper",value:function registerHelper(callback,eventName){var options=arguments.length>2&&arguments[2]!==undefined?arguments[2]:{};var defaults={trackerName:'',page:null,merchant:true,shopgate:true,options:{}};var pluginOptions=_extends({},defaults,{},options);if(!pluginOptions.trackerName){logger.warn("'SgTrackingCore': Attempt to register for event \"".concat(eventName,"\" by a nameless tracker"));}// Get the correct store var store=this.getEventStore(eventName,pluginOptions.page);// Add the callback to queue var index=store.push({callback:callback,trackerName:pluginOptions.trackerName,useNativeSdk:pluginOptions.options.useNativeSdk,overrideUnified:pluginOptions.options.overrideUnified,merchant:pluginOptions.merchant,shopgate:pluginOptions.shopgate})-1;// Provide handle back for removal of event return{remove:function remove(){delete store[index];}};}/** * Notify plugins helper * * @param {Object} rawData Object with tracking data for the event * @param {string} eventName Name of the event * @param {string} [page] Identifier of the page * @param {Object} [scope] Scope of the event * @param {Object} [state] The redux state * @returns {void} */},{key:"notifyHelper",value:function notifyHelper(rawData,eventName,page){var _this2=this;var scope=arguments.length>3&&arguments[3]!==undefined?arguments[3]:{};var state=arguments.length>4?arguments[4]:undefined;// Exit if the user opt out. But not for the explicit add or remove tracker events if([REMOVE_TRACKER,ADD_TRACKER].indexOf(eventName)===-1&&isOptOut()){return;}// Default scope var scopeOptions=_extends({shopgate:true,merchant:true},scope);// Get the global list of registered callbacks for the event name var storeGlobal=this.getEventStore(eventName);// Get the page specific list of registered callbacks var storePage=page?this.getEventStore(eventName,page):[];// Initialize the event payload var eventData=typeof rawData!=='undefined'?rawData:{};// Merge the global with the page specific callbacks var combinedStorage=storeGlobal.concat(storePage);var blacklist=[];var useBlacklist=false;/** * Loop through the queue and check if there is a unified and other plugins. * Registered callbacks of plugins not equal to the unified one, have to be added to a * blacklist, so that the app does not propagate the unified data, but the custom one * to the related trackers. */combinedStorage.forEach(function(entry){if(entry.trackerName!=='unified'&&entry.useNativeSdk){blacklist.push(entry.trackerName.slice(0));}else{// If there is a unified plugin registered for this event -> use the blacklist useBlacklist=true;}});// Only use the blacklist if it contains elements if(useBlacklist){useBlacklist=!!blacklist.length;}// Cycle through events queue, fire! combinedStorage.forEach(function(entry){// Merchant only command if(scopeOptions.merchant&&!scopeOptions.shopgate&&!entry.merchant){return;// Shopgate only command }if(!scopeOptions.merchant&&scopeOptions.shopgate&&!entry.shopgate){return;}var params=[eventData,scopeOptions,undefined,state];if(entry.trackerName==='unified'&&useBlacklist){// Pass the unifiedBlacklist to the plugin if the plugin is the unified one params[2]=blacklist;}try{entry.callback.apply(_this2,params);}catch(err){logger.error("'SgTrackingCore': Error in plugin [".concat(entry.trackerName,"]"),err);err.code=CODE_TRACKING;err.source=SOURCE_TRACKING;err.context=entry.trackerName;errorManager.queue(err);}});}/** * Notify plugins * * @param {Object} rawData Object with tracking data for the event * @param {string} eventName Name of the event * @param {string} [page] Identifier of the page * @param {Object} [scope] Scope of the event * @param {Object} [state] The redux state * @returns {Core} Instance of the core instance */},{key:"notifyPlugins",value:function notifyPlugins(rawData,eventName,page,scope,state){// If registration is finished if(this.registerFinish){this.notifyHelper(rawData,eventName,page,scope,state);}else{// Store the event if not this.triggeredEvents.push({"function":this.notifyHelper,params:[rawData,eventName,page,scope,state]});}return this;}/** * Opt out mechanism for all tracking tools * * @param {boolean} [optOutParam = true] If false -> revert the opt out (enable tracking) * @returns {boolean|null} State which was set */},{key:"optOut",value:function optOut(optOutParam){var out=optOutParam;if(typeof optOutParam==='undefined'){out=true;}if(out){// Notify Plugins about the removal. this.notifyPlugins(null,REMOVE_TRACKER);}else{// Notify Plugins about the adding this.notifyPlugins(null,ADD_TRACKER);}return _optOut(out);}},{key:"registerFinished",value:/** * Called from the outside when all plugins are registered * @return {Core} */function registerFinished(){var _this3=this;if(this.registerFinish){return this;}// Trigger all events that happened till now this.triggeredEvents.forEach(function(entry){entry["function"].apply(_this3,entry.params);});this.registerFinish=true;// Reset the event store this.triggeredEvents=[];return this;}/** * Remove all registered callbacks. Only used and needed for unit tests * @returns {Core} */},{key:"reset",value:function reset(){this.events={};this.triggeredEvents=[];this.registerFinish=false;return this;}}]);}();/** * Fix to prevent multiple instances of this class caused by two node_modules folders */if(!window.SgTrackingCore){window.SgTrackingCore=new Core();}export default window.SgTrackingCore;