@hanzo/event 0.2.0 → 0.3.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 +54 -7
- package/dist/{core-DDGwms7M.d.cts → core-CrbiAQhN.d.cts} +48 -19
- package/dist/{core-DDGwms7M.d.ts → core-CrbiAQhN.d.ts} +48 -19
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +35 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +35 -19
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +35 -19
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +35 -19
- package/dist/react.mjs.map +1 -1
- package/package.json +10 -10
- package/src/core.test.ts +84 -18
- package/src/core.ts +74 -30
- package/src/types.ts +27 -10
- package/LICENSE.md +0 -21
package/README.md
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
# @hanzo/event
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`pageview` / `event` / `identify` / `group`
|
|
5
|
-
third-party
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
The ONE telemetry client for Hanzo surfaces. Emits every kind of event —
|
|
4
|
+
`pageview` / `event` / `identify` / `group` **and errors** — to **Hanzo Cloud**,
|
|
5
|
+
never to a third-party. There is exactly one front door:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
POST {host}/v1/event body: { batch: [Event, …] } -> { accepted, dropped }
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Cloud resolves the tenant server-side and fans the one stream out into three
|
|
12
|
+
lenses: **product analytics** (insights.hanzo.ai), **web analytics**
|
|
13
|
+
(analytics.hanzo.ai), and **error tracking** (sentry.hanzo.ai). Errors are just
|
|
14
|
+
events (`type:'error'`) on the same pipe — one client, one door.
|
|
8
15
|
|
|
9
16
|
- **Batched** with a size + interval flush, and **beacon-on-unload**
|
|
10
|
-
(`sendBeacon` for cookie apps, `fetch(keepalive)` for token apps).
|
|
17
|
+
(`sendBeacon` for cookie/publishable-key apps, `fetch(keepalive)` for token apps).
|
|
18
|
+
- **Auto error capture** (subsumes `@sentry`): `window.onerror`,
|
|
19
|
+
`unhandledrejection`, and a React `ErrorBoundary` become `type:'error'` events;
|
|
20
|
+
Cloud stamps `event_type='error'` → the sentry lens. Opt out with
|
|
21
|
+
`captureErrors: false`.
|
|
11
22
|
- **First-touch attribution**: UTM + referrer + `refCode` are parsed once and
|
|
12
23
|
persisted, then attached to every event.
|
|
13
24
|
- **Cohorts**: `signupWeek`, `channel`, `refCode` ride each event.
|
|
14
25
|
- **Tenant-safe**: the client NEVER sends an org/tenant — Cloud stamps it from
|
|
15
|
-
the validated session.
|
|
26
|
+
the validated session or the signed publishable key.
|
|
27
|
+
- **Fail-soft**: telemetry loss is swallowed; the client never throws into the app.
|
|
16
28
|
- **SSR-safe**: importing on the server is a no-op; it only acts in the browser.
|
|
17
29
|
|
|
18
30
|
## Core (framework-agnostic)
|
|
@@ -58,6 +70,41 @@ function UpgradeButton() {
|
|
|
58
70
|
}
|
|
59
71
|
```
|
|
60
72
|
|
|
73
|
+
## Errors (the @sentry replacement)
|
|
74
|
+
|
|
75
|
+
Unhandled errors and promise rejections are captured automatically. React render
|
|
76
|
+
errors never reach `window.onerror`, so wrap your tree in the `ErrorBoundary` to
|
|
77
|
+
catch those too. Report caught errors yourself with `captureError`.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { ErrorBoundary } from '@hanzo/event/react'
|
|
81
|
+
|
|
82
|
+
<AnalyticsProvider config={{ product: 'console' }}>
|
|
83
|
+
<ErrorBoundary fallback={(err, reset) => <Crash error={err} onReset={reset} />}>
|
|
84
|
+
<App />
|
|
85
|
+
</ErrorBoundary>
|
|
86
|
+
</AnalyticsProvider>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
try { risky() } catch (err) { analytics.captureError(err, { properties: { where: 'checkout' } }) }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Each becomes a `type:'error'` event carrying the exception; Cloud folds it into
|
|
94
|
+
`properties.$exception`, stamps `event_type='error'`, and it surfaces in the
|
|
95
|
+
error-tracking lens (`GET /v1/errors` → sentry.hanzo.ai).
|
|
96
|
+
|
|
97
|
+
## Publishable key (public pages, no bearer)
|
|
98
|
+
|
|
99
|
+
Marketing/public pages have no session. Mint a write-only publishable key
|
|
100
|
+
(`POST /v1/ingest/keys`) and pass it as `ingestKey`; it rides `Authorization`
|
|
101
|
+
on fetch and `?ingest_key` on an unload beacon, so all three lenses light up
|
|
102
|
+
anonymously. It is safe to ship in a bundle (write-only, cannot read).
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
createAnalytics({ product: 'site', host: 'https://api.hanzo.ai', ingestKey: 'pk_live_…' })
|
|
106
|
+
```
|
|
107
|
+
|
|
61
108
|
## Goals & cohorts
|
|
62
109
|
|
|
63
110
|
`GOALS` and `COHORTS` (see `goals.ts`) are the shared, machine-readable insights
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** The event kinds — the closed set the server understands. An error is just
|
|
2
|
-
* another event on the one stream (
|
|
2
|
+
* another event on the one stream (Cloud stamps type:'error' → event_type='error',
|
|
3
|
+
* the key the error-tracking lens filters on). */
|
|
3
4
|
type EventKind = 'pageview' | 'event' | 'identify' | 'group' | 'error';
|
|
4
|
-
/** A captured exception. Carried on a `type:'error'` event
|
|
5
|
-
* into the
|
|
5
|
+
/** A captured exception. Carried on a `type:'error'` event's top-level `error`
|
|
6
|
+
* field; Cloud folds it into properties.$exception and lenses the event into the
|
|
7
|
+
* error-tracking view (sentry.hanzo.ai). */
|
|
6
8
|
interface Exception {
|
|
7
9
|
/** Constructor/class name, e.g. "TypeError". */
|
|
8
10
|
type?: string;
|
|
@@ -35,8 +37,10 @@ interface Cohort {
|
|
|
35
37
|
channel?: string;
|
|
36
38
|
refCode?: string;
|
|
37
39
|
}
|
|
38
|
-
/** One event as sent on the wire
|
|
39
|
-
*
|
|
40
|
+
/** One event as sent on the wire — the canonical Hanzo Cloud event. Maps 1:1 to
|
|
41
|
+
* the cloud `CaptureEvent` (camelCase JSON keys); a batch of these is POSTed to
|
|
42
|
+
* the ONE front door `/v1/event` as `{ batch: [WireEvent, …] }`. tenant/org is
|
|
43
|
+
* NEVER a field here — the server stamps it from the validated session/key. */
|
|
40
44
|
interface WireEvent {
|
|
41
45
|
messageId: string;
|
|
42
46
|
type: EventKind;
|
|
@@ -59,29 +63,43 @@ interface WireEvent {
|
|
|
59
63
|
quantity?: number;
|
|
60
64
|
revenue?: number;
|
|
61
65
|
currency?: string;
|
|
62
|
-
/** Set on `type:'error'` events — the captured exception.
|
|
66
|
+
/** Set on `type:'error'` events — the captured exception. Cloud lifts it into
|
|
67
|
+
* properties.$exception (foldException) for the error-tracking lens. */
|
|
63
68
|
error?: Exception;
|
|
64
69
|
properties?: Record<string, unknown>;
|
|
65
70
|
library?: string;
|
|
66
71
|
libraryVersion?: string;
|
|
67
72
|
}
|
|
68
|
-
/** Injectable transports — overridden in tests; default in core.ts uses fetch
|
|
73
|
+
/** Injectable transports — overridden in tests; the default in core.ts uses fetch
|
|
74
|
+
* (keepalive) and sendBeacon. A bearer JWT or a publishable pk_ key rides
|
|
75
|
+
* Authorization on fetch; on a headerless beacon a publishable key rides the
|
|
76
|
+
* ?ingest_key query. */
|
|
69
77
|
interface Transport {
|
|
70
78
|
/** Durable POST usable during page unload (fetch keepalive / sendBeacon). */
|
|
71
79
|
send(url: string, body: string, opts: {
|
|
72
80
|
beacon: boolean;
|
|
73
81
|
token?: string;
|
|
82
|
+
ingestKey?: string;
|
|
74
83
|
}): void;
|
|
75
84
|
}
|
|
76
85
|
interface AnalyticsConfig {
|
|
77
|
-
/** Cloud base URL.
|
|
78
|
-
*
|
|
86
|
+
/** Cloud base URL. Defaults to "https://api.hanzo.ai" (the one edge). Set to
|
|
87
|
+
* same-origin ("") for cookie-auth apps served behind the same edge
|
|
88
|
+
* (console/admin/chat), so the browser rides the session cookie. */
|
|
79
89
|
host?: string;
|
|
80
90
|
/** Emitting surface: console | chat | app | site | admin. */
|
|
81
91
|
product: string;
|
|
82
92
|
/** Bearer token provider for token-auth apps. Omit for cookie/session apps
|
|
83
93
|
* (the client then relies on same-origin credentials). */
|
|
84
94
|
getToken?: () => string | undefined | null;
|
|
95
|
+
/** Publishable ingest key (pk_…). When set, the client authenticates to the ONE
|
|
96
|
+
* front door `/v1/event` with this key instead of a bearer/cookie: it rides
|
|
97
|
+
* Authorization: Bearer pk_… on fetch and ?ingest_key=pk_… on a headerless
|
|
98
|
+
* page-unload beacon, so ALL THREE lenses (web + product + error) light up with
|
|
99
|
+
* no bearer and unload beacons work anonymously. The key is write-only (cannot
|
|
100
|
+
* read) and safe to ship in a bundle; mint one per org via POST /v1/ingest/keys.
|
|
101
|
+
* Recommended for marketing/public pages and the full sentry-subsuming setup. */
|
|
102
|
+
ingestKey?: string;
|
|
85
103
|
/** Max events buffered before an automatic flush. */
|
|
86
104
|
batchSize?: number;
|
|
87
105
|
/** Auto-flush cadence in ms. */
|
|
@@ -98,7 +116,7 @@ interface AnalyticsConfig {
|
|
|
98
116
|
debug?: boolean;
|
|
99
117
|
}
|
|
100
118
|
|
|
101
|
-
declare const VERSION = "0.
|
|
119
|
+
declare const VERSION = "0.3.0";
|
|
102
120
|
declare class Analytics {
|
|
103
121
|
private cfg;
|
|
104
122
|
private transport;
|
|
@@ -110,8 +128,9 @@ declare class Analytics {
|
|
|
110
128
|
private started;
|
|
111
129
|
constructor(config: AnalyticsConfig);
|
|
112
130
|
/** init is idempotent and browser-only for its side effects: capture first-touch
|
|
113
|
-
* attribution, hydrate cohort,
|
|
114
|
-
* a React effect on every
|
|
131
|
+
* attribution, hydrate cohort, register the unload flush, and (unless opted out)
|
|
132
|
+
* auto-capture unhandled errors. Safe to call from a React effect on every
|
|
133
|
+
* render. */
|
|
115
134
|
init(): void;
|
|
116
135
|
/** identify binds the current visitor to a stable person id (post-login). */
|
|
117
136
|
identify(personId: string, traits?: Record<string, unknown>): void;
|
|
@@ -126,11 +145,12 @@ declare class Analytics {
|
|
|
126
145
|
/** track is an alias of capture (Segment familiarity). */
|
|
127
146
|
track: (event: string, properties?: Record<string, unknown>, commerce?: Pick<WireEvent, "productId" | "quantity" | "revenue" | "currency">) => void;
|
|
128
147
|
/** captureError records an exception as a first-class error event — the ONE
|
|
129
|
-
* error path (subsumes @sentry). A caught error, an unhandled rejection,
|
|
130
|
-
* manual report all become a type:'error' event on the
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* the
|
|
148
|
+
* error path (subsumes @sentry). A caught error, an unhandled rejection, a
|
|
149
|
+
* React render error, or a manual report all become a type:'error' event on the
|
|
150
|
+
* same stream; Cloud folds the exception into properties.$exception and stamps
|
|
151
|
+
* event_type='error', so it surfaces in the error-tracking lens. Never throws
|
|
152
|
+
* back into the app; errors are higher-signal than pageviews, so it flushes
|
|
153
|
+
* promptly (a crash may unload the page moments later). */
|
|
134
154
|
captureError(err: unknown, context?: {
|
|
135
155
|
handled?: boolean;
|
|
136
156
|
properties?: Record<string, unknown>;
|
|
@@ -143,8 +163,17 @@ declare class Analytics {
|
|
|
143
163
|
/** setCohort persists cohort dimensions (e.g. signupWeek at signup) so they ride
|
|
144
164
|
* every subsequent event. */
|
|
145
165
|
setCohort(patch: Cohort): void;
|
|
146
|
-
/** flush drains the buffer to the server as
|
|
147
|
-
*
|
|
166
|
+
/** flush drains the buffer to the server as ONE batch through the ONE ingest
|
|
167
|
+
* front door POST /v1/event, body { batch: [Event…] }. beacon=true selects the
|
|
168
|
+
* unload-safe transport. Auth is orthogonal to the wire:
|
|
169
|
+
*
|
|
170
|
+
* • publishable key set → rides Authorization: Bearer pk_… (fetch) or
|
|
171
|
+
* ?ingest_key=pk_… (beacon), so unload beacons work anonymously.
|
|
172
|
+
* • else a bearer JWT rides Authorization (fetch only — sendBeacon cannot
|
|
173
|
+
* carry a header, so token apps fall back to keepalive fetch on unload).
|
|
174
|
+
* • else a cookie app rides same-origin credentials (beacon carries the
|
|
175
|
+
* cookie fine).
|
|
176
|
+
*/
|
|
148
177
|
flush(beacon?: boolean): void;
|
|
149
178
|
private enqueue;
|
|
150
179
|
private build;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** The event kinds — the closed set the server understands. An error is just
|
|
2
|
-
* another event on the one stream (
|
|
2
|
+
* another event on the one stream (Cloud stamps type:'error' → event_type='error',
|
|
3
|
+
* the key the error-tracking lens filters on). */
|
|
3
4
|
type EventKind = 'pageview' | 'event' | 'identify' | 'group' | 'error';
|
|
4
|
-
/** A captured exception. Carried on a `type:'error'` event
|
|
5
|
-
* into the
|
|
5
|
+
/** A captured exception. Carried on a `type:'error'` event's top-level `error`
|
|
6
|
+
* field; Cloud folds it into properties.$exception and lenses the event into the
|
|
7
|
+
* error-tracking view (sentry.hanzo.ai). */
|
|
6
8
|
interface Exception {
|
|
7
9
|
/** Constructor/class name, e.g. "TypeError". */
|
|
8
10
|
type?: string;
|
|
@@ -35,8 +37,10 @@ interface Cohort {
|
|
|
35
37
|
channel?: string;
|
|
36
38
|
refCode?: string;
|
|
37
39
|
}
|
|
38
|
-
/** One event as sent on the wire
|
|
39
|
-
*
|
|
40
|
+
/** One event as sent on the wire — the canonical Hanzo Cloud event. Maps 1:1 to
|
|
41
|
+
* the cloud `CaptureEvent` (camelCase JSON keys); a batch of these is POSTed to
|
|
42
|
+
* the ONE front door `/v1/event` as `{ batch: [WireEvent, …] }`. tenant/org is
|
|
43
|
+
* NEVER a field here — the server stamps it from the validated session/key. */
|
|
40
44
|
interface WireEvent {
|
|
41
45
|
messageId: string;
|
|
42
46
|
type: EventKind;
|
|
@@ -59,29 +63,43 @@ interface WireEvent {
|
|
|
59
63
|
quantity?: number;
|
|
60
64
|
revenue?: number;
|
|
61
65
|
currency?: string;
|
|
62
|
-
/** Set on `type:'error'` events — the captured exception.
|
|
66
|
+
/** Set on `type:'error'` events — the captured exception. Cloud lifts it into
|
|
67
|
+
* properties.$exception (foldException) for the error-tracking lens. */
|
|
63
68
|
error?: Exception;
|
|
64
69
|
properties?: Record<string, unknown>;
|
|
65
70
|
library?: string;
|
|
66
71
|
libraryVersion?: string;
|
|
67
72
|
}
|
|
68
|
-
/** Injectable transports — overridden in tests; default in core.ts uses fetch
|
|
73
|
+
/** Injectable transports — overridden in tests; the default in core.ts uses fetch
|
|
74
|
+
* (keepalive) and sendBeacon. A bearer JWT or a publishable pk_ key rides
|
|
75
|
+
* Authorization on fetch; on a headerless beacon a publishable key rides the
|
|
76
|
+
* ?ingest_key query. */
|
|
69
77
|
interface Transport {
|
|
70
78
|
/** Durable POST usable during page unload (fetch keepalive / sendBeacon). */
|
|
71
79
|
send(url: string, body: string, opts: {
|
|
72
80
|
beacon: boolean;
|
|
73
81
|
token?: string;
|
|
82
|
+
ingestKey?: string;
|
|
74
83
|
}): void;
|
|
75
84
|
}
|
|
76
85
|
interface AnalyticsConfig {
|
|
77
|
-
/** Cloud base URL.
|
|
78
|
-
*
|
|
86
|
+
/** Cloud base URL. Defaults to "https://api.hanzo.ai" (the one edge). Set to
|
|
87
|
+
* same-origin ("") for cookie-auth apps served behind the same edge
|
|
88
|
+
* (console/admin/chat), so the browser rides the session cookie. */
|
|
79
89
|
host?: string;
|
|
80
90
|
/** Emitting surface: console | chat | app | site | admin. */
|
|
81
91
|
product: string;
|
|
82
92
|
/** Bearer token provider for token-auth apps. Omit for cookie/session apps
|
|
83
93
|
* (the client then relies on same-origin credentials). */
|
|
84
94
|
getToken?: () => string | undefined | null;
|
|
95
|
+
/** Publishable ingest key (pk_…). When set, the client authenticates to the ONE
|
|
96
|
+
* front door `/v1/event` with this key instead of a bearer/cookie: it rides
|
|
97
|
+
* Authorization: Bearer pk_… on fetch and ?ingest_key=pk_… on a headerless
|
|
98
|
+
* page-unload beacon, so ALL THREE lenses (web + product + error) light up with
|
|
99
|
+
* no bearer and unload beacons work anonymously. The key is write-only (cannot
|
|
100
|
+
* read) and safe to ship in a bundle; mint one per org via POST /v1/ingest/keys.
|
|
101
|
+
* Recommended for marketing/public pages and the full sentry-subsuming setup. */
|
|
102
|
+
ingestKey?: string;
|
|
85
103
|
/** Max events buffered before an automatic flush. */
|
|
86
104
|
batchSize?: number;
|
|
87
105
|
/** Auto-flush cadence in ms. */
|
|
@@ -98,7 +116,7 @@ interface AnalyticsConfig {
|
|
|
98
116
|
debug?: boolean;
|
|
99
117
|
}
|
|
100
118
|
|
|
101
|
-
declare const VERSION = "0.
|
|
119
|
+
declare const VERSION = "0.3.0";
|
|
102
120
|
declare class Analytics {
|
|
103
121
|
private cfg;
|
|
104
122
|
private transport;
|
|
@@ -110,8 +128,9 @@ declare class Analytics {
|
|
|
110
128
|
private started;
|
|
111
129
|
constructor(config: AnalyticsConfig);
|
|
112
130
|
/** init is idempotent and browser-only for its side effects: capture first-touch
|
|
113
|
-
* attribution, hydrate cohort,
|
|
114
|
-
* a React effect on every
|
|
131
|
+
* attribution, hydrate cohort, register the unload flush, and (unless opted out)
|
|
132
|
+
* auto-capture unhandled errors. Safe to call from a React effect on every
|
|
133
|
+
* render. */
|
|
115
134
|
init(): void;
|
|
116
135
|
/** identify binds the current visitor to a stable person id (post-login). */
|
|
117
136
|
identify(personId: string, traits?: Record<string, unknown>): void;
|
|
@@ -126,11 +145,12 @@ declare class Analytics {
|
|
|
126
145
|
/** track is an alias of capture (Segment familiarity). */
|
|
127
146
|
track: (event: string, properties?: Record<string, unknown>, commerce?: Pick<WireEvent, "productId" | "quantity" | "revenue" | "currency">) => void;
|
|
128
147
|
/** captureError records an exception as a first-class error event — the ONE
|
|
129
|
-
* error path (subsumes @sentry). A caught error, an unhandled rejection,
|
|
130
|
-
* manual report all become a type:'error' event on the
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* the
|
|
148
|
+
* error path (subsumes @sentry). A caught error, an unhandled rejection, a
|
|
149
|
+
* React render error, or a manual report all become a type:'error' event on the
|
|
150
|
+
* same stream; Cloud folds the exception into properties.$exception and stamps
|
|
151
|
+
* event_type='error', so it surfaces in the error-tracking lens. Never throws
|
|
152
|
+
* back into the app; errors are higher-signal than pageviews, so it flushes
|
|
153
|
+
* promptly (a crash may unload the page moments later). */
|
|
134
154
|
captureError(err: unknown, context?: {
|
|
135
155
|
handled?: boolean;
|
|
136
156
|
properties?: Record<string, unknown>;
|
|
@@ -143,8 +163,17 @@ declare class Analytics {
|
|
|
143
163
|
/** setCohort persists cohort dimensions (e.g. signupWeek at signup) so they ride
|
|
144
164
|
* every subsequent event. */
|
|
145
165
|
setCohort(patch: Cohort): void;
|
|
146
|
-
/** flush drains the buffer to the server as
|
|
147
|
-
*
|
|
166
|
+
/** flush drains the buffer to the server as ONE batch through the ONE ingest
|
|
167
|
+
* front door POST /v1/event, body { batch: [Event…] }. beacon=true selects the
|
|
168
|
+
* unload-safe transport. Auth is orthogonal to the wire:
|
|
169
|
+
*
|
|
170
|
+
* • publishable key set → rides Authorization: Bearer pk_… (fetch) or
|
|
171
|
+
* ?ingest_key=pk_… (beacon), so unload beacons work anonymously.
|
|
172
|
+
* • else a bearer JWT rides Authorization (fetch only — sendBeacon cannot
|
|
173
|
+
* carry a header, so token apps fall back to keepalive fetch on unload).
|
|
174
|
+
* • else a cookie app rides same-origin credentials (beacon carries the
|
|
175
|
+
* cookie fine).
|
|
176
|
+
*/
|
|
148
177
|
flush(beacon?: boolean): void;
|
|
149
178
|
private enqueue;
|
|
150
179
|
private build;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as Cohort, A as Attribution } from './core-
|
|
2
|
-
export { a as Analytics, b as AnalyticsConfig, E as EventKind, c as Exception, T as Transport, V as VERSION, W as WireEvent, d as createAnalytics } from './core-
|
|
1
|
+
import { C as Cohort, A as Attribution } from './core-CrbiAQhN.cjs';
|
|
2
|
+
export { a as Analytics, b as AnalyticsConfig, E as EventKind, c as Exception, T as Transport, V as VERSION, W as WireEvent, d as createAnalytics } from './core-CrbiAQhN.cjs';
|
|
3
3
|
|
|
4
4
|
/** Read the persisted first-touch attribution. */
|
|
5
5
|
declare function getFirstTouch(): Attribution | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as Cohort, A as Attribution } from './core-
|
|
2
|
-
export { a as Analytics, b as AnalyticsConfig, E as EventKind, c as Exception, T as Transport, V as VERSION, W as WireEvent, d as createAnalytics } from './core-
|
|
1
|
+
import { C as Cohort, A as Attribution } from './core-CrbiAQhN.js';
|
|
2
|
+
export { a as Analytics, b as AnalyticsConfig, E as EventKind, c as Exception, T as Transport, V as VERSION, W as WireEvent, d as createAnalytics } from './core-CrbiAQhN.js';
|
|
3
3
|
|
|
4
4
|
/** Read the persisted first-touch attribution. */
|
|
5
5
|
declare function getFirstTouch(): Attribution | undefined;
|
package/dist/index.js
CHANGED
|
@@ -193,9 +193,12 @@ function mergeCohort(patch) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// src/core.ts
|
|
196
|
-
var VERSION = "0.
|
|
197
|
-
var
|
|
198
|
-
var
|
|
196
|
+
var VERSION = "0.3.0";
|
|
197
|
+
var EVENT_PATH = "/v1/event";
|
|
198
|
+
var DEFAULT_HOST = "https://api.hanzo.ai";
|
|
199
|
+
function appendQuery(url, key, value) {
|
|
200
|
+
return url + (url.includes("?") ? "&" : "?") + key + "=" + encodeURIComponent(value);
|
|
201
|
+
}
|
|
199
202
|
function uid2() {
|
|
200
203
|
const c = typeof crypto !== "undefined" ? crypto : void 0;
|
|
201
204
|
if (c && "randomUUID" in c) return c.randomUUID();
|
|
@@ -216,15 +219,17 @@ var isBrowser = () => typeof window !== "undefined";
|
|
|
216
219
|
var DefaultTransport = class {
|
|
217
220
|
send(url, body, opts) {
|
|
218
221
|
if (opts.beacon && isBrowser() && typeof navigator.sendBeacon === "function") {
|
|
222
|
+
const beaconUrl = opts.ingestKey ? appendQuery(url, "ingest_key", opts.ingestKey) : url;
|
|
219
223
|
try {
|
|
220
|
-
navigator.sendBeacon(
|
|
224
|
+
navigator.sendBeacon(beaconUrl, new Blob([body], { type: "application/json" }));
|
|
221
225
|
return;
|
|
222
226
|
} catch {
|
|
223
227
|
}
|
|
224
228
|
}
|
|
225
229
|
if (typeof fetch !== "function") return;
|
|
226
230
|
const headers = { "Content-Type": "application/json" };
|
|
227
|
-
|
|
231
|
+
const bearer = opts.ingestKey ?? opts.token;
|
|
232
|
+
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
228
233
|
void fetch(url, {
|
|
229
234
|
method: "POST",
|
|
230
235
|
headers,
|
|
@@ -247,7 +252,7 @@ var Analytics = class {
|
|
|
247
252
|
/** captureException — @sentry-familiar alias of captureError. */
|
|
248
253
|
this.captureException = this.captureError.bind(this);
|
|
249
254
|
this.cfg = {
|
|
250
|
-
host:
|
|
255
|
+
host: DEFAULT_HOST,
|
|
251
256
|
batchSize: 20,
|
|
252
257
|
flushIntervalMs: 5e3,
|
|
253
258
|
enabled: true,
|
|
@@ -257,8 +262,9 @@ var Analytics = class {
|
|
|
257
262
|
this.transport = config.transport ?? new DefaultTransport();
|
|
258
263
|
}
|
|
259
264
|
/** init is idempotent and browser-only for its side effects: capture first-touch
|
|
260
|
-
* attribution, hydrate cohort,
|
|
261
|
-
* a React effect on every
|
|
265
|
+
* attribution, hydrate cohort, register the unload flush, and (unless opted out)
|
|
266
|
+
* auto-capture unhandled errors. Safe to call from a React effect on every
|
|
267
|
+
* render. */
|
|
262
268
|
init() {
|
|
263
269
|
if (this.started || !this.cfg.enabled) return;
|
|
264
270
|
this.started = true;
|
|
@@ -305,11 +311,12 @@ var Analytics = class {
|
|
|
305
311
|
this.enqueue("event", event, { properties, ...commerce });
|
|
306
312
|
}
|
|
307
313
|
/** captureError records an exception as a first-class error event — the ONE
|
|
308
|
-
* error path (subsumes @sentry). A caught error, an unhandled rejection,
|
|
309
|
-
* manual report all become a type:'error' event on the
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* the
|
|
314
|
+
* error path (subsumes @sentry). A caught error, an unhandled rejection, a
|
|
315
|
+
* React render error, or a manual report all become a type:'error' event on the
|
|
316
|
+
* same stream; Cloud folds the exception into properties.$exception and stamps
|
|
317
|
+
* event_type='error', so it surfaces in the error-tracking lens. Never throws
|
|
318
|
+
* back into the app; errors are higher-signal than pageviews, so it flushes
|
|
319
|
+
* promptly (a crash may unload the page moments later). */
|
|
313
320
|
captureError(err, context) {
|
|
314
321
|
const ex = normalizeError(err);
|
|
315
322
|
ex.handled = context?.handled ?? true;
|
|
@@ -321,19 +328,28 @@ var Analytics = class {
|
|
|
321
328
|
setCohort(patch) {
|
|
322
329
|
this.cohort = mergeCohort(patch);
|
|
323
330
|
}
|
|
324
|
-
/** flush drains the buffer to the server as
|
|
325
|
-
*
|
|
331
|
+
/** flush drains the buffer to the server as ONE batch through the ONE ingest
|
|
332
|
+
* front door POST /v1/event, body { batch: [Event…] }. beacon=true selects the
|
|
333
|
+
* unload-safe transport. Auth is orthogonal to the wire:
|
|
334
|
+
*
|
|
335
|
+
* • publishable key set → rides Authorization: Bearer pk_… (fetch) or
|
|
336
|
+
* ?ingest_key=pk_… (beacon), so unload beacons work anonymously.
|
|
337
|
+
* • else a bearer JWT rides Authorization (fetch only — sendBeacon cannot
|
|
338
|
+
* carry a header, so token apps fall back to keepalive fetch on unload).
|
|
339
|
+
* • else a cookie app rides same-origin credentials (beacon carries the
|
|
340
|
+
* cookie fine).
|
|
341
|
+
*/
|
|
326
342
|
flush(beacon = false) {
|
|
327
343
|
if (!this.cfg.enabled || this.queue.length === 0) return;
|
|
328
344
|
const batch = this.queue;
|
|
329
345
|
this.queue = [];
|
|
330
346
|
this.clearTimer();
|
|
331
|
-
const
|
|
347
|
+
const key = this.cfg.ingestKey?.trim() || void 0;
|
|
348
|
+
const token = key ? void 0 : this.cfg.getToken?.() ?? void 0;
|
|
332
349
|
const useBeacon = beacon && !token;
|
|
333
|
-
const path = useBeacon ? TRACKER_PATH : ANALYTICS_PATH;
|
|
334
350
|
const body = JSON.stringify({ batch });
|
|
335
|
-
if (this.cfg.debug) console.debug("[
|
|
336
|
-
this.transport.send(this.cfg.host +
|
|
351
|
+
if (this.cfg.debug) console.debug("[event] flush \u2192", EVENT_PATH, batch.length);
|
|
352
|
+
this.transport.send(this.cfg.host + EVENT_PATH, body, { beacon: useBeacon, token, ingestKey: key });
|
|
337
353
|
}
|
|
338
354
|
// ── internals ────────────────────────────────────────────────────────────
|
|
339
355
|
enqueue(kind, event, extra) {
|