UNPKG

@shopgate/tracking-core

Version:

Tracking core library for the Shopgate Connect PWA.

394 lines (360 loc) • 12.7 kB
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. */ let Core = /*#__PURE__*/function () { /** * Constructor */ function Core() { /** * Check if the opt-out state is set * * @returns {boolean} Information about the opt out state */ this.isOptOut = () => isOptOut(); /** * Returns scanner (adscanner and QR) event action constants. * * @returns {Object} Scanner events */ this.getScannerEvents = () => 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 */ this.buildAdImageIdentifierName = (pageTitle, id) => { const name = pageTitle ? `${pageTitle} ` : ''; return `${name}(id: ${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. */ this.crossDomainTracking = (originalUrl, formElement) => { if (window.sgData && window.sgData.device.access === 'App') { return false; } let 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(() => { const tracker = window.ga.getByName('shopgate'); const 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(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] = (callback, options) => 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(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] = (rawData, page, scope, state) => this.notifyPlugins({ ...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 */ var _proto = Core.prototype; _proto.getEventStore = 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 let 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 */; _proto.registerHelper = function registerHelper(callback, eventName, options = {}) { const defaults = { trackerName: '', page: null, merchant: true, shopgate: true, options: {} }; const pluginOptions = { ...defaults, ...options }; if (!pluginOptions.trackerName) { logger.warn(`'SgTrackingCore': Attempt to register for event "${eventName}" by a nameless tracker`); } // Get the correct store const store = this.getEventStore(eventName, pluginOptions.page); // Add the callback to queue const index = store.push({ 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() { 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} */; _proto.notifyHelper = function notifyHelper(rawData, eventName, page, scope = {}, state) { // 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 const scopeOptions = { shopgate: true, merchant: true, ...scope }; // Get the global list of registered callbacks for the event name const storeGlobal = this.getEventStore(eventName); // Get the page specific list of registered callbacks const storePage = page ? this.getEventStore(eventName, page) : []; // Initialize the event payload const eventData = typeof rawData !== 'undefined' ? rawData : {}; // Merge the global with the page specific callbacks const combinedStorage = storeGlobal.concat(storePage); const blacklist = []; let 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(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(entry => { // Merchant only command if (scopeOptions.merchant && !scopeOptions.shopgate && !entry.merchant) { return; // Shopgate only command } if (!scopeOptions.merchant && scopeOptions.shopgate && !entry.shopgate) { return; } const 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(this, params); } catch (err) { logger.error(`'SgTrackingCore': Error in plugin [${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 */; _proto.notifyPlugins = 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 */; _proto.optOut = function optOut(optOutParam) { let 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); }; /** * Called from the outside when all plugins are registered * @return {Core} */ _proto.registerFinished = function registerFinished() { if (this.registerFinish) { return this; } // Trigger all events that happened till now this.triggeredEvents.forEach(entry => { entry.function.apply(this, 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} */; _proto.reset = function reset() { this.events = {}; this.triggeredEvents = []; this.registerFinish = false; return this; }; return Core; }(); /** * 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;