@chordcommerce/analytics 1.18.3-beta.1 → 1.19.0

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.
Files changed (35) hide show
  1. package/dist/cjs/client/cdp.d.ts +5 -2
  2. package/dist/cjs/client/cdp.js +15 -7
  3. package/dist/cjs/client/consent/index.d.ts +6 -8
  4. package/dist/cjs/client/consent/index.js +55 -89
  5. package/dist/cjs/client/consent/ketch.d.ts +2 -0
  6. package/dist/cjs/client/consent/ketch.js +5 -1
  7. package/dist/cjs/client/consent/onetrust-api.d.ts +103 -0
  8. package/dist/cjs/client/consent/onetrust-api.js +128 -0
  9. package/dist/cjs/client/consent/onetrust.d.ts +4 -16
  10. package/dist/cjs/client/consent/onetrust.js +25 -34
  11. package/dist/cjs/client/consent/segment.d.ts +2 -0
  12. package/dist/cjs/client/consent/segment.js +16 -10
  13. package/dist/cjs/client/consent/shopify.d.ts +2 -0
  14. package/dist/cjs/client/consent/shopify.js +12 -1
  15. package/dist/cjs/client/index.d.ts +54 -2
  16. package/dist/cjs/client/index.js +375 -81
  17. package/dist/cjs/types/consent.d.ts +13 -2
  18. package/dist/client/cdp.d.ts +5 -2
  19. package/dist/client/cdp.js +13 -5
  20. package/dist/client/consent/index.d.ts +6 -8
  21. package/dist/client/consent/index.js +50 -62
  22. package/dist/client/consent/ketch.d.ts +2 -0
  23. package/dist/client/consent/ketch.js +5 -1
  24. package/dist/client/consent/onetrust-api.d.ts +103 -0
  25. package/dist/client/consent/onetrust-api.js +119 -0
  26. package/dist/client/consent/onetrust.d.ts +4 -16
  27. package/dist/client/consent/onetrust.js +25 -33
  28. package/dist/client/consent/segment.d.ts +2 -0
  29. package/dist/client/consent/segment.js +10 -3
  30. package/dist/client/consent/shopify.d.ts +2 -0
  31. package/dist/client/consent/shopify.js +12 -1
  32. package/dist/client/index.d.ts +54 -2
  33. package/dist/client/index.js +244 -10
  34. package/dist/types/consent.d.ts +13 -2
  35. package/package.json +1 -1
@@ -30,10 +30,24 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
30
30
  * Promise that resolves when CDP is ready
31
31
  */
32
32
  private cdpReadyPromise;
33
+ private consentConfig;
33
34
  private resetInProgress;
35
+ /**
36
+ * Queue for events waiting for consent to be ready
37
+ */
38
+ private consentEventQueue;
39
+ /**
40
+ * Whether we're currently waiting for consent and watching for it
41
+ */
42
+ private consentWatcherActive;
43
+ /**
44
+ * Promise that resolves when consent is ready (shared across all queued events)
45
+ */
46
+ private consentReadyPromise;
34
47
  constructor(options: ChordAnalyticsOptions<T>);
35
48
  /**
36
- * Initialize CDP snippet and wait for it to be ready
49
+ * Initialize CDP snippet and wait for it to be ready.
50
+ * Fires initial consent track event after CDP is fully initialized to avoid circular await.
37
51
  */
38
52
  private initializeCdp;
39
53
  /**
@@ -45,6 +59,29 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
45
59
  ccdp: () => AnyAnalytics;
46
60
  logger: (message?: any, ...optionalParams: any[]) => void;
47
61
  isResetting: () => boolean;
62
+ /**
63
+ * Wait for consent manager to be ready (no timeout - queues indefinitely).
64
+ * Uses a shared promise so multiple events share the same watcher.
65
+ */
66
+ private waitForConsent;
67
+ /**
68
+ * Queue an event to be sent when consent is ready.
69
+ * Starts a consent watcher if not already active.
70
+ */
71
+ private queueEventForConsent;
72
+ /**
73
+ * Watch for consent to become ready, then flush the queue.
74
+ */
75
+ private watchForConsentAndFlush;
76
+ /**
77
+ * Flush all queued events now that consent is ready.
78
+ */
79
+ private flushConsentQueue;
80
+ /**
81
+ * Configure CDP with fresh consent categories before each event.
82
+ * Uses Jitsu's configure() to update privacy.consentCategories.
83
+ */
84
+ private configureConsentOnCdp;
48
85
  validate: (event: string, props: EventProperties) => ValidateResult[];
