Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/blog/src/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import posthog from "posthog-js";
import { hasAnalyticsConsent, onAnalyticsConsentChange } from "@prisma-docs/ui/lib/consent";

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
capture_pageview: "history_change",
defaults: "2025-11-30",
// GDPR/ePrivacy: do not set cookies or capture anything until the visitor
// grants analytics consent via CookieYes. Opt-in is handled below.
opt_out_capturing_by_default: true,
loaded: (posthog) => {
posthog.register({
site_name: "mono-blog",
environment: "production",
});
// Returning visitor whose stored consent is already available at init.
if (hasAnalyticsConsent()) posthog.opt_in_capturing();
},
});

// React to live banner interactions and to CookieYes restoring stored consent.
onAnalyticsConsentChange((granted) => {
if (granted) posthog.opt_in_capturing();
else posthog.opt_out_capturing();
});
12 changes: 12 additions & 0 deletions apps/docs/src/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import posthog from "posthog-js";
import * as Sentry from "@sentry/nextjs";
import { hasAnalyticsConsent, onAnalyticsConsentChange } from "@prisma-docs/ui/lib/consent";

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
capture_pageview: "history_change",
defaults: "2025-11-30",
// GDPR/ePrivacy: do not set cookies or capture anything until the visitor
// grants analytics consent via CookieYes. Opt-in is handled below.
opt_out_capturing_by_default: true,
loaded: (posthog) => {
posthog.register({
site_name: "mono-docs",
environment: "production",
});
// Returning visitor whose stored consent is already available at init.
if (hasAnalyticsConsent()) posthog.opt_in_capturing();
},
});

// React to live banner interactions and to CookieYes restoring stored consent.
onAnalyticsConsentChange((granted) => {
if (granted) posthog.opt_in_capturing();
else posthog.opt_out_capturing();
});

Sentry.init({
dsn: "https://e83ce4699e59051fdeaa330bf4a0dfb9@o4510879743737856.ingest.us.sentry.io/4510879744000000",

Expand Down
12 changes: 12 additions & 0 deletions apps/site/src/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import posthog from "posthog-js";
import { hasAnalyticsConsent, onAnalyticsConsentChange } from "@prisma-docs/ui/lib/consent";

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
capture_pageview: "history_change",
defaults: "2025-11-30",
// GDPR/ePrivacy: do not set cookies or capture anything until the visitor
// grants analytics consent via CookieYes. Opt-in is handled below.
opt_out_capturing_by_default: true,
loaded: (posthog) => {
posthog.register({
site_name: "mono-site",
environment: "production",
});
// Returning visitor whose stored consent is already available at init.
if (hasAnalyticsConsent()) posthog.opt_in_capturing();
},
});

// React to live banner interactions and to CookieYes restoring stored consent.
onAnalyticsConsentChange((granted) => {
if (granted) posthog.opt_in_capturing();
else posthog.opt_out_capturing();
});
61 changes: 61 additions & 0 deletions packages/ui/src/lib/consent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* CookieYes analytics-consent helpers.
*
* These read the exact same signals the GTM consent bridge already uses
* (see `components/google-tag-manager.tsx`): the `cookieyes_consent_update`
* and `cookieyes_banner_load` events, and the `getCkyConsent()` global.
* Using one source of truth keeps every analytics SDK gated consistently.
*
* GDPR/ePrivacy note: analytics SDKs must not set cookies or send data until
* the visitor grants analytics consent. Callers should start opted-out and
* only opt in from these helpers.
*/

/** CookieYes category key for analytics cookies. */
const ANALYTICS_CATEGORY = "analytics";

type CkyConsent = { categories?: Record<string, boolean> };

declare global {
interface Window {
getCkyConsent?: () => CkyConsent;
}
}

/**
* True when CookieYes has a stored decision granting analytics consent.
*
* Returns false during SSR, before CookieYes has loaded, or when the visitor
* has not (yet) accepted analytics — i.e. the safe default is "no consent".
*/
export function hasAnalyticsConsent(): boolean {
if (typeof window === "undefined") return false;
try {
return Boolean(window.getCkyConsent?.().categories?.[ANALYTICS_CATEGORY]);
} catch {
return false;
}
}

/**
* Invokes `onChange(granted)` whenever analytics consent changes.
*
* - Fires on `cookieyes_consent_update` when the visitor accepts/rejects from
* the banner.
* - Fires on `cookieyes_banner_load` so returning visitors who previously
* consented are opted in once CookieYes restores their stored decision.
*
* Safe no-op during SSR.
*/
export function onAnalyticsConsentChange(onChange: (granted: boolean) => void): void {
if (typeof document === "undefined") return;

document.addEventListener("cookieyes_consent_update", (event) => {
const accepted = (event as CustomEvent<{ accepted?: string[] }>).detail?.accepted ?? [];
onChange(accepted.includes(ANALYTICS_CATEGORY));
});

document.addEventListener("cookieyes_banner_load", () => {
onChange(hasAnalyticsConsent());
});
}
Loading