UNPKG

@chordcommerce/analytics

Version:

Chord Commerce event tracking

913 lines 77.3 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { addCdpSnippet, cdpQueue } from './cdp.js'; import { addCdpConsent } from './consent/index.js'; import { eventSchemas } from '../validators/index.js'; import { pruneNullValues } from '../utils.js'; const DEBUG_MODE_STORAGE_KEY = 'chordcommerce/analytics/debug'; const CONSENT_QUEUE_STORAGE_KEY = 'chordcommerce/analytics/consent-queue'; const CONSENT_QUEUE_TTL_MS = 5 * 60 * 1000; // 5 minutes const CONSENT_QUEUE_MAX_SIZE_BYTES = 100 * 1024; // 100KB max queue size const CONSENT_QUEUE_MAX_EVENTS = 50; // Max number of events to queue export class ChordAnalytics { constructor(options) { var _a, _b, _c, _d; /** * Allows snippet.js to detect whether this library has been initialized yet. */ this.initialize = true; /** * Allows snippet.js to detect whether the snippet has started running yet. */ this.invoked = true; /** * Middleware to be applied to the event properties */ this.middleware = []; /** * Promise that resolves when CDP is ready */ this.cdpReadyPromise = null; this.consentConfig = null; // Non-null while reset() is in flight. Acts as both the coalescing handle and // the gate that track/identify/page await before executing. this.resetCompletePromise = null; /** * Queue for events waiting for consent to be ready */ this.consentEventQueue = []; /** * Whether we're currently waiting for consent and watching for it */ this.consentWatcherActive = false; /** * Promise that resolves when consent is ready (shared across all queued events) */ this.consentReadyPromise = null; /** * Persist the consent event queue to sessionStorage for recovery on instance recreation. * Validates queue size before storing to prevent QuotaExceededError. */ this.persistQueueToStorage = () => { if (typeof window === 'undefined' || !window.sessionStorage) return; try { // Limit number of events to prevent unbounded queue growth const eventsToStore = this.consentEventQueue.slice(-CONSENT_QUEUE_MAX_EVENTS); const persistedEvents = eventsToStore.map((item) => ({ type: item.type, args: item.args, timestamp: item.timestamp, })); const serialized = JSON.stringify(persistedEvents); // Check serialized size before attempting to store const sizeInBytes = new Blob([serialized]).size; if (sizeInBytes > CONSENT_QUEUE_MAX_SIZE_BYTES) { this.logger(`[Chord Analytics]: Consent queue too large (${Math.round(sizeInBytes / 1024)}KB), skipping persistence`); return; } window.sessionStorage.setItem(CONSENT_QUEUE_STORAGE_KEY, serialized); } catch (error) { this.logger('[Chord Analytics]: Error persisting queue to storage:', error); } }; /** * Restore pending events from sessionStorage (keeps events less than 5 min old). * Called on new instance initialization. */ this.restoreQueueFromStorage = () => { if (typeof window === 'undefined' || !window.sessionStorage) return; try { const stored = window.sessionStorage.getItem(CONSENT_QUEUE_STORAGE_KEY); if (!stored) return; let persistedEvents; try { persistedEvents = JSON.parse(stored); } catch (parseError) { this.logger('[Chord Analytics]: Invalid JSON in stored consent queue, clearing storage:', parseError); window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY); return; } // Validate that parsed data is an array if (!Array.isArray(persistedEvents)) { this.logger('[Chord Analytics]: Stored consent queue is not an array, clearing storage'); window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY); return; } const now = Date.now(); // Filter out expired events and events with invalid structure const validEvents = persistedEvents.filter((event) => { // Validate event structure if (typeof event !== 'object' || event === null) return false; if (!['track', 'page', 'identify'].includes(event.type)) return false; if (!Array.isArray(event.args)) return false; if (typeof event.timestamp !== 'number') return false; // Filter out expired events return now - event.timestamp < CONSENT_QUEUE_TTL_MS; }); if (validEvents.length === 0) { window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY); return; } if (this.options.enableLogging && this.options.debug) { // eslint-disable-next-line no-console console.log(`[Chord Analytics DEBUG]: Restoring ${validEvents.length} events from sessionStorage`); } // Add restored events to the queue validEvents.forEach((event) => { this.consentEventQueue.push({ type: event.type, args: event.args, timestamp: event.timestamp, // eslint-disable-next-line @typescript-eslint/no-empty-function resolve: () => { }, }); }); // Start watching for consent if we restored events if (validEvents.length > 0 && !this.consentWatcherActive) { this.consentWatcherActive = true; this.watchForConsentAndFlush(); } } catch (error) { this.logger('[Chord Analytics]: Error restoring queue from storage:', error); // Clear corrupted storage try { window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY); } catch (_a) { // Ignore cleanup errors } } }; /** * Clear the persisted queue from sessionStorage after successful flush. */ this.clearQueueFromStorage = () => { if (typeof window === 'undefined' || !window.sessionStorage) return; try { window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY); } catch (error) { this.logger('[Chord Analytics]: Error clearing queue from storage:', error); } }; /** * Set debug mode for Chord Analytics * @param enabled - Whether debug mode should be enabled */ // eslint-disable-next-line class-methods-use-this this.setDebugMode = (enabled) => { if (window === null || window === void 0 ? void 0 : window.localStorage) { window.localStorage.setItem(DEBUG_MODE_STORAGE_KEY, enabled.toString()); } }; /* * Return the CDP instance provided in `options.cdp`. */ this.cdp = () => { var _a; const a = (_a = this.options) === null || _a === void 0 ? void 0 : _a.cdp; if (typeof a === 'function') return a(); return a; }; /* * Return the Chord CDP instance or queue. */ this.ccdp = () => { const { cdpDomain, cdpWriteKey, namespace } = this.options; if (!cdpDomain || !cdpWriteKey) return null; return (window === null || window === void 0 ? void 0 : window[`_${namespace}`]) || cdpQueue(namespace); }; /* * Log a message to the console if `options.enableLogging` is true. */ this.logger = (message, ...optionalParams) => { if (!this.options.enableLogging) return; // eslint-disable-next-line no-console console.log(message, ...optionalParams); }; this.isResetting = () => { return this.resetCompletePromise !== null; }; // Single gate for all outgoing operations. Returns a Promise only when there // is actually something to wait for, preserving synchronous execution in the // common case where CDP is ready and no reset is in flight. this.waitForReadyState = () => { const cdp = this.cdpReadyPromise; const reset = this.resetCompletePromise; if (!cdp && !reset) return undefined; // eslint-disable-next-line @typescript-eslint/no-empty-function if (cdp && reset) return Promise.all([cdp, reset]).then(() => { }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return cdp !== null && cdp !== void 0 ? cdp : reset; }; /** * Wait for consent manager to be ready (no timeout - queues indefinitely). * Uses a shared promise so multiple events share the same watcher. */ this.waitForConsent = () => __awaiter(this, void 0, void 0, function* () { if (!this.consentConfig) return; if (this.consentConfig.isConsentReady()) return; // If already waiting, return the existing promise if (this.consentReadyPromise) { yield this.consentReadyPromise; return; } // Start a new consent watcher - polls until consent is ready this.consentReadyPromise = new Promise((resolve) => { const interval = setInterval(() => { var _a; if ((_a = this.consentConfig) === null || _a === void 0 ? void 0 : _a.isConsentReady()) { clearInterval(interval); this.consentReadyPromise = null; resolve(true); } }, 100); }); yield this.consentReadyPromise; }); /** * Queue an event to be sent when consent is ready. * Persists to sessionStorage for recovery on instance recreation. * Starts a consent watcher if not already active. */ this.queueEventForConsent = (type, args) => { return new Promise((resolve) => { this.consentEventQueue.push({ type, args, timestamp: Date.now(), resolve, }); // Persist queue to sessionStorage for recovery on instance recreation this.persistQueueToStorage(); // Start watching for consent if not already if (!this.consentWatcherActive) { this.consentWatcherActive = true; this.watchForConsentAndFlush(); } }); }; /** * Watch for consent to become ready, then flush the queue. */ this.watchForConsentAndFlush = () => __awaiter(this, void 0, void 0, function* () { yield this.waitForConsent(); yield this.flushConsentQueue(); this.consentWatcherActive = false; }); /** * Flush all queued events now that consent is ready. * Clears sessionStorage after successful flush. */ this.flushConsentQueue = () => __awaiter(this, void 0, void 0, function* () { const queue = [...this.consentEventQueue]; this.consentEventQueue = []; // Clear persisted queue from storage since we're processing them now this.clearQueueFromStorage(); if (this.options.enableLogging && this.options.debug) { // eslint-disable-next-line no-console console.log(`[Chord Analytics DEBUG]: Flushing ${queue.length} queued events`); } const sendPromises = queue.map((item) => __awaiter(this, void 0, void 0, function* () { try { switch (item.type) { case 'track': yield this.sendTrackEvent(item.args[0], item.args[1], item.args[2]); break; case 'page': yield this.sendPageEvent(item.args[0], item.args[1]); break; case 'identify': yield this.sendIdentifyEvent(item.args[0], item.args[1], item.args[2]); break; default: break; } } catch (error) { this.logger('[Chord Analytics]: Error sending queued event:', error); } item.resolve(); })); yield Promise.all(sendPromises); }); /** * Configure CDP with fresh consent categories before each event. * Uses Jitsu's configure() to update privacy.consentCategories. */ this.configureConsentOnCdp = () => { if (!this.consentConfig) return; const categories = this.consentConfig.getCategories(); const privacyConfig = { privacy: { consentCategories: categories } }; const a = this.ccdp(); if (a && typeof a.configure === 'function') { a.configure(privacyConfig); } const b = this.cdp(); if (b && typeof b.configure === 'function') { b.configure(privacyConfig); } }; this.validate = (event, props) => { if (!event) { this.logger('No event name provided'); return [{ success: false }]; } const schema = event && eventSchemas[event]; if (!schema) return [{ success: true, data: props }]; return schema.map((s) => s.safeParse(props)); }; /** * Generate the event `meta` property. */ this.meta = () => { var _a, _b, _c, _d, _e, _f; return Object.assign(Object.assign({}, this.options.metadata), { ownership: { oms_id: (_b = (_a = this.options.metadata) === null || _a === void 0 ? void 0 : _a.ownership) === null || _b === void 0 ? void 0 : _b.omsId, store_id: (_d = (_c = this.options.metadata) === null || _c === void 0 ? void 0 : _c.ownership) === null || _d === void 0 ? void 0 : _d.storeId, tenant_id: (_f = (_e = this.options.metadata) === null || _e === void 0 ? void 0 : _e.ownership) === null || _f === void 0 ? void 0 : _f.tenantId, }, version: { // TODO: make this dynamic major: 3, minor: 0, patch: 0, } }); }; // eslint-disable-next-line class-methods-use-this this.addSourceMiddleware = (func) => { this.middleware.push(func); }; /** * Send a `track` event to the CDP with any event name and properties. * If consent is not ready, the event is queued until consent is available. */ this.track = (event, props, options) => __awaiter(this, void 0, void 0, function* () { if (this.options.enableLogging && this.options.debug) { // eslint-disable-next-line no-console console.log(`[Chord Analytics DEBUG]: track() called with event: "${event}"`); } if (!event) this.logger('No event name provided'); const gate = this.waitForReadyState(); if (gate) yield gate; // If consent is configured but not ready, queue the event if (this.consentConfig && !this.consentConfig.isConsentReady()) { if (this.options.enableLogging && this.options.debug) { // eslint-disable-next-line no-console console.log(`[Chord Analytics DEBUG]: track("${event}") consent not ready, queuing event`); } return this.queueEventForConsent('track', [event, props, options]); } if (this.options.enableLogging && this.options.debug) { // eslint-disable-next-line no-console console.log(`[Chord Analytics DEBUG]: track("${event}") sending event now`); } // Consent ready (or not configured) - send immediately return this.sendTrackEvent(event, props, options); }); /** * Internal method to actually send a track event to CDP. */ this.sendTrackEvent = (event, props, options) => __awaiter(this, void 0, void 0, function* () { const formattedProps = this.options.stripNull && props ? pruneNullValues(props) : props; let middlewareProps = {}; if (this.middleware.length > 0) { const searchParams = typeof window !== 'undefined' ? window.location.search : ''; const urlSearchParams = new URLSearchParams(searchParams); const searchData = Object.fromEntries(urlSearchParams.entries()); // process middleware synchronously this.middleware.forEach((func) => { try { const result = func({ payload: { obj: { properties: props, event, context: { page: { search: searchParams, searchParams: searchData, }, }, }, }, next: (_) => _, }); if (result && typeof result === 'object') { middlewareProps = Object.assign(Object.assign({}, middlewareProps), result); } } catch (error) { console.warn('Middleware error:', error); } }); } const finalEventProps = Object.assign(Object.assign(Object.assign({}, formattedProps), middlewareProps), { meta: this.meta() }); if (this.options.debug) { this.validate(event, finalEventProps).forEach((valid) => { if (!valid.success) { this.logger('Chord tracking plan violation', valid.error); } }); } // Configure CDP with fresh consent before sending this.configureConsentOnCdp(); yield new Promise((resolve) => { const cdpPromises = []; try { const a = this.ccdp(); if (a && typeof a.track === 'function') { cdpPromises.push(new Promise((cdpResolve) => { a.track(event, finalEventProps, options, () => cdpResolve()); })); } const b = this.cdp(); if (b && typeof b.track === 'function') { cdpPromises.push(new Promise((cdpResolve) => { b.track(event, finalEventProps, options, () => cdpResolve()); })); } if (cdpPromises.length > 0) { Promise.allSettled(cdpPromises).then(() => resolve()); } else { resolve(); } } catch (error) { this.logger(error); resolve(); } }); }); /** * Send an `identify` event to the CDP with user id and traits. * If consent is not ready, the event is queued until consent is available. */ this.identify = (userIdOrTraits, traitsOrOptions, options) => __awaiter(this, void 0, void 0, function* () { const gate = this.waitForReadyState(); if (gate) yield gate; // If consent is configured but not ready, queue the event if (this.consentConfig && !this.consentConfig.isConsentReady()) { return this.queueEventForConsent('identify', [ userIdOrTraits, traitsOrOptions, options, ]); } // Consent ready (or not configured) - send immediately return this.sendIdentifyEvent(userIdOrTraits, traitsOrOptions, options); }); /** * Internal method to actually send an identify event to CDP. */ this.sendIdentifyEvent = (userIdOrTraits, traitsOrOptions, options) => __awaiter(this, void 0, void 0, function* () { // Parse arguments to extract userId, traits, and options let userId; let traits; let opts; if (typeof userIdOrTraits === 'string') { userId = userIdOrTraits; traits = traitsOrOptions; opts = options; } else { traits = userIdOrTraits; opts = traitsOrOptions; } // Configure CDP with fresh consent before sending this.configureConsentOnCdp(); try { const identifyPromises = []; const a = this.ccdp(); if (a && typeof a.identify === 'function') { identifyPromises.push(new Promise((resolve) => { if (userId) { a.identify(userId, traits, opts, () => resolve()); } else { a.identify(traits, opts, () => resolve()); } })); } const b = this.cdp(); if (b && typeof b.identify === 'function') { identifyPromises.push(new Promise((resolve) => { if (userId) { b.identify(userId, traits, opts, () => resolve()); } else { b.identify(traits, opts, () => resolve()); } })); } const results = yield Promise.allSettled(identifyPromises); this.logger('identify event sent', results); } catch (error) { this.logger(error); } }); /** * Send a `page` event to the CDP. * If consent is not ready, the event is queued until consent is available. */ this.page = (options, props = {}) => __awaiter(this, void 0, void 0, function* () { const gate = this.waitForReadyState(); if (gate) yield gate; // If consent is configured but not ready, queue the event if (this.consentConfig && !this.consentConfig.isConsentReady()) { return this.queueEventForConsent('page', [options, props]); } // Consent ready (or not configured) - send immediately return this.sendPageEvent(options, props); }); /** * Internal method to actually send a page event to CDP. */ this.sendPageEvent = (options, props = {}) => __awaiter(this, void 0, void 0, function* () { // Configure CDP with fresh consent before sending this.configureConsentOnCdp(); yield new Promise((resolve) => { const pagePromises = []; try { const a = this.ccdp(); if (a && typeof a.page === 'function') { pagePromises.push(new Promise((cdpResolve) => { a.page(Object.assign(Object.assign({}, props), { meta: this.meta() }), options, () => cdpResolve()); })); } const b = this.cdp(); if (b && typeof b.page === 'function') { pagePromises.push(new Promise((cdpResolve) => { b.page(Object.assign(Object.assign({}, props), { meta: this.meta() }), options, () => cdpResolve()); })); } if (pagePromises.length > 0) { Promise.allSettled(pagePromises).then((results) => { this.logger('page event sent', results); resolve(); }); } else { resolve(); } } catch (error) { this.logger(error); resolve(); } }); }); // Coalesces concurrent reset() calls — a second caller gets the same in-flight // Promise rather than triggering a duplicate reset. this.reset = () => { if (!this.resetCompletePromise) { this.resetCompletePromise = this.performReset(); } return this.resetCompletePromise; }; this.performReset = () => __awaiter(this, void 0, void 0, function* () { var _e; try { const a = this.ccdp(); if (a && typeof a.reset === 'function') { a.reset(); } const b = this.cdp(); if (b && typeof b.reset === 'function') { b.reset(); } const maxAttempts = 50; let attempts = 0; while (attempts < maxAttempts) { // eslint-disable-next-line yield new Promise((resolve) => setTimeout(resolve, 10)); try { const userState = ((_e = a === null || a === void 0 ? void 0 : a.user) === null || _e === void 0 ? void 0 : _e.call(a)) || {}; if (userState.userId === null || userState.userId === undefined) { break; } } catch (_f) { break; } attempts++; } } catch (error) { this.logger(error); } finally { this.resetCompletePromise = null; } }); /** * Handler for postMessage events from the Shopify Web Pixel sandbox. * Bound to the instance for proper `this` context and cleanup. */ this.webPixelMessageHandler = null; /** * Set up a listener for postMessage events from the Shopify Web Pixel sandbox. * This allows the web pixel (running in Shopify's sandboxed iframe) * to forward analytics events to this ChordAnalytics instance on the storefront. * * The web pixel sends messages in the format: * - { source: 'chord-web-pixel', type: 'track', payload: [eventName, properties] } * - { source: 'chord-web-pixel', type: 'identify', payload: [userId, traits] } * - { source: 'chord-web-pixel', type: 'page', payload: [properties] } * * @returns A cleanup function to remove the listener */ this.setupWebPixelListener = () => { if (typeof window === 'undefined') { this.logger('[Chord Analytics]: setupWebPixelListener called in non-browser environment'); // eslint-disable-next-line @typescript-eslint/no-empty-function return () => { }; } // Remove existing listener if one exists if (this.webPixelMessageHandler) { window.removeEventListener('message', this.webPixelMessageHandler); } this.webPixelMessageHandler = (event) => { // Validate message structure if (!event.data || typeof event.data !== 'object') return; if (event.data.source !== 'chord-web-pixel') return; // Validate origin — only accept messages from the same hostname or from // null-origin sandboxed iframes (Shopify web pixel sandbox has origin "null") try { if (event.origin && event.origin !== 'null') { const eventHostname = new URL(event.origin).hostname; if (eventHostname !== window.location.hostname) return; } } catch (_a) { return; } const message = event.data; if (this.options.enableLogging && this.options.debug) { // eslint-disable-next-line no-console console.log('[Chord Analytics DEBUG]: Received web pixel message:', message.type, message.payload); } try { switch (message.type) { case 'track': { const [eventName, properties] = message.payload; if (eventName) { Promise.resolve(this.track(eventName, properties)).catch((err) => this.logger('[Chord Analytics]: Error in web pixel track:', err)); } break; } case 'identify': { const [userId, traits] = message.payload; Promise.resolve(this.identify(userId, traits)).catch((err) => this.logger('[Chord Analytics]: Error in web pixel identify:', err)); break; } case 'page': { const [properties] = message.payload; Promise.resolve(this.page(undefined, properties)).catch((err) => this.logger('[Chord Analytics]: Error in web pixel page:', err)); break; } default: this.logger(`[Chord Analytics]: Unknown web pixel message type: ${message.type}`); } } catch (error) { this.logger('[Chord Analytics]: Error processing web pixel message:', error); } }; window.addEventListener('message', this.webPixelMessageHandler); this.logger('[Chord Analytics]: Web pixel listener initialized'); // Return cleanup function return () => { if (this.webPixelMessageHandler) { window.removeEventListener('message', this.webPixelMessageHandler); this.webPixelMessageHandler = null; this.logger('[Chord Analytics]: Web pixel listener removed'); } }; }; // TODO: Add support for props.products this.trackCartViewed = (props, options) => __awaiter(this, void 0, void 0, function* () { var _g, _h, _j, _k, _l, _m, _o, _p, _q, _r; const cart = (_k = (_j = (_h = (_g = this.options) === null || _g === void 0 ? void 0 : _g.formatters) === null || _h === void 0 ? void 0 : _h.objects) === null || _j === void 0 ? void 0 : _j.cart) === null || _k === void 0 ? void 0 : _k.call(_j, { cart: props === null || props === void 0 ? void 0 : props.cart, }); const payload = { cart_id: cart === null || cart === void 0 ? void 0 : cart.cart_id, currency: cart === null || cart === void 0 ? void 0 : cart.currency, products: cart === null || cart === void 0 ? void 0 : cart.products, value: cart === null || cart === void 0 ? void 0 : cart.value, }; if ((_o = (_m = (_l = this.options) === null || _l === void 0 ? void 0 : _l.formatters) === null || _m === void 0 ? void 0 : _m.events) === null || _o === void 0 ? void 0 : _o.cartViewed) { const formattedPayload = (_r = (_q = (_p = this.options) === null || _p === void 0 ? void 0 : _p.formatters) === null || _q === void 0 ? void 0 : _q.events) === null || _r === void 0 ? void 0 : _r.cartViewed(props, payload); return this.track('Cart Viewed', formattedPayload, options); } return this.track('Cart Viewed', payload, options); }); // TODO: Add support for props.products this.trackCheckoutStarted = (props, options) => __awaiter(this, void 0, void 0, function* () { var _s, _t, _u, _v, _w, _x, _y; const formatter = (_u = (_t = (_s = this.options) === null || _s === void 0 ? void 0 : _s.formatters) === null || _t === void 0 ? void 0 : _t.events) === null || _u === void 0 ? void 0 : _u.checkoutStarted; const checkout = (_y = (_x = (_w = (_v = this.options) === null || _v === void 0 ? void 0 : _v.formatters) === null || _w === void 0 ? void 0 : _w.objects) === null || _x === void 0 ? void 0 : _x.checkout) === null || _y === void 0 ? void 0 : _y.call(_x, { checkout: props === null || props === void 0 ? void 0 : props.checkout, }); const payload = checkout; if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Checkout Started', formattedPayload, options); } return this.track('Checkout Started', payload, options); }); this.trackCheckoutStepCompleted = (props, options) => __awaiter(this, void 0, void 0, function* () { var _z, _0, _1; const formatter = (_1 = (_0 = (_z = this.options) === null || _z === void 0 ? void 0 : _z.formatters) === null || _0 === void 0 ? void 0 : _0.events) === null || _1 === void 0 ? void 0 : _1.checkoutStepCompleted; const payload = { checkout_id: props === null || props === void 0 ? void 0 : props.checkoutId, payment_method: props === null || props === void 0 ? void 0 : props.paymentMethod, shipping_method: props === null || props === void 0 ? void 0 : props.shippingMethod, step: props === null || props === void 0 ? void 0 : props.step, }; if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Checkout Step Completed', formattedPayload, options); } return this.track('Checkout Step Completed', payload, options); }); this.trackCheckoutStepViewed = (props, options) => __awaiter(this, void 0, void 0, function* () { var _2, _3, _4; const formatter = (_4 = (_3 = (_2 = this.options) === null || _2 === void 0 ? void 0 : _2.formatters) === null || _3 === void 0 ? void 0 : _3.events) === null || _4 === void 0 ? void 0 : _4.checkoutStepViewed; const payload = { checkout_id: props === null || props === void 0 ? void 0 : props.checkoutId, payment_method: props === null || props === void 0 ? void 0 : props.paymentMethod, shipping_method: props === null || props === void 0 ? void 0 : props.shippingMethod, step: props === null || props === void 0 ? void 0 : props.step, }; if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Checkout Step Viewed', formattedPayload, options); } return this.track('Checkout Step Viewed', payload, options); }); this.trackCouponApplied = (props, options) => __awaiter(this, void 0, void 0, function* () { var _5, _6, _7; const formatter = (_7 = (_6 = (_5 = this.options) === null || _5 === void 0 ? void 0 : _5.formatters) === null || _6 === void 0 ? void 0 : _6.events) === null || _7 === void 0 ? void 0 : _7.couponApplied; const payload = { cart_id: props === null || props === void 0 ? void 0 : props.cartId, coupon_id: props === null || props === void 0 ? void 0 : props.couponId, coupon_name: props === null || props === void 0 ? void 0 : props.couponName, discount: props === null || props === void 0 ? void 0 : props.discount, order_id: props === null || props === void 0 ? void 0 : props.orderId, }; if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Coupon Applied', formattedPayload, options); } return this.track('Coupon Applied', payload, options); }); this.trackCouponDenied = (props, options) => __awaiter(this, void 0, void 0, function* () { var _8, _9, _10; const formatter = (_10 = (_9 = (_8 = this.options) === null || _8 === void 0 ? void 0 : _8.formatters) === null || _9 === void 0 ? void 0 : _9.events) === null || _10 === void 0 ? void 0 : _10.couponDenied; const payload = { cart_id: props === null || props === void 0 ? void 0 : props.cartId, coupon_id: props === null || props === void 0 ? void 0 : props.couponId, coupon_name: props === null || props === void 0 ? void 0 : props.couponName, order_id: props === null || props === void 0 ? void 0 : props.orderId, reason: props === null || props === void 0 ? void 0 : props.reason, }; if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Coupon Denied', formattedPayload, options); } return this.track('Coupon Denied', payload, options); }); this.trackCouponEntered = (props, options) => __awaiter(this, void 0, void 0, function* () { var _11, _12, _13; const formatter = (_13 = (_12 = (_11 = this.options) === null || _11 === void 0 ? void 0 : _11.formatters) === null || _12 === void 0 ? void 0 : _12.events) === null || _13 === void 0 ? void 0 : _13.couponEntered; const payload = { cart_id: props === null || props === void 0 ? void 0 : props.cartId, coupon_id: props === null || props === void 0 ? void 0 : props.couponId, coupon_name: props === null || props === void 0 ? void 0 : props.couponName, order_id: props === null || props === void 0 ? void 0 : props.orderId, }; if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Coupon Entered', formattedPayload, options); } return this.track('Coupon Entered', payload, options); }); this.trackCouponRemoved = (props, options) => __awaiter(this, void 0, void 0, function* () { var _14, _15, _16; const formatter = (_16 = (_15 = (_14 = this.options) === null || _14 === void 0 ? void 0 : _14.formatters) === null || _15 === void 0 ? void 0 : _15.events) === null || _16 === void 0 ? void 0 : _16.couponRemoved; const payload = { cart_id: props === null || props === void 0 ? void 0 : props.cartId, coupon_id: props === null || props === void 0 ? void 0 : props.couponId, coupon_name: props === null || props === void 0 ? void 0 : props.couponName, discount: props === null || props === void 0 ? void 0 : props.discount, order_id: props === null || props === void 0 ? void 0 : props.orderId, }; if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Coupon Removed', formattedPayload, options); } return this.track('Coupon Removed', payload, options); }); this.trackEmailCaptured = (props, options) => __awaiter(this, void 0, void 0, function* () { var _17, _18, _19; const formatter = (_19 = (_18 = (_17 = this.options) === null || _17 === void 0 ? void 0 : _17.formatters) === null || _18 === void 0 ? void 0 : _18.events) === null || _19 === void 0 ? void 0 : _19.emailCaptured; const payload = { email: props === null || props === void 0 ? void 0 : props.email, placement_component: props === null || props === void 0 ? void 0 : props.placementComponent, placement_page: props === null || props === void 0 ? void 0 : props.placementPage, }; if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Email Captured', formattedPayload, options); } return this.track('Email Captured', payload, options); }); this.trackProductAdded = (props, options) => __awaiter(this, void 0, void 0, function* () { var _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31; const formatter = (_22 = (_21 = (_20 = this.options) === null || _20 === void 0 ? void 0 : _20.formatters) === null || _21 === void 0 ? void 0 : _21.events) === null || _22 === void 0 ? void 0 : _22.productAdded; const product = (_26 = (_25 = (_24 = (_23 = this.options) === null || _23 === void 0 ? void 0 : _23.formatters) === null || _24 === void 0 ? void 0 : _24.objects) === null || _25 === void 0 ? void 0 : _25.product) === null || _26 === void 0 ? void 0 : _26.call(_25, props === null || props === void 0 ? void 0 : props.product); const cart = (_30 = (_29 = (_28 = (_27 = this.options) === null || _27 === void 0 ? void 0 : _27.formatters) === null || _28 === void 0 ? void 0 : _28.objects) === null || _29 === void 0 ? void 0 : _29.cart) === null || _30 === void 0 ? void 0 : _30.call(_29, { cart: props === null || props === void 0 ? void 0 : props.cart, }); const quantity = ((_31 = props === null || props === void 0 ? void 0 : props.product) === null || _31 === void 0 ? void 0 : _31.quantity) || 1; const payload = Object.assign(Object.assign({}, product), { cart_id: cart.cart_id, currency: cart.currency, products: [Object.assign(Object.assign({}, product), { quantity })], total: product.price * (quantity || 1), value: product.price * (quantity || 1) }); if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Product Added', formattedPayload, options); } return this.track('Product Added', payload, options); }); this.trackProductClicked = (props, options) => __awaiter(this, void 0, void 0, function* () { var _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44; const formatter = (_34 = (_33 = (_32 = this.options) === null || _32 === void 0 ? void 0 : _32.formatters) === null || _33 === void 0 ? void 0 : _33.events) === null || _34 === void 0 ? void 0 : _34.productClicked; const product = (_38 = (_37 = (_36 = (_35 = this.options) === null || _35 === void 0 ? void 0 : _35.formatters) === null || _36 === void 0 ? void 0 : _36.objects) === null || _37 === void 0 ? void 0 : _37.product) === null || _38 === void 0 ? void 0 : _38.call(_37, props === null || props === void 0 ? void 0 : props.product); const cart = (_42 = (_41 = (_40 = (_39 = this.options) === null || _39 === void 0 ? void 0 : _39.formatters) === null || _40 === void 0 ? void 0 : _40.objects) === null || _41 === void 0 ? void 0 : _41.cart) === null || _42 === void 0 ? void 0 : _42.call(_41, { cart: props === null || props === void 0 ? void 0 : props.cart, }); const quantity = ((_43 = props === null || props === void 0 ? void 0 : props.product) === null || _43 === void 0 ? void 0 : _43.quantity) || 1; const payload = Object.assign(Object.assign({}, product), { cart_id: cart.cart_id, currency: cart.currency, item_list_id: (_44 = props === null || props === void 0 ? void 0 : props.listId) === null || _44 === void 0 ? void 0 : _44.toString(), item_list_name: props === null || props === void 0 ? void 0 : props.listName, products: [Object.assign(Object.assign({}, product), { quantity })] }); if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Product Clicked', formattedPayload, options); } return this.track('Product Clicked', payload, options); }); this.trackVariantClicked = (props, options) => __awaiter(this, void 0, void 0, function* () { var _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56; const formatter = (_47 = (_46 = (_45 = this.options) === null || _45 === void 0 ? void 0 : _45.formatters) === null || _46 === void 0 ? void 0 : _46.events) === null || _47 === void 0 ? void 0 : _47.variantClicked; const product = (_51 = (_50 = (_49 = (_48 = this.options) === null || _48 === void 0 ? void 0 : _48.formatters) === null || _49 === void 0 ? void 0 : _49.objects) === null || _50 === void 0 ? void 0 : _50.product) === null || _51 === void 0 ? void 0 : _51.call(_50, props === null || props === void 0 ? void 0 : props.product); const cart = (_55 = (_54 = (_53 = (_52 = this.options) === null || _52 === void 0 ? void 0 : _52.formatters) === null || _53 === void 0 ? void 0 : _53.objects) === null || _54 === void 0 ? void 0 : _54.cart) === null || _55 === void 0 ? void 0 : _55.call(_54, { cart: props === null || props === void 0 ? void 0 : props.cart, }); const quantity = ((_56 = props === null || props === void 0 ? void 0 : props.product) === null || _56 === void 0 ? void 0 : _56.quantity) || 1; const payload = Object.assign(Object.assign({}, product), { cart_id: cart.cart_id, currency: cart.currency, quantity: quantity, line_item_id: props === null || props === void 0 ? void 0 : props.lineItemId, coupon: props === null || props === void 0 ? void 0 : props.coupon }); if (typeof formatter === 'function') { const formattedPayload = formatter(props, payload); return this.track('Variant Clicked', formattedPayload, options); } return this.track('Variant Clicked', payload, options); }); this.trackProductListFiltered = (props, options) => __awaiter(this, void 0, void 0, function* () { var _57, _58, _59, _60, _61; const formatter = (_59 = (_58 = (_57 = this.options) === null || _57 === void 0 ? void 0 : _57.formatters) === null || _58 === void 0 ? void 0 : _58.events) === null || _59 === void 0 ? void 0 : _59.productListFiltered; const payload = { category: props === null || props === void 0 ? void 0 : props.category, filters: props === null || props === void 0 ? void 0 : props.filters, list_id: (_60 = props === null || props === void 0 ? void 0 :