@hanzo/event 0.3.48 → 0.3.49
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 +11 -50
- package/dist/core.d.ts +6 -36
- package/dist/core.d.ts.map +1 -1
- package/dist/index.cjs +206 -409
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +208 -406
- package/dist/index.mjs.map +1 -1
- package/dist/org.d.ts.map +1 -1
- package/dist/react.cjs +118 -404
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +119 -405
- package/dist/react.mjs.map +1 -1
- package/dist/throwable.d.ts +15 -0
- package/dist/throwable.d.ts.map +1 -0
- package/dist/types.d.ts +19 -74
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core.test.ts +28 -300
- package/src/core.ts +36 -185
- package/src/exception.ts +1 -1
- package/src/index.ts +1 -5
- package/src/org.ts +4 -0
- package/src/stream.test.ts +0 -7
- package/src/throwable.test.ts +72 -0
- package/src/throwable.ts +126 -0
- package/src/types.ts +15 -75
- package/src/uid.test.ts +1 -1
- package/dist/dsn.d.ts +0 -43
- package/dist/dsn.d.ts.map +0 -1
- package/dist/sentry.d.ts +0 -62
- package/dist/sentry.d.ts.map +0 -1
- package/src/dsn.test.ts +0 -75
- package/src/dsn.ts +0 -56
- package/src/sentry.test.ts +0 -312
- package/src/sentry.ts +0 -311
|
@@ -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`
|
|
2
|
-
*
|
|
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
|
|
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.
|
|
125
|
-
*
|
|
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,
|
|
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.
|
|
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). */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA
|
|
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.
|
|
3
|
+
"version": "0.3.49",
|
|
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 })
|
|
@@ -467,313 +452,56 @@ describe('Analytics capture', () => {
|
|
|
467
452
|
})
|
|
468
453
|
})
|
|
469
454
|
|
|
470
|
-
describe('
|
|
471
|
-
it('
|
|
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
|
-
|
|
478
|
-
|
|
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?.
|
|
492
|
-
|
|
493
|
-
expect(e.
|
|
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('
|
|
477
|
+
it('never sends a Sentry envelope or a $exception event', () => {
|
|
497
478
|
const a = mk()
|
|
498
|
-
a.captureError(new
|
|
499
|
-
|
|
500
|
-
expect(
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
expect(p.source).toBe('onerror')
|
|
524
|
-
expect(p.$exception_handled).toBe(false)
|
|
495
|
+
expect(e.level).toBe('fatal')
|
|
496
|
+
expect((e.properties as Record<string, unknown>).source).toBe('onerror')
|
|
525
497
|
})
|
|
526
498
|
|
|
527
|
-
it(
|
|
499
|
+
it('circular properties do not lose the report', () => {
|
|
528
500
|
const a = mk()
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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()
|
|
546
|
-
})
|
|
547
|
-
})
|
|
548
|
-
|
|
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', () => {
|
|
569
|
-
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
|
})
|