@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.
- package/README.md +28 -2
- package/dist/cjs/client/consent/index.js +49 -33
- package/dist/cjs/client/consent/persistence.d.ts +49 -0
- package/dist/cjs/client/consent/persistence.js +149 -0
- package/dist/cjs/client/index.d.ts +5 -42
- package/dist/cjs/client/index.js +334 -490
- package/dist/cjs/setupTests.js +14 -4
- package/dist/cjs/types/client.d.ts +14 -1
- package/dist/cjs/utils.d.ts +19 -0
- package/dist/cjs/utils.js +79 -1
- package/dist/client/consent/index.js +29 -14
- package/dist/client/consent/persistence.d.ts +49 -0
- package/dist/client/consent/persistence.js +135 -0
- package/dist/client/index.d.ts +5 -42
- package/dist/client/index.js +302 -439
- package/dist/setupTests.js +14 -4
- package/dist/types/client.d.ts +14 -1
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +39 -0
- package/package.json +11 -1
package/dist/cjs/setupTests.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
4
4
|
var globals_1 = require("@jest/globals");
|
|
5
|
+
var persistence_js_1 = require("./client/consent/persistence.js");
|
|
5
6
|
// Make jest available globally
|
|
6
7
|
globalThis.jest = globals_1.jest;
|
|
7
|
-
|
|
8
|
-
var sessionStorageMock = (function () {
|
|
8
|
+
var createStorageMock = function () {
|
|
9
9
|
var store = {};
|
|
10
10
|
return {
|
|
11
11
|
getItem: function (key) { return store[key] || null; },
|
|
@@ -19,11 +19,21 @@ var sessionStorageMock = (function () {
|
|
|
19
19
|
store = {};
|
|
20
20
|
},
|
|
21
21
|
};
|
|
22
|
-
}
|
|
22
|
+
};
|
|
23
|
+
var sessionStorageMock = createStorageMock();
|
|
24
|
+
var localStorageMock = createStorageMock();
|
|
23
25
|
Object.defineProperty(window, 'sessionStorage', {
|
|
24
26
|
value: sessionStorageMock,
|
|
25
27
|
});
|
|
26
|
-
|
|
28
|
+
Object.defineProperty(window, 'localStorage', {
|
|
29
|
+
value: localStorageMock,
|
|
30
|
+
});
|
|
31
|
+
// Clear storage before each test to ensure test isolation. localStorage now
|
|
32
|
+
// holds the last-tracked consent signature, which would otherwise leak between
|
|
33
|
+
// tests and suppress expected consent events. clearConsentMemory() covers the
|
|
34
|
+
// module-level fallback stores, which localStorage.clear() does not reach.
|
|
27
35
|
beforeEach(function () {
|
|
28
36
|
sessionStorageMock.clear();
|
|
37
|
+
localStorageMock.clear();
|
|
38
|
+
(0, persistence_js_1.clearConsentMemory)();
|
|
29
39
|
});
|
|
@@ -88,10 +88,23 @@ type MiddlewarePayload = {
|
|
|
88
88
|
page: PageContext;
|
|
89
89
|
};
|
|
90
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* The current user and anonymous ids, read from the CDP at send time.
|
|
93
|
+
* Segment's middleware payload exposes these as `userId()`/`anonymousId()`
|
|
94
|
+
* methods on a facade; they are plain values here.
|
|
95
|
+
*/
|
|
96
|
+
userId?: string;
|
|
97
|
+
anonymousId?: string;
|
|
91
98
|
};
|
|
92
99
|
type MiddlewareArgs = {
|
|
93
100
|
payload: MiddlewarePayload;
|
|
94
101
|
next: <T>(arg: T) => T;
|
|
95
102
|
};
|
|
96
|
-
export type
|
|
103
|
+
export type MiddlewareResult = Partial<EventProperties> | void;
|
|
104
|
+
/**
|
|
105
|
+
* A source middleware may be synchronous or asynchronous. An asynchronous one
|
|
106
|
+
* is awaited before the event is dispatched, bounded by a timeout — see
|
|
107
|
+
* `addSourceMiddleware` in the README.
|
|
108
|
+
*/
|
|
109
|
+
export type AnalyticsMiddleware = (args: MiddlewareArgs) => MiddlewareResult | Promise<MiddlewareResult>;
|
|
97
110
|
export {};
|
package/dist/cjs/utils.d.ts
CHANGED
|
@@ -1,2 +1,21 @@
|
|
|
1
1
|
import type { EventProperties } from './types/index.js';
|
|
2
2
|
export declare const pruneNullValues: (props: EventProperties) => EventProperties;
|
|
3
|
+
/**
|
|
4
|
+
* How long a single asynchronous source middleware may take before its
|
|
5
|
+
* contribution is abandoned.
|
|
6
|
+
*
|
|
7
|
+
* Middleware runs on the path to the CDP, so an unbounded wait would let one
|
|
8
|
+
* slow lookup hold up delivery indefinitely. `Order Completed` in particular
|
|
9
|
+
* fires on a confirmation page shoppers close, so a late event is a lost one.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MIDDLEWARE_TIMEOUT_MS = 1000;
|
|
12
|
+
/** Returned by {@link withTimeout} when the promise did not settle in time. */
|
|
13
|
+
export declare const TIMED_OUT: unique symbol;
|
|
14
|
+
export declare const isPromise: (value: unknown) => value is Promise<unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve `promise`, or {@link TIMED_OUT} if it has not settled within `ms`.
|
|
17
|
+
*
|
|
18
|
+
* The timer is cleared either way, so a promise that settles quickly does not
|
|
19
|
+
* leave a pending timeout behind.
|
|
20
|
+
*/
|
|
21
|
+
export declare const withTimeout: <T>(promise: Promise<T>, ms: number) => Promise<typeof TIMED_OUT | T>;
|
package/dist/cjs/utils.js
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
13
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
2
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.pruneNullValues = void 0;
|
|
39
|
+
exports.withTimeout = exports.isPromise = exports.TIMED_OUT = exports.MIDDLEWARE_TIMEOUT_MS = exports.pruneNullValues = void 0;
|
|
4
40
|
var pruneNullValues = function (props) {
|
|
5
41
|
if (typeof props !== 'object' || props === null)
|
|
6
42
|
return props;
|
|
@@ -23,3 +59,45 @@ var pruneNullValues = function (props) {
|
|
|
23
59
|
return props;
|
|
24
60
|
};
|
|
25
61
|
exports.pruneNullValues = pruneNullValues;
|
|
62
|
+
/**
|
|
63
|
+
* How long a single asynchronous source middleware may take before its
|
|
64
|
+
* contribution is abandoned.
|
|
65
|
+
*
|
|
66
|
+
* Middleware runs on the path to the CDP, so an unbounded wait would let one
|
|
67
|
+
* slow lookup hold up delivery indefinitely. `Order Completed` in particular
|
|
68
|
+
* fires on a confirmation page shoppers close, so a late event is a lost one.
|
|
69
|
+
*/
|
|
70
|
+
exports.MIDDLEWARE_TIMEOUT_MS = 1000;
|
|
71
|
+
/** Returned by {@link withTimeout} when the promise did not settle in time. */
|
|
72
|
+
exports.TIMED_OUT = Symbol('timed-out');
|
|
73
|
+
var isPromise = function (value) {
|
|
74
|
+
return typeof (value === null || value === void 0 ? void 0 : value.then) === 'function';
|
|
75
|
+
};
|
|
76
|
+
exports.isPromise = isPromise;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve `promise`, or {@link TIMED_OUT} if it has not settled within `ms`.
|
|
79
|
+
*
|
|
80
|
+
* The timer is cleared either way, so a promise that settles quickly does not
|
|
81
|
+
* leave a pending timeout behind.
|
|
82
|
+
*/
|
|
83
|
+
var withTimeout = function (promise, ms) { return __awaiter(void 0, void 0, void 0, function () {
|
|
84
|
+
var timer, timeout;
|
|
85
|
+
return __generator(this, function (_a) {
|
|
86
|
+
switch (_a.label) {
|
|
87
|
+
case 0:
|
|
88
|
+
timeout = new Promise(function (resolve) {
|
|
89
|
+
timer = setTimeout(function () { return resolve(exports.TIMED_OUT); }, ms);
|
|
90
|
+
});
|
|
91
|
+
_a.label = 1;
|
|
92
|
+
case 1:
|
|
93
|
+
_a.trys.push([1, , 3, 4]);
|
|
94
|
+
return [4 /*yield*/, Promise.race([promise, timeout])];
|
|
95
|
+
case 2: return [2 /*return*/, _a.sent()];
|
|
96
|
+
case 3:
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
return [7 /*endfinally*/];
|
|
99
|
+
case 4: return [2 /*return*/];
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}); };
|
|
103
|
+
exports.withTimeout = withTimeout;
|
|
@@ -12,9 +12,12 @@ import { ketchConsentAdapter } from './ketch.js';
|
|
|
12
12
|
import { onetrustConsentAdapter } from './onetrust.js';
|
|
13
13
|
import { shopifyConsentAdapter } from './shopify.js';
|
|
14
14
|
import { segmentConsentAdapter } from './segment.js';
|
|
15
|
+
import { claimConsentEmit, commitConsentEmit, releaseConsentEmit, } from './persistence.js';
|
|
15
16
|
export const addCdpConsent = (options) => {
|
|
16
|
-
let lastProcessedConsent = null;
|
|
17
17
|
const POLLING_INTERVAL_MS = 50;
|
|
18
|
+
// Always populated: ChordAnalytics applies a `namespace` default before
|
|
19
|
+
// constructing the consent manager.
|
|
20
|
+
const namespace = options.namespace;
|
|
18
21
|
const TIMEOUT_MS = options.awaitConsentTimeout
|
|
19
22
|
? options.awaitConsentTimeout
|
|
20
23
|
: 60000;
|
|
@@ -64,24 +67,36 @@ export const addCdpConsent = (options) => {
|
|
|
64
67
|
});
|
|
65
68
|
});
|
|
66
69
|
/**
|
|
67
|
-
* Tracks consent preference
|
|
68
|
-
*
|
|
70
|
+
* Tracks consent preference *transitions* only, deduped against the last
|
|
71
|
+
* state we emitted (persisted, so the guard survives navigation).
|
|
72
|
+
* Per-event consent stamping is handled by configureConsentOnCdp().
|
|
69
73
|
*/
|
|
70
74
|
const trackConsentUpdate = (updatedConsent) => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
const chordAnalytics = window[namespace];
|
|
76
|
+
if (!chordAnalytics ||
|
|
77
|
+
typeof chordAnalytics.track !== 'function' ||
|
|
78
|
+
updatedConsent.consentCategories === undefined) {
|
|
73
79
|
return;
|
|
74
80
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
if (!claimConsentEmit(namespace, updatedConsent)) {
|
|
82
|
+
if (options.enableLogging && options.debug) {
|
|
83
|
+
console.log('[Chord Analytics DEBUG]: Consent unchanged or emit already in flight, skipping');
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
80
88
|
yield chordAnalytics.track('Consent Preferences Updated', updatedConsent);
|
|
89
|
+
// Only recorded once delivery resolves. Committing before this would
|
|
90
|
+
// suppress the event permanently if it were queued and never flushed.
|
|
91
|
+
commitConsentEmit(namespace, updatedConsent);
|
|
81
92
|
if (options.enableLogging && options.debug) {
|
|
82
93
|
console.log('[Chord Analytics DEBUG]: Consent preferences updated, sent tracking event');
|
|
83
94
|
}
|
|
84
95
|
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
releaseConsentEmit(namespace, updatedConsent);
|
|
98
|
+
console.error('Error tracking consent preferences update:', error);
|
|
99
|
+
}
|
|
85
100
|
});
|
|
86
101
|
/**
|
|
87
102
|
* Sets up consent management by initializing the adapter and handling consent updates.
|
|
@@ -94,10 +109,10 @@ export const addCdpConsent = (options) => {
|
|
|
94
109
|
adapter.handleConsentUpdates((updatedConsent) => {
|
|
95
110
|
trackConsentUpdate(updatedConsent);
|
|
96
111
|
});
|
|
97
|
-
// Return initial consent to be tracked after CDP is fully initialized
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return
|
|
112
|
+
// Return initial consent to be tracked after CDP is fully initialized.
|
|
113
|
+
// Not marked as processed here: whichever path emits first records the
|
|
114
|
+
// signature, and the other is then deduped against it.
|
|
115
|
+
return adapter.getCurrentConsent();
|
|
101
116
|
});
|
|
102
117
|
/**
|
|
103
118
|
* Initializes consent management and sets up consent update handlers.
|
|
@@ -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,135 @@
|
|
|
1
|
+
const LAST_CONSENT_STORAGE_KEY = 'chordcommerce/analytics/last-consent';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory fallback for environments where localStorage is unavailable
|
|
4
|
+
* (SSR, private browsing, storage disabled). Keyed by namespace so multiple
|
|
5
|
+
* ChordAnalytics instances on one page don't clobber each other.
|
|
6
|
+
*/
|
|
7
|
+
const memoryStore = {};
|
|
8
|
+
const storageKey = (namespace) => `${LAST_CONSENT_STORAGE_KEY}/${namespace !== null && namespace !== void 0 ? namespace : 'default'}`;
|
|
9
|
+
/**
|
|
10
|
+
* Build a stable, order-independent signature for a consent configuration.
|
|
11
|
+
*
|
|
12
|
+
* Category keys are sorted so a change in the order the consent manager
|
|
13
|
+
* reports its groups does not read as a change in consent.
|
|
14
|
+
*/
|
|
15
|
+
export const consentSignature = (consent) => {
|
|
16
|
+
var _a;
|
|
17
|
+
const categories = (_a = consent.consentCategories) !== null && _a !== void 0 ? _a : {};
|
|
18
|
+
const sorted = Object.keys(categories)
|
|
19
|
+
.sort()
|
|
20
|
+
.reduce((acc, key) => {
|
|
21
|
+
acc[key] = categories[key];
|
|
22
|
+
return acc;
|
|
23
|
+
}, {});
|
|
24
|
+
return JSON.stringify(sorted);
|
|
25
|
+
};
|
|
26
|
+
const readSignature = (namespace) => {
|
|
27
|
+
var _a;
|
|
28
|
+
const key = storageKey(namespace);
|
|
29
|
+
try {
|
|
30
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
31
|
+
return window.localStorage.getItem(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (_b) {
|
|
35
|
+
// localStorage can throw in private browsing; fall through to memory
|
|
36
|
+
}
|
|
37
|
+
return (_a = memoryStore[key]) !== null && _a !== void 0 ? _a : null;
|
|
38
|
+
};
|
|
39
|
+
const writeSignature = (namespace, signature) => {
|
|
40
|
+
const key = storageKey(namespace);
|
|
41
|
+
memoryStore[key] = signature;
|
|
42
|
+
try {
|
|
43
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
44
|
+
window.localStorage.setItem(key, signature);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (_a) {
|
|
48
|
+
// Quota or private browsing; the in-memory copy still dedupes within
|
|
49
|
+
// this page load.
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Claims for emits currently mid-delivery, as `<storage key>::<signature>`.
|
|
54
|
+
* Delivery is awaited and a queued event can stay unresolved for a long time,
|
|
55
|
+
* so without this two callers in the same tick would both pass the guard and
|
|
56
|
+
* emit duplicates.
|
|
57
|
+
*
|
|
58
|
+
* Keyed by signature, not just namespace, so two different consent states can
|
|
59
|
+
* be in flight at once without clearing each other's claim.
|
|
60
|
+
*/
|
|
61
|
+
const inFlight = new Set();
|
|
62
|
+
const inFlightKey = (namespace, signature) => `${storageKey(namespace)}::${signature}`;
|
|
63
|
+
/**
|
|
64
|
+
* Whether this consent configuration differs from the last one we tracked.
|
|
65
|
+
*
|
|
66
|
+
* Returns true when nothing has been stored yet, so every visitor still gets
|
|
67
|
+
* one baseline `Consent Preferences Updated` event recording their starting
|
|
68
|
+
* state. Subsequent page loads with unchanged consent return false.
|
|
69
|
+
*/
|
|
70
|
+
export const hasConsentChanged = (namespace, consent) => readSignature(namespace) !== consentSignature(consent);
|
|
71
|
+
/**
|
|
72
|
+
* Claim the right to emit a consent event, atomically.
|
|
73
|
+
*
|
|
74
|
+
* Returns false when this state was already tracked, or when an emit for the
|
|
75
|
+
* same state is already in flight. Callers that get true MUST follow with
|
|
76
|
+
* `commitConsentEmit` on success or `releaseConsentEmit` on failure.
|
|
77
|
+
*
|
|
78
|
+
* The claim is deliberately not persisted: it exists only to dedupe within a
|
|
79
|
+
* page load. If delivery never completes, the next page load starts clean and
|
|
80
|
+
* retries rather than suppressing the event forever.
|
|
81
|
+
*/
|
|
82
|
+
export const claimConsentEmit = (namespace, consent) => {
|
|
83
|
+
const signature = consentSignature(consent);
|
|
84
|
+
if (readSignature(namespace) === signature)
|
|
85
|
+
return false;
|
|
86
|
+
if (inFlight.has(inFlightKey(namespace, signature)))
|
|
87
|
+
return false;
|
|
88
|
+
inFlight.add(inFlightKey(namespace, signature));
|
|
89
|
+
return true;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Record a consent configuration as tracked, once delivery has actually
|
|
93
|
+
* completed. Later page loads dedupe against it.
|
|
94
|
+
*/
|
|
95
|
+
export const commitConsentEmit = (namespace, consent) => {
|
|
96
|
+
const signature = consentSignature(consent);
|
|
97
|
+
inFlight.delete(inFlightKey(namespace, signature));
|
|
98
|
+
writeSignature(namespace, signature);
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Give up a claim without recording it, so a later attempt can retry. Takes
|
|
102
|
+
* the consent it claimed so a failing emit cannot clear another state's claim.
|
|
103
|
+
*/
|
|
104
|
+
export const releaseConsentEmit = (namespace, consent) => {
|
|
105
|
+
inFlight.delete(inFlightKey(namespace, consentSignature(consent)));
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Clear the stored consent signature so the next init emits a fresh baseline.
|
|
109
|
+
* Called on reset(), where a new anonymous ID would otherwise never get a
|
|
110
|
+
* consent record of its own.
|
|
111
|
+
*/
|
|
112
|
+
export const forgetConsent = (namespace) => {
|
|
113
|
+
const key = storageKey(namespace);
|
|
114
|
+
delete memoryStore[key];
|
|
115
|
+
inFlight.forEach((claim) => {
|
|
116
|
+
if (claim.startsWith(`${key}::`))
|
|
117
|
+
inFlight.delete(claim);
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
121
|
+
window.localStorage.removeItem(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (_a) {
|
|
125
|
+
// Nothing to do; the in-memory copy is already gone.
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Drop all module-level state. Test-only: `localStorage` is cleared between
|
|
130
|
+
* tests, but these would otherwise persist and suppress expected events.
|
|
131
|
+
*/
|
|
132
|
+
export const clearConsentMemory = () => {
|
|
133
|
+
Object.keys(memoryStore).forEach((key) => delete memoryStore[key]);
|
|
134
|
+
inFlight.clear();
|
|
135
|
+
};
|
package/dist/client/index.d.ts
CHANGED
|
@@ -31,34 +31,10 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
|
|
|
31
31
|
*/
|
|
32
32
|
private cdpReadyPromise;
|
|
33
33
|
private consentConfig;
|
|
34
|
-
private
|
|
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
|
-
*/
|
|
34
|
+
private operationQueue;
|
|
35
|
+
private resetting;
|
|
46
36
|
private consentReadyPromise;
|
|
47
37
|
constructor(options: ChordAnalyticsOptions<T>);
|
|
48
|
-
/**
|
|
49
|
-
* Persist the consent event queue to sessionStorage for recovery on instance recreation.
|
|
50
|
-
* Validates queue size before storing to prevent QuotaExceededError.
|
|
51
|
-
*/
|
|
52
|
-
private persistQueueToStorage;
|
|
53
|
-
/**
|
|
54
|
-
* Restore pending events from sessionStorage (keeps events less than 5 min old).
|
|
55
|
-
* Called on new instance initialization.
|
|
56
|
-
*/
|
|
57
|
-
private restoreQueueFromStorage;
|
|
58
|
-
/**
|
|
59
|
-
* Clear the persisted queue from sessionStorage after successful flush.
|
|
60
|
-
*/
|
|
61
|
-
private clearQueueFromStorage;
|
|
62
38
|
/**
|
|
63
39
|
* Initialize CDP snippet and wait for it to be ready.
|
|
64
40
|
* Fires initial consent track event after CDP is fully initialized to avoid circular await.
|
|
@@ -71,29 +47,17 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
|
|
|
71
47
|
setDebugMode: (enabled: boolean) => void;
|
|
72
48
|
cdp: () => AnyAnalytics;
|
|
73
49
|
ccdp: () => AnyAnalytics;
|
|
50
|
+
private currentUser;
|
|
74
51
|
logger: (message?: any, ...optionalParams: any[]) => void;
|
|
75
52
|
isResetting: () => boolean;
|
|
53
|
+
private enqueue;
|
|
54
|
+
private withTimeout;
|
|
76
55
|
private waitForReadyState;
|
|
77
56
|
/**
|
|
78
57
|
* Wait for consent manager to be ready (no timeout - queues indefinitely).
|
|
79
58
|
* Uses a shared promise so multiple events share the same watcher.
|
|
80
59
|
*/
|
|
81
60
|
private waitForConsent;
|
|
82
|
-
/**
|
|
83
|
-
* Queue an event to be sent when consent is ready.
|
|
84
|
-
* Persists to sessionStorage for recovery on instance recreation.
|
|
85
|
-
* Starts a consent watcher if not already active.
|
|
86
|
-
*/
|
|
87
|
-
private queueEventForConsent;
|
|
88
|
-
/**
|
|
89
|
-
* Watch for consent to become ready, then flush the queue.
|
|
90
|
-
*/
|
|
91
|
-
private watchForConsentAndFlush;
|
|
92
|
-
/**
|
|
93
|
-
* Flush all queued events now that consent is ready.
|
|
94
|
-
* Clears sessionStorage after successful flush.
|
|
95
|
-
*/
|
|
96
|
-
private flushConsentQueue;
|
|
97
61
|
/**
|
|
98
62
|
* Configure CDP with fresh consent categories before each event.
|
|
99
63
|
* Uses Jitsu's configure() to update privacy.consentCategories.
|
|
@@ -152,7 +116,6 @@ export declare class ChordAnalytics<T extends ObjectTypes = ObjectTypes> {
|
|
|
152
116
|
*/
|
|
153
117
|
private sendPageEvent;
|
|
154
118
|
reset: () => Promise<void>;
|
|
155
|
-
private performReset;
|
|
156
119
|
/**
|
|
157
120
|
* Handler for postMessage events from the Shopify Web Pixel sandbox.
|
|
158
121
|
* Bound to the instance for proper `this` context and cleanup.
|