@hanzo/event 0.3.16 → 0.3.18
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/TAXONOMY.md +1 -1
- package/dist/{core-BZraSvcr.d.cts → core-D4PaPlOB.d.cts} +3 -3
- package/dist/{core-BZraSvcr.d.ts → core-D4PaPlOB.d.ts} +3 -3
- package/dist/index.cjs +22 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +22 -16
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +22 -16
- 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 +22 -16
- package/dist/react.mjs.map +1 -1
- package/hz.js +9 -3
- package/package.json +1 -1
- package/src/core.test.ts +45 -9
- package/src/core.ts +21 -16
- package/src/dsn.ts +26 -0
- package/src/hz.test.ts +13 -4
- package/src/types.ts +3 -3
- package/src/version.ts +1 -1
package/hz.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* <script async src="https://unpkg.com/@hanzo/event/hz.js"
|
|
11
11
|
* data-product="hanzo.ai" // required: which surface this is
|
|
12
|
-
* data-
|
|
12
|
+
* data-publishable-key="pk-…" // required off api.hanzo.ai's own origin
|
|
13
13
|
* data-host="https://api.hanzo.ai" // optional: API host override
|
|
14
14
|
* data-ga="G-XXXX" data-fb="123" // optional: also fan out to GA4 / Meta
|
|
15
15
|
* data-capture="1"></script> // optional: autocapture off with "0"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
var LIB = 'hz.js'
|
|
58
|
-
var VERSION = '0.3.
|
|
58
|
+
var VERSION = '0.3.18'
|
|
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'
|
|
@@ -67,7 +67,13 @@
|
|
|
67
67
|
// nothing here reads the response. Through 0.3.11 this file had no way to
|
|
68
68
|
// present a key at all, so every keyed static surface looked wired, measured
|
|
69
69
|
// fine in the browser, and filed nothing.
|
|
70
|
-
|
|
70
|
+
// data-publishable-key is the name; a static tag has no lockstep build to
|
|
71
|
+
// migrate it, so data-ingest-key stays readable as the retiring spelling. With
|
|
72
|
+
// neither, a tag on the hanzo cloud falls to the baked hanzo org key (the same
|
|
73
|
+
// default the npm client bakes in dsn.ts), so a bare tag still emits attributed
|
|
74
|
+
// rather than dropping to $public. A custom data-host gets no default.
|
|
75
|
+
var key = s.getAttribute('data-publishable-key') || s.getAttribute('data-ingest-key') ||
|
|
76
|
+
(host === 'https://api.hanzo.ai' ? 'pk-live-c88649f1085fb6ad441d8a0072933a9b' : '')
|
|
71
77
|
|
|
72
78
|
// ── the shared anonymous-identity chain ───────────────────────────────────
|
|
73
79
|
// COPIED VERBATIM from @hanzo/event's src/anon.js, markers and all, for the same
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/event",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
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
|
@@ -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' }
|
|
@@ -360,41 +361,76 @@ describe('Analytics capture', () => {
|
|
|
360
361
|
// The failure this closes is silent: a surface with no key attributes nothing
|
|
361
362
|
// for a logged-out visitor, the door refuses the write, and the page shows no
|
|
362
363
|
// sign of it. The key must resolve from the env exactly as the DSN does.
|
|
363
|
-
process.env.
|
|
364
|
+
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-live-from-env'
|
|
364
365
|
try {
|
|
365
366
|
const a = mk() // no key in config
|
|
366
367
|
a.capture('x')
|
|
367
368
|
a.flush(true)
|
|
368
369
|
expect(tx.sent[0].ingestKey).toBe('pk-live-from-env')
|
|
369
370
|
} finally {
|
|
370
|
-
delete process.env.
|
|
371
|
+
delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
|
|
371
372
|
}
|
|
372
373
|
})
|
|
373
374
|
|
|
374
375
|
it('prefers an explicit ingest key over the build env', () => {
|
|
375
|
-
process.env.
|
|
376
|
+
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-live-from-env'
|
|
376
377
|
try {
|
|
377
378
|
const a = mk({ ingestKey: 'pk-live-explicit' })
|
|
378
379
|
a.capture('x')
|
|
379
380
|
a.flush(true)
|
|
380
381
|
expect(tx.sent[0].ingestKey).toBe('pk-live-explicit')
|
|
381
382
|
} finally {
|
|
382
|
-
delete process.env.
|
|
383
|
+
delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
|
|
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
|
|
396
432
|
// signed-in user's events would re-file under whichever org minted the key.
|
|
397
|
-
process.env.
|
|
433
|
+
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-live-one-org'
|
|
398
434
|
try {
|
|
399
435
|
const a = mk({ getToken: () => 'jwt-of-a-real-person' })
|
|
400
436
|
a.capture('x')
|
|
@@ -402,12 +438,12 @@ describe('Analytics capture', () => {
|
|
|
402
438
|
expect(tx.sent[0].token).toBe('jwt-of-a-real-person')
|
|
403
439
|
expect(tx.sent[0].ingestKey).toBeUndefined()
|
|
404
440
|
} finally {
|
|
405
|
-
delete process.env.
|
|
441
|
+
delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
|
|
406
442
|
}
|
|
407
443
|
})
|
|
408
444
|
|
|
409
445
|
it('an anonymous visitor still rides the key', () => {
|
|
410
|
-
process.env.
|
|
446
|
+
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-live-one-org'
|
|
411
447
|
try {
|
|
412
448
|
const a = mk({ getToken: () => undefined }) // logged out
|
|
413
449
|
a.capture('x')
|
|
@@ -415,7 +451,7 @@ describe('Analytics capture', () => {
|
|
|
415
451
|
expect(tx.sent[0].ingestKey).toBe('pk-live-one-org')
|
|
416
452
|
expect(tx.sent[0].token).toBeUndefined()
|
|
417
453
|
} finally {
|
|
418
|
-
delete process.env.
|
|
454
|
+
delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
|
|
419
455
|
}
|
|
420
456
|
})
|
|
421
457
|
|
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_EVENT_INGEST_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.ts
CHANGED
|
@@ -43,3 +43,29 @@ export function dsnForProduct(product: string | undefined): string | undefined {
|
|
|
43
43
|
if (!product) return undefined
|
|
44
44
|
return PRODUCT_DSN[product]
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/** The hanzo org's publishable ingest key. Like the DSNs above it is PUBLIC by
|
|
48
|
+
* construction: write-only — it attributes a write and mints no reading
|
|
49
|
+
* principal — so it ships in the bundle and is readable in devtools, exactly as
|
|
50
|
+
* hanzo.id already inlines it. It is the value KMS holds at `deploy/PUBLISHABLE_KEY`
|
|
51
|
+
* and every fleet Dockerfile passes as the `PUBLISHABLE_KEY` build-arg; baking it
|
|
52
|
+
* as the default is the SAME move `dsnForProduct` makes for errors — a hanzo
|
|
53
|
+
* surface on the hanzo cloud emits attributed with zero wiring, and no build can
|
|
54
|
+
* ship unkeyed (the failure that files anonymous traffic under `$public`, which
|
|
55
|
+
* drops every track/identify/group and answers 200). */
|
|
56
|
+
export const HANZO_PUBLISHABLE_KEY =
|
|
57
|
+
'pk-live-c88649f1085fb6ad441d8a0072933a9b'
|
|
58
|
+
|
|
59
|
+
/** defaultPublishableKey resolves the baked hanzo key, but ONLY for the explicit
|
|
60
|
+
* hanzo cloud host (api.hanzo.ai) — the host the public, anonymous-traffic
|
|
61
|
+
* surfaces use. It is deliberately NOT applied to a same-origin `''` host: that
|
|
62
|
+
* is the shape a cookie/session app uses (it attributes signed-in users through
|
|
63
|
+
* the session, so it needs no anonymous key), AND it is the one host a
|
|
64
|
+
* white-label surface shares with hanzo, so defaulting it could attribute the
|
|
65
|
+
* wrong org. A custom host gets undefined. An explicit `ingestKey`, or an
|
|
66
|
+
* inlined NEXT_PUBLIC_PUBLISHABLE_KEY, still wins over this. */
|
|
67
|
+
export function defaultPublishableKey(host: string | undefined): string | undefined {
|
|
68
|
+
return host === undefined || host === 'https://api.hanzo.ai'
|
|
69
|
+
? HANZO_PUBLISHABLE_KEY
|
|
70
|
+
: undefined
|
|
71
|
+
}
|
package/src/hz.test.ts
CHANGED
|
@@ -230,8 +230,8 @@ describe('hz.js', () => {
|
|
|
230
230
|
// keyed static surface therefore sent UNATTRIBUTED writes, which the door
|
|
231
231
|
// refuses — silently, because nothing here reads the response.
|
|
232
232
|
|
|
233
|
-
it('presents the
|
|
234
|
-
const r = runSnippet({ attrs: { 'data-
|
|
233
|
+
it('presents the publishable key as a bearer on fetch', () => {
|
|
234
|
+
const r = runSnippet({ attrs: { 'data-publishable-key': 'pk-abc123' } })
|
|
235
235
|
r.api!.flush()
|
|
236
236
|
const post = r.posts.at(-1)!
|
|
237
237
|
expect(post.via).toBe('fetch')
|
|
@@ -239,7 +239,7 @@ describe('hz.js', () => {
|
|
|
239
239
|
expect(post.url).toBe('https://api.hanzo.ai/v1/event')
|
|
240
240
|
})
|
|
241
241
|
|
|
242
|
-
it('
|
|
242
|
+
it('still reads the retiring data-ingest-key, on a headerless beacon', () => {
|
|
243
243
|
const r = runSnippet({ attrs: { 'data-ingest-key': 'pk-abc123' }, beacon: true })
|
|
244
244
|
r.api!.flush()
|
|
245
245
|
const post = r.posts.at(-1)!
|
|
@@ -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/types.ts
CHANGED
|
@@ -120,11 +120,11 @@ export interface AnalyticsConfig {
|
|
|
120
120
|
* a reading principal — so it is safe to ship in a bundle. Mint one per org with
|
|
121
121
|
* POST /v1/keys {"type":"publishable"}.
|
|
122
122
|
*
|
|
123
|
-
* Omit it and the client reads
|
|
123
|
+
* Omit it and the client reads NEXT_PUBLIC_PUBLISHABLE_KEY from the inlined
|
|
124
124
|
* build env, the same way `dsn` falls back — so a surface declares BOTH planes
|
|
125
125
|
* in its build and neither needs code to switch on. That is the ONE spelling
|
|
126
|
-
* the fleet already carries: KMS holds deploy/
|
|
127
|
-
* Dockerfile takes
|
|
126
|
+
* the fleet already carries: KMS holds deploy/PUBLISHABLE_KEY, and each
|
|
127
|
+
* Dockerfile takes PUBLISHABLE_KEY as a build-arg and re-exports it with the
|
|
128
128
|
* NEXT_PUBLIC_ prefix that makes Next inline it.
|
|
129
129
|
*
|
|
130
130
|
* A surface with no key at all still reports for whoever is SIGNED IN (the
|
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.18'
|