@chordcommerce/analytics 1.23.0 → 1.25.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59,13 +59,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
59
59
  exports.ChordAnalytics = void 0;
60
60
  var cdp_js_1 = require("./cdp.js");
61
61
  var index_js_1 = require("./consent/index.js");
62
+ var persistence_js_1 = require("./consent/persistence.js");
62
63
  var index_js_2 = require("../validators/index.js");
63
64
  var utils_js_1 = require("../utils.js");
64
65
  var DEBUG_MODE_STORAGE_KEY = 'chordcommerce/analytics/debug';
65
- var CONSENT_QUEUE_STORAGE_KEY = 'chordcommerce/analytics/consent-queue';
66
- var CONSENT_QUEUE_TTL_MS = 5 * 60 * 1000; // 5 minutes
67
- var CONSENT_QUEUE_MAX_SIZE_BYTES = 100 * 1024; // 100KB max queue size
68
- var CONSENT_QUEUE_MAX_EVENTS = 50; // Max number of events to queue
66
+ var OPERATION_TIMEOUT_MS = 5000;
69
67
  var ChordAnalytics = /** @class */ (function () {
70
68
  function ChordAnalytics(options) {
71
69
  var _this = this;
@@ -87,138 +85,12 @@ var ChordAnalytics = /** @class */ (function () {
87
85
  */
88
86
  this.cdpReadyPromise = null;
89
87
  this.consentConfig = null;
90
- // Non-null while reset() is in flight. Acts as both the coalescing handle and
91
- // the gate that track/identify/page await before executing.
92
- this.resetCompletePromise = null;
93
- /**
94
- * Queue for events waiting for consent to be ready
95
- */
96
- this.consentEventQueue = [];
97
- /**
98
- * Whether we're currently waiting for consent and watching for it
99
- */
100
- this.consentWatcherActive = false;
101
- /**
102
- * Promise that resolves when consent is ready (shared across all queued events)
103
- */
88
+ // Serial queue — all outgoing operations run one at a time in call order.
89
+ this.operationQueue = Promise.resolve();
90
+ // True while a reset() operation is executing in the queue.
91
+ this.resetting = false;
92
+ // Shared promise for concurrent waitForConsent() callers.
104
93
  this.consentReadyPromise = null;
105
- /**
106
- * Persist the consent event queue to sessionStorage for recovery on instance recreation.
107
- * Validates queue size before storing to prevent QuotaExceededError.
108
- */
109
- this.persistQueueToStorage = function () {
110
- if (typeof window === 'undefined' || !window.sessionStorage)
111
- return;
112
- try {
113
- // Limit number of events to prevent unbounded queue growth
114
- var eventsToStore = _this.consentEventQueue.slice(-CONSENT_QUEUE_MAX_EVENTS);
115
- var persistedEvents = eventsToStore.map(function (item) { return ({
116
- type: item.type,
117
- args: item.args,
118
- timestamp: item.timestamp,
119
- }); });
120
- var serialized = JSON.stringify(persistedEvents);
121
- // Check serialized size before attempting to store
122
- var sizeInBytes = new Blob([serialized]).size;
123
- if (sizeInBytes > CONSENT_QUEUE_MAX_SIZE_BYTES) {
124
- _this.logger("[Chord Analytics]: Consent queue too large (".concat(Math.round(sizeInBytes / 1024), "KB), skipping persistence"));
125
- return;
126
- }
127
- window.sessionStorage.setItem(CONSENT_QUEUE_STORAGE_KEY, serialized);
128
- }
129
- catch (error) {
130
- _this.logger('[Chord Analytics]: Error persisting queue to storage:', error);
131
- }
132
- };
133
- /**
134
- * Restore pending events from sessionStorage (keeps events less than 5 min old).
135
- * Called on new instance initialization.
136
- */
137
- this.restoreQueueFromStorage = function () {
138
- if (typeof window === 'undefined' || !window.sessionStorage)
139
- return;
140
- try {
141
- var stored = window.sessionStorage.getItem(CONSENT_QUEUE_STORAGE_KEY);
142
- if (!stored)
143
- return;
144
- var persistedEvents = void 0;
145
- try {
146
- persistedEvents = JSON.parse(stored);
147
- }
148
- catch (parseError) {
149
- _this.logger('[Chord Analytics]: Invalid JSON in stored consent queue, clearing storage:', parseError);
150
- window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY);
151
- return;
152
- }
153
- // Validate that parsed data is an array
154
- if (!Array.isArray(persistedEvents)) {
155
- _this.logger('[Chord Analytics]: Stored consent queue is not an array, clearing storage');
156
- window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY);
157
- return;
158
- }
159
- var now_1 = Date.now();
160
- // Filter out expired events and events with invalid structure
161
- var validEvents = persistedEvents.filter(function (event) {
162
- // Validate event structure
163
- if (typeof event !== 'object' || event === null)
164
- return false;
165
- if (!['track', 'page', 'identify'].includes(event.type))
166
- return false;
167
- if (!Array.isArray(event.args))
168
- return false;
169
- if (typeof event.timestamp !== 'number')
170
- return false;
171
- // Filter out expired events
172
- return now_1 - event.timestamp < CONSENT_QUEUE_TTL_MS;
173
- });
174
- if (validEvents.length === 0) {
175
- window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY);
176
- return;
177
- }
178
- if (_this.options.enableLogging && _this.options.debug) {
179
- // eslint-disable-next-line no-console
180
- console.log("[Chord Analytics DEBUG]: Restoring ".concat(validEvents.length, " events from sessionStorage"));
181
- }
182
- // Add restored events to the queue
183
- validEvents.forEach(function (event) {
184
- _this.consentEventQueue.push({
185
- type: event.type,
186
- args: event.args,
187
- timestamp: event.timestamp,
188
- // eslint-disable-next-line @typescript-eslint/no-empty-function
189
- resolve: function () { },
190
- });
191
- });
192
- // Start watching for consent if we restored events
193
- if (validEvents.length > 0 && !_this.consentWatcherActive) {
194
- _this.consentWatcherActive = true;
195
- _this.watchForConsentAndFlush();
196
- }
197
- }
198
- catch (error) {
199
- _this.logger('[Chord Analytics]: Error restoring queue from storage:', error);
200
- // Clear corrupted storage
201
- try {
202
- window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY);
203
- }
204
- catch (_a) {
205
- // Ignore cleanup errors
206
- }
207
- }
208
- };
209
- /**
210
- * Clear the persisted queue from sessionStorage after successful flush.
211
- */
212
- this.clearQueueFromStorage = function () {
213
- if (typeof window === 'undefined' || !window.sessionStorage)
214
- return;
215
- try {
216
- window.sessionStorage.removeItem(CONSENT_QUEUE_STORAGE_KEY);
217
- }
218
- catch (error) {
219
- _this.logger('[Chord Analytics]: Error clearing queue from storage:', error);
220
- }
221
- };
222
94
  /**
223
95
  * Set debug mode for Chord Analytics
224
96
  * @param enabled - Whether debug mode should be enabled
@@ -248,6 +120,32 @@ var ChordAnalytics = /** @class */ (function () {
248
120
  return null;
249
121
  return (window === null || window === void 0 ? void 0 : window["_".concat(namespace)]) || (0, cdp_js_1.cdpQueue)(namespace);
250
122
  };
123
+ /*
124
+ * The current user and anonymous ids as the CDP knows them.
125
+ *
126
+ * Exposed to source middleware, which on Segment could read these from the
127
+ * event facade. Either CDP may be absent or may not implement `user()`, so
128
+ * every step is guarded and an unknown id is simply undefined.
129
+ */
130
+ this.currentUser = function () {
131
+ var read = function (analytics) {
132
+ var _a, _b, _c;
133
+ try {
134
+ var user = (_a = analytics === null || analytics === void 0 ? void 0 : analytics.user) === null || _a === void 0 ? void 0 : _a.call(analytics);
135
+ if (!user)
136
+ return undefined;
137
+ var userId = (_b = user.userId) !== null && _b !== void 0 ? _b : undefined;
138
+ var anonymousId = (_c = user.anonymousId) !== null && _c !== void 0 ? _c : undefined;
139
+ if (userId === undefined && anonymousId === undefined)
140
+ return undefined;
141
+ return { userId: userId, anonymousId: anonymousId };
142
+ }
143
+ catch (_d) {
144
+ return undefined;
145
+ }
146
+ };
147
+ return read(_this.ccdp()) || read(_this.cdp()) || {};
148
+ };
251
149
  /*
252
150
  * Log a message to the console if `options.enableLogging` is true.
253
151
  */
@@ -262,21 +160,56 @@ var ChordAnalytics = /** @class */ (function () {
262
160
  console.log.apply(console, __spreadArray([message], optionalParams, false));
263
161
  };
264
162
  this.isResetting = function () {
265
- return _this.resetCompletePromise !== null;
163
+ return _this.resetting;
266
164
  };
267
- // Single gate for all outgoing operations. Returns a Promise only when there
268
- // is actually something to wait for, preserving synchronous execution in the
269
- // common case where CDP is ready and no reset is in flight.
165
+ // Enqueues an operation to run after all previously enqueued operations
166
+ // complete. Errors are swallowed on the internal chain so a failed operation
167
+ // doesn't stall the queue.
168
+ this.enqueue = function (operation) {
169
+ var queued = _this.operationQueue.then(function () { return operation(); });
170
+ _this.operationQueue = queued.catch(function (err) { return _this.logger(err); });
171
+ return queued;
172
+ };
173
+ // Wraps a CDP call with a timeout so a stuck or unresponsive CDP cannot
174
+ // block the queue indefinitely.
175
+ // eslint-disable-next-line class-methods-use-this
176
+ this.withTimeout = function (promise) {
177
+ return Promise.race([
178
+ promise,
179
+ new Promise(function (_, reject) {
180
+ setTimeout(function () {
181
+ return reject(new Error("CDP call timed out after ".concat(OPERATION_TIMEOUT_MS, "ms")));
182
+ }, OPERATION_TIMEOUT_MS);
183
+ }),
184
+ ]);
185
+ };
186
+ // Gate for CDP readiness and consent. Reset ordering is handled by the queue.
187
+ // Returns undefined when nothing needs to be awaited (common case) to avoid
188
+ // an unnecessary microtask yield.
270
189
  this.waitForReadyState = function () {
271
190
  var cdp = _this.cdpReadyPromise;
272
- var reset = _this.resetCompletePromise;
273
- if (!cdp && !reset)
191
+ var consentPending = _this.consentConfig && !_this.consentConfig.isConsentReady();
192
+ if (!cdp && !consentPending)
274
193
  return undefined;
275
- // eslint-disable-next-line @typescript-eslint/no-empty-function
276
- if (cdp && reset)
277
- return Promise.all([cdp, reset]).then(function () { });
278
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
279
- return cdp !== null && cdp !== void 0 ? cdp : reset;
194
+ return (function () { return __awaiter(_this, void 0, void 0, function () {
195
+ return __generator(this, function (_a) {
196
+ switch (_a.label) {
197
+ case 0:
198
+ if (!cdp) return [3 /*break*/, 2];
199
+ return [4 /*yield*/, cdp];
200
+ case 1:
201
+ _a.sent();
202
+ _a.label = 2;
203
+ case 2:
204
+ if (!consentPending) return [3 /*break*/, 4];
205
+ return [4 /*yield*/, this.waitForConsent()];
206
+ case 3:
207
+ _a.sent();
208
+ _a.label = 4;
209
+ case 4: return [2 /*return*/];
210
+ }
211
+ });
212
+ }); })();
280
213
  };
281
214
  /**
282
215
  * Wait for consent manager to be ready (no timeout - queues indefinitely).
@@ -315,107 +248,6 @@ var ChordAnalytics = /** @class */ (function () {
315
248
  }
316
249
  });
317
250
  }); };
