Add consent-gated Conceptual Analytics pixel to dev docs - #550
Add consent-gated Conceptual Analytics pixel to dev docs#550andrewleesteele wants to merge 4 commits into
Conversation
Mintlify loads every .js file in the content directory on every page, which is how the pixel reaches docs pages that the marketing site's layout never renders. The docs are served under www.kernel.sh/docs, so the c15t consent cookie set on the marketing site is readable here and gates the pixel the same way. respectDNT and anonymizeIP are off in the shipped pixel config and are turned on before the loader runs. The pixel has no router hooks, so docs navigation sends its own page_view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
The consent banner lives in the marketing site's app and never renders on a docs page, so a visitor whose first page is a docs page had no way to reach a decision and never got the pixel — which is most of the traffic this is meant to cover. An existing decision still wins. Absent one, ask the same c15t endpoint the marketing site uses and apply its rule: prompt in regulated jurisdictions, auto-grant elsewhere. The endpoint is same-origin here. Any failure leaves the pixel unloaded.
There was a problem hiding this comment.
Stale comment
Risk assessment: Medium
Verdict: Do not auto-approve.
This is not a documentation-only change. The diff adds
conceptual.js, which Mintlify loads on every docs page. That script injects a third-party analytics loader fromplfalg.kernel.sh, readsc15tconsent state from cookies andlocalStorage, calls/api/c15t/show-consent-banner, and monkey-patcheshistory.pushState/history.replaceStateto emitpage_viewevents.Why this is Medium (not Low / Very Low)
- Site-wide user-facing surface. The new file runs for all
/docs/*visitors, not an isolated page or internal tool.- Privacy / consent model. Tracking can start without a stored decision whenever the jurisdiction endpoint returns
showConsentBanner === false. The second commit widened that from fail-closed (require stored consent) to geo-gated auto-load.- Third-party script injection. A remote loader is appended to
document.headon production docs traffic.- Global History API patching. Wrapping
pushState/replaceStatecan interact with Mintlify's own client router.- Blast radius is the full docs origin. A wrong consent parse, endpoint response, or loader behavior affects measurement and privacy for every visitor.
Why this is not High
The change is a single self-contained file, fails closed on fetch/parse errors, uses
credentials: "same-origin", and does not touch auth, billing, infra, or server-side code in this repo. Those bounds keep it at Medium rather than higher.Action: Comment only. Human review of the consent/geo-gate and third-party loader is required before merge.
Sent by Cursor Automation: Assign PR reviewers
There was a problem hiding this comment.
Stale comment
Risk assessment: Medium
Verdict: Do not auto-approve.
This is not a documentation-only change. The diff adds a single new file,
conceptual.js(110 lines). Mintlify loads every.jsfile in the content directory on every docs page, so this runs for all/docs/*visitors.What the code actually does
- Injects a third-party loader from
https://plfalg.kernel.sh/analytics/loader-v1.jsintodocument.head- Reads
c15tconsent state from cookies andlocalStorage- If no stored decision exists, calls
/api/c15t/show-consent-bannerand starts tracking whenshowConsentBanner === false- Monkey-patches
history.pushState/history.replaceStateand listens forpopstateto emitpage_vieweventsWhy this is Medium (not Low / Very Low)
- Site-wide user-facing surface. Every docs visitor executes this script, not an isolated page or internal tool.
- Privacy / consent model. Tracking can start without a stored user decision whenever the jurisdiction endpoint returns
showConsentBanner === false. The second commit widened this from fail-closed (require stored consent) to geo-gated auto-load.- Third-party script injection. A remote analytics loader is appended on production docs traffic.
- Global History API patching. Wrapping
pushState/replaceStatecan interact with Mintlify's client router.- Blast radius is the full docs origin. A wrong consent parse, unexpected endpoint response, or loader behavior affects measurement and privacy for every visitor.
Why this is not High
The change is one self-contained file. Fetch/parse failures leave the pixel unloaded, the jurisdiction request uses
credentials: "same-origin", and this repo's auth, billing, infra, and server-side code are untouched. Those bounds keep it at Medium rather than higher.Action: Comment only. Human review of the consent/geo-gate and third-party loader is required before merge.
Sent by Cursor Automation: Assign PR reviewers
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 05231c2. Configure here.
The device ID is stored per-domain, so the dashboard mints its own and the ad click that led to a signup is never credited. The docs link to dashboard sign-up in several places, so this path matters here too. Appends the ID to onkernel.com links on click. The dashboard side reads it back, which is not in this repo.
|
Pushed the cross-domain device ID handoff here too. The docs link to Verified against the live The receiving half lives in |
There was a problem hiding this comment.
Stale comment
Risk assessment: Medium
Verdict: Do not auto-approve.
Re-evaluated after the
00f2bf5synchronize (outboundca_device_idhandoff). This is still not a documentation-only change. The diff adds a single new file,conceptual.js(138 lines). Mintlify loads every.jsfile in the content directory on every docs page, so this runs for all/docs/*visitors.No prior approval from this automation exists, so there is nothing to revoke.
What the code actually does
- Injects a third-party loader from
https://plfalg.kernel.sh/analytics/loader-v1.jsintodocument.head- Reads
c15tconsent state from cookies andlocalStorage- If no stored decision exists, calls
/api/c15t/show-consent-bannerand starts tracking whenshowConsentBanner === false- Monkey-patches
history.pushState/history.replaceStateand listens forpopstateto emitpage_viewevents- New in this update: capture-phase click interceptor that mutates
a[href]towardonkernel.com/*.onkernel.comby appendingca_device_idWhy this is Medium (not Low / Very Low)
- Site-wide user-facing surface. Every docs visitor executes this script, not an isolated page or internal tool.
- Privacy / consent model. Tracking can start without a stored user decision whenever the jurisdiction endpoint returns
showConsentBanner === false.- Third-party script injection. A remote analytics loader is appended on production docs traffic.
- Global History API patching. Wrapping
pushState/replaceStatecan interact with Mintlify's client router.- Cross-domain identifier handoff (new). A visitor identifier is written into outbound URLs. That expands privacy blast radius (query strings, logs, Referer, history) beyond same-origin measurement.
- Blast radius is the full docs origin. A wrong consent parse, unexpected endpoint response, or loader behavior affects measurement and privacy for every visitor.
What changed vs. the last assessment
Risk stays Medium, not High. The new click interceptor increases the tracking/privacy surface but does not touch auth, billing, infra, or server-side code. Those bounds still keep it below High.
Action: Comment only. Human review of the consent/geo-gate, third-party loader, and cross-domain device-ID handoff is required before merge.
Sent by Cursor Automation: Assign PR reviewers
c15t omits false booleans from its cookie rather than writing them, so a decline never appears as `c.marketing:0` — it's the absence of the key. Reading only for an explicit `:0` made a decline indistinguishable from no decision, and the jurisdiction fallback then loaded the pixel for someone who had opted out. Any stored consent record now means a decision was made, and a missing category in one means no.
There was a problem hiding this comment.
Risk assessment: Medium
Verdict: Do not auto-approve.
Re-evaluated after the e2a4b02 synchronize (treat a stored consent record without marketing as a decline). This is still not a documentation-only change. The diff adds a single new file, conceptual.js (140 lines). As the only .js file in this Mintlify content root, it is loaded on every docs page and runs for all /docs/* visitors.
No prior approval from this automation exists, so there is nothing to revoke. No CODEOWNERS file is present.
What the code actually does
- Injects a third-party loader from
https://plfalg.kernel.sh/analytics/loader-v1.jsintodocument.head - Reads
c15tconsent state from cookies andlocalStorage - If a stored consent record exists, starts tracking only when
marketingis explicitly granted; otherwise treats the record as a decline and does not fall through to geo-gating - If no stored decision exists, calls
/api/c15t/show-consent-bannerand starts tracking whenshowConsentBanner === false - Monkey-patches
history.pushState/history.replaceStateand listens forpopstateto emitpage_viewevents - Capture-phase click interceptor that mutates
a[href]towardonkernel.com/*.onkernel.comby appendingca_device_id
Why this is Medium (not Low / Very Low)
- Site-wide user-facing surface. Every docs visitor executes this script, not an isolated page or internal tool.
- Privacy / consent model. Tracking can start without a stored user decision whenever the jurisdiction endpoint returns
showConsentBanner === false. - Third-party script injection. A remote analytics loader is appended on production docs traffic.
- Global History API patching. Wrapping
pushState/replaceStatecan interact with Mintlify's client router. - Cross-domain identifier handoff. A visitor identifier is written into outbound URLs (query strings, logs, Referer, history).
- Blast radius is the full docs origin. A wrong consent parse, unexpected endpoint response, or loader behavior affects measurement and privacy for every visitor.
What changed vs. the last assessment
Risk stays Medium, not High, and did not increase.
e2a4b02 is a consent-parse tightening: a present c15t cookie or localStorage record without an explicit marketing:1 / marketing === true now returns decline (false) instead of "no decision" (null). That closes a path where an opt-out could fall through to the geo-gate and load the pixel. It reduces one privacy-bug risk; it does not shrink the site-wide tracking, third-party loader, History API, or device-ID handoff surface.
This repo's auth, billing, infra, and server-side code remain untouched, which keeps the rating below High.
Action: Comment only. Human review of the consent/geo-gate, third-party loader, and cross-domain device-ID handoff is required before merge.
Sent by Cursor Automation: Assign PR reviewers



Summary
Adds the Conceptual Analytics front-end pixel to the dev docs site, so ad click IDs and page views are captured on
/docs/*as well as the marketing site. Pairs with kernel/website#305, which covers the marketing site.Why a custom script
www.kernel.sh/docs/*is a Next rewrite that proxies to this Mintlify app, so the marketing site's root layout — and the pixel component in it — never renders on a docs page. Docs currently have no pixel at all.Mintlify loads every
.jsfile in the content directory on every page, which is the supported way to inject a third-party script.docs.jsonintegrationsis a fixed vendor allowlist with no Conceptual entry, so a custom script is the only route short of adding a GTM container.How it decides whether to load
/api/c15t/show-consent-banner, which is same-origin here, and load only where no banner is required. In regulated jurisdictions c15t prompts; everywhere else it auto-grantsmarketingandmeasurement, so this matches the behaviour a visitor would get had they landed on a marketing page instead.The gate is needed because the consent banner lives in the marketing site's React tree and never renders on a docs page. Requiring a stored decision would have meant almost no coverage for visitors whose first page is a docs page, which is the traffic this is for.
It also turns on
respectDNTandanonymizeIP, both off in the shipped pixel config, before the loader runs, and sends apage_viewon docs navigation — the pixel sends one on init and has no router hooks, so client-side navigation between docs pages is otherwise invisible.The key in this file is the public pixel key, which is designed to be visible in browser code. The server-side conversion key is not used here and must never appear in a browser.
Testing
Verified end to end against the live
www.kernel.sh/docsorigin by injecting this script with the vendor's dev key, which validates requests but stores nothing:NONEpage_viewGDPRnode --checkpasses. Not verified in a Mintlify preview deployment — the jurisdiction endpoint is only reachable when the docs are served under the marketing domain, and the script fails closed anywhere else.Note for reviewers
The consent category is
marketing, matching kernel/website#305. Our privacy policy files ad-campaign measurement under analytics, someasurementis arguable; it is one constant in each PR if we want to switch.Note
Medium Risk
Touches consent gating and third-party analytics loading; behavior is fail-closed but mis-read consent or jurisdiction logic could load or block tracking incorrectly.
Overview
Adds
conceptual.js, a Mintlify-injected script so Conceptual Analytics runs on docs pages underwww.kernel.sh/docs(where the marketing layout pixel never loads).The pixel only starts after
marketingconsent is read from the shared c15t cookie/localStorage, or—if there is no stored decision—a same-origin call to/api/c15t/show-consent-bannerindicates no banner is required (matching auto-grant outside regulated regions). Errors or banner-required jurisdictions leave the pixel unloaded.When allowed, it injects the CA loader with
respectDNTandanonymizeIPset via__CA_CONFIG, emitspage_viewon client-side route changes (pushState/replaceState/popstate), and appendsca_device_idto outbound links toonkernel.comfor attribution on dashboard sign-up.Reviewed by Cursor Bugbot for commit e2a4b02. Bugbot is set up for automated code reviews on this repo. Configure here.