@hanzo/event 0.3.17 → 0.3.19
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/index.cjs +33 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -8
- package/dist/index.d.ts +15 -8
- package/dist/index.mjs +33 -25
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +32 -24
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +32 -24
- package/dist/react.mjs.map +1 -1
- package/hz.js +7 -3
- package/package.json +1 -1
- package/src/core.test.ts +37 -1
- package/src/core.ts +21 -16
- package/src/dsn.test.ts +14 -11
- package/src/dsn.ts +45 -14
- package/src/hz.test.ts +10 -1
- package/src/index.ts +1 -1
- package/src/version.ts +1 -1
package/src/core.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
2
|
import { Analytics, VERSION } from './core'
|
|
3
|
+
import { HANZO_PUBLISHABLE_KEY } from './dsn'
|
|
3
4
|
import { EVENTS, PAGEVIEW } from './events'
|
|
4
5
|
import type { Transport, WireEvent } from './types'
|
|
5
6
|
import pkg from '../package.json' with { type: 'json' }
|
|
@@ -383,13 +384,48 @@ describe('Analytics capture', () => {
|
|
|
383
384
|
}
|
|
384
385
|
})
|
|
385
386
|
|
|
386
|
-
it('stays keyless when neither config nor env names a key', () => {
|
|
387
|
+
it('stays keyless when neither config nor env names a key, off the hanzo cloud', () => {
|
|
388
|
+
// mk() uses host:'' — a same-origin app, deliberately NOT defaulted.
|
|
387
389
|
const a = mk()
|
|
388
390
|
a.capture('x')
|
|
389
391
|
a.flush(true)
|
|
390
392
|
expect(tx.sent[0].ingestKey).toBeUndefined()
|
|
391
393
|
})
|
|
392
394
|
|
|
395
|
+
it('bakes the hanzo publishable key as the default ON the hanzo cloud', () => {
|
|
396
|
+
// publishable_key for all: a hanzo surface that passes no key and inlines no
|
|
397
|
+
// env still emits attributed, the same way declaring `product` is enough for
|
|
398
|
+
// the DSN. No wiring, no $public, no silent drop of track/identify/group.
|
|
399
|
+
const a = mk({ host: 'https://api.hanzo.ai' })
|
|
400
|
+
a.capture('x')
|
|
401
|
+
a.flush(true)
|
|
402
|
+
expect(tx.sent[0].ingestKey).toBe(HANZO_PUBLISHABLE_KEY)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('does NOT bake the default for a white-label (non-hanzo) host', () => {
|
|
406
|
+
const a = mk({ host: 'https://api.zoo.ngo' })
|
|
407
|
+
a.capture('x')
|
|
408
|
+
a.flush(true)
|
|
409
|
+
expect(tx.sent[0].ingestKey).toBeUndefined()
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('lets an explicit key and the build env each override the baked default', () => {
|
|
413
|
+
const explicit = mk({ host: 'https://api.hanzo.ai', ingestKey: 'pk-explicit' })
|
|
414
|
+
explicit.capture('x')
|
|
415
|
+
explicit.flush(true)
|
|
416
|
+
expect(tx.sent[0].ingestKey).toBe('pk-explicit')
|
|
417
|
+
|
|
418
|
+
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-from-env'
|
|
419
|
+
try {
|
|
420
|
+
const env = mk({ host: 'https://api.hanzo.ai' }) // mk() reassigns `tx`
|
|
421
|
+
env.capture('y')
|
|
422
|
+
env.flush(true)
|
|
423
|
+
expect(tx.sent[0].ingestKey).toBe('pk-from-env')
|
|
424
|
+
} finally {
|
|
425
|
+
delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
|
|
393
429
|
it('a signed-in bearer WINS over a key from the build env', () => {
|
|
394
430
|
// The leak this closes: one console bundle is served to several brands, and a
|
|
395
431
|
// pk- names ONE org. If an env-sourced key displaced the bearer, every
|
package/src/core.ts
CHANGED
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
hasAttribution,
|
|
44
44
|
deriveChannel,
|
|
45
45
|
} from './attribution'
|
|
46
|
-
import { dsnForProduct } from './dsn'
|
|
46
|
+
import { dsnForProduct, defaultPublishableKey } from './dsn'
|
|
47
47
|
import { PAGEVIEW } from './events'
|
|
48
48
|
import { scrubText } from './scrub'
|
|
49
49
|
import {
|
|
@@ -238,22 +238,27 @@ export class Analytics {
|
|
|
238
238
|
enabled: true,
|
|
239
239
|
captureErrors: true,
|
|
240
240
|
...config,
|
|
241
|
-
// The publishable key resolves the SAME way the DSN below does
|
|
242
|
-
//
|
|
241
|
+
// The publishable key resolves the SAME way the DSN below does, and now
|
|
242
|
+
// ends the SAME way too — in a baked default, so declaring nothing is
|
|
243
|
+
// enough. Most specific first:
|
|
244
|
+
// 1. an explicit `ingestKey` in config;
|
|
245
|
+
// 2. NEXT_PUBLIC_PUBLISHABLE_KEY, the inlined build-time env the fleet
|
|
246
|
+
// carries end to end (KMS `deploy/PUBLISHABLE_KEY` -> the PUBLISHABLE_KEY
|
|
247
|
+
// build-arg -> the NEXT_PUBLIC_ prefix Next inlines);
|
|
248
|
+
// 3. the hanzo org key baked in `dsn.ts`, on the hanzo cloud only.
|
|
243
249
|
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
ingestKey: config.ingestKey ?? readEnv('NEXT_PUBLIC_PUBLISHABLE_KEY'),
|
|
250
|
+
// (3) is why this is `publishable_key for all`: a hanzo surface that passes
|
|
251
|
+
// no key in code and inlines no env still emits attributed, exactly as
|
|
252
|
+
// declaring `product` is enough for the DSN. Without it that surface sent
|
|
253
|
+
// its beacons to `$public` — which drops every track/identify/group and
|
|
254
|
+
// still answers 200 — silent in the page and invisible until you read the
|
|
255
|
+
// warehouse and find the host missing entirely. A white-label surface on
|
|
256
|
+
// its own cloud (a non-default host) gets no default, so the org this
|
|
257
|
+
// attributes to is never the wrong one.
|
|
258
|
+
ingestKey:
|
|
259
|
+
config.ingestKey ??
|
|
260
|
+
readEnv('NEXT_PUBLIC_PUBLISHABLE_KEY') ??
|
|
261
|
+
defaultPublishableKey(config.host ?? DEFAULT_HOST),
|
|
257
262
|
}
|
|
258
263
|
this.transport = config.transport ?? new DefaultTransport()
|
|
259
264
|
// Error plane, most specific source first: an explicit DSN wins, then the
|
package/src/dsn.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import { createAnalytics } from './core'
|
|
4
|
-
import {
|
|
4
|
+
import { PRODUCT_PROJECT, dsnForProduct, HANZO_PUBLISHABLE_KEY } from './dsn'
|
|
5
5
|
import { parseDsn } from './sentry'
|
|
6
6
|
|
|
7
7
|
const ENV = 'NEXT_PUBLIC_HANZO_EVENT_DSN'
|
|
@@ -13,10 +13,13 @@ afterEach(() => {
|
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
describe('the product registry', () => {
|
|
16
|
-
it('
|
|
17
|
-
expect(dsnForProduct('console')).toBe(
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
it('builds a product DSN — the org publishable key at its project envelope', () => {
|
|
17
|
+
expect(dsnForProduct('console')).toBe(
|
|
18
|
+
`https://${HANZO_PUBLISHABLE_KEY}@api.hanzo.ai/v1/sentry/${PRODUCT_PROJECT.console}`
|
|
19
|
+
)
|
|
20
|
+
expect(dsnForProduct('site')).toBe(
|
|
21
|
+
`https://${HANZO_PUBLISHABLE_KEY}@api.hanzo.ai/v1/sentry/${PRODUCT_PROJECT.site}`
|
|
22
|
+
)
|
|
20
23
|
})
|
|
21
24
|
|
|
22
25
|
it('returns undefined for an unregistered or missing product, rather than guessing', () => {
|
|
@@ -25,12 +28,12 @@ describe('the product registry', () => {
|
|
|
25
28
|
expect(dsnForProduct('')).toBeUndefined()
|
|
26
29
|
})
|
|
27
30
|
|
|
28
|
-
it('
|
|
29
|
-
for (const [product,
|
|
30
|
-
const parsed = parseDsn(
|
|
31
|
+
it('every product DSN parses to the org key + its own project id', () => {
|
|
32
|
+
for (const [product, projectId] of Object.entries(PRODUCT_PROJECT)) {
|
|
33
|
+
const parsed = parseDsn(dsnForProduct(product))
|
|
31
34
|
expect(parsed, `${product} DSN must parse`).not.toBeNull()
|
|
32
|
-
expect(parsed!.projectId, `${product}
|
|
33
|
-
expect(parsed!.publicKey, `${product}
|
|
35
|
+
expect(parsed!.projectId, `${product} project id`).toBe(projectId)
|
|
36
|
+
expect(parsed!.publicKey, `${product} carries the org key`).toBe(HANZO_PUBLISHABLE_KEY)
|
|
34
37
|
}
|
|
35
38
|
})
|
|
36
39
|
})
|
|
@@ -39,7 +42,7 @@ describe('DSN precedence — most specific source wins', () => {
|
|
|
39
42
|
it('lights up the error plane from `product` alone, with no dsn and no env', () => {
|
|
40
43
|
const a = createAnalytics({ product: 'console', enabled: false })
|
|
41
44
|
expect(a.errorPlaneEnabled).toBe(true)
|
|
42
|
-
expect(a.errorIngestUrl).toContain(
|
|
45
|
+
expect(a.errorIngestUrl).toContain(PRODUCT_PROJECT.console)
|
|
43
46
|
})
|
|
44
47
|
|
|
45
48
|
it('prefers an explicit dsn over both the env and the registry', () => {
|
package/src/dsn.ts
CHANGED
|
@@ -24,22 +24,53 @@
|
|
|
24
24
|
* product name the app passes to `createAnalytics`.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
/** PRODUCT_PROJECT maps a `product` to its Sentry 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`)
|
|
36
38
|
})
|
|
37
39
|
|
|
38
|
-
/** dsnForProduct
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
/** dsnForProduct builds the product's Sentry DSN — the ONE org publishable key at
|
|
41
|
+
* its project's envelope endpoint — or undefined when the product has no project
|
|
42
|
+
* yet, which leaves the error plane inert rather than posting into the wrong one.
|
|
43
|
+
* Same key as the event stream: cloud resolves it to the org and attributes the
|
|
44
|
+
* errors there. */
|
|
42
45
|
export function dsnForProduct(product: string | undefined): string | undefined {
|
|
43
46
|
if (!product) return undefined
|
|
44
|
-
|
|
47
|
+
const projectId = PRODUCT_PROJECT[product]
|
|
48
|
+
if (!projectId) return undefined
|
|
49
|
+
return `https://${HANZO_PUBLISHABLE_KEY}@api.hanzo.ai/v1/sentry/${projectId}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The hanzo org's publishable ingest key. Like the DSNs above it is PUBLIC by
|
|
53
|
+
* construction: write-only — it attributes a write and mints no reading
|
|
54
|
+
* principal — so it ships in the bundle and is readable in devtools, exactly as
|
|
55
|
+
* hanzo.id already inlines it. It is the value KMS holds at `deploy/PUBLISHABLE_KEY`
|
|
56
|
+
* and every fleet Dockerfile passes as the `PUBLISHABLE_KEY` build-arg; baking it
|
|
57
|
+
* as the default is the SAME move `dsnForProduct` makes for errors — a hanzo
|
|
58
|
+
* surface on the hanzo cloud emits attributed with zero wiring, and no build can
|
|
59
|
+
* ship unkeyed (the failure that files anonymous traffic under `$public`, which
|
|
60
|
+
* drops every track/identify/group and answers 200). */
|
|
61
|
+
export const HANZO_PUBLISHABLE_KEY =
|
|
62
|
+
'pk-live-c88649f1085fb6ad441d8a0072933a9b'
|
|
63
|
+
|
|
64
|
+
/** defaultPublishableKey resolves the baked hanzo key, but ONLY for the explicit
|
|
65
|
+
* hanzo cloud host (api.hanzo.ai) — the host the public, anonymous-traffic
|
|
66
|
+
* surfaces use. It is deliberately NOT applied to a same-origin `''` host: that
|
|
67
|
+
* is the shape a cookie/session app uses (it attributes signed-in users through
|
|
68
|
+
* the session, so it needs no anonymous key), AND it is the one host a
|
|
69
|
+
* white-label surface shares with hanzo, so defaulting it could attribute the
|
|
70
|
+
* wrong org. A custom host gets undefined. An explicit `ingestKey`, or an
|
|
71
|
+
* inlined NEXT_PUBLIC_PUBLISHABLE_KEY, still wins over this. */
|
|
72
|
+
export function defaultPublishableKey(host: string | undefined): string | undefined {
|
|
73
|
+
return host === undefined || host === 'https://api.hanzo.ai'
|
|
74
|
+
? HANZO_PUBLISHABLE_KEY
|
|
75
|
+
: undefined
|
|
45
76
|
}
|
package/src/hz.test.ts
CHANGED
|
@@ -247,9 +247,18 @@ describe('hz.js', () => {
|
|
|
247
247
|
expect(post.url).toBe('https://api.hanzo.ai/v1/event?ingest_key=pk-abc123')
|
|
248
248
|
})
|
|
249
249
|
|
|
250
|
-
it('
|
|
250
|
+
it('falls to the baked hanzo key when no key is declared on the hanzo cloud', () => {
|
|
251
|
+
// publishable_key for all: a bare tag on the hanzo cloud still emits
|
|
252
|
+
// attributed, rather than dropping unkeyed to $public.
|
|
251
253
|
run.api!.flush()
|
|
252
254
|
const post = run.posts.at(-1)!
|
|
255
|
+
expect(post.headers.authorization).toBe('Bearer pk-live-c88649f1085fb6ad441d8a0072933a9b')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('sends no credential when no key is declared and the host is not the hanzo cloud', () => {
|
|
259
|
+
const r = runSnippet({ attrs: { 'data-host': 'https://api.zoo.ngo' } })
|
|
260
|
+
r.api!.flush()
|
|
261
|
+
const post = r.posts.at(-1)!
|
|
253
262
|
expect(post.headers.authorization).toBeUndefined()
|
|
254
263
|
expect(post.url).not.toContain('ingest_key')
|
|
255
264
|
})
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export { Analytics, createAnalytics, VERSION, getCohort, getFirstTouch } from './core'
|
|
11
11
|
export { parseDsn, buildSentryEvent, buildEnvelope, framesFromStack } from './sentry'
|
|
12
12
|
export { uuidv7, uuidv7Time } from './uid'
|
|
13
|
-
export {
|
|
13
|
+
export { PRODUCT_PROJECT, dsnForProduct } from './dsn'
|
|
14
14
|
export type { ErrorIdentity } from './sentry'
|
|
15
15
|
export { scrubText, redactSecrets, scrubPII } from './scrub'
|
|
16
16
|
export { EVENTS, PAGEVIEW } from './events'
|
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.19'
|