@chordcommerce/analytics
Version:
Chord Commerce event tracking
956 lines (955 loc) • 96.2 kB
JavaScript
"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;