49
86
  /**
50
87
  * Generate the event `meta` property.
@@ -67,21 +104,36 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
67
104
  addSourceMiddleware: (func: AnalyticsMiddleware) => void;
68
105
  /**
69
106
  * Send a `track` event to the CDP with any event name and properties.
107
+ * If consent is not ready, the event is queued until consent is available.
70
108
  */
71
109
  track: (event: string, props?: EventProperties, options?: Options) => Promise<void>;
110
+ /**
111
+ * Internal method to actually send a track event to CDP.
112
+ */
113
+ private sendTrackEvent;
72
114
  /**
73
115
  * Send an `identify` event to the CDP with user id and traits.
116
+ * If consent is not ready, the event is queued until consent is available.
74
117
  */
75
118
  identify: {
76
119
  (userId?: string, traits?: IdentifyTraits, options?: AnyOptions): Promise<void>;
77
120
  (traits?: IdentifyTraits, options?: AnyOptions): Promise<void>;
78
121
  };
122
+ /**
123
+ * Internal method to actually send an identify event to CDP.
124
+ */
125
+ private sendIdentifyEvent;
79
126
  /**
80
127
  * Send a `page` event to the CDP.
128
+ * If consent is not ready, the event is queued until consent is available.
81
129
  */
82
130
  page: {
83
- (props?: Record<string, any>): void;
131
+ (props?: Record<string, any>, options?: Options): void;
84
132
  };
133
+ /**
134
+ * Internal method to actually send a page event to CDP.
135
+ */
136
+ private sendPageEvent;
85
137
  reset: () => Promise<void>;
86
138
  trackCartViewed: (props: CartViewedInput<T>, options?: AnyOptions) => Promise<void>;
87
139
  trackCheckoutStarted: (props: CheckoutStartedInput<T>, options?: AnyOptions) => Promise<void>;
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { addCdpSnippet, cdpQueue } from './cdp.js';
11
+ import { addCdpConsent } from './consent/index.js';
11
12
  import { eventSchemas } from '../validators/index.js';
12
13
  import { pruneNullValues } from '../utils.js';
13
14
  const DEBUG_MODE_STORAGE_KEY = 'chordcommerce/analytics/debug';
