@chordcommerce/analytics 1.23.0 → 1.24.1

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.
package/README.md CHANGED
@@ -121,8 +121,16 @@ export const cartFormatter: CartFormatter<StorefrontCart> = (props) => {
121
121
  addSourceMiddleware(func: AnalyticsMiddleware)
122
122
 
123
123
  Registers a middleware function to modify event properties before they are
124
- sent to analytics providers. Middleware functions are executed
125
- synchronously in the order they were added during event tracking.
124
+ sent to analytics providers. Middleware functions are invoked in the order they
125
+ were added during event tracking, and each one's properties are merged into the
126
+ event in that order.
127
+
128
+ A middleware may be synchronous or asynchronous. Synchronous middleware is never
129
+ awaited, so it adds no delay to event delivery. Asynchronous middleware is
130
+ awaited before the event is sent, capped at one second — a middleware that has
131
+ not resolved by then is abandoned, a warning is logged, and the event is sent
132
+ without its properties. A middleware that throws or rejects is logged and
133
+ skipped. A middleware never prevents an event from being sent.
126
134
 
127
135
  Parameters
128
136
 
@@ -135,6 +143,15 @@ The middleware function can return:
135
143
 
136
144
  - Partial<EventProperties> - Properties to merge into the event payload
137
145
  - void - No additional properties to add
146
+ - A Promise of either of the above - Awaited before the event is sent
147
+
148
+ The `payload` argument carries `payload.obj` (the event's properties, name and
149
+ page context) plus `payload.userId` and `payload.anonymousId` as the CDP
150
+ currently knows them — either may be undefined.
151
+
152
+ Note that middleware contributes properties by **returning** them. Mutating the
153
+ `payload` argument has no effect; `next` is accepted for signature
154
+ compatibility and does nothing.
138
155
 
139
156
  Useage Example
140
157
 
@@ -156,6 +173,15 @@ Useage Example
156
173
  timestamp: Date.now()
157
174
  };
158
175
  });
