@apideck/agent-analytics 0.10.0 → 0.12.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 CHANGED
@@ -82,6 +82,62 @@ Now you can build:
82
82
 
83
83
  ---
84
84
 
85
+ ## Upgrading to 0.12
86
+
87
+ Four breaking changes, all deliberate. Each one existed because the previous
88
+ behaviour was wrong in a way that failed quietly.
89
+
90
+ **`distinctId` is now keyed.** The old identifier was an unsalted 32-bit djb2
91
+ over `ip:userAgent`. Since the user agent ships in plaintext on the same event,
92
+ only the IP had to be searched — a laptop recovered a residential address in
93
+ 75 seconds. Set `idSecret` (or `AGENT_ANALYTICS_ID_SECRET`) to a stable secret;
94
+ without one, a random per-instance secret is used, which stays private but
95
+ means ids no longer correlate across instances. Existing ids will not match the
96
+ new ones either way.
97
+
98
+ ```diff
99
+ - void trackVisit(req, { analytics })
100
+ + void trackVisit(req, { analytics, idSecret: process.env.AGENT_ANALYTICS_ID_SECRET })
101
+ ```
102
+
103
+ **`verifyIdentity: true` is replaced by an injected verifier.** The published
104
+ IP range tables are the largest thing in the package, and importing them from
105
+ the root entry shipped them to every consumer whether or not they verified
106
+ anything. They now live behind `@apideck/agent-analytics/verify`.
107
+
108
+ ```diff
109
+ - void trackVisit(req, { analytics, verifyIdentity: true })
110
+ + import { verifyRequest } from '@apideck/agent-analytics/verify'
111
+ + void trackVisit(req, { analytics, verify: verifyRequest })
112
+ ```
113
+
114
+ **Caller `properties` no longer override computed fields.** They were spread
115
+ last, so `properties: { path }` silently replaced the real path and
116
+ `properties: { is_ai_bot }` could contradict the classification on the same
117
+ event. Non-colliding keys are unaffected.
118
+
119
+ **Headless automation is labelled `Headless`, not `Browser`.** A browser user
120
+ agent with headless headers accounted for 79% of one production site's agent
121
+ traffic, and calling it `Browser` hid it behind the obvious
122
+ `bot_name != 'Browser'` filter. `headless_score` and `headless_likely` are now
123
+ omitted on declared crawlers and HTTP clients, where they fired on 99% of
124
+ events and carried no signal.
125
+
126
+ **Node 18 is no longer supported; the minimum is Node 20.** `globalThis.crypto`
127
+ only became available by default in Node 19, and shipping a `node:crypto`
128
+ fallback would mean a static import of a Node builtin in a library whose main
129
+ target is edge runtimes. Node 18 reached end of life in April 2025. Runtimes
130
+ without Web Crypto now fail with an explicit message rather than a confusing
131
+ `undefined` dereference.
132
+
133
+ ### Also in 0.12
134
+
135
+ - Adapters surface non-2xx responses as `CaptureTransportError` instead of
136
+ swallowing them. Pass `onError` to `trackVisit` to see them; capture still
137
+ never throws into the response path.
138
+ - Outbound captures carry a 3s `AbortSignal` (`timeoutMs` to change it).
139
+ - Root bundle is 65% smaller (27.7 kB → 9.6 kB, 3.8 kB gzipped).
140
+
85
141
  ## Install
86
142
 
