posthog-zero 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/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # posthog-zero
2
+
3
+ Unofficial minimal PostHog **event capture** for edge and embedded runtimes.
4
+
5
+ Built for:
6
+
7
+ - **Cloudflare Workers** - `fetch`, use with `ctx.waitUntil()`
8
+ - **Browser / Tauri webview** - `sendBeacon` when available, else `fetch`
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm i posthog-zero
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { posthogZero } from 'posthog-zero'
20
+
21
+ const posthog = posthogZero({
22
+ apiKey: process.env.POSTHOG_KEY!,
23
+ host: 'https://us.i.posthog.com',
24
+ defaults: { $lib: 'posthog-zero-example' },
25
+ })
26
+
27
+ await posthog.capture({
28
+ event: 'button_clicked',
29
+ distinctId: anonId,
30
+ properties: { button: 'signup' },
31
+ })
32
+ ```
33
+
34
+ ### Cloudflare Workers
35
+
36
+ ```ts
37
+ import { posthogZero } from 'posthog-zero'
38
+ import { getRequestProperties } from 'posthog-zero/request'
39
+
40
+ const posthog = posthogZero({
41
+ apiKey: env.POSTHOG_KEY,
42
+ host: env.POSTHOG_HOST,
43
+ bots: 'drop', // or 'flag'
44
+ })
45
+
46
+ ctx.waitUntil(
47
+ posthog.capture({
48
+ event: '$pageview',
49
+ distinctId: anonId,
50
+ properties: getRequestProperties(request),
51
+ })
52
+ )
53
+ ```
54
+
55
+ Custom transport (tests, logging, or your own fetch/beacon):
56
+
57
+ ```ts
58
+ posthogZero({
59
+ apiKey: process.env.POSTHOG_KEY!,
60
+ transport: (url, body) =>
61
+ fetch(url, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ body: JSON.stringify(body),
65
+ keepalive: true,
66
+ }),
67
+ })
68
+ ```
69
+
70
+ ## Options
71
+
72
+ | Option | Description |
73
+ | ----------- | ------------------------------------------------------------------------------ |
74
+ | `apiKey` | Project API key (`phc_...`) |
75
+ | `host` | Ingest host (default US) |
76
+ | `defaults` | Properties merged on every event |
77
+ | `disabled` | No network, `capture` still resolves |
78
+ | `debug` | Log payloads |
79
+ | `transport` | `'auto'` \| `'beacon'` \| `'fetch'`, or `(url, body) => void \| Promise<void>` |
80
+ | `bots` | `'drop'` \| `'flag'` |
81
+
82
+ `transport` as a function receives the wire `CaptureBody` (POST JSON to `url` yourself). Built-in `'auto'` uses `sendBeacon` when available, else `fetch`.
83
+
84
+ With `bots: 'flag'`, only bot traffic gets `is_bot: true` and `bot_name` (the matched blocklist substring). Human events are unchanged. Detection uses the same UA list as `@posthog/core` (browser SDK bot blocking).
package/dist/body.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { CaptureBody, CaptureInput, EventProperties } from './types.js';
2
+ export declare function normalizeHost(host?: string): string;
3
+ export declare function captureUrl(host: string): string;
4
+ export declare function toTimestamp(value?: string | Date): string;
5
+ export declare function buildCaptureBody(apiKey: string, defaults: EventProperties | undefined, input: CaptureInput): CaptureBody;
6
+ /** For debug logs, omit `api_key` */
7
+ export declare function sanitizeCaptureBody(body: CaptureBody): Omit<CaptureBody, 'api_key'>;
8
+ //# sourceMappingURL=body.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../src/body.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAI5E,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAIzD;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,eAAe,GAAG,SAAS,EACrC,KAAK,EAAE,YAAY,GAClB,WAAW,CAWb;AAED,qCAAqC;AACrC,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,WAAW,GAChB,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAG9B"}
package/dist/body.js ADDED
@@ -0,0 +1,32 @@
1
+ const DEFAULT_HOST = 'https://us.i.posthog.com';
2
+ export function normalizeHost(host) {
3
+ return (host ?? DEFAULT_HOST).replace(/\/$/, '');
4
+ }
5
+ export function captureUrl(host) {
6
+ return `${normalizeHost(host)}/i/v0/e/`;
7
+ }
8
+ export function toTimestamp(value) {
9
+ if (value === undefined)
10
+ return new Date().toISOString();
11
+ if (value instanceof Date)
12
+ return value.toISOString();
13
+ return value;
14
+ }
15
+ export function buildCaptureBody(apiKey, defaults, input) {
16
+ return {
17
+ api_key: apiKey,
18
+ event: input.event,
19
+ distinct_id: input.distinctId,
20
+ properties: {
21
+ ...defaults,
22
+ ...input.properties,
23
+ },
24
+ timestamp: toTimestamp(input.timestamp),
25
+ };
26
+ }
27
+ /** For debug logs, omit `api_key` */
28
+ export function sanitizeCaptureBody(body) {
29
+ const { api_key: _key, ...rest } = body;
30
+ return rest;
31
+ }
32
+ //# sourceMappingURL=body.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.js","sourceRoot":"","sources":["../src/body.ts"],"names":[],"mappings":"AAEA,MAAM,YAAY,GAAG,0BAA0B,CAAA;AAE/C,MAAM,UAAU,aAAa,CAAC,IAAa;IACzC,OAAO,CAAC,IAAI,IAAI,YAAY,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AAClD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,UAAU,CAAA;AACzC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IACxD,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,KAAK,CAAC,WAAW,EAAE,CAAA;IACrD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,MAAc,EACd,QAAqC,EACrC,KAAmB;IAEnB,OAAO;QACL,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,WAAW,EAAE,KAAK,CAAC,UAAU;QAC7B,UAAU,EAAE;YACV,GAAG,QAAQ;YACX,GAAG,KAAK,CAAC,UAAU;SACpB;QACD,SAAS,EAAE,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC;KACxC,CAAA;AACH,CAAC;AAED,qCAAqC;AACrC,MAAM,UAAU,mBAAmB,CACjC,IAAiB;IAEjB,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IACvC,OAAO,IAAI,CAAA;AACb,CAAC"}
package/dist/bots.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { BotsMode, CaptureBody, EventProperties } from './types.js';
2
+ /** Matched blocklist substring, if any. */
3
+ export declare function getBotName(ua: string | undefined): string | undefined;
4
+ export declare function isBotFromProperties(properties: EventProperties | undefined): boolean;
5
+ /** Adds `is_bot` and `bot_name` when UA matches PostHog's blocklist. */
6
+ export declare function enrichBotFlags(properties: EventProperties): EventProperties;
7
+ /** Returns `null` when `bots: 'drop'` and properties indicate a bot. */
8
+ export declare function applyBotsMode(body: CaptureBody, bots: BotsMode | undefined): CaptureBody | null;
9
+ //# sourceMappingURL=bots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bots.d.ts","sourceRoot":"","sources":["../src/bots.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAExE,2CAA2C;AAC3C,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASrE;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,eAAe,GAAG,SAAS,GACtC,OAAO,CAMT;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,UAAU,EAAE,eAAe,GAAG,eAAe,CAM3E;AAED,wEAAwE;AACxE,wBAAgB,aAAa,CAC3B,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,QAAQ,GAAG,SAAS,GACzB,WAAW,GAAG,IAAI,CAQpB"}
package/dist/bots.js ADDED
@@ -0,0 +1,43 @@
1
+ import { DEFAULT_BLOCKED_UA_STRS, isBlockedUA } from '@posthog/core/utils';
2
+ /** Matched blocklist substring, if any. */
3
+ export function getBotName(ua) {
4
+ if (!ua)
5
+ return undefined;
6
+ const uaLower = ua.toLowerCase();
7
+ for (const blocked of DEFAULT_BLOCKED_UA_STRS) {
8
+ if (uaLower.indexOf(blocked.toLowerCase()) !== -1) {
9
+ return blocked;
10
+ }
11
+ }
12
+ return undefined;
13
+ }
14
+ export function isBotFromProperties(properties) {
15
+ if (!properties)
16
+ return false;
17
+ if (properties.is_bot === true)
18
+ return true;
19
+ const ua = properties.$raw_user_agent;
20
+ if (typeof ua === 'string')
21
+ return isBlockedUA(ua);
22
+ return false;
23
+ }
24
+ /** Adds `is_bot` and `bot_name` when UA matches PostHog's blocklist. */
25
+ export function enrichBotFlags(properties) {
26
+ const ua = properties.$raw_user_agent;
27
+ if (typeof ua !== 'string')
28
+ return properties;
29
+ const botName = getBotName(ua);
30
+ if (!botName)
31
+ return properties;
32
+ return { ...properties, is_bot: true, bot_name: botName };
33
+ }
34
+ /** Returns `null` when `bots: 'drop'` and properties indicate a bot. */
35
+ export function applyBotsMode(body, bots) {
36
+ if (!bots)
37
+ return body;
38
+ if (bots === 'drop') {
39
+ return isBotFromProperties(body.properties) ? null : body;
40
+ }
41
+ return { ...body, properties: enrichBotFlags(body.properties) };
42
+ }
43
+ //# sourceMappingURL=bots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bots.js","sourceRoot":"","sources":["../src/bots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAI1E,2CAA2C;AAC3C,MAAM,UAAU,UAAU,CAAC,EAAsB;IAC/C,IAAI,CAAC,EAAE;QAAE,OAAO,SAAS,CAAA;IACzB,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,EAAE,CAAA;IAChC,KAAK,MAAM,OAAO,IAAI,uBAAuB,EAAE,CAAC;QAC9C,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAClD,OAAO,OAAO,CAAA;QAChB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,UAAuC;IAEvC,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAA;IAC7B,IAAI,UAAU,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC3C,MAAM,EAAE,GAAG,UAAU,CAAC,eAAe,CAAA;IACrC,IAAI,OAAO,EAAE,KAAK,QAAQ;QAAE,OAAO,WAAW,CAAC,EAAE,CAAC,CAAA;IAClD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,cAAc,CAAC,UAA2B;IACxD,MAAM,EAAE,GAAG,UAAU,CAAC,eAAe,CAAA;IACrC,IAAI,OAAO,EAAE,KAAK,QAAQ;QAAE,OAAO,UAAU,CAAA;IAC7C,MAAM,OAAO,GAAG,UAAU,CAAC,EAAE,CAAC,CAAA;IAC9B,IAAI,CAAC,OAAO;QAAE,OAAO,UAAU,CAAA;IAC/B,OAAO,EAAE,GAAG,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAA;AAC3D,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,aAAa,CAC3B,IAAiB,EACjB,IAA0B;IAE1B,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IAEtB,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,mBAAmB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;IAC3D,CAAC;IAED,OAAO,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAA;AACjE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { PosthogZeroClient, PosthogZeroOptions } from './types.js';
2
+ export declare function posthogZero(options: PosthogZeroOptions): PosthogZeroClient;
3
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,YAAY,CAAA;AAEnB,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,iBAAiB,CA+B1E"}
package/dist/client.js ADDED
@@ -0,0 +1,28 @@
1
+ import { buildCaptureBody, captureUrl, sanitizeCaptureBody } from './body.js';
2
+ import { applyBotsMode } from './bots.js';
3
+ import { sendCapture } from './transport.js';
4
+ export function posthogZero(options) {
5
+ const host = options.host;
6
+ const url = captureUrl(host ?? 'https://us.i.posthog.com');
7
+ const defaults = options.defaults;
8
+ const disabled = options.disabled ?? false;
9
+ const debug = options.debug ?? false;
10
+ const transport = options.transport ?? 'auto';
11
+ const bots = options.bots;
12
+ return {
13
+ capture(input) {
14
+ const body = applyBotsMode(buildCaptureBody(options.apiKey, defaults, input), bots);
15
+ if (!body) {
16
+ return Promise.resolve();
17
+ }
18
+ if (debug) {
19
+ console.info('[posthog-zero]', sanitizeCaptureBody(body));
20
+ }
21
+ if (disabled) {
22
+ return Promise.resolve();
23
+ }
24
+ return sendCapture(url, body, transport);
25
+ },
26
+ };
27
+ }
28
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAO5C,MAAM,UAAU,WAAW,CAAC,OAA2B;IACrD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAA;IACzB,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,IAAI,0BAA0B,CAAC,CAAA;IAC1D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAA;IACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,KAAK,CAAA;IAC1C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAA;IACpC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAA;IAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAA;IAEzB,OAAO;QACL,OAAO,CAAC,KAAmB;YACzB,MAAM,IAAI,GAAG,aAAa,CACxB,gBAAgB,CAAC,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,EACjD,IAAI,CACL,CAAA;YAED,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;YAC1B,CAAC;YAED,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAA;YAC3D,CAAC;YAED,IAAI,QAAQ,EAAE,CAAC;gBACb,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;YAC1B,CAAC;YAED,OAAO,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAA;QAC1C,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { posthogZero } from './client.js';
2
+ export type { BotsMode, CaptureBody, CaptureInput, CaptureTransport, EventProperties, PosthogZeroClient, PosthogZeroOptions, Transport, TransportPreset, } from './types.js';
3
+ export { getBotName, isBotFromProperties } from './bots.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,YAAY,EACV,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,SAAS,EACT,eAAe,GAChB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { posthogZero } from './client.js';
2
+ export { getBotName, isBotFromProperties } from './bots.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAYzC,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1,8 @@
1
+ import type { EventProperties } from './types.js';
2
+ /**
3
+ * PostHog-style properties from a generic `Request` (Workers, Astro middleware, etc.):
4
+ * request URL, referrer, user-agent, browser/OS/device (`$raw_user_agent` for `bots` option).
5
+ * Merge any extra properties in your `capture` call.
6
+ */
7
+ export declare function getRequestProperties(request: Request): EventProperties;
8
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA+BjD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,CAoCtE"}
@@ -0,0 +1,74 @@
1
+ import { detectBrowser, detectBrowserVersion, detectDevice, detectDeviceType, detectOS, } from '@posthog/core/utils';
2
+ function getSearchEngine(referrer) {
3
+ if (!referrer)
4
+ return undefined;
5
+ try {
6
+ const host = new URL(referrer).hostname;
7
+ if (host === 'google.com' || host.endsWith('.google.com'))
8
+ return 'google';
9
+ if (host === 'bing.com' || host.endsWith('.bing.com'))
10
+ return 'bing';
11
+ if (host === 'yahoo.com' || host.endsWith('.yahoo.com'))
12
+ return 'yahoo';
13
+ if (host === 'duckduckgo.com' || host.endsWith('.duckduckgo.com'))
14
+ return 'duckduckgo';
15
+ }
16
+ catch {
17
+ // ignore malformed URLs
18
+ }
19
+ return undefined;
20
+ }
21
+ function getUrlProperties(request) {
22
+ try {
23
+ const parsedUrl = new URL(request.url);
24
+ return {
25
+ $current_url: parsedUrl.href,
26
+ $host: parsedUrl.hostname,
27
+ $pathname: parsedUrl.pathname,
28
+ $search: parsedUrl.search || undefined,
29
+ };
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ }
35
+ /**
36
+ * PostHog-style properties from a generic `Request` (Workers, Astro middleware, etc.):
37
+ * request URL, referrer, user-agent, browser/OS/device (`$raw_user_agent` for `bots` option).
38
+ * Merge any extra properties in your `capture` call.
39
+ */
40
+ export function getRequestProperties(request) {
41
+ const referer = request.headers.get('referer') ?? undefined;
42
+ let referringDomain;
43
+ try {
44
+ referringDomain = referer
45
+ ? new URL(referer).hostname || undefined
46
+ : undefined;
47
+ }
48
+ catch {
49
+ referringDomain = undefined;
50
+ }
51
+ const uaString = request.headers.get('user-agent') ?? undefined;
52
+ const [osName, osVersion] = uaString ? detectOS(uaString) : ['', ''];
53
+ const browserName = uaString ? detectBrowser(uaString, '') : '';
54
+ const browserVersion = uaString ? detectBrowserVersion(uaString, '') : null;
55
+ const browserLanguage = request.headers.get('accept-language')?.split(',')[0]?.trim() || undefined;
56
+ return {
57
+ ...getUrlProperties(request),
58
+ $referrer: referer,
59
+ $referring_domain: referringDomain,
60
+ $raw_user_agent: uaString,
61
+ $browser_language: browserLanguage,
62
+ $browser_language_prefix: browserLanguage?.split('-')[0],
63
+ $search_engine: getSearchEngine(referer),
64
+ $os: osName || undefined,
65
+ $os_version: osVersion || undefined,
66
+ $browser: browserName || undefined,
67
+ $browser_version: browserVersion ?? undefined,
68
+ $device: uaString ? detectDevice(uaString) || undefined : undefined,
69
+ $device_type: uaString
70
+ ? detectDeviceType(uaString) || undefined
71
+ : undefined,
72
+ };
73
+ }
74
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { CaptureBody, Transport } from './types.js';
2
+ export declare function sendCapture(url: string, body: CaptureBody, transport: Transport): Promise<void>;
3
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAEX,SAAS,EAEV,MAAM,YAAY,CAAA;AA4CnB,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -0,0 +1,43 @@
1
+ function isCaptureTransport(transport) {
2
+ return typeof transport === 'function';
3
+ }
4
+ function beaconAvailable() {
5
+ return (typeof navigator !== 'undefined' &&
6
+ typeof navigator.sendBeacon === 'function' &&
7
+ typeof Blob !== 'undefined');
8
+ }
9
+ function useBeaconPreset(preset) {
10
+ return beaconAvailable() && (preset === 'beacon' || preset === 'auto');
11
+ }
12
+ function resolveFetch() {
13
+ if (typeof globalThis.fetch === 'function')
14
+ return globalThis.fetch;
15
+ throw new Error('posthog-zero: fetch is not available in this runtime');
16
+ }
17
+ function sendViaBeacon(url, body) {
18
+ const json = JSON.stringify(body);
19
+ const blob = new Blob([json], { type: 'application/json' });
20
+ navigator.sendBeacon(url, blob);
21
+ return Promise.resolve();
22
+ }
23
+ function sendViaFetch(url, body) {
24
+ const json = JSON.stringify(body);
25
+ return resolveFetch()(url, {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: json,
29
+ keepalive: true,
30
+ })
31
+ .then(() => undefined)
32
+ .catch(() => undefined);
33
+ }
34
+ export function sendCapture(url, body, transport) {
35
+ if (isCaptureTransport(transport)) {
36
+ return Promise.resolve(transport(url, body)).catch(() => undefined);
37
+ }
38
+ if (useBeaconPreset(transport)) {
39
+ return sendViaBeacon(url, body);
40
+ }
41
+ return sendViaFetch(url, body);
42
+ }
43
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAOA,SAAS,kBAAkB,CACzB,SAAoB;IAEpB,OAAO,OAAO,SAAS,KAAK,UAAU,CAAA;AACxC,CAAC;AAED,SAAS,eAAe;IACtB,OAAO,CACL,OAAO,SAAS,KAAK,WAAW;QAChC,OAAO,SAAS,CAAC,UAAU,KAAK,UAAU;QAC1C,OAAO,IAAI,KAAK,WAAW,CAC5B,CAAA;AACH,CAAC;AAED,SAAS,eAAe,CAAC,MAAuB;IAC9C,OAAO,eAAe,EAAE,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,MAAM,CAAC,CAAA;AACxE,CAAC;AAED,SAAS,YAAY;IACnB,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,UAAU;QAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IACnE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAA;AACzE,CAAC;AAED,SAAS,aAAa,CAAC,GAAW,EAAE,IAAiB;IACnD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IACjC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,CAAA;IAC3D,SAAS,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IAC/B,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;AAC1B,CAAC;AAED,SAAS,YAAY,CAAC,GAAW,EAAE,IAAiB;IAClD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;IACjC,OAAO,YAAY,EAAE,CAAC,GAAG,EAAE;QACzB,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI;QACV,SAAS,EAAE,IAAI;KAChB,CAAC;SACC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;SACrB,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;AAC3B,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,GAAW,EACX,IAAiB,EACjB,SAAoB;IAEpB,IAAI,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC;QAClC,OAAO,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IACrE,CAAC;IAED,IAAI,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/B,OAAO,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACjC,CAAC;IAED,OAAO,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;AAChC,CAAC"}
@@ -0,0 +1,46 @@
1
+ export type EventProperties = Record<string, string | number | boolean | undefined>;
2
+ export type CaptureInput = {
3
+ event: string;
4
+ distinctId: string;
5
+ properties?: EventProperties;
6
+ timestamp?: string | Date;
7
+ };
8
+ /** Wire payload for PostHog /i/v0/e/ */
9
+ export type CaptureBody = {
10
+ api_key: string;
11
+ event: string;
12
+ distinct_id: string;
13
+ properties: EventProperties;
14
+ timestamp: string;
15
+ };
16
+ export type TransportPreset = 'auto' | 'beacon' | 'fetch';
17
+ /** POST JSON to `url` (e.g. via fetch or sendBeacon). Errors are swallowed by the client. */
18
+ export type CaptureTransport = (url: string, body: CaptureBody) => void | Promise<void>;
19
+ export type Transport = TransportPreset | CaptureTransport;
20
+ /**
21
+ * Bot handling uses PostHog's UA blocklist (`@posthog/core`).
22
+ * - `drop`: skip capture when `$raw_user_agent` (or `is_bot: true`) indicates a bot.
23
+ * - `flag`: add `is_bot` and `bot_name` on bot traffic (from `$raw_user_agent`).
24
+ */
25
+ export type BotsMode = 'drop' | 'flag';
26
+ export type PosthogZeroOptions = {
27
+ apiKey: string;
28
+ /** Ingest host, default `https://us.i.posthog.com` */
29
+ host?: string;
30
+ /** Merged into every event's `properties` */
31
+ defaults?: EventProperties;
32
+ /** Skip network (capture still resolves) */
33
+ disabled?: boolean;
34
+ /** Log sanitized payloads to `console` */
35
+ debug?: boolean;
36
+ /**
37
+ * `auto`: sendBeacon when available, else fetch.
38
+ * Or a custom sender that receives the wire payload (serialize as needed).
39
+ */
40
+ transport?: Transport;
41
+ bots?: BotsMode;
42
+ };
43
+ export type PosthogZeroClient = {
44
+ capture: (input: CaptureInput) => Promise<void>;
45
+ };
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,MAAM,CAClC,MAAM,EACN,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CACtC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,CAAA;AAED,wCAAwC;AACxC,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,eAAe,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEzD,6FAA6F;AAC7F,MAAM,MAAM,gBAAgB,GAAG,CAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,KACd,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAEzB,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,gBAAgB,CAAA;AAE1D;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAA;AAEtC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,IAAI,CAAC,EAAE,QAAQ,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAChD,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "posthog-zero",
3
+ "version": "0.1.0",
4
+ "description": "Minimal PostHog event capture for Cloudflare Workers, edge, and browser/Tauri.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": "EveToolsHQ/posthog-zero",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./request": {
14
+ "types": "./dist/request.d.ts",
15
+ "import": "./dist/request.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "dependencies": {
23
+ "@posthog/core": "^1.29.9"
24
+ },
25
+ "devDependencies": {
26
+ "@eslint/js": "^9.39.4",
27
+ "eslint": "^9.39.4",
28
+ "eslint-config-prettier": "^10.1.8",
29
+ "eslint-plugin-import-x": "^4.16.2",
30
+ "eslint-plugin-prettier": "^5.5.5",
31
+ "eslint-plugin-unused-imports": "^4.4.1",
32
+ "prettier": "^3.8.3",
33
+ "typescript": "~5.8.3",
34
+ "typescript-eslint": "^8.60.0",
35
+ "vitest": "^3.2.4"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "check": "pnpm run typecheck && pnpm run lint && pnpm run format:check && pnpm run test",
43
+ "format": "prettier --write .",
44
+ "format:check": "prettier --check .",
45
+ "lint": "eslint .",
46
+ "lint:fix": "eslint . --fix",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "typecheck": "tsc --noEmit"
50
+ }
51
+ }