Skip to content

Add consent-gated Conceptual Analytics pixel to dev docs - #550

Open
andrewleesteele wants to merge 4 commits into
mainfrom
hypeship/conceptual-analytics-pixel
Open

Add consent-gated Conceptual Analytics pixel to dev docs#550
andrewleesteele wants to merge 4 commits into
mainfrom
hypeship/conceptual-analytics-pixel

Conversation

@andrewleesteele

@andrewleesteele andrewleesteele commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

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 .js file in the content directory on every page, which is the supported way to inject a third-party script. docs.json integrations is 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

  1. If a c15t decision is already stored, honour it. Docs share an origin with the marketing site, so the cookie set there is readable here and an accept or decline carries over.
  2. Otherwise apply the same rule c15t applies on the marketing site: ask /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-grants marketing and measurement, so this matches the behaviour a visitor would get had they landed on a marketing page instead.
  3. Any failure — endpoint unreachable, non-OK response, parse error — leaves the pixel unloaded.

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 respectDNT and anonymizeIP, both off in the shipped pixel config, before the loader runs, and sends a page_view on 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/docs origin by injecting this script with the vendor's dev key, which validates requests but stores nothing:

egress resolved jurisdiction banner required pixel loaded device cookie events
us-east US NONE no yes set 1 page_view
eu-west GB GDPR yes no none none

node --check passes. 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, so measurement is 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 under www.kernel.sh/docs (where the marketing layout pixel never loads).

The pixel only starts after marketing consent is read from the shared c15t cookie/localStorage, or—if there is no stored decision—a same-origin call to /api/c15t/show-consent-banner indicates 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 respectDNT and anonymizeIP set via __CA_CONFIG, emits page_view on client-side route changes (pushState/replaceState/popstate), and appends ca_device_id to outbound links to onkernel.com for 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.

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>
@mintlify

mintlify Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
Kernel 🟢 Ready View Preview Sep 5, 2026, 6:54 PM

💡 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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from plfalg.kernel.sh, reads c15t consent state from cookies and localStorage, calls /api/c15t/show-consent-banner, and monkey-patches history.pushState / history.replaceState to emit page_view events.

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.head on production docs traffic.
  • Global History API patching. Wrapping pushState / replaceState can 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.

Open in Web View Automation 

Sent by Cursor Automation: Assign PR reviewers

@andrewleesteele andrewleesteele changed the title Add consent-gated Conceptual Analytics pixel to docs Add consent-gated Conceptual Analytics pixel to devdocs Sep 5, 2026
@andrewleesteele andrewleesteele changed the title Add consent-gated Conceptual Analytics pixel to devdocs Add consent-gated Conceptual Analytics pixel to dev docs Sep 5, 2026
@andrewleesteele
andrewleesteele marked this pull request as ready for review September 5, 2026 19:46

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .js file 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.js into document.head
  • Reads c15t consent state from cookies and localStorage
  • If no stored decision exists, calls /api/c15t/show-consent-banner and starts tracking when showConsentBanner === false
  • Monkey-patches history.pushState / history.replaceState and listens for popstate to emit page_view events

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. 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 / replaceState can 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.

Open in Web View Automation 

Sent by Cursor Automation: Assign PR reviewers

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread conceptual.js
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.
@andrewleesteele

Copy link
Copy Markdown
Contributor Author

Pushed the cross-domain device ID handoff here too.

The docs link to dashboard.onkernel.com/sign-up in several places, so a reader signing up from a docs page hits the same problem: the device ID is domain-scoped, the dashboard mints its own, and the ad click is never credited. This appends the ID to outbound onkernel.com links on click.

Verified against the live www.kernel.sh/docs origin with the vendor's dev key: the pixel loads, getDeviceId() returns an ID, a dashboard.onkernel.com/sign-up link is decorated with it, and a notonkernel.com lookalike is left untouched.

The receiving half lives in kernel/kernel.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk assessment: Medium

Verdict: Do not auto-approve.

Re-evaluated after the 00f2bf5 synchronize (outbound ca_device_id handoff). This is still not a documentation-only change. The diff adds a single new file, conceptual.js (138 lines). Mintlify loads every .js file 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.js into document.head
  • Reads c15t consent state from cookies and localStorage
  • If no stored decision exists, calls /api/c15t/show-consent-banner and starts tracking when showConsentBanner === false
  • Monkey-patches history.pushState / history.replaceState and listens for popstate to emit page_view events
  • New in this update: capture-phase click interceptor that mutates a[href] toward onkernel.com / *.onkernel.com by appending ca_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 / replaceState can 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.

Open in Web View Automation 

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.js into document.head
  • Reads c15t consent state from cookies and localStorage
  • If a stored consent record exists, starts tracking only when marketing is 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-banner and starts tracking when showConsentBanner === false
  • Monkey-patches history.pushState / history.replaceState and listens for popstate to emit page_view events
  • Capture-phase click interceptor that mutates a[href] toward onkernel.com / *.onkernel.com by appending ca_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 / replaceState can 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.

Open in Web View Automation 

Sent by Cursor Automation: Assign PR reviewers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant