@easy-cookie/posthog 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +34 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +34 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,33 @@ function connectPostHog(posthog, api, options = {}) {
|
|
|
30
30
|
const category = options.category ?? "measurement";
|
|
31
31
|
const granted = () => options.service ? api.isServiceGranted(options.service) : api.isGranted(category);
|
|
32
32
|
let last = null;
|
|
33
|
+
let remoteConfig;
|
|
34
|
+
if (options.sessionReplay && typeof posthog._onRemoteConfig === "function") {
|
|
35
|
+
const original = posthog._onRemoteConfig.bind(posthog);
|
|
36
|
+
posthog._onRemoteConfig = (config) => {
|
|
37
|
+
remoteConfig = config;
|
|
38
|
+
original(config);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const fetchRemoteConfig = async () => {
|
|
42
|
+
try {
|
|
43
|
+
const token = posthog.config?.token;
|
|
44
|
+
const url = token && posthog.requestRouter?.endpointFor?.("assets", `/array/${token}/config`);
|
|
45
|
+
if (!url) return void 0;
|
|
46
|
+
const res = await fetch(url);
|
|
47
|
+
return res.ok ? await res.json() : void 0;
|
|
48
|
+
} catch {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const restartReplay = async () => {
|
|
53
|
+
try {
|
|
54
|
+
const config = remoteConfig ?? await fetchRemoteConfig();
|
|
55
|
+
if (config && "sessionRecording" in config) posthog.sessionRecording?.onRemoteConfig?.(config);
|
|
56
|
+
posthog.startSessionRecording?.();
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
};
|
|
33
60
|
const status = () => {
|
|
34
61
|
const explicit = posthog.get_explicit_consent_status?.();
|
|
35
62
|
if (explicit === "granted") return "in";
|
|
@@ -44,7 +71,7 @@ function connectPostHog(posthog, api, options = {}) {
|
|
|
44
71
|
if (!api.get()) {
|
|
45
72
|
if (options.beforeChoice === "cookieless") {
|
|
46
73
|
if (last !== "out") {
|
|
47
|
-
if (status() !== "out") posthog.opt_out_capturing();
|
|
74
|
+
if (status() !== "out" || last === null) posthog.opt_out_capturing();
|
|
48
75
|
last = "out";
|
|
49
76
|
}
|
|
50
77
|
} else if (status() === "in") {
|
|
@@ -54,15 +81,19 @@ function connectPostHog(posthog, api, options = {}) {
|
|
|
54
81
|
}
|
|
55
82
|
if (granted()) {
|
|
56
83
|
if (last !== "in") {
|
|
84
|
+
const fromCookieless = status() === "out";
|
|
57
85
|
if (status() !== "in") posthog.opt_in_capturing({ captureEventName: false });
|
|
58
|
-
if (options.sessionReplay)
|
|
86
|
+
if (options.sessionReplay) {
|
|
87
|
+
if (fromCookieless) void restartReplay();
|
|
88
|
+
else posthog.startSessionRecording?.();
|
|
89
|
+
}
|
|
59
90
|
last = "in";
|
|
60
91
|
}
|
|
61
92
|
const id = options.identify?.();
|
|
62
93
|
if (id && posthog.identify && posthog.get_distinct_id?.() !== id) posthog.identify(id);
|
|
63
94
|
} else if (last !== "out") {
|
|
64
95
|
if (options.sessionReplay) posthog.stopSessionRecording?.();
|
|
65
|
-
if (status() !== "out") posthog.opt_out_capturing();
|
|
96
|
+
if (status() !== "out" || last === null) posthog.opt_out_capturing();
|
|
66
97
|
last = "out";
|
|
67
98
|
}
|
|
68
99
|
};
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { EasyCookieApi, EasyCookieEvent } from '@easy-cookie/core/shared';\n\n/**\n * The few posthog-js methods the adapter uses. Typed loosely on purpose, so\n * `posthog-js` stays an optional peer dependency and any compatible client works.\n */\nexport interface PostHogLike {\n opt_in_capturing(options?: { captureEventName?: string | null | false; [key: string]: unknown }): void;\n opt_out_capturing(): void;\n has_opted_in_capturing?(): boolean;\n has_opted_out_capturing?(): boolean;\n clear_opt_in_out_capturing?(): void;\n /** posthog-js ≥ 1.2xx: 'granted' | 'denied' | 'pending' (reliable with cookieless_mode). */\n get_explicit_consent_status?(): string;\n startSessionRecording?(...args: unknown[]): void;\n stopSessionRecording?(): void;\n identify?(distinctId?: string, ...rest: unknown[]): void;\n get_distinct_id?(): string;\n}\n\n/** Minimal shape of a PostHog event passed to `before_send`. */\nexport interface PostHogEventLike {\n event: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- matches posthog-js `Properties`\n properties?: Record<string, any>;\n}\n\nexport interface ConnectPostHogOptions {\n /** Category that allows PostHog cookies/profiles. Default `measurement`. */\n category?: string;\n /** Service id: when set, the service's own consent decides instead of the category. */\n service?: string;\n /**\n * Return the logged-in user id (or undefined). Called after opt-in, so the\n * identified person profile is created only with consent.\n */\n identify?: () => string | undefined | null;\n /**\n * Before the visitor's first choice:\n * - `'pending'` (default): capture nothing. A stale opt-in left in storage by an\n * earlier setup is cleared, so nothing is captured with cookies.\n * - `'cookieless'`: count visits without cookies (same as after a rejection).\n * Only if your privacy policy covers cookieless measurement without consent.\n */\n beforeChoice?: 'pending' | 'cookieless';\n /** Start session recording on opt-in and stop it on opt-out. Default false. */\n sessionReplay?: boolean;\n}\n\n/**\n * Keep PostHog's capture state in sync with the visitor's consent, without reloads.\n *\n * PostHog must be initialised by the host with `cookieless_mode: 'on_reject'`\n * (see {@link posthogInitOptions}): before a choice it captures nothing (or counts\n * cookieless with `beforeChoice: 'cookieless'`), after a\n * rejection it keeps counting anonymously with no cookies or storage, and after\n * an acceptance it switches to normal cookie-based capture.\n *\n * On `replay`, `consent` and `change` the adapter calls `opt_in_capturing()` or\n * `opt_out_capturing()`. Returns an unsubscribe function.\n */\nexport function connectPostHog(posthog: PostHogLike, api: EasyCookieApi, options: ConnectPostHogOptions = {}): () => void {\n const category = options.category ?? 'measurement';\n const granted = () => (options.service ? api.isServiceGranted(options.service) : api.isGranted(category));\n let last: 'in' | 'out' | null = null;\n\n // With cookieless_mode, has_opted_out_capturing() is already true while consent\n // is pending, so prefer the explicit consent status when the client has it.\n const status = (): 'in' | 'out' | 'pending' => {\n const explicit = posthog.get_explicit_consent_status?.();\n if (explicit === 'granted') return 'in';\n if (explicit === 'denied') return 'out';\n if (explicit === 'pending') return 'pending';\n if (posthog.has_opted_in_capturing?.()) return 'in';\n if (posthog.has_opted_out_capturing?.()) return 'out';\n return 'pending';\n };\n\n const apply = () => {\n // Until run() has read the stored choice, \"no choice\" is not known yet:\n // acting now could opt out (and reset) a returning visitor who accepted.\n if (!api.getConfig()) return;\n if (!api.get()) {\n if (options.beforeChoice === 'cookieless') {\n if (last !== 'out') {\n if (status() !== 'out') posthog.opt_out_capturing();\n last = 'out';\n }\n } else if (status() === 'in') {\n // Pending: an opt-in stored by a previous banner must not keep capturing.\n posthog.clear_opt_in_out_capturing?.();\n }\n return;\n }\n if (granted()) {\n if (last !== 'in') {\n // No `$opt_in` event: the consent itself is recorded by easy-cookie.\n if (status() !== 'in') posthog.opt_in_capturing({ captureEventName: false });\n if (options.sessionReplay) posthog.startSessionRecording?.();\n last = 'in';\n }\n const id = options.identify?.();\n if (id && posthog.identify && posthog.get_distinct_id?.() !== id) posthog.identify(id);\n } else if (last !== 'out') {\n if (options.sessionReplay) posthog.stopSessionRecording?.();\n if (status() !== 'out') posthog.opt_out_capturing();\n last = 'out';\n }\n };\n\n const events: EasyCookieEvent[] = ['ready', 'replay', 'consent', 'change'];\n const unsubs = events.map((ev) => api.on(ev, apply));\n // The stored choice may have been replayed before we subscribed.\n apply();\n return () => unsubs.forEach((u) => u());\n}\n\n/** Events dropped by {@link consentBeforeSend} while consent is missing. */\nexport const NON_ESSENTIAL_EVENTS: readonly string[] = [\n '$autocapture',\n '$rageclick',\n '$dead_click',\n '$exception',\n '$web_vitals',\n '$$heatmap',\n '$heatmap',\n];\n\nexport interface ConsentBeforeSendOptions {\n /** Category checked by the default `isGranted`. Default `measurement`. */\n category?: string;\n /**\n * Consent check. Defaults to the runtime when `api` is given, otherwise to the\n * state published by the head script (`window.__easyCookieHead.granted`).\n */\n isGranted?: () => boolean;\n api?: Pick<EasyCookieApi, 'isGranted'> | null;\n /** Event names to drop without consent. Default {@link NON_ESSENTIAL_EVENTS}. */\n drop?: readonly string[];\n}\n\nfunction headGranted(category: string): boolean {\n const head = (globalThis as { __easyCookieHead?: { granted?: string[]; valid?: boolean } }).__easyCookieHead;\n return !!head?.valid && !!head.granted?.includes(category);\n}\n\n/**\n * `before_send` filter: without consent, drop autocapture, exceptions, web vitals\n * and heatmap events, so cookieless mode only sends the essentials (pageviews,\n * explicit captures). Pass it as `before_send` or use {@link posthogInitOptions}.\n */\nexport function consentBeforeSend(options: ConsentBeforeSendOptions = {}) {\n const category = options.category ?? 'measurement';\n const drop = new Set(options.drop ?? NON_ESSENTIAL_EVENTS);\n const isGranted =\n options.isGranted ?? (() => (options.api ? options.api.isGranted(category) : headGranted(category)));\n return <T extends PostHogEventLike | null>(event: T): T | null => {\n if (!event) return event;\n if (drop.has(event.event) && !isGranted()) return null;\n return event;\n };\n}\n\nexport interface PostHogInitOptions {\n cookieless_mode: 'on_reject';\n persistence: 'localStorage+cookie';\n person_profiles: 'identified_only';\n before_send: ReturnType<typeof consentBeforeSend>;\n [key: string]: unknown;\n}\n\n/**\n * Recommended `posthog.init()` options for easy-cookie:\n * - `cookieless_mode: 'on_reject'` (requires \"Cookieless server hash mode\" enabled\n * in the PostHog project settings);\n * - `persistence: 'localStorage+cookie'` (only used after opt-in);\n * - `person_profiles: 'identified_only'`;\n * - a consent-aware `before_send` filter.\n *\n * Spread your own options after it: `posthog.init(key, { ...posthogInitOptions(), api_host })`.\n */\nexport function posthogInitOptions(options: ConsentBeforeSendOptions = {}): PostHogInitOptions {\n return {\n cookieless_mode: 'on_reject',\n persistence: 'localStorage+cookie',\n person_profiles: 'identified_only',\n before_send: consentBeforeSend(options),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DO,SAAS,eAAe,SAAsB,KAAoB,UAAiC,CAAC,GAAe;AACxH,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,UAAU,MAAO,QAAQ,UAAU,IAAI,iBAAiB,QAAQ,OAAO,IAAI,IAAI,UAAU,QAAQ;AACvG,MAAI,OAA4B;AAIhC,QAAM,SAAS,MAAgC;AAC7C,UAAM,WAAW,QAAQ,8BAA8B;AACvD,QAAI,aAAa,UAAW,QAAO;AACnC,QAAI,aAAa,SAAU,QAAO;AAClC,QAAI,aAAa,UAAW,QAAO;AACnC,QAAI,QAAQ,yBAAyB,EAAG,QAAO;AAC/C,QAAI,QAAQ,0BAA0B,EAAG,QAAO;AAChD,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,MAAM;AAGlB,QAAI,CAAC,IAAI,UAAU,EAAG;AACtB,QAAI,CAAC,IAAI,IAAI,GAAG;AACd,UAAI,QAAQ,iBAAiB,cAAc;AACzC,YAAI,SAAS,OAAO;AAClB,cAAI,OAAO,MAAM,MAAO,SAAQ,kBAAkB;AAClD,iBAAO;AAAA,QACT;AAAA,MACF,WAAW,OAAO,MAAM,MAAM;AAE5B,gBAAQ,6BAA6B;AAAA,MACvC;AACA;AAAA,IACF;AACA,QAAI,QAAQ,GAAG;AACb,UAAI,SAAS,MAAM;AAEjB,YAAI,OAAO,MAAM,KAAM,SAAQ,iBAAiB,EAAE,kBAAkB,MAAM,CAAC;AAC3E,YAAI,QAAQ,cAAe,SAAQ,wBAAwB;AAC3D,eAAO;AAAA,MACT;AACA,YAAM,KAAK,QAAQ,WAAW;AAC9B,UAAI,MAAM,QAAQ,YAAY,QAAQ,kBAAkB,MAAM,GAAI,SAAQ,SAAS,EAAE;AAAA,IACvF,WAAW,SAAS,OAAO;AACzB,UAAI,QAAQ,cAAe,SAAQ,uBAAuB;AAC1D,UAAI,OAAO,MAAM,MAAO,SAAQ,kBAAkB;AAClD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,SAA4B,CAAC,SAAS,UAAU,WAAW,QAAQ;AACzE,QAAM,SAAS,OAAO,IAAI,CAAC,OAAO,IAAI,GAAG,IAAI,KAAK,CAAC;AAEnD,QAAM;AACN,SAAO,MAAM,OAAO,QAAQ,CAAC,MAAM,EAAE,CAAC;AACxC;AAGO,IAAM,uBAA0C;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAeA,SAAS,YAAY,UAA2B;AAC9C,QAAM,OAAQ,WAA8E;AAC5F,SAAO,CAAC,CAAC,MAAM,SAAS,CAAC,CAAC,KAAK,SAAS,SAAS,QAAQ;AAC3D;AAOO,SAAS,kBAAkB,UAAoC,CAAC,GAAG;AACxE,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,OAAO,IAAI,IAAI,QAAQ,QAAQ,oBAAoB;AACzD,QAAM,YACJ,QAAQ,cAAc,MAAO,QAAQ,MAAM,QAAQ,IAAI,UAAU,QAAQ,IAAI,YAAY,QAAQ;AACnG,SAAO,CAAoC,UAAuB;AAChE,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,KAAK,IAAI,MAAM,KAAK,KAAK,CAAC,UAAU,EAAG,QAAO;AAClD,WAAO;AAAA,EACT;AACF;AAoBO,SAAS,mBAAmB,UAAoC,CAAC,GAAuB;AAC7F,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,aAAa,kBAAkB,OAAO;AAAA,EACxC;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { EasyCookieApi, EasyCookieEvent } from '@easy-cookie/core/shared';\n\n/**\n * The few posthog-js methods the adapter uses. Typed loosely on purpose, so\n * `posthog-js` stays an optional peer dependency and any compatible client works.\n */\nexport interface PostHogLike {\n opt_in_capturing(options?: { captureEventName?: string | null | false; [key: string]: unknown }): void;\n opt_out_capturing(): void;\n has_opted_in_capturing?(): boolean;\n has_opted_out_capturing?(): boolean;\n clear_opt_in_out_capturing?(): void;\n /** posthog-js ≥ 1.2xx: 'granted' | 'denied' | 'pending' (reliable with cookieless_mode). */\n get_explicit_consent_status?(): string;\n startSessionRecording?(...args: unknown[]): void;\n stopSessionRecording?(): void;\n // posthog-js internals, used only to restart session replay after a cookieless → opt-in switch.\n _onRemoteConfig?(config: Record<string, unknown>): void;\n sessionRecording?: { onRemoteConfig?(config: Record<string, unknown>): void } | null;\n config?: { token?: string };\n requestRouter?: { endpointFor?(target: string, path: string): string };\n identify?(distinctId?: string, ...rest: unknown[]): void;\n get_distinct_id?(): string;\n}\n\n/** Minimal shape of a PostHog event passed to `before_send`. */\nexport interface PostHogEventLike {\n event: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- matches posthog-js `Properties`\n properties?: Record<string, any>;\n}\n\nexport interface ConnectPostHogOptions {\n /** Category that allows PostHog cookies/profiles. Default `measurement`. */\n category?: string;\n /** Service id: when set, the service's own consent decides instead of the category. */\n service?: string;\n /**\n * Return the logged-in user id (or undefined). Called after opt-in, so the\n * identified person profile is created only with consent.\n */\n identify?: () => string | undefined | null;\n /**\n * Before the visitor's first choice:\n * - `'pending'` (default): capture nothing. A stale opt-in left in storage by an\n * earlier setup is cleared, so nothing is captured with cookies.\n * - `'cookieless'`: count visits without cookies (same as after a rejection).\n * Only if your privacy policy covers cookieless measurement without consent.\n */\n beforeChoice?: 'pending' | 'cookieless';\n /** Start session recording on opt-in and stop it on opt-out. Default false. */\n sessionReplay?: boolean;\n}\n\n/**\n * Keep PostHog's capture state in sync with the visitor's consent, without reloads.\n *\n * PostHog must be initialised by the host with `cookieless_mode: 'on_reject'`\n * (see {@link posthogInitOptions}): before a choice it captures nothing (or counts\n * cookieless with `beforeChoice: 'cookieless'`), after a\n * rejection it keeps counting anonymously with no cookies or storage, and after\n * an acceptance it switches to normal cookie-based capture.\n *\n * On `replay`, `consent` and `change` the adapter calls `opt_in_capturing()` or\n * `opt_out_capturing()`. Returns an unsubscribe function.\n */\nexport function connectPostHog(posthog: PostHogLike, api: EasyCookieApi, options: ConnectPostHogOptions = {}): () => void {\n const category = options.category ?? 'measurement';\n const granted = () => (options.service ? api.isServiceGranted(options.service) : api.isGranted(category));\n let last: 'in' | 'out' | null = null;\n\n // Session replay after a cookieless → opt-in switch. posthog-js resets the instance\n // and creates a new recorder that waits for the remote config, which it only\n // fetches once per page load (and which the cookieless instance never stored).\n // Keep the config posthog-js receives, or fetch it again, and hand it to the new\n // recorder. Every call is guarded: with a different posthog-js the replay simply\n // starts on the next page load, as before.\n let remoteConfig: Record<string, unknown> | undefined;\n if (options.sessionReplay && typeof posthog._onRemoteConfig === 'function') {\n const original = posthog._onRemoteConfig.bind(posthog);\n posthog._onRemoteConfig = (config) => {\n remoteConfig = config;\n original(config);\n };\n }\n const fetchRemoteConfig = async (): Promise<Record<string, unknown> | undefined> => {\n try {\n const token = posthog.config?.token;\n const url = token && posthog.requestRouter?.endpointFor?.('assets', `/array/${token}/config`);\n if (!url) return undefined;\n const res = await fetch(url);\n return res.ok ? ((await res.json()) as Record<string, unknown>) : undefined;\n } catch {\n return undefined;\n }\n };\n const restartReplay = async () => {\n try {\n const config = remoteConfig ?? (await fetchRemoteConfig());\n if (config && 'sessionRecording' in config) posthog.sessionRecording?.onRemoteConfig?.(config);\n posthog.startSessionRecording?.();\n } catch {\n /* replay starts on the next page load */\n }\n };\n\n // With cookieless_mode, has_opted_out_capturing() is already true while consent\n // is pending, so prefer the explicit consent status when the client has it.\n const status = (): 'in' | 'out' | 'pending' => {\n const explicit = posthog.get_explicit_consent_status?.();\n if (explicit === 'granted') return 'in';\n if (explicit === 'denied') return 'out';\n if (explicit === 'pending') return 'pending';\n if (posthog.has_opted_in_capturing?.()) return 'in';\n if (posthog.has_opted_out_capturing?.()) return 'out';\n return 'pending';\n };\n\n const apply = () => {\n // Until run() has read the stored choice, \"no choice\" is not known yet:\n // acting now could opt out (and reset) a returning visitor who accepted.\n if (!api.getConfig()) return;\n if (!api.get()) {\n if (options.beforeChoice === 'cookieless') {\n if (last !== 'out') {\n // Also when already out: on a full page load posthog-js sends the first\n // cookieless $pageview only from opt_out_capturing() (no reset when not opted in).\n if (status() !== 'out' || last === null) posthog.opt_out_capturing();\n last = 'out';\n }\n } else if (status() === 'in') {\n // Pending: an opt-in stored by a previous banner must not keep capturing.\n posthog.clear_opt_in_out_capturing?.();\n }\n return;\n }\n if (granted()) {\n if (last !== 'in') {\n // No `$opt_in` event: the consent itself is recorded by easy-cookie.\n const fromCookieless = status() === 'out';\n if (status() !== 'in') posthog.opt_in_capturing({ captureEventName: false });\n if (options.sessionReplay) {\n if (fromCookieless) void restartReplay();\n else posthog.startSessionRecording?.();\n }\n last = 'in';\n }\n const id = options.identify?.();\n if (id && posthog.identify && posthog.get_distinct_id?.() !== id) posthog.identify(id);\n } else if (last !== 'out') {\n if (options.sessionReplay) posthog.stopSessionRecording?.();\n if (status() !== 'out' || last === null) posthog.opt_out_capturing();\n last = 'out';\n }\n };\n\n const events: EasyCookieEvent[] = ['ready', 'replay', 'consent', 'change'];\n const unsubs = events.map((ev) => api.on(ev, apply));\n // The stored choice may have been replayed before we subscribed.\n apply();\n return () => unsubs.forEach((u) => u());\n}\n\n/** Events dropped by {@link consentBeforeSend} while consent is missing. */\nexport const NON_ESSENTIAL_EVENTS: readonly string[] = [\n '$autocapture',\n '$rageclick',\n '$dead_click',\n '$exception',\n '$web_vitals',\n '$$heatmap',\n '$heatmap',\n];\n\nexport interface ConsentBeforeSendOptions {\n /** Category checked by the default `isGranted`. Default `measurement`. */\n category?: string;\n /**\n * Consent check. Defaults to the runtime when `api` is given, otherwise to the\n * state published by the head script (`window.__easyCookieHead.granted`).\n */\n isGranted?: () => boolean;\n api?: Pick<EasyCookieApi, 'isGranted'> | null;\n /** Event names to drop without consent. Default {@link NON_ESSENTIAL_EVENTS}. */\n drop?: readonly string[];\n}\n\nfunction headGranted(category: string): boolean {\n const head = (globalThis as { __easyCookieHead?: { granted?: string[]; valid?: boolean } }).__easyCookieHead;\n return !!head?.valid && !!head.granted?.includes(category);\n}\n\n/**\n * `before_send` filter: without consent, drop autocapture, exceptions, web vitals\n * and heatmap events, so cookieless mode only sends the essentials (pageviews,\n * explicit captures). Pass it as `before_send` or use {@link posthogInitOptions}.\n */\nexport function consentBeforeSend(options: ConsentBeforeSendOptions = {}) {\n const category = options.category ?? 'measurement';\n const drop = new Set(options.drop ?? NON_ESSENTIAL_EVENTS);\n const isGranted =\n options.isGranted ?? (() => (options.api ? options.api.isGranted(category) : headGranted(category)));\n return <T extends PostHogEventLike | null>(event: T): T | null => {\n if (!event) return event;\n if (drop.has(event.event) && !isGranted()) return null;\n return event;\n };\n}\n\nexport interface PostHogInitOptions {\n cookieless_mode: 'on_reject';\n persistence: 'localStorage+cookie';\n person_profiles: 'identified_only';\n before_send: ReturnType<typeof consentBeforeSend>;\n [key: string]: unknown;\n}\n\n/**\n * Recommended `posthog.init()` options for easy-cookie:\n * - `cookieless_mode: 'on_reject'` (requires \"Cookieless server hash mode\" enabled\n * in the PostHog project settings);\n * - `persistence: 'localStorage+cookie'` (only used after opt-in);\n * - `person_profiles: 'identified_only'`;\n * - a consent-aware `before_send` filter.\n *\n * Spread your own options after it: `posthog.init(key, { ...posthogInitOptions(), api_host })`.\n */\nexport function posthogInitOptions(options: ConsentBeforeSendOptions = {}): PostHogInitOptions {\n return {\n cookieless_mode: 'on_reject',\n persistence: 'localStorage+cookie',\n person_profiles: 'identified_only',\n before_send: consentBeforeSend(options),\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -17,6 +17,16 @@ interface PostHogLike {
|
|
|
17
17
|
get_explicit_consent_status?(): string;
|
|
18
18
|
startSessionRecording?(...args: unknown[]): void;
|
|
19
19
|
stopSessionRecording?(): void;
|
|
20
|
+
_onRemoteConfig?(config: Record<string, unknown>): void;
|
|
21
|
+
sessionRecording?: {
|
|
22
|
+
onRemoteConfig?(config: Record<string, unknown>): void;
|
|
23
|
+
} | null;
|
|
24
|
+
config?: {
|
|
25
|
+
token?: string;
|
|
26
|
+
};
|
|
27
|
+
requestRouter?: {
|
|
28
|
+
endpointFor?(target: string, path: string): string;
|
|
29
|
+
};
|
|
20
30
|
identify?(distinctId?: string, ...rest: unknown[]): void;
|
|
21
31
|
get_distinct_id?(): string;
|
|
22
32
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,16 @@ interface PostHogLike {
|
|
|
17
17
|
get_explicit_consent_status?(): string;
|
|
18
18
|
startSessionRecording?(...args: unknown[]): void;
|
|
19
19
|
stopSessionRecording?(): void;
|
|
20
|
+
_onRemoteConfig?(config: Record<string, unknown>): void;
|
|
21
|
+
sessionRecording?: {
|
|
22
|
+
onRemoteConfig?(config: Record<string, unknown>): void;
|
|
23
|
+
} | null;
|
|
24
|
+
config?: {
|
|
25
|
+
token?: string;
|
|
26
|
+
};
|
|
27
|
+
requestRouter?: {
|
|
28
|
+
endpointFor?(target: string, path: string): string;
|
|
29
|
+
};
|
|
20
30
|
identify?(distinctId?: string, ...rest: unknown[]): void;
|
|
21
31
|
get_distinct_id?(): string;
|
|
22
32
|
}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,33 @@ function connectPostHog(posthog, api, options = {}) {
|
|
|
3
3
|
const category = options.category ?? "measurement";
|
|
4
4
|
const granted = () => options.service ? api.isServiceGranted(options.service) : api.isGranted(category);
|
|
5
5
|
let last = null;
|
|
6
|
+
let remoteConfig;
|
|
7
|
+
if (options.sessionReplay && typeof posthog._onRemoteConfig === "function") {
|
|
8
|
+
const original = posthog._onRemoteConfig.bind(posthog);
|
|
9
|
+
posthog._onRemoteConfig = (config) => {
|
|
10
|
+
remoteConfig = config;
|
|
11
|
+
original(config);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const fetchRemoteConfig = async () => {
|
|
15
|
+
try {
|
|
16
|
+
const token = posthog.config?.token;
|
|
17
|
+
const url = token && posthog.requestRouter?.endpointFor?.("assets", `/array/${token}/config`);
|
|
18
|
+
if (!url) return void 0;
|
|
19
|
+
const res = await fetch(url);
|
|
20
|
+
return res.ok ? await res.json() : void 0;
|
|
21
|
+
} catch {
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const restartReplay = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const config = remoteConfig ?? await fetchRemoteConfig();
|
|
28
|
+
if (config && "sessionRecording" in config) posthog.sessionRecording?.onRemoteConfig?.(config);
|
|
29
|
+
posthog.startSessionRecording?.();
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
};
|
|
6
33
|
const status = () => {
|
|
7
34
|
const explicit = posthog.get_explicit_consent_status?.();
|
|
8
35
|
if (explicit === "granted") return "in";
|
|
@@ -17,7 +44,7 @@ function connectPostHog(posthog, api, options = {}) {
|
|
|
17
44
|
if (!api.get()) {
|
|
18
45
|
if (options.beforeChoice === "cookieless") {
|
|
19
46
|
if (last !== "out") {
|
|
20
|
-
if (status() !== "out") posthog.opt_out_capturing();
|
|
47
|
+
if (status() !== "out" || last === null) posthog.opt_out_capturing();
|
|
21
48
|
last = "out";
|
|
22
49
|
}
|
|
23
50
|
} else if (status() === "in") {
|
|
@@ -27,15 +54,19 @@ function connectPostHog(posthog, api, options = {}) {
|
|
|
27
54
|
}
|
|
28
55
|
if (granted()) {
|
|
29
56
|
if (last !== "in") {
|
|
57
|
+
const fromCookieless = status() === "out";
|
|
30
58
|
if (status() !== "in") posthog.opt_in_capturing({ captureEventName: false });
|
|
31
|
-
if (options.sessionReplay)
|
|
59
|
+
if (options.sessionReplay) {
|
|
60
|
+
if (fromCookieless) void restartReplay();
|
|
61
|
+
else posthog.startSessionRecording?.();
|
|
62
|
+
}
|
|
32
63
|
last = "in";
|
|
33
64
|
}
|
|
34
65
|
const id = options.identify?.();
|
|
35
66
|
if (id && posthog.identify && posthog.get_distinct_id?.() !== id) posthog.identify(id);
|
|
36
67
|
} else if (last !== "out") {
|
|
37
68
|
if (options.sessionReplay) posthog.stopSessionRecording?.();
|
|
38
|
-
if (status() !== "out") posthog.opt_out_capturing();
|
|
69
|
+
if (status() !== "out" || last === null) posthog.opt_out_capturing();
|
|
39
70
|
last = "out";
|
|
40
71
|
}
|
|
41
72
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { EasyCookieApi, EasyCookieEvent } from '@easy-cookie/core/shared';\n\n/**\n * The few posthog-js methods the adapter uses. Typed loosely on purpose, so\n * `posthog-js` stays an optional peer dependency and any compatible client works.\n */\nexport interface PostHogLike {\n opt_in_capturing(options?: { captureEventName?: string | null | false; [key: string]: unknown }): void;\n opt_out_capturing(): void;\n has_opted_in_capturing?(): boolean;\n has_opted_out_capturing?(): boolean;\n clear_opt_in_out_capturing?(): void;\n /** posthog-js ≥ 1.2xx: 'granted' | 'denied' | 'pending' (reliable with cookieless_mode). */\n get_explicit_consent_status?(): string;\n startSessionRecording?(...args: unknown[]): void;\n stopSessionRecording?(): void;\n identify?(distinctId?: string, ...rest: unknown[]): void;\n get_distinct_id?(): string;\n}\n\n/** Minimal shape of a PostHog event passed to `before_send`. */\nexport interface PostHogEventLike {\n event: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- matches posthog-js `Properties`\n properties?: Record<string, any>;\n}\n\nexport interface ConnectPostHogOptions {\n /** Category that allows PostHog cookies/profiles. Default `measurement`. */\n category?: string;\n /** Service id: when set, the service's own consent decides instead of the category. */\n service?: string;\n /**\n * Return the logged-in user id (or undefined). Called after opt-in, so the\n * identified person profile is created only with consent.\n */\n identify?: () => string | undefined | null;\n /**\n * Before the visitor's first choice:\n * - `'pending'` (default): capture nothing. A stale opt-in left in storage by an\n * earlier setup is cleared, so nothing is captured with cookies.\n * - `'cookieless'`: count visits without cookies (same as after a rejection).\n * Only if your privacy policy covers cookieless measurement without consent.\n */\n beforeChoice?: 'pending' | 'cookieless';\n /** Start session recording on opt-in and stop it on opt-out. Default false. */\n sessionReplay?: boolean;\n}\n\n/**\n * Keep PostHog's capture state in sync with the visitor's consent, without reloads.\n *\n * PostHog must be initialised by the host with `cookieless_mode: 'on_reject'`\n * (see {@link posthogInitOptions}): before a choice it captures nothing (or counts\n * cookieless with `beforeChoice: 'cookieless'`), after a\n * rejection it keeps counting anonymously with no cookies or storage, and after\n * an acceptance it switches to normal cookie-based capture.\n *\n * On `replay`, `consent` and `change` the adapter calls `opt_in_capturing()` or\n * `opt_out_capturing()`. Returns an unsubscribe function.\n */\nexport function connectPostHog(posthog: PostHogLike, api: EasyCookieApi, options: ConnectPostHogOptions = {}): () => void {\n const category = options.category ?? 'measurement';\n const granted = () => (options.service ? api.isServiceGranted(options.service) : api.isGranted(category));\n let last: 'in' | 'out' | null = null;\n\n // With cookieless_mode, has_opted_out_capturing() is already true while consent\n // is pending, so prefer the explicit consent status when the client has it.\n const status = (): 'in' | 'out' | 'pending' => {\n const explicit = posthog.get_explicit_consent_status?.();\n if (explicit === 'granted') return 'in';\n if (explicit === 'denied') return 'out';\n if (explicit === 'pending') return 'pending';\n if (posthog.has_opted_in_capturing?.()) return 'in';\n if (posthog.has_opted_out_capturing?.()) return 'out';\n return 'pending';\n };\n\n const apply = () => {\n // Until run() has read the stored choice, \"no choice\" is not known yet:\n // acting now could opt out (and reset) a returning visitor who accepted.\n if (!api.getConfig()) return;\n if (!api.get()) {\n if (options.beforeChoice === 'cookieless') {\n if (last !== 'out') {\n if (status() !== 'out') posthog.opt_out_capturing();\n last = 'out';\n }\n } else if (status() === 'in') {\n // Pending: an opt-in stored by a previous banner must not keep capturing.\n posthog.clear_opt_in_out_capturing?.();\n }\n return;\n }\n if (granted()) {\n if (last !== 'in') {\n // No `$opt_in` event: the consent itself is recorded by easy-cookie.\n if (status() !== 'in') posthog.opt_in_capturing({ captureEventName: false });\n if (options.sessionReplay) posthog.startSessionRecording?.();\n last = 'in';\n }\n const id = options.identify?.();\n if (id && posthog.identify && posthog.get_distinct_id?.() !== id) posthog.identify(id);\n } else if (last !== 'out') {\n if (options.sessionReplay) posthog.stopSessionRecording?.();\n if (status() !== 'out') posthog.opt_out_capturing();\n last = 'out';\n }\n };\n\n const events: EasyCookieEvent[] = ['ready', 'replay', 'consent', 'change'];\n const unsubs = events.map((ev) => api.on(ev, apply));\n // The stored choice may have been replayed before we subscribed.\n apply();\n return () => unsubs.forEach((u) => u());\n}\n\n/** Events dropped by {@link consentBeforeSend} while consent is missing. */\nexport const NON_ESSENTIAL_EVENTS: readonly string[] = [\n '$autocapture',\n '$rageclick',\n '$dead_click',\n '$exception',\n '$web_vitals',\n '$$heatmap',\n '$heatmap',\n];\n\nexport interface ConsentBeforeSendOptions {\n /** Category checked by the default `isGranted`. Default `measurement`. */\n category?: string;\n /**\n * Consent check. Defaults to the runtime when `api` is given, otherwise to the\n * state published by the head script (`window.__easyCookieHead.granted`).\n */\n isGranted?: () => boolean;\n api?: Pick<EasyCookieApi, 'isGranted'> | null;\n /** Event names to drop without consent. Default {@link NON_ESSENTIAL_EVENTS}. */\n drop?: readonly string[];\n}\n\nfunction headGranted(category: string): boolean {\n const head = (globalThis as { __easyCookieHead?: { granted?: string[]; valid?: boolean } }).__easyCookieHead;\n return !!head?.valid && !!head.granted?.includes(category);\n}\n\n/**\n * `before_send` filter: without consent, drop autocapture, exceptions, web vitals\n * and heatmap events, so cookieless mode only sends the essentials (pageviews,\n * explicit captures). Pass it as `before_send` or use {@link posthogInitOptions}.\n */\nexport function consentBeforeSend(options: ConsentBeforeSendOptions = {}) {\n const category = options.category ?? 'measurement';\n const drop = new Set(options.drop ?? NON_ESSENTIAL_EVENTS);\n const isGranted =\n options.isGranted ?? (() => (options.api ? options.api.isGranted(category) : headGranted(category)));\n return <T extends PostHogEventLike | null>(event: T): T | null => {\n if (!event) return event;\n if (drop.has(event.event) && !isGranted()) return null;\n return event;\n };\n}\n\nexport interface PostHogInitOptions {\n cookieless_mode: 'on_reject';\n persistence: 'localStorage+cookie';\n person_profiles: 'identified_only';\n before_send: ReturnType<typeof consentBeforeSend>;\n [key: string]: unknown;\n}\n\n/**\n * Recommended `posthog.init()` options for easy-cookie:\n * - `cookieless_mode: 'on_reject'` (requires \"Cookieless server hash mode\" enabled\n * in the PostHog project settings);\n * - `persistence: 'localStorage+cookie'` (only used after opt-in);\n * - `person_profiles: 'identified_only'`;\n * - a consent-aware `before_send` filter.\n *\n * Spread your own options after it: `posthog.init(key, { ...posthogInitOptions(), api_host })`.\n */\nexport function posthogInitOptions(options: ConsentBeforeSendOptions = {}): PostHogInitOptions {\n return {\n cookieless_mode: 'on_reject',\n persistence: 'localStorage+cookie',\n person_profiles: 'identified_only',\n before_send: consentBeforeSend(options),\n };\n}\n"],"mappings":";AA6DO,SAAS,eAAe,SAAsB,KAAoB,UAAiC,CAAC,GAAe;AACxH,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,UAAU,MAAO,QAAQ,UAAU,IAAI,iBAAiB,QAAQ,OAAO,IAAI,IAAI,UAAU,QAAQ;AACvG,MAAI,OAA4B;AAIhC,QAAM,SAAS,MAAgC;AAC7C,UAAM,WAAW,QAAQ,8BAA8B;AACvD,QAAI,aAAa,UAAW,QAAO;AACnC,QAAI,aAAa,SAAU,QAAO;AAClC,QAAI,aAAa,UAAW,QAAO;AACnC,QAAI,QAAQ,yBAAyB,EAAG,QAAO;AAC/C,QAAI,QAAQ,0BAA0B,EAAG,QAAO;AAChD,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,MAAM;AAGlB,QAAI,CAAC,IAAI,UAAU,EAAG;AACtB,QAAI,CAAC,IAAI,IAAI,GAAG;AACd,UAAI,QAAQ,iBAAiB,cAAc;AACzC,YAAI,SAAS,OAAO;AAClB,cAAI,OAAO,MAAM,MAAO,SAAQ,kBAAkB;AAClD,iBAAO;AAAA,QACT;AAAA,MACF,WAAW,OAAO,MAAM,MAAM;AAE5B,gBAAQ,6BAA6B;AAAA,MACvC;AACA;AAAA,IACF;AACA,QAAI,QAAQ,GAAG;AACb,UAAI,SAAS,MAAM;AAEjB,YAAI,OAAO,MAAM,KAAM,SAAQ,iBAAiB,EAAE,kBAAkB,MAAM,CAAC;AAC3E,YAAI,QAAQ,cAAe,SAAQ,wBAAwB;AAC3D,eAAO;AAAA,MACT;AACA,YAAM,KAAK,QAAQ,WAAW;AAC9B,UAAI,MAAM,QAAQ,YAAY,QAAQ,kBAAkB,MAAM,GAAI,SAAQ,SAAS,EAAE;AAAA,IACvF,WAAW,SAAS,OAAO;AACzB,UAAI,QAAQ,cAAe,SAAQ,uBAAuB;AAC1D,UAAI,OAAO,MAAM,MAAO,SAAQ,kBAAkB;AAClD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,SAA4B,CAAC,SAAS,UAAU,WAAW,QAAQ;AACzE,QAAM,SAAS,OAAO,IAAI,CAAC,OAAO,IAAI,GAAG,IAAI,KAAK,CAAC;AAEnD,QAAM;AACN,SAAO,MAAM,OAAO,QAAQ,CAAC,MAAM,EAAE,CAAC;AACxC;AAGO,IAAM,uBAA0C;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAeA,SAAS,YAAY,UAA2B;AAC9C,QAAM,OAAQ,WAA8E;AAC5F,SAAO,CAAC,CAAC,MAAM,SAAS,CAAC,CAAC,KAAK,SAAS,SAAS,QAAQ;AAC3D;AAOO,SAAS,kBAAkB,UAAoC,CAAC,GAAG;AACxE,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,OAAO,IAAI,IAAI,QAAQ,QAAQ,oBAAoB;AACzD,QAAM,YACJ,QAAQ,cAAc,MAAO,QAAQ,MAAM,QAAQ,IAAI,UAAU,QAAQ,IAAI,YAAY,QAAQ;AACnG,SAAO,CAAoC,UAAuB;AAChE,QAAI,CAAC,MAAO,QAAO;AACnB,QAAI,KAAK,IAAI,MAAM,KAAK,KAAK,CAAC,UAAU,EAAG,QAAO;AAClD,WAAO;AAAA,EACT;AACF;AAoBO,SAAS,mBAAmB,UAAoC,CAAC,GAAuB;AAC7F,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,aAAa,kBAAkB,OAAO;AAAA,EACxC;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { EasyCookieApi, EasyCookieEvent } from '@easy-cookie/core/shared';\n\n/**\n * The few posthog-js methods the adapter uses. Typed loosely on purpose, so\n * `posthog-js` stays an optional peer dependency and any compatible client works.\n */\nexport interface PostHogLike {\n opt_in_capturing(options?: { captureEventName?: string | null | false; [key: string]: unknown }): void;\n opt_out_capturing(): void;\n has_opted_in_capturing?(): boolean;\n has_opted_out_capturing?(): boolean;\n clear_opt_in_out_capturing?(): void;\n /** posthog-js ≥ 1.2xx: 'granted' | 'denied' | 'pending' (reliable with cookieless_mode). */\n get_explicit_consent_status?(): string;\n startSessionRecording?(...args: unknown[]): void;\n stopSessionRecording?(): void;\n // posthog-js internals, used only to restart session replay after a cookieless → opt-in switch.\n _onRemoteConfig?(config: Record<string, unknown>): void;\n sessionRecording?: { onRemoteConfig?(config: Record<string, unknown>): void } | null;\n config?: { token?: string };\n requestRouter?: { endpointFor?(target: string, path: string): string };\n identify?(distinctId?: string, ...rest: unknown[]): void;\n get_distinct_id?(): string;\n}\n\n/** Minimal shape of a PostHog event passed to `before_send`. */\nexport interface PostHogEventLike {\n event: string;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- matches posthog-js `Properties`\n properties?: Record<string, any>;\n}\n\nexport interface ConnectPostHogOptions {\n /** Category that allows PostHog cookies/profiles. Default `measurement`. */\n category?: string;\n /** Service id: when set, the service's own consent decides instead of the category. */\n service?: string;\n /**\n * Return the logged-in user id (or undefined). Called after opt-in, so the\n * identified person profile is created only with consent.\n */\n identify?: () => string | undefined | null;\n /**\n * Before the visitor's first choice:\n * - `'pending'` (default): capture nothing. A stale opt-in left in storage by an\n * earlier setup is cleared, so nothing is captured with cookies.\n * - `'cookieless'`: count visits without cookies (same as after a rejection).\n * Only if your privacy policy covers cookieless measurement without consent.\n */\n beforeChoice?: 'pending' | 'cookieless';\n /** Start session recording on opt-in and stop it on opt-out. Default false. */\n sessionReplay?: boolean;\n}\n\n/**\n * Keep PostHog's capture state in sync with the visitor's consent, without reloads.\n *\n * PostHog must be initialised by the host with `cookieless_mode: 'on_reject'`\n * (see {@link posthogInitOptions}): before a choice it captures nothing (or counts\n * cookieless with `beforeChoice: 'cookieless'`), after a\n * rejection it keeps counting anonymously with no cookies or storage, and after\n * an acceptance it switches to normal cookie-based capture.\n *\n * On `replay`, `consent` and `change` the adapter calls `opt_in_capturing()` or\n * `opt_out_capturing()`. Returns an unsubscribe function.\n */\nexport function connectPostHog(posthog: PostHogLike, api: EasyCookieApi, options: ConnectPostHogOptions = {}): () => void {\n const category = options.category ?? 'measurement';\n const granted = () => (options.service ? api.isServiceGranted(options.service) : api.isGranted(category));\n let last: 'in' | 'out' | null = null;\n\n // Session replay after a cookieless → opt-in switch. posthog-js resets the instance\n // and creates a new recorder that waits for the remote config, which it only\n // fetches once per page load (and which the cookieless instance never stored).\n // Keep the config posthog-js receives, or fetch it again, and hand it to the new\n // recorder. Every call is guarded: with a different posthog-js the replay simply\n // starts on the next page load, as before.\n let remoteConfig: Record<string, unknown> | undefined;\n if (options.sessionReplay && typeof posthog._onRemoteConfig === 'function') {\n const original = posthog._onRemoteConfig.bind(posthog);\n posthog._onRemoteConfig = (config) => {\n remoteConfig = config;\n original(config);\n };\n }\n const fetchRemoteConfig = async (): Promise<Record<string, unknown> | undefined> => {\n try {\n const token = posthog.config?.token;\n const url = token && posthog.requestRouter?.endpointFor?.('assets', `/array/${token}/config`);\n if (!url) return undefined;\n const res = await fetch(url);\n return res.ok ? ((await res.json()) as Record<string, unknown>) : undefined;\n } catch {\n return undefined;\n }\n };\n const restartReplay = async () => {\n try {\n const config = remoteConfig ?? (await fetchRemoteConfig());\n if (config && 'sessionRecording' in config) posthog.sessionRecording?.onRemoteConfig?.(config);\n posthog.startSessionRecording?.();\n } catch {\n /* replay starts on the next page load */\n }\n };\n\n // With cookieless_mode, has_opted_out_capturing() is already true while consent\n // is pending, so prefer the explicit consent status when the client has it.\n const status = (): 'in' | 'out' | 'pending' => {\n const explicit = posthog.get_explicit_consent_status?.();\n if (explicit === 'granted') return 'in';\n if (explicit === 'denied') return 'out';\n if (explicit === 'pending') return 'pending';\n if (posthog.has_opted_in_capturing?.()) return 'in';\n if (posthog.has_opted_out_capturing?.()) return 'out';\n return 'pending';\n };\n\n const apply = () => {\n // Until run() has read the stored choice, \"no choice\" is not known yet:\n // acting now could opt out (and reset) a returning visitor who accepted.\n if (!api.getConfig()) return;\n if (!api.get()) {\n if (options.beforeChoice === 'cookieless') {\n if (last !== 'out') {\n // Also when already out: on a full page load posthog-js sends the first\n // cookieless $pageview only from opt_out_capturing() (no reset when not opted in).\n if (status() !== 'out' || last === null) posthog.opt_out_capturing();\n last = 'out';\n }\n } else if (status() === 'in') {\n // Pending: an opt-in stored by a previous banner must not keep capturing.\n posthog.clear_opt_in_out_capturing?.();\n }\n return;\n }\n if (granted()) {\n if (last !== 'in') {\n // No `$opt_in` event: the consent itself is recorded by easy-cookie.\n const fromCookieless = status() === 'out';\n if (status() !== 'in') posthog.opt_in_capturing({ captureEventName: false });\n if (options.sessionReplay) {\n if (fromCookieless) void restartReplay();\n else posthog.startSessionRecording?.();\n }\n last = 'in';\n }\n const id = options.identify?.();\n if (id && posthog.identify && posthog.get_distinct_id?.() !== id) posthog.identify(id);\n } else if (last !== 'out') {\n if (options.sessionReplay) posthog.stopSessionRecording?.();\n if (status() !== 'out' || last === null) posthog.opt_out_capturing();\n last = 'out';\n }\n };\n\n const events: EasyCookieEvent[] = ['ready', 'replay', 'consent', 'change'];\n const unsubs = events.map((ev) => api.on(ev, apply));\n // The stored choice may have been replayed before we subscribed.\n apply();\n return () => unsubs.forEach((u) => u());\n}\n\n/** Events dropped by {@link consentBeforeSend} while consent is missing. */\nexport const NON_ESSENTIAL_EVENTS: readonly string[] = [\n '$autocapture',\n '$rageclick',\n '$dead_click',\n '$exception',\n '$web_vitals',\n '$$heatmap',\n '$heatmap',\n];\n\nexport interface ConsentBeforeSendOptions {\n /** Category checked by the default `isGranted`. Default `measurement`. */\n category?: string;\n /**\n * Consent check. Defaults to the runtime when `api` is given, otherwise to the\n * state published by the head script (`window.__easyCookieHead.granted`).\n */\n isGranted?: () => boolean;\n api?: Pick<EasyCookieApi, 'isGranted'> | null;\n /** Event names to drop without consent. Default {@link NON_ESSENTIAL_EVENTS}. */\n drop?: readonly string[];\n}\n\nfunction headGranted(category: string): boolean {\n const head = (globalThis as { __easyCookieHead?: { granted?: string[]; valid?: boolean } }).__easyCookieHead;\n return !!head?.valid && !!head.granted?.includes(category);\n}\n\n/**\n * `before_send` filter: without consent, drop autocapture, exceptions, web vitals\n * and heatmap events, so cookieless mode only sends the essentials (pageviews,\n * explicit captures). Pass it as `before_send` or use {@link posthogInitOptions}.\n */\nexport function consentBeforeSend(options: ConsentBeforeSendOptions = {}) {\n const category = options.category ?? 'measurement';\n const drop = new Set(options.drop ?? NON_ESSENTIAL_EVENTS);\n const isGranted =\n options.isGranted ?? (() => (options.api ? options.api.isGranted(category) : headGranted(category)));\n return <T extends PostHogEventLike | null>(event: T): T | null => {\n if (!event) return event;\n if (drop.has(event.event) && !isGranted()) return null;\n return event;\n };\n}\n\nexport interface PostHogInitOptions {\n cookieless_mode: 'on_reject';\n persistence: 'localStorage+cookie';\n person_profiles: 'identified_only';\n before_send: ReturnType<typeof consentBeforeSend>;\n [key: string]: unknown;\n}\n\n/**\n * Recommended `posthog.init()` options for easy-cookie:\n * - `cookieless_mode: 'on_reject'` (requires \"Cookieless server hash mode\" enabled\n * in the PostHog project settings);\n * - `persistence: 'localStorage+cookie'` (only used after opt-in);\n * - `person_profiles: 'identified_only'`;\n * - a consent-aware `before_send` filter.\n *\n * Spread your own options after it: `posthog.init(key, { ...posthogInitOptions(), api_host })`.\n */\nexport function posthogInitOptions(options: ConsentBeforeSendOptions = {}): PostHogInitOptions {\n return {\n cookieless_mode: 'on_reject',\n persistence: 'localStorage+cookie',\n person_profiles: 'identified_only',\n before_send: consentBeforeSend(options),\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easy-cookie/posthog",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "PostHog adapter for easy-cookie: cookieless on_reject mode and opt-in/out synced with consent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@easy-cookie/core": "^0.1.
|
|
31
|
+
"@easy-cookie/core": "^0.1.4"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"posthog-js": ">=1.200.0"
|