@chordcommerce/analytics 1.14.0 → 1.15.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.
@@ -8,15 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { addCdpConsent } from './consent/index.js';
11
+ import { normalizeCDPDomain } from './utils.js';
11
12
  /*
12
13
  * Adds the Chord CDP snippet to the page.
13
14
  */
14
15
  export const addCdpSnippet = (options) => __awaiter(void 0, void 0, void 0, function* () {
15
16
  if (!document || typeof document.createElement === 'undefined')
16
17
  return;
18
+ const normalizedCDPDomain = normalizeCDPDomain(options.cdpDomain);
17
19
  const s = document.createElement('script');
18
20
  s.async = true;
19
- s.src = `${options.cdpDomain}/p.js`;
21
+ s.src = `${normalizedCDPDomain}/p.js`;
20
22
  s.setAttribute('data-debug', `${!!options.debug}`);
21
23
  s.setAttribute('data-init-only', 'true');
22
24
  s.setAttribute('data-namespace', `_${options.namespace}`);
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { ketchConsentAdapter } from './ketch.js';
11
11
  import { onetrustConsentAdapter } from './onetrust.js';
12
12
  import { shopifyConsentAdapter } from './shopify.js';
13
+ import { segmentConsentAdapter } from './segment.js';
13
14
  export const addCdpConsent = (options) => {
14
15
  let adapter = null;
15
16
  let lastProcessedConsent = null;
@@ -27,6 +28,8 @@ export const addCdpConsent = (options) => {
27
28
  return onetrustConsentAdapter();
28
29
  case 'shopify':
29
30
  return shopifyConsentAdapter();
31
+ case 'segment':
32
+ return segmentConsentAdapter();
30
33
  default:
31
34
  return null;
32
35
  }
@@ -54,6 +57,35 @@ export const addCdpConsent = (options) => {
54
57
  }, TIMEOUT_MS);
55
58
  });
56
59
  });
60
+ /**
61
+ * Attempts to configure CDP with retry logic.
62
+ */
63
+ const configureCdpWithRetry = (updatedConsent, maxRetries = 5, baseDelay = 100) => __awaiter(void 0, void 0, void 0, function* () {
64
+ let attempt = 0;
65
+ const tryConfigure = () => __awaiter(void 0, void 0, void 0, function* () {
66
+ const cdp = window[`_${options.namespace}`];
67
+ if (cdp && typeof cdp.configure === 'function') {
68
+ try {
69
+ cdp.configure({ privacy: updatedConsent });
70
+ return true;
71
+ }
72
+ catch (error) {
73
+ // Continue to retry on error
74
+ }
75
+ }
76
+ if (attempt < maxRetries) {
77
+ const delay = baseDelay * Math.pow(2, attempt); // Exponential backoff
78
+ yield new Promise((resolve) => {
79
+ setTimeout(resolve, delay);
80
+ });
81
+ attempt++;
82
+ return tryConfigure();
83
+ }
84
+ console.warn('CDP configuration failed after all retries, falling back to jitsuConfig');
85
+ return false;
86
+ });
87
+ return tryConfigure();
88
+ });
57
89
  /**
58
90
  * Updates consent for all future events.
59
91
  */
@@ -64,7 +96,6 @@ export const addCdpConsent = (options) => {
64
96
  }
65
97
  lastProcessedConsent = updatedConsent;
66
98
  const chordAnalytics = window[options.namespace];
67
- const cdp = window[`_${options.namespace}`];
68
99
  const shouldTrackConsentUpdate = chordAnalytics &&
69
100
  typeof chordAnalytics.track === 'function' &&
70
101
  updatedConsent.consentCategories !== undefined;
@@ -73,11 +104,9 @@ export const addCdpConsent = (options) => {
73
104
  updatedConsent.dontSend === true) {
74
105
  yield chordAnalytics.track('Consent Preferences Updated', updatedConsent);
75
106
  }
76
- if (cdp && typeof cdp.configure === 'function') {
77
- cdp.configure({ privacy: updatedConsent });
78
- }
79
- else {
80
- // CDP has not yet loaded via the snippet.
107
+ const cdpConfigured = yield configureCdpWithRetry(updatedConsent);
108
+ if (!cdpConfigured) {
109
+ // CDP has not yet loaded via the snippet or all retries failed.
81
110
  window.jitsuConfig = { privacy: updatedConsent };
82
111
  }
83
112
  if (shouldTrackConsentUpdate &&
@@ -97,8 +126,8 @@ export const addCdpConsent = (options) => {
97
126
  updateCdpConsent(updatedConsent);
98
127
  });
99
128
  // Process initial consent - this will be skipped if the adapter already triggered an update
100
- const initialConsent = adapter.getCurrentConsent();
101
- updateCdpConsent(initialConsent);
129
+ const initialConsent = yield adapter.getCurrentConsent();
130
+ yield updateCdpConsent(initialConsent);
102
131
  });
