@hanzo/event 0.3.28 → 0.3.30
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/{core-usULsfUB.d.cts → core-ZXDaPEyY.d.cts} +5 -1
- package/dist/{core-usULsfUB.d.ts → core-ZXDaPEyY.d.ts} +5 -1
- package/dist/index.cjs +6 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -9
- package/dist/index.d.ts +9 -9
- package/dist/index.mjs +6 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +6 -5
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.mjs +6 -5
- package/dist/react.mjs.map +1 -1
- package/hz.js +1 -1
- package/package.json +1 -1
- package/src/core.test.ts +3 -3
- package/src/core.ts +1 -1
- package/src/dsn.test.ts +4 -4
- package/src/dsn.ts +6 -6
- package/src/sentry.test.ts +3 -3
- package/src/sentry.ts +11 -7
- package/src/types.ts +5 -1
- package/src/version.ts +1 -1
package/hz.js
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
var LIB = 'hz.js'
|
|
58
|
-
var VERSION = '0.3.
|
|
58
|
+
var VERSION = '0.3.30'
|
|
59
59
|
var host = (s.getAttribute('data-host') || 'https://api.hanzo.ai').replace(/\/+$/, '')
|
|
60
60
|
var product = s.getAttribute('data-product') || location.hostname
|
|
61
61
|
var capture = s.getAttribute('data-capture') !== '0'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/event",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.30",
|
|
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
|
@@ -552,7 +552,7 @@ describe('Event error capture', () => {
|
|
|
552
552
|
const TEST_DSN =
|
|
553
553
|
'https://1:' +
|
|
554
554
|
'0'.repeat(64) +
|
|
555
|
-
'@sentry.hanzo.ai/v1/
|
|
555
|
+
'@sentry.hanzo.ai/v1/event/019f9b1e-5785-7359-ad0b-f75db8e58c99'
|
|
556
556
|
|
|
557
557
|
describe('error plane', () => {
|
|
558
558
|
it('is INERT without a DSN — nothing sent to sentry, event stream unaffected', () => {
|
|
@@ -574,7 +574,7 @@ describe('error plane', () => {
|
|
|
574
574
|
const env = tx.envelopes[0]
|
|
575
575
|
// The exact route the o11y ingest registers, key on the query string.
|
|
576
576
|
expect(env.url).toBe(
|
|
577
|
-
'https://sentry.hanzo.ai/v1/
|
|
577
|
+
'https://sentry.hanzo.ai/v1/event/019f9b1e-5785-7359-ad0b-f75db8e58c99/envelope/' +
|
|
578
578
|
'?sentry_key=1%3A' + '0'.repeat(64),
|
|
579
579
|
)
|
|
580
580
|
expect(env.contentType).toBe('application/x-sentry-envelope')
|
|
@@ -600,7 +600,7 @@ describe('error plane', () => {
|
|
|
600
600
|
// byte length, not the JS string length.
|
|
601
601
|
expect(item.length).toBe(new TextEncoder().encode(payload).length)
|
|
602
602
|
expect(header.dsn).toBe(
|
|
603
|
-
'https://sentry.hanzo.ai/v1/
|
|
603
|
+
'https://sentry.hanzo.ai/v1/event/019f9b1e-5785-7359-ad0b-f75db8e58c99',
|
|
604
604
|
)
|
|
605
605
|
const ev = JSON.parse(payload) as {
|
|
606
606
|
event_id: string
|
package/src/core.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// 2. ERROR PLANE — every captured exception is ALSO framed as a real Sentry
|
|
11
11
|
// envelope and POSTed to the error host named by the DSN:
|
|
12
|
-
// POST {dsn.
|
|
12
|
+
// POST {dsn.base}/envelope/?sentry_key=… ({base} = /v1/event/{projectId})
|
|
13
13
|
// This is what reaches sentry.hanzo.ai (issues, grouping, stack frames).
|
|
14
14
|
//
|
|
15
15
|
// These are NOT the same pipe and one does NOT feed the other. The event stream
|
package/src/dsn.test.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { PRODUCT_PROJECT, dsnForProduct } from './dsn'
|
|
|
5
5
|
import { parseDsn } from './sentry'
|
|
6
6
|
|
|
7
7
|
const ENV = 'NEXT_PUBLIC_HANZO_EVENT_DSN'
|
|
8
|
-
const OVERRIDE = 'https://1:aaaa@api.hanzo.ai/v1/
|
|
9
|
-
const EXPLICIT = 'https://1:bbbb@api.hanzo.ai/v1/
|
|
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
10
|
// A stand-in for the surface's own resolved key — the DSN carries whatever key
|
|
11
11
|
// the caller resolved (config or the KMS-sourced env), never a literal.
|
|
12
12
|
const KEY = 'pk-test-resolved-key'
|
|
@@ -18,10 +18,10 @@ afterEach(() => {
|
|
|
18
18
|
describe('the product registry', () => {
|
|
19
19
|
it('builds a product DSN from the CALLER key + the project — nothing baked', () => {
|
|
20
20
|
expect(dsnForProduct('console', KEY)).toBe(
|
|
21
|
-
`https://${KEY}@api.hanzo.ai/v1/
|
|
21
|
+
`https://${KEY}@api.hanzo.ai/v1/event/${PRODUCT_PROJECT.console}`
|
|
22
22
|
)
|
|
23
23
|
expect(dsnForProduct('site', KEY)).toBe(
|
|
24
|
-
`https://${KEY}@api.hanzo.ai/v1/
|
|
24
|
+
`https://${KEY}@api.hanzo.ai/v1/event/${PRODUCT_PROJECT.site}`
|
|
25
25
|
)
|
|
26
26
|
})
|
|
27
27
|
|
package/src/dsn.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The product →
|
|
2
|
+
* The product → Sentinel DSN registry.
|
|
3
3
|
*
|
|
4
4
|
* An app declares WHAT it is (`product: 'console'`); this module knows WHERE its
|
|
5
5
|
* errors go. That split is the whole point: no surface has to learn a DSN, carry
|
|
6
6
|
* a build argument, or grow a config file to report errors — declaring the
|
|
7
7
|
* product it already declares is enough.
|
|
8
8
|
*
|
|
9
|
-
* A
|
|
9
|
+
* A Sentinel DSN is PUBLIC by construction. It ships inside the client bundle and
|
|
10
10
|
* is readable in devtools on any deployed page, and it grants exactly one
|
|
11
11
|
* capability: submitting new events. It cannot read issues, projects, or any
|
|
12
12
|
* other data. So committing it is not leaking a secret — it is recording a public
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
* exception table is the same data with a trap in it.
|
|
21
21
|
*
|
|
22
22
|
* Projects are org-scoped and named `<org>-<app>`. To add one: create the project
|
|
23
|
-
* (POST /v1/
|
|
23
|
+
* (POST /v1/sentinel/projects with X-Org-Id), then add its `dsn` here keyed by the
|
|
24
24
|
* product name the app passes to `createAnalytics`.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
/** PRODUCT_PROJECT maps a `product` to its
|
|
27
|
+
/** PRODUCT_PROJECT maps a `product` to its Sentinel project id. The DSN's KEY is no
|
|
28
28
|
* longer a per-project secret — it is the ONE org publishable key (below), so a
|
|
29
29
|
* surface's errors ride the SAME key its events do. The id only names WHICH
|
|
30
30
|
* project the errors group under, and cloud auto-provisions that project on first
|
|
@@ -37,7 +37,7 @@ export const PRODUCT_PROJECT: Readonly<Record<string, string>> = Object.freeze({
|
|
|
37
37
|
site: '019f9b1e-5785-7359-ad0b-f75db8e58c99', // hanzo.ai (marketing; product `site`)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
/** dsnForProduct builds the product's
|
|
40
|
+
/** dsnForProduct builds the product's Sentinel DSN from the caller's resolved
|
|
41
41
|
* publishable `key` and the product's project — the SAME key the event stream
|
|
42
42
|
* carries, at the product's envelope endpoint. Returns undefined when there is no
|
|
43
43
|
* key or no project for the product, leaving the error plane inert rather than
|
|
@@ -52,5 +52,5 @@ export function dsnForProduct(
|
|
|
52
52
|
if (!product || !key) return undefined
|
|
53
53
|
const projectId = PRODUCT_PROJECT[product]
|
|
54
54
|
if (!projectId) return undefined
|
|
55
|
-
return `https://${key}@api.hanzo.ai/v1/
|
|
55
|
+
return `https://${key}@api.hanzo.ai/v1/event/${projectId}`
|
|
56
56
|
}
|
package/src/sentry.test.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import type { SentryEvent, SentryFrame } from './types'
|
|
12
12
|
import { VERSION } from './version'
|
|
13
13
|
|
|
14
|
-
const DSN = 'https://1:deadbeefcafe@api.hanzo.ai/v1/
|
|
14
|
+
const DSN = 'https://1:deadbeefcafe@api.hanzo.ai/v1/event/00000000-0000-0000-0000-000000000000'
|
|
15
15
|
|
|
16
16
|
describe('parseDsn', () => {
|
|
17
17
|
it('keeps the version:hmac key intact (does NOT split on the colon) and derives the ingest url', () => {
|
|
@@ -20,14 +20,14 @@ describe('parseDsn', () => {
|
|
|
20
20
|
expect(dsn.origin).toBe('https://api.hanzo.ai')
|
|
21
21
|
expect(dsn.projectId).toBe('00000000-0000-0000-0000-000000000000')
|
|
22
22
|
expect(dsn.ingestUrl).toBe(
|
|
23
|
-
'https://api.hanzo.ai/v1/
|
|
23
|
+
'https://api.hanzo.ai/v1/event/00000000-0000-0000-0000-000000000000/envelope/?sentry_key=1%3Adeadbeefcafe',
|
|
24
24
|
)
|
|
25
25
|
})
|
|
26
26
|
it('returns null for malformed input (fail-safe)', () => {
|
|
27
27
|
expect(parseDsn(undefined)).toBeNull()
|
|
28
28
|
expect(parseDsn('')).toBeNull()
|
|
29
29
|
expect(parseDsn('not-a-dsn')).toBeNull()
|
|
30
|
-
expect(parseDsn('https://api.hanzo.ai/v1/
|
|
30
|
+
expect(parseDsn('https://api.hanzo.ai/v1/event/x')).toBeNull() // no key
|
|
31
31
|
expect(parseDsn('https://1:k@api.hanzo.ai')).toBeNull() // no project
|
|
32
32
|
})
|
|
33
33
|
})
|
package/src/sentry.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Analytics client (core.ts) wires these to identity + transport.
|
|
4
4
|
//
|
|
5
5
|
// The wire shapes are a from-scratch model of the PUBLIC, documented Sentry ingest
|
|
6
|
-
// protocol (develop.sentry.dev), verified against the Hanzo /v1/
|
|
6
|
+
// protocol (develop.sentry.dev), verified against the Hanzo /v1/event ingest
|
|
7
7
|
// (o11y pkg/modules/errortracking/implerrortracking: parseEnvelope, SentryEvent,
|
|
8
8
|
// normalizeEvent, computeFingerprint). No upstream (FSL) code is used.
|
|
9
9
|
|
|
@@ -44,7 +44,7 @@ function byteLen(s: string): number {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* parseDsn parses "https://<version>:<hmac>@<host>/v1/
|
|
47
|
+
* parseDsn parses "https://<version>:<hmac>@<host>/v1/event/<projectId>" into its
|
|
48
48
|
* public key + the derived envelope ingest URL. The key (which itself contains a
|
|
49
49
|
* ':') is taken verbatim as the userinfo — we do NOT split it as user:pass. The
|
|
50
50
|
* key rides ?sentry_key= (not the DSN in the body) because that is the credential
|
|
@@ -64,10 +64,14 @@ export function parseDsn(dsn: string | undefined | null): Dsn | null {
|
|
|
64
64
|
const projectId = segs.length > 0 ? segs[segs.length - 1] : ''
|
|
65
65
|
if (!projectId) return null
|
|
66
66
|
const origin = `${scheme}://${host}`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
// THE DSN'S OWN PATH IS THE ADDRESS — derived here, never restated. It used to
|
|
68
|
+
// be rebuilt as a literal, which threw away the path the DSN carried and wrote
|
|
69
|
+
// the ingest address a second time; buildEnvelope wrote it a third. Three copies
|
|
70
|
+
// of one fact. dsnForProduct (dsn.ts) is the ONE place it is spelled; this
|
|
71
|
+
// follows it.
|
|
72
|
+
const base = `${origin}/${segs.map(encodeURIComponent).join('/')}`
|
|
73
|
+
const ingestUrl = `${base}/envelope/?sentry_key=${encodeURIComponent(publicKey)}`
|
|
74
|
+
return { publicKey, origin, projectId, base, ingestUrl }
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
const V8_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+)|([^)]+))\)?\s*$/
|
|
@@ -295,7 +299,7 @@ export function buildEnvelope(event: SentryEvent, dsn: Dsn, sentAt?: string): st
|
|
|
295
299
|
const payload = JSON.stringify(event)
|
|
296
300
|
const header = JSON.stringify({
|
|
297
301
|
event_id: event.event_id,
|
|
298
|
-
dsn:
|
|
302
|
+
dsn: dsn.base,
|
|
299
303
|
sent_at: sentAt ?? new Date().toISOString(),
|
|
300
304
|
})
|
|
301
305
|
const itemHeader = JSON.stringify({
|
package/src/types.ts
CHANGED
|
@@ -209,7 +209,7 @@ export interface AnalyticsConfig {
|
|
|
209
209
|
|
|
210
210
|
// ── error plane (Sentry envelope -> sentry.hanzo.ai) ──────────────────────
|
|
211
211
|
|
|
212
|
-
/** Hanzo-minted Sentry DSN: "https://<version>:<hmac>@<host>/v1/
|
|
212
|
+
/** Hanzo-minted Sentry DSN: "https://<version>:<hmac>@<host>/v1/event/<projectId>".
|
|
213
213
|
* Publishable — the key authorizes writes to ONE project and can read nothing,
|
|
214
214
|
* so it is safe in a browser bundle (same trust class as `ingestKey`). When
|
|
215
215
|
* absent the client reads NEXT_PUBLIC_HANZO_EVENT_DSN; when neither is set the
|
|
@@ -278,6 +278,10 @@ export interface Dsn {
|
|
|
278
278
|
origin: string
|
|
279
279
|
/** Project id segment. */
|
|
280
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
|
|
281
285
|
/** Fully-derived envelope ingest URL incl. ?sentry_key=. */
|
|
282
286
|
ingestUrl: string
|
|
283
287
|
}
|
package/src/version.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// The library version, stamped on every event (`libraryVersion`) and on the
|
|
2
2
|
// Sentry `sdk` block. It lives alone so `sentry.ts` can read it without importing
|
|
3
3
|
// `core.ts` — core imports sentry, so the reverse would be an import cycle.
|
|
4
|
-
export const VERSION = '0.3.
|
|
4
|
+
export const VERSION = '0.3.30'
|