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
34 changes: 25 additions & 9 deletions apps/blog/src/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
import posthog from "posthog-js";
import { hasAnalyticsConsent, onAnalyticsConsentChange } from "@prisma-docs/ui/lib/consent";

const SUPER_PROPERTIES = {
site_name: "mono-blog",
environment: "production",
};

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.
// GDPR/ePrivacy: no cookies, storage, or persistent identifiers until the
// visitor grants analytics consent via CookieYes. Opt-in is handled below.
// Until then (banner ignored or analytics rejected) visitors are counted
// cookielessly: events carry the $posthog_cookieless sentinel and PostHog's
// servers derive a daily rotating hash; nothing identifying is stored
// on-device. Requires "Cookieless server hash mode" in project settings,
// otherwise these events are dropped at ingestion.
cookieless_mode: "on_reject",
// With cookieless_mode this also makes not-yet-decided visitors count as
// rejected (cookieless) rather than uncaptured.
opt_out_capturing_by_default: true,
loaded: (posthog) => {
posthog.register({
site_name: "mono-blog",
environment: "production",
});
posthog.register(SUPER_PROPERTIES);
// Returning visitor whose stored consent is already available at init.
if (hasAnalyticsConsent()) posthog.opt_in_capturing();
},
Comment on lines +25 to 28

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 6 'loaded:|opt_in_capturing|register\(SUPER_PROPERTIES\)' \
  apps/blog/src/instrumentation-client.ts \
  apps/docs/src/instrumentation-client.ts \
  apps/site/src/instrumentation-client.ts

Repository: prisma/web

Length of output: 7191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PostHog dependency declarations ---'
rg -n -C 3 '"posthog-js"|posthog-js@|posthog-js/' \
  package.json pnpm-lock.yaml yarn.lock package-lock.json apps 2>/dev/null || true

printf '%s\n' '--- Instrumentation imports and consent helpers ---'
rg -n -C 8 'from "posthog-js"|from '\''posthog-js'\''|function hasAnalyticsConsent|const hasAnalyticsConsent|onAnalyticsConsentChange' \
  apps/blog/src/instrumentation-client.ts \
  apps/docs/src/instrumentation-client.ts \
  apps/site/src/instrumentation-client.ts \
  apps/blog apps/docs apps/site 2>/dev/null | head -n 240

Repository: prisma/web

Length of output: 25171


🌐 Web query:

PostHog JavaScript SDK opt_in_capturing register super properties reset behavior source

💡 Result:

