From 2b3f9d5d7468407b7865a71e057c049fb81522cf Mon Sep 17 00:00:00 2001 From: andrewleesteele <8799863+andrewleesteele@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:53:40 +0000 Subject: [PATCH 1/5] Add consent-gated Conceptual Analytics pixel to docs 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 --- conceptual.js | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 conceptual.js diff --git a/conceptual.js b/conceptual.js new file mode 100644 index 00000000..98e59e0f --- /dev/null +++ b/conceptual.js @@ -0,0 +1,84 @@ +/* + * Conceptual Analytics pixel. + * + * Mintlify loads every .js file in this directory on every docs page. The docs + * are served under www.kernel.sh/docs via a rewrite from the marketing site, so + * the c15t consent cookie the marketing site sets is readable here and the two + * halves of the origin share one consent decision. + */ +(function () { + var CONSENT_CATEGORY = "marketing"; + var PIXEL_KEY = + "acfc03eea265bec68ec3dfb5bc9f1bf6cda7644ceed3260980286b6f0b4098a3"; + var LOADER_SRC = + "https://plfalg.kernel.sh/analytics/loader-v1.js?key=" + + encodeURIComponent(PIXEL_KEY) + + "&v=1.1.0"; + + function hasConsent() { + try { + var cookie = document.cookie.match(/(?:^|;\s*)c15t=([^;]*)/); + if (cookie && cookie[1]) { + return cookie[1].indexOf("c." + CONSENT_CATEGORY + ":1") !== -1; + } + } catch (e) {} + + try { + var raw = localStorage.getItem("c15t"); + if (raw) { + var parsed = JSON.parse(raw); + return !!(parsed && parsed.consents && parsed.consents[CONSENT_CATEGORY]); + } + } catch (e) {} + + return false; + } + + function load() { + if (window.ca) return; + + // measure-v1.js reads __CA_CONFIG once at init, so it has to be set before + // the loader injects it. Both flags are off in the shipped pixel config. + window.__CA_CONFIG = window.__CA_CONFIG || {}; + window.__CA_CONFIG.respectDNT = true; + window.__CA_CONFIG.anonymizeIP = true; + + window.ca = function () { + (window.ca.q = window.ca.q || []).push(arguments); + }; + + var script = document.createElement("script"); + script.async = true; + script.src = LOADER_SRC; + document.head.appendChild(script); + } + + // The pixel sends one page_view on init and has no router hooks, so docs + // navigation between pages is invisible unless we send it ourselves. + function trackNavigations() { + var lastPath = location.pathname + location.search; + + function onNavigate() { + var path = location.pathname + location.search; + if (path === lastPath) return; + lastPath = path; + if (window.ca) window.ca("track", "page_view"); + } + + ["pushState", "replaceState"].forEach(function (method) { + var original = history[method]; + history[method] = function () { + var result = original.apply(this, arguments); + onNavigate(); + return result; + }; + }); + + window.addEventListener("popstate", onNavigate); + } + + if (!hasConsent()) return; + + load(); + trackNavigations(); +})(); From 05231c24bcb6f92a04b997ff20505ec63a8c7d12 Mon Sep 17 00:00:00 2001 From: andrewleesteele <8799863+andrewleesteele@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:05:18 +0000 Subject: [PATCH 2/5] Geo-gate the docs pixel instead of requiring a stored consent decision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- conceptual.js | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/conceptual.js b/conceptual.js index 98e59e0f..f5be2e52 100644 --- a/conceptual.js +++ b/conceptual.js @@ -3,8 +3,8 @@ * * Mintlify loads every .js file in this directory on every docs page. The docs * are served under www.kernel.sh/docs via a rewrite from the marketing site, so - * the c15t consent cookie the marketing site sets is readable here and the two - * halves of the origin share one consent decision. + * both the c15t consent cookie and the c15t API are same-origin here and the + * two halves of the site can share one consent decision. */ (function () { var CONSENT_CATEGORY = "marketing"; @@ -14,12 +14,15 @@ "https://plfalg.kernel.sh/analytics/loader-v1.js?key=" + encodeURIComponent(PIXEL_KEY) + "&v=1.1.0"; + var JURISDICTION_URL = "/api/c15t/show-consent-banner"; - function hasConsent() { + // true granted, false declined, null no decision recorded yet. + function storedConsent() { try { var cookie = document.cookie.match(/(?:^|;\s*)c15t=([^;]*)/); if (cookie && cookie[1]) { - return cookie[1].indexOf("c." + CONSENT_CATEGORY + ":1") !== -1; + if (cookie[1].indexOf("c." + CONSENT_CATEGORY + ":1") !== -1) return true; + if (cookie[1].indexOf("c." + CONSENT_CATEGORY + ":0") !== -1) return false; } } catch (e) {} @@ -27,11 +30,14 @@ var raw = localStorage.getItem("c15t"); if (raw) { var parsed = JSON.parse(raw); - return !!(parsed && parsed.consents && parsed.consents[CONSENT_CATEGORY]); + var consents = parsed && parsed.consents; + if (consents && typeof consents[CONSENT_CATEGORY] === "boolean") { + return consents[CONSENT_CATEGORY]; + } } } catch (e) {} - return false; + return null; } function load() { @@ -77,8 +83,28 @@ window.addEventListener("popstate", onNavigate); } - if (!hasConsent()) return; + function start() { + load(); + trackNavigations(); + } + + var stored = storedConsent(); + if (stored !== null) { + if (stored) start(); + return; + } - load(); - trackNavigations(); + // Nobody has decided yet, which is the normal case for a visitor whose first + // page is a docs page — the consent banner lives in the marketing site's app + // and never renders here. Apply the same rule c15t applies there: it only + // prompts in regulated jurisdictions and auto-grants everywhere else. Any + // failure leaves the pixel unloaded. + fetch(JURISDICTION_URL, { credentials: "same-origin" }) + .then(function (response) { + return response.ok ? response.json() : null; + }) + .then(function (data) { + if (data && data.showConsentBanner === false) start(); + }) + .catch(function () {}); })(); From 00f2bf5a1b3b089782cfded636481a10c102cc32 Mon Sep 17 00:00:00 2001 From: andrewleesteele <8799863+andrewleesteele@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:07:37 +0000 Subject: [PATCH 3/5] Hand the device ID to onkernel.com on outbound links 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. --- conceptual.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/conceptual.js b/conceptual.js index f5be2e52..5b577976 100644 --- a/conceptual.js +++ b/conceptual.js @@ -15,6 +15,8 @@ encodeURIComponent(PIXEL_KEY) + "&v=1.1.0"; var JURISDICTION_URL = "/api/c15t/show-consent-banner"; + var HANDOFF_PARAM = "ca_device_id"; + var HANDOFF_DOMAIN = "onkernel.com"; // true granted, false declined, null no decision recorded yet. function storedConsent() { @@ -83,9 +85,35 @@ window.addEventListener("popstate", onNavigate); } + // The device ID is stored per-domain, and the docs link to dashboard sign-up + // in several places, so without this the ad click that led to a signup is + // never credited. Hand ours over on the way out; the dashboard reads it back. + function handOffDeviceIdOnNavigation() { + document.addEventListener( + "click", + function (event) { + var link = event.target.closest && event.target.closest("a[href]"); + if (!link || typeof window.ca.getDeviceId !== "function") return; + + var url = new URL(link.href, window.location.href); + if ( + url.hostname !== HANDOFF_DOMAIN && + !url.hostname.endsWith("." + HANDOFF_DOMAIN) + ) { + return; + } + + url.searchParams.set(HANDOFF_PARAM, window.ca.getDeviceId()); + link.href = url.toString(); + }, + { capture: true } + ); + } + function start() { load(); trackNavigations(); + handOffDeviceIdOnNavigation(); } var stored = storedConsent(); From e2a4b029e2bbc54487d10c420270b81343061164 Mon Sep 17 00:00:00 2001 From: andrewleesteele <8799863+andrewleesteele@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:57:43 +0000 Subject: [PATCH 4/5] Treat a stored consent record without marketing as a decline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- conceptual.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/conceptual.js b/conceptual.js index 5b577976..71f3aacf 100644 --- a/conceptual.js +++ b/conceptual.js @@ -19,12 +19,15 @@ var HANDOFF_DOMAIN = "onkernel.com"; // true granted, false declined, null no decision recorded yet. + // + // c15t omits false values from its cookie rather than writing them, so a + // decline is the absence of the key. Any stored consent record therefore + // means a decision was made, and a missing category in one means no. function storedConsent() { try { var cookie = document.cookie.match(/(?:^|;\s*)c15t=([^;]*)/); if (cookie && cookie[1]) { - if (cookie[1].indexOf("c." + CONSENT_CATEGORY + ":1") !== -1) return true; - if (cookie[1].indexOf("c." + CONSENT_CATEGORY + ":0") !== -1) return false; + return cookie[1].indexOf("c." + CONSENT_CATEGORY + ":1") !== -1; } } catch (e) {} @@ -32,9 +35,8 @@ var raw = localStorage.getItem("c15t"); if (raw) { var parsed = JSON.parse(raw); - var consents = parsed && parsed.consents; - if (consents && typeof consents[CONSENT_CATEGORY] === "boolean") { - return consents[CONSENT_CATEGORY]; + if (parsed && parsed.consents) { + return parsed.consents[CONSENT_CATEGORY] === true; } } } catch (e) {} From fd57bcc366936d323b6aa5fc659a075690e0568d Mon Sep 17 00:00:00 2001 From: andrewleesteele <8799863+andrewleesteele@users.noreply.github.com> Date: Wed, 9 Sep 2026 03:36:22 +0000 Subject: [PATCH 5/5] Re-read consent on each navigation, and stop implying anonymizeIP works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both from review on the marketing-site pixel, which shares this approach. Consent is decided once at page load here, but it can be withdrawn on the marketing site while a docs page stays open — and Mintlify is a single-page app, so that page can live a long time. There's no consent UI on docs to react to, so navigation page views and the device-ID handoff now re-read the decision instead of trusting the one made at load. anonymizeIP is documented as inert rather than implied to be active: the shipped script defines it and never reads it, and resolves the visitor's full address to send as client_ip regardless. --- conceptual.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/conceptual.js b/conceptual.js index 71f3aacf..2ce7dcb5 100644 --- a/conceptual.js +++ b/conceptual.js @@ -49,6 +49,12 @@ // measure-v1.js reads __CA_CONFIG once at init, so it has to be set before // the loader injects it. Both flags are off in the shipped pixel config. + // + // respectDNT takes effect: the pixel checks it before every event. + // anonymizeIP currently does not — the shipped script defines it and never + // reads it, and resolves the visitor's full address from the vendor's IP + // endpoint to send as client_ip regardless. It's set here so it applies if + // the vendor implements it; until then full-IP processing is what happens. window.__CA_CONFIG = window.__CA_CONFIG || {}; window.__CA_CONFIG.respectDNT = true; window.__CA_CONFIG.anonymizeIP = true; @@ -72,7 +78,10 @@ var path = location.pathname + location.search; if (path === lastPath) return; lastPath = path; - if (window.ca) window.ca("track", "page_view"); + // Consent can be withdrawn on the marketing site while a docs page stays + // open. There's no consent UI here to react to, so every navigation + // re-reads the decision rather than trusting the one made at page load. + if (window.ca && storedConsent() !== false) window.ca("track", "page_view"); } ["pushState", "replaceState"].forEach(function (method) { @@ -96,6 +105,7 @@ function (event) { var link = event.target.closest && event.target.closest("a[href]"); if (!link || typeof window.ca.getDeviceId !== "function") return; + if (storedConsent() === false) return; var url = new URL(link.href, window.location.href); if (