@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
package/src/types.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
// Public types for the Hanzo Event client.
|
|
2
2
|
|
|
3
|
-
/** The event kinds — the closed set the server understands. `error`
|
|
4
|
-
*
|
|
5
|
-
* event_type='error' for the warehouse, GET /v1/errors). It does NOT reach the
|
|
6
|
-
* Sentry dashboard — the envelope on the error plane does that. */
|
|
3
|
+
/** The event kinds — the closed set the server understands. `error` is a captured
|
|
4
|
+
* exception; the server files it on the error plane. */
|
|
7
5
|
export type EventKind = 'pageview' | 'event' | 'identify' | 'group' | 'error'
|
|
8
6
|
|
|
9
|
-
/** A captured exception as
|
|
10
|
-
* properties.$exception for the warehouse. The richer copy (parsed stack frames,
|
|
11
|
-
* grouping, release) travels on the error plane as a Sentry envelope; see
|
|
12
|
-
* AnalyticsConfig.dsn. */
|
|
7
|
+
/** A captured exception, as the `error` field of a `type:'error'` event. */
|
|
13
8
|
export interface Exception {
|
|
14
9
|
/** Constructor/class name, e.g. "TypeError". */
|
|
15
10
|
type?: string
|
|
@@ -20,6 +15,8 @@ export interface Exception {
|
|
|
20
15
|
/** false = an unhandled/global error (window.onerror, unhandledrejection);
|
|
21
16
|
* true = a caught error the app chose to report. Defaults true. */
|
|
22
17
|
handled?: boolean
|
|
18
|
+
/** The structured stack, oldest first. */
|
|
19
|
+
frames?: { function?: string; file?: string; line?: number; column?: number }[]
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
/** One stack frame as Error Tracking renders it. The key names are the product's
|
|
@@ -125,9 +122,13 @@ export interface WireEvent {
|
|
|
125
122
|
revenue?: number
|
|
126
123
|
currency?: string
|
|
127
124
|
/** Set on `type:'error'` events — the captured exception. Cloud lifts it into
|
|
128
|
-
* properties.$exception (foldException) for the event warehouse.
|
|
129
|
-
*
|
|
125
|
+
* properties.$exception (foldException) for the event warehouse. The
|
|
126
|
+
* server files it on the error plane. */
|
|
130
127
|
error?: Exception
|
|
128
|
+
level?: string
|
|
129
|
+
release?: string
|
|
130
|
+
environment?: string
|
|
131
|
+
site?: string
|
|
131
132
|
properties?: Record<string, unknown>
|
|
132
133
|
library?: string
|
|
133
134
|
libraryVersion?: string
|
|
@@ -176,8 +177,7 @@ export interface AnalyticsConfig {
|
|
|
176
177
|
* POST /v1/keys {"type":"publishable"}.
|
|
177
178
|
*
|
|
178
179
|
* Omit it and the client reads NEXT_PUBLIC_PUBLISHABLE_KEY from the inlined
|
|
179
|
-
* build env,
|
|
180
|
-
* in its build and neither needs code to switch on. That is the ONE spelling
|
|
180
|
+
* build env, That is the ONE spelling
|
|
181
181
|
* the fleet already carries: KMS holds deploy/PUBLISHABLE_KEY, and each
|
|
182
182
|
* Dockerfile takes PUBLISHABLE_KEY as a build-arg and re-exports it with the
|
|
183
183
|
* NEXT_PUBLIC_ prefix that makes Next inline it.
|
|
@@ -187,9 +187,7 @@ export interface AnalyticsConfig {
|
|
|
187
187
|
* edge refuses an unattributable write rather than filing it where its owner
|
|
188
188
|
* cannot read it. That failure is invisible from the page, which is why the key
|
|
189
189
|
* belongs in the env next to the DSN and not in a checklist.
|
|
190
|
-
|
|
191
|
-
* This attributes the EVENT STREAM only — the error plane authenticates
|
|
192
|
-
* independently with `dsn`, and one does not stand in for the other. */
|
|
190
|
+
*/
|
|
193
191
|
ingestKey?: string
|
|
194
192
|
/** Max events buffered before an automatic flush. */
|
|
195
193
|
batchSize?: number
|
|
@@ -198,24 +196,13 @@ export interface AnalyticsConfig {
|
|
|
198
196
|
/** Turn the client off entirely (e.g. opt-out / DNT). Defaults to enabled. */
|
|
199
197
|
enabled?: boolean
|
|
200
198
|
/** Auto-capture unhandled errors + promise rejections (window.onerror,
|
|
201
|
-
* unhandledrejection). Browser-only, defaults to enabled.
|
|
202
|
-
* this is what makes the client a drop-in @sentry replacement — without a
|
|
203
|
-
* `dsn` the captures never reach the Sentry dashboard. */
|
|
199
|
+
* unhandledrejection). Browser-only, defaults to enabled. */
|
|
204
200
|
captureErrors?: boolean
|
|
205
201
|
/** Override the transport (tests). */
|
|
206
202
|
transport?: Transport
|
|
207
203
|
/** Debug logging. */
|
|
208
204
|
debug?: boolean
|
|
209
205
|
|
|
210
|
-
// ── error plane (Sentry envelope -> sentry.hanzo.ai) ──────────────────────
|
|
211
|
-
|
|
212
|
-
/** Hanzo-minted Sentry DSN: "https://<version>:<hmac>@<host>/v1/event/<projectId>".
|
|
213
|
-
* Publishable — the key authorizes writes to ONE project and can read nothing,
|
|
214
|
-
* so it is safe in a browser bundle (same trust class as `ingestKey`). When
|
|
215
|
-
* absent the client reads NEXT_PUBLIC_HANZO_EVENT_DSN; when neither is set the
|
|
216
|
-
* error plane is inert (fail-safe: nothing sent, nothing thrown, analytics
|
|
217
|
-
* unaffected). Mint one per property: POST /v1/sentry/projects. */
|
|
218
|
-
dsn?: string
|
|
219
206
|
/** Release stamped on error events (a git SHA / app version). */
|
|
220
207
|
release?: string
|
|
221
208
|
/** Deployment environment for error events (production | staging | …). */
|
|
@@ -225,8 +212,7 @@ export interface AnalyticsConfig {
|
|
|
225
212
|
capturePII?: boolean
|
|
226
213
|
}
|
|
227
214
|
|
|
228
|
-
// ──
|
|
229
|
-
// Sentry ingest protocol — develop.sentry.dev; no upstream code) ───────────
|
|
215
|
+
// ── stack frame and level types ──────────────────────────────────────────────
|
|
230
216
|
|
|
231
217
|
export type SentryLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug'
|
|
232
218
|
|
|
@@ -240,52 +226,6 @@ export interface SentryFrame {
|
|
|
240
226
|
in_app?: boolean
|
|
241
227
|
}
|
|
242
228
|
|
|
243
|
-
export interface SentryExceptionValue {
|
|
244
|
-
type?: string
|
|
245
|
-
value?: string
|
|
246
|
-
module?: string
|
|
247
|
-
stacktrace?: { frames: SentryFrame[] }
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export interface SentryUser {
|
|
251
|
-
/** Stable subject id (OIDC sub / anon id). NEVER email/username/ip. */
|
|
252
|
-
id?: string
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export interface SentryEvent {
|
|
256
|
-
event_id: string
|
|
257
|
-
timestamp: number
|
|
258
|
-
platform: 'javascript'
|
|
259
|
-
level: SentryLevel
|
|
260
|
-
logger?: string
|
|
261
|
-
environment?: string
|
|
262
|
-
release?: string
|
|
263
|
-
transaction?: string
|
|
264
|
-
fingerprint?: string[]
|
|
265
|
-
message?: string
|
|
266
|
-
exception?: { values: SentryExceptionValue[] }
|
|
267
|
-
tags?: Record<string, string>
|
|
268
|
-
user?: SentryUser
|
|
269
|
-
contexts?: Record<string, Record<string, unknown>>
|
|
270
|
-
sdk?: { name: string; version: string }
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/** Parsed DSN — the public key + the derived ingest URL. */
|
|
274
|
-
export interface Dsn {
|
|
275
|
-
/** "<version>:<hmac>" public key presented via ?sentry_key= (beacon-safe). */
|
|
276
|
-
publicKey: string
|
|
277
|
-
/** Ingest origin, e.g. "https://sentry.hanzo.ai". */
|
|
278
|
-
origin: string
|
|
279
|
-
/** Project id segment. */
|
|
280
|
-
projectId: string
|
|
281
|
-
/** The DSN's own origin + path, e.g. "https://api.hanzo.ai/v1/event/<projectId>".
|
|
282
|
-
* Every URL below is derived from this, so the ingest address is named once —
|
|
283
|
-
* in dsnForProduct — and nowhere else. */
|
|
284
|
-
base: string
|
|
285
|
-
/** Fully-derived envelope ingest URL incl. ?sentry_key=. */
|
|
286
|
-
ingestUrl: string
|
|
287
|
-
}
|
|
288
|
-
|
|
289
229
|
/** Options for Analytics.captureError. */
|
|
290
230
|
export interface CaptureErrorOptions {
|
|
291
231
|
/** false => uncaught (window.onerror / unhandledrejection / render crash). */
|
package/src/uid.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, afterEach } from 'vitest'
|
|
2
2
|
import { uuidv7, uuidv7Time } from './uid'
|
|
3
|
-
|
|
3
|
+
const eventId = () => uuidv7().replace(/-/g, '')
|
|
4
4
|
|
|
5
5
|
/** The event plane's admission gate, transcribed from the session rollup's own SQL:
|
|
6
6
|
* `bitAnd(bitShiftRight(toUInt128(accurateCastOrNull(id,'UUID')), 76), 15) = 7`.
|
package/dist/dsn.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The product → Sentinel DSN registry.
|
|
3
|
-
*
|
|
4
|
-
* An app declares WHAT it is (`product: 'console'`); this module knows WHERE its
|
|
5
|
-
* errors go. That split is the whole point: no surface has to learn a DSN, carry
|
|
6
|
-
* a build argument, or grow a config file to report errors — declaring the
|
|
7
|
-
* product it already declares is enough.
|
|
8
|
-
*
|
|
9
|
-
* A Sentinel DSN is PUBLIC by construction. It ships inside the client bundle and
|
|
10
|
-
* is readable in devtools on any deployed page, and it grants exactly one
|
|
11
|
-
* capability: submitting new events. It cannot read issues, projects, or any
|
|
12
|
-
* other data. So committing it is not leaking a secret — it is recording a public
|
|
13
|
-
* identifier next to the code that needs it. (Contrast the server-side collector
|
|
14
|
-
* DSN in the `team-analytics-sentry` Secret, which is HMAC-derived and revocable
|
|
15
|
-
* precisely because a server-side credential is NOT public.)
|
|
16
|
-
*
|
|
17
|
-
* Why a literal map instead of deriving `hanzo-${product}`: the projects predate
|
|
18
|
-
* this registry and do not derive cleanly — `site` lives in `hanzo-ai`, not
|
|
19
|
-
* `hanzo-site`. An explicit map is honest about that; a derivation rule plus an
|
|
20
|
-
* exception table is the same data with a trap in it.
|
|
21
|
-
*
|
|
22
|
-
* Projects are org-scoped and named `<org>-<app>`. To add one: create the project
|
|
23
|
-
* (POST /v1/sentinel/projects with X-Org-Id), then add its `dsn` here keyed by the
|
|
24
|
-
* product name the app passes to `createAnalytics`.
|
|
25
|
-
*/
|
|
26
|
-
/** PRODUCT_PROJECT maps a `product` to its Sentinel project id. The DSN's KEY is no
|
|
27
|
-
* longer a per-project secret — it is the ONE org publishable key (below), so a
|
|
28
|
-
* surface's errors ride the SAME key its events do. The id only names WHICH
|
|
29
|
-
* project the errors group under, and cloud auto-provisions that project on first
|
|
30
|
-
* keyed ingest, so a new id needs nothing minted. `site` lives in the `hanzo-ai`
|
|
31
|
-
* project — an explicit map, because the projects predate this and do not derive
|
|
32
|
-
* cleanly from the product name. */
|
|
33
|
-
export declare const PRODUCT_PROJECT: Readonly<Record<string, string>>;
|
|
34
|
-
/** dsnForProduct builds the product's Sentinel DSN from the caller's resolved
|
|
35
|
-
* publishable `key` and the product's project — the SAME key the event stream
|
|
36
|
-
* carries, at the product's envelope endpoint. Returns undefined when there is no
|
|
37
|
-
* key or no project for the product, leaving the error plane inert rather than
|
|
38
|
-
* posting into the wrong one. The key is NOT baked here: it is the value the
|
|
39
|
-
* surface resolved (an explicit `ingestKey`, or the KMS-sourced
|
|
40
|
-
* NEXT_PUBLIC_PUBLISHABLE_KEY the build inlines) — the ONE live source, never a
|
|
41
|
-
* literal committed beside the code. */
|
|
42
|
-
export declare function dsnForProduct(product: string | undefined, key: string | undefined): string | undefined;
|
|
43
|
-
//# sourceMappingURL=dsn.d.ts.map
|
package/dist/dsn.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dsn.d.ts","sourceRoot":"","sources":["../src/dsn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;qCAMqC;AACrC,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI3D,CAAA;AAEF;;;;;;;yCAOyC;AACzC,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,MAAM,GAAG,SAAS,CAKpB"}
|
package/dist/sentry.d.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { CaptureErrorOptions, Dsn, SentryEvent, SentryFrame } from './types';
|
|
2
|
-
/** eventId mints a 32-hex-char id (no dashes) — the Sentry event_id shape. Same
|
|
3
|
-
* minter as everything else, just formatted for Sentry's wire. */
|
|
4
|
-
export declare function eventId(): string;
|
|
5
|
-
/**
|
|
6
|
-
* parseDsn parses "https://<version>:<hmac>@<host>/v1/event/<projectId>" into its
|
|
7
|
-
* public key + the derived envelope ingest URL. The key (which itself contains a
|
|
8
|
-
* ':') is taken verbatim as the userinfo — we do NOT split it as user:pass. The
|
|
9
|
-
* key rides ?sentry_key= (not the DSN in the body) because that is the credential
|
|
10
|
-
* channel the server trusts AND the only one sendBeacon can carry on unload.
|
|
11
|
-
* Returns null for anything malformed (fail-safe: the caller then stays inert).
|
|
12
|
-
*/
|
|
13
|
-
export declare function parseDsn(dsn: string | undefined | null): Dsn | null;
|
|
14
|
-
/**
|
|
15
|
-
* framesFromStack parses a browser Error.stack into Sentry frames, OLDEST-FIRST
|
|
16
|
-
* (Sentry orders caller->callee; the crash site is last — matching the server's
|
|
17
|
-
* pickCrashFrame). Handles both V8 ("at fn (file:li:co)") and
|
|
18
|
-
* Firefox/Safari ("fn@file:li:co"). Unparseable lines are skipped.
|
|
19
|
-
*/
|
|
20
|
-
export declare function framesFromStack(stack: string | undefined): SentryFrame[];
|
|
21
|
-
/** normalizeError coerces an unknown throwable into {name, message, stack}. */
|
|
22
|
-
export declare function normalizeError(err: unknown): {
|
|
23
|
-
name: string;
|
|
24
|
-
message: string;
|
|
25
|
-
stack?: string;
|
|
26
|
-
};
|
|
27
|
-
/** Identity carried onto every error event — the SAME ids analytics uses. */
|
|
28
|
-
export interface ErrorIdentity {
|
|
29
|
-
/** OIDC sub (post-identify) or anon id. NEVER email/PII. */
|
|
30
|
-
userId?: string;
|
|
31
|
-
sessionId?: string;
|
|
32
|
-
product?: string;
|
|
33
|
-
release?: string;
|
|
34
|
-
environment?: string;
|
|
35
|
-
}
|
|
36
|
-
export interface BuildEventInput {
|
|
37
|
-
error: unknown;
|
|
38
|
-
options?: CaptureErrorOptions;
|
|
39
|
-
identity: ErrorIdentity;
|
|
40
|
-
capturePII?: boolean;
|
|
41
|
-
/** Injectable for deterministic tests. */
|
|
42
|
-
now?: number;
|
|
43
|
-
id?: string;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* buildSentryEvent turns a throwable + identity into a Sentry `event`. The message
|
|
47
|
-
* (the leak surface) is scrubbed client-side; the user is ONLY the stable subject
|
|
48
|
-
* id — never email/username/ip. Level defaults to error, or fatal for uncaught.
|
|
49
|
-
*/
|
|
50
|
-
export declare function buildSentryEvent(input: BuildEventInput): SentryEvent;
|
|
51
|
-
/**
|
|
52
|
-
* buildEnvelope frames a Sentry event into a newline-delimited envelope:
|
|
53
|
-
*
|
|
54
|
-
* {"event_id","dsn","sent_at"}\n
|
|
55
|
-
* {"type":"event","content_type":"application/json","length":N}\n
|
|
56
|
-
* <event json>\n
|
|
57
|
-
*
|
|
58
|
-
* The item is length-delimited (N = UTF-8 byte length) — the framing the server's
|
|
59
|
-
* parseEnvelope reads first (falling back to newline-delimited otherwise).
|
|
60
|
-
*/
|
|
61
|
-
export declare function buildEnvelope(event: SentryEvent, dsn: Dsn, sentAt?: string): string;
|
|
62
|
-
//# sourceMappingURL=sentry.d.ts.map
|
package/dist/sentry.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sentry.d.ts","sourceRoot":"","sources":["../src/sentry.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,mBAAmB,EACnB,GAAG,EACH,WAAW,EACX,WAAW,EAEZ,MAAM,SAAS,CAAA;AAahB;mEACmE;AACnE,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAUD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,CAqBnE;AAiBD;;;;;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;AAsBD,6EAA6E;AAC7E,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,mBAAmB,CAAA;IAC7B,QAAQ,EAAE,aAAa,CAAA;IACvB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AAiBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,WAAW,CAqDpE;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAanF"}
|
package/src/dsn.test.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { createAnalytics } from './core'
|
|
4
|
-
import { PRODUCT_PROJECT, dsnForProduct } from './dsn'
|
|
5
|
-
import { parseDsn } from './sentry'
|
|
6
|
-
|
|
7
|
-
const ENV = 'NEXT_PUBLIC_HANZO_EVENT_DSN'
|
|
8
|
-
const OVERRIDE = 'https://1:aaaa@api.hanzo.ai/v1/event/env-project'
|
|
9
|
-
const EXPLICIT = 'https://1:bbbb@api.hanzo.ai/v1/event/explicit-project'
|
|
10
|
-
// A stand-in for the surface's own resolved key — the DSN carries whatever key
|
|
11
|
-
// the caller resolved (config or the KMS-sourced env), never a literal.
|
|
12
|
-
const KEY = 'pk-test-resolved-key'
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
delete process.env[ENV]
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('the product registry', () => {
|
|
19
|
-
it('builds a product DSN from the CALLER key + the project — nothing baked', () => {
|
|
20
|
-
expect(dsnForProduct('console', KEY)).toBe(
|
|
21
|
-
`https://${KEY}@api.hanzo.ai/v1/event/${PRODUCT_PROJECT.console}`
|
|
22
|
-
)
|
|
23
|
-
expect(dsnForProduct('site', KEY)).toBe(
|
|
24
|
-
`https://${KEY}@api.hanzo.ai/v1/event/${PRODUCT_PROJECT.site}`
|
|
25
|
-
)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('returns undefined without a key or without a project, rather than guessing', () => {
|
|
29
|
-
expect(dsnForProduct('console', undefined)).toBeUndefined() // no key -> inert
|
|
30
|
-
expect(dsnForProduct('not-a-product', KEY)).toBeUndefined()
|
|
31
|
-
expect(dsnForProduct(undefined, KEY)).toBeUndefined()
|
|
32
|
-
expect(dsnForProduct('', KEY)).toBeUndefined()
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('every product DSN parses to the caller key + its own project id', () => {
|
|
36
|
-
for (const [product, projectId] of Object.entries(PRODUCT_PROJECT)) {
|
|
37
|
-
const parsed = parseDsn(dsnForProduct(product, KEY))
|
|
38
|
-
expect(parsed, `${product} DSN must parse`).not.toBeNull()
|
|
39
|
-
expect(parsed!.projectId, `${product} project id`).toBe(projectId)
|
|
40
|
-
expect(parsed!.publicKey, `${product} carries the resolved key`).toBe(KEY)
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
describe('DSN precedence — most specific source wins', () => {
|
|
46
|
-
it('lights up the error plane from `product` + a resolved key, with no explicit dsn', () => {
|
|
47
|
-
const a = createAnalytics({ product: 'console', ingestKey: KEY, enabled: false })
|
|
48
|
-
expect(a.errorPlaneEnabled).toBe(true)
|
|
49
|
-
expect(a.errorIngestUrl).toContain(PRODUCT_PROJECT.console)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('prefers an explicit dsn over both the env and the registry', () => {
|
|
53
|
-
process.env[ENV] = OVERRIDE
|
|
54
|
-
const a = createAnalytics({ product: 'console', ingestKey: KEY, dsn: EXPLICIT, enabled: false })
|
|
55
|
-
expect(a.errorIngestUrl).toContain('explicit-project')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('prefers the env override over the registry, so a deploy can repoint a surface', () => {
|
|
59
|
-
process.env[ENV] = OVERRIDE
|
|
60
|
-
const a = createAnalytics({ product: 'console', ingestKey: KEY, enabled: false })
|
|
61
|
-
expect(a.errorIngestUrl).toContain('env-project')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('stays inert for an unregistered product — never posts one surface into another project', () => {
|
|
65
|
-
const a = createAnalytics({ product: 'not-a-product', ingestKey: KEY, enabled: false })
|
|
66
|
-
expect(a.errorPlaneEnabled).toBe(false)
|
|
67
|
-
expect(a.errorIngestUrl).toBeUndefined()
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('stays inert with a product but no key — the error plane needs the live key', () => {
|
|
71
|
-
const a = createAnalytics({ product: 'console', enabled: false })
|
|
72
|
-
expect(a.errorPlaneEnabled).toBe(false)
|
|
73
|
-
expect(a.errorIngestUrl).toBeUndefined()
|
|
74
|
-
})
|
|
75
|
-
})
|
package/src/dsn.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The product → Sentinel DSN registry.
|
|
3
|
-
*
|
|
4
|
-
* An app declares WHAT it is (`product: 'console'`); this module knows WHERE its
|
|
5
|
-
* errors go. That split is the whole point: no surface has to learn a DSN, carry
|
|
6
|
-
* a build argument, or grow a config file to report errors — declaring the
|
|
7
|
-
* product it already declares is enough.
|
|
8
|
-
*
|
|
9
|
-
* A Sentinel DSN is PUBLIC by construction. It ships inside the client bundle and
|
|
10
|
-
* is readable in devtools on any deployed page, and it grants exactly one
|
|
11
|
-
* capability: submitting new events. It cannot read issues, projects, or any
|
|
12
|
-
* other data. So committing it is not leaking a secret — it is recording a public
|
|
13
|
-
* identifier next to the code that needs it. (Contrast the server-side collector
|
|
14
|
-
* DSN in the `team-analytics-sentry` Secret, which is HMAC-derived and revocable
|
|
15
|
-
* precisely because a server-side credential is NOT public.)
|
|
16
|
-
*
|
|
17
|
-
* Why a literal map instead of deriving `hanzo-${product}`: the projects predate
|
|
18
|
-
* this registry and do not derive cleanly — `site` lives in `hanzo-ai`, not
|
|
19
|
-
* `hanzo-site`. An explicit map is honest about that; a derivation rule plus an
|
|
20
|
-
* exception table is the same data with a trap in it.
|
|
21
|
-
*
|
|
22
|
-
* Projects are org-scoped and named `<org>-<app>`. To add one: create the project
|
|
23
|
-
* (POST /v1/sentinel/projects with X-Org-Id), then add its `dsn` here keyed by the
|
|
24
|
-
* product name the app passes to `createAnalytics`.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/** PRODUCT_PROJECT maps a `product` to its Sentinel project id. The DSN's KEY is no
|
|
28
|
-
* longer a per-project secret — it is the ONE org publishable key (below), so a
|
|
29
|
-
* surface's errors ride the SAME key its events do. The id only names WHICH
|
|
30
|
-
* project the errors group under, and cloud auto-provisions that project on first
|
|
31
|
-
* keyed ingest, so a new id needs nothing minted. `site` lives in the `hanzo-ai`
|
|
32
|
-
* project — an explicit map, because the projects predate this and do not derive
|
|
33
|
-
* cleanly from the product name. */
|
|
34
|
-
export const PRODUCT_PROJECT: Readonly<Record<string, string>> = Object.freeze({
|
|
35
|
-
console: '019fa40b-94ae-7f1d-8f7b-e92f123fad42', // console.hanzo.ai
|
|
36
|
-
app: '019f9b1e-57eb-7171-9d92-72c0b85e4b4b', // hanzo.app
|
|
37
|
-
site: '019f9b1e-5785-7359-ad0b-f75db8e58c99', // hanzo.ai (marketing; product `site`)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
/** dsnForProduct builds the product's Sentinel DSN from the caller's resolved
|
|
41
|
-
* publishable `key` and the product's project — the SAME key the event stream
|
|
42
|
-
* carries, at the product's envelope endpoint. Returns undefined when there is no
|
|
43
|
-
* key or no project for the product, leaving the error plane inert rather than
|
|
44
|
-
* posting into the wrong one. The key is NOT baked here: it is the value the
|
|
45
|
-
* surface resolved (an explicit `ingestKey`, or the KMS-sourced
|
|
46
|
-
* NEXT_PUBLIC_PUBLISHABLE_KEY the build inlines) — the ONE live source, never a
|
|
47
|
-
* literal committed beside the code. */
|
|
48
|
-
export function dsnForProduct(
|
|
49
|
-
product: string | undefined,
|
|
50
|
-
key: string | undefined
|
|
51
|
-
): string | undefined {
|
|
52
|
-
if (!product || !key) return undefined
|
|
53
|
-
const projectId = PRODUCT_PROJECT[product]
|
|
54
|
-
if (!projectId) return undefined
|
|
55
|
-
return `https://${key}@api.hanzo.ai/v1/event/${projectId}`
|
|
56
|
-
}
|