|
| 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 | +})(); |
0 commit comments