In the PostHog JavaScript SDK, the interaction between reset behavior, consent, and super properties is critical for accurate data tracking [1][2]. Reset and Consent Behavior When you call posthog.reset, the SDK clears the user's identity (setting a new random distinct_id), removes cached data, and clears the consent state [1][2]. Crucially, because reset clears consent, it returns the instance to the default consent state [1][2]. If you have configured the SDK with opt_out_capturing_by_default: true, the default state is "opted out" [1][2]. Consequently, if you call posthog.reset after posthog.opt_in_capturing, the SDK will silently stop capturing data [1][2]. The recommended pattern is to always call posthog.reset before calling posthog.opt_in_capturing [1][2]. Super Properties Behavior Super properties are properties set using posthog.register that are automatically included with every event [3][2]. - Persistence: Super properties are stored in persistent storage (e.g., cookies or local storage) [3][2]. - Reset Impact: Calling posthog.reset clears these stored super properties, as it clears the instance's cached data and persistence [1][2]. - Updates: If you register the same property multiple times, the new value overwrites the old one [3][2]. To remove a specific super property without a full reset, use posthog.unregister('property_name') [3]. Summary of Best Practices To maintain proper state and consent, follow this sequence: 1. Initialize with appropriate defaults (e.g., opt_out_capturing_by_default: true) [4][5]. 2. Call posthog.reset to clear previous identity and super properties [1][2][6]. 3. Call posthog.opt_in_capturing to establish consent [1][2][4]. 4. Call posthog.register to set your desired super properties for the new session [3][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
import urllib.request

urls = [
    "https://github.com/ghraw/PostHog/posthog-js/v1.415.7/src/posthog-core.ts",
    "https://github.com/ghraw/PostHog/posthog-js/v1.415.7/src/posthog-surveys.ts",
    "https://github.com/ghraw/PostHog/posthog-js/v1.415.7/src/posthog.ts",
]
for url in urls:
    print(f"--- {url} ---")
    try:
        text = urllib.request.urlopen(url, timeout=15).read().decode()
    except Exception as exc:
        print(f"FETCH_ERROR {exc}")
        continue
    lines = text.splitlines()
    for i, line in enumerate(lines):
        if any(token in line for token in (
            "opt_in_capturing", "opt_out_capturing", "register(", "reset(",
            "clear_opt_in_out_capturing", "clear_opt_in_out_capturing",
        )):
            lo = max(0, i - 8)
            hi = min(len(lines), i + 18)
            print("\n".join(f"{n+1}:{lines[n]}" for n in range(lo, hi)))
            print("---")
PY

Repository: prisma/web

Length of output: 857


🌐 Web query:

github PostHog posthog-js v1.415.7 opt_in_capturing implementation

💡 Result:

In the PostHog JavaScript SDK (posthog-js), the opt_in_capturing method is used to enable data capturing and persistence (cookies/localStorage) for the current PostHog instance [1][2]. This is typically used in conjunction with a consent management platform (CMP) or a manual consent banner to comply with privacy regulations [3]. Key Implementation Details: 1. Recommended Pattern: The standard approach is to initialize PostHog with opt_out_capturing_by_default: true in your config [3][4]. This ensures no data is captured until the user provides explicit consent [3]. When the user grants consent, you then call posthog.opt_in_capturing [3]. 2. Method Signature: The method accepts an optional configuration object to customize the opt-in behavior [1][2]: posthog.opt_in_capturing(options?: { captureEventName?: EventName | null | false; captureProperties?: Properties; }) 3. Customization Options: - captureEventName: By default, calling this method captures an $opt_in event [1][2]. You can set this to a custom string to use a different event name, or set it to false if you wish to opt the user in without sending an event [1][2]. - captureProperties: You can provide a dictionary of properties to be attached to the opt-in event [1][2]. 4. Persistence: Calling opt_in_capturing automatically enables persistence (re-enabling cookies/localStorage) [1][5]. The SDK manages this state based on your instance configuration, such as the opt_out_capturing_persistence_type setting [6]. Example Usage: // Simple opt-in posthog.opt_in_capturing; // Opt-in with custom event and properties posthog.opt_in_capturing({ captureEventName: 'Privacy Accepted', captureProperties: { source: 'banner' } }); // Opt-in without sending an event posthog.opt_in_capturing({ captureEventName: false }); When a user withdraws or denies consent, you should call posthog.opt_out_capturing to stop further data collection and disable persistence [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for file in src/posthog-core.ts src/posthog.ts; do
  url="https://github.com/ghraw/PostHog/posthog-js/v1.415.7/$file"
  echo "--- $url ---"
  curl -fsSLk "$url" |
    nl -ba |
    awk '
      /opt_in_capturing|opt_out_capturing|register[[:space:]]*\(|reset[[:space:]]*\(|clear_opt_in_out_capturing/ {
        start = $1 - 10
        if (start < 1) start = 1
        end = $1 + 22
        print "lines " start "-" end
        show = 1
      }
      show && $1 >= start && $1 <= end { print }
      show && $1 > end { show = 0; print "---" }
    '
done

Repository: prisma/web

Length of output: 378


🌐 Web query:

site:github.com/PostHog/posthog-js/blob/v1.415.7/src/posthog-core.ts "opt_in_capturing"

💡 Result:

The requested GitHub path appears to be unavailable: tag v1.415.7 or src/posthog-core.ts returns 404 Not Found. ()

The opt_in_capturing API is documented as enabling event capture and has corresponding status-check methods such as has_opted_in_capturing(). (app.unpkg.com)

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import io
import json
import ssl
import tarfile
import urllib.request

ctx = ssl._create_unverified_context()
meta_url = "https://registry.npmjs.org/posthog-js/1.415.7"
with urllib.request.urlopen(meta_url, context=ctx, timeout=20) as response:
    meta = json.load(response)

tarball_url = meta["dist"]["tarball"]
print("--- package ---")
print(meta["name"], meta["version"], tarball_url)

with urllib.request.urlopen(tarball_url, context=ctx, timeout=30) as response:
    data = response.read()

with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive:
    names = archive.getnames()
    candidates = [
        name for name in names
        if name.endswith((".js", ".ts", ".d.ts")) and
        any(token in name.lower() for token in ("posthog", "index"))
    ]
    print("--- candidate files ---")
    print("\n".join(candidates[:80]))

    terms = (
        "opt_in_capturing", "opt_out_capturing", "register(", "reset(",
        "clear_opt_in_out_capturing", "super_properties",
    )
    for name in candidates:
        text = archive.extractfile(name).read().decode("utf-8", errors="replace")
        lines = text.splitlines()
        hits = [i for i, line in enumerate(lines) if any(term in line for term in terms)]
        if hits:
            print(f"--- {name} ---")
            shown = set()
            for i in hits:
                for j in range(max(0, i - 5), min(len(lines), i + 12)):
                    if j not in shown:
                        print(f"{j+1}:{lines[j]}")
                        shown.add(j)
                print("---")
PY

Repository: prisma/web

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

log=/tmp/coderabbit-shell-logs/shell-output-wHF0v2
printf '%s\n' '--- opt-in/opt-out matches ---'
rg -n -o '.{0,500}opt_in_capturing.{0,1200}' "$log" | head -n 20

printf '%s\n' '--- reset and super-property matches near consent methods ---'
rg -n -o '.{0,500}(clear_opt_in_out_capturing|clearPersistence|reset\(\)|super_properties|register\().{0,1200}' "$log" | head -n 40

Repository: prisma/web

Length of output: 4761


🏁 Script executed:

#!/bin/bash
set -euo pipefail

log=/tmp/coderabbit-shell-logs/shell-output-wHF0v2
for range in '660,715' '515,610' '250,345' '1210,1335'; do
  echo "--- lines $range ---"
  sed -n "${range}p" "$log"
done

Repository: prisma/web

Length of output: 17644


🏁 Script executed:

#!/bin/bash
set -euo pipefail

log=/tmp/coderabbit-shell-logs/shell-output-wHF0v2
printf '%s\n' '--- opt-in implementation continuation ---'
sed -n '683,735p' "$log"

printf '%s\n' '--- cookieless predicate and initialization state ---'
rg -n -o '.{0,700}(_inCookielessMode|COOKIELESS_ON_REJECT|cookieless_mode|opt_out_capturing_by_default).{0,1200}' "$log" | head -n 40

printf '%s\n' '--- all three callback orderings ---'
for file in apps/blog/src/instrumentation-client.ts apps/docs/src/instrumentation-client.ts apps/site/src/instrumentation-client.ts; do
  echo "--- $file ---"
  sed -n '1,45p' "$file"
done

Repository: prisma/web

Length of output: 13642


Register properties after initial opt-in.

When hasAnalyticsConsent() is true, posthog.opt_in_capturing() resets persistence and removes SUPER_PROPERTIES. Move posthog.register(SUPER_PROPERTIES) after the opt-in in all three instrumentation files.

📍 Affects 3 files
  • apps/blog/src/instrumentation-client.ts#L25-L28 (this comment)
  • apps/docs/src/instrumentation-client.ts#L25-L28
  • apps/site/src/instrumentation-client.ts#L24-L27
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog/src/instrumentation-client.ts` around lines 25 - 28, In the
instrumentation initialization callback, move register(SUPER_PROPERTIES) to
after the conditional opt_in_capturing() call so properties persist when
hasAnalyticsConsent() is true. Apply this change in
apps/blog/src/instrumentation-client.ts lines 25-28,
apps/docs/src/instrumentation-client.ts lines 25-28, and
apps/site/src/instrumentation-client.ts lines 24-27.

});

// React to live banner interactions and to CookieYes restoring stored consent.
onAnalyticsConsentChange((granted) => {
if (granted) posthog.opt_in_capturing();
else posthog.opt_out_capturing();
// "pending" must NOT opt out: an explicit opt-out writes an opt-out flag to
// device storage, and the visitor has not made a decision yet; cookieless
// capture already covers them.
onAnalyticsConsentChange((status) => {
if (status === "granted") posthog.opt_in_capturing();
else if (status === "denied") posthog.opt_out_capturing();
// Both transitions reset the SDK state that held the registered
// super-properties, so re-register or later events lose site_name.
if (status !== "pending") posthog.register(SUPER_PROPERTIES);
});
34 changes: 25 additions & 9 deletions apps/docs/src/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,43 @@ import posthog from "posthog-js";
import * as Sentry from "@sentry/nextjs";
import { hasAnalyticsConsent, onAnalyticsConsentChange } from "@prisma-docs/ui/lib/consent";

const SUPER_PROPERTIES = {
site_name: "mono-docs",
environment: "production",
};

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.
// GDPR/ePrivacy: no cookies, storage, or persistent identifiers until the
// visitor grants analytics consent via CookieYes. Opt-in is handled below.
// Until then (banner ignored or analytics rejected) visitors are counted
// cookielessly: events carry the $posthog_cookieless sentinel and PostHog's
// servers derive a daily rotating hash; nothing identifying is stored
// on-device. Requires "Cookieless server hash mode" in project settings,
// otherwise these events are dropped at ingestion.
cookieless_mode: "on_reject",
// With cookieless_mode this also makes not-yet-decided visitors count as
// rejected (cookieless) rather than uncaptured.
opt_out_capturing_by_default: true,
loaded: (posthog) => {
posthog.register({
site_name: "mono-docs",
environment: "production",
});
posthog.register(SUPER_PROPERTIES);
// 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();
// "pending" must NOT opt out: an explicit opt-out writes an opt-out flag to
// device storage, and the visitor has not made a decision yet; cookieless
// capture already covers them.
onAnalyticsConsentChange((status) => {
if (status === "granted") posthog.opt_in_capturing();
else if (status === "denied") posthog.opt_out_capturing();
// Both transitions reset the SDK state that held the registered
// super-properties, so re-register or later events lose site_name.
if (status !== "pending") posthog.register(SUPER_PROPERTIES);
});

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

const SUPER_PROPERTIES = {
site_name: "mono-site",
environment: "production",
};

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.
// GDPR/ePrivacy: no cookies, storage, or persistent identifiers until the
// visitor grants analytics consent via CookieYes. Opt-in is handled below.
// Until then (banner ignored or analytics rejected) visitors are counted
// cookielessly: events carry the $posthog_cookieless sentinel and PostHog's
// servers derive a daily rotating hash; nothing identifying is stored
// on-device. Requires "Cookieless server hash mode" in project settings,
// otherwise these events are dropped at ingestion.
cookieless_mode: "on_reject",
// With cookieless_mode this also makes not-yet-decided visitors count as
// rejected (cookieless) rather than uncaptured.
opt_out_capturing_by_default: true,
loaded: (posthog) => {
posthog.register({
site_name: "mono-site",
environment: "production",
});
posthog.register(SUPER_PROPERTIES);
// 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();
// "pending" must NOT opt out: an explicit opt-out writes an opt-out flag to
// device storage, and the visitor has not made a decision yet; cookieless
// capture already covers them.
onAnalyticsConsentChange((status) => {
if (status === "granted") posthog.opt_in_capturing();
else if (status === "denied") posthog.opt_out_capturing();
// Both transitions reset the SDK state that held the registered
// super-properties, so re-register or later events lose site_name.
if (status !== "pending") posthog.register(SUPER_PROPERTIES);
});
51 changes: 36 additions & 15 deletions packages/ui/src/lib/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,24 @@
* 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.
*
* Consent is tri-state: a visitor who has never interacted with the banner
* ("pending", `isUserActionCompleted: false`) is not the same as one who
* rejected analytics ("denied"). PostHog's cookieless mode counts pending
* visitors without touching device storage, so callers must not collapse
* "pending" into an explicit opt-out that writes an opt-out flag.
*/

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

type CkyConsent = { categories?: Record<string, boolean> };
export type AnalyticsConsentStatus = "granted" | "denied" | "pending";

type CkyConsent = {
categories?: Record<string, boolean>;
/** True once the visitor has accepted/rejected/saved from the banner. */
isUserActionCompleted?: boolean;
};

declare global {
interface Window {
Expand All @@ -23,39 +35,48 @@ declare global {
}

/**
* True when CookieYes has a stored decision granting analytics consent.
* The visitor's stored analytics-consent decision.
*
* 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".
* - `"granted"`: the visitor accepted analytics cookies.
* - `"denied"`: the visitor made a choice that excludes analytics.
* - `"pending"`: SSR, CookieYes not loaded yet, or no banner interaction yet.
*/
export function hasAnalyticsConsent(): boolean {
if (typeof window === "undefined") return false;
export function getAnalyticsConsentStatus(): AnalyticsConsentStatus {
if (typeof window === "undefined") return "pending";
try {
return Boolean(window.getCkyConsent?.().categories?.[ANALYTICS_CATEGORY]);
const consent = window.getCkyConsent?.();
if (!consent || !consent.isUserActionCompleted) return "pending";
return consent.categories?.[ANALYTICS_CATEGORY] ? "granted" : "denied";
} catch {
return false;
return "pending";
}
}

/** True when CookieYes has a stored decision granting analytics consent. */
export function hasAnalyticsConsent(): boolean {
return getAnalyticsConsentStatus() === "granted";
}

/**
* Invokes `onChange(granted)` whenever analytics consent changes.
* Invokes `onChange(status)` whenever the analytics-consent status 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.
* the banner. This is always an explicit decision, so never "pending".
* - Fires on `cookieyes_banner_load` so returning visitors' stored decisions
* are applied once CookieYes restores them. Reports "pending" when the
* visitor has not interacted with the banner yet.
*
* Safe no-op during SSR.
*/
export function onAnalyticsConsentChange(onChange: (granted: boolean) => void): void {
export function onAnalyticsConsentChange(onChange: (status: AnalyticsConsentStatus) => 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));
onChange(accepted.includes(ANALYTICS_CATEGORY) ? "granted" : "denied");
});

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