@dreamdata_io/verify-tracking 0.1.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.
- package/LICENSE +21 -0
- package/README.md +756 -0
- package/dist/ai/checker.js +222 -0
- package/dist/ai/checker.js.map +1 -0
- package/dist/ai/checklist.js +190 -0
- package/dist/ai/checklist.js.map +1 -0
- package/dist/ai/cli-presets.js +27 -0
- package/dist/ai/cli-presets.js.map +1 -0
- package/dist/ai/evidence-view.js +143 -0
- package/dist/ai/evidence-view.js.map +1 -0
- package/dist/ai/local-cli.js +58 -0
- package/dist/ai/local-cli.js.map +1 -0
- package/dist/ai/openai-compatible.js +78 -0
- package/dist/ai/openai-compatible.js.map +1 -0
- package/dist/ai/response-parser.js +105 -0
- package/dist/ai/response-parser.js.map +1 -0
- package/dist/ai/types.js +2 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/analysis/consent-mode.js +225 -0
- package/dist/analysis/consent-mode.js.map +1 -0
- package/dist/analysis/deployment.js +2658 -0
- package/dist/analysis/deployment.js.map +1 -0
- package/dist/analysis/diagnosis/copy.en.js +303 -0
- package/dist/analysis/diagnosis/copy.en.js.map +1 -0
- package/dist/analysis/diagnosis/diagnose.js +172 -0
- package/dist/analysis/diagnosis/diagnose.js.map +1 -0
- package/dist/analysis/diagnosis/docs.js +118 -0
- package/dist/analysis/diagnosis/docs.js.map +1 -0
- package/dist/analysis/diagnosis/index.js +5 -0
- package/dist/analysis/diagnosis/index.js.map +1 -0
- package/dist/analysis/diagnosis/rules.js +656 -0
- package/dist/analysis/diagnosis/rules.js.map +1 -0
- package/dist/analysis/diagnosis/types.js +2 -0
- package/dist/analysis/diagnosis/types.js.map +1 -0
- package/dist/cli.js +344 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/env.js +100 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/preflight.js +167 -0
- package/dist/config/preflight.js.map +1 -0
- package/dist/config/user-config.js +79 -0
- package/dist/config/user-config.js.map +1 -0
- package/dist/evidence/collect.js +527 -0
- package/dist/evidence/collect.js.map +1 -0
- package/dist/evidence/consent-accept.js +377 -0
- package/dist/evidence/consent-accept.js.map +1 -0
- package/dist/evidence/consent.js +340 -0
- package/dist/evidence/consent.js.map +1 -0
- package/dist/evidence/constants.js +615 -0
- package/dist/evidence/constants.js.map +1 -0
- package/dist/evidence/globals.js +132 -0
- package/dist/evidence/globals.js.map +1 -0
- package/dist/evidence/gtm-container.js +432 -0
- package/dist/evidence/gtm-container.js.map +1 -0
- package/dist/evidence/heuristic-banner.js +107 -0
- package/dist/evidence/heuristic-banner.js.map +1 -0
- package/dist/evidence/http-probe.js +139 -0
- package/dist/evidence/http-probe.js.map +1 -0
- package/dist/evidence/init-script.js +112 -0
- package/dist/evidence/init-script.js.map +1 -0
- package/dist/evidence/network.js +365 -0
- package/dist/evidence/network.js.map +1 -0
- package/dist/evidence/preference-center.js +368 -0
- package/dist/evidence/preference-center.js.map +1 -0
- package/dist/evidence/script.js +425 -0
- package/dist/evidence/script.js.map +1 -0
- package/dist/evidence/second-page.js +332 -0
- package/dist/evidence/second-page.js.map +1 -0
- package/dist/evidence/selector-probe.js +53 -0
- package/dist/evidence/selector-probe.js.map +1 -0
- package/dist/evidence/stealth.js +21 -0
- package/dist/evidence/stealth.js.map +1 -0
- package/dist/evidence/types.js +14 -0
- package/dist/evidence/types.js.map +1 -0
- package/dist/interactive/prompt.js +31 -0
- package/dist/interactive/prompt.js.map +1 -0
- package/dist/mcp/server.js +308 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/summary.js +41 -0
- package/dist/mcp/summary.js.map +1 -0
- package/dist/report/context.js +184 -0
- package/dist/report/context.js.map +1 -0
- package/dist/report/projection.js +125 -0
- package/dist/report/projection.js.map +1 -0
- package/dist/report/quote-check.js +90 -0
- package/dist/report/quote-check.js.map +1 -0
- package/dist/report/render.js +178 -0
- package/dist/report/render.js.map +1 -0
- package/dist/report/serializers.js +124 -0
- package/dist/report/serializers.js.map +1 -0
- package/dist/report/store.js +139 -0
- package/dist/report/store.js.map +1 -0
- package/dist/run/verify.js +77 -0
- package/dist/run/verify.js.map +1 -0
- package/dist/utils/duration.js +10 -0
- package/dist/utils/duration.js.map +1 -0
- package/dist/utils/errors.js +17 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.js +59 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/runTimingsLog.js +27 -0
- package/dist/utils/runTimingsLog.js.map +1 -0
- package/dist/utils/slug.js +27 -0
- package/dist/utils/slug.js.map +1 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/package.json +57 -0
- package/scripts/install.sh +314 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { BANNER_CONTEXT_KEYWORDS, GENERIC_ACCEPT_PHRASES, GENERIC_REJECT_PHRASES, PREFERENCE_CENTER_COUNTERPART_PHRASES, } from "./constants.js";
|
|
2
|
+
import { runHeuristicBanner } from "./heuristic-banner.js";
|
|
3
|
+
import { drivePreferenceCenter } from "./preference-center.js";
|
|
4
|
+
import { clickFirstMatching } from "./selector-probe.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
/**
|
|
7
|
+
* Selectors used only when no vendor API is reachable. Kept narrow and
|
|
8
|
+
* vendor-specific rather than matching any button whose text says "accept"
|
|
9
|
+
* or "reject", so we never click an unrelated control.
|
|
10
|
+
*/
|
|
11
|
+
const ACCEPT_SELECTORS = [
|
|
12
|
+
"#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
|
|
13
|
+
"#CybotCookiebotDialogBodyButtonAccept",
|
|
14
|
+
"#onetrust-accept-btn-handler",
|
|
15
|
+
".onetrust-close-btn-handler.accept-cookies",
|
|
16
|
+
"#usercentrics-root >>> button[data-testid='uc-accept-all-button']",
|
|
17
|
+
".cky-btn-accept",
|
|
18
|
+
"#cookiescript_accept",
|
|
19
|
+
"button#iubenda-cs-accept-btn",
|
|
20
|
+
"[aria-label='Accept all cookies']",
|
|
21
|
+
".osano-cm-accept-all",
|
|
22
|
+
".cmplz-accept",
|
|
23
|
+
"#hs-eu-confirmation-button",
|
|
24
|
+
"#pi_tracking_opt_in_yes",
|
|
25
|
+
];
|
|
26
|
+
/** Reject-side equivalents of ACCEPT_SELECTORS, for the same vendors. */
|
|
27
|
+
const REJECT_SELECTORS = [
|
|
28
|
+
"#CybotCookiebotDialogBodyLevelButtonLevelOptinDeclineAll",
|
|
29
|
+
"#CybotCookiebotDialogBodyButtonDecline",
|
|
30
|
+
"#onetrust-reject-all-handler",
|
|
31
|
+
".onetrust-close-btn-handler.reject-cookies",
|
|
32
|
+
"#usercentrics-root >>> button[data-testid='uc-deny-all-button']",
|
|
33
|
+
".cky-btn-reject",
|
|
34
|
+
"#cookiescript_reject",
|
|
35
|
+
"button#iubenda-cs-reject-btn",
|
|
36
|
+
"[aria-label='Reject all cookies']",
|
|
37
|
+
"[aria-label='Decline all cookies']",
|
|
38
|
+
".osano-cm-denyAll",
|
|
39
|
+
".cmplz-deny",
|
|
40
|
+
"#hs-eu-decline-button",
|
|
41
|
+
"#pi_tracking_opt_in_no",
|
|
42
|
+
];
|
|
43
|
+
const HEURISTIC_ACCEPT_ARGS = {
|
|
44
|
+
mode: "click",
|
|
45
|
+
direction: "accept",
|
|
46
|
+
acceptPhrases: GENERIC_ACCEPT_PHRASES,
|
|
47
|
+
rejectPhrases: GENERIC_REJECT_PHRASES,
|
|
48
|
+
preferenceCounterpartPhrases: PREFERENCE_CENTER_COUNTERPART_PHRASES,
|
|
49
|
+
contextKeywords: BANNER_CONTEXT_KEYWORDS,
|
|
50
|
+
};
|
|
51
|
+
const HEURISTIC_REJECT_ARGS = {
|
|
52
|
+
mode: "click",
|
|
53
|
+
direction: "reject",
|
|
54
|
+
acceptPhrases: GENERIC_ACCEPT_PHRASES,
|
|
55
|
+
rejectPhrases: GENERIC_REJECT_PHRASES,
|
|
56
|
+
preferenceCounterpartPhrases: PREFERENCE_CENTER_COUNTERPART_PHRASES,
|
|
57
|
+
contextKeywords: BANNER_CONTEXT_KEYWORDS,
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Attempts to grant analytics consent through the CMP's own API, falling back
|
|
61
|
+
* to its accept button. Returns what was tried so the report can state plainly
|
|
62
|
+
* whether the page was observed pre- or post-consent.
|
|
63
|
+
*
|
|
64
|
+
* Only ever called by the grant phase.
|
|
65
|
+
*/
|
|
66
|
+
export async function acceptConsent(page, timeoutMs) {
|
|
67
|
+
const viaApi = await page
|
|
68
|
+
.evaluate(() => {
|
|
69
|
+
const w = window;
|
|
70
|
+
if (typeof w.Cookiebot?.submitCustomConsent === "function") {
|
|
71
|
+
w.Cookiebot.submitCustomConsent(true, true, true);
|
|
72
|
+
return "Cookiebot.submitCustomConsent(true, true, true)";
|
|
73
|
+
}
|
|
74
|
+
if (typeof w.Cookiebot?.dialog?.submitAll === "function") {
|
|
75
|
+
w.Cookiebot.dialog.submitAll();
|
|
76
|
+
return "Cookiebot.dialog.submitAll()";
|
|
77
|
+
}
|
|
78
|
+
if (typeof w.OneTrust?.AllowAll === "function") {
|
|
79
|
+
w.OneTrust.AllowAll();
|
|
80
|
+
return "OneTrust.AllowAll()";
|
|
81
|
+
}
|
|
82
|
+
if (typeof w.UC_UI?.acceptAllConsents === "function") {
|
|
83
|
+
void w.UC_UI.acceptAllConsents().then(() => w.UC_UI?.closeCMP?.());
|
|
84
|
+
return "UC_UI.acceptAllConsents()";
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
logger.debug("Consent API call failed", err);
|
|
90
|
+
return null;
|
|
91
|
+
});
|
|
92
|
+
if (viaApi)
|
|
93
|
+
return { attempted: true, succeeded: true, detail: `granted via ${viaApi}` };
|
|
94
|
+
const matched = await clickFirstMatching(page, ACCEPT_SELECTORS, timeoutMs);
|
|
95
|
+
if (matched) {
|
|
96
|
+
return { attempted: true, succeeded: true, detail: `clicked ${matched}` };
|
|
97
|
+
}
|
|
98
|
+
// Last resort: no known vendor API or selector reached anything, which is
|
|
99
|
+
// expected for a bespoke banner. Try the generic text-matched fallback
|
|
100
|
+
// before giving up — see heuristic-banner.ts for what counts as a match.
|
|
101
|
+
const viaHeuristic = await page.evaluate(runHeuristicBanner, HEURISTIC_ACCEPT_ARGS).catch(() => null);
|
|
102
|
+
if (viaHeuristic?.clicked) {
|
|
103
|
+
return { attempted: true, succeeded: true, detail: `clicked heuristically-matched button "${viaHeuristic.clicked}"` };
|
|
104
|
+
}
|
|
105
|
+
// Nothing on the banner itself answers it. Some banners only ever answer
|
|
106
|
+
// through their preference centre — see preference-center.ts.
|
|
107
|
+
const viaPreferences = await drivePreferenceCenter(page, "accept", timeoutMs);
|
|
108
|
+
if (viaPreferences.succeeded)
|
|
109
|
+
return { attempted: true, succeeded: true, detail: `granted via the preference centre: ${viaPreferences.detail}` };
|
|
110
|
+
return {
|
|
111
|
+
attempted: true,
|
|
112
|
+
succeeded: false,
|
|
113
|
+
detail: `no CMP API, accept button, or heuristically-matched banner control could be reached, and the preference centre did not answer it either (${viaPreferences.detail})`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Symmetric counterpart to acceptConsent: attempts to DENY analytics consent
|
|
118
|
+
* through the CMP's own API, falling back to its reject/decline button.
|
|
119
|
+
*
|
|
120
|
+
* This lets a run observe the explicit-denial state (as opposed to the
|
|
121
|
+
* pre-consent default), which is required to tell "consent was never asked"
|
|
122
|
+
* apart from "consent was asked and refused, but the site tracked anyway" —
|
|
123
|
+
* a distinct defect (see checklist consent-mode / event-behavior items).
|
|
124
|
+
*
|
|
125
|
+
* Only ever called by the reject phase.
|
|
126
|
+
*/
|
|
127
|
+
export async function rejectConsent(page, timeoutMs) {
|
|
128
|
+
const viaApi = await page
|
|
129
|
+
.evaluate(() => {
|
|
130
|
+
const w = window;
|
|
131
|
+
if (typeof w.Cookiebot?.submitCustomConsent === "function") {
|
|
132
|
+
w.Cookiebot.submitCustomConsent(false, false, false);
|
|
133
|
+
return "Cookiebot.submitCustomConsent(false, false, false)";
|
|
134
|
+
}
|
|
135
|
+
if (typeof w.Cookiebot?.dialog?.submitDecline === "function") {
|
|
136
|
+
w.Cookiebot.dialog.submitDecline();
|
|
137
|
+
return "Cookiebot.dialog.submitDecline()";
|
|
138
|
+
}
|
|
139
|
+
if (typeof w.OneTrust?.RejectAll === "function") {
|
|
140
|
+
w.OneTrust.RejectAll();
|
|
141
|
+
return "OneTrust.RejectAll()";
|
|
142
|
+
}
|
|
143
|
+
if (typeof w.UC_UI?.denyAllConsents === "function") {
|
|
144
|
+
void w.UC_UI.denyAllConsents().then(() => w.UC_UI?.closeCMP?.());
|
|
145
|
+
return "UC_UI.denyAllConsents()";
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
})
|
|
149
|
+
.catch((err) => {
|
|
150
|
+
logger.debug("Consent API call failed", err);
|
|
151
|
+
return null;
|
|
152
|
+
});
|
|
153
|
+
if (viaApi)
|
|
154
|
+
return { attempted: true, succeeded: true, detail: `denied via ${viaApi}` };
|
|
155
|
+
const matched = await clickFirstMatching(page, REJECT_SELECTORS, timeoutMs);
|
|
156
|
+
if (matched) {
|
|
157
|
+
return { attempted: true, succeeded: true, detail: `clicked ${matched}` };
|
|
158
|
+
}
|
|
159
|
+
const viaHeuristic = await page.evaluate(runHeuristicBanner, HEURISTIC_REJECT_ARGS).catch(() => null);
|
|
160
|
+
if (viaHeuristic?.clicked) {
|
|
161
|
+
return { attempted: true, succeeded: true, detail: `clicked heuristically-matched button "${viaHeuristic.clicked}"` };
|
|
162
|
+
}
|
|
163
|
+
// The common shape this exists for: a banner offering only "Accept all"
|
|
164
|
+
// and a "Settings" link, where the visitor's only way to refuse is to open
|
|
165
|
+
// the panel, leave analytics off and save. Without this the phase reports
|
|
166
|
+
// the refusal as undrivable and every consent verdict goes inconclusive on
|
|
167
|
+
// a site a visitor can plainly say no to.
|
|
168
|
+
const viaPreferences = await drivePreferenceCenter(page, "reject", timeoutMs);
|
|
169
|
+
if (viaPreferences.succeeded)
|
|
170
|
+
return { attempted: true, succeeded: true, detail: `denied via the preference centre: ${viaPreferences.detail}` };
|
|
171
|
+
return {
|
|
172
|
+
attempted: true,
|
|
173
|
+
succeeded: false,
|
|
174
|
+
detail: `no CMP API, reject button, or heuristically-matched banner control could be reached, and the preference centre did not answer it either (${viaPreferences.detail})`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Cookies CMPs write to record the visitor's answer. Prefix-matched, since
|
|
179
|
+
* several append an id or a group suffix.
|
|
180
|
+
*/
|
|
181
|
+
const CONSENT_COOKIE_PREFIXES = [
|
|
182
|
+
"CookieConsent",
|
|
183
|
+
"OptanonConsent",
|
|
184
|
+
"OptanonAlertBoxClosed",
|
|
185
|
+
"cookieyes-consent",
|
|
186
|
+
"usercentrics",
|
|
187
|
+
"cookiescript_key",
|
|
188
|
+
"euconsent-v2",
|
|
189
|
+
"osano_consentmanager",
|
|
190
|
+
"cmplz_",
|
|
191
|
+
"__hs_cookie_cat_pref",
|
|
192
|
+
"__hs_opt_out",
|
|
193
|
+
"pi_opt_in987432",
|
|
194
|
+
];
|
|
195
|
+
/**
|
|
196
|
+
* Cookies third-party infrastructure writes on a schedule of its own —
|
|
197
|
+
* bot-management, CDN session, and load-balancer cookies being the most
|
|
198
|
+
* common — that have nothing to do with the banner. The generic "any cookie
|
|
199
|
+
* that wasn't there before" fallback below exists for bespoke banners with no
|
|
200
|
+
* recognised name; without this exclusion list it fires on the first
|
|
201
|
+
* Cloudflare `__cf_bm` rotation a click happens to race against, on any site
|
|
202
|
+
* sitting behind it, regardless of what the visitor actually answered.
|
|
203
|
+
*/
|
|
204
|
+
const NOISE_COOKIE_PREFIXES = ["__cf_bm", "_cfuvid", "cf_clearance", "__cflb", "AWSALB", "AWSALBCORS", "incap_ses", "visid_incap"];
|
|
205
|
+
/**
|
|
206
|
+
* Entries a CMP writes on page load and then REWRITES IN PLACE when the
|
|
207
|
+
* visitor answers, rather than creating fresh. Usercentrics is the common
|
|
208
|
+
* case: ucData/ucString are there from the first page view, so neither the
|
|
209
|
+
* cookie-name rule nor the "something new appeared" fallback can ever see a
|
|
210
|
+
* decision land on one of those sites — the value changing is the only
|
|
211
|
+
* evidence that exists. Matched against cookie names and localStorage keys
|
|
212
|
+
* alike, and only ever consulted as a value-changed check, so an entry that
|
|
213
|
+
* merely exists still proves nothing.
|
|
214
|
+
*/
|
|
215
|
+
const CONSENT_STORAGE_PREFIXES = [...CONSENT_COOKIE_PREFIXES, "ucData", "ucString", "uc_"];
|
|
216
|
+
/**
|
|
217
|
+
* The same idea for a banner nobody has heard of, matched on the name rather
|
|
218
|
+
* than a vendor prefix.
|
|
219
|
+
*
|
|
220
|
+
* meteomatics.com is the case: its hand-rolled banner keeps the decision in
|
|
221
|
+
* localStorage under `cookie-consent`, and writes that key on page load —
|
|
222
|
+
* `{"consents":[],"consentedAt":null,"rejectedAt":null}` — before the visitor
|
|
223
|
+
* has answered anything. So neither the vendor-prefix rule nor the "a key
|
|
224
|
+
* that wasn't there before appeared" fallback can ever see the answer land,
|
|
225
|
+
* and a click that worked perfectly was reported as `not-persisted`, which
|
|
226
|
+
* sank the whole phase. The value moving is the only evidence such a banner
|
|
227
|
+
* produces.
|
|
228
|
+
*
|
|
229
|
+
* Only ever consulted as a value-CHANGED check within the few seconds after
|
|
230
|
+
* a click we drove ourselves, so a key merely existing still proves nothing.
|
|
231
|
+
*/
|
|
232
|
+
const CONSENT_ENTRY_NAME_PATTERN = /consent|cookie[-_. ]?(?:prefs|preferences|settings|choices|notice|banner)/i;
|
|
233
|
+
/**
|
|
234
|
+
* Snapshots what's already there BEFORE the banner is clicked, so
|
|
235
|
+
* waitForConsentPersisted's generic fallback can tell "the site just wrote
|
|
236
|
+
* something new" apart from "this cookie was already here on load".
|
|
237
|
+
*/
|
|
238
|
+
export async function snapshotStorage(page) {
|
|
239
|
+
const cookies = await page.context().cookies().catch(() => []);
|
|
240
|
+
const localStorageEntries = await page
|
|
241
|
+
.evaluate(() => {
|
|
242
|
+
try {
|
|
243
|
+
return Object.keys(window.localStorage).map((k) => [k, window.localStorage.getItem(k)]);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
.catch(() => []);
|
|
250
|
+
return {
|
|
251
|
+
cookies: Object.fromEntries(cookies.map((c) => [c.name, c.value])),
|
|
252
|
+
localStorage: Object.fromEntries(localStorageEntries),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Waits until the CMP has durably recorded the grant, then returns.
|
|
257
|
+
*
|
|
258
|
+
* submitCustomConsent() and friends persist asynchronously, so reloading the
|
|
259
|
+
* moment they return can beat the cookie being written — the reload then
|
|
260
|
+
* replays the pre-consent page and the run reports nothing tracked on a
|
|
261
|
+
* healthy install. Polling both the vendor API and the cookie jar closes that
|
|
262
|
+
* race; a false return means the grant may not have stuck.
|
|
263
|
+
*
|
|
264
|
+
* `before` — taken with snapshotStorage() ahead of the click — backs two
|
|
265
|
+
* fallbacks for banners with no recognised vendor API and no cookie name in
|
|
266
|
+
* CONSENT_COOKIE_PREFIXES: any cookie or localStorage key that did not exist
|
|
267
|
+
* before the click is evidence the site recorded *something*, even though
|
|
268
|
+
* this harness cannot name what; and any recognised consent entry whose
|
|
269
|
+
* value changed covers the CMPs that rewrite an entry they already had
|
|
270
|
+
* (see CONSENT_STORAGE_PREFIXES).
|
|
271
|
+
*/
|
|
272
|
+
export async function waitForConsentPersisted(page, timeoutMs, before) {
|
|
273
|
+
const deadline = Date.now() + Math.min(8000, timeoutMs);
|
|
274
|
+
const isConsentEntry = (name) => CONSENT_STORAGE_PREFIXES.some((prefix) => name.startsWith(prefix)) || CONSENT_ENTRY_NAME_PATTERN.test(name);
|
|
275
|
+
while (Date.now() < deadline) {
|
|
276
|
+
const viaApi = await page
|
|
277
|
+
.evaluate(() => {
|
|
278
|
+
const w = window;
|
|
279
|
+
if (typeof w.Cookiebot?.hasResponse === "boolean")
|
|
280
|
+
return w.Cookiebot.hasResponse;
|
|
281
|
+
if (typeof w.OneTrust?.IsAlertBoxClosed === "function") {
|
|
282
|
+
try {
|
|
283
|
+
return Boolean(w.OneTrust.IsAlertBoxClosed());
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
})
|
|
291
|
+
.catch(() => false);
|
|
292
|
+
if (viaApi)
|
|
293
|
+
return true;
|
|
294
|
+
const cookies = await page.context().cookies().catch(() => []);
|
|
295
|
+
if (cookies.some((c) => CONSENT_COOKIE_PREFIXES.some((prefix) => c.name.startsWith(prefix))))
|
|
296
|
+
return true;
|
|
297
|
+
// Generic fallback for a custom banner with no recognised cookie name.
|
|
298
|
+
// Third-party infrastructure noise is excluded — see NOISE_COOKIE_PREFIXES.
|
|
299
|
+
if (cookies.some((c) => !(c.name in before.cookies) && !NOISE_COOKIE_PREFIXES.some((prefix) => c.name.startsWith(prefix))))
|
|
300
|
+
return true;
|
|
301
|
+
// A recognised consent entry whose value moved. Restricted to names we
|
|
302
|
+
// recognise, because plenty of unrelated entries (session ids, A/B test
|
|
303
|
+
// buckets, analytics timestamps) churn on their own and would otherwise
|
|
304
|
+
// certify any drive at all.
|
|
305
|
+
if (cookies.some((c) => isConsentEntry(c.name) && c.name in before.cookies && before.cookies[c.name] !== c.value))
|
|
306
|
+
return true;
|
|
307
|
+
const storage = await page
|
|
308
|
+
.evaluate(() => {
|
|
309
|
+
try {
|
|
310
|
+
return Object.keys(window.localStorage).map((k) => [k, window.localStorage.getItem(k)]);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
.catch(() => []);
|
|
317
|
+
if (storage.some(([k]) => !(k in before.localStorage)))
|
|
318
|
+
return true;
|
|
319
|
+
if (storage.some(([k, v]) => isConsentEntry(k) && k in before.localStorage && before.localStorage[k] !== v))
|
|
320
|
+
return true;
|
|
321
|
+
await page.waitForTimeout(250);
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Waits for Consent Mode to actually reflect the choice we just drove.
|
|
327
|
+
*
|
|
328
|
+
* waitForConsentPersisted only proves the CMP wrote *a* cookie. A CMP that
|
|
329
|
+
* records a decision but never emits gtag('consent','update',…) leaves every
|
|
330
|
+
* Consent-Mode-gated tag — including a GTM-managed Dreamdata bundle — exactly
|
|
331
|
+
* where it was, which is itself the defect worth reporting. Confirming the
|
|
332
|
+
* signal separately is what lets the report say which of the two happened.
|
|
333
|
+
*
|
|
334
|
+
* Only an explicit `update` counts as confirmation. A value the page was
|
|
335
|
+
* already sitting on — a declared default, or gtag.js's own fallback (see
|
|
336
|
+
* classifyAnalyticsStorage) — was never caused by anything we did, even when
|
|
337
|
+
* it happens to equal `expected`: a site that defaults analytics_storage to
|
|
338
|
+
* denied, or never wires up Consent Mode at all, reads "denied" that way by
|
|
339
|
+
* coincidence on every reject phase, which would otherwise be
|
|
340
|
+
* indistinguishable from a CMP that genuinely relayed the refusal.
|
|
341
|
+
*
|
|
342
|
+
* null means there was no readable Consent Mode state at all (no gtag on the
|
|
343
|
+
* page), which is not a failure — only sites using Consent Mode can be held
|
|
344
|
+
* to it.
|
|
345
|
+
*/
|
|
346
|
+
export async function waitForConsentSignal(page, expected, timeoutMs) {
|
|
347
|
+
const deadline = Date.now() + Math.min(8000, timeoutMs);
|
|
348
|
+
let sawConsentMode = false;
|
|
349
|
+
while (Date.now() < deadline) {
|
|
350
|
+
const result = await page
|
|
351
|
+
.evaluate(() => {
|
|
352
|
+
const w = window;
|
|
353
|
+
const entry = w.google_tag_data?.ics?.entries?.["analytics_storage"];
|
|
354
|
+
if (!entry)
|
|
355
|
+
return null;
|
|
356
|
+
const hasUpdate = entry.update !== undefined && entry.update !== null;
|
|
357
|
+
const value = hasUpdate ? entry.update : entry.default;
|
|
358
|
+
let state;
|
|
359
|
+
if (value === true || value === "granted")
|
|
360
|
+
state = "granted";
|
|
361
|
+
else if (value === false || value === "denied")
|
|
362
|
+
state = "denied";
|
|
363
|
+
else
|
|
364
|
+
state = "unset";
|
|
365
|
+
return { state, hasUpdate };
|
|
366
|
+
})
|
|
367
|
+
.catch(() => null);
|
|
368
|
+
if (result !== null) {
|
|
369
|
+
sawConsentMode = true;
|
|
370
|
+
if (result.state === expected && result.hasUpdate)
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
await page.waitForTimeout(250);
|
|
374
|
+
}
|
|
375
|
+
return sawConsentMode ? false : null;
|
|
376
|
+
}
|
|
377
|
+
//# sourceMappingURL=consent-accept.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent-accept.js","sourceRoot":"","sources":["../../src/evidence/consent-accept.ts"],"names":[],"mappings":"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"}
|