87
143
  ```bash
@@ -306,6 +362,82 @@ Full middleware example: [`README.md → Markdown mirror helpers`](./README.md#m
306
362
 
307
363
  ---
308
364
 
365
+ ## Advanced: verifying crawler identity against published IP ranges
366
+
367
+ User agents are trivially forged — `curl -A "ChatGPT-User"` is indistinguishable
368
+ from the real thing at the UA layer. Set `verifyIdentity: true` to check the
369
+ client IP against the vendor's published crawler ranges:
370
+
371
+ ```ts
372
+ void trackVisit(request, {
373
+ analytics,
374
+ verifyIdentity: true,
375
+ captureIp: true // not required, but useful for auditing a 'spoofed' verdict
376
+ })
377
+ ```
378
+
379
+ Three properties land on the event:
380
+
381
+ | property | values |
382
+ | --- | --- |
383
+ | `bot_verification` | `verified` \| `spoofed` \| `unverifiable` \| `not-claimed` |
384
+ | `bot_verified` | `true` \| `false` \| `null` — tri-state, for quick filtering |
385
+ | `bot_verification_reason` | why, when the verdict is `unverifiable` |
386
+
387
+ ### What can actually be verified
388
+
389
+ Only vendors that publish a machine-readable range feed: **OpenAI**,
390
+ **Anthropic**, **Perplexity**, and **Apple**. Bytespider, Amazonbot, Meta and
391
+ the rest report `unverifiable` — never `spoofed`. Collapsing "we can't check"
392
+ into "impostor" would be a false accusation, which is why `bot_verified` is
393
+ tri-state rather than a boolean.
394
+
395
+ ### Server-side crawlers vs client-side agents
396
+
397
+ A published range list covers a vendor's **crawler fleet**, not its products
398
+ that fetch from the end user's device. Claude Code runs on a developer's
399
+ laptop, so the request carries *their* IP and will never appear in Anthropic's
400
+ ranges. Measured over 30 days of production traffic:
401
+
402
+ | user agent | events | distinct IPs | in published range |
403
+ | --- | ---: | ---: | ---: |
404
+ | `ClaudeBot` | 13,671 | 236 | 96% |
405
+ | `PerplexityBot` | 6,897 | 158 | 91% |
406
+ | `ChatGPT-User` | ~72,000 | 43 | 99% |
407
+ | `Claude-User` (claude-code CLI) | 6,492 | 4,486 | **0%** |
408
+ | `Perplexity-User` | 493 | 148 | **0%** |
409
+
410
+ A naive vendor-level check would brand the bottom two rows — roughly 7,000
411
+ legitimate fetches a month — as impersonation. So the library gates verdicts on
412
+ the *product*, returning `unverifiable` with reason `client-side-agent` for
413
+ those. Note the distinction is not a `-User` suffix: OpenAI's `ChatGPT-User`
414
+ fetches server-side from Azure and verifies at ~99%.
415
+
416
+ ### Keeping the ranges fresh
417
+
418
+ The bundled snapshot is in `src/bot-ranges.ts`, stamped with
419
+ `BOT_RANGES_CAPTURED_AT`. Refresh it on a schedule:
420
+
421
+ ```bash
422
+ node scripts/refresh-bot-ranges.mjs
423
+ ```
424
+
425
+ Freshness is the whole game. Nearly every OpenAI prefix is an Azure block and
426
+ Anthropic's are GCP, so "came from a datacenter" proves nothing on its own —
427
+ only membership in the *current* published list does. A stale snapshot produces
428
+ false `spoofed` verdicts on real crawlers, so the refresh script refuses to
429
+ write a list that shrinks by more than half or when any feed errors.
430
+
431
+ ### Trusting the client IP
432
+
433
+ The verdict is only as good as the IP. On Vercel and Cloudflare the edge
434
+ overwrites `x-forwarded-for`, so the first hop is trustworthy. Behind a proxy
435
+ that passes a client-supplied header through, an attacker controls the value
436
+ and `verified` means nothing — confirm your proxy's behaviour before acting on
437
+ this data.
438
+
439
+ ---
440
+
309
441
  ## Advanced: Peec.ai crawl-insights export
310
442
 
311
443
  [Peec.ai](https://peec.ai)'s **Agent analytics** product ingests a CSV/CLF access log and produces dashboards on top of it. The Peec docs assume you have a Vercel Log Drain → Axiom (or similar) pipeline that emits these eight columns: `timestamp, request_method, request_url, response_status, client_ip, user_agent, country_code, referer`.
@@ -1,31 +1,3 @@
1
- 'use strict';
2
-
3
- // src/adapters/posthog.ts
4
- function posthogAnalytics(config) {
5
- const hostRaw = config.host ?? "https://us.i.posthog.com";
6
- const base = (/^https?:\/\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\/$/, "");
7
- const path = (config.path ?? "/i/v0/e/").replace(/^(?!\/)/, "/");
8
- const endpoint = `${base}${path}`;
9
- const fetchImpl = config.fetchImpl ?? fetch;
10
- return {
11
- async capture(event) {
12
- const payload = {
13
- api_key: config.apiKey,
14
- event: event.event,
15
- distinct_id: event.distinctId,
16
- timestamp: event.timestamp,
17
- properties: event.properties
18
- };
19
- await fetchImpl(endpoint, {
20
- method: "POST",
21
- headers: { "Content-Type": "application/json" },
22
- body: JSON.stringify(payload),
23
- keepalive: true
24
- });
25
- }
26
- };
27
- }
28
-
29
- exports.posthogAnalytics = posthogAnalytics;
30
- //# sourceMappingURL=posthog.cjs.map
1
+ 'use strict';var r=class extends Error{status;body;constructor(e,a,i){super(e),this.name="CaptureTransportError",this.status=a,this.body=i;}};function h(t){let e=t.host??"https://us.i.posthog.com",a=(/^https?:\/\//.test(e)?e:`https://${e}`).replace(/\/$/,""),i=(t.path??"/i/v0/e/").replace(/^(?!\/)/,"/"),n=`${a}${i}`,p=t.fetchImpl??fetch;return {async capture(o){let c={api_key:t.apiKey,event:o.event,distinct_id:o.distinctId,timestamp:o.timestamp,properties:o.properties},s=await p(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c),keepalive:true,signal:AbortSignal.timeout(t.timeoutMs??3e3)});if(!s.ok)throw new r(`PostHog capture failed: ${s.status} ${s.statusText}`,s.status,await s.text().catch(()=>{}))}}}
2
+ exports.posthogAnalytics=h;//# sourceMappingURL=posthog.cjs.map
31
3
  //# sourceMappingURL=posthog.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/posthog.ts"],"names":[],"mappings":";;;AA6BO,SAAS,iBAAiB,MAAA,EAAgD;AAC/E,EAAA,MAAM,OAAA,GAAU,OAAO,IAAA,IAAQ,0BAAA;AAC/B,EAAA,MAAM,IAAA,GAAA,CAAQ,cAAA,CAAe,IAAA,CAAK,OAAO,CAAA,GAAI,OAAA,GAAU,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAC9F,EAAA,MAAM,QAAQ,MAAA,CAAO,IAAA,IAAQ,UAAA,EAAY,OAAA,CAAQ,WAAW,GAAG,CAAA;AAC/D,EAAA,MAAM,QAAA,GAAW,CAAA,EAAG,IAAI,CAAA,EAAG,IAAI,CAAA,CAAA;AAC/B,EAAA,MAAM,SAAA,GAAY,OAAO,SAAA,IAAa,KAAA;AAEtC,EAAA,OAAO;AAAA,IACL,MAAM,QAAQ,KAAA,EAAoC;AAChD,MAAA,MAAM,OAAA,GAAU;AAAA,QACd,SAAS,MAAA,CAAO,MAAA;AAAA,QAChB,OAAO,KAAA,CAAM,KAAA;AAAA,QACb,aAAa,KAAA,CAAM,UAAA;AAAA,QACnB,WAAW,KAAA,CAAM,SAAA;AAAA,QACjB,YAAY,KAAA,CAAM;AAAA,OACpB;AACA,MAAA,MAAM,UAAU,QAAA,EAAU;AAAA,QACxB,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,OAAO,CAAA;AAAA,QAC5B,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH;AAAA,GACF;AACF","file":"posthog.cjs","sourcesContent":["import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\nexport interface PostHogAdapterConfig {\n /** PostHog project API key (the public one used by the JS SDK). */\n apiKey: string\n /**\n * PostHog host, with or without scheme. Defaults to `https://us.i.posthog.com`.\n * Use `https://eu.i.posthog.com` for EU cloud, or your own reverse-proxy\n * domain (e.g. `https://svc.example.com`).\n */\n host?: string\n /**\n * Path on the host that accepts single-event captures. Defaults to\n * `/i/v0/e/` which is PostHog's current endpoint for this.\n */\n path?: string\n /**\n * Override the `fetch` implementation (useful for tests or custom runtimes\n * that need a pinned fetch).\n */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Adapter that posts each event to the PostHog capture endpoint. Uses\n * `keepalive: true` so the request survives after a serverless response\n * returns — events aren't guaranteed (fire-and-forget), but that's the\n * trade we want to keep the hot path fast.\n */\nexport function posthogAnalytics(config: PostHogAdapterConfig): AnalyticsAdapter {\n const hostRaw = config.host ?? 'https://us.i.posthog.com'\n const base = (/^https?:\\/\\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\\/$/, '')\n const path = (config.path ?? '/i/v0/e/').replace(/^(?!\\/)/, '/')\n const endpoint = `${base}${path}`\n const fetchImpl = config.fetchImpl ?? fetch\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n const payload = {\n api_key: config.apiKey,\n event: event.event,\n distinct_id: event.distinctId,\n timestamp: event.timestamp,\n properties: event.properties\n }\n await fetchImpl(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true\n })\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/adapters/posthog.ts"],"names":["CaptureTransportError","message","status","body","posthogAnalytics","config","hostRaw","base","path","endpoint","fetchImpl","event","payload","res"],"mappings":"aACO,IAAMA,CAAAA,CAAN,cAAoC,KAAM,CACtC,OACA,IAAA,CACT,WAAA,CAAYC,CAAAA,CAAiBC,CAAAA,CAAiBC,CAAAA,CAAe,CAC3D,KAAA,CAAMF,CAAO,EACb,IAAA,CAAK,IAAA,CAAO,uBAAA,CACZ,IAAA,CAAK,MAAA,CAASC,CAAAA,CACd,IAAA,CAAK,IAAA,CAAOC,EACd,CACF,CAAA,CC2BO,SAASC,CAAAA,CAAiBC,EAAgD,CAC/E,IAAMC,CAAAA,CAAUD,CAAAA,CAAO,MAAQ,0BAAA,CACzBE,CAAAA,CAAAA,CAAQ,cAAA,CAAe,IAAA,CAAKD,CAAO,CAAA,CAAIA,CAAAA,CAAU,CAAA,QAAA,EAAWA,CAAO,CAAA,CAAA,EAAI,OAAA,CAAQ,KAAA,CAAO,EAAE,CAAA,CACxFE,CAAAA,CAAAA,CAAQH,CAAAA,CAAO,IAAA,EAAQ,YAAY,OAAA,CAAQ,SAAA,CAAW,GAAG,CAAA,CACzDI,CAAAA,CAAW,CAAA,EAAGF,CAAI,CAAA,EAAGC,CAAI,CAAA,CAAA,CACzBE,CAAAA,CAAYL,CAAAA,CAAO,SAAA,EAAa,MAEtC,OAAO,CACL,MAAM,OAAA,CAAQM,EAAoC,CAChD,IAAMC,CAAAA,CAAU,CACd,OAAA,CAASP,CAAAA,CAAO,MAAA,CAChB,KAAA,CAAOM,EAAM,KAAA,CACb,WAAA,CAAaA,CAAAA,CAAM,UAAA,CACnB,SAAA,CAAWA,CAAAA,CAAM,SAAA,CACjB,UAAA,CAAYA,EAAM,UACpB,CAAA,CAIME,CAAAA,CAAM,MAAMH,CAAAA,CAAUD,CAAAA,CAAU,CACpC,MAAA,CAAQ,OACR,OAAA,CAAS,CAAE,cAAA,CAAgB,kBAAmB,EAC9C,IAAA,CAAM,IAAA,CAAK,SAAA,CAAUG,CAAO,EAC5B,SAAA,CAAW,IAAA,CACX,MAAA,CAAQ,WAAA,CAAY,OAAA,CAAQP,CAAAA,CAAO,SAAA,EAAa,GAAI,CACtD,CAAC,CAAA,CACD,GAAI,CAACQ,CAAAA,CAAI,EAAA,CACP,MAAM,IAAIb,EACR,CAAA,wBAAA,EAA2Ba,CAAAA,CAAI,MAAM,CAAA,CAAA,EAAIA,CAAAA,CAAI,UAAU,CAAA,CAAA,CACvDA,CAAAA,CAAI,OACJ,MAAMA,CAAAA,CAAI,IAAA,EAAK,CAAE,MAAM,IAAG,CAAA,CAAY,CACxC,CAEJ,CACF,CACF","file":"posthog.cjs","sourcesContent":["/** Thrown when the analytics backend rejects, errors, or times out a capture. */\nexport class CaptureTransportError extends Error {\n readonly status: number | undefined\n readonly body: string | undefined\n constructor(message: string, status?: number, body?: string) {\n super(message)\n this.name = 'CaptureTransportError'\n this.status = status\n this.body = body\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\nimport { CaptureTransportError } from '../errors.js'\n\n\nexport interface PostHogAdapterConfig {\n /** PostHog project API key (the public one used by the JS SDK). */\n apiKey: string\n /**\n * PostHog host, with or without scheme. Defaults to `https://us.i.posthog.com`.\n * Use `https://eu.i.posthog.com` for EU cloud, or your own reverse-proxy\n * domain (e.g. `https://svc.example.com`).\n */\n host?: string\n /**\n * Path on the host that accepts single-event captures. Defaults to\n * `/i/v0/e/` which is PostHog's current endpoint for this.\n */\n path?: string\n /**\n * Override the `fetch` implementation (useful for tests or custom runtimes\n * that need a pinned fetch).\n */\n fetchImpl?: typeof fetch\n /**\n * Abort the capture after this many milliseconds. Defaults to 3000. Without\n * a bound, a hung backend leaves a pending promise for the lifetime of an\n * edge invocation.\n */\n timeoutMs?: number\n}\n\n/**\n * Adapter that posts each event to the PostHog capture endpoint. Uses\n * `keepalive: true` so the request survives after a serverless response\n * returns — events aren't guaranteed (fire-and-forget), but that's the\n * trade we want to keep the hot path fast.\n */\nexport function posthogAnalytics(config: PostHogAdapterConfig): AnalyticsAdapter {\n const hostRaw = config.host ?? 'https://us.i.posthog.com'\n const base = (/^https?:\\/\\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\\/$/, '')\n const path = (config.path ?? '/i/v0/e/').replace(/^(?!\\/)/, '/')\n const endpoint = `${base}${path}`\n const fetchImpl = config.fetchImpl ?? fetch\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n const payload = {\n api_key: config.apiKey,\n event: event.event,\n distinct_id: event.distinctId,\n timestamp: event.timestamp,\n properties: event.properties\n }\n // A 401 from a mistyped key used to look identical to success. Surface\n // it: `trackVisit` routes it to `onError` and still never throws into\n // the response path.\n const res = await fetchImpl(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true,\n signal: AbortSignal.timeout(config.timeoutMs ?? 3000)\n })\n if (!res.ok) {\n throw new CaptureTransportError(\n `PostHog capture failed: ${res.status} ${res.statusText}`,\n res.status,\n await res.text().catch(() => undefined)\n )\n }\n }\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { A as AnalyticsAdapter } from '../types-B7jSKtLz.cjs';
