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
3 changes: 2 additions & 1 deletion components/feedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faThumbsUp, faThumbsDown } from "@fortawesome/free-solid-svg-icons";
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import posthog from "posthog-js";
import { consentedIdentify } from "@/lib/consent";

export function Feedback() {
const [isVisible, setIsVisible] = useState(false);
Expand Down Expand Up @@ -49,7 +50,7 @@ export function Feedback() {
e.preventDefault();

if (formData.email) {
posthog.identify(formData.email);
consentedIdentify(posthog, formData.email);
}

posthog.capture("docs-feedback-detail", {
Expand Down
10 changes: 3 additions & 7 deletions components/scripts.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"use client";

import inEU from "@segment/in-eu";
import { usePathname, useSearchParams } from "next/navigation";
import { Router } from "next/router";
import Script from "next/script";
import posthog from "posthog-js";
import { Suspense, useEffect, useState } from "react";
import { isEUVisitor, shouldOptOutCapturing } from "@/lib/consent";

const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";
const baseDir = process.env.NEXT_PUBLIC_BASE_DIR || "";
Expand All @@ -18,10 +16,8 @@ function HubSpot() {
const searchParams = useSearchParams();

useEffect(() => {
if (window) {
setLoadHs(!inEU());
}
}, [loadHs]);
setLoadHs(!shouldOptOutCapturing(isEUVisitor()));
}, []);

useEffect(() => {
// @ts-ignore
Expand Down
14 changes: 10 additions & 4 deletions instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import posthog from "posthog-js";
import inEU from "@segment/in-eu";
import { isEUVisitor, shouldOptOutCapturing } from "@/lib/consent";

const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";

const initPostHog = () => {
if (inEU() || !process.env.NEXT_PUBLIC_POSTHOG_KEY) {
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY || !isProd) {
return;
}

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY ?? "", {
api_host: isProd ? "/i" : process.env.NEXT_PUBLIC_POSTHOG_HOST, // See Posthog rewrites in next config
const optOut = shouldOptOutCapturing(isEUVisitor());

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: "/i",
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
defaults: "2025-11-30",
person_profiles: "always",
cross_subdomain_cookie: true,
opt_out_capturing_by_default: optOut,
opt_out_persistence_by_default: optOut,
cookieless_mode: "on_reject",
loaded: (posthog) => {
if (process.env.NODE_ENV === "development") posthog.debug();
},
Expand Down
19 changes: 19 additions & 0 deletions lib/consent/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/** Debug cookie names used by ConsentProvider to override detection. */
export const DEBUG_EU_COOKIE = "__consent_debug_eu";
export const DEBUG_GPC_COOKIE = "__consent_debug_gpc";

/**
* Read a debug override cookie by name. Returns null if not set or
* if running in production (Vercel production environment).
*/
export function getDebugCookie(name: string): string | null {
if (typeof document === "undefined") return null;
if (typeof process !== "undefined") {
const env =
(process.env as Record<string, string | undefined>).VERCEL_ENV ??
(process.env as Record<string, string | undefined>).NEXT_PUBLIC_VERCEL_ENV;
if (env === "production") return null;
}
const match = document.cookie.split("; ").find((row) => row.startsWith(`${name}=`));
return match ? match.split("=")[1] : null;
}
70 changes: 70 additions & 0 deletions lib/consent/eu-detection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { DEBUG_EU_COOKIE, getDebugCookie } from "./debug";

const EU_TIMEZONES = new Set([
// EU member states
"Europe/Vienna",
"Europe/Brussels",
"Europe/Sofia",
"Europe/Zagreb",
"Asia/Famagusta",
"Asia/Nicosia",
"Europe/Prague",
"Europe/Copenhagen",
"Europe/Tallinn",
"Europe/Helsinki",
"Europe/Paris",
"Europe/Berlin",
"Europe/Busingen",
"Europe/Athens",
"Europe/Budapest",
"Europe/Dublin",
"Europe/Rome",
"Europe/Riga",
"Europe/Vilnius",
"Europe/Luxembourg",
"Europe/Malta",
"Europe/Amsterdam",
"Europe/Warsaw",
"Europe/Lisbon",
"Atlantic/Azores",
"Atlantic/Madeira",
"Europe/Bucharest",
"Europe/Bratislava",
"Europe/Ljubljana",
"Europe/Madrid",
"Africa/Ceuta",
"Atlantic/Canary",
"Europe/Stockholm",
// EEA (non-EU)
"Europe/Oslo",
"Arctic/Longyearbyen",
"Atlantic/Reykjavik",
"Europe/Vaduz",
// UK (still applies GDPR-equivalent)
"Europe/London",
"Europe/Belfast",
"Europe/Guernsey",
"Europe/Isle_of_Man",
"Europe/Jersey",
]);

let cached: boolean | undefined;

export function isEUVisitor(): boolean {
if (typeof window === "undefined") return true;
if (cached !== undefined) return cached;

const debugOverride = getDebugCookie(DEBUG_EU_COOKIE);
if (debugOverride !== null) {
cached = debugOverride === "true";
return cached;
}

try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
cached = EU_TIMEZONES.has(tz);
} catch {
cached = true;
}
return cached;
}
19 changes: 19 additions & 0 deletions lib/consent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @authzed/consent core (read-only consumer)
*
* Copied from authzed/web src/consent/core/.
* Portable consent utilities with zero external dependencies.
*
* This is a read-only consumer: it reads the `az-consent` cookie set by
* the marketing site (authzed.com) but does not write it or show a banner.
* Consent decisions are made on authzed.com and shared via the cookie.
*/
export {
readConsentCookie,
parseConsentCookie,
consentedIdentify,
shouldOptOutCapturing,
CONSENT_COOKIE_NAME,
} from "./storage";
export { isEUVisitor } from "./eu-detection";
export type { ConsentPreferences } from "./types";
55 changes: 55 additions & 0 deletions lib/consent/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { ConsentPreferences } from "./types";

const COOKIE_NAME = "az-consent";
const COOKIE_VERSION = 1;

export function readConsentCookie(): ConsentPreferences | null {
if (typeof document === "undefined") return null;

const raw = getCookieValue(COOKIE_NAME);
if (!raw) return null;

try {
const parsed = JSON.parse(raw);
if (parsed.version === COOKIE_VERSION) return parsed as ConsentPreferences;
} catch {
// invalid cookie
}
return null;
}

export function parseConsentCookie(rawValue: string | undefined | null): ConsentPreferences | null {
if (!rawValue) return null;
try {
const parsed = JSON.parse(decodeURIComponent(rawValue));
if (parsed.version === COOKIE_VERSION) return parsed as ConsentPreferences;
} catch {
// invalid cookie
}
return null;
}

export const CONSENT_COOKIE_NAME = COOKIE_NAME;

export function consentedIdentify(
ph: { identify: (id: string, props?: Record<string, unknown>) => void },
distinctId: string,
properties?: Record<string, unknown>,
): void {
const consent = readConsentCookie();
if (consent?.statistics) {
ph.identify(distinctId, properties);
}
}

export function shouldOptOutCapturing(isEU: boolean): boolean {
const consent = readConsentCookie();
if (consent !== null) return !consent.statistics;
return isEU;
}

function getCookieValue(name: string): string | null {
if (typeof document === "undefined") return null;
const match = document.cookie.split("; ").find((row) => row.startsWith(`${name}=`));
return match ? decodeURIComponent(match.split("=").slice(1).join("=")) : null;
}
8 changes: 8 additions & 0 deletions lib/consent/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface ConsentPreferences {
version: 1;
necessary: true;
preferences: boolean;
statistics: boolean;
marketing: boolean;
updatedAt: string; // ISO-8601
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@radix-ui/react-slot": "^1.2.4",
"@segment/in-eu": "^0.4.0",
"@svgr/webpack": "^8.1.0",
"@vercel/speed-insights": "^1.0.12",
"class-variance-authority": "^0.7.1",
Expand Down
Loading
Loading