176
+
177
+ // Asynchronous middleware: the event waits for the lookup, up to one second.
178
+ analytics.addSourceMiddleware(async ({ payload }) => {
179
+ if (payload.obj.event !== 'Order Completed') return;
180
+
181
+ const isNewCustomer = await fetchIsNewCustomer();
182
+
183
+ return { is_new_customer: isNewCustomer ? 1 : 0 };
184
+ });
159
185
  ```
160
186
 
161
187
  ## API Reference
@@ -42,9 +42,12 @@ var ketch_js_1 = require("./ketch.js");
42
42
  var onetrust_js_1 = require("./onetrust.js");
43
43
  var shopify_js_1 = require("./shopify.js");
44
44
  var segment_js_1 = require("./segment.js");
45
+ var persistence_js_1 = require("./persistence.js");
45
46
  var addCdpConsent = function (options) {
46
- var lastProcessedConsent = null;
47
47
  var POLLING_INTERVAL_MS = 50;
48
+ // Always populated: ChordAnalytics applies a `namespace` default before
49
+ // constructing the consent manager.
50
+ var namespace = options.namespace;
48
51
  var TIMEOUT_MS = options.awaitConsentTimeout
49
52
  ? options.awaitConsentTimeout
50
53
  : 60000;
@@ -96,31 +99,49 @@ var addCdpConsent = function (options) {
96
99
  });
97
100
  }); };
98
101
  /**
99
- * Tracks consent preference changes.
100
- * Note: Per-event consent stamping is handled in ChordAnalytics via stampConsent().
102
+ * Tracks consent preference *transitions* only, deduped against the last
103
+ * state we emitted (persisted, so the guard survives navigation).
104
+ * Per-event consent stamping is handled by configureConsentOnCdp().
101
105
  */
102
106
  var trackConsentUpdate = function (updatedConsent) { return __awaiter(void 0, void 0, void 0, function () {
103
- var chordAnalytics;
107
+ var chordAnalytics, error_1;
104
108
  return __generator(this, function (_a) {
105
109
  switch (_a.label) {
106
110
  case 0:
107
- if (lastProcessedConsent &&
108
- JSON.stringify(lastProcessedConsent) === JSON.stringify(updatedConsent)) {
111
+ chordAnalytics = window[namespace];
112
+ if (!chordAnalytics ||
113
+ typeof chordAnalytics.track !== 'function' ||
114
+ updatedConsent.consentCategories === undefined) {
109
115
  return [2 /*return*/];
110
116
  }
111
- lastProcessedConsent = updatedConsent;
112
- chordAnalytics = window[options.namespace];
113
- if (!(chordAnalytics &&
114
- typeof chordAnalytics.track === 'function' &&
115
- updatedConsent.consentCategories !== undefined)) return [3 /*break*/, 2];
116
- return [4 /*yield*/, chordAnalytics.track('Consent Preferences Updated', updatedConsent)];
117
+ if (!(0, persistence_js_1.claimConsentEmit)(namespace, updatedConsent)) {
118
+ if (options.enableLogging && options.debug) {
119
+ console.log('[Chord Analytics DEBUG]: Consent unchanged or emit already in flight, skipping');
120
+ }
121
+ return [2 /*return*/];
122
+ }
123
+ _a.label = 1;
117
124
  case 1:
125
+ _a.trys.push([1, 3, , 4]);
126
+ return [4 /*yield*/, chordAnalytics.track('Consent Preferences Updated', updatedConsent)
127
+ // Only recorded once delivery resolves. Committing before this would
128
+ // suppress the event permanently if it were queued and never flushed.
129
+ ];
130
+ case 2:
118
131
  _a.sent();
132
+ // Only recorded once delivery resolves. Committing before this would
133
+ // suppress the event permanently if it were queued and never flushed.
134
+ (0, persistence_js_1.commitConsentEmit)(namespace, updatedConsent);
119
135
  if (options.enableLogging && options.debug) {
120
136
  console.log('[Chord Analytics DEBUG]: Consent preferences updated, sent tracking event');
121
137
  }
122
- _a.label = 2;
123
- case 2: return [2 /*return*/];
138
+ return [3 /*break*/, 4];
139
+ case 3:
140
+ error_1 = _a.sent();
141
+ (0, persistence_js_1.releaseConsentEmit)(namespace, updatedConsent);
142
+ console.error('Error tracking consent preferences update:', error_1);
143
+ return [3 /*break*/, 4];
144
+ case 4: return [2 /*return*/];
124
145
  }
125
146
  });
126
147
  }); };
@@ -129,24 +150,19 @@ var addCdpConsent = function (options) {
129
150
  * Returns the initial consent so it can be tracked after CDP is fully ready (avoiding circular await).
130
151
  */
131
152
  var setupConsentManagement = function () { return __awaiter(void 0, void 0, void 0, function () {
132
- var initialConsent;
133
153
  return __generator(this, function (_a) {
134
- switch (_a.label) {
135
- case 0:
136
- if (!adapter)
137
- return [2 /*return*/, null
138
- // Set up the consent update handler first to catch any immediate updates
139
- ];
154
+ if (!adapter)
155
+ return [2 /*return*/, null
140
156
  // Set up the consent update handler first to catch any immediate updates
141
- adapter.handleConsentUpdates(function (updatedConsent) {
142
- trackConsentUpdate(updatedConsent);
143
- });
144
- return [4 /*yield*/, adapter.getCurrentConsent()];
145
- case 1:
146
- initialConsent = _a.sent();
147
- lastProcessedConsent = initialConsent;
148
- return [2 /*return*/, initialConsent];
149
- }
157
+ ];
158
+ // Set up the consent update handler first to catch any immediate updates
159
+ adapter.handleConsentUpdates(function (updatedConsent) {
160
+ trackConsentUpdate(updatedConsent);
161
+ });
162
+ // Return initial consent to be tracked after CDP is fully initialized.
163
+ // Not marked as processed here: whichever path emits first records the
164
+ // signature, and the other is then deduped against it.
165
+ return [2 /*return*/, adapter.getCurrentConsent()];
150
166
  });
151
167
  }); };
152
168
  /**
@@ -155,7 +171,7 @@ var addCdpConsent = function (options) {
155
171
  * or null if consent setup failed or was blocked.
156
172
  */
157
173
  var init = function () { return __awaiter(void 0, void 0, void 0, function () {
158
- var canBeTracked, error_1;
174
+ var canBeTracked, error_2;
159
175
  return __generator(this, function (_a) {
160
176
  switch (_a.label) {
161
177
  case 0:
@@ -185,8 +201,8 @@ var addCdpConsent = function (options) {
185
201
  return [4 /*yield*/, setupConsentManagement()];
186
202
  case 3: return [2 /*return*/, _a.sent()];
187
203
  case 4:
188
- error_1 = _a.sent();
189
- console.error('Error setting up consent management:', error_1);
204
+ error_2 = _a.sent();
205
+ console.error('Error setting up consent management:', error_2);
190
206
  return [2 /*return*/, null];
191
207
  case 5: return [2 /*return*/];
192
208
  }
@@ -0,0 +1,49 @@
1
+ import type { ConsentConfiguration } from '../../types/index.js';
2
+ /**
3
+ * Build a stable, order-independent signature for a consent configuration.
4
+ *
5
+ * Category keys are sorted so a change in the order the consent manager
6
+ * reports its groups does not read as a change in consent.
7
+ */
8
+ export declare const consentSignature: (consent: ConsentConfiguration) => string;
9
+ /**
10
+ * Whether this consent configuration differs from the last one we tracked.
11
+ *
12
+ * Returns true when nothing has been stored yet, so every visitor still gets
13
+ * one baseline `Consent Preferences Updated` event recording their starting
14
+ * state. Subsequent page loads with unchanged consent return false.
15
+ */
16
+ export declare const hasConsentChanged: (namespace: string | undefined, consent: ConsentConfiguration) => boolean;
17
+ /**
18
+ * Claim the right to emit a consent event, atomically.
19
+ *
20
+ * Returns false when this state was already tracked, or when an emit for the
21
+ * same state is already in flight. Callers that get true MUST follow with
22
+ * `commitConsentEmit` on success or `releaseConsentEmit` on failure.
23
+ *
24
+ * The claim is deliberately not persisted: it exists only to dedupe within a
25
+ * page load. If delivery never completes, the next page load starts clean and
26
+ * retries rather than suppressing the event forever.
27
+ */
28
+ export declare const claimConsentEmit: (namespace: string | undefined, consent: ConsentConfiguration) => boolean;
29
+ /**
30
+ * Record a consent configuration as tracked, once delivery has actually
31
+ * completed. Later page loads dedupe against it.
32
+ */
33
+ export declare const commitConsentEmit: (namespace: string | undefined, consent: ConsentConfiguration) => void;
34
+ /**
35
+ * Give up a claim without recording it, so a later attempt can retry. Takes
36
+ * the consent it claimed so a failing emit cannot clear another state's claim.
37
+ */
38
+ export declare const releaseConsentEmit: (namespace: string | undefined, consent: ConsentConfiguration) => void;
39
+ /**
40
+ * Clear the stored consent signature so the next init emits a fresh baseline.
41
+ * Called on reset(), where a new anonymous ID would otherwise never get a
42
+ * consent record of its own.
43
+ */
44
+ export declare const forgetConsent: (namespace: string | undefined) => void;
45
+ /**
46
+ * Drop all module-level state. Test-only: `localStorage` is cleared between
47
+ * tests, but these would otherwise persist and suppress expected events.
48
+ */
49
+ export declare const clearConsentMemory: () => void;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clearConsentMemory = exports.forgetConsent = exports.releaseConsentEmit = exports.commitConsentEmit = exports.claimConsentEmit = exports.hasConsentChanged = exports.consentSignature = void 0;
4
+ var LAST_CONSENT_STORAGE_KEY = 'chordcommerce/analytics/last-consent';
5
+ /**
6
+ * In-memory fallback for environments where localStorage is unavailable
7
+ * (SSR, private browsing, storage disabled). Keyed by namespace so multiple
8
+ * ChordAnalytics instances on one page don't clobber each other.
9
+ */
10
+ var memoryStore = {};
11
+ var storageKey = function (namespace) {
12
+ return "".concat(LAST_CONSENT_STORAGE_KEY, "/").concat(namespace !== null && namespace !== void 0 ? namespace : 'default');
13
+ };
14
+ /**
15
+ * Build a stable, order-independent signature for a consent configuration.
16
+ *
17
+ * Category keys are sorted so a change in the order the consent manager
18
+ * reports its groups does not read as a change in consent.
19
+ */
20
+ var consentSignature = function (consent) {
21
+ var _a;
22
+ var categories = (_a = consent.consentCategories) !== null && _a !== void 0 ? _a : {};
23
+ var sorted = Object.keys(categories)
24
+ .sort()
25
+ .reduce(function (acc, key) {
26
+ acc[key] = categories[key];
27
+ return acc;
28
+ }, {});
29
+ return JSON.stringify(sorted);
30
+ };
31
+ exports.consentSignature = consentSignature;
32
+ var readSignature = function (namespace) {
33
+ var _a;
34
+ var key = storageKey(namespace);
35
+ try {
36
+ if (typeof window !== 'undefined' && window.localStorage) {
37
+ return window.localStorage.getItem(key);
38
+ }
39
+ }
40
+ catch (_b) {
41
+ // localStorage can throw in private browsing; fall through to memory
42
+ }
43
+ return (_a = memoryStore[key]) !== null && _a !== void 0 ? _a : null;
44
+ };
45
+ var writeSignature = function (namespace, signature) {
46
+ var key = storageKey(namespace);
47
+ memoryStore[key] = signature;
48
+ try {
49
+ if (typeof window !== 'undefined' && window.localStorage) {
50
+ window.localStorage.setItem(key, signature);
51
+ }
52
+ }
53
+ catch (_a) {
54
+ // Quota or private browsing; the in-memory copy still dedupes within
55
+ // this page load.
56
+ }
57
+ };
58
+ /**
59
+ * Claims for emits currently mid-delivery, as `<storage key>::<signature>`.
60
+ * Delivery is awaited and a queued event can stay unresolved for a long time,
61
+ * so without this two callers in the same tick would both pass the guard and
62
+ * emit duplicates.
63
+ *
64
+ * Keyed by signature, not just namespace, so two different consent states can
65
+ * be in flight at once without clearing each other's claim.
66
+ */
67
+ var inFlight = new Set();
68
+ var inFlightKey = function (namespace, signature) {
69
+ return "".concat(storageKey(namespace), "::").concat(signature);
70
+ };
71
+ /**
72
+ * Whether this consent configuration differs from the last one we tracked.
73
+ *
74
+ * Returns true when nothing has been stored yet, so every visitor still gets
75
+ * one baseline `Consent Preferences Updated` event recording their starting
76
+ * state. Subsequent page loads with unchanged consent return false.
77
+ */
78
+ var hasConsentChanged = function (namespace, consent) { return readSignature(namespace) !== (0, exports.consentSignature)(consent); };
79
+ exports.hasConsentChanged = hasConsentChanged;
80
+ /**
81
+ * Claim the right to emit a consent event, atomically.
82
+ *
83
+ * Returns false when this state was already tracked, or when an emit for the
84
+ * same state is already in flight. Callers that get true MUST follow with
85
+ * `commitConsentEmit` on success or `releaseConsentEmit` on failure.
86
+ *
87
+ * The claim is deliberately not persisted: it exists only to dedupe within a
88
+ * page load. If delivery never completes, the next page load starts clean and
89
+ * retries rather than suppressing the event forever.
90
+ */
91
+ var claimConsentEmit = function (namespace, consent) {
92
+ var signature = (0, exports.consentSignature)(consent);
93
+ if (readSignature(namespace) === signature)
94
+ return false;
95
+ if (inFlight.has(inFlightKey(namespace, signature)))
96
+ return false;
97
+ inFlight.add(inFlightKey(namespace, signature));
98
+ return true;
99
+ };
100
+ exports.claimConsentEmit = claimConsentEmit;
101
+ /**
102
+ * Record a consent configuration as tracked, once delivery has actually
103
+ * completed. Later page loads dedupe against it.
104
+ */
105
+ var commitConsentEmit = function (namespace, consent) {
106
+ var signature = (0, exports.consentSignature)(consent);
107
+ inFlight.delete(inFlightKey(namespace, signature));
108
+ writeSignature(namespace, signature);
109
+ };
110
+ exports.commitConsentEmit = commitConsentEmit;
111
+ /**
112
+ * Give up a claim without recording it, so a later attempt can retry. Takes
113
+ * the consent it claimed so a failing emit cannot clear another state's claim.
114
+ */
115
+ var releaseConsentEmit = function (namespace, consent) {
116
+ inFlight.delete(inFlightKey(namespace, (0, exports.consentSignature)(consent)));
117
+ };
118
+ exports.releaseConsentEmit = releaseConsentEmit;
119
+ /**
120
+ * Clear the stored consent signature so the next init emits a fresh baseline.
121
+ * Called on reset(), where a new anonymous ID would otherwise never get a
122
+ * consent record of its own.
123
+ */
124
+ var forgetConsent = function (namespace) {
125
+ var key = storageKey(namespace);
126
+ delete memoryStore[key];
127
+ inFlight.forEach(function (claim) {
128
+ if (claim.startsWith("".concat(key, "::")))
129
+ inFlight.delete(claim);
130
+ });
131
+ try {
132
+ if (typeof window !== 'undefined' && window.localStorage) {
133
+ window.localStorage.removeItem(key);
134
+ }
135
+ }
136
+ catch (_a) {
137
+ // Nothing to do; the in-memory copy is already gone.
138
+ }
139
+ };
140
+ exports.forgetConsent = forgetConsent;
141
+ /**
142
+ * Drop all module-level state. Test-only: `localStorage` is cleared between
143
+ * tests, but these would otherwise persist and suppress expected events.
144
+ */
145
+ var clearConsentMemory = function () {
146
+ Object.keys(memoryStore).forEach(function (key) { return delete memoryStore[key]; });
147
+ inFlight.clear();
148
+ };
149
+ exports.clearConsentMemory = clearConsentMemory;
@@ -71,6 +71,7 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
71
71
  setDebugMode: (enabled: boolean) => void;
72
72
  cdp: () => AnyAnalytics;
73
73
  ccdp: () => AnyAnalytics;
74
+ private currentUser;
74
75
  logger: (message?: any, ...optionalParams: any[]) => void;
75
76
  isResetting: () => boolean;
76
77
  private waitForReadyState;
@@ -59,6 +59,7 @@ 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';
@@ -248,6 +249,32 @@ var ChordAnalytics = /** @class */ (function () {
248
249
  return null;
249
250
  return (window === null || window === void 0 ? void 0 : window["_".concat(namespace)]) || (0, cdp_js_1.cdpQueue)(namespace);
250
251
  };
252
+ /*
253
+ * The current user and anonymous ids as the CDP knows them.
254
+ *
255
+ * Exposed to source middleware, which on Segment could read these from the
256
+ * event facade. Either CDP may be absent or may not implement `user()`, so
257
+ * every step is guarded and an unknown id is simply undefined.
258
+ */
259
+ this.currentUser = function () {
260
+ var read = function (analytics) {
261
+ var _a, _b, _c;
262
+ try {
263
+ var user = (_a = analytics === null || analytics === void 0 ? void 0 : analytics.user) === null || _a === void 0 ? void 0 : _a.call(analytics);
264
+ if (!user)
265
+ return undefined;
266
+ var userId = (_b = user.userId) !== null && _b !== void 0 ? _b : undefined;
267
+ var anonymousId = (_c = user.anonymousId) !== null && _c !== void 0 ? _c : undefined;
268
+ if (userId === undefined && anonymousId === undefined)
269
+ return undefined;
270
+ return { userId: userId, anonymousId: anonymousId };
271
+ }
272
+ catch (_d) {
273
+ return undefined;
274
+ }
275
+ };
276
+ return read(_this.ccdp()) || read(_this.cdp()) || {};
277
+ };
251
278
  /*
252
279
  * Log a message to the console if `options.enableLogging` is true.
253
280
  */
@@ -509,44 +536,77 @@ var ChordAnalytics = /** @class */ (function () {
509
536
  * Internal method to actually send a track event to CDP.
510
537
  */
511
538
  this.sendTrackEvent = function (event, props, options) { return __awaiter(_this, void 0, void 0, function () {
512
- var formattedProps, middlewareProps, searchParams_1, urlSearchParams, searchData_1, finalEventProps;
539
+ var formattedProps, middlewareProps, searchParams_1, urlSearchParams, searchData_1, _a, userId_1, anonymousId_1, invoke, settle, returned, results, _b, finalEventProps;
513
540
  var _this = this;
514
- return __generator(this, function (_a) {
515
- switch (_a.label) {
541
+ return __generator(this, function (_c) {
542
+ switch (_c.label) {
516
543
  case 0:
517
544
  formattedProps = this.options.stripNull && props ? (0, utils_js_1.pruneNullValues)(props) : props;
518
545
  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
- },
546
+ if (!(this.middleware.length > 0)) return [3 /*break*/, 4];
547
+ searchParams_1 = typeof window !== 'undefined' ? window.location.search : '';
548
+ urlSearchParams = new URLSearchParams(searchParams_1);
549
+ searchData_1 = Object.fromEntries(urlSearchParams.entries());
550
+ _a = this.currentUser(), userId_1 = _a.userId, anonymousId_1 = _a.anonymousId;
551
+ invoke = function (func) {
552
+ try {
553
+ return func({
554
+ payload: {
555
+ userId: userId_1,
556
+ anonymousId: anonymousId_1,
557
+ obj: {
558
+ properties: props,
559
+ event: event,
560
+ context: {
561
+ page: {
562
+ search: searchParams_1,
563
+ searchParams: searchData_1,
536
564
  },
537
565
  },
538
566
  },
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);
567
+ },
568
+ next: function (_) { return _; },
569
+ });
570
+ }
571
+ catch (error) {
572
+ console.warn('Middleware error:', error);
573
+ return undefined;
574
+ }
575
+ };
576
+ settle = function (returned) {
577
+ if (!(0, utils_js_1.isPromise)(returned))
578
+ return returned;
579
+ return (0, utils_js_1.withTimeout)(returned, utils_js_1.MIDDLEWARE_TIMEOUT_MS)
580
+ .then(function (settled) {
581
+ if (settled === utils_js_1.TIMED_OUT) {
582
+ console.warn("Middleware timed out after ".concat(utils_js_1.MIDDLEWARE_TIMEOUT_MS, "ms; its properties were dropped"));
583
+ return undefined;
547
584
  }
585
+ return settled;
586
+ })
587
+ .catch(function (error) {
588
+ console.warn('Middleware error:', error);
589
+ return undefined;
548
590
  });
549
- }
591
+ };
592
+ returned = this.middleware.map(invoke);
593
+ if (!returned.some(utils_js_1.isPromise)) return [3 /*break*/, 2];
594
+ return [4 /*yield*/, Promise.all(returned.map(settle))];
595
+ case 1:
596
+ _b = _c.sent();
597
+ return [3 /*break*/, 3];
598
+ case 2:
599
+ _b = returned;
600
+ _c.label = 3;
601
+ case 3:
602
+ results = _b;
603
+ results.forEach(function (result) {
604
+ if (result && typeof result === 'object') {
605
+ middlewareProps = __assign(__assign({}, middlewareProps), result);
606
+ }
607
+ });
608
+ _c.label = 4;
609
+ case 4:
550
610
  finalEventProps = __assign(__assign(__assign({}, formattedProps), middlewareProps), { meta: this.meta() });
551
611
  if (this.options.debug) {
552
612
  this.validate(event, finalEventProps).forEach(function (valid) {
@@ -584,8 +644,8 @@ var ChordAnalytics = /** @class */ (function () {
584
644
  resolve();
585
645
  }
586
646
  })];
587
- case 1:
588
- _a.sent();
647
+ case 5:
648
+ _c.sent();
589
649
  return [2 /*return*/];
590
650
  }
591
651
  });
@@ -777,6 +837,9 @@ var ChordAnalytics = /** @class */ (function () {
777
837
  switch (_b.label) {
778
838
  case 0:
779
839
  _b.trys.push([0, 4, 5, 6]);
840
+ // The new identity has no consent event of its own yet, so drop the
841
+ // stored signature and let the next init emit a fresh baseline.
842
+ (0, persistence_js_1.forgetConsent)(this.options.namespace);
780
843
  a = this.ccdp();
781
844
  if (a && typeof a.reset === 'function') {
782
845
  a.reset();
@@ -1481,7 +1544,7 @@ var ChordAnalytics = /** @class */ (function () {
1481
1544
  */
1482
1545
  ChordAnalytics.prototype.initializeCdp = function () {
1483
1546
  return __awaiter(this, void 0, void 0, function () {
1484
- var snippetPromise, initialConsent, error_4;
1547
+ var snippetPromise, initialConsent, namespace, error_4, error_5;
1485
1548
  return __generator(this, function (_a) {
1486
1549
  switch (_a.label) {
1487
1550
  case 0:
@@ -1491,7 +1554,7 @@ var ChordAnalytics = /** @class */ (function () {
1491
1554
  }
1492
1555
  _a.label = 1;
1493
1556
  case 1:
1494
- _a.trys.push([1, 6, , 7]);
1557
+ _a.trys.push([1, 9, , 10]);
1495
1558
  snippetPromise = (0, cdp_js_1.addCdpSnippet)(this.options);
1496
1559
  // Create a void promise for cdpReadyPromise that resolves when snippet is done
1497
1560
  // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -1505,32 +1568,57 @@ var ChordAnalytics = /** @class */ (function () {
1505
1568
  // eslint-disable-next-line no-console
1506
1569
  console.log('[Chord Analytics DEBUG]: consentCategories defined?', (initialConsent === null || initialConsent === void 0 ? void 0 : initialConsent.consentCategories) !== undefined);
1507
1570
  }
1508
- if (!(initialConsent && initialConsent.consentCategories !== undefined)) return [3 /*break*/, 4];
1571
+ namespace = this.options.namespace;
1572
+ if (!(initialConsent && initialConsent.consentCategories !== undefined)) return [3 /*break*/, 7];
1573
+ if (!(0, persistence_js_1.claimConsentEmit)(namespace, initialConsent)) {
1574
+ if (this.options.enableLogging && this.options.debug) {
1575
+ // eslint-disable-next-line no-console
1576
+ console.log('[Chord Analytics DEBUG]: Initial consent unchanged or emit already in flight, skipping track');
1577
+ }
1578
+ return [2 /*return*/];
1579
+ }
1509
1580
  if (this.options.enableLogging && this.options.debug) {
1510
1581
  // eslint-disable-next-line no-console
1511
1582
  console.log('[Chord Analytics DEBUG]: About to call this.track() for Consent Preferences Updated');
1512
1583
  }
1513
- return [4 /*yield*/, this.track('Consent Preferences Updated', initialConsent)];
1584
+ _a.label = 3;
1514
1585
  case 3:
1586
+ _a.trys.push([3, 5, , 6]);
1587
+ return [4 /*yield*/, this.track('Consent Preferences Updated', initialConsent)
1588
+ // Only recorded once delivery resolves. Committing before this would
1589
+ // suppress the event permanently if it were queued and never
1590
+ // flushed — the next page load must be free to retry.
1591
+ ];
1592
+ case 4:
1515
1593
  _a.sent();
1594
+ // Only recorded once delivery resolves. Committing before this would
1595
+ // suppress the event permanently if it were queued and never
1596
+ // flushed — the next page load must be free to retry.
1597
+ (0, persistence_js_1.commitConsentEmit)(namespace, initialConsent);
1598
+ return [3 /*break*/, 6];
1599
+ case 5:
1600
+ error_4 = _a.sent();
1601
+ (0, persistence_js_1.releaseConsentEmit)(namespace, initialConsent);
1602
+ throw error_4;
1603
+ case 6:
1516
1604
  if (this.options.enableLogging && this.options.debug) {
1517
1605
  // eslint-disable-next-line no-console
1518
1606
  console.log('[Chord Analytics DEBUG]: Initial consent preferences tracked');
1519
1607
  }
1520
- return [3 /*break*/, 5];
1521
- case 4:
1608
+ return [3 /*break*/, 8];
1609
+ case 7:
1522
1610
  if (this.options.enableLogging && this.options.debug) {
1523
1611
  // eslint-disable-next-line no-console
1524
1612
  console.log('[Chord Analytics DEBUG]: Skipping initial consent track - no consent or no consentCategories');
1525
1613
  }
1526
- _a.label = 5;
1527
- case 5: return [3 /*break*/, 7];
1528
- case 6:
1529
- error_4 = _a.sent();
1614
+ _a.label = 8;
1615
+ case 8: return [3 /*break*/, 10];
1616
+ case 9:
1617
+ error_5 = _a.sent();
1530
1618
  // eslint-disable-next-line no-console
1531
- console.error('CDP initialization failed:', error_4);
1532
- return [3 /*break*/, 7];
1533
- case 7: return [2 /*return*/];
1619
+ console.error('CDP initialization failed:', error_5);
1620
+ return [3 /*break*/, 10];
1621
+ case 10: return [2 /*return*/];
1534
1622
  }
1535
1623
  });
1536
1624
  });