1
+ import { A as AnalyticsAdapter } from '../types-sQoQK-ox.cjs';
2
2
 
3
3
  interface PostHogAdapterConfig {
4
4
  /** PostHog project API key (the public one used by the JS SDK). */
@@ -19,6 +19,12 @@ interface PostHogAdapterConfig {
19
19
  * that need a pinned fetch).
20
20
  */
21
21
  fetchImpl?: typeof fetch;
22
+ /**
23
+ * Abort the capture after this many milliseconds. Defaults to 3000. Without
24
+ * a bound, a hung backend leaves a pending promise for the lifetime of an
25
+ * edge invocation.
26
+ */
27
+ timeoutMs?: number;
22
28
  }
23
29
  /**
24
30
  * Adapter that posts each event to the PostHog capture endpoint. Uses
@@ -1,4 +1,4 @@
1
- import { A as AnalyticsAdapter } from '../types-B7jSKtLz.js';
1
+ import { A as AnalyticsAdapter } from '../types-sQoQK-ox.js';
2
2
 
3
3
  interface PostHogAdapterConfig {
4
4
  /** PostHog project API key (the public one used by the JS SDK). */
@@ -19,6 +19,12 @@ interface PostHogAdapterConfig {
19
19
  * that need a pinned fetch).
20
20
  */
