@hanzo/event 0.3.3 → 0.3.5

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.
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+
3
+ import { createAnalytics } from './core'
4
+ import { PRODUCT_DSN, 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/sentry/env-project'
9
+ const EXPLICIT = 'https://1:bbbb@api.hanzo.ai/v1/sentry/explicit-project'
10
+
11
+ afterEach(() => {
12
+ delete process.env[ENV]
13
+ })
14
+
15
+ describe('the product registry', () => {
16
+ it('resolves a registered product to its DSN', () => {
17
+ expect(dsnForProduct('console')).toBe(PRODUCT_DSN.console)
18
+ expect(dsnForProduct('app')).toBe(PRODUCT_DSN.app)
19
+ expect(dsnForProduct('site')).toBe(PRODUCT_DSN.site)
20
+ })
21
+
22
+ it('returns undefined for an unregistered or missing product, rather than guessing', () => {
23
+ expect(dsnForProduct('not-a-product')).toBeUndefined()
24
+ expect(dsnForProduct(undefined)).toBeUndefined()
25
+ expect(dsnForProduct('')).toBeUndefined()
26
+ })
27
+
28
+ it('registers only DSNs that actually parse — a typo here would silently disable a surface', () => {
29
+ for (const [product, dsn] of Object.entries(PRODUCT_DSN)) {
30
+ const parsed = parseDsn(dsn)
31
+ expect(parsed, `${product} DSN must parse`).not.toBeNull()
32
+ expect(parsed!.projectId, `${product} needs a project id`).toBeTruthy()
33
+ expect(parsed!.publicKey, `${product} needs a public key`).toBeTruthy()
34
+ }
35
+ })
36
+ })
37
+
38
+ describe('DSN precedence — most specific source wins', () => {
39
+ it('lights up the error plane from `product` alone, with no dsn and no env', () => {
40
+ const a = createAnalytics({ product: 'console', enabled: false })
41
+ expect(a.errorPlaneEnabled).toBe(true)
42
+ expect(a.errorIngestUrl).toContain(parseDsn(PRODUCT_DSN.console)!.projectId)
43
+ })
44
+
45
+ it('prefers an explicit dsn over both the env and the registry', () => {
46
+ process.env[ENV] = OVERRIDE
47
+ const a = createAnalytics({ product: 'console', dsn: EXPLICIT, enabled: false })
48
+ expect(a.errorIngestUrl).toContain('explicit-project')
49
+ })
50
+
51
+ it('prefers the env override over the registry, so a deploy can repoint a surface', () => {
52
+ process.env[ENV] = OVERRIDE
53
+ const a = createAnalytics({ product: 'console', enabled: false })
54
+ expect(a.errorIngestUrl).toContain('env-project')
55
+ })
56
+
57
+ it('stays inert for an unregistered product — never posts one surface into another project', () => {
58
+ const a = createAnalytics({ product: 'not-a-product', enabled: false })
59
+ expect(a.errorPlaneEnabled).toBe(false)
60
+ expect(a.errorIngestUrl).toBeUndefined()
61
+ })
62
+ })
package/src/dsn.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The product → Sentry 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 Sentry 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/sentry/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_DSN maps a `product` to the DSN its errors are submitted to. */
28
+ export const PRODUCT_DSN: Readonly<Record<string, string>> = Object.freeze({
29
+ // hanzo-console — console.hanzo.ai (also served embedded by the cloud binary)
30
+ console:
31
+ 'https://1:0c8054dbde157f4f420c56b58660052b2ad782293c4de1d606ef8fbc46a0bf34@api.hanzo.ai/v1/sentry/019fa40b-94ae-7f1d-8f7b-e92f123fad42',
32
+ // hanzo-app — hanzo.app
33
+ app: 'https://1:b3e1173125568c80f91ef4b1fabbbd2d7e22341de02b33ce7e22ef4fc16a196e@api.hanzo.ai/v1/sentry/019f9b1e-57eb-7171-9d92-72c0b85e4b4b',
34
+ // hanzo-ai — hanzo.ai (the marketing site; `site` is the product name it declares)
35
+ site: 'https://1:d9cbfb844958bd7ef2a455600f00fbf237fbd71b75c1504f137773096d6aa53f@api.hanzo.ai/v1/sentry/019f9b1e-5785-7359-ad0b-f75db8e58c99',
36
+ })
37
+
38
+ /** dsnForProduct resolves the registered DSN for a product, or undefined when the
39
+ * product has no project yet — which leaves the error plane inert rather than
40
+ * guessing a destination and silently posting a surface's errors into the wrong
41
+ * project. */
42
+ export function dsnForProduct(product: string | undefined): string | undefined {
43
+ if (!product) return undefined
44
+ return PRODUCT_DSN[product]
45
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  export { Analytics, createAnalytics, VERSION, getCohort, getFirstTouch } from './core'
11
11
  export { parseDsn, buildSentryEvent, buildEnvelope, framesFromStack } from './sentry'
12
+ export { PRODUCT_DSN, dsnForProduct } from './dsn'
12
13
  export type { ErrorIdentity } from './sentry'
13
14
  export { scrubText, redactSecrets, scrubPII } from './scrub'
14
15
  export { EVENTS, PAGEVIEW } from './events'