@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/hz.js CHANGED
@@ -55,7 +55,7 @@
55
55
  }
56
56
 
57
57
  var LIB = 'hz.js'
58
- var VERSION = '0.3.28'
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.28",
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/sentry/019f9b1e-5785-7359-ad0b-f75db8e58c99'
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/sentry/019f9b1e-5785-7359-ad0b-f75db8e58c99/envelope/' +
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/sentry/019f9b1e-5785-7359-ad0b-f75db8e58c99',
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.origin}/v1/sentry/{projectId}/envelope/?sentry_key=…
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/sentry/env-project'
9
- const EXPLICIT = 'https://1:bbbb@api.hanzo.ai/v1/sentry/explicit-project'
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/sentry/${PRODUCT_PROJECT.console}`
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/sentry/${PRODUCT_PROJECT.site}`
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 → Sentry DSN registry.
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 Sentry DSN is PUBLIC by construction. It ships inside the client bundle and
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/sentry/projects with X-Org-Id), then add its `dsn` here keyed by the
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 Sentry project id. The DSN's KEY is no
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 Sentry DSN from the caller's resolved
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/sentry/${projectId}`
55
+ return `https://${key}@api.hanzo.ai/v1/event/${projectId}`
56
56
  }
@@ -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/sentry/00000000-0000-0000-0000-000000000000'
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/sentry/00000000-0000-0000-0000-000000000000/envelope/?sentry_key=1%3Adeadbeefcafe',
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/sentry/x')).toBeNull() // no key
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/sentry ingest
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/sentry/<projectId>" into its
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
- const ingestUrl =
68
- `${origin}/v1/sentry/${encodeURIComponent(projectId)}/envelope/` +
69
- `?sentry_key=${encodeURIComponent(publicKey)}`
70
- return { publicKey, origin, projectId, ingestUrl }
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: `${dsn.origin}/v1/sentry/${dsn.projectId}`,
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/sentry/<projectId>".
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.28'
4
+ export const VERSION = '0.3.30'