@shopgate/tracking-core
Version:
Tracking core library for the Shopgate Connect PWA.
138 lines • 12.4 kB
JavaScript
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;