318
- /**
319
- * Queue an event to be sent when consent is ready.
320
- * Persists to sessionStorage for recovery on instance recreation.
321
- * Starts a consent watcher if not already active.
322
- */
323
- this.queueEventForConsent = function (type, args) {
324
- return new Promise(function (resolve) {
325
- _this.consentEventQueue.push({
326
- type: type,
327
- args: args,
328
- timestamp: Date.now(),
329
- resolve: resolve,
330
- });
331
- // Persist queue to sessionStorage for recovery on instance recreation
332
- _this.persistQueueToStorage();
333
- // Start watching for consent if not already
334
- if (!_this.consentWatcherActive) {
335
- _this.consentWatcherActive = true;
336
- _this.watchForConsentAndFlush();
337
- }
338
- });
339
- };
340
- /**
341
- * Watch for consent to become ready, then flush the queue.
342
- */
343
- this.watchForConsentAndFlush = function () { return __awaiter(_this, void 0, void 0, function () {
344
- return __generator(this, function (_a) {
345
- switch (_a.label) {
346
- case 0: return [4 /*yield*/, this.waitForConsent()];
347
- case 1:
348
- _a.sent();
349
- return [4 /*yield*/, this.flushConsentQueue()];
350
- case 2:
351
- _a.sent();
352
- this.consentWatcherActive = false;
353
- return [2 /*return*/];
354
- }
355
- });
356
- }); };
357
- /**
358
- * Flush all queued events now that consent is ready.
359
- * Clears sessionStorage after successful flush.
360
- */
361
- this.flushConsentQueue = function () { return __awaiter(_this, void 0, void 0, function () {
362
- var queue, sendPromises;
363
- var _this = this;
364
- return __generator(this, function (_a) {
365
- switch (_a.label) {
366
- case 0:
367
- queue = __spreadArray([], this.consentEventQueue, true);
368
- this.consentEventQueue = [];
369
- // Clear persisted queue from storage since we're processing them now
370
- this.clearQueueFromStorage();
371
- if (this.options.enableLogging && this.options.debug) {
372
- // eslint-disable-next-line no-console
373
- console.log("[Chord Analytics DEBUG]: Flushing ".concat(queue.length, " queued events"));
374
- }
375
- sendPromises = queue.map(function (item) { return __awaiter(_this, void 0, void 0, function () {
376
- var _a, error_1;
377
- return __generator(this, function (_b) {
378
- switch (_b.label) {
379
- case 0:
380
- _b.trys.push([0, 9, , 10]);
381
- _a = item.type;
382
- switch (_a) {
383
- case 'track': return [3 /*break*/, 1];
384
- case 'page': return [3 /*break*/, 3];
385
- case 'identify': return [3 /*break*/, 5];
386
- }
387
- return [3 /*break*/, 7];
388
- case 1: return [4 /*yield*/, this.sendTrackEvent(item.args[0], item.args[1], item.args[2])];
389
- case 2:
390
- _b.sent();
391
- return [3 /*break*/, 8];
392
- case 3: return [4 /*yield*/, this.sendPageEvent(item.args[0], item.args[1])];
393
- case 4:
394
- _b.sent();
395
- return [3 /*break*/, 8];
396
- case 5: return [4 /*yield*/, this.sendIdentifyEvent(item.args[0], item.args[1], item.args[2])];
397
- case 6:
398
- _b.sent();
399
- return [3 /*break*/, 8];
400
- case 7: return [3 /*break*/, 8];
401
- case 8: return [3 /*break*/, 10];
402
- case 9:
403
- error_1 = _b.sent();
404
- this.logger('[Chord Analytics]: Error sending queued event:', error_1);
405
- return [3 /*break*/, 10];
406
- case 10:
407
- item.resolve();
408
- return [2 /*return*/];
409
- }
410
- });
411
- }); });
412
- return [4 /*yield*/, Promise.all(sendPromises)];
413
- case 1:
414
- _a.sent();
415
- return [2 /*return*/];
416
- }
417
- });
418
- }); };
419
251
  /**
420
252
  * Configure CDP with fresh consent categories before each event.
421
253
  * Uses Jitsu's configure() to update privacy.consentCategories.
@@ -468,85 +300,107 @@ var ChordAnalytics = /** @class */ (function () {
468
300
  * Send a `track` event to the CDP with any event name and properties.
469
301
  * If consent is not ready, the event is queued until consent is available.
470
302
  */
471
- this.track = function (event, props, options) { return __awaiter(_this, void 0, void 0, function () {
472
- var gate;
473
- return __generator(this, function (_a) {
474
- switch (_a.label) {
475
- case 0:
476
- if (this.options.enableLogging && this.options.debug) {
477
- // eslint-disable-next-line no-console
478
- console.log("[Chord Analytics DEBUG]: track() called with event: \"".concat(event, "\""));
479
- }
480
- if (!event)
481
- this.logger('No event name provided');
482
- gate = this.waitForReadyState();
483
- if (!gate) return [3 /*break*/, 2];
484
- return [4 /*yield*/, gate
485
- // If consent is configured but not ready, queue the event
486
- ];
487
- case 1:
488
- _a.sent();
489
- _a.label = 2;
490
- case 2:
491
- // If consent is configured but not ready, queue the event
492
- if (this.consentConfig && !this.consentConfig.isConsentReady()) {
493
- if (this.options.enableLogging && this.options.debug) {
494
- // eslint-disable-next-line no-console
495
- console.log("[Chord Analytics DEBUG]: track(\"".concat(event, "\") consent not ready, queuing event"));
496
- }
497
- return [2 /*return*/, this.queueEventForConsent('track', [event, props, options])];
498
- }
499
- if (this.options.enableLogging && this.options.debug) {
500
- // eslint-disable-next-line no-console
501
- console.log("[Chord Analytics DEBUG]: track(\"".concat(event, "\") sending event now"));
502
- }
503
- // Consent ready (or not configured) - send immediately
504
- return [2 /*return*/, this.sendTrackEvent(event, props, options)];
505
- }
506
- });
507
- }); };
303
+ this.track = function (event, props, options) {
304
+ if (_this.options.enableLogging && _this.options.debug) {
305
+ // eslint-disable-next-line no-console
306
+ console.log("[Chord Analytics DEBUG]: track() called with event: \"".concat(event, "\""));
307
+ }
308
+ if (!event)
309
+ _this.logger('No event name provided');
310
+ return _this.enqueue(function () { return __awaiter(_this, void 0, void 0, function () {
311
+ var gate;
312
+ return __generator(this, function (_a) {
313
+ switch (_a.label) {
314
+ case 0:
315
+ gate = this.waitForReadyState();
316
+ if (!gate) return [3 /*break*/, 2];
317
+ return [4 /*yield*/, gate];
318
+ case 1:
319
+ _a.sent();
320
+ _a.label = 2;
321
+ case 2: return [4 /*yield*/, this.sendTrackEvent(event, props, options)];
322
+ case 3:
323
+ _a.sent();
324
+ return [2 /*return*/];
325
+ }
326
+ });
327
+ }); });
328
+ };
508
329
  /**
509
330
  * Internal method to actually send a track event to CDP.
510
331
  */
511
332
  this.sendTrackEvent = function (event, props, options) { return __awaiter(_this, void 0, void 0, function () {
512
- var formattedProps, middlewareProps, searchParams_1, urlSearchParams, searchData_1, finalEventProps;
333
+ var formattedProps, middlewareProps, searchParams_1, urlSearchParams, searchData_1, _a, userId_1, anonymousId_1, invoke, settle, returned, results, _b, finalEventProps, cdpPromises, a_1, b_1;
513
334
  var _this = this;
514
- return __generator(this, function (_a) {
515
- switch (_a.label) {
335
+ return __generator(this, function (_c) {
336
+ switch (_c.label) {
516
337
  case 0:
517
338
  formattedProps = this.options.stripNull && props ? (0, utils_js_1.pruneNullValues)(props) : props;
518
339
  middlewareProps = {};
519
- if (this.middleware.length > 0) {
520
- searchParams_1 = typeof window !== 'undefined' ? window.location.search : '';
521
- urlSearchParams = new URLSearchParams(searchParams_1);
522
- searchData_1 = Object.fromEntries(urlSearchParams.entries());
523
- // process middleware synchronously
524
- this.middleware.forEach(function (func) {
525
- try {
526
- var result = func({
527
- payload: {
528
- obj: {
529
- properties: props,
530
- event: event,
531
- context: {
532
- page: {
533
- search: searchParams_1,
534
- searchParams: searchData_1,
535
- },
340
+ if (!(this.middleware.length > 0)) return [3 /*break*/, 4];
341
+ searchParams_1 = typeof window !== 'undefined' ? window.location.search : '';
342
+ urlSearchParams = new URLSearchParams(searchParams_1);
343
+ searchData_1 = Object.fromEntries(urlSearchParams.entries());
344
+ _a = this.currentUser(), userId_1 = _a.userId, anonymousId_1 = _a.anonymousId;
345
+ invoke = function (func) {
346
+ try {
347
+ return func({
348
+ payload: {
349
+ userId: userId_1,
350
+ anonymousId: anonymousId_1,
351
+ obj: {
352
+ properties: props,
353
+ event: event,
354
+ context: {
355
+ page: {
356
+ search: searchParams_1,
357
+ searchParams: searchData_1,
536
358
  },
537
359
  },
538
360
  },
539
- next: function (_) { return _; },
540
- });
541
- if (result && typeof result === 'object') {
542
- middlewareProps = __assign(__assign({}, middlewareProps), result);
543
- }
544
- }
545
- catch (error) {
546
- console.warn('Middleware error:', error);
361
+ },
362
+ next: function (_) { return _; },
363
+ });
364
+ }
365
+ catch (error) {
366
+ console.warn('Middleware error:', error);
367
+ return undefined;
368
+ }
369
+ };
370
+ settle = function (returned) {
371
+ if (!(0, utils_js_1.isPromise)(returned))
372
+ return returned;
373
+ return (0, utils_js_1.withTimeout)(returned, utils_js_1.MIDDLEWARE_TIMEOUT_MS)
374
+ .then(function (settled) {
375
+ if (settled === utils_js_1.TIMED_OUT) {
376
+ console.warn("Middleware timed out after ".concat(utils_js_1.MIDDLEWARE_TIMEOUT_MS, "ms; its properties were dropped"));
377
+ return undefined;
547
378
  }
379
+ return settled;
380
+ })
381
+ .catch(function (error) {
382
+ console.warn('Middleware error:', error);
383
+ return undefined;
548
384
  });
549
- }
385
+ };
386
+ returned = this.middleware.map(invoke);
387
+ if (!returned.some(utils_js_1.isPromise)) return [3 /*break*/, 2];
388
+ return [4 /*yield*/, Promise.all(returned.map(settle))];
389
+ case 1:
390
+ _b = _c.sent();
391
+ return [3 /*break*/, 3];
392
+ case 2:
393
+ _b = returned;
394
+ _c.label = 3;
395
+ case 3:
396
+ results = _b;
397
+ results.forEach(function (result) {
398
+ if (result && typeof result === 'object') {
399
+ middlewareProps = __assign(__assign({}, middlewareProps), result);
400
+ }
401
+ });
402
+ _c.label = 4;
403
+ case 4:
550
404
  finalEventProps = __assign(__assign(__assign({}, formattedProps), middlewareProps), { meta: this.meta() });
551
405
  if (this.options.debug) {
552
406
  this.validate(event, finalEventProps).forEach(function (valid) {
@@ -557,35 +411,27 @@ var ChordAnalytics = /** @class */ (function () {
557
411
  }
558
412
  // Configure CDP with fresh consent before sending
559
413
  this.configureConsentOnCdp();
560
- return [4 /*yield*/, new Promise(function (resolve) {
561
- var cdpPromises = [];
562
- try {
563
- var a_1 = _this.ccdp();
564
- if (a_1 && typeof a_1.track === 'function') {
565
- cdpPromises.push(new Promise(function (cdpResolve) {
566
- a_1.track(event, finalEventProps, options, function () { return cdpResolve(); });
567
- }));
568
- }
569
- var b_1 = _this.cdp();
570
- if (b_1 && typeof b_1.track === 'function') {
571
- cdpPromises.push(new Promise(function (cdpResolve) {
572
- b_1.track(event, finalEventProps, options, function () { return cdpResolve(); });
573
- }));
574
- }
575
- if (cdpPromises.length > 0) {
576
- Promise.allSettled(cdpPromises).then(function () { return resolve(); });
577
- }
578
- else {
579
- resolve();
580
- }
581
- }
582
- catch (error) {
583
- _this.logger(error);
584
- resolve();
585
- }
586
- })];
587
- case 1:
588
- _a.sent();
414
+ cdpPromises = [];
415
+ try {
416
+ a_1 = this.ccdp();
417
+ if (a_1 && typeof a_1.track === 'function') {
418
+ cdpPromises.push(this.withTimeout(new Promise(function (resolve) {
419
+ a_1.track(event, finalEventProps, options, function () { return resolve(); });
420
+ })));
421
+ }
422
+ b_1 = this.cdp();
423
+ if (b_1 && typeof b_1.track === 'function') {
424
+ cdpPromises.push(this.withTimeout(new Promise(function (resolve) {
425
+ b_1.track(event, finalEventProps, options, function () { return resolve(); });
426
+ })));
427
+ }
428
+ }
429
+ catch (error) {
430
+ this.logger(error);
431
+ }
432
+ return [4 /*yield*/, Promise.allSettled(cdpPromises)];
433
+ case 5:
434
+ _c.sent();
589
435
  return [2 /*return*/];
590
436
  }
591
437
  });
@@ -594,38 +440,31 @@ var ChordAnalytics = /** @class */ (function () {
594
440
  * Send an `identify` event to the CDP with user id and traits.
595
441
  * If consent is not ready, the event is queued until consent is available.
596
442
  */
597
- this.identify = function (userIdOrTraits, traitsOrOptions, options) { return __awaiter(_this, void 0, void 0, function () {
598
- var gate;
599
- return __generator(this, function (_a) {
600
- switch (_a.label) {
601
- case 0:
602
- gate = this.waitForReadyState();
603
- if (!gate) return [3 /*break*/, 2];
604
- return [4 /*yield*/, gate
605
- // If consent is configured but not ready, queue the event
606
- ];
607
- case 1:
608
- _a.sent();
609
- _a.label = 2;
610
- case 2:
611
- // If consent is configured but not ready, queue the event
612
- if (this.consentConfig && !this.consentConfig.isConsentReady()) {
613
- return [2 /*return*/, this.queueEventForConsent('identify', [
614
- userIdOrTraits,
615
- traitsOrOptions,
616
- options,
617
- ])];
618
- }
619
- // Consent ready (or not configured) - send immediately
620
- return [2 /*return*/, this.sendIdentifyEvent(userIdOrTraits, traitsOrOptions, options)];
621
- }
622
- });
623
- }); };
443
+ this.identify = function (userIdOrTraits, traitsOrOptions, options) {
444
+ return _this.enqueue(function () { return __awaiter(_this, void 0, void 0, function () {
445
+ var gate;
446
+ return __generator(this, function (_a) {
447
+ switch (_a.label) {
448
+ case 0:
449
+ gate = this.waitForReadyState();
450
+ if (!gate) return [3 /*break*/, 2];
451
+ return [4 /*yield*/, gate];
452
+ case 1:
453
+ _a.sent();
454
+ _a.label = 2;
455
+ case 2: return [4 /*yield*/, this.sendIdentifyEvent(userIdOrTraits, traitsOrOptions, options)];
456
+ case 3:
457
+ _a.sent();
458
+ return [2 /*return*/];
459
+ }
460
+ });
461
+ }); });
462
+ };
624
463
  /**
625
464
  * Internal method to actually send an identify event to CDP.
626
465
  */
627
466
  this.sendIdentifyEvent = function (userIdOrTraits, traitsOrOptions, options) { return __awaiter(_this, void 0, void 0, function () {
628
- var userId, traits, opts, identifyPromises, a_2, b_2, results, error_2;
467
+ var userId, traits, opts, identifyPromises, a_2, b_2, results, error_1;
629
468
  return __generator(this, function (_a) {
630
469
  switch (_a.label) {
631
470
  case 0:
@@ -646,25 +485,25 @@ var ChordAnalytics = /** @class */ (function () {
646
485
  identifyPromises = [];
647
486
  a_2 = this.ccdp();
648
487
  if (a_2 && typeof a_2.identify === 'function') {
649
- identifyPromises.push(new Promise(function (resolve) {
488
+ identifyPromises.push(this.withTimeout(new Promise(function (resolve) {
650
489
  if (userId) {
651
490
  a_2.identify(userId, traits, opts, function () { return resolve(); });
652
491
  }
653
492
  else {
654
493
  a_2.identify(traits, opts, function () { return resolve(); });
655
494
  }
656
- }));
495
+ })));
657
496
  }
658
497
  b_2 = this.cdp();
659
498
  if (b_2 && typeof b_2.identify === 'function') {
660
- identifyPromises.push(new Promise(function (resolve) {
499
+ identifyPromises.push(this.withTimeout(new Promise(function (resolve) {
661
500
  if (userId) {
662
501
  b_2.identify(userId, traits, opts, function () { return resolve(); });
663
502
  }
664
503
  else {
665
504
  b_2.identify(traits, opts, function () { return resolve(); });
666
505
  }
667
- }));
506
+ })));
668
507
  }
669
508
  return [4 /*yield*/, Promise.allSettled(identifyPromises)];
670
509
  case 2:
@@ -672,8 +511,8 @@ var ChordAnalytics = /** @class */ (function () {
672
511
  this.logger('identify event sent', results);
673
512
  return [3 /*break*/, 4];
674
513
  case 3:
675
- error_2 = _a.sent();
676
- this.logger(error_2);
514
+ error_1 = _a.sent();
515
+ this.logger(error_1);
677
516
  return [3 /*break*/, 4];
678
517
  case 4: return [2 /*return*/];
679
518
  }
@@ -685,29 +524,24 @@ var ChordAnalytics = /** @class */ (function () {
685
524
  */
686
525
  this.page = function (options, props) {
687
526
  if (props === void 0) { props = {}; }
688
- return __awaiter(_this, void 0, void 0, function () {
527
+ return _this.enqueue(function () { return __awaiter(_this, void 0, void 0, function () {
689
528
  var gate;
690
529
  return __generator(this, function (_a) {
691
530
  switch (_a.label) {
692
531
  case 0:
693
532
  gate = this.waitForReadyState();
694
533
  if (!gate) return [3 /*break*/, 2];
695
- return [4 /*yield*/, gate
696
- // If consent is configured but not ready, queue the event
697
- ];
534
+ return [4 /*yield*/, gate];
698
535
  case 1:
699
536
  _a.sent();
700
537
  _a.label = 2;
701
- case 2:
702
- // If consent is configured but not ready, queue the event
703
- if (this.consentConfig && !this.consentConfig.isConsentReady()) {
704
- return [2 /*return*/, this.queueEventForConsent('page', [options, props])];
705
- }
706
- // Consent ready (or not configured) - send immediately
707
- return [2 /*return*/, this.sendPageEvent(options, props)];
538
+ case 2: return [4 /*yield*/, this.sendPageEvent(options, props)];
539
+ case 3:
540
+ _a.sent();
541
+ return [2 /*return*/];
708
542
  }
709
543
  });
710
- });
544
+ }); });
711
545
  };
712
546
  /**
713
547
  * Internal method to actually send a page event to CDP.
@@ -715,109 +549,96 @@ var ChordAnalytics = /** @class */ (function () {
715
549
  this.sendPageEvent = function (options, props) {
716
550
  if (props === void 0) { props = {}; }
717
551
  return __awaiter(_this, void 0, void 0, function () {
552
+ var pagePromises, a_3, b_3, results;
718
553
  var _this = this;
719
554
  return __generator(this, function (_a) {
720
555
  switch (_a.label) {
721
556
  case 0:
722
557
  // Configure CDP with fresh consent before sending
723
558
  this.configureConsentOnCdp();
724
- return [4 /*yield*/, new Promise(function (resolve) {
725
- var pagePromises = [];
726
- try {
727
- var a_3 = _this.ccdp();
728
- if (a_3 && typeof a_3.page === 'function') {
729
- pagePromises.push(new Promise(function (cdpResolve) {
730
- a_3.page(__assign(__assign({}, props), { meta: _this.meta() }), options, function () {
731
- return cdpResolve();
732
- });
733
- }));
734
- }
735
- var b_3 = _this.cdp();
736
- if (b_3 && typeof b_3.page === 'function') {
737
- pagePromises.push(new Promise(function (cdpResolve) {
738
- b_3.page(__assign(__assign({}, props), { meta: _this.meta() }), options, function () {
739
- return cdpResolve();
740
- });
741
- }));
742
- }
743
- if (pagePromises.length > 0) {
744
- Promise.allSettled(pagePromises).then(function (results) {
745
- _this.logger('page event sent', results);
746
- resolve();
747
- });
748
- }
749
- else {
750
- resolve();
751
- }
752
- }
753
- catch (error) {
754
- _this.logger(error);
755
- resolve();
756
- }
757
- })];
559
+ pagePromises = [];
560
+ try {
561
+ // The new identity has no consent event of its own yet, so drop the
562
+ // stored signature and let the next init emit a fresh baseline.
563
+ (0, persistence_js_1.forgetConsent)(this.options.namespace);
564
+ a_3 = this.ccdp();
565
+ if (a_3 && typeof a_3.page === 'function') {
566
+ pagePromises.push(this.withTimeout(new Promise(function (resolve) {
567
+ a_3.page(__assign(__assign({}, props), { meta: _this.meta() }), options, function () { return resolve(); });
568
+ })));
569
+ }
570
+ b_3 = this.cdp();
571
+ if (b_3 && typeof b_3.page === 'function') {
572
+ pagePromises.push(this.withTimeout(new Promise(function (resolve) {
573
+ b_3.page(__assign(__assign({}, props), { meta: _this.meta() }), options, function () { return resolve(); });
574
+ })));
575
+ }
576
+ }
577
+ catch (error) {
578
+ this.logger(error);
579
+ }
580
+ return [4 /*yield*/, Promise.allSettled(pagePromises)];
758
581
  case 1:
759
- _a.sent();
582
+ results = _a.sent();
583
+ this.logger('page event sent', results);
760
584
  return [2 /*return*/];
761
585
  }
762
586
  });
763
587
  });
764
588
  };
765
- // Coalesces concurrent reset() calls — a second caller gets the same in-flight
766
- // Promise rather than triggering a duplicate reset.
767
589
  this.reset = function () {
768
- if (!_this.resetCompletePromise) {
769
- _this.resetCompletePromise = _this.performReset();
770
- }
771
- return _this.resetCompletePromise;
772
- };
773
- this.performReset = function () { return __awaiter(_this, void 0, void 0, function () {
774
- var a, b, maxAttempts, attempts, userState, error_3;
775
- var _a;
776
- return __generator(this, function (_b) {
777
- switch (_b.label) {
778
- case 0:
779
- _b.trys.push([0, 4, 5, 6]);
780
- a = this.ccdp();
781
- if (a && typeof a.reset === 'function') {
782
- a.reset();
783
- }
784
- b = this.cdp();
785
- if (b && typeof b.reset === 'function') {
786
- b.reset();
787
- }
788
- maxAttempts = 50;
789
- attempts = 0;
790
- _b.label = 1;
791
- case 1:
792
- if (!(attempts < maxAttempts)) return [3 /*break*/, 3];
793
- // eslint-disable-next-line
794
- return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10); })];
795
- case 2:
796
- // eslint-disable-next-line
797
- _b.sent();
798
- try {
799
- userState = ((_a = a === null || a === void 0 ? void 0 : a.user) === null || _a === void 0 ? void 0 : _a.call(a)) || {};
800
- if (userState.userId === null || userState.userId === undefined) {
801
- return [3 /*break*/, 3];
590
+ return _this.enqueue(function () { return __awaiter(_this, void 0, void 0, function () {
591
+ var a, b, maxAttempts, attempts, userState, error_2;
592
+ var _a;
593
+ return __generator(this, function (_b) {
594
+ switch (_b.label) {
595
+ case 0:
596
+ this.resetting = true;
597
+ _b.label = 1;
598
+ case 1:
599
+ _b.trys.push([1, 5, 6, 7]);
600
+ a = this.ccdp();
601
+ if (a && typeof a.reset === 'function') {
602
+ a.reset();
802
603
  }
803
- }
804
- catch (_c) {
805
- return [3 /*break*/, 3];
806
- }
807
- attempts++;
808
- return [3 /*break*/, 1];
809
- case 3: return [3 /*break*/, 6];
810
- case 4:
811
- error_3 = _b.sent();
812
- this.logger(error_3);
813
- return [3 /*break*/, 6];
814
- case 5:
815
- this.resetCompletePromise = null;
816
- return [7 /*endfinally*/];
817
- case 6: return [2 /*return*/];
818
- }
819
- });
820
- }); };
604
+ b = this.cdp();
605
+ if (b && typeof b.reset === 'function') {
606
+ b.reset();
607
+ }
608
+ maxAttempts = 50;
609
+ attempts = 0;
610
+ _b.label = 2;
611
+ case 2:
612
+ if (!(attempts < maxAttempts)) return [3 /*break*/, 4];
613
+ // eslint-disable-next-line
614
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10); })];
615
+ case 3:
616
+ // eslint-disable-next-line
617
+ _b.sent();
618
+ try {
619
+ userState = ((_a = a === null || a === void 0 ? void 0 : a.user) === null || _a === void 0 ? void 0 : _a.call(a)) || {};
620
+ if (userState.userId === null || userState.userId === undefined) {
621
+ return [3 /*break*/, 4];
622
+ }
623
+ }
624
+ catch (_c) {
625
+ return [3 /*break*/, 4];
626
+ }
627
+ attempts++;
628
+ return [3 /*break*/, 2];
629
+ case 4: return [3 /*break*/, 7];
630
+ case 5:
631
+ error_2 = _b.sent();
632
+ this.logger(error_2);
633
+ return [3 /*break*/, 7];
634
+ case 6:
635
+ this.resetting = false;
636
+ return [7 /*endfinally*/];
637
+ case 7: return [2 /*return*/];
638
+ }
639
+ });
640
+ }); });
641
+ };
821
642
  /**
822
643
  * Handler for postMessage events from the Shopify Web Pixel sandbox.
823
644
  * Bound to the instance for proper `this` context and cleanup.
@@ -1460,8 +1281,6 @@ var ChordAnalytics = /** @class */ (function () {
1460
1281
  var consentManager = (0, index_js_1.addCdpConsent)(this.options);
1461
1282
  this.consentConfig = consentManager.getConsentConfig() || null;
1462
1283
  }
1463
- // Restore any pending events from sessionStorage (survives instance recreation)
1464
- this.restoreQueueFromStorage();
1465
1284
  if (((_b = this.options) === null || _b === void 0 ? void 0 : _b.cdpDomain) && ((_c = this.options) === null || _c === void 0 ? void 0 : _c.cdpWriteKey) && window) {
1466
1285
  window[this.options.namespace] = (window === null || window === void 0 ? void 0 : window[this.options.namespace]) || this;
1467
1286
  this.initializeCdp();
@@ -1481,7 +1300,7 @@ var ChordAnalytics = /** @class */ (function () {
1481
1300
  */
1482
1301
  ChordAnalytics.prototype.initializeCdp = function () {
1483
1302
  return __awaiter(this, void 0, void 0, function () {
1484
- var snippetPromise, initialConsent, error_4;
1303
+ var snippetPromise, initialConsent, namespace, error_3, error_4;
1485
1304
  return __generator(this, function (_a) {
1486
1305
  switch (_a.label) {
1487
1306
  case 0:
@@ -1491,7 +1310,7 @@ var ChordAnalytics = /** @class */ (function () {
1491
1310
  }
1492
1311
  _a.label = 1;
1493
1312
  case 1:
1494
- _a.trys.push([1, 6, , 7]);
1313
+ _a.trys.push([1, 9, , 10]);
1495
1314
  snippetPromise = (0, cdp_js_1.addCdpSnippet)(this.options);
1496
1315
  // Create a void promise for cdpReadyPromise that resolves when snippet is done
1497
1316
  // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -1505,32 +1324,57 @@ var ChordAnalytics = /** @class */ (function () {
1505
1324
  // eslint-disable-next-line no-console
1506
1325
  console.log('[Chord Analytics DEBUG]: consentCategories defined?', (initialConsent === null || initialConsent === void 0 ? void 0 : initialConsent.consentCategories) !== undefined);
1507
1326
  }
1508
- if (!(initialConsent && initialConsent.consentCategories !== undefined)) return [3 /*break*/, 4];
1327
+ namespace = this.options.namespace;
1328
+ if (!(initialConsent && initialConsent.consentCategories !== undefined)) return [3 /*break*/, 7];
1329
+ if (!(0, persistence_js_1.claimConsentEmit)(namespace, initialConsent)) {
1330
+ if (this.options.enableLogging && this.options.debug) {
1331
+ // eslint-disable-next-line no-console
1332
+ console.log('[Chord Analytics DEBUG]: Initial consent unchanged or emit already in flight, skipping track');
1333
+ }
1334
+ return [2 /*return*/];
1335
+ }
1509
1336
  if (this.options.enableLogging && this.options.debug) {
1510
1337
  // eslint-disable-next-line no-console
1511
1338
  console.log('[Chord Analytics DEBUG]: About to call this.track() for Consent Preferences Updated');
1512
1339
  }
1513
- return [4 /*yield*/, this.track('Consent Preferences Updated', initialConsent)];
1340
+ _a.label = 3;
1514
1341
  case 3:
1342
+ _a.trys.push([3, 5, , 6]);
1343
+ return [4 /*yield*/, this.track('Consent Preferences Updated', initialConsent)
1344
+ // Only recorded once delivery resolves. Committing before this would
1345
+ // suppress the event permanently if it were queued and never
1346
+ // flushed — the next page load must be free to retry.
1347
+ ];
1348
+ case 4:
1515
1349
  _a.sent();
1350
+ // Only recorded once delivery resolves. Committing before this would
1351
+ // suppress the event permanently if it were queued and never
1352
+ // flushed — the next page load must be free to retry.
1353
+ (0, persistence_js_1.commitConsentEmit)(namespace, initialConsent);
1354
+ return [3 /*break*/, 6];
1355
+ case 5:
1356
+ error_3 = _a.sent();
1357
+ (0, persistence_js_1.releaseConsentEmit)(namespace, initialConsent);
1358
+ throw error_3;
1359
+ case 6:
1516
1360
  if (this.options.enableLogging && this.options.debug) {
1517
1361
  // eslint-disable-next-line no-console
1518
1362
  console.log('[Chord Analytics DEBUG]: Initial consent preferences tracked');
1519
1363
  }
1520
- return [3 /*break*/, 5];
1521
- case 4:
1364
+ return [3 /*break*/, 8];
1365
+ case 7:
1522
1366
  if (this.options.enableLogging && this.options.debug) {
1523
1367
  // eslint-disable-next-line no-console
1524
1368
  console.log('[Chord Analytics DEBUG]: Skipping initial consent track - no consent or no consentCategories');
1525
1369
  }
1526
- _a.label = 5;
1527
- case 5: return [3 /*break*/, 7];
1528
- case 6:
1370
+ _a.label = 8;
1371
+ case 8: return [3 /*break*/, 10];
1372
+ case 9:
1529
1373
  error_4 = _a.sent();
1530
1374
  // eslint-disable-next-line no-console
1531
1375
  console.error('CDP initialization failed:', error_4);
1532
- return [3 /*break*/, 7];
1533
- case 7: return [2 /*return*/];
1376
+ return [3 /*break*/, 10];
1377
+ case 10: return [2 /*return*/];
1534
1378
  }
1535
1379
  });
1536
1380
  });