@@ -30,7 +31,20 @@ export class ChordAnalytics {
30
31
  * Promise that resolves when CDP is ready
31
32
  */
32
33
  this.cdpReadyPromise = null;
34
+ this.consentConfig = null;
33
35
  this.resetInProgress = false;
36
+ /**
37
+ * Queue for events waiting for consent to be ready
38
+ */
39
+ this.consentEventQueue = [];
40
+ /**
41
+ * Whether we're currently waiting for consent and watching for it
42
+ */
43
+ this.consentWatcherActive = false;
44
+ /**
45
+ * Promise that resolves when consent is ready (shared across all queued events)
46
+ */
47
+ this.consentReadyPromise = null;
34
48
  /**
35
49
  * Set debug mode for Chord Analytics
36
50
  * @param enabled - Whether debug mode should be enabled
@@ -72,6 +86,102 @@ export class ChordAnalytics {
72
86
  this.isResetting = () => {
73
87
  return this.resetInProgress;
74
88
  };
89
+ /**
90
+ * Wait for consent manager to be ready (no timeout - queues indefinitely).
91
+ * Uses a shared promise so multiple events share the same watcher.
92
+ */
93
+ this.waitForConsent = () => __awaiter(this, void 0, void 0, function* () {
94
+ if (!this.consentConfig)
95
+ return;
96
+ if (this.consentConfig.isConsentReady())
97
+ return;
98
+ // If already waiting, return the existing promise
99
+ if (this.consentReadyPromise) {
100
+ yield this.consentReadyPromise;
101
+ return;
102
+ }
103
+ // Start a new consent watcher - polls until consent is ready
104
+ this.consentReadyPromise = new Promise((resolve) => {
105
+ const interval = setInterval(() => {
106
+ var _a;
107
+ if ((_a = this.consentConfig) === null || _a === void 0 ? void 0 : _a.isConsentReady()) {
108
+ clearInterval(interval);
109
+ this.consentReadyPromise = null;
110
+ resolve(true);
111
+ }
112
+ }, 100);
113
+ });
114
+ yield this.consentReadyPromise;
115
+ });
116
+ /**
117
+ * Queue an event to be sent when consent is ready.
118
+ * Starts a consent watcher if not already active.
119
+ */
120
+ this.queueEventForConsent = (type, args) => {
121
+ return new Promise((resolve) => {
122
+ this.consentEventQueue.push({ type, args, resolve });
123
+ // Start watching for consent if not already
124
+ if (!this.consentWatcherActive) {
125
+ this.consentWatcherActive = true;
126
+ this.watchForConsentAndFlush();
127
+ }
128
+ });
129
+ };
130
+ /**
131
+ * Watch for consent to become ready, then flush the queue.
132
+ */
133
+ this.watchForConsentAndFlush = () => __awaiter(this, void 0, void 0, function* () {
134
+ yield this.waitForConsent();
135
+ yield this.flushConsentQueue();
136
+ this.consentWatcherActive = false;
137
+ });
138
+ /**
139
+ * Flush all queued events now that consent is ready.
140
+ */
141
+ this.flushConsentQueue = () => __awaiter(this, void 0, void 0, function* () {
142
+ const queue = [...this.consentEventQueue];
143
+ this.consentEventQueue = [];
144
+ const sendPromises = queue.map((item) => __awaiter(this, void 0, void 0, function* () {
145
+ try {
146
+ switch (item.type) {
147
+ case 'track':
148
+ yield this.sendTrackEvent(item.args[0], item.args[1], item.args[2]);
149
+ break;
150
+ case 'page':
151
+ yield this.sendPageEvent(item.args[0], item.args[1]);
152
+ break;
153
+ case 'identify':
154
+ yield this.sendIdentifyEvent(item.args[0], item.args[1], item.args[2]);
155
+ break;
156
+ default:
157
+ break;
158
+ }
159
+ }
160
+ catch (error) {
161
+ this.logger('[Chord Analytics]: Error sending queued event:', error);
162
+ }
163
+ item.resolve();
164
+ }));
165
+ yield Promise.all(sendPromises);
166
+ });
167
+ /**
168
+ * Configure CDP with fresh consent categories before each event.
169
+ * Uses Jitsu's configure() to update privacy.consentCategories.
170
+ */
171
+ this.configureConsentOnCdp = () => {
172
+ if (!this.consentConfig)
173
+ return;
174
+ const categories = this.consentConfig.getCategories();
175
+ const privacyConfig = { privacy: { consentCategories: categories } };
176
+ const a = this.ccdp();
177
+ if (a && typeof a.configure === 'function') {
178
+ a.configure(privacyConfig);
179
+ }
180
+ const b = this.cdp();
181
+ if (b && typeof b.configure === 'function') {
182
+ b.configure(privacyConfig);
183
+ }
184
+ };
75
185
  this.validate = (event, props) => {
76
186
  if (!event) {
77
187
  this.logger('No event name provided');
@@ -104,14 +214,46 @@ export class ChordAnalytics {
104
214
  };
105
215
  /**
106
216
  * Send a `track` event to the CDP with any event name and properties.
217
+ * If consent is not ready, the event is queued until consent is available.
107
218
  */
108
219
  this.track = (event, props, options) => __awaiter(this, void 0, void 0, function* () {
220
+ if (this.options.enableLogging && this.options.debug) {
221
+ // eslint-disable-next-line no-console
222
+ console.log(`[Chord Analytics DEBUG]: track() called with event: "${event}"`);
223
+ }
109
224
  if (!event)
110
225
  this.logger('No event name provided');
111
226
  // Wait for CDP to be ready before sending track events
112
227
  if (this.cdpReadyPromise) {
228
+ if (this.options.enableLogging && this.options.debug) {
229
+ // eslint-disable-next-line no-console
230
+ console.log(`[Chord Analytics DEBUG]: track("${event}") waiting for cdpReadyPromise`);
231
+ }
113
232
  yield this.cdpReadyPromise;
233
+ if (this.options.enableLogging && this.options.debug) {
234
+ // eslint-disable-next-line no-console
235
+ console.log(`[Chord Analytics DEBUG]: track("${event}") cdpReadyPromise resolved`);
236
+ }
114
237
  }
238
+ // If consent is configured but not ready, queue the event
239
+ if (this.consentConfig && !this.consentConfig.isConsentReady()) {
240
+ if (this.options.enableLogging && this.options.debug) {
241
+ // eslint-disable-next-line no-console
242
+ console.log(`[Chord Analytics DEBUG]: track("${event}") consent not ready, queuing event`);
243
+ }
244
+ return this.queueEventForConsent('track', [event, props, options]);
245
+ }
246
+ if (this.options.enableLogging && this.options.debug) {
247
+ // eslint-disable-next-line no-console
248
+ console.log(`[Chord Analytics DEBUG]: track("${event}") sending event now`);
249
+ }
250
+ // Consent ready (or not configured) - send immediately
251
+ return this.sendTrackEvent(event, props, options);
252
+ });
253
+ /**
254
+ * Internal method to actually send a track event to CDP.
255
+ */
256
+ this.sendTrackEvent = (event, props, options) => __awaiter(this, void 0, void 0, function* () {
115
257
  const formattedProps = this.options.stripNull && props ? pruneNullValues(props) : props;
116
258
  let middlewareProps = {};
117
259
  if (this.middleware.length > 0) {
@@ -153,7 +295,9 @@ export class ChordAnalytics {
153
295
  }
154
296
  });
155
297
  }
156
- return new Promise((resolve) => {
298
+ // Configure CDP with fresh consent before sending
299
+ this.configureConsentOnCdp();
300
+ yield new Promise((resolve) => {
157
301
  const cdpPromises = [];
158
302
  try {
159
303
  const a = this.ccdp();
@@ -188,24 +332,65 @@ export class ChordAnalytics {
188
332
  });
189
333
  /**
190
334
  * Send an `identify` event to the CDP with user id and traits.
335
+ * If consent is not ready, the event is queued until consent is available.
191
336
  */
192
- this.identify = (...props) => __awaiter(this, void 0, void 0, function* () {
337
+ this.identify = (userIdOrTraits, traitsOrOptions, options) => __awaiter(this, void 0, void 0, function* () {
193
338
  // Wait for CDP to be ready before sending identify events
194
339
  if (this.cdpReadyPromise) {
195
340
  yield this.cdpReadyPromise;
196
341
  }
342
+ // If consent is configured but not ready, queue the event
343
+ if (this.consentConfig && !this.consentConfig.isConsentReady()) {
344
+ return this.queueEventForConsent('identify', [
345
+ userIdOrTraits,
346
+ traitsOrOptions,
347
+ options,
348
+ ]);
349
+ }
350
+ // Consent ready (or not configured) - send immediately
351
+ return this.sendIdentifyEvent(userIdOrTraits, traitsOrOptions, options);
352
+ });
353
+ /**
354
+ * Internal method to actually send an identify event to CDP.
355
+ */
356
+ this.sendIdentifyEvent = (userIdOrTraits, traitsOrOptions, options) => __awaiter(this, void 0, void 0, function* () {
357
+ // Parse arguments to extract userId, traits, and options
358
+ let userId;
359
+ let traits;
360
+ let opts;
361
+ if (typeof userIdOrTraits === 'string') {
362
+ userId = userIdOrTraits;
363
+ traits = traitsOrOptions;
364
+ opts = options;
365
+ }
366
+ else {
367
+ traits = userIdOrTraits;
368
+ opts = traitsOrOptions;
369
+ }
370
+ // Configure CDP with fresh consent before sending
371
+ this.configureConsentOnCdp();
197
372
  try {
198
373
  const identifyPromises = [];
199
374
  const a = this.ccdp();
200
375
  if (a && typeof a.identify === 'function') {
201
376
  identifyPromises.push(new Promise((resolve) => {
202
- a.identify(...props, () => resolve());
377
+ if (userId) {
378
+ a.identify(userId, traits, opts, () => resolve());
379
+ }
380
+ else {
381
+ a.identify(traits, opts, () => resolve());
382
+ }
203
383
  }));
204
384
  }
205
385
  const b = this.cdp();
206
386
  if (b && typeof b.identify === 'function') {
207
387
  identifyPromises.push(new Promise((resolve) => {
208
- b.identify(...props, () => resolve());
388
+ if (userId) {
389
+ b.identify(userId, traits, opts, () => resolve());
390
+ }
391
+ else {
392
+ b.identify(traits, opts, () => resolve());
393
+ }
209
394
  }));
210
395
  }
211
396
  const results = yield Promise.allSettled(identifyPromises);
@@ -217,24 +402,38 @@ export class ChordAnalytics {
217
402
  });
218
403
  /**
219
404
  * Send a `page` event to the CDP.
405
+ * If consent is not ready, the event is queued until consent is available.
220
406
  */
221
- this.page = (props = {}) => __awaiter(this, void 0, void 0, function* () {
407
+ this.page = (options, props = {}) => __awaiter(this, void 0, void 0, function* () {
222
408
  // Wait for CDP to be ready before sending page events
223
409
  if (this.cdpReadyPromise) {
224
410
  yield this.cdpReadyPromise;
225
411
  }
412
+ // If consent is configured but not ready, queue the event
413
+ if (this.consentConfig && !this.consentConfig.isConsentReady()) {
414
+ return this.queueEventForConsent('page', [options, props]);
415
+ }
416
+ // Consent ready (or not configured) - send immediately
417
+ return this.sendPageEvent(options, props);
418
+ });
419
+ /**
420
+ * Internal method to actually send a page event to CDP.
421
+ */
422
+ this.sendPageEvent = (options, props = {}) => __awaiter(this, void 0, void 0, function* () {
423
+ // Configure CDP with fresh consent before sending
424
+ this.configureConsentOnCdp();
226
425
  try {
227
426
  const pagePromises = [];
228
427
  const a = this.ccdp();
229
428
  if (a && typeof a.page === 'function') {
230
429
  pagePromises.push(new Promise((resolve) => {
231
- a.page(Object.assign(Object.assign({}, props), { meta: this.meta() }), () => resolve());
430
+ a.page(Object.assign(Object.assign({}, props), { meta: this.meta() }), options, () => resolve());
232
431
  }));
233
432
  }
234
433
  const b = this.cdp();
235
434
  if (b && typeof b.page === 'function') {
236
435
  pagePromises.push(new Promise((resolve) => {
237
- b.page(Object.assign(Object.assign({}, props), { meta: this.meta() }), () => resolve());
436
+ b.page(Object.assign(Object.assign({}, props), { meta: this.meta() }), options, () => resolve());
238
437
  }));
239
438
  }
240
439
  // We don't return the promise to maintain the void return type
@@ -680,21 +879,56 @@ export class ChordAnalytics {
680
879
  stripNull: true,
681
880
  };
682
881
  this.options = Object.assign(Object.assign({}, defaults), options);
882
+ if (this.options.consent) {
883
+ const consentManager = addCdpConsent(this.options);
884
+ this.consentConfig = consentManager.getConsentConfig() || null;
885
+ }
683
886
  if (((_b = this.options) === null || _b === void 0 ? void 0 : _b.cdpDomain) && ((_c = this.options) === null || _c === void 0 ? void 0 : _c.cdpWriteKey) && window) {
684
887
  window[this.options.namespace] = (window === null || window === void 0 ? void 0 : window[this.options.namespace]) || this;
685
888
  this.initializeCdp();
686
889
  }
687
890
  }
688
891
  /**
689
- * Initialize CDP snippet and wait for it to be ready
892
+ * Initialize CDP snippet and wait for it to be ready.
893
+ * Fires initial consent track event after CDP is fully initialized to avoid circular await.
690
894
  */
691
895
  initializeCdp() {
692
896
  return __awaiter(this, void 0, void 0, function* () {
897
+ if (this.options.enableLogging && this.options.debug) {
898
+ // eslint-disable-next-line no-console
899
+ console.log('[Chord Analytics DEBUG]: initializeCdp() started');
900
+ }
693
901
  try {
694
- this.cdpReadyPromise = addCdpSnippet(this.options);
695
- yield this.cdpReadyPromise;
902
+ const snippetPromise = addCdpSnippet(this.options);
903
+ // Create a void promise for cdpReadyPromise that resolves when snippet is done
904
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
905
+ this.cdpReadyPromise = snippetPromise.then(() => { });
906
+ const { initialConsent } = yield snippetPromise;
907
+ if (this.options.enableLogging && this.options.debug) {
908
+ // eslint-disable-next-line no-console
909
+ console.log('[Chord Analytics DEBUG]: snippetPromise resolved, initialConsent:', JSON.stringify(initialConsent));
910
+ // eslint-disable-next-line no-console
911
+ console.log('[Chord Analytics DEBUG]: consentCategories defined?', (initialConsent === null || initialConsent === void 0 ? void 0 : initialConsent.consentCategories) !== undefined);
912
+ }
913
+ // Now that CDP is fully ready, fire the initial consent track event
914
+ if (initialConsent && initialConsent.consentCategories !== undefined) {
915
+ if (this.options.enableLogging && this.options.debug) {
916
+ // eslint-disable-next-line no-console
917
+ console.log('[Chord Analytics DEBUG]: About to call this.track() for Consent Preferences Updated');
918
+ }
919
+ yield this.track('Consent Preferences Updated', initialConsent);
920
+ if (this.options.enableLogging && this.options.debug) {
921
+ // eslint-disable-next-line no-console
922
+ console.log('[Chord Analytics DEBUG]: Initial consent preferences tracked');
923
+ }
924
+ }
925
+ else if (this.options.enableLogging && this.options.debug) {
926
+ // eslint-disable-next-line no-console
927
+ console.log('[Chord Analytics DEBUG]: Skipping initial consent track - no consent or no consentCategories');
928
+ }
696
929
  }
697
930
  catch (error) {
931
+ // eslint-disable-next-line no-console
698
932
  console.error('CDP initialization failed:', error);
699
933
  }
700
934
  });
@@ -1,11 +1,22 @@
1
1
  export interface ConsentAdapter {
2
2
  getCurrentConsent: () => ConsentConfiguration | Promise<ConsentConfiguration>;
3
- handleConsentUpdates: (fn: (ConsentConfiguration: any) => void) => void;
3
+ handleConsentUpdates: (fn: (consent: ConsentConfiguration) => void) => void;
4
4
  userCanBeTracked: () => boolean | Promise<boolean>;
5
+ /**
6
+ * Get current consent categories - called FRESH on every event, no caching!
7
+ */
8
+ getCategories?: () => Record<string, boolean>;
9
+ /**
10
+ * Check if consent manager is ready (duck typing check)
11
+ */
12
+ isConsentReady?: () => boolean;
13
+ /**
14
+ * Get the consent model ('opt-in' or 'opt-out')
15
+ */
16
+ getConsentModel?: () => 'opt-in' | 'opt-out';
5
17
  }
6
18
  export interface ConsentConfiguration {
7
19
  consentCategories?: {
8
20
  [key: string]: boolean;
9
21
  };
10
- dontSend?: boolean;
11
22
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chordcommerce/analytics",
3
3
  "description": "Chord Commerce event tracking",
4
- "version": "1.18.3-beta.1",
4
+ "version": "1.19.0",
5
5
  "author": "Chord Commerce <help@chord.co> (https://www.chord.co)",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",