-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocs-analytics.js
More file actions
373 lines (352 loc) · 18.1 KB
/
Copy pathdocs-analytics.js
File metadata and controls
373 lines (352 loc) · 18.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
/**
* Docs analytics (Growth Analytics epic, nevermined.ai-website#191 docs
* surface + #188 handoff instrumentation).
*
* Mintlify auto-includes any .js in the content directory on every page
* (no docs.json reference needed). This file carries:
*
* 1. RB2B visitor identification — geography/consent-gated (added with
* the 2026-08-04 port, decision in website#237: docs is the
* highest-intent surface on the property; same account + webhook as
* site/blog, fires only for the geo-granted/consented population).
* 2. PostHog, EU cloud, anonymous mode — loaded as custom JS instead of
* Mintlify's native integration because the native one exposes no
* config: it can't disable autocapture or set our curated posture
* ($pageview + session replay ONLY, no identify, masked inputs),
* and proxies events through Mintlify's host. Ingestion is
* first-party via the site's /px reverse proxy (website#235) — the
* docs are served on nevermined.ai, so the proxy is same-origin.
* 3. nvm_o outbound-link decorator + app_handoff_click GA event — docs
* readers are the highest-intent handoff to nevermined.app. Carries
* the anonymous PostHog id as `pid` (website#235 payload contract).
*
* Consent model (GEOGRAPHY-AWARE port of website#235, 2026-08-04 —
* line-for-line from lib/consent-client.ts / webflow-analytics.js,
* keep in sync): the middleware publishes its geography verdict as the
* nvm_cg cookie ("1" = protection geography EU/UK/EEA, "0" = outside).
* Outside it, an undecided visitor counts as granted (no banner will
* ever ask them); inside it, granted-only fail-closed stands. Explicit
* deny always wins; GPC browsers never get the geo-grant; CookieYes's
* out-of-banner auto-grant is routed through the same rules. FULL mode
* (localStorage + session replay) requires an EXPLICITLY ACTIONED
* grant everywhere.
*
* BANNERLESS BY DESIGN (website#237): this file deliberately does NOT
* inject the CookieYes script — docs never ask for consent. The gate
* still READS the cookieyes-consent cookie (same nevermined.ai domain
* as the site), so an answer given on the marketing site — grant or
* deny — is honored here. EU docs-only visitors stay in the
* storage-free floor (memory-mode pageviews, nothing on the device),
* which is exactly why no banner is required. Docs never asks, always
* listens.
*
* GA4 (moved here 2026-07-21): docs.json integrations.ga4 is REMOVED —
* Mintlify's native injection gave gtag no consent defaults, so EU
* visitors got GA cookies pre-consent. This file owns the full GA
* load: GCM defaults first, then gtag. Do not re-add
* integrations.ga4 — that would double-init.
*/
(function () {
"use strict";
var RB2B_ID = "4N210HQZE26Z";
var POSTHOG_KEY = "phc_wydoKzXeudS4oNmTTJeeeifyHVQtVg8gUregwFeDfi4k";
var host = window.location.hostname.toLowerCase();
var isProduction =
host === "nevermined.ai" || host === "www.nevermined.ai";
if (!isProduction) return;
if (window.__nvmDocsAnalytics) return;
window.__nvmDocsAnalytics = true;
/* First-party ingestion via the /px middleware reverse proxy
(website#235): the snippet's assets-host derivation no-ops on this
value, so array.js loads from /px/static/* and the middleware
relays it. Derived from the page origin (Copilot review): the host
guard admits both apex and www, and the middleware serves /px on
either - origin-derivation keeps ingestion same-origin by
construction. */
var POSTHOG_HOST = window.location.origin + "/px";
/* ------------- 0. Google Consent Mode defaults + GA4 ------------- */
/* Defaults MUST precede gtag. Region list mirrors
CONSENT_COUNTRY_CODES in the website repo (lib/attribution.ts) -
keep in sync. Ruling 2026-07-22: protection geography includes
GDPR-EEA (IS/LI/NO); EEA visitors get denied defaults and no
banner, staying cookieless-anonymous. The asymmetry is
deliberate. */
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
// The decorator (and anything else) calls window.gtag — with
// docs.json's native GA gone, nothing else assigns it globally.
window.gtag = window.gtag || gtag;
gtag("consent", "default", { ad_storage: "granted", ad_user_data: "granted", ad_personalization: "granted", analytics_storage: "granted", functionality_storage: "granted", personalization_storage: "granted", security_storage: "granted" });
gtag("consent", "default", { ad_storage: "denied", ad_user_data: "denied", ad_personalization: "denied", analytics_storage: "denied", functionality_storage: "denied", personalization_storage: "denied", security_storage: "granted", wait_for_update: 500, region: ["AT","BE","BG","HR","CY","CZ","DK","EE","FI","FR","DE","GR","HU","IE","IT","LV","LT","LU","MT","NL","PL","PT","RO","SK","SI","ES","SE","GB","IS","LI","NO"] });
gtag("set", "ads_data_redaction", true);
if (!document.getElementById("ga-gtag")) {
var ga = document.createElement("script");
ga.id = "ga-gtag";
ga.async = true;
ga.src = "https://www.googletagmanager.com/gtag/js?id=G-47H9F2MT29";
document.head.appendChild(ga);
}
gtag("js", new Date());
gtag("config", "G-47H9F2MT29");
/* -------- consent gate (geography-aware, fail closed) -------- */
/* Port of website lib/consent-client.ts / webflow-analytics.js -
KEEP IN SYNC. No CookieYes script on docs (bannerless), so the
getCkyConsent API branch is normally absent and the cookie
fallback carries the state; the consent_update listener is kept
for defense-in-depth should the CMP ever appear on this surface. */
function consentRequiredByGeo() {
/* matchAll-equivalent + any-"1"-wins: duplicate nvm_cg cookies
(host-only vs domain drift) resolve to the protective value
(PR website#235 review). Lookahead terminator so back-to-back
duplicates both match; optional whitespace separator. */
var all = [];
var re = /(?:^|;\s*)nvm_cg=([01])(?=\s*;|$)/g;
var m;
while ((m = re.exec(document.cookie)) !== null) all.push(m[1]);
return all.length === 0 ? null : all.indexOf("1") !== -1;
}
/* True only for a grant made by explicit user action. CookieYes
AUTO-GRANTS categories outside its banner geography; that must
never unlock PostHog FULL (localStorage + replay). */
function explicitlyGranted() {
try {
var api = window.getCkyConsent && window.getCkyConsent();
if (api && api.categories) {
return api.isUserActionCompleted === true && api.categories.analytics === true;
}
} catch (e) { /* fall through to cookie */ }
var m = document.cookie.match(/(?:^|; )cookieyes-consent=([^;]+)/);
if (!m) return false;
var v;
try { v = decodeURIComponent(m[1]); } catch (e) { v = m[1]; }
return /(?:^|,)analytics:yes/.test(v) && /(?:^|,)action:yes/.test(v);
}
function consentState() {
try {
var api = window.getCkyConsent && window.getCkyConsent();
if (api && api.categories) {
if (api.categories.analytics === true) return "granted";
if (api.isUserActionCompleted === true && api.categories.analytics === false) return "denied";
}
} catch (e) { /* fall through to cookie */ }
var m = document.cookie.match(/(?:^|; )cookieyes-consent=([^;]+)/);
if (!m) return "unknown";
var v;
try { v = decodeURIComponent(m[1]); } catch (e) { v = m[1]; }
if (/(?:^|,)analytics:yes/.test(v)) return "granted";
if (/(?:^|,)analytics:no/.test(v) && /(?:^|,)action:yes/.test(v)) return "denied";
return "unknown";
}
function effectiveConsentState() {
var s = consentState();
/* GPC: a browser sending Global Privacy Control never gets the
geo-grant - the US-undecided population is exactly who GPC
protects. */
var gpc = typeof navigator !== "undefined" && navigator.globalPrivacyControl === true;
/* A grant WITHOUT user action is CookieYes's out-of-banner
auto-grant - geography-derived, so it obeys the same rules as
the geo-grant: outside protection geography only, never past
GPC. */
if (s === "granted" && !explicitlyGranted()) {
return consentRequiredByGeo() === false && !gpc ? "granted" : "unknown";
}
if (s === "unknown" && consentRequiredByGeo() === false && !gpc) return "granted";
return s;
}
function whenConsented(fn) {
var done = false;
var timer = null;
function fire() {
if (done) return;
done = true;
document.removeEventListener("cookieyes_consent_update", check);
if (timer) clearInterval(timer);
fn();
}
function check() {
if (effectiveConsentState() === "granted") fire();
}
document.addEventListener("cookieyes_consent_update", check);
timer = setInterval(check, 500);
check();
}
/* Capture-on-consent recovery: only meaningful for an EXPLICITLY
consenting visitor whose capture the middleware deferred (they
consented on the marketing site, then landed here first on a later
visit). Geo-granted visitors got their cookies server-side
already. */
whenConsented(function consentTouch() {
if (!explicitlyGranted()) return;
if (document.cookie.indexOf("nvm_ft=") !== -1) return;
fetch("/api/consent-touch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
referrer: document.referrer,
page: window.location.pathname,
search: window.location.search
})
}).catch(function () { /* best effort */ });
});
/* ---------------- 1. RB2B (website#237 decision) ---------------- */
whenConsented(function loadRB2B() {
if (document.getElementById("rb2b-script")) return;
var s = document.createElement("script");
s.id = "rb2b-script";
s.async = true;
s.src =
"https://ddwl4m2hdecbv.cloudfront.net/b/" + RB2B_ID + "/" + RB2B_ID + ".js.gz";
document.body.appendChild(s);
});
/* ------- 2. PostHog (EU, anonymous mode) - geography/consent-gated ------- */
function loadPostHog(full) {
if (window.posthog && window.posthog.__loaded) return;
/* Geography-aware persistence (website#235): localStorage from
first visit outside the protection geography; memory inside it
pre-consent and for GPC (both resolve to non-granted effective
state). Replay unchanged: explicit grants only. */
var persist = (full || effectiveConsentState() === "granted") ? "localStorage" : "memory";
/* PostHog snippet (current official loader, 2026 method surface,
assets-host delivery, crossOrigin) */
!function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="Ji Yi init fn mn Ur pn bn cn capture calculateEventProperties Sn register register_once register_for_session unregister unregister_for_session Tn getFeatureFlag getFeatureFlagPayload getFeatureFlagResult getAllFeatureFlags isFeatureEnabled reloadFeatureFlags updateFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey cancelPendingSurvey canRenderSurvey canRenderSurveyAsync Mn identify setPersonProperties unsetPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset shutdown setIdentity clearIdentity get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException addExceptionStep captureLog startExceptionAutocapture stopExceptionAutocapture loadToolbar get_property getSessionProperty Cn xn createPersonProfile setInternalOrTestUser In hn Pn opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing debug Vr Rt getPageViewId captureTraceFeedback captureTraceMetric an".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
window.posthog.init(POSTHOG_KEY, {
api_host: POSTHOG_HOST,
/* Dashboard/toolbar links must NOT go through the proxy. */
ui_host: "https://eu.posthog.com",
// 2026-05-30 defaults: notably pageview capture on history changes,
// essential here - Mintlify docs are an SPA and client-side
// navigations would otherwise be invisible.
defaults: "2026-05-30",
autocapture: false,
capture_pageview: "history_change",
// Curated set v1: $pageview + replay ONLY (no $pageleave).
capture_pageleave: false,
person_profiles: "identified_only",
persistence: persist,
disable_session_recording: !full,
disable_surveys: true,
session_recording: { maskAllInputs: true }
});
}
/* PostHog consent controller - mirror of the blog loader (keep in
sync): denied -> never load / opt out; non-granted -> memory-mode
pageviews only; effective grant -> load with geography-aware
persistence; FULL (replay) only on an explicitly actioned grant. */
var phUpgraded = false;
/* THE replay policy switch - mirror of website lib/consent-client
REPLAY_REQUIRES_EXPLICIT_GRANT, keep in sync (all three ports flip
together + privacy-checklist amendment). */
var REPLAY_REQUIRES_EXPLICIT_GRANT = true;
function applyPostHogConsent() {
var s = effectiveConsentState();
var raw = (REPLAY_REQUIRES_EXPLICIT_GRANT ? explicitlyGranted() : s === "granted")
? "granted" : "not-explicit";
if (s === "denied") {
if (window.posthog && window.posthog.__loaded && window.posthog.opt_out_capturing) {
window.posthog.opt_out_capturing();
}
return;
}
if (!(window.posthog && window.posthog.__loaded)) {
loadPostHog(raw === "granted");
phUpgraded = raw === "granted";
return;
}
if (raw === "granted" && !phUpgraded) {
phUpgraded = true;
if (window.posthog.has_opted_out_capturing && window.posthog.has_opted_out_capturing() && window.posthog.opt_in_capturing) {
window.posthog.opt_in_capturing();
}
if (window.posthog.set_config) window.posthog.set_config({ persistence: "localStorage" });
if (window.posthog.startSessionRecording) window.posthog.startSessionRecording();
}
}
document.addEventListener("cookieyes_consent_update", applyPostHogConsent);
/* The 1 Hz poll is the ONLY change channel on a bannerless surface
(no CMP script means no consent_update events): it notices a
consent given on the marketing site in another tab, and the
nvm_cg cookie on a first navigation. Kept running by design. */
setInterval(applyPostHogConsent, 1000);
applyPostHogConsent();
/* ------- 3. nvm_o decorator + app_handoff_click ------- */
function readCookie(name) {
var m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
if (!m) return null;
try { return decodeURIComponent(m[1]); } catch (e) { return null; }
}
function unwrapTouch(value) {
if (!value) return null;
if (value.indexOf("v1.") !== 0) return value;
var parts = value.split(".");
if (parts.length !== 3) return null;
try {
return atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"));
} catch (e) {
return null;
}
}
function b64url(s) {
var bytes = new TextEncoder().encode(s);
var bin = "";
for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function parseTouch(v) {
if (!v) return null;
try {
var t = JSON.parse(v);
return t && typeof t === "object" ? t : null;
} catch (e) {
return null;
}
}
function isAppHost(h) {
h = h.toLowerCase();
return h === "nevermined.app" || h.slice(-15) === ".nevermined.app";
}
function decorate(event) {
var t = event.target;
var el = t && t.nodeType === 1 ? t : t ? t.parentElement : null;
var a = el && el.closest ? el.closest("a[href]") : null;
if (!a) return;
var url;
try {
url = new URL(a.href);
} catch (e) {
return;
}
if (!isAppHost(url.hostname)) return;
/* Explicit denial means no attribution forwarding — the nvm_o
payload stays off the URL. The click event below still fires
(GCM decides what GA does with it under denial). */
var lt = null;
if (consentState() !== "denied") {
var ft = parseTouch(unwrapTouch(readCookie("nvm_ft")));
lt = parseTouch(unwrapTouch(readCookie("nvm_lt")));
var payload = {};
if (ft) payload.ft = ft;
if (lt) payload.lt = lt;
/* pid: anonymous PostHog distinct_id rides the handoff so the
webapp merges the docs click-session into the product person
(website#235 payload contract). ANONYMOUS ids only - never an
email-shaped id (no identify() runs on docs, but the guard is
part of the contract). */
try {
if (window.posthog && window.posthog.__loaded && window.posthog.get_distinct_id) {
var pid = window.posthog.get_distinct_id();
if (pid && pid.indexOf("@") === -1) payload.pid = pid.slice(0, 120);
}
} catch (e) { /* optional */ }
url.searchParams.set("nvm_o", b64url(JSON.stringify(payload)));
a.href = url.toString();
}
if (window.gtag) {
window.gtag("event", "app_handoff_click", {
link_url: url.hostname + url.pathname,
channel: lt && lt.ch ? lt.ch : "unknown"
});
}
}
document.addEventListener("click", decorate, true);
document.addEventListener("auxclick", decorate, true);
})();