UNPKG

@chordcommerce/analytics

Version:

Chord Commerce event tracking

956 lines (955 loc) 96.2 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; 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()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChordAnalytics = void 0; var cdp_js_1 = require("./cdp.js"); var index_js_1 = require("./consent/index.js"); var index_js_2 = require("../validators/index.js"); var utils_js_1 = require("../utils.js"); var DEBUG_MODE_STORAGE_KEY = 'chordcommerce/analytics/debug'; var CONSENT_QUEUE_STORAGE_KEY = 'chordcommerce/analytics/consent-queue'; var CONSENT_QUEUE_TTL_MS = 5 * 60 * 1000; // 5 minutes var CONSENT_QUEUE_MAX_SIZE_BYTES = 100 * 1024; // 100KB max queue size var CONSENT_QUEUE_MAX_EVENTS = 50; // Max number of events to queue var ChordAnalytics = /** @class */ (function () { function ChordAnalytics(options) { var _this = this; 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 = function () { if (typeof window === 'undefined' || !window.sessionStorage) return; try { // Limit number of events to prevent unbounded queue growth var eventsToStore = _this.consentEventQueue.slice(-CONSENT_QUEUE_MAX_EVENTS); var persistedEvents = eventsToStore.map(function (item) { return ({ type: item.type, args: item.args, timestamp: item.timestamp, }); }); var serialized = JSON.stringify(persistedEvents); // Check serialized size before attempting to store var sizeInBytes = new Blob([serialized]).size; if (sizeInBytes > CONSENT_QUEUE_MAX_SIZE_BYTES) { _this.logger("[Chord Analytics]: Consent queue too large (".concat(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 = function () { if (typeof window === 'undefined' || !window.sessionStorage) return; try { var stored = window.sessionStorage.getItem(CONSENT_QUEUE_STORAGE_KEY); if (!stored) return; var persistedEvents = void 0; 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; } var now_1 = Date.now(); // Filter out expired events and events with invalid structure var validEvents = persistedEvents.filter(function (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_1 - 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 ".concat(validEvents.length, " events from sessionStorage")); } // Add restored events to the queue validEvents.forEach(function (event) { _this.consentEventQueue.push({ type: event.type, args: event.args, timestamp: event.timestamp, // eslint-disable-next-line @typescript-eslint/no-empty-function resolve: function () { }, }); }); // 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 = function () { 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 = function (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 = function () { var _a; var 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 = function () { var _a = _this.options, cdpDomain = _a.cdpDomain, cdpWriteKey = _a.cdpWriteKey, namespace = _a.namespace; if (!cdpDomain || !cdpWriteKey) return null; return (window === null || window === void 0 ? void 0 : window["_".concat(namespace)]) || (0, cdp_js_1.cdpQueue)(namespace); }; /* * Log a message to the console if `options.enableLogging` is true. */ this.logger = function (message) { var optionalParams = []; for (var _i = 1; _i < arguments.length; _i++) { optionalParams[_i - 1] = arguments[_i]; } if (!_this.options.enableLogging) return; // eslint-disable-next-line no-console console.log.apply(console, __spreadArray([message], optionalParams, false)); }; this.isResetting = function () { 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 = function () { var cdp = _this.cdpReadyPromise; var 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(function () { }); // 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 = function () { return __awaiter(_this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.consentConfig) return [2 /*return*/]; if (this.consentConfig.isConsentReady()) return [2 /*return*/]; if (!this.consentReadyPromise) return [3 /*break*/, 2]; return [4 /*yield*/, this.consentReadyPromise]; case 1: _a.sent(); return [2 /*return*/]; case 2: // Start a new consent watcher - polls until consent is ready this.consentReadyPromise = new Promise(function (resolve) { var interval = setInterval(function () { var _a; if ((_a = _this.consentConfig) === null || _a === void 0 ? void 0 : _a.isConsentReady()) { clearInterval(interval); _this.consentReadyPromise = null; resolve(true); } }, 100); }); return [4 /*yield*/, this.consentReadyPromise]; case 3: _a.sent(); return [2 /*return*/]; } }); }); }; /** * 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 = function (type, args) { return new Promise(function (resolve) { _this.consentEventQueue.push({ type: type, args: args, timestamp: Date.now(), resolve: 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 = function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.waitForConsent()]; case 1: _a.sent(); return [4 /*yield*/, this.flushConsentQueue()]; case 2: _a.sent(); this.consentWatcherActive = false; return [2 /*return*/]; } }); }); }; /** * Flush all queued events now that consent is ready. * Clears sessionStorage after successful flush. */ this.flushConsentQueue = function () { return __awaiter(_this, void 0, void 0, function () { var queue, sendPromises; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: queue = __spreadArray([], this.consentEventQueue, true); 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 ".concat(queue.length, " queued events")); } sendPromises = queue.map(function (item) { return __awaiter(_this, void 0, void 0, function () { var _a, error_1; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 9, , 10]); _a = item.type; switch (_a) { case 'track': return [3 /*break*/, 1]; case 'page': return [3 /*break*/, 3]; case 'identify': return [3 /*break*/, 5]; } return [3 /*break*/, 7]; case 1: return [4 /*yield*/, this.sendTrackEvent(item.args[0], item.args[1], item.args[2])]; case 2: _b.sent(); return [3 /*break*/, 8]; case 3: return [4 /*yield*/, this.sendPageEvent(item.args[0], item.args[1])]; case 4: _b.sent(); return [3 /*break*/, 8]; case 5: return [4 /*yield*/, this.sendIdentifyEvent(item.args[0], item.args[1], item.args[2])]; case 6: _b.sent(); return [3 /*break*/, 8]; case 7: return [3 /*break*/, 8]; case 8: return [3 /*break*/, 10]; case 9: error_1 = _b.sent(); this.logger('[Chord Analytics]: Error sending queued event:', error_1); return [3 /*break*/, 10]; case 10: item.resolve(); return [2 /*return*/]; } }); }); }); return [4 /*yield*/, Promise.all(sendPromises)]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; /** * Configure CDP with fresh consent categories before each event. * Uses Jitsu's configure() to update privacy.consentCategories. */ this.configureConsentOnCdp = function () { if (!_this.consentConfig) return; var categories = _this.consentConfig.getCategories(); var privacyConfig = { privacy: { consentCategories: categories } }; var a = _this.ccdp(); if (a && typeof a.configure === 'function') { a.configure(privacyConfig); } var b = _this.cdp(); if (b && typeof b.configure === 'function') { b.configure(privacyConfig); } }; this.validate = function (event, props) { if (!event) { _this.logger('No event name provided'); return [{ success: false }]; } var schema = event && index_js_2.eventSchemas[event]; if (!schema) return [{ success: true, data: props }]; return schema.map(function (s) { return s.safeParse(props); }); }; /** * Generate the event `meta` property. */ this.meta = function () { var _a, _b, _c, _d, _e, _f; return __assign(__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 = function (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 = function (event, props, options) { return __awaiter(_this, void 0, void 0, function () { var gate; return __generator(this, function (_a) { switch (_a.label) { case 0: if (this.options.enableLogging && this.options.debug) { // eslint-disable-next-line no-console console.log("[Chord Analytics DEBUG]: track() called with event: \"".concat(event, "\"")); } if (!event) this.logger('No event name provided'); gate = this.waitForReadyState(); if (!gate) return [3 /*break*/, 2]; return [4 /*yield*/, gate // If consent is configured but not ready, queue the event ]; case 1: _a.sent(); _a.label = 2; case 2: // 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(\"".concat(event, "\") consent not ready, queuing event")); } return [2 /*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(\"".concat(event, "\") sending event now")); } // Consent ready (or not configured) - send immediately return [2 /*return*/, this.sendTrackEvent(event, props, options)]; } }); }); }; /** * Internal method to actually send a track event to CDP. */ this.sendTrackEvent = function (event, props, options) { return __awaiter(_this, void 0, void 0, function () { var formattedProps, middlewareProps, searchParams_1, urlSearchParams, searchData_1, finalEventProps; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: formattedProps = this.options.stripNull && props ? (0, utils_js_1.pruneNullValues)(props) : props; middlewareProps = {}; if (this.middleware.length > 0) { searchParams_1 = typeof window !== 'undefined' ? window.location.search : ''; urlSearchParams = new URLSearchParams(searchParams_1); searchData_1 = Object.fromEntries(urlSearchParams.entries()); // process middleware synchronously this.middleware.forEach(function (func) { try { var result = func({ payload: { obj: { properties: props, event: event, context: { page: { search: searchParams_1, searchParams: searchData_1, }, }, }, }, next: function (_) { return _; }, }); if (result && typeof result === 'object') { middlewareProps = __assign(__assign({}, middlewareProps), result); } } catch (error) { console.warn('Middleware error:', error); } }); } finalEventProps = __assign(__assign(__assign({}, formattedProps), middlewareProps), { meta: this.meta() }); if (this.options.debug) { this.validate(event, finalEventProps).forEach(function (valid) { if (!valid.success) { _this.logger('Chord tracking plan violation', valid.error); } }); } // Configure CDP with fresh consent before sending this.configureConsentOnCdp(); return [4 /*yield*/, new Promise(function (resolve) { var cdpPromises = []; try { var a_1 = _this.ccdp(); if (a_1 && typeof a_1.track === 'function') { cdpPromises.push(new Promise(function (cdpResolve) { a_1.track(event, finalEventProps, options, function () { return cdpResolve(); }); })); } var b_1 = _this.cdp(); if (b_1 && typeof b_1.track === 'function') { cdpPromises.push(new Promise(function (cdpResolve) { b_1.track(event, finalEventProps, options, function () { return cdpResolve(); }); })); } if (cdpPromises.length > 0) { Promise.allSettled(cdpPromises).then(function () { return resolve(); }); } else { resolve(); } } catch (error) { _this.logger(error); resolve(); } })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; /** * 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 = function (userIdOrTraits, traitsOrOptions, options) { return __awaiter(_this, void 0, void 0, function () { var gate; return __generator(this, function (_a) { switch (_a.label) { case 0: gate = this.waitForReadyState(); if (!gate) return [3 /*break*/, 2]; return [4 /*yield*/, gate // If consent is configured but not ready, queue the event ]; case 1: _a.sent(); _a.label = 2; case 2: // If consent is configured but not ready, queue the event if (this.consentConfig && !this.consentConfig.isConsentReady()) { return [2 /*return*/, this.queueEventForConsent('identify', [ userIdOrTraits, traitsOrOptions, options, ])]; } // Consent ready (or not configured) - send immediately return [2 /*return*/, this.sendIdentifyEvent(userIdOrTraits, traitsOrOptions, options)]; } }); }); }; /** * Internal method to actually send an identify event to CDP. */ this.sendIdentifyEvent = function (userIdOrTraits, traitsOrOptions, options) { return __awaiter(_this, void 0, void 0, function () { var userId, traits, opts, identifyPromises, a_2, b_2, results, error_2; return __generator(this, function (_a) { switch (_a.label) { case 0: if (typeof userIdOrTraits === 'string') { userId = userIdOrTraits; traits = traitsOrOptions; opts = options; } else { traits = userIdOrTraits; opts = traitsOrOptions; } // Configure CDP with fresh consent before sending this.configureConsentOnCdp(); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); identifyPromises = []; a_2 = this.ccdp(); if (a_2 && typeof a_2.identify === 'function') { identifyPromises.push(new Promise(function (resolve) { if (userId) { a_2.identify(userId, traits, opts, function () { return resolve(); }); } else { a_2.identify(traits, opts, function () { return resolve(); }); } })); } b_2 = this.cdp(); if (b_2 && typeof b_2.identify === 'function') { identifyPromises.push(new Promise(function (resolve) { if (userId) { b_2.identify(userId, traits, opts, function () { return resolve(); }); } else { b_2.identify(traits, opts, function () { return resolve(); }); } })); } return [4 /*yield*/, Promise.allSettled(identifyPromises)]; case 2: results = _a.sent(); this.logger('identify event sent', results); return [3 /*break*/, 4]; case 3: error_2 = _a.sent(); this.logger(error_2); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; /** * Send a `page` event to the CDP. * If consent is not ready, the event is queued until consent is available. */ this.page = function (options, props) { if (props === void 0) { props = {}; } return __awaiter(_this, void 0, void 0, function () { var gate; return __generator(this, function (_a) { switch (_a.label) { case 0: gate = this.waitForReadyState(); if (!gate) return [3 /*break*/, 2]; return [4 /*yield*/, gate // If consent is configured but not ready, queue the event ]; case 1: _a.sent(); _a.label = 2; case 2: // If consent is configured but not ready, queue the event if (this.consentConfig && !this.consentConfig.isConsentReady()) { return [2 /*return*/, this.queueEventForConsent('page', [options, props])]; } // Consent ready (or not configured) - send immediately return [2 /*return*/, this.sendPageEvent(options, props)]; } }); }); }; /** * Internal method to actually send a page event to CDP. */ this.sendPageEvent = function (options, props) { if (props === void 0) { props = {}; } return __awaiter(_this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: // Configure CDP with fresh consent before sending this.configureConsentOnCdp(); return [4 /*yield*/, new Promise(function (resolve) { var pagePromises = []; try { var a_3 = _this.ccdp(); if (a_3 && typeof a_3.page === 'function') { pagePromises.push(new Promise(function (cdpResolve) { a_3.page(__assign(__assign({}, props), { meta: _this.meta() }), options, function () { return cdpResolve(); }); })); } var b_3 = _this.cdp(); if (b_3 && typeof b_3.page === 'function') { pagePromises.push(new Promise(function (cdpResolve) { b_3.page(__assign(__assign({}, props), { meta: _this.meta() }), options, function () { return cdpResolve(); }); })); } if (pagePromises.length > 0) { Promise.allSettled(pagePromises).then(function (results) { _this.logger('page event sent', results); resolve(); }); } else { resolve(); } } catch (error) { _this.logger(error); resolve(); } })]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; // Coalesces concurrent reset() calls — a second caller gets the same in-flight // Promise rather than triggering a duplicate reset. this.reset = function () { if (!_this.resetCompletePromise) { _this.resetCompletePromise = _this.performReset(); } return _this.resetCompletePromise; }; this.performReset = function () { return __awaiter(_this, void 0, void 0, function () { var a, b, maxAttempts, attempts, userState, error_3; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 4, 5, 6]); a = this.ccdp(); if (a && typeof a.reset === 'function') { a.reset(); } b = this.cdp(); if (b && typeof b.reset === 'function') { b.reset(); } maxAttempts = 50; attempts = 0; _b.label = 1; case 1: if (!(attempts < maxAttempts)) return [3 /*break*/, 3]; // eslint-disable-next-line return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10); })]; case 2: // eslint-disable-next-line _b.sent(); try { userState = ((_a = a === null || a === void 0 ? void 0 : a.user) === null || _a === void 0 ? void 0 : _a.call(a)) || {}; if (userState.userId === null || userState.userId === undefined) { return [3 /*break*/, 3]; } } catch (_c) { return [3 /*break*/, 3]; } attempts++; return [3 /*break*/, 1]; case 3: return [3 /*break*/, 6]; case 4: error_3 = _b.sent(); this.logger(error_3); return [3 /*break*/, 6]; case 5: this.resetCompletePromise = null; return [7 /*endfinally*/]; case 6: return [2 /*return*/]; } }); }); }; /** * 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 = function () { if (typeof window === 'undefined') { _this.logger('[Chord Analytics]: setupWebPixelListener called in non-browser environment'); // eslint-disable-next-line @typescript-eslint/no-empty-function return function () { }; } // Remove existing listener if one exists if (_this.webPixelMessageHandler) { window.removeEventListener('message', _this.webPixelMessageHandler); } _this.webPixelMessageHandler = function (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') { var eventHostname = new URL(event.origin).hostname; if (eventHostname !== window.location.hostname) return; } } catch (_a) { return; } var 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': { var _b = message.payload, eventName = _b[0], properties = _b[1]; if (eventName) { Promise.resolve(_this.track(eventName, properties)).catch(function (err) { return _this.logger('[Chord Analytics]: Error in web pixel track:', err); }); } break; } case 'identify': { var _c = message.payload, userId = _c[0], traits = _c[1]; Promise.resolve(_this.identify(userId, traits)).catch(function (err) { return _this.logger('[Chord Analytics]: Error in web pixel identify:', err); }); break; } case 'page': { var properties = message.payload[0]; Promise.resolve(_this.page(undefined, properties)).catch(function (err) { return _this.logger('[Chord Analytics]: Error in web pixel page:', err); }); break; } default: _this.logger("[Chord Analytics]: Unknown web pixel message type: ".concat(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 function () { 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 = function (props, options) { return __awaiter(_this, void 0, void 0, function () { var cart, payload, formattedPayload; var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; return __generator(this, function (_l) { cart = (_d = (_c = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.formatters) === null || _b === void 0 ? void 0 : _b.objects) === null || _c === void 0 ? void 0 : _c.cart) === null || _d === void 0 ? void 0 : _d.call(_c, { cart: props === null || props === void 0 ? void 0 : props.cart, }); 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 ((_g = (_f = (_e = this.options) === null || _e === void 0 ? void 0 : _e.formatters) === null || _f === void 0 ? void 0 : _f.events) === null || _g === void 0 ? void 0 : _g.cartViewed) { formattedPayload = (_k = (_j = (_h = this.options) === null || _h === void 0 ? void 0 : _h.formatters) === null || _j === void 0 ? void 0 : _j.events) === null || _k === void 0 ? void 0 : _k.cartViewed(props, payload); return [2 /*return*/, this.track('Cart Viewed', formattedPayload, options)]; } return [2 /*return*/, this.track('Cart Viewed', payload, options)]; }); }); }; // TODO: Add support for props.products this.trackCheckoutStarted = function (props, options) { return __awaiter(_this, void 0, void 0, function () { var formatter, checkout, payload, formattedPayload; var _a, _b, _c, _d, _e, _f, _g; return __generator(this, function (_h) { formatter = (_c = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.formatters) === null || _b === void 0 ? void 0 : _b.events) === null || _c === void 0 ? void 0 : _c.checkoutStarted; checkout = (_g = (_f = (_e = (_d = this.options) === null || _d === void 0 ? void 0 : _d.formatters) === null || _e === void 0 ? void 0 : _e.objects) === null || _f === void 0 ? void 0 : _f.checkout) === null || _g === void 0 ? void 0 : _g.call(_f, { checkout: props === null || props === void 0 ? void 0 : props.checkout, }); payload = checkout; if (typeof formatter === 'function') { formattedPayload = formatter(props, payload); return [2 /*return*/, this.track('Checkout Started', formattedPayload, options)]; } return [2 /*return*/, this.track('Checkout Started', payload, options)]; }); }); }; this.trackCheckoutStepCompleted = function (props, options) { return __awaiter(_this, void 0, void 0, function () { var formatter, payload, formattedPayload; var _a, _b, _c;