103
132
  /**
104
133
  * Initializes consent management.
@@ -0,0 +1,17 @@
1
+ import type { ConsentConfiguration } from '../../types/index.js';
2
+ declare global {
3
+ interface Window {
4
+ segmentConsent?: SegmentConsentCategories;
5
+ }
6
+ }
7
+ interface SegmentConsentCategories {
8
+ [key: string]: {
9
+ [key: string]: boolean;
10
+ };
11
+ }
12
+ export declare const segmentConsentAdapter: () => {
13
+ getCurrentConsent: () => Promise<ConsentConfiguration>;
14
+ handleConsentUpdates: (updateCdpConsent: (consent: ConsentConfiguration) => void) => void;
15
+ userCanBeTracked: () => Promise<boolean>;
16
+ };
17
+ export {};
@@ -0,0 +1,74 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ const getApi = () => window.segmentConsent;
11
+ /**
12
+ * Waits for the Segment consent API to be populated with retry logic.
13
+ */
14
+ const waitForApi = (maxRetries = 10, baseDelay = 100) => __awaiter(void 0, void 0, void 0, function* () {
15
+ let attempt = 0;
16
+ const checkAndWait = () => __awaiter(void 0, void 0, void 0, function* () {
17
+ const api = getApi();
18
+ if (api &&
19
+ typeof api === 'object' &&
20
+ api.customPreferences &&
21
+ typeof api.customPreferences === 'object' &&
22
+ Object.keys(api.customPreferences).length > 0) {
23
+ return api;
24
+ }
25
+ if (attempt < maxRetries) {
26
+ const delay = baseDelay * Math.pow(2, attempt);
27
+ yield new Promise((resolve) => {
28
+ setTimeout(resolve, delay);
29
+ });
30
+ attempt++;
31
+ return checkAndWait();
32
+ }
33
+ return null;
34
+ });
35
+ return checkAndWait();
36
+ console.warn('Segment API failed to load after all retries');
37
+ return null;
38
+ });
39
+ /**
40
+ * Returns whether the user can currently be tracked.
41
+ */
42
+ const userCanBeTracked = () => __awaiter(void 0, void 0, void 0, function* () {
43
+ const api = yield waitForApi();
44
+ return (!!api &&
45
+ !!(api.customPreferences.functional &&
46
+ api.customPreferences.marketingAndAnalytics));
47
+ });
48
+ /**
49
+ * Returns the user's current consent configuration.
50
+ */
51
+ const getCurrentConsent = () => __awaiter(void 0, void 0, void 0, function* () {
52
+ const api = yield waitForApi();
53
+ if (!api)
54
+ return {};
55
+ return {
56
+ consentCategories: Object.assign(Object.assign({}, api.customPreferences), api.destinationPreferences),
57
+ dontSend: !(yield userCanBeTracked()),
58
+ };
59
+ });
60
+ /**
61
+ * Handles subsequent changes to consent configuration.
62
+ */
63
+ const handleConsentUpdates = (updateCdpConsent) => {
64
+ document.addEventListener('chord-cdp:consent-updated', () => __awaiter(void 0, void 0, void 0, function* () {
65
+ updateCdpConsent(yield getCurrentConsent());
66
+ }));
67
+ };
68
+ export const segmentConsentAdapter = () => {
69
+ return {
70
+ getCurrentConsent,
71
+ handleConsentUpdates,
72
+ userCanBeTracked,
73
+ };
74
+ };
@@ -0,0 +1 @@
1
+ export declare function normalizeCDPDomain(cdpDomain: string): string;
@@ -0,0 +1,8 @@
1
+ export function normalizeCDPDomain(cdpDomain) {
2
+ let domain = cdpDomain.replace(/\/+$/, '');
3
+ if (domain.endsWith('/p.js')) {
4
+ domain = domain.slice(0, -'/p.js'.length);
5
+ domain = domain.replace(/\/+$/, '');
6
+ }
7
+ return domain;
8
+ }
@@ -30,7 +30,7 @@ export interface ChordAnalyticsOptions<T extends ObjectTypes = ObjectTypes> {
30
30
  cdp?: AnyAnalytics | (() => AnyAnalytics);
31
31
  cdpDomain?: string;
32
32
  cdpWriteKey?: string;
33
- consent?: 'ketch' | 'onetrust' | 'shopify';
33
+ consent?: 'ketch' | 'onetrust' | 'shopify' | 'segment';
34
34
  awaitConsent?: boolean;
35
35
  debug?: boolean;
36
36
  enableLogging?: boolean;
@@ -1,7 +1,7 @@
1
1
  export interface ConsentAdapter {
2
- getCurrentConsent: () => ConsentConfiguration;
2
+ getCurrentConsent: () => ConsentConfiguration | Promise<ConsentConfiguration>;
3
3
  handleConsentUpdates: (fn: (ConsentConfiguration: any) => void) => void;
4
- userCanBeTracked: () => boolean;
4
+ userCanBeTracked: () => boolean | Promise<boolean>;
5
5
  }
6
6
  export interface ConsentConfiguration {
7
7
  consentCategories?: {
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.14.0",
4
+ "version": "1.15.0",
5
5
  "author": "Chord Commerce <help@chord.co> (https://www.chord.co)",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",