@easy-cookie/posthog 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +93 -2
- package/dist/index.cjs +90 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 easy-cookie contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @easy-cookie/posthog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Keeps [PostHog](https://posthog.com) in sync with easy-cookie consent, with no page reload. It follows the pattern MythicVoices uses in production:
|
|
4
|
+
|
|
5
|
+
- **Before a choice:** PostHog captures nothing.
|
|
6
|
+
- **After a rejection:** cookieless mode only. PostHog sets no cookies and no storage, and creates no person profiles. Autocapture, exceptions, web vitals and heatmaps are dropped.
|
|
7
|
+
- **After an acceptance:** normal capture with cookies, then `identify()` for logged-in users.
|
|
8
|
+
|
|
9
|
+
`posthog-js` is an optional peer dependency. The adapter types PostHog loosely, so any compatible client works.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- **posthog-js** with `cookieless_mode` support (≥ 1.200).
|
|
14
|
+
- **Cookieless server hash mode** enabled in the PostHog project settings. Without it, cookieless events are discarded.
|
|
15
|
+
- PostHog **must be initialised with `cookieless_mode: 'on_reject'`**. `posthogInitOptions()` does this for you.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pnpm add @easy-cookie/posthog posthog-js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import posthog from 'posthog-js';
|
|
27
|
+
import easyCookie from '@easy-cookie/core';
|
|
28
|
+
import { connectPostHog, posthogInitOptions } from '@easy-cookie/posthog';
|
|
29
|
+
|
|
30
|
+
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
|
|
31
|
+
...posthogInitOptions({ api: easyCookie }), // cookieless_mode: 'on_reject', persistence, person_profiles, before_send
|
|
32
|
+
api_host: 'https://eu.i.posthog.com',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const disconnect = connectPostHog(posthog, easyCookie, {
|
|
36
|
+
category: 'measurement', // default
|
|
37
|
+
identify: () => session?.user?.id, // optional, called after opt-in only
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
With React, connect it in an effect inside the provider:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
'use client';
|
|
45
|
+
import { useEffect } from 'react';
|
|
46
|
+
import posthog from 'posthog-js';
|
|
47
|
+
import { useEasyCookie } from '@easy-cookie/react';
|
|
48
|
+
import { connectPostHog } from '@easy-cookie/posthog';
|
|
49
|
+
|
|
50
|
+
export function PostHogConsent({ userId }: { userId?: string }) {
|
|
51
|
+
const { api } = useEasyCookie();
|
|
52
|
+
useEffect(() => (api ? connectPostHog(posthog, api, { identify: () => userId }) : undefined), [api, userId]);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
### `connectPostHog(posthog, api, { category?, service?, identify? })`
|
|
60
|
+
|
|
61
|
+
The adapter listens to `replay`, `consent` and `change`, and applies the current state immediately, in case the stored choice was replayed before you connected.
|
|
62
|
+
|
|
63
|
+
- **Granted:** it calls `posthog.opt_in_capturing()`, skipped when already opted in. It then calls `posthog.identify(id)` when `identify()` returns an id that differs from the current one.
|
|
64
|
+
- **Not granted (a choice exists):** it calls `posthog.opt_out_capturing()`. With `on_reject`, PostHog keeps counting anonymously without cookies.
|
|
65
|
+
- **No choice yet:** it does nothing.
|
|
66
|
+
|
|
67
|
+
It returns an unsubscribe function. Use `service: 'posthog'` to follow the service toggle instead of the category.
|
|
68
|
+
|
|
69
|
+
### `posthogInitOptions(opts?)`
|
|
70
|
+
|
|
71
|
+
Returns the recommended init options:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
{
|
|
75
|
+
cookieless_mode: 'on_reject',
|
|
76
|
+
persistence: 'localStorage+cookie', // used only after opt-in
|
|
77
|
+
person_profiles: 'identified_only',
|
|
78
|
+
before_send: consentBeforeSend(opts),
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `consentBeforeSend({ category?, api?, isGranted?, drop? })`
|
|
83
|
+
|
|
84
|
+
A `before_send` filter. Without consent it drops `$autocapture`, `$rageclick`, `$dead_click`, `$exception`, `$web_vitals` and `$$heatmap` (the list is `NON_ESSENTIAL_EVENTS`, and you can override it with `drop`). Pageviews and your own events are kept.
|
|
85
|
+
|
|
86
|
+
The consent check works like this:
|
|
87
|
+
|
|
88
|
+
1. `isGranted`, when you pass it.
|
|
89
|
+
2. Otherwise the runtime, when you pass `api`.
|
|
90
|
+
3. Otherwise the state published by the head snippet (`window.__easyCookieHead`). That state is available before the runtime loads.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
NON_ESSENTIAL_EVENTS: () => NON_ESSENTIAL_EVENTS,
|
|
24
|
+
connectPostHog: () => connectPostHog,
|
|
25
|
+
consentBeforeSend: () => consentBeforeSend,
|
|
26
|
+
posthogInitOptions: () => posthogInitOptions
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(src_exports);
|
|
29
|
+
function connectPostHog(posthog, api, options = {}) {
|
|
30
|
+
const category = options.category ?? "measurement";
|
|
31
|
+
const granted = () => options.service ? api.isServiceGranted(options.service) : api.isGranted(category);
|
|
32
|
+
let last = null;
|
|
33
|
+
const apply = () => {
|
|
34
|
+
if (!api.get()) return;
|
|
35
|
+
if (granted()) {
|
|
36
|
+
if (last !== "in") {
|
|
37
|
+
if (!posthog.has_opted_in_capturing?.()) posthog.opt_in_capturing();
|
|
38
|
+
last = "in";
|
|
39
|
+
}
|
|
40
|
+
const id = options.identify?.();
|
|
41
|
+
if (id && posthog.identify && posthog.get_distinct_id?.() !== id) posthog.identify(id);
|
|
42
|
+
} else if (last !== "out") {
|
|
43
|
+
if (!posthog.has_opted_out_capturing?.()) posthog.opt_out_capturing();
|
|
44
|
+
last = "out";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const events = ["replay", "consent", "change"];
|
|
48
|
+
const unsubs = events.map((ev) => api.on(ev, apply));
|
|
49
|
+
apply();
|
|
50
|
+
return () => unsubs.forEach((u) => u());
|
|
51
|
+
}
|
|
52
|
+
var NON_ESSENTIAL_EVENTS = [
|
|
53
|
+
"$autocapture",
|
|
54
|
+
"$rageclick",
|
|
55
|
+
"$dead_click",
|
|
56
|
+
"$exception",
|
|
57
|
+
"$web_vitals",
|
|
58
|
+
"$$heatmap",
|
|
59
|
+
"$heatmap"
|
|
60
|
+
];
|
|
61
|
+
function headGranted(category) {
|
|
62
|
+
const head = globalThis.__easyCookieHead;
|
|
63
|
+
return !!head?.valid && !!head.granted?.includes(category);
|
|
64
|
+
}
|
|
65
|
+
function consentBeforeSend(options = {}) {
|
|
66
|
+
const category = options.category ?? "measurement";
|
|
67
|
+
const drop = new Set(options.drop ?? NON_ESSENTIAL_EVENTS);
|
|
68
|
+
const isGranted = options.isGranted ?? (() => options.api ? options.api.isGranted(category) : headGranted(category));
|
|
69
|
+
return (event) => {
|
|
70
|
+
if (!event) return event;
|
|
71
|
+
if (drop.has(event.event) && !isGranted()) return null;
|
|
72
|
+
return event;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function posthogInitOptions(options = {}) {
|
|
76
|
+
return {
|
|
77
|
+
cookieless_mode: "on_reject",
|
|
78
|
+
persistence: "localStorage+cookie",
|
|
79
|
+
person_profiles: "identified_only",
|
|
80
|
+
before_send: consentBeforeSend(options)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
84
|
+
0 && (module.exports = {
|
|
85
|
+
NON_ESSENTIAL_EVENTS,
|
|
86
|
+
connectPostHog,
|
|
87
|
+
consentBeforeSend,
|
|
88
|
+
posthogInitOptions
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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?: unknown): void;\n opt_out_capturing(): void;\n has_opted_in_capturing?(): boolean;\n has_opted_out_capturing?(): boolean;\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 properties?: Record<string, unknown>;\n [key: string]: unknown;\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\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, 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 const apply = () => {\n // No valid choice yet: leave PostHog in its pending state (it captures nothing).\n if (!api.get()) return;\n if (granted()) {\n if (last !== 'in') {\n if (!posthog.has_opted_in_capturing?.()) posthog.opt_in_capturing();\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 (!posthog.has_opted_out_capturing?.()) posthog.opt_out_capturing();\n last = 'out';\n }\n };\n\n const events: EasyCookieEvent[] = ['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;AA6CO,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;AAEhC,QAAM,QAAQ,MAAM;AAElB,QAAI,CAAC,IAAI,IAAI,EAAG;AAChB,QAAI,QAAQ,GAAG;AACb,UAAI,SAAS,MAAM;AACjB,YAAI,CAAC,QAAQ,yBAAyB,EAAG,SAAQ,iBAAiB;AAClE,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,CAAC,QAAQ,0BAA0B,EAAG,SAAQ,kBAAkB;AACpE,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,SAA4B,CAAC,UAAU,WAAW,QAAQ;AAChE,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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { EasyCookieApi } from '@easy-cookie/core/shared';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The few posthog-js methods the adapter uses. Typed loosely on purpose, so
|
|
5
|
+
* `posthog-js` stays an optional peer dependency and any compatible client works.
|
|
6
|
+
*/
|
|
7
|
+
interface PostHogLike {
|
|
8
|
+
opt_in_capturing(options?: unknown): void;
|
|
9
|
+
opt_out_capturing(): void;
|
|
10
|
+
has_opted_in_capturing?(): boolean;
|
|
11
|
+
has_opted_out_capturing?(): boolean;
|
|
12
|
+
identify?(distinctId?: string, ...rest: unknown[]): void;
|
|
13
|
+
get_distinct_id?(): string;
|
|
14
|
+
}
|
|
15
|
+
/** Minimal shape of a PostHog event passed to `before_send`. */
|
|
16
|
+
interface PostHogEventLike {
|
|
17
|
+
event: string;
|
|
18
|
+
properties?: Record<string, unknown>;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
interface ConnectPostHogOptions {
|
|
22
|
+
/** Category that allows PostHog cookies/profiles. Default `measurement`. */
|
|
23
|
+
category?: string;
|
|
24
|
+
/** Service id: when set, the service's own consent decides instead of the category. */
|
|
25
|
+
service?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Return the logged-in user id (or undefined). Called after opt-in, so the
|
|
28
|
+
* identified person profile is created only with consent.
|
|
29
|
+
*/
|
|
30
|
+
identify?: () => string | undefined | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Keep PostHog's capture state in sync with the visitor's consent, without reloads.
|
|
34
|
+
*
|
|
35
|
+
* PostHog must be initialised by the host with `cookieless_mode: 'on_reject'`
|
|
36
|
+
* (see {@link posthogInitOptions}): before a choice it captures nothing, after a
|
|
37
|
+
* rejection it keeps counting anonymously with no cookies or storage, and after
|
|
38
|
+
* an acceptance it switches to normal cookie-based capture.
|
|
39
|
+
*
|
|
40
|
+
* On `replay`, `consent` and `change` the adapter calls `opt_in_capturing()` or
|
|
41
|
+
* `opt_out_capturing()`. Returns an unsubscribe function.
|
|
42
|
+
*/
|
|
43
|
+
declare function connectPostHog(posthog: PostHogLike, api: EasyCookieApi, options?: ConnectPostHogOptions): () => void;
|
|
44
|
+
/** Events dropped by {@link consentBeforeSend} while consent is missing. */
|
|
45
|
+
declare const NON_ESSENTIAL_EVENTS: readonly string[];
|
|
46
|
+
interface ConsentBeforeSendOptions {
|
|
47
|
+
/** Category checked by the default `isGranted`. Default `measurement`. */
|
|
48
|
+
category?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Consent check. Defaults to the runtime when `api` is given, otherwise to the
|
|
51
|
+
* state published by the head script (`window.__easyCookieHead.granted`).
|
|
52
|
+
*/
|
|
53
|
+
isGranted?: () => boolean;
|
|
54
|
+
api?: Pick<EasyCookieApi, 'isGranted'> | null;
|
|
55
|
+
/** Event names to drop without consent. Default {@link NON_ESSENTIAL_EVENTS}. */
|
|
56
|
+
drop?: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `before_send` filter: without consent, drop autocapture, exceptions, web vitals
|
|
60
|
+
* and heatmap events, so cookieless mode only sends the essentials (pageviews,
|
|
61
|
+
* explicit captures). Pass it as `before_send` or use {@link posthogInitOptions}.
|
|
62
|
+
*/
|
|
63
|
+
declare function consentBeforeSend(options?: ConsentBeforeSendOptions): <T extends PostHogEventLike | null>(event: T) => T | null;
|
|
64
|
+
interface PostHogInitOptions {
|
|
65
|
+
cookieless_mode: 'on_reject';
|
|
66
|
+
persistence: 'localStorage+cookie';
|
|
67
|
+
person_profiles: 'identified_only';
|
|
68
|
+
before_send: ReturnType<typeof consentBeforeSend>;
|
|
69
|
+
[key: string]: unknown;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Recommended `posthog.init()` options for easy-cookie:
|
|
73
|
+
* - `cookieless_mode: 'on_reject'` (requires "Cookieless server hash mode" enabled
|
|
74
|
+
* in the PostHog project settings);
|
|
75
|
+
* - `persistence: 'localStorage+cookie'` (only used after opt-in);
|
|
76
|
+
* - `person_profiles: 'identified_only'`;
|
|
77
|
+
* - a consent-aware `before_send` filter.
|
|
78
|
+
*
|
|
79
|
+
* Spread your own options after it: `posthog.init(key, { ...posthogInitOptions(), api_host })`.
|
|
80
|
+
*/
|
|
81
|
+
declare function posthogInitOptions(options?: ConsentBeforeSendOptions): PostHogInitOptions;
|
|
82
|
+
|
|
83
|
+
export { type ConnectPostHogOptions, type ConsentBeforeSendOptions, NON_ESSENTIAL_EVENTS, type PostHogEventLike, type PostHogInitOptions, type PostHogLike, connectPostHog, consentBeforeSend, posthogInitOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { EasyCookieApi } from '@easy-cookie/core/shared';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The few posthog-js methods the adapter uses. Typed loosely on purpose, so
|
|
5
|
+
* `posthog-js` stays an optional peer dependency and any compatible client works.
|
|
6
|
+
*/
|
|
7
|
+
interface PostHogLike {
|
|
8
|
+
opt_in_capturing(options?: unknown): void;
|
|
9
|
+
opt_out_capturing(): void;
|
|
10
|
+
has_opted_in_capturing?(): boolean;
|
|
11
|
+
has_opted_out_capturing?(): boolean;
|
|
12
|
+
identify?(distinctId?: string, ...rest: unknown[]): void;
|
|
13
|
+
get_distinct_id?(): string;
|
|
14
|
+
}
|
|
15
|
+
/** Minimal shape of a PostHog event passed to `before_send`. */
|
|
16
|
+
interface PostHogEventLike {
|
|
17
|
+
event: string;
|
|
18
|
+
properties?: Record<string, unknown>;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
interface ConnectPostHogOptions {
|
|
22
|
+
/** Category that allows PostHog cookies/profiles. Default `measurement`. */
|
|
23
|
+
category?: string;
|
|
24
|
+
/** Service id: when set, the service's own consent decides instead of the category. */
|
|
25
|
+
service?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Return the logged-in user id (or undefined). Called after opt-in, so the
|
|
28
|
+
* identified person profile is created only with consent.
|
|
29
|
+
*/
|
|
30
|
+
identify?: () => string | undefined | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Keep PostHog's capture state in sync with the visitor's consent, without reloads.
|
|
34
|
+
*
|
|
35
|
+
* PostHog must be initialised by the host with `cookieless_mode: 'on_reject'`
|
|
36
|
+
* (see {@link posthogInitOptions}): before a choice it captures nothing, after a
|
|
37
|
+
* rejection it keeps counting anonymously with no cookies or storage, and after
|
|
38
|
+
* an acceptance it switches to normal cookie-based capture.
|
|
39
|
+
*
|
|
40
|
+
* On `replay`, `consent` and `change` the adapter calls `opt_in_capturing()` or
|
|
41
|
+
* `opt_out_capturing()`. Returns an unsubscribe function.
|
|
42
|
+
*/
|
|
43
|
+
declare function connectPostHog(posthog: PostHogLike, api: EasyCookieApi, options?: ConnectPostHogOptions): () => void;
|
|
44
|
+
/** Events dropped by {@link consentBeforeSend} while consent is missing. */
|
|
45
|
+
declare const NON_ESSENTIAL_EVENTS: readonly string[];
|
|
46
|
+
interface ConsentBeforeSendOptions {
|
|
47
|
+
/** Category checked by the default `isGranted`. Default `measurement`. */
|
|
48
|
+
category?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Consent check. Defaults to the runtime when `api` is given, otherwise to the
|
|
51
|
+
* state published by the head script (`window.__easyCookieHead.granted`).
|
|
52
|
+
*/
|
|
53
|
+
isGranted?: () => boolean;
|
|
54
|
+
api?: Pick<EasyCookieApi, 'isGranted'> | null;
|
|
55
|
+
/** Event names to drop without consent. Default {@link NON_ESSENTIAL_EVENTS}. */
|
|
56
|
+
drop?: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `before_send` filter: without consent, drop autocapture, exceptions, web vitals
|
|
60
|
+
* and heatmap events, so cookieless mode only sends the essentials (pageviews,
|
|
61
|
+
* explicit captures). Pass it as `before_send` or use {@link posthogInitOptions}.
|
|
62
|
+
*/
|
|
63
|
+
declare function consentBeforeSend(options?: ConsentBeforeSendOptions): <T extends PostHogEventLike | null>(event: T) => T | null;
|
|
64
|
+
interface PostHogInitOptions {
|
|
65
|
+
cookieless_mode: 'on_reject';
|
|
66
|
+
persistence: 'localStorage+cookie';
|
|
67
|
+
person_profiles: 'identified_only';
|
|
68
|
+
before_send: ReturnType<typeof consentBeforeSend>;
|
|
69
|
+
[key: string]: unknown;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Recommended `posthog.init()` options for easy-cookie:
|
|
73
|
+
* - `cookieless_mode: 'on_reject'` (requires "Cookieless server hash mode" enabled
|
|
74
|
+
* in the PostHog project settings);
|
|
75
|
+
* - `persistence: 'localStorage+cookie'` (only used after opt-in);
|
|
76
|
+
* - `person_profiles: 'identified_only'`;
|
|
77
|
+
* - a consent-aware `before_send` filter.
|
|
78
|
+
*
|
|
79
|
+
* Spread your own options after it: `posthog.init(key, { ...posthogInitOptions(), api_host })`.
|
|
80
|
+
*/
|
|
81
|
+
declare function posthogInitOptions(options?: ConsentBeforeSendOptions): PostHogInitOptions;
|
|
82
|
+
|
|
83
|
+
export { type ConnectPostHogOptions, type ConsentBeforeSendOptions, NON_ESSENTIAL_EVENTS, type PostHogEventLike, type PostHogInitOptions, type PostHogLike, connectPostHog, consentBeforeSend, posthogInitOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function connectPostHog(posthog, api, options = {}) {
|
|
3
|
+
const category = options.category ?? "measurement";
|
|
4
|
+
const granted = () => options.service ? api.isServiceGranted(options.service) : api.isGranted(category);
|
|
5
|
+
let last = null;
|
|
6
|
+
const apply = () => {
|
|
7
|
+
if (!api.get()) return;
|
|
8
|
+
if (granted()) {
|
|
9
|
+
if (last !== "in") {
|
|
10
|
+
if (!posthog.has_opted_in_capturing?.()) posthog.opt_in_capturing();
|
|
11
|
+
last = "in";
|
|
12
|
+
}
|
|
13
|
+
const id = options.identify?.();
|
|
14
|
+
if (id && posthog.identify && posthog.get_distinct_id?.() !== id) posthog.identify(id);
|
|
15
|
+
} else if (last !== "out") {
|
|
16
|
+
if (!posthog.has_opted_out_capturing?.()) posthog.opt_out_capturing();
|
|
17
|
+
last = "out";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const events = ["replay", "consent", "change"];
|
|
21
|
+
const unsubs = events.map((ev) => api.on(ev, apply));
|
|
22
|
+
apply();
|
|
23
|
+
return () => unsubs.forEach((u) => u());
|
|
24
|
+
}
|
|
25
|
+
var NON_ESSENTIAL_EVENTS = [
|
|
26
|
+
"$autocapture",
|
|
27
|
+
"$rageclick",
|
|
28
|
+
"$dead_click",
|
|
29
|
+
"$exception",
|
|
30
|
+
"$web_vitals",
|
|
31
|
+
"$$heatmap",
|
|
32
|
+
"$heatmap"
|
|
33
|
+
];
|
|
34
|
+
function headGranted(category) {
|
|
35
|
+
const head = globalThis.__easyCookieHead;
|
|
36
|
+
return !!head?.valid && !!head.granted?.includes(category);
|
|
37
|
+
}
|
|
38
|
+
function consentBeforeSend(options = {}) {
|
|
39
|
+
const category = options.category ?? "measurement";
|
|
40
|
+
const drop = new Set(options.drop ?? NON_ESSENTIAL_EVENTS);
|
|
41
|
+
const isGranted = options.isGranted ?? (() => options.api ? options.api.isGranted(category) : headGranted(category));
|
|
42
|
+
return (event) => {
|
|
43
|
+
if (!event) return event;
|
|
44
|
+
if (drop.has(event.event) && !isGranted()) return null;
|
|
45
|
+
return event;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function posthogInitOptions(options = {}) {
|
|
49
|
+
return {
|
|
50
|
+
cookieless_mode: "on_reject",
|
|
51
|
+
persistence: "localStorage+cookie",
|
|
52
|
+
person_profiles: "identified_only",
|
|
53
|
+
before_send: consentBeforeSend(options)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
NON_ESSENTIAL_EVENTS,
|
|
58
|
+
connectPostHog,
|
|
59
|
+
consentBeforeSend,
|
|
60
|
+
posthogInitOptions
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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?: unknown): void;\n opt_out_capturing(): void;\n has_opted_in_capturing?(): boolean;\n has_opted_out_capturing?(): boolean;\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 properties?: Record<string, unknown>;\n [key: string]: unknown;\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\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, 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 const apply = () => {\n // No valid choice yet: leave PostHog in its pending state (it captures nothing).\n if (!api.get()) return;\n if (granted()) {\n if (last !== 'in') {\n if (!posthog.has_opted_in_capturing?.()) posthog.opt_in_capturing();\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 (!posthog.has_opted_out_capturing?.()) posthog.opt_out_capturing();\n last = 'out';\n }\n };\n\n const events: EasyCookieEvent[] = ['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":";AA6CO,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;AAEhC,QAAM,QAAQ,MAAM;AAElB,QAAI,CAAC,IAAI,IAAI,EAAG;AAChB,QAAI,QAAQ,GAAG;AACb,UAAI,SAAS,MAAM;AACjB,YAAI,CAAC,QAAQ,yBAAyB,EAAG,SAAQ,iBAAiB;AAClE,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,CAAC,QAAQ,0BAA0B,EAAG,SAAQ,kBAAkB;AACpE,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,SAA4B,CAAC,UAAU,WAAW,QAAQ;AAChE,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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easy-cookie/posthog",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PostHog adapter for easy-cookie: cookieless on_reject mode and opt-in/out synced with consent.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@easy-cookie/core": "^0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"posthog-js": ">=1.200.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"posthog-js": {
|
|
30
|
+
"optional": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://consent.aleloca.dev",
|
|
37
|
+
"keywords": [
|
|
38
|
+
"cookie",
|
|
39
|
+
"consent",
|
|
40
|
+
"gdpr",
|
|
41
|
+
"cookie-banner",
|
|
42
|
+
"consent-mode"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
|
+
}
|
|
6
49
|
}
|