21
21
  fetchImpl?: typeof fetch;
22
+ /**
23
+ * Abort the capture after this many milliseconds. Defaults to 3000. Without
24
+ * a bound, a hung backend leaves a pending promise for the lifetime of an
25
+ * edge invocation.
26
+ */
27
+ timeoutMs?: number;
22
28
  }
23
29
  /**
24
30
  * Adapter that posts each event to the PostHog capture endpoint. Uses
@@ -1,29 +1,3 @@
1
- // src/adapters/posthog.ts
2
- function posthogAnalytics(config) {
3
- const hostRaw = config.host ?? "https://us.i.posthog.com";
4
- const base = (/^https?:\/\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\/$/, "");
5
- const path = (config.path ?? "/i/v0/e/").replace(/^(?!\/)/, "/");
6
- const endpoint = `${base}${path}`;
7
- const fetchImpl = config.fetchImpl ?? fetch;
8
- return {
9
- async capture(event) {
10
- const payload = {
11
- api_key: config.apiKey,
12
- event: event.event,
13
- distinct_id: event.distinctId,
14
- timestamp: event.timestamp,
15
- properties: event.properties
16
- };
17
- await fetchImpl(endpoint, {
18
- method: "POST",
19
- headers: { "Content-Type": "application/json" },
20
- body: JSON.stringify(payload),
21
- keepalive: true
22
- });
23
- }
24
- };
25
- }
26
-
27
- export { posthogAnalytics };
28
- //# sourceMappingURL=posthog.js.map
1
+ var r=class extends Error{status;body;constructor(e,a,i){super(e),this.name="CaptureTransportError",this.status=a,this.body=i;}};function h(t){let e=t.host??"https://us.i.posthog.com",a=(/^https?:\/\//.test(e)?e:`https://${e}`).replace(/\/$/,""),i=(t.path??"/i/v0/e/").replace(/^(?!\/)/,"/"),n=`${a}${i}`,p=t.fetchImpl??fetch;return {async capture(o){let c={api_key:t.apiKey,event:o.event,distinct_id:o.distinctId,timestamp:o.timestamp,properties:o.properties},s=await p(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c),keepalive:true,signal:AbortSignal.timeout(t.timeoutMs??3e3)});if(!s.ok)throw new r(`PostHog capture failed: ${s.status} ${s.statusText}`,s.status,await s.text().catch(()=>{}))}}}
2
+ export{h as posthogAnalytics};//# sourceMappingURL=posthog.js.map
29
3
  //# sourceMappingURL=posthog.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/posthog.ts"],"names":[],"mappings":";AA6BO,SAAS,iBAAiB,MAAA,EAAgD;AAC/E,EAAA,MAAM,OAAA,GAAU,OAAO,IAAA,IAAQ,0BAAA;AAC/B,EAAA,MAAM,IAAA,GAAA,CAAQ,cAAA,CAAe,IAAA,CAAK,OAAO,CAAA,GAAI,OAAA,GAAU,CAAA,QAAA,EAAW,OAAO,CAAA,CAAA,EAAI,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAC9F,EAAA,MAAM,QAAQ,MAAA,CAAO,IAAA,IAAQ,UAAA,EAAY,OAAA,CAAQ,WAAW,GAAG,CAAA;AAC/D,EAAA,MAAM,QAAA,GAAW,CAAA,EAAG,IAAI,CAAA,EAAG,IAAI,CAAA,CAAA;AAC/B,EAAA,MAAM,SAAA,GAAY,OAAO,SAAA,IAAa,KAAA;AAEtC,EAAA,OAAO;AAAA,IACL,MAAM,QAAQ,KAAA,EAAoC;AAChD,MAAA,MAAM,OAAA,GAAU;AAAA,QACd,SAAS,MAAA,CAAO,MAAA;AAAA,QAChB,OAAO,KAAA,CAAM,KAAA;AAAA,QACb,aAAa,KAAA,CAAM,UAAA;AAAA,QACnB,WAAW,KAAA,CAAM,SAAA;AAAA,QACjB,YAAY,KAAA,CAAM;AAAA,OACpB;AACA,MAAA,MAAM,UAAU,QAAA,EAAU;AAAA,QACxB,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,OAAO,CAAA;AAAA,QAC5B,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH;AAAA,GACF;AACF","file":"posthog.js","sourcesContent":["import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\nexport interface PostHogAdapterConfig {\n /** PostHog project API key (the public one used by the JS SDK). */\n apiKey: string\n /**\n * PostHog host, with or without scheme. Defaults to `https://us.i.posthog.com`.\n * Use `https://eu.i.posthog.com` for EU cloud, or your own reverse-proxy\n * domain (e.g. `https://svc.example.com`).\n */\n host?: string\n /**\n * Path on the host that accepts single-event captures. Defaults to\n * `/i/v0/e/` which is PostHog's current endpoint for this.\n */\n path?: string\n /**\n * Override the `fetch` implementation (useful for tests or custom runtimes\n * that need a pinned fetch).\n */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Adapter that posts each event to the PostHog capture endpoint. Uses\n * `keepalive: true` so the request survives after a serverless response\n * returns — events aren't guaranteed (fire-and-forget), but that's the\n * trade we want to keep the hot path fast.\n */\nexport function posthogAnalytics(config: PostHogAdapterConfig): AnalyticsAdapter {\n const hostRaw = config.host ?? 'https://us.i.posthog.com'\n const base = (/^https?:\\/\\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\\/$/, '')\n const path = (config.path ?? '/i/v0/e/').replace(/^(?!\\/)/, '/')\n const endpoint = `${base}${path}`\n const fetchImpl = config.fetchImpl ?? fetch\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n const payload = {\n api_key: config.apiKey,\n event: event.event,\n distinct_id: event.distinctId,\n timestamp: event.timestamp,\n properties: event.properties\n }\n await fetchImpl(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true\n })\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/adapters/posthog.ts"],"names":["CaptureTransportError","message","status","body","posthogAnalytics","config","hostRaw","base","path","endpoint","fetchImpl","event","payload","res"],"mappings":"AACO,IAAMA,CAAAA,CAAN,cAAoC,KAAM,CACtC,OACA,IAAA,CACT,WAAA,CAAYC,CAAAA,CAAiBC,CAAAA,CAAiBC,CAAAA,CAAe,CAC3D,KAAA,CAAMF,CAAO,EACb,IAAA,CAAK,IAAA,CAAO,uBAAA,CACZ,IAAA,CAAK,MAAA,CAASC,CAAAA,CACd,IAAA,CAAK,IAAA,CAAOC,EACd,CACF,CAAA,CC2BO,SAASC,CAAAA,CAAiBC,EAAgD,CAC/E,IAAMC,CAAAA,CAAUD,CAAAA,CAAO,MAAQ,0BAAA,CACzBE,CAAAA,CAAAA,CAAQ,cAAA,CAAe,IAAA,CAAKD,CAAO,CAAA,CAAIA,CAAAA,CAAU,CAAA,QAAA,EAAWA,CAAO,CAAA,CAAA,EAAI,OAAA,CAAQ,KAAA,CAAO,EAAE,CAAA,CACxFE,CAAAA,CAAAA,CAAQH,CAAAA,CAAO,IAAA,EAAQ,YAAY,OAAA,CAAQ,SAAA,CAAW,GAAG,CAAA,CACzDI,CAAAA,CAAW,CAAA,EAAGF,CAAI,CAAA,EAAGC,CAAI,CAAA,CAAA,CACzBE,CAAAA,CAAYL,CAAAA,CAAO,SAAA,EAAa,MAEtC,OAAO,CACL,MAAM,OAAA,CAAQM,EAAoC,CAChD,IAAMC,CAAAA,CAAU,CACd,OAAA,CAASP,CAAAA,CAAO,MAAA,CAChB,KAAA,CAAOM,EAAM,KAAA,CACb,WAAA,CAAaA,CAAAA,CAAM,UAAA,CACnB,SAAA,CAAWA,CAAAA,CAAM,SAAA,CACjB,UAAA,CAAYA,EAAM,UACpB,CAAA,CAIME,CAAAA,CAAM,MAAMH,CAAAA,CAAUD,CAAAA,CAAU,CACpC,MAAA,CAAQ,OACR,OAAA,CAAS,CAAE,cAAA,CAAgB,kBAAmB,EAC9C,IAAA,CAAM,IAAA,CAAK,SAAA,CAAUG,CAAO,EAC5B,SAAA,CAAW,IAAA,CACX,MAAA,CAAQ,WAAA,CAAY,OAAA,CAAQP,CAAAA,CAAO,SAAA,EAAa,GAAI,CACtD,CAAC,CAAA,CACD,GAAI,CAACQ,CAAAA,CAAI,EAAA,CACP,MAAM,IAAIb,EACR,CAAA,wBAAA,EAA2Ba,CAAAA,CAAI,MAAM,CAAA,CAAA,EAAIA,CAAAA,CAAI,UAAU,CAAA,CAAA,CACvDA,CAAAA,CAAI,OACJ,MAAMA,CAAAA,CAAI,IAAA,EAAK,CAAE,MAAM,IAAG,CAAA,CAAY,CACxC,CAEJ,CACF,CACF","file":"posthog.js","sourcesContent":["/** Thrown when the analytics backend rejects, errors, or times out a capture. */\nexport class CaptureTransportError extends Error {\n readonly status: number | undefined\n readonly body: string | undefined\n constructor(message: string, status?: number, body?: string) {\n super(message)\n this.name = 'CaptureTransportError'\n this.status = status\n this.body = body\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\nimport { CaptureTransportError } from '../errors.js'\n\n\nexport interface PostHogAdapterConfig {\n /** PostHog project API key (the public one used by the JS SDK). */\n apiKey: string\n /**\n * PostHog host, with or without scheme. Defaults to `https://us.i.posthog.com`.\n * Use `https://eu.i.posthog.com` for EU cloud, or your own reverse-proxy\n * domain (e.g. `https://svc.example.com`).\n */\n host?: string\n /**\n * Path on the host that accepts single-event captures. Defaults to\n * `/i/v0/e/` which is PostHog's current endpoint for this.\n */\n path?: string\n /**\n * Override the `fetch` implementation (useful for tests or custom runtimes\n * that need a pinned fetch).\n */\n fetchImpl?: typeof fetch\n /**\n * Abort the capture after this many milliseconds. Defaults to 3000. Without\n * a bound, a hung backend leaves a pending promise for the lifetime of an\n * edge invocation.\n */\n timeoutMs?: number\n}\n\n/**\n * Adapter that posts each event to the PostHog capture endpoint. Uses\n * `keepalive: true` so the request survives after a serverless response\n * returns — events aren't guaranteed (fire-and-forget), but that's the\n * trade we want to keep the hot path fast.\n */\nexport function posthogAnalytics(config: PostHogAdapterConfig): AnalyticsAdapter {\n const hostRaw = config.host ?? 'https://us.i.posthog.com'\n const base = (/^https?:\\/\\//.test(hostRaw) ? hostRaw : `https://${hostRaw}`).replace(/\\/$/, '')\n const path = (config.path ?? '/i/v0/e/').replace(/^(?!\\/)/, '/')\n const endpoint = `${base}${path}`\n const fetchImpl = config.fetchImpl ?? fetch\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n const payload = {\n api_key: config.apiKey,\n event: event.event,\n distinct_id: event.distinctId,\n timestamp: event.timestamp,\n properties: event.properties\n }\n // A 401 from a mistyped key used to look identical to success. Surface\n // it: `trackVisit` routes it to `onError` and still never throws into\n // the response path.\n const res = await fetchImpl(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true,\n signal: AbortSignal.timeout(config.timeoutMs ?? 3000)\n })\n if (!res.ok) {\n throw new CaptureTransportError(\n `PostHog capture failed: ${res.status} ${res.statusText}`,\n res.status,\n await res.text().catch(() => undefined)\n )\n }\n }\n }\n}\n"]}
@@ -1,24 +1,2 @@
1
- 'use strict';
2
-
3
- // src/adapters/webhook.ts
4
- function webhookAnalytics(config) {
5
- const fetchImpl = config.fetchImpl ?? fetch;
6
- const transform = config.transform ?? ((e) => e);
7
- return {
8
- async capture(event) {
9
- await fetchImpl(config.url, {
10
- method: "POST",
11
- headers: {
12
- "Content-Type": "application/json",
13
- ...config.headers ?? {}
14
- },
15
- body: JSON.stringify(transform(event)),
16
- keepalive: true
17
- });
18
- }
19
- };
20
- }
21
-
22
- exports.webhookAnalytics = webhookAnalytics;
23
- //# sourceMappingURL=webhook.cjs.map
1
+ 'use strict';var n=class extends Error{status;body;constructor(s,o,e){super(s),this.name="CaptureTransportError",this.status=o,this.body=e;}};function u(t){let s=t.fetchImpl??fetch,o=t.transform??(e=>e);return {async capture(e){let r=await s(t.url,{method:"POST",headers:{"Content-Type":"application/json",...t.headers??{}},body:JSON.stringify(o(e)),keepalive:true,signal:AbortSignal.timeout(t.timeoutMs??3e3)});if(!r.ok)throw new n(`Webhook capture failed: ${r.status} ${r.statusText}`,r.status,await r.text().catch(()=>{}))}}}exports.webhookAnalytics=u;//# sourceMappingURL=webhook.cjs.map
24
2
  //# sourceMappingURL=webhook.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/webhook.ts"],"names":[],"mappings":";;;AAsBO,SAAS,iBAAiB,MAAA,EAAgD;AAC/E,EAAA,MAAM,SAAA,GAAY,OAAO,SAAA,IAAa,KAAA;AACtC,EAAA,MAAM,SAAA,GAAY,MAAA,CAAO,SAAA,KAAc,CAAC,CAAA,KAA6B,CAAA,CAAA;AAErE,EAAA,OAAO;AAAA,IACL,MAAM,QAAQ,KAAA,EAAoC;AAChD,MAAA,MAAM,SAAA,CAAU,OAAO,GAAA,EAAK;AAAA,QAC1B,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,cAAA,EAAgB,kBAAA;AAAA,UAChB,GAAI,MAAA,CAAO,OAAA,IAAW;AAAC,SACzB;AAAA,QACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,SAAA,CAAU,KAAK,CAAC,CAAA;AAAA,QACrC,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH;AAAA,GACF;AACF","file":"webhook.cjs","sourcesContent":["import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\nexport interface WebhookAdapterConfig {\n /** Destination URL that receives a POST for each event. */\n url: string\n /** Extra headers merged onto the POST (useful for shared-secret auth). */\n headers?: Record<string, string>\n /**\n * Transform the event into the exact JSON body the destination expects.\n * Defaults to sending the {@link CaptureEvent} as-is.\n */\n transform?: (event: CaptureEvent) => unknown\n /** Override the `fetch` implementation. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Adapter that POSTs each event to an arbitrary webhook URL. Keeps the\n * library analytics-backend-agnostic — use this when PostHog isn't your\n * analytics of record, or when you want to multiplex events through your\n * own ingestion layer.\n */\nexport function webhookAnalytics(config: WebhookAdapterConfig): AnalyticsAdapter {\n const fetchImpl = config.fetchImpl ?? fetch\n const transform = config.transform ?? ((e: CaptureEvent): unknown => e)\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n await fetchImpl(config.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(config.headers ?? {})\n },\n body: JSON.stringify(transform(event)),\n keepalive: true\n })\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/adapters/webhook.ts"],"names":["CaptureTransportError","message","status","body","webhookAnalytics","config","fetchImpl","transform","event","res"],"mappings":"aACO,IAAMA,CAAAA,CAAN,cAAoC,KAAM,CACtC,MAAA,CACA,KACT,WAAA,CAAYC,CAAAA,CAAiBC,CAAAA,CAAiBC,CAAAA,CAAe,CAC3D,KAAA,CAAMF,CAAO,CAAA,CACb,IAAA,CAAK,IAAA,CAAO,uBAAA,CACZ,IAAA,CAAK,MAAA,CAASC,EACd,IAAA,CAAK,IAAA,CAAOC,EACd,CACF,CAAA,CCeO,SAASC,EAAiBC,CAAAA,CAAgD,CAC/E,IAAMC,CAAAA,CAAYD,CAAAA,CAAO,SAAA,EAAa,MAChCE,CAAAA,CAAYF,CAAAA,CAAO,SAAA,GAAe,CAAA,EAA6B,CAAA,CAAA,CAErE,OAAO,CACL,MAAM,OAAA,CAAQG,CAAAA,CAAoC,CAChD,IAAMC,CAAAA,CAAM,MAAMH,CAAAA,CAAUD,CAAAA,CAAO,GAAA,CAAK,CACtC,MAAA,CAAQ,MAAA,CACR,QAAS,CACP,cAAA,CAAgB,kBAAA,CAChB,GAAIA,CAAAA,CAAO,OAAA,EAAW,EACxB,CAAA,CACA,IAAA,CAAM,IAAA,CAAK,SAAA,CAAUE,CAAAA,CAAUC,CAAK,CAAC,CAAA,CACrC,SAAA,CAAW,IAAA,CACX,MAAA,CAAQ,WAAA,CAAY,QAAQH,CAAAA,CAAO,SAAA,EAAa,GAAI,CACtD,CAAC,CAAA,CACD,GAAI,CAACI,CAAAA,CAAI,EAAA,CACP,MAAM,IAAIT,CAAAA,CACR,2BAA2BS,CAAAA,CAAI,MAAM,CAAA,CAAA,EAAIA,CAAAA,CAAI,UAAU,CAAA,CAAA,CACvDA,EAAI,MAAA,CACJ,MAAMA,CAAAA,CAAI,IAAA,EAAK,CAAE,KAAA,CAAM,IAAG,CAAA,CAAY,CACxC,CAEJ,CACF,CACF","file":"webhook.cjs","sourcesContent":["/** Thrown when the analytics backend rejects, errors, or times out a capture. */\nexport class CaptureTransportError extends Error {\n readonly status: number | undefined\n readonly body: string | undefined\n constructor(message: string, status?: number, body?: string) {\n super(message)\n this.name = 'CaptureTransportError'\n this.status = status\n this.body = body\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\nimport { CaptureTransportError } from '../errors.js'\n\nexport interface WebhookAdapterConfig {\n /** Destination URL that receives a POST for each event. */\n url: string\n /** Extra headers merged onto the POST (useful for shared-secret auth). */\n headers?: Record<string, string>\n /**\n * Transform the event into the exact JSON body the destination expects.\n * Defaults to sending the {@link CaptureEvent} as-is.\n */\n transform?: (event: CaptureEvent) => unknown\n /** Override the `fetch` implementation. */\n fetchImpl?: typeof fetch\n /** Abort the capture after this many milliseconds. Defaults to 3000. */\n timeoutMs?: number\n}\n\n/**\n * Adapter that POSTs each event to an arbitrary webhook URL. Keeps the\n * library analytics-backend-agnostic — use this when PostHog isn't your\n * analytics of record, or when you want to multiplex events through your\n * own ingestion layer.\n */\nexport function webhookAnalytics(config: WebhookAdapterConfig): AnalyticsAdapter {\n const fetchImpl = config.fetchImpl ?? fetch\n const transform = config.transform ?? ((e: CaptureEvent): unknown => e)\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n const res = await fetchImpl(config.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(config.headers ?? {})\n },\n body: JSON.stringify(transform(event)),\n keepalive: true,\n signal: AbortSignal.timeout(config.timeoutMs ?? 3000)\n })\n if (!res.ok) {\n throw new CaptureTransportError(\n `Webhook capture failed: ${res.status} ${res.statusText}`,\n res.status,\n await res.text().catch(() => undefined)\n )\n }\n }\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { C as CaptureEvent, A as AnalyticsAdapter } from '../types-B7jSKtLz.cjs';
1
+ import { C as CaptureEvent, A as AnalyticsAdapter } from '../types-sQoQK-ox.cjs';
2
2
 
3
3
  interface WebhookAdapterConfig {
4
4
  /** Destination URL that receives a POST for each event. */
@@ -12,6 +12,8 @@ interface WebhookAdapterConfig {
12
12
  transform?: (event: CaptureEvent) => unknown;
13
13
  /** Override the `fetch` implementation. */
14
14
  fetchImpl?: typeof fetch;
15
+ /** Abort the capture after this many milliseconds. Defaults to 3000. */
16
+ timeoutMs?: number;
15
17
  }
16
18
  /**
17
19
  * Adapter that POSTs each event to an arbitrary webhook URL. Keeps the
@@ -1,4 +1,4 @@
1
- import { C as CaptureEvent, A as AnalyticsAdapter } from '../types-B7jSKtLz.js';
1
+ import { C as CaptureEvent, A as AnalyticsAdapter } from '../types-sQoQK-ox.js';
2
2
 
3
3
  interface WebhookAdapterConfig {
4
4
  /** Destination URL that receives a POST for each event. */
@@ -12,6 +12,8 @@ interface WebhookAdapterConfig {
12
12
  transform?: (event: CaptureEvent) => unknown;
13
13
  /** Override the `fetch` implementation. */
14
14
  fetchImpl?: typeof fetch;
15
+ /** Abort the capture after this many milliseconds. Defaults to 3000. */
16
+ timeoutMs?: number;
15
17
  }
16
18
  /**
17
19
  * Adapter that POSTs each event to an arbitrary webhook URL. Keeps the
@@ -1,22 +1,2 @@
1
- // src/adapters/webhook.ts
2
- function webhookAnalytics(config) {
3
- const fetchImpl = config.fetchImpl ?? fetch;
4
- const transform = config.transform ?? ((e) => e);
5
- return {
6
- async capture(event) {
7
- await fetchImpl(config.url, {
8
- method: "POST",
9
- headers: {
10
- "Content-Type": "application/json",
11
- ...config.headers ?? {}
12
- },
13
- body: JSON.stringify(transform(event)),
14
- keepalive: true
15
- });
16
- }
17
- };
18
- }
19
-
20
- export { webhookAnalytics };
21
- //# sourceMappingURL=webhook.js.map
1
+ var n=class extends Error{status;body;constructor(s,o,e){super(s),this.name="CaptureTransportError",this.status=o,this.body=e;}};function u(t){let s=t.fetchImpl??fetch,o=t.transform??(e=>e);return {async capture(e){let r=await s(t.url,{method:"POST",headers:{"Content-Type":"application/json",...t.headers??{}},body:JSON.stringify(o(e)),keepalive:true,signal:AbortSignal.timeout(t.timeoutMs??3e3)});if(!r.ok)throw new n(`Webhook capture failed: ${r.status} ${r.statusText}`,r.status,await r.text().catch(()=>{}))}}}export{u as webhookAnalytics};//# sourceMappingURL=webhook.js.map
22
2
  //# sourceMappingURL=webhook.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/webhook.ts"],"names":[],"mappings":";AAsBO,SAAS,iBAAiB,MAAA,EAAgD;AAC/E,EAAA,MAAM,SAAA,GAAY,OAAO,SAAA,IAAa,KAAA;AACtC,EAAA,MAAM,SAAA,GAAY,MAAA,CAAO,SAAA,KAAc,CAAC,CAAA,KAA6B,CAAA,CAAA;AAErE,EAAA,OAAO;AAAA,IACL,MAAM,QAAQ,KAAA,EAAoC;AAChD,MAAA,MAAM,SAAA,CAAU,OAAO,GAAA,EAAK;AAAA,QAC1B,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS;AAAA,UACP,cAAA,EAAgB,kBAAA;AAAA,UAChB,GAAI,MAAA,CAAO,OAAA,IAAW;AAAC,SACzB;AAAA,QACA,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,SAAA,CAAU,KAAK,CAAC,CAAA;AAAA,QACrC,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH;AAAA,GACF;AACF","file":"webhook.js","sourcesContent":["import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\n\nexport interface WebhookAdapterConfig {\n /** Destination URL that receives a POST for each event. */\n url: string\n /** Extra headers merged onto the POST (useful for shared-secret auth). */\n headers?: Record<string, string>\n /**\n * Transform the event into the exact JSON body the destination expects.\n * Defaults to sending the {@link CaptureEvent} as-is.\n */\n transform?: (event: CaptureEvent) => unknown\n /** Override the `fetch` implementation. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Adapter that POSTs each event to an arbitrary webhook URL. Keeps the\n * library analytics-backend-agnostic — use this when PostHog isn't your\n * analytics of record, or when you want to multiplex events through your\n * own ingestion layer.\n */\nexport function webhookAnalytics(config: WebhookAdapterConfig): AnalyticsAdapter {\n const fetchImpl = config.fetchImpl ?? fetch\n const transform = config.transform ?? ((e: CaptureEvent): unknown => e)\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n await fetchImpl(config.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(config.headers ?? {})\n },\n body: JSON.stringify(transform(event)),\n keepalive: true\n })\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/adapters/webhook.ts"],"names":["CaptureTransportError","message","status","body","webhookAnalytics","config","fetchImpl","transform","event","res"],"mappings":"AACO,IAAMA,CAAAA,CAAN,cAAoC,KAAM,CACtC,MAAA,CACA,KACT,WAAA,CAAYC,CAAAA,CAAiBC,CAAAA,CAAiBC,CAAAA,CAAe,CAC3D,KAAA,CAAMF,CAAO,CAAA,CACb,IAAA,CAAK,IAAA,CAAO,uBAAA,CACZ,IAAA,CAAK,MAAA,CAASC,EACd,IAAA,CAAK,IAAA,CAAOC,EACd,CACF,CAAA,CCeO,SAASC,EAAiBC,CAAAA,CAAgD,CAC/E,IAAMC,CAAAA,CAAYD,CAAAA,CAAO,SAAA,EAAa,MAChCE,CAAAA,CAAYF,CAAAA,CAAO,SAAA,GAAe,CAAA,EAA6B,CAAA,CAAA,CAErE,OAAO,CACL,MAAM,OAAA,CAAQG,CAAAA,CAAoC,CAChD,IAAMC,CAAAA,CAAM,MAAMH,CAAAA,CAAUD,CAAAA,CAAO,GAAA,CAAK,CACtC,MAAA,CAAQ,MAAA,CACR,QAAS,CACP,cAAA,CAAgB,kBAAA,CAChB,GAAIA,CAAAA,CAAO,OAAA,EAAW,EACxB,CAAA,CACA,IAAA,CAAM,IAAA,CAAK,SAAA,CAAUE,CAAAA,CAAUC,CAAK,CAAC,CAAA,CACrC,SAAA,CAAW,IAAA,CACX,MAAA,CAAQ,WAAA,CAAY,QAAQH,CAAAA,CAAO,SAAA,EAAa,GAAI,CACtD,CAAC,CAAA,CACD,GAAI,CAACI,CAAAA,CAAI,EAAA,CACP,MAAM,IAAIT,CAAAA,CACR,2BAA2BS,CAAAA,CAAI,MAAM,CAAA,CAAA,EAAIA,CAAAA,CAAI,UAAU,CAAA,CAAA,CACvDA,EAAI,MAAA,CACJ,MAAMA,CAAAA,CAAI,IAAA,EAAK,CAAE,KAAA,CAAM,IAAG,CAAA,CAAY,CACxC,CAEJ,CACF,CACF","file":"webhook.js","sourcesContent":["/** Thrown when the analytics backend rejects, errors, or times out a capture. */\nexport class CaptureTransportError extends Error {\n readonly status: number | undefined\n readonly body: string | undefined\n constructor(message: string, status?: number, body?: string) {\n super(message)\n this.name = 'CaptureTransportError'\n this.status = status\n this.body = body\n }\n}\n","import type { AnalyticsAdapter, CaptureEvent } from '../types.js'\nimport { CaptureTransportError } from '../errors.js'\n\nexport interface WebhookAdapterConfig {\n /** Destination URL that receives a POST for each event. */\n url: string\n /** Extra headers merged onto the POST (useful for shared-secret auth). */\n headers?: Record<string, string>\n /**\n * Transform the event into the exact JSON body the destination expects.\n * Defaults to sending the {@link CaptureEvent} as-is.\n */\n transform?: (event: CaptureEvent) => unknown\n /** Override the `fetch` implementation. */\n fetchImpl?: typeof fetch\n /** Abort the capture after this many milliseconds. Defaults to 3000. */\n timeoutMs?: number\n}\n\n/**\n * Adapter that POSTs each event to an arbitrary webhook URL. Keeps the\n * library analytics-backend-agnostic — use this when PostHog isn't your\n * analytics of record, or when you want to multiplex events through your\n * own ingestion layer.\n */\nexport function webhookAnalytics(config: WebhookAdapterConfig): AnalyticsAdapter {\n const fetchImpl = config.fetchImpl ?? fetch\n const transform = config.transform ?? ((e: CaptureEvent): unknown => e)\n\n return {\n async capture(event: CaptureEvent): Promise<void> {\n const res = await fetchImpl(config.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(config.headers ?? {})\n },\n body: JSON.stringify(transform(event)),\n keepalive: true,\n signal: AbortSignal.timeout(config.timeoutMs ?? 3000)\n })\n if (!res.ok) {\n throw new CaptureTransportError(\n `Webhook capture failed: ${res.status} ${res.statusText}`,\n res.status,\n await res.text().catch(() => undefined)\n )\n }\n }\n }\n}\n"]}