@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/src/core.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest'
2
- import { Analytics } from './core'
2
+ import { Analytics, VERSION } from './core'
3
3
  import { EVENTS, PAGEVIEW } from './events'
4
4
  import type { Transport, WireEvent } from './types'
5
5
 
@@ -7,14 +7,25 @@ interface Sent {
7
7
  url: string
8
8
  beacon: boolean
9
9
  token?: string
10
+ ingestKey?: string
11
+ raw: string
10
12
  batch: WireEvent[]
11
13
  }
12
14
 
15
+ // FakeTransport records the EXACT bytes the client would put on the wire, so the
16
+ // tests assert the real POST /v1/event body shape ({ batch: [...] }), not a mock.
13
17
  class FakeTransport implements Transport {
14
18
  sent: Sent[] = []
15
- send(url: string, body: string, opts: { beacon: boolean; token?: string }) {
19
+ send(url: string, body: string, opts: { beacon: boolean; token?: string; ingestKey?: string }) {
16
20
  const parsed = JSON.parse(body) as { batch: WireEvent[] }
17
- this.sent.push({ url, beacon: opts.beacon, token: opts.token, batch: parsed.batch })
21
+ this.sent.push({
22
+ url,
23
+ beacon: opts.beacon,
24
+ token: opts.token,
25
+ ingestKey: opts.ingestKey,
26
+ raw: body,
27
+ batch: parsed.batch,
28
+ })
18
29
  }
19
30
  get all(): WireEvent[] {
20
31
  return this.sent.flatMap((s) => s.batch)
@@ -22,9 +33,11 @@ class FakeTransport implements Transport {
22
33
  }
23
34
 
24
35
  let tx: FakeTransport
36
+ // Default to same-origin (host:'') so path assertions read the bare /v1/event door;
37
+ // tests that care about the edge host pass it explicitly.
25
38
  function mk(overrides = {}) {
26
39
  tx = new FakeTransport()
27
- return new Analytics({ product: 'console', transport: tx, flushIntervalMs: 999999, ...overrides })
40
+ return new Analytics({ product: 'console', host: '', transport: tx, flushIntervalMs: 999999, ...overrides })
28
41
  }
29
42
 
30
43
  describe('Analytics capture', () => {
@@ -32,23 +45,57 @@ describe('Analytics capture', () => {
32
45
  tx = new FakeTransport()
33
46
  })
34
47
 
35
- it('flushes an event as a {batch:[...]} with product + name, no tenant/org field', () => {
48
+ it('flushes an event as { batch:[] } to /v1/event, no tenant/org field', () => {
36
49
  const a = mk()
37
50
  a.capture(EVENTS.SIGNUP_COMPLETED, { plan: 'pro' })
38
51
  a.flush()
39
52
  expect(tx.sent).toHaveLength(1)
40
- expect(tx.sent[0].url).toBe('/v1/analytics')
53
+ expect(tx.sent[0].url).toBe('/v1/event')
54
+ // The body is the canonical { batch: [...] } envelope, exactly one door.
55
+ const parsed = JSON.parse(tx.sent[0].raw)
56
+ expect(Array.isArray(parsed.batch)).toBe(true)
41
57
  const e = tx.all[0]
42
58
  expect(e.type).toBe('event')
43
59
  expect(e.event).toBe('signup_completed')
44
60
  expect(e.product).toBe('console')
45
61
  expect(e.properties).toEqual({ plan: 'pro' })
46
- // The client must never send a tenant/org — the server stamps it.
62
+ // The client must NEVER send a tenant/org — the server stamps it.
47
63
  expect((e as Record<string, unknown>).tenant).toBeUndefined()
48
64
  expect((e as Record<string, unknown>).org).toBeUndefined()
49
65
  expect((e as Record<string, unknown>).tenantId).toBeUndefined()
50
66
  })
51
67
 
68
+ it('the wire is the canonical /v1/event Event: only known cloud fields, no tenant', () => {
69
+ const a = mk({ host: 'https://api.hanzo.ai' })
70
+ a.identify('user-9')
71
+ a.capture(EVENTS.ORDER_COMPLETED, { kind: 'plan' }, { productId: 'plan_pro', revenue: 49, quantity: 1, currency: 'usd' })
72
+ a.flush()
73
+ // ONE POST, ONE door, ONE batched envelope.
74
+ expect(tx.sent).toHaveLength(1)
75
+ expect(tx.sent[0].url).toBe('https://api.hanzo.ai/v1/event')
76
+ const parsed = JSON.parse(tx.sent[0].raw) as { batch: WireEvent[] }
77
+ expect(Object.keys(parsed)).toEqual(['batch'])
78
+ // Every field the client emits is a known cloud CaptureEvent field — no tenant.
79
+ const allowed = new Set([
80
+ 'messageId', 'type', 'event', 'timestamp', 'distinctId', 'anonymousId',
81
+ 'personId', 'sessionId', 'product', 'url', 'path', 'referrer', 'utm',
82
+ 'refCode', 'channel', 'groupId', 'signupWeek', 'productId', 'quantity',
83
+ 'revenue', 'currency', 'error', 'properties', 'library', 'libraryVersion',
84
+ ])
85
+ for (const ev of parsed.batch) {
86
+ for (const k of Object.keys(ev)) expect(allowed.has(k)).toBe(true)
87
+ expect((ev as Record<string, unknown>).tenant).toBeUndefined()
88
+ expect((ev as Record<string, unknown>).tenantId).toBeUndefined()
89
+ expect(ev.library).toBe('@hanzo/event')
90
+ expect(ev.libraryVersion).toBe(VERSION)
91
+ }
92
+ const order = parsed.batch.find((e) => e.event === EVENTS.ORDER_COMPLETED)!
93
+ expect(order.productId).toBe('plan_pro')
94
+ expect(order.revenue).toBe(49)
95
+ expect(order.quantity).toBe(1)
96
+ expect(order.currency).toBe('usd')
97
+ })
98
+
52
99
  it('pageview emits the reserved $pageview name', () => {
53
100
  const a = mk()
54
101
  a.pageview('/pricing')
@@ -107,16 +154,29 @@ describe('Analytics capture', () => {
107
154
  a.capture('x')
108
155
  a.flush(true) // beacon requested…
109
156
  expect(tx.sent[0].token).toBe('jwt-abc')
110
- expect(tx.sent[0].beacon).toBe(false) // …but a token forces keepalive fetch
111
- expect(tx.sent[0].url).toBe('/v1/analytics')
157
+ expect(tx.sent[0].beacon).toBe(false) // …but a JWT forces keepalive fetch
158
+ expect(tx.sent[0].url).toBe('/v1/event')
159
+ })
160
+
161
+ it('cookie apps beacon to /v1/event on unload flush', () => {
162
+ const a = mk() // no token, no key
163
+ a.capture('x')
164
+ a.flush(true)
165
+ expect(tx.sent[0].beacon).toBe(true)
166
+ expect(tx.sent[0].token).toBeUndefined()
167
+ expect(tx.sent[0].url).toBe('/v1/event')
112
168
  })
113
169
 
114
- it('cookie apps beacon to the tracker route on unload flush', () => {
115
- const a = mk() // no token
170
+ it('publishable-key apps ride ingestKey and still beacon on unload', () => {
171
+ const a = mk({ ingestKey: 'pk_live_123' })
116
172
  a.capture('x')
117
173
  a.flush(true)
174
+ // The key is offered to the transport (rides ?ingest_key on the beacon), and
175
+ // a publishable key does NOT block the unload beacon.
176
+ expect(tx.sent[0].ingestKey).toBe('pk_live_123')
177
+ expect(tx.sent[0].token).toBeUndefined()
118
178
  expect(tx.sent[0].beacon).toBe(true)
119
- expect(tx.sent[0].url).toBe('/v1/tracker')
179
+ expect(tx.sent[0].url).toBe('/v1/event')
120
180
  })
121
181
 
122
182
  it('setCohort rides subsequent events', () => {
@@ -134,30 +194,35 @@ describe('Analytics capture', () => {
134
194
  const a = mk({ host: 'https://api.hanzo.ai' })
135
195
  a.capture('x')
136
196
  a.flush()
137
- expect(tx.sent[0].url).toBe('https://api.hanzo.ai/v1/analytics')
197
+ expect(tx.sent[0].url).toBe('https://api.hanzo.ai/v1/event')
138
198
  })
139
199
 
140
- it('stamps every event with the @hanzo/event library id', () => {
200
+ it('stamps every event with the @hanzo/event library id + version', () => {
141
201
  const a = mk()
142
202
  a.capture('x')
143
203
  a.flush()
144
204
  expect(tx.all[0].library).toBe('@hanzo/event')
205
+ expect(tx.all[0].libraryVersion).toBe(VERSION)
145
206
  })
146
207
  })
147
208
 
148
209
  describe('Event error capture', () => {
149
- it('captureError emits a type:error event with the exception, and flushes at once', () => {
210
+ it('captureError emits a type:error event carrying a TOP-LEVEL exception, flushed at once', () => {
150
211
  const a = mk()
151
212
  a.captureError(new TypeError('boom'))
152
213
  // captureError flushes promptly — no explicit flush() needed.
153
214
  expect(tx.sent).toHaveLength(1)
154
215
  const e = tx.all[0]
216
+ // type:'error' is the field Cloud folds to event_type='error' → sentry lens.
155
217
  expect(e.type).toBe('error')
156
218
  expect(e.event).toBe('boom')
219
+ // The exception rides the TOP-LEVEL `error` field (what cloud foldException
220
+ // reads), NOT properties — a properties-only exception would not be lensed.
157
221
  expect(e.error?.type).toBe('TypeError')
158
222
  expect(e.error?.message).toBe('boom')
159
223
  expect(e.error?.stack).toBeTruthy()
160
224
  expect(e.error?.handled).toBe(true) // a caught, manually-reported error
225
+ expect((e.properties ?? {})).not.toHaveProperty('$exception')
161
226
  })
162
227
 
163
228
  it('normalizes a thrown string into an exception', () => {
@@ -184,11 +249,12 @@ describe('Event error capture', () => {
184
249
  expect(e.error?.message).toBe('via alias')
185
250
  })
186
251
 
187
- it('an error is still an event on the ONE stream — same route + product', () => {
252
+ it('an error is still an event on the ONE stream — same /v1/event door + product', () => {
188
253
  const a = mk({ host: 'https://api.hanzo.ai' })
189
254
  a.captureError(new Error('x'))
190
- // Same batched route as any other event — one pipe, not a second SDK.
191
- expect(tx.sent[0].url).toBe('https://api.hanzo.ai/v1/analytics')
255
+ // Same batched door as any other event — one pipe, not a second SDK.
256
+ expect(tx.sent[0].url).toBe('https://api.hanzo.ai/v1/event')
257
+ expect(tx.sent[0].raw.startsWith('{"batch":')).toBe(true)
192
258
  expect(tx.all[0].product).toBe('console')
193
259
  // never the tenant — the server stamps it, errors included.
194
260
  expect((tx.all[0] as Record<string, unknown>).tenant).toBeUndefined()
package/src/core.ts CHANGED
@@ -1,8 +1,28 @@
1
- // The framework-agnostic event client. Buffers events and flushes them as one
2
- // batch to Hanzo Cloud /v1/analytics normally, /v1/tracker via sendBeacon on
3
- // page unload. It NEVER sends the org/tenant: the server stamps that from the
4
- // validated session. The client only supplies its own visitor identity. Errors
5
- // are just events (type:'error') on the same stream — one client, one pipe.
1
+ // The framework-agnostic event client. Buffers events and flushes them as ONE
2
+ // batch through the ONE Hanzo Cloud ingestion front door:
3
+ //
4
+ // POST {host}/v1/event body: { batch: [Event, …] } -> { accepted, dropped }
5
+ //
6
+ // It NEVER sends the org/tenant: Cloud resolves that server-side (from the
7
+ // validated session, or the signed publishable key) and stamps it. The client
8
+ // only supplies its own visitor identity. Errors are just events (type:'error')
9
+ // on the same stream — one client, one pipe, lensed server-side into product
10
+ // analytics (insights), web analytics (analytics), and error tracking (sentry).
11
+ //
12
+ // Auth is orthogonal — the SAME body to the SAME door, differing only in how the
13
+ // caller proves its tenant:
14
+ //
15
+ // • cookie/session app (host:'') — same-origin credentials ride the request.
16
+ // • bearer app (getToken) — Authorization: Bearer <jwt>.
17
+ // • publishable-key app (ingestKey: 'pk_…') — Authorization: Bearer pk_… on
18
+ // fetch, ?ingest_key=pk_… on a headerless page-unload beacon. Write-only and
19
+ // safe to ship in a bundle; the door HMAC-verifies it to an org server-side.
20
+ //
21
+ // The wire is the canonical `Event` (== the cloud CaptureEvent): its `type` field
22
+ // is what Cloud folds to event_type='error', so a captured exception reaches the
23
+ // error-tracking lens. (A four-field {event,distinctId,time,properties} object has
24
+ // no `type`, so it can never be lensed as an error — this batched Event wire is
25
+ // the one that lights up all three lenses.)
6
26
 
7
27
  import {
8
28
  parseAttribution,
@@ -28,10 +48,16 @@ import type {
28
48
  WireEvent,
29
49
  } from './types'
30
50
 
31
- export const VERSION = '0.2.0'
51
+ export const VERSION = '0.3.0'
32
52
 
33
- const ANALYTICS_PATH = '/v1/analytics'
34
- const TRACKER_PATH = '/v1/tracker' // beacon-on-unload alias
53
+ const EVENT_PATH = '/v1/event' // the ONE canonical ingestion front door
54
+ const DEFAULT_HOST = 'https://api.hanzo.ai' // the one edge; cookie apps pass host:''
55
+
56
+ /** appendQuery adds a single query param to a URL string — used to carry a
57
+ * publishable key on a headerless sendBeacon (?ingest_key=…). */
58
+ function appendQuery(url: string, key: string, value: string): string {
59
+ return url + (url.includes('?') ? '&' : '?') + key + '=' + encodeURIComponent(value)
60
+ }
35
61
 
36
62
  function uid(): string {
37
63
  const c = typeof crypto !== 'undefined' ? crypto : undefined
@@ -55,12 +81,15 @@ function normalizeError(err: unknown): Exception {
55
81
  const isBrowser = () => typeof window !== 'undefined'
56
82
 
57
83
  /** DefaultTransport: fetch(keepalive) for authenticated/normal sends;
58
- * navigator.sendBeacon for headerless page-unload beacons. */
84
+ * navigator.sendBeacon for headerless page-unload beacons. A bearer (a JWT or a
85
+ * publishable pk_ key) rides Authorization on fetch; on a beacon — which cannot
86
+ * set headers — a publishable key rides the ?ingest_key query instead. */
59
87
  class DefaultTransport implements Transport {
60
- send(url: string, body: string, opts: { beacon: boolean; token?: string }): void {
88
+ send(url: string, body: string, opts: { beacon: boolean; token?: string; ingestKey?: string }): void {
61
89
  if (opts.beacon && isBrowser() && typeof navigator.sendBeacon === 'function') {
90
+ const beaconUrl = opts.ingestKey ? appendQuery(url, 'ingest_key', opts.ingestKey) : url
62
91
  try {
63
- navigator.sendBeacon(url, new Blob([body], { type: 'application/json' }))
92
+ navigator.sendBeacon(beaconUrl, new Blob([body], { type: 'application/json' }))
64
93
  return
65
94
  } catch {
66
95
  /* fall through to fetch */
@@ -68,7 +97,8 @@ class DefaultTransport implements Transport {
68
97
  }
69
98
  if (typeof fetch !== 'function') return
70
99
  const headers: Record<string, string> = { 'Content-Type': 'application/json' }
71
- if (opts.token) headers.Authorization = `Bearer ${opts.token}`
100
+ const bearer = opts.ingestKey ?? opts.token
101
+ if (bearer) headers.Authorization = `Bearer ${bearer}`
72
102
  void fetch(url, {
73
103
  method: 'POST',
74
104
  headers,
@@ -76,7 +106,7 @@ class DefaultTransport implements Transport {
76
106
  keepalive: true,
77
107
  credentials: 'include',
78
108
  }).catch(() => {
79
- /* analytics loss is acceptable; never throw into the app */
109
+ /* telemetry loss is acceptable; never throw into the app */
80
110
  })
81
111
  }
82
112
  }
@@ -96,7 +126,7 @@ export class Analytics {
96
126
 
97
127
  constructor(config: AnalyticsConfig) {
98
128
  this.cfg = {
99
- host: '',
129
+ host: DEFAULT_HOST,
100
130
  batchSize: 20,
101
131
  flushIntervalMs: 5000,
102
132
  enabled: true,
@@ -107,8 +137,9 @@ export class Analytics {
107
137
  }
108
138
 
109
139
  /** init is idempotent and browser-only for its side effects: capture first-touch
110
- * attribution, hydrate cohort, and register the unload flush. Safe to call from
111
- * a React effect on every render. */
140
+ * attribution, hydrate cohort, register the unload flush, and (unless opted out)
141
+ * auto-capture unhandled errors. Safe to call from a React effect on every
142
+ * render. */
112
143
  init(): void {
113
144
  if (this.started || !this.cfg.enabled) return
114
145
  this.started = true
@@ -130,7 +161,8 @@ export class Analytics {
130
161
  window.addEventListener('pagehide', () => this.flush(true))
131
162
 
132
163
  // Auto error capture — the drop-in @sentry replacement. Unhandled errors and
133
- // rejected promises become type:'error' events on the same stream.
164
+ // rejected promises become type:'error' events on the same stream, which Cloud
165
+ // stamps event_type='error' → the sentry.hanzo.ai lens.
134
166
  if (this.cfg.captureErrors) {
135
167
  window.addEventListener('error', (e: ErrorEvent) => {
136
168
  this.captureError(e.error ?? e.message, { handled: false })
@@ -174,11 +206,12 @@ export class Analytics {
174
206
  track = this.capture.bind(this)
175
207
 
176
208
  /** captureError records an exception as a first-class error event — the ONE
177
- * error path (subsumes @sentry). A caught error, an unhandled rejection, or a
178
- * manual report all become a type:'error' event on the same stream, lensed to
179
- * the error-tracking view server-side. Never throws back into the app; errors
180
- * are higher-signal than pageviews, so it flushes promptly (a crash may unload
181
- * the page moments later). */
209
+ * error path (subsumes @sentry). A caught error, an unhandled rejection, a
210
+ * React render error, or a manual report all become a type:'error' event on the
211
+ * same stream; Cloud folds the exception into properties.$exception and stamps
212
+ * event_type='error', so it surfaces in the error-tracking lens. Never throws
213
+ * back into the app; errors are higher-signal than pageviews, so it flushes
214
+ * promptly (a crash may unload the page moments later). */
182
215
  captureError(
183
216
  err: unknown,
184
217
  context?: { handled?: boolean; properties?: Record<string, unknown> },
@@ -198,21 +231,32 @@ export class Analytics {
198
231
  this.cohort = mergeCohort(patch)
199
232
  }
200
233
 
201
- /** flush drains the buffer to the server as one batch. beacon=true uses the
202
- * unload-safe path. */
234
+ /** flush drains the buffer to the server as ONE batch through the ONE ingest
235
+ * front door POST /v1/event, body { batch: [Event…] }. beacon=true selects the
236
+ * unload-safe transport. Auth is orthogonal to the wire:
237
+ *
238
+ * • publishable key set → rides Authorization: Bearer pk_… (fetch) or
239
+ * ?ingest_key=pk_… (beacon), so unload beacons work anonymously.
240
+ * • else a bearer JWT rides Authorization (fetch only — sendBeacon cannot
241
+ * carry a header, so token apps fall back to keepalive fetch on unload).
242
+ * • else a cookie app rides same-origin credentials (beacon carries the
243
+ * cookie fine).
244
+ */
203
245
  flush(beacon = false): void {
204
246
  if (!this.cfg.enabled || this.queue.length === 0) return
205
247
  const batch = this.queue
206
248
  this.queue = []
207
249
  this.clearTimer()
208
- const token = this.cfg.getToken?.() ?? undefined
209
- // sendBeacon cannot carry an Authorization header, so token apps always use
210
- // keepalive fetch; cookie apps may beacon to the tracker route on unload.
250
+
251
+ const key = this.cfg.ingestKey?.trim() || undefined
252
+ // A publishable key and a bearer JWT are mutually exclusive doors; the key wins.
253
+ const token = key ? undefined : this.cfg.getToken?.() ?? undefined
254
+ // Only a headerful bearer JWT blocks the beacon: sendBeacon cannot set an
255
+ // Authorization header. A pk_ rides ?ingest_key; a cookie rides credentials.
211
256
  const useBeacon = beacon && !token
212
- const path = useBeacon ? TRACKER_PATH : ANALYTICS_PATH
213
257
  const body = JSON.stringify({ batch })
214
- if (this.cfg.debug) console.debug('[analytics] flush', batch.length, path)
215
- this.transport.send(this.cfg.host + path, body, { beacon: useBeacon, token: token ?? undefined })
258
+ if (this.cfg.debug) console.debug('[event] flush', EVENT_PATH, batch.length)
259
+ this.transport.send(this.cfg.host + EVENT_PATH, body, { beacon: useBeacon, token, ingestKey: key })
216
260
  }
217
261
 
218
262
  // ── internals ────────────────────────────────────────────────────────────
package/src/types.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  // Public types for the Hanzo Event client.
2
2
 
3
3
  /** The event kinds — the closed set the server understands. An error is just
4
- * another event on the one stream (lensed to the error-tracking view). */
4
+ * another event on the one stream (Cloud stamps type:'error' event_type='error',
5
+ * the key the error-tracking lens filters on). */
5
6
  export type EventKind = 'pageview' | 'event' | 'identify' | 'group' | 'error'
6
7
 
7
- /** A captured exception. Carried on a `type:'error'` event; the server lenses it
8
- * into the error-tracking view (sentry.hanzo.ai). */
8
+ /** A captured exception. Carried on a `type:'error'` event's top-level `error`
9
+ * field; Cloud folds it into properties.$exception and lenses the event into the
10
+ * error-tracking view (sentry.hanzo.ai). */
9
11
  export interface Exception {
10
12
  /** Constructor/class name, e.g. "TypeError". */
11
13
  type?: string
@@ -41,8 +43,10 @@ export interface Cohort {
41
43
  refCode?: string
42
44
  }
43
45
 
44
- /** One event as sent on the wire. tenant/org is NEVER set here the server
45
- * stamps it from the validated session. */
46
+ /** One event as sent on the wire the canonical Hanzo Cloud event. Maps 1:1 to
47
+ * the cloud `CaptureEvent` (camelCase JSON keys); a batch of these is POSTed to
48
+ * the ONE front door `/v1/event` as `{ batch: [WireEvent, …] }`. tenant/org is
49
+ * NEVER a field here — the server stamps it from the validated session/key. */
46
50
  export interface WireEvent {
47
51
  messageId: string
48
52
  type: EventKind
@@ -65,28 +69,41 @@ export interface WireEvent {
65
69
  quantity?: number
66
70
  revenue?: number
67
71
  currency?: string
68
- /** Set on `type:'error'` events — the captured exception. */
72
+ /** Set on `type:'error'` events — the captured exception. Cloud lifts it into
73
+ * properties.$exception (foldException) for the error-tracking lens. */
69
74
  error?: Exception
70
75
  properties?: Record<string, unknown>
71
76
  library?: string
72
77
  libraryVersion?: string
73
78
  }
74
79
 
75
- /** Injectable transports — overridden in tests; default in core.ts uses fetch. */
80
+ /** Injectable transports — overridden in tests; the default in core.ts uses fetch
81
+ * (keepalive) and sendBeacon. A bearer JWT or a publishable pk_ key rides
82
+ * Authorization on fetch; on a headerless beacon a publishable key rides the
83
+ * ?ingest_key query. */
76
84
  export interface Transport {
77
85
  /** Durable POST usable during page unload (fetch keepalive / sendBeacon). */
78
- send(url: string, body: string, opts: { beacon: boolean; token?: string }): void
86
+ send(url: string, body: string, opts: { beacon: boolean; token?: string; ingestKey?: string }): void
79
87
  }
80
88
 
81
89
  export interface AnalyticsConfig {
82
- /** Cloud base URL. Same-origin ("") for cookie-auth apps (console/admin);
83
- * e.g. "https://api.hanzo.ai" for bearer apps (app/site). */
90
+ /** Cloud base URL. Defaults to "https://api.hanzo.ai" (the one edge). Set to
91
+ * same-origin ("") for cookie-auth apps served behind the same edge
92
+ * (console/admin/chat), so the browser rides the session cookie. */
84
93
  host?: string
85
94
  /** Emitting surface: console | chat | app | site | admin. */
86
95
  product: string
87
96
  /** Bearer token provider for token-auth apps. Omit for cookie/session apps
88
97
  * (the client then relies on same-origin credentials). */
89
98
  getToken?: () => string | undefined | null
99
+ /** Publishable ingest key (pk_…). When set, the client authenticates to the ONE
100
+ * front door `/v1/event` with this key instead of a bearer/cookie: it rides
101
+ * Authorization: Bearer pk_… on fetch and ?ingest_key=pk_… on a headerless
102
+ * page-unload beacon, so ALL THREE lenses (web + product + error) light up with
103
+ * no bearer and unload beacons work anonymously. The key is write-only (cannot
104
+ * read) and safe to ship in a bundle; mint one per org via POST /v1/ingest/keys.
105
+ * Recommended for marketing/public pages and the full sentry-subsuming setup. */
106
+ ingestKey?: string
90
107
  /** Max events buffered before an automatic flush. */
91
108
  batchSize?: number
92
109
  /** Auto-flush cadence in ms. */
package/LICENSE.md DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2023 hanzo
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.