@hanzo/event 0.3.27 → 0.3.29
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 +7 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +7 -6
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +7 -6
- 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 +7 -6
- 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 +9 -2
- package/src/dsn.test.ts +4 -4
- package/src/dsn.ts +1 -1
- package/src/sentry.test.ts +3 -3
- package/src/sentry.ts +11 -7
- package/src/stream.test.ts +26 -0
- 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.29'
|
|
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.29",
|
|
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
|
|
@@ -699,7 +699,14 @@ function registry(): Map<string, Analytics> {
|
|
|
699
699
|
* This is the ONE way to get a client: `new Analytics` bypasses the registry
|
|
700
700
|
* and is for tests and for a deliberately separate instance. */
|
|
701
701
|
export function createAnalytics(config: AnalyticsConfig): Analytics {
|
|
702
|
-
|
|
702
|
+
// A disabled client emits nothing, so it can double nothing and does not take
|
|
703
|
+
// the stream. Callers reach their answer at different moments — a provider
|
|
704
|
+
// whose ownership resolves in an effect renders once saying `false` before it
|
|
705
|
+
// has decided anything — and the client built in that moment must not become
|
|
706
|
+
// the one the page keeps. Registering it would silence the page, and adopting
|
|
707
|
+
// `enabled` from a later caller would let one surface overrule another's
|
|
708
|
+
// reading of consent. Neither: it is simply not the stream's client.
|
|
709
|
+
if (!isBrowser() || config.enabled === false) return new Analytics(config)
|
|
703
710
|
const key = (config.host ?? DEFAULT_HOST) + '\0' + config.product
|
|
704
711
|
const clients = registry()
|
|
705
712
|
const existing = clients.get(key)
|
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
|
@@ -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/stream.test.ts
CHANGED
|
@@ -95,6 +95,32 @@ describe('one stream, one client', () => {
|
|
|
95
95
|
expect(tx.sent[0].ingestKey).toBe(KEY)
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
+
// A provider whose ownership resolves in an effect renders once saying
|
|
99
|
+
// `enabled: false` and then again saying true. Measured on hanzo.ai: the
|
|
100
|
+
// library's first render built a disabled client, and a registry that kept it
|
|
101
|
+
// handed that one to the app too — the page went silent, which is worse than
|
|
102
|
+
// the double it replaced.
|
|
103
|
+
it('does not let a disabled client take the stream', () => {
|
|
104
|
+
const off = createAnalytics({ product: 'site', host: HOST, enabled: false })
|
|
105
|
+
const on = createAnalytics({ product: 'site', host: HOST, transport: tx, flushIntervalMs: 999999 })
|
|
106
|
+
expect(on).not.toBe(off)
|
|
107
|
+
on.pageview('/pricing')
|
|
108
|
+
on.flush()
|
|
109
|
+
expect(tx.pageviews).toHaveLength(1)
|
|
110
|
+
expect(createAnalytics({ product: 'site', host: HOST, getToken: () => KEY })).toBe(on)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('gives every disabled caller its own inert client', () => {
|
|
114
|
+
const a = createAnalytics({ product: 'site', host: HOST, enabled: false, transport: tx })
|
|
115
|
+
const b = createAnalytics({ product: 'site', host: HOST, enabled: false, transport: tx })
|
|
116
|
+
expect(a).not.toBe(b)
|
|
117
|
+
a.pageview('/pricing')
|
|
118
|
+
b.pageview('/pricing')
|
|
119
|
+
a.flush()
|
|
120
|
+
b.flush()
|
|
121
|
+
expect(tx.sent).toHaveLength(0)
|
|
122
|
+
})
|
|
123
|
+
|
|
98
124
|
it('keeps separate streams separate', () => {
|
|
99
125
|
expect(createAnalytics({ product: 'chat', host: HOST })).not.toBe(
|
|
100
126
|
createAnalytics({ product: 'site', host: HOST }),
|
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.29'
|