Skip to content

Commit f921b3f

Browse files
authored
Merge branch 'master' into production
2 parents 684ee46 + 0690eb2 commit f921b3f

3 files changed

Lines changed: 303 additions & 6 deletions

File tree

documentation/docs.json

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -351,14 +351,12 @@
351351
]
352352
},
353353
"integrations": {
354-
"ga4": {
355-
"measurementId": "G-7FZEBFLWK0"
356-
},
357-
"amplitude": {
358-
"apiKey": "6d37003f0cdb1921422bc474c634c135"
359-
},
360354
"telemetry": {
361355
"enabled": true
356+
},
357+
"cookies": {
358+
"key": "rq_consent_tracking",
359+
"value": "granted"
362360
}
363361
},
364362
"errors": {

documentation/js/analytics.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/* Analytics for interceptor-docs.requestly.com — Amplitude + GA4, both CONSENT-GATED on "tracking".
2+
3+
WHY THIS FILE EXISTS
4+
Mintlify can inject Amplitude and GA4 for you via `integrations` in docs.json, but it injects
5+
them itself, before custom JS runs, and there is no setting that makes them wait for consent.
6+
So both were removed from docs.json and re-implemented here instead. Same vendors, same keys,
7+
same data — the only difference is that nothing loads until the visitor grants tracking consent,
8+
and a withdrawal tears both down.
9+
10+
PAGEVIEWS
11+
Mintlify is a single-page app: the first load is a real navigation, everything after is
12+
history.pushState. Amplitude's autocapture and GA4's default page_view only see the first one,
13+
so route changes are tracked explicitly below. Without that, gating would have quietly turned a
14+
full pageview stream into a single event per session.
15+
16+
DELIBERATELY NOT the CDN snippet build (cdn.amplitude.com/script/<key>.js). That bundle pulls in
17+
Session Replay, which starts a recording and contacts sr-client-cfg.amplitude.com. The docs do
18+
not need recording, and quietly acquiring it would need its own disclosure.
19+
20+
SOURCE ATTRIBUTION
21+
All three Requestly properties share Amplitude project key 6d37003f0c..., so without a
22+
distinguishing property their traffic is indistinguishable in that project. Every event and
23+
user here carries source="interceptor-docs", matching the convention api-explorer already uses.
24+
docs.requestly.com does NOT set this yet — it is a listed follow-up on
25+
requestly/requestly-api-client#4540.
26+
27+
Timing note: custom JS runs after the page is interactive, so these load slightly later than
28+
Mintlify's platform integrations did. Pageview totals should be comparable but not identical to
29+
history — do not read the changeover as data loss. */
30+
(function () {
31+
var AMP_KEY = "6d37003f0cdb1921422bc474c634c135";
32+
var AMP_SDK = "https://cdn.amplitude.com/libs/analytics-browser-2.11.0-min.js.gz";
33+
var GA4_ID = "G-7FZEBFLWK0";
34+
var SOURCE = "interceptor-docs";
35+
36+
var granted = false;
37+
var ampReady = false;
38+
var ga4Ready = false;
39+
40+
/* ---------- Amplitude ---------- */
41+
function bootAmplitude() {
42+
if (document.getElementById("rq-amp-sdk")) return;
43+
var s = document.createElement("script");
44+
s.id = "rq-amp-sdk";
45+
s.src = AMP_SDK;
46+
s.onload = function () {
47+
if (!granted) return; // consent withdrawn while the SDK was in flight
48+
if (!window.amplitude || typeof window.amplitude.init !== "function") return;
49+
window.amplitude.init(AMP_KEY, {
50+
autocapture: {
51+
attribution: true,
52+
pageViews: true,
53+
sessions: true,
54+
formInteractions: false,
55+
fileDownloads: false,
56+
elementInteractions: false,
57+
},
58+
});
59+
window.amplitude.setOptOut(false); // clear a persisted opt-out from an earlier withdrawal
60+
try {
61+
var id = new window.amplitude.Identify();
62+
id.set("source", SOURCE);
63+
window.amplitude.identify(id);
64+
} catch (e) {}
65+
ampReady = true;
66+
};
67+
document.head.appendChild(s);
68+
}
69+
70+
/* ---------- GA4 ---------- */
71+
function bootGa4() {
72+
if (document.getElementById("rq-ga4-sdk")) return;
73+
window.dataLayer = window.dataLayer || [];
74+
window.gtag = function () {
75+
window.dataLayer.push(arguments);
76+
};
77+
window["ga-disable-" + GA4_ID] = false; // undo a previous teardown
78+
var s = document.createElement("script");
79+
s.id = "rq-ga4-sdk";
80+
s.async = true;
81+
s.src = "https://www.googletagmanager.com/gtag/js?id=" + GA4_ID;
82+
s.onload = function () {
83+
if (!granted) return;
84+
window.gtag("js", new Date());
85+
/* send_page_view is left ON for the first load; SPA routes are sent explicitly below. */
86+
window.gtag("config", GA4_ID, { source: SOURCE });
87+
ga4Ready = true;
88+
};
89+
document.head.appendChild(s);
90+
}
91+
92+
/* ---------- SPA pageviews ---------- */
93+
var lastPath = location.pathname + location.search;
94+
95+
function trackPageView() {
96+
if (!granted) return;
97+
var path = location.pathname + location.search;
98+
if (path === lastPath) return; // pushState fires for hash/no-op changes too
99+
lastPath = path;
100+
if (ampReady && window.amplitude) {
101+
try {
102+
window.amplitude.track("Page Viewed", { path: location.pathname, source: SOURCE });
103+
} catch (e) {}
104+
}
105+
if (ga4Ready && window.gtag) {
106+
window.gtag("event", "page_view", {
107+
page_path: path,
108+
page_title: document.title,
109+
source: SOURCE,
110+
});
111+
}
112+
}
113+
114+
function patchHistory(method) {
115+
var original = history[method];
116+
history[method] = function () {
117+
original.apply(this, arguments);
118+
setTimeout(trackPageView, 0);
119+
};
120+
}
121+
patchHistory("pushState");
122+
patchHistory("replaceState");
123+
window.addEventListener("popstate", function () {
124+
setTimeout(trackPageView, 0);
125+
});
126+
127+
/* ---------- Shared event API ----------
128+
Nothing on this property emits custom events today. Exposed so that when something does, the
129+
sanctioned way to send it is already gated — rather than a new file loading its own SDK, which
130+
is how this property ended up with ungated analytics in the first place. Safe to call at any
131+
time: a no-op until consent exists, re-checked per call so a withdrawal takes effect. */
132+
window.rqAnalytics = {
133+
track: function (name, props) {
134+
if (!granted || !ampReady || !window.amplitude) return;
135+
var p = props || {};
136+
p.source = SOURCE;
137+
try {
138+
window.amplitude.track(name, p);
139+
} catch (e) {}
140+
},
141+
};
142+
143+
/* ---------- Consent ---------- */
144+
function onGrant() {
145+
granted = true;
146+
bootAmplitude();
147+
bootGa4();
148+
}
149+
150+
/* A loaded bundle cannot be unloaded, so silence both SDKs and clear what they persisted.
151+
GA4 honours the window['ga-disable-<id>'] flag; Amplitude honours setOptOut. */
152+
function onRevoke() {
153+
granted = false;
154+
ampReady = false;
155+
ga4Ready = false;
156+
try {
157+
if (window.amplitude && window.amplitude.setOptOut) window.amplitude.setOptOut(true);
158+
} catch (e) {}
159+
window["ga-disable-" + GA4_ID] = true;
160+
if (window.rqConsent && window.rqConsent.clearCookies) {
161+
window.rqConsent.clearCookies(/^(AMP_|EXP_|_ga|_gid|_gat)/);
162+
}
163+
try {
164+
Object.keys(localStorage)
165+
.filter(function (k) {
166+
return /^(AMP_|EXP_)/.test(k);
167+
})
168+
.forEach(function (k) {
169+
localStorage.removeItem(k);
170+
});
171+
} catch (e) {}
172+
}
173+
174+
if (window.rqDocsConsent) {
175+
window.rqDocsConsent.whenConsent("tracking", onGrant, onRevoke);
176+
} else {
177+
console.warn("[analytics] consent gate unavailable — Amplitude and GA4 not loaded");
178+
}
179+
})();
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/* Consent bootstrap for interceptor-docs.requestly.com.
2+
3+
WHY THIS EXISTS
4+
Until this landed, this docs site ran GA4 and Amplitude with no consent banner and no gate of
5+
any kind. It also carries the SAME GA4 measurement id (G-7FZEBFLWK0) and the SAME Amplitude
6+
project key (6d37003f0c...) as requestly.com and docs.requestly.com, so a visitor who pressed
7+
"Reject Optional" on the marketing site and then followed a link here was tracked anyway, under
8+
the same device id.
9+
10+
HOW IT IS FIXED
11+
TrustArc writes its decision cookies on `.requestly.com`, so a choice made on the marketing
12+
site is ALREADY readable here. We therefore load the marketing site's gate rather than shipping
13+
a third copy of it:
14+
15+
https://requestly.com/js/consent.js -> window.rqConsent
16+
17+
One implementation, one set of category indexes, no drift across the three properties. If that
18+
request fails, rqConsent never appears and every consumer stays shut — fail closed, which is
19+
the correct direction.
20+
21+
MINTLIFY CONSTRAINTS worth knowing before changing any of this:
22+
* Custom JS cannot be injected into <head>; it runs after the page is interactive. TrustArc
23+
auto-block is therefore NOT available on this property (it must be the first script on the
24+
page to intercept anything). Enforcement here is the explicit rqConsent gate only.
25+
* Mintlify auto-includes every .js file under this folder on every page, in an order we do
26+
not control. Consumers must not assume window.rqConsent exists yet — use whenConsent()
27+
below, which waits for it.
28+
* Mintlify's OWN telemetry is injected by the platform and cannot be gated from JS. It is
29+
gated instead by `integrations.cookies` in docs.json, which disables telemetry unless a
30+
localStorage key is present. This file writes that key on grant and removes it on revoke.
31+
Keep the key/value here in sync with docs.json.
32+
33+
This file is deliberately identical to documentation/js/consent-bootstrap.js in
34+
requestly/requestly-api-client (docs.requestly.com). If one changes, change both. */
35+
(function () {
36+
var GATE_SRC = "https://requestly.com/js/consent.js";
37+
var NOTICE_SRC = "https://consent.trustarc.com/v2/notice/87juza";
38+
39+
/* Mirrors the "tracking" grant into localStorage for docs.json -> integrations.cookies.
40+
Must match docs.json exactly. */
41+
var MINTLIFY_KEY = "rq_consent_tracking";
42+
var MINTLIFY_VALUE = "granted";
43+
44+
if (window.rqDocsConsent) return; // Mintlify is a SPA; only bootstrap once per load.
45+
46+
/* TrustArc draws the banner into this element. requestly.com provides it in its layout; on
47+
Mintlify we have no template to edit, so create it. */
48+
function ensureMount() {
49+
if (document.getElementById("consent-banner")) return;
50+
var d = document.createElement("div");
51+
d.id = "consent-banner";
52+
d.style.cssText = "position:fixed;bottom:0;left:0;width:100%;z-index:999999";
53+
document.body.appendChild(d);
54+
}
55+
56+
function inject(src, id) {
57+
if (document.getElementById(id)) return;
58+
var s = document.createElement("script");
59+
s.id = id;
60+
s.src = src;
61+
s.async = true;
62+
document.head.appendChild(s);
63+
}
64+
65+
/* Consumers call this instead of touching window.rqConsent directly, because file order is not
66+
guaranteed and the gate arrives over the network. Polls for up to ~20s, then gives up —
67+
giving up means nothing non-essential ever runs, which is the safe outcome. */
68+
var waiters = [];
69+
function whenConsent(need, onGrant, onRevoke) {
70+
waiters.push({ need: need, onGrant: onGrant, onRevoke: onRevoke });
71+
}
72+
73+
var tries = 0;
74+
var poll = setInterval(function () {
75+
tries++;
76+
if (window.rqConsent) {
77+
clearInterval(poll);
78+
/* Keep Mintlify's telemetry flag in step with the tracking grant. */
79+
window.rqConsent.on(
80+
"tracking",
81+
function () {
82+
try {
83+
localStorage.setItem(MINTLIFY_KEY, MINTLIFY_VALUE);
84+
} catch (e) {}
85+
},
86+
function () {
87+
try {
88+
localStorage.removeItem(MINTLIFY_KEY);
89+
} catch (e) {}
90+
}
91+
);
92+
waiters.forEach(function (w) {
93+
window.rqConsent.on(w.need, w.onGrant, w.onRevoke);
94+
});
95+
waiters = [];
96+
/* Late callers (SPA navigation) go straight through. */
97+
whenConsent = function (need, onGrant, onRevoke) {
98+
window.rqConsent.on(need, onGrant, onRevoke);
99+
};
100+
window.rqDocsConsent.whenConsent = function (n, g, r) {
101+
whenConsent(n, g, r);
102+
};
103+
return;
104+
}
105+
if (tries > 200) {
106+
clearInterval(poll);
107+
console.warn("[consent] " + GATE_SRC + " did not load — nothing non-essential will run.");
108+
}
109+
}, 100);
110+
111+
window.rqDocsConsent = {
112+
whenConsent: function (n, g, r) {
113+
whenConsent(n, g, r);
114+
},
115+
};
116+
117+
ensureMount();
118+
inject(GATE_SRC, "rq-consent-gate");
119+
inject(NOTICE_SRC, "truste-consent-js");
120+
})();

0 commit comments

Comments
 (0)