@chordcommerce/analytics 1.23.0 → 1.23.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/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.js +42 -13
- package/dist/cjs/setupTests.js +14 -4
- 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.js +26 -2
- package/dist/setupTests.js +14 -4
- package/package.json +1 -1
|
@@ -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
|
|
100
|
-
*
|
|
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
|
-
|
|
108
|
-
|
|
111
|
+
chordAnalytics = window[namespace];
|
|
112
|
+
if (!chordAnalytics ||
|
|
113
|
+
typeof chordAnalytics.track !== 'function' ||
|
|
114
|
+
updatedConsent.consentCategories === undefined) {
|
|
109
115
|
return [2 /*return*/];
|
|
110
116
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
123
|
-
case
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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,
|
|
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
|
-
|
|
189
|
-
console.error('Error setting up consent management:',
|
|
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;
|
package/dist/cjs/client/index.js
CHANGED
|
@@ -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';
|
|
@@ -777,6 +778,9 @@ var ChordAnalytics = /** @class */ (function () {
|
|
|
777
778
|
switch (_b.label) {
|
|
778
779
|
case 0:
|
|
779
780
|
_b.trys.push([0, 4, 5, 6]);
|
|
781
|
+
// The new identity has no consent event of its own yet, so drop the
|
|
782
|
+
// stored signature and let the next init emit a fresh baseline.
|
|
783
|
+
(0, persistence_js_1.forgetConsent)(this.options.namespace);
|
|
780
784
|
a = this.ccdp();
|
|
781
785
|
if (a && typeof a.reset === 'function') {
|
|
782
786
|
a.reset();
|
|
@@ -1481,7 +1485,7 @@ var ChordAnalytics = /** @class */ (function () {
|
|
|
1481
1485
|
*/
|
|
1482
1486
|
ChordAnalytics.prototype.initializeCdp = function () {
|
|
1483
1487
|
return __awaiter(this, void 0, void 0, function () {
|
|
1484
|
-
var snippetPromise, initialConsent, error_4;
|
|
1488
|
+
var snippetPromise, initialConsent, namespace, error_4, error_5;
|
|
1485
1489
|
return __generator(this, function (_a) {
|
|
1486
1490
|
switch (_a.label) {
|
|
1487
1491
|
case 0:
|
|
@@ -1491,7 +1495,7 @@ var ChordAnalytics = /** @class */ (function () {
|
|
|
1491
1495
|
}
|
|
1492
1496
|
_a.label = 1;
|
|
1493
1497
|
case 1:
|
|
1494
|
-
_a.trys.push([1,
|
|
1498
|
+
_a.trys.push([1, 9, , 10]);
|
|
1495
1499
|
snippetPromise = (0, cdp_js_1.addCdpSnippet)(this.options);
|
|
1496
1500
|
// Create a void promise for cdpReadyPromise that resolves when snippet is done
|
|
1497
1501
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
@@ -1505,32 +1509,57 @@ var ChordAnalytics = /** @class */ (function () {
|
|
|
1505
1509
|
// eslint-disable-next-line no-console
|
|
1506
1510
|
console.log('[Chord Analytics DEBUG]: consentCategories defined?', (initialConsent === null || initialConsent === void 0 ? void 0 : initialConsent.consentCategories) !== undefined);
|
|
1507
1511
|
}
|
|
1508
|
-
|
|
1512
|
+
namespace = this.options.namespace;
|
|
1513
|
+
if (!(initialConsent && initialConsent.consentCategories !== undefined)) return [3 /*break*/, 7];
|
|
1514
|
+
if (!(0, persistence_js_1.claimConsentEmit)(namespace, initialConsent)) {
|
|
1515
|
+
if (this.options.enableLogging && this.options.debug) {
|
|
1516
|
+
// eslint-disable-next-line no-console
|
|
1517
|
+
console.log('[Chord Analytics DEBUG]: Initial consent unchanged or emit already in flight, skipping track');
|
|
1518
|
+
}
|
|
1519
|
+
return [2 /*return*/];
|
|
1520
|
+
}
|
|
1509
1521
|
if (this.options.enableLogging && this.options.debug) {
|
|
1510
1522
|
// eslint-disable-next-line no-console
|
|
1511
1523
|
console.log('[Chord Analytics DEBUG]: About to call this.track() for Consent Preferences Updated');
|
|
1512
1524
|
}
|
|
1513
|
-
|
|
1525
|
+
_a.label = 3;
|
|
1514
1526
|
case 3:
|
|
1527
|
+
_a.trys.push([3, 5, , 6]);
|
|
1528
|
+
return [4 /*yield*/, this.track('Consent Preferences Updated', initialConsent)
|
|
1529
|
+
// Only recorded once delivery resolves. Committing before this would
|
|
1530
|
+
// suppress the event permanently if it were queued and never
|
|
1531
|
+
// flushed — the next page load must be free to retry.
|
|
1532
|
+
];
|
|
1533
|
+
case 4:
|
|
1515
1534
|
_a.sent();
|
|
1535
|
+
// Only recorded once delivery resolves. Committing before this would
|
|
1536
|
+
// suppress the event permanently if it were queued and never
|
|
1537
|
+
// flushed — the next page load must be free to retry.
|
|
1538
|
+
(0, persistence_js_1.commitConsentEmit)(namespace, initialConsent);
|
|
1539
|
+
return [3 /*break*/, 6];
|
|
1540
|
+
case 5:
|
|
1541
|
+
error_4 = _a.sent();
|
|
1542
|
+
(0, persistence_js_1.releaseConsentEmit)(namespace, initialConsent);
|
|
1543
|
+
throw error_4;
|
|
1544
|
+
case 6:
|
|
1516
1545
|
if (this.options.enableLogging && this.options.debug) {
|
|
1517
1546
|
// eslint-disable-next-line no-console
|
|
1518
1547
|
console.log('[Chord Analytics DEBUG]: Initial consent preferences tracked');
|
|
1519
1548
|
}
|
|
1520
|
-
return [3 /*break*/,
|
|
1521
|
-
case
|
|
1549
|
+
return [3 /*break*/, 8];
|
|
1550
|
+
case 7:
|
|
1522
1551
|
if (this.options.enableLogging && this.options.debug) {
|
|
1523
1552
|
// eslint-disable-next-line no-console
|
|
1524
1553
|
console.log('[Chord Analytics DEBUG]: Skipping initial consent track - no consent or no consentCategories');
|
|
1525
1554
|
}
|
|
1526
|
-
_a.label =
|
|
1527
|
-
case
|
|
1528
|
-
case
|
|
1529
|
-
|
|
1555
|
+
_a.label = 8;
|
|
1556
|
+
case 8: return [3 /*break*/, 10];
|
|
1557
|
+
case 9:
|
|
1558
|
+
error_5 = _a.sent();
|
|
1530
1559
|
// eslint-disable-next-line no-console
|
|
1531
|
-
console.error('CDP initialization failed:',
|
|
1532
|
-
return [3 /*break*/,
|
|
1533
|
-
case
|
|
1560
|
+
console.error('CDP initialization failed:', error_5);
|
|
1561
|
+
return [3 /*break*/, 10];
|
|
1562
|
+
case 10: return [2 /*return*/];
|
|
1534
1563
|
}
|
|
1535
1564
|
});
|
|
1536
1565
|
});
|
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
|
});
|
|
@@ -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.js
CHANGED
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import { addCdpSnippet, cdpQueue } from './cdp.js';
|
|
11
11
|
import { addCdpConsent } from './consent/index.js';
|
|
12
|
+
import { claimConsentEmit, commitConsentEmit, forgetConsent, releaseConsentEmit, } from './consent/persistence.js';
|
|
12
13
|
import { eventSchemas } from '../validators/index.js';
|
|
13
14
|
import { pruneNullValues } from '../utils.js';
|
|
14
15
|
const DEBUG_MODE_STORAGE_KEY = 'chordcommerce/analytics/debug';
|
|
@@ -600,6 +601,9 @@ export class ChordAnalytics {
|
|
|
600
601
|
this.performReset = () => __awaiter(this, void 0, void 0, function* () {
|
|
601
602
|
var _e;
|
|
602
603
|
try {
|
|
604
|
+
// The new identity has no consent event of its own yet, so drop the
|
|
605
|
+
// stored signature and let the next init emit a fresh baseline.
|
|
606
|
+
forgetConsent(this.options.namespace);
|
|
603
607
|
const a = this.ccdp();
|
|
604
608
|
if (a && typeof a.reset === 'function') {
|
|
605
609
|
a.reset();
|
|
@@ -1235,13 +1239,33 @@ export class ChordAnalytics {
|
|
|
1235
1239
|
// eslint-disable-next-line no-console
|
|
1236
1240
|
console.log('[Chord Analytics DEBUG]: consentCategories defined?', (initialConsent === null || initialConsent === void 0 ? void 0 : initialConsent.consentCategories) !== undefined);
|
|
1237
1241
|
}
|
|
1238
|
-
// Now that CDP is fully ready, fire the initial consent track event
|
|
1242
|
+
// Now that CDP is fully ready, fire the initial consent track event.
|
|
1243
|
+
// Skipped when consent is unchanged since the last event we emitted —
|
|
1244
|
+
// otherwise this fires once per page load on multi-page storefronts.
|
|
1245
|
+
const { namespace } = this.options;
|
|
1239
1246
|
if (initialConsent && initialConsent.consentCategories !== undefined) {
|
|
1247
|
+
if (!claimConsentEmit(namespace, initialConsent)) {
|
|
1248
|
+
if (this.options.enableLogging && this.options.debug) {
|
|
1249
|
+
// eslint-disable-next-line no-console
|
|
1250
|
+
console.log('[Chord Analytics DEBUG]: Initial consent unchanged or emit already in flight, skipping track');
|
|
1251
|
+
}
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1240
1254
|
if (this.options.enableLogging && this.options.debug) {
|
|
1241
1255
|
// eslint-disable-next-line no-console
|
|
1242
1256
|
console.log('[Chord Analytics DEBUG]: About to call this.track() for Consent Preferences Updated');
|
|
1243
1257
|
}
|
|
1244
|
-
|
|
1258
|
+
try {
|
|
1259
|
+
yield this.track('Consent Preferences Updated', initialConsent);
|
|
1260
|
+
// Only recorded once delivery resolves. Committing before this would
|
|
1261
|
+
// suppress the event permanently if it were queued and never
|
|
1262
|
+
// flushed — the next page load must be free to retry.
|
|
1263
|
+
commitConsentEmit(namespace, initialConsent);
|
|
1264
|
+
}
|
|
1265
|
+
catch (error) {
|
|
1266
|
+
releaseConsentEmit(namespace, initialConsent);
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1245
1269
|
if (this.options.enableLogging && this.options.debug) {
|
|
1246
1270
|
// eslint-disable-next-line no-console
|
|
1247
1271
|
console.log('[Chord Analytics DEBUG]: Initial consent preferences tracked');
|
package/dist/setupTests.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
2
|
import { jest } from '@jest/globals';
|
|
3
|
+
import { clearConsentMemory } from './client/consent/persistence.js';
|
|
3
4
|
// Make jest available globally
|
|
4
5
|
globalThis.jest = jest;
|
|
5
|
-
|
|
6
|
-
const sessionStorageMock = (() => {
|
|
6
|
+
const createStorageMock = () => {
|
|
7
7
|
let store = {};
|
|
8
8
|
return {
|
|
9
9
|
getItem: (key) => store[key] || null,
|
|
@@ -17,11 +17,21 @@ const sessionStorageMock = (() => {
|
|
|
17
17
|
store = {};
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
|
-
}
|
|
20
|
+
};
|
|
21
|
+
const sessionStorageMock = createStorageMock();
|
|
22
|
+
const localStorageMock = createStorageMock();
|
|
21
23
|
Object.defineProperty(window, 'sessionStorage', {
|
|
22
24
|
value: sessionStorageMock,
|
|
23
25
|
});
|
|
24
|
-
|
|
26
|
+
Object.defineProperty(window, 'localStorage', {
|
|
27
|
+
value: localStorageMock,
|
|
28
|
+
});
|
|
29
|
+
// Clear storage before each test to ensure test isolation. localStorage now
|
|
30
|
+
// holds the last-tracked consent signature, which would otherwise leak between
|
|
31
|
+
// tests and suppress expected consent events. clearConsentMemory() covers the
|
|
32
|
+
// module-level fallback stores, which localStorage.clear() does not reach.
|
|
25
33
|
beforeEach(() => {
|
|
26
34
|
sessionStorageMock.clear();
|
|
35
|
+
localStorageMock.clear();
|
|
36
|
+
clearConsentMemory();
|
|
27
37
|
});
|
package/package.json
CHANGED