@hanzo/event 0.3.48 → 0.3.50

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/dist/tags.d.ts CHANGED
@@ -22,6 +22,14 @@ export interface TagOptions {
22
22
  * given a key.
23
23
  */
24
24
  export declare const SITE_KEY: Readonly<Record<string, string>>;
25
+ /**
26
+ * Resolves once every tag this visitor's choice loads has taken the page view, or after
27
+ * `limit` ms, whichever is first: the tag set has answered, gtag.js has loaded or failed,
28
+ * and Meta's library has replaced the stub that queued the call. A page that is about to
29
+ * leave waits on this so the visit is counted where it arrived. A tag the choice does not
30
+ * load is not waited for.
31
+ */
32
+ export declare function ready(limit?: number): Promise<void>;
25
33
  /**
26
34
  * Starts the tag manager: fetches the site's tag set and loads what consent
27
35
  * allows, again on every consent change. Safe to call on every page load, and
@@ -1 +1 @@
1
- {"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAEvC,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUpD,CAAA;AAuNF;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,CAAC,GAAE,UAAe,GAAG,MAAM,IAAI,CAmCpD;AAOD,uFAAuF;AACvF,wBAAgB,KAAK,IAAI,MAAM,CAc9B;AA+CD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,SAAU,GAAG,IAAI,CAiCpG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CACnB,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;AACpC,kGAAkG;AAClG,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CAsBN;AAED,gGAAgG;AAChG,wBAAgB,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAG/C"}
1
+ {"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAEvC,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAepD,CAAA;AAqCF;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,KAAK,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjD;AAoLD;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,CAAC,GAAE,UAAe,GAAG,MAAM,IAAI,CAmCpD;AAOD,uFAAuF;AACvF,wBAAgB,KAAK,IAAI,MAAM,CAc9B;AA+CD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,SAAU,GAAG,IAAI,CAiCpG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CACnB,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;AACpC,kGAAkG;AAClG,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CAsBN;AAED,gGAAgG;AAChG,wBAAgB,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAG/C"}
@@ -0,0 +1,15 @@
1
+ import type { SentryFrame } from './types';
2
+ /**
3
+ * framesFromStack parses a browser Error.stack into Sentry frames, OLDEST-FIRST
4
+ * (Sentry orders caller->callee; the crash site is last — matching the server's
5
+ * pickCrashFrame). Handles both V8 ("at fn (file:li:co)") and
6
+ * Firefox/Safari ("fn@file:li:co"). Unparseable lines are skipped.
7
+ */
8
+ export declare function framesFromStack(stack: string | undefined): SentryFrame[];
9
+ /** normalizeError coerces an unknown throwable into {name, message, stack}. */
10
+ export declare function normalizeError(err: unknown): {
11
+ name: string;
12
+ message: string;
13
+ stack?: string;
14
+ };
15
+ //# sourceMappingURL=throwable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throwable.d.ts","sourceRoot":"","sources":["../src/throwable.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAuB1C;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,EAAE,CAkDxE;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAoB9F"}
package/dist/types.d.ts CHANGED
@@ -1,12 +1,7 @@
1
- /** The event kinds — the closed set the server understands. `error` marks the
2
- * breadcrumb an exception leaves on the event stream (Cloud stamps
3
- * event_type='error' for the warehouse, GET /v1/errors). It does NOT reach the
4
- * Sentry dashboard — the envelope on the error plane does that. */
1
+ /** The event kinds — the closed set the server understands. `error` is a captured
2
+ * exception; the server files it on the error plane. */
5
3
  export type EventKind = 'pageview' | 'event' | 'identify' | 'group' | 'error';
6
- /** A captured exception as it rides the EVENT STREAM — Cloud folds it into
7
- * properties.$exception for the warehouse. The richer copy (parsed stack frames,
8
- * grouping, release) travels on the error plane as a Sentry envelope; see
9
- * AnalyticsConfig.dsn. */
4
+ /** A captured exception, as the `error` field of a `type:'error'` event. */
10
5
  export interface Exception {
11
6
  /** Constructor/class name, e.g. "TypeError". */
12
7
  type?: string;
@@ -17,6 +12,13 @@ export interface Exception {
17
12
  /** false = an unhandled/global error (window.onerror, unhandledrejection);
18
13
  * true = a caught error the app chose to report. Defaults true. */
19
14
  handled?: boolean;
15
+ /** The structured stack, oldest first. */
16
+ frames?: {
17
+ function?: string;
18
+ file?: string;
19
+ line?: number;
20
+ column?: number;
21
+ }[];
20
22
  }
21
23
  /** One stack frame as Error Tracking renders it. The key names are the product's
22
24
  * POST-symbolication vocabulary (`mangled_name`/`source`/`line`/`column`), which
@@ -121,9 +123,13 @@ export interface WireEvent {
121
123
  revenue?: number;
122
124
  currency?: string;
123
125
  /** Set on `type:'error'` events — the captured exception. Cloud lifts it into
124
- * properties.$exception (foldException) for the event warehouse. Not a Sentry
125
- * path: the envelope on the error plane is what feeds the dashboard. */
126
+ * properties.$exception (foldException) for the event warehouse. The
127
+ * server files it on the error plane. */
126
128
  error?: Exception;
129
+ level?: string;
130
+ release?: string;
131
+ environment?: string;
132
+ site?: string;
127
133
  properties?: Record<string, unknown>;
128
134
  library?: string;
129
135
  libraryVersion?: string;
@@ -166,8 +172,7 @@ export interface AnalyticsConfig {
166
172
  * POST /v1/keys {"type":"publishable"}.
167
173
  *
168
174
  * Omit it and the client reads NEXT_PUBLIC_PUBLISHABLE_KEY from the inlined
169
- * build env, the same way `dsn` falls back — so a surface declares BOTH planes
170
- * in its build and neither needs code to switch on. That is the ONE spelling
175
+ * build env, That is the ONE spelling
171
176
  * the fleet already carries: KMS holds deploy/PUBLISHABLE_KEY, and each
172
177
  * Dockerfile takes PUBLISHABLE_KEY as a build-arg and re-exports it with the
173
178
  * NEXT_PUBLIC_ prefix that makes Next inline it.
@@ -177,9 +182,7 @@ export interface AnalyticsConfig {
177
182
  * edge refuses an unattributable write rather than filing it where its owner
178
183
  * cannot read it. That failure is invisible from the page, which is why the key
179
184
  * belongs in the env next to the DSN and not in a checklist.
180
- *
181
- * This attributes the EVENT STREAM only — the error plane authenticates
182
- * independently with `dsn`, and one does not stand in for the other. */
185
+ */
183
186
  ingestKey?: string;
184
187
  /** Max events buffered before an automatic flush. */
185
188
  batchSize?: number;
@@ -188,21 +191,12 @@ export interface AnalyticsConfig {
188
191
  /** Turn the client off entirely (e.g. opt-out / DNT). Defaults to enabled. */
189
192
  enabled?: boolean;
190
193
  /** Auto-capture unhandled errors + promise rejections (window.onerror,
191
- * unhandledrejection). Browser-only, defaults to enabled. Together with `dsn`
192
- * this is what makes the client a drop-in @sentry replacement — without a
193
- * `dsn` the captures never reach the Sentry dashboard. */
194
+ * unhandledrejection). Browser-only, defaults to enabled. */
194
195
  captureErrors?: boolean;
195
196
  /** Override the transport (tests). */
196
197
  transport?: Transport;
197
198
  /** Debug logging. */
198
199
  debug?: boolean;
199
- /** Hanzo-minted Sentry DSN: "https://<version>:<hmac>@<host>/v1/event/<projectId>".
200
- * Publishable — the key authorizes writes to ONE project and can read nothing,
201
- * so it is safe in a browser bundle (same trust class as `ingestKey`). When
202
- * absent the client reads NEXT_PUBLIC_HANZO_EVENT_DSN; when neither is set the
203
- * error plane is inert (fail-safe: nothing sent, nothing thrown, analytics
204
- * unaffected). Mint one per property: POST /v1/sentry/projects. */
205
- dsn?: string;
206
200
  /** Release stamped on error events (a git SHA / app version). */
207
201
  release?: string;
208
202
  /** Deployment environment for error events (production | staging | …). */
@@ -221,55 +215,6 @@ export interface SentryFrame {
221
215
  colno?: number;
222
216
  in_app?: boolean;
223
217
  }
224
- export interface SentryExceptionValue {
225
- type?: string;
226
- value?: string;
227
- module?: string;
228
- stacktrace?: {
229
- frames: SentryFrame[];
230
- };
231
- }
232
- export interface SentryUser {
233
- /** Stable subject id (OIDC sub / anon id). NEVER email/username/ip. */
234
- id?: string;
235
- }
236
- export interface SentryEvent {
237
- event_id: string;
238
- timestamp: number;
239
- platform: 'javascript';
240
- level: SentryLevel;
241
- logger?: string;
242
- environment?: string;
243
- release?: string;
244
- transaction?: string;
245
- fingerprint?: string[];
246
- message?: string;
247
- exception?: {
248
- values: SentryExceptionValue[];
249
- };
250
- tags?: Record<string, string>;
251
- user?: SentryUser;
252
- contexts?: Record<string, Record<string, unknown>>;
253
- sdk?: {
254
- name: string;
255
- version: string;
256
- };
257
- }
258
- /** Parsed DSN — the public key + the derived ingest URL. */
259
- export interface Dsn {
260
- /** "<version>:<hmac>" public key presented via ?sentry_key= (beacon-safe). */
261
- publicKey: string;
262
- /** Ingest origin, e.g. "https://sentry.hanzo.ai". */
263
- origin: string;
264
- /** Project id segment. */
265
- projectId: string;
266
- /** The DSN's own origin + path, e.g. "https://api.hanzo.ai/v1/event/<projectId>".
267
- * Every URL below is derived from this, so the ingest address is named once —
268
- * in dsnForProduct — and nowhere else. */
269
- base: string;
270
- /** Fully-derived envelope ingest URL incl. ?sentry_key=. */
271
- ingestUrl: string;
272
- }
273
218
  /** Options for Analytics.captureError. */
274
219
  export interface CaptureErrorOptions {
275
220
  /** false => uncaught (window.onerror / unhandledrejection / render crash). */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/event",
3
- "version": "0.3.48",
3
+ "version": "0.3.50",
4
4
  "description": "Hanzo Event — the ONE telemetry client. Emits pageview/event/identify/group to the Hanzo Cloud event stream (POST /v1/event), AND reports errors to Sentry as real Sentry envelopes — the error plane needs a DSN, without one nothing reaches Sentry. First-touch attribution, beacon-on-unload, auto error capture, client-side secret/PII scrubbing, a shared event + goal vocabulary. Subsumes @sentry.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
package/src/core.test.ts CHANGED
@@ -29,8 +29,6 @@ interface Sent {
29
29
 
30
30
  // FakeTransport records the EXACT bytes the client would put on the wire, so the
31
31
  // tests assert the real POST /v1/event body shape ({ batch: [...] }), not a mock.
32
- // The error plane puts a newline-delimited Sentry envelope on the same transport,
33
- // which is deliberately NOT JSON — it is recorded raw, with an empty batch.
34
32
  class FakeTransport implements Transport {
35
33
  sent: Sent[] = []
36
34
  send(
@@ -57,24 +55,11 @@ class FakeTransport implements Transport {
57
55
  get all(): WireEvent[] {
58
56
  return this.sent.flatMap((s) => s.batch)
59
57
  }
60
- /** Sends on the error plane — the envelope content type is the discriminator. */
61
- get envelopes(): Sent[] {
62
- return this.sent.filter((s) => s.contentType === 'application/x-sentry-envelope')
63
- }
64
- /** Sends on the event stream. */
65
- get streams(): Sent[] {
66
- return this.sent.filter((s) => s.contentType !== 'application/x-sentry-envelope')
67
- }
68
58
  }
69
59
 
70
60
  let tx: FakeTransport
71
61
  // Default to same-origin (host:'') so path assertions read the bare /v1/event path;
72
62
  // tests that care about the edge host pass it explicitly.
73
- // `test-app` is deliberately NOT a product in the DSN registry (src/dsn.ts), so
74
- // the default client here has no error plane and these tests exercise the client
75
- // itself rather than whichever real products happen to be registered. Tests that
76
- // want a live error plane pass `dsn:` explicitly; registry resolution is covered
77
- // in dsn.test.ts.
78
63
  function mk(overrides = {}) {
79
64
  tx = new FakeTransport()
80
65
  return new Analytics({ product: 'test-app', host: '', transport: tx, flushIntervalMs: 999999, ...overrides })
@@ -411,17 +396,17 @@ describe('Analytics capture', () => {
411
396
  expect(tx.sent[0].ingestKey).toBeUndefined()
412
397
  })
413
398
 
414
- it('a signed-in bearer WINS over a key from the build env', () => {
415
- // The leak this closes: one console bundle is served to several brands, and a
416
- // pk- names ONE org. If an env-sourced key displaced the bearer, every
417
- // signed-in user's events would re-file under whichever org minted the key.
399
+ it('a signed-in page sends the key AND the bearer; the bearer never replaces the key', () => {
400
+ // The person is the bearer and the project is the key. The server files the event
401
+ // under the key's project only when it belongs to the bearer's own org, so a key
402
+ // from the build env cannot re-file a person's events under another org.
418
403
  process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-live-one-org'
419
404
  try {
420
405
  const a = mk({ getToken: () => 'jwt-of-a-real-person' })
421
406
  a.capture('x')
422
407
  a.flush()
423
408
  expect(tx.sent[0].token).toBe('jwt-of-a-real-person')
424
- expect(tx.sent[0].ingestKey).toBeUndefined()
409
+ expect(tx.sent[0].ingestKey).toBe('pk-live-one-org')
425
410
  } finally {
426
411
  delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
427
412
  }
@@ -467,313 +452,56 @@ describe('Analytics capture', () => {
467
452
  })
468
453
  })
469
454
 
470
- describe('Event error capture', () => {
471
- it('emits under the RESERVED $exception name, never under the message', () => {
472
- const a = mk()
455
+ describe('error capture', () => {
456
+ it('is ONE type:error event on the one endpoint, not a second plane', () => {
457
+ const a = mk({ host: 'https://api.hanzo.ai', release: 'r1', environment: 'production' })
473
458
  a.captureError(new TypeError('boom'))
474
- // captureError flushes promptly — no explicit flush() needed.
475
459
  expect(tx.sent).toHaveLength(1)
460
+ expect(tx.sent[0].url).toBe('https://api.hanzo.ai/v1/event')
461
+ expect(tx.all).toHaveLength(1)
476
462
  const e = tx.all[0]
477
- // THE NAME. Until 0.3.20 this was `ex.message`, which made every distinct
478
- // error string a permanent entry in the event taxonomy and left Error
479
- // Tracking — which reads this exact name — at zero rows.
480
- expect(e.event).toBe('$exception')
481
- // THE TYPE. `type` alone picks the storage plane: 'error' routes to the error
482
- // plane, which the product-event projection does not read, so an exception
483
- // filed there cannot reach Error Tracking. The full error record still goes to
484
- // the error plane as a Sentry envelope.
485
- expect(e.type).toBe('event')
486
- // The exception STILL rides the TOP-LEVEL `error` field, which is what cloud's
487
- // foldException reads to stamp properties.$exception (scrubbing on the way).
463
+ expect(e.type).toBe('error')
464
+ expect(e.event).toBeUndefined()
488
465
  expect(e.error?.type).toBe('TypeError')
489
466
  expect(e.error?.message).toBe('boom')
490
467
  expect(e.error?.stack).toBeTruthy()
491
- expect(e.error?.handled).toBe(true) // a caught, manually-reported error
492
- // The client does not stamp $exception itself — the server fold owns that key.
493
- expect(e.properties ?? {}).not.toHaveProperty('$exception')
468
+ expect(Array.isArray(e.error?.frames)).toBe(true)
469
+ expect(e.error?.handled).toBe(true)
470
+ expect(e.level).toBe('error')
471
+ expect(e.release).toBe('r1')
472
+ expect(e.environment).toBe('production')
473
+ expect(e.product).toBe('test-app')
474
+ expect((e as Record<string, unknown>).tenant).toBeUndefined()
494
475
  })
495
476
 
496
- it('carries the $exception_* bag Error Tracking reads', () => {
477
+ it('never sends a Sentry envelope or a $exception event', () => {
497
478
  const a = mk()
498
- a.captureError(new TypeError('boom'))
499
- const p = tx.all[0].properties as Record<string, unknown>
500
- expect(Array.isArray(p.$exception_list)).toBe(true)
501
- // Without a fingerprint the issue query drops the event outright.
502
- expect(p.$exception_fingerprint).toMatch(/^[0-9a-f]{32}$/)
503
- expect(p.$exception_type).toBe('TypeError')
504
- expect(p.$exception_handled).toBe(true)
479
+ a.captureError(new Error('x'))
480
+ expect(tx.sent.every((s) => s.contentType === undefined)).toBe(true)
481
+ expect(tx.all.some((e) => e.event === '$exception')).toBe(false)
505
482
  })
506
483
 
507
484
  it('normalizes a thrown string into an exception', () => {
508
485
  const a = mk()
509
486
  a.captureError('plain failure')
510
- const e = tx.all[0]
511
- expect(e.event).toBe('$exception')
512
- expect(e.type).toBe('event')
513
- expect(e.error?.message).toBe('plain failure')
487
+ expect(tx.all[0].error?.message).toBe('plain failure')
514
488
  })
515
489
 
516
- it('marks handled=false for unhandled/global errors and carries caller properties', () => {
490
+ it('an unhandled error is fatal and carries caller properties', () => {
517
491
  const a = mk()
518
492
  a.captureError(new Error('unhandled'), { handled: false, properties: { source: 'onerror' } })
519
493
  const e = tx.all[0]
520
494
  expect(e.error?.handled).toBe(false)
521
- const p = e.properties as Record<string, unknown>
522
- // The caller's own properties survive alongside the exception bag.
523
- expect(p.source).toBe('onerror')
524
- expect(p.$exception_handled).toBe(false)
525
- })
526
-
527
- it("a caller property cannot overwrite the exception bag it shares a name with", () => {
528
- const a = mk()
529
- a.captureError(new Error('x'), {
530
- handled: false,
531
- properties: { $exception_fingerprint: 'forged' },
532
- })
533
- const p = tx.all[0].properties as Record<string, unknown>
534
- expect(p.$exception_fingerprint).not.toBe('forged')
535
- })
536
-
537
- it('an error is still an event on the ONE stream — same /v1/event endpoint + product', () => {
538
- const a = mk({ host: 'https://api.hanzo.ai' })
539
- a.captureError(new Error('x'))
540
- // Same batched endpoint as any other event — one pipe, not a second SDK.
541
- expect(tx.sent[0].url).toBe('https://api.hanzo.ai/v1/event')
542
- expect(tx.sent[0].raw.startsWith('{"batch":')).toBe(true)
543
- expect(tx.all[0].product).toBe('test-app')
544
- // never the tenant — the server stamps it, errors included.
545
- expect((tx.all[0] as Record<string, unknown>).tenant).toBeUndefined()
495
+ expect(e.level).toBe('fatal')
496
+ expect((e.properties as Record<string, unknown>).source).toBe('onerror')
546
497
  })
547
- })
548
498
 
549
- // ── the error plane ────────────────────────────────────────────────────────
550
- //
551
- // The regression these tests exist to prevent: @hanzo/event 0.3.1 documented
552
- // errors as "lensed server-side into … error tracking (sentry)". No such
553
- // server-side fan-out exists — POST /v1/event stores an event_type='error' row in
554
- // the cloud warehouse and stops there. The result was that every Hanzo property
555
- // silently reported ZERO errors to sentry.hanzo.ai. Reaching Sentry requires the
556
- // client to emit a real Sentry ENVELOPE to the DSN's ingest URL, which is what
557
- // these assert.
558
-
559
- // A syntactically real Hanzo-minted DSN. The key is "<version>:<hmac>"; this hmac
560
- // is a dummy (32 zero-bytes hex) — the shape is what the parser cares about, and
561
- // no test performs network I/O.
562
- const TEST_DSN =
563
- 'https://1:' +
564
- '0'.repeat(64) +
565
- '@sentry.hanzo.ai/v1/event/019f9b1e-5785-7359-ad0b-f75db8e58c99'
566
-
567
- describe('error plane', () => {
568
- it('is INERT without a DSN — nothing sent to sentry, event stream unaffected', () => {
499
+ it('circular properties do not lose the report', () => {
569
500
  const a = mk()
570
- expect(a.errorPlaneEnabled).toBe(false)
571
- a.captureError(new Error('boom'))
572
- expect(tx.envelopes).toHaveLength(0)
573
- // fail-safe: the event stream still carries the error, analytics untouched.
574
- expect(tx.streams).toHaveLength(1)
575
- expect(tx.all[0].event).toBe('$exception')
576
- })
577
-
578
- it('with a DSN, an error POSTs a Sentry envelope to the DERIVED ingest URL', () => {
579
- const a = mk({ dsn: TEST_DSN })
580
- expect(a.errorPlaneEnabled).toBe(true)
581
- a.captureError(new TypeError('kaboom'))
582
-
583
- expect(tx.envelopes).toHaveLength(1)
584
- const env = tx.envelopes[0]
585
- // The exact route the o11y ingest registers, key on the query string.
586
- expect(env.url).toBe(
587
- 'https://sentry.hanzo.ai/v1/event/019f9b1e-5785-7359-ad0b-f75db8e58c99/envelope/' +
588
- '?sentry_key=1%3A' + '0'.repeat(64),
589
- )
590
- expect(env.contentType).toBe('application/x-sentry-envelope')
591
- expect(a.errorIngestUrl).toBe(env.url)
592
- })
593
-
594
- it('never sends an /api/ path — Hanzo routes are /v1/ only', () => {
595
- const a = mk({ dsn: TEST_DSN })
596
- a.captureError(new Error('x'))
597
- for (const s of tx.sent) expect(s.url).not.toContain('/api/')
598
- })
599
-
600
- it('frames a VALID envelope: header / item-header / payload, byte length correct', () => {
601
- const a = mk({ dsn: TEST_DSN })
602
- a.captureError(new Error('framing'))
603
- const lines = tx.envelopes[0].raw.split('\n')
604
- const header = JSON.parse(lines[0]) as { event_id: string; dsn: string; sent_at: string }
605
- const item = JSON.parse(lines[1]) as { type: string; length: number }
606
- const payload = lines[2]
607
-
608
- expect(item.type).toBe('event')
609
- // The server reads the length-delimited framing first — it must be the UTF-8
610
- // byte length, not the JS string length.
611
- expect(item.length).toBe(new TextEncoder().encode(payload).length)
612
- expect(header.dsn).toBe(
613
- 'https://sentry.hanzo.ai/v1/event/019f9b1e-5785-7359-ad0b-f75db8e58c99',
614
- )
615
- const ev = JSON.parse(payload) as {
616
- event_id: string
617
- exception: { values: { type: string; value: string }[] }
618
- }
619
- expect(ev.event_id).toBe(header.event_id)
620
- expect(ev.exception.values[0].type).toBe('Error')
621
- expect(ev.exception.values[0].value).toBe('framing')
622
- })
623
-
624
- it('carries the SAME session + subject identity as the event stream', () => {
625
- const a = mk({ dsn: TEST_DSN })
626
- a.identify('user-sub-123')
627
- a.captureError(new Error('correlate me'))
628
- const ev = JSON.parse(tx.envelopes[0].raw.split('\n')[2]) as {
629
- user: { id: string }
630
- tags: Record<string, string>
631
- }
632
- expect(ev.user.id).toBe('user-sub-123')
633
- const streamed = tx.all.find((e) => e.type === 'error')
634
- expect(ev.tags.session).toBe(streamed?.sessionId)
635
- expect(ev.tags.product).toBe('test-app')
636
- })
637
-
638
- it('an uncaught error is fatal; a reported one is error', () => {
639
- const a = mk({ dsn: TEST_DSN })
640
- a.captureError(new Error('uncaught'), { handled: false })
641
- a.captureError(new Error('reported'))
642
- const lvl = (i: number) =>
643
- (JSON.parse(tx.envelopes[i].raw.split('\n')[2]) as { level: string }).level
644
- expect(lvl(0)).toBe('fatal')
645
- expect(lvl(1)).toBe('error')
646
- })
647
-
648
- it('scrubs secrets and PII from the error message before it leaves the browser', () => {
649
- const a = mk({ dsn: TEST_DSN })
650
- a.captureError(new Error('login failed for tam@hanzo.ai with hk-' + 'a'.repeat(20)))
651
- const value = (
652
- JSON.parse(tx.envelopes[0].raw.split('\n')[2]) as {
653
- exception: { values: { value: string }[] }
654
- }
655
- ).exception.values[0].value
656
- expect(value).not.toContain('tam@hanzo.ai')
657
- expect(value).not.toContain('hk-aaaa')
658
- expect(value).toContain('[email]')
659
- expect(value).toContain('[redacted]')
660
- })
661
-
662
- it('does NOT attach the event-stream credential to the sentry request', () => {
663
- // The two planes authenticate independently: the DSN key rides ?sentry_key=,
664
- // so a publishable ingest key must not leak onto the error host.
665
- const a = mk({ dsn: TEST_DSN, ingestKey: 'pk_test.sig', getToken: () => 'jwt-abc' })
666
- a.captureError(new Error('x'))
667
- const env = tx.envelopes[0]
668
- expect(env.ingestKey).toBeUndefined()
669
- expect(env.token).toBeUndefined()
670
- expect(env.beacon).toBe(false)
671
- })
672
-
673
- it('ONE captureError feeds BOTH planes', () => {
674
- const a = mk({ dsn: TEST_DSN })
675
- a.captureError(new Error('both'))
676
- expect(tx.streams).toHaveLength(1)
677
- expect(tx.envelopes).toHaveLength(1)
678
- })
679
-
680
- it('a malformed DSN leaves the plane inert rather than throwing', () => {
681
- const a = mk({ dsn: 'not-a-dsn' })
682
- expect(a.errorPlaneEnabled).toBe(false)
683
- expect(() => a.captureError(new Error('x'))).not.toThrow()
684
- expect(tx.envelopes).toHaveLength(0)
685
- })
686
-
687
- it('never throws back into the host app when the transport fails', () => {
688
- const a = mk({ dsn: TEST_DSN })
689
- tx.send = () => {
690
- throw new Error('network down')
691
- }
692
- expect(() => a.captureError(new Error('x'))).not.toThrow()
693
- })
694
- })
695
-
696
- // ── plane independence under hostile input ─────────────────────────────────
697
- //
698
- // The regression: captureError ran enqueue()+flush() BEFORE sendError(), all in
699
- // one try{}. `properties` is arbitrary caller data — hanzoai/app spreads
700
- // ErrorContext.metadata straight in, and a DOM node, a React synthetic event or
701
- // an axios error are all circular — so serializing the stream threw, the catch
702
- // swallowed it, and the CRASH REPORT WAS NEVER SENT. Both planes went to zero on
703
- // exactly the inputs a real app produces. They must not be able to starve each
704
- // other.
705
-
706
- describe('plane independence', () => {
707
- function circular(): Record<string, unknown> {
708
- const o: Record<string, unknown> = { name: 'node' }
709
- o.self = o
710
- return o
711
- }
712
- const throwingGetter = () =>
713
- Object.defineProperty({}, 'boom', {
714
- get() {
715
- throw new Error('getter exploded')
716
- },
717
- enumerable: true,
718
- }) as Record<string, unknown>
719
-
720
- it('circular properties: the error still reaches Sentry', () => {
721
- const a = mk({ dsn: TEST_DSN })
722
- a.captureError(new Error('circular payload'), { properties: circular() })
723
- expect(tx.envelopes).toHaveLength(1)
724
- })
725
-
726
- it('throwing getter in properties: the error still reaches Sentry', () => {
727
- const a = mk({ dsn: TEST_DSN })
728
- a.captureError(new Error('hostile getter'), { properties: throwingGetter() })
729
- expect(tx.envelopes).toHaveLength(1)
730
- // The exception survives even though the tag did not.
731
- const ev = JSON.parse(tx.envelopes[0].raw.split('\n')[2]) as {
732
- exception: { values: { value: string }[] }
733
- }
734
- expect(ev.exception.values[0].value).toBe('hostile getter')
735
- })
736
-
737
- it('a throwing event-stream transport does not stop the error plane', () => {
738
- const a = mk({ dsn: TEST_DSN })
739
- const real = tx.send.bind(tx)
740
- tx.send = (url, body, opts) => {
741
- if (opts.contentType !== 'application/x-sentry-envelope') throw new Error('stream down')
742
- real(url, body, opts)
743
- }
744
- a.captureError(new Error('stream is broken'))
745
- expect(tx.envelopes).toHaveLength(1)
746
- })
747
-
748
- it('a throwing error-plane transport does not stop the event stream', () => {
749
- const a = mk({ dsn: TEST_DSN })
750
- const real = tx.send.bind(tx)
751
- tx.send = (url, body, opts) => {
752
- if (opts.contentType === 'application/x-sentry-envelope') throw new Error('sentry down')
753
- real(url, body, opts)
754
- }
755
- a.captureError(new Error('sentry is broken'))
756
- expect(tx.streams).toHaveLength(1)
757
- })
758
-
759
- it('one poisoned event never discards previously buffered clean events', () => {
760
- const a = mk({ dsn: TEST_DSN })
761
- a.pageview('/one')
762
- a.capture('clean_event')
763
- // Now poison the buffer, then force a flush.
764
- a.capture('poisoned', circular())
765
- a.flush()
766
- const names = tx.all.map((e) => e.event)
767
- expect(names).toContain('$pageview')
768
- expect(names).toContain('clean_event')
769
- // The poisoned one is kept too — with its payload replaced, not silently lost.
770
- const bad = tx.all.find((e) => e.event === 'poisoned')
771
- expect(bad?.properties).toEqual({ $unserializable: true })
772
- })
773
-
774
- it('never throws into the host app on any of these', () => {
775
- const a = mk({ dsn: TEST_DSN })
776
- expect(() => a.captureError(new Error('x'), { properties: circular() })).not.toThrow()
777
- expect(() => a.captureError(new Error('y'), { properties: throwingGetter() })).not.toThrow()
501
+ const loop: Record<string, unknown> = {}
502
+ loop.self = loop
503
+ a.captureError(new Error('x'), { properties: loop })
504
+ expect(tx.sent).toHaveLength(1)
505
+ expect(tx.all[0].error?.message).toBe('x')
778
506
  })
779
507
  })