@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/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-ingest-key="pk-…" // required off api.hanzo.ai's own origin
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.16'
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
- var key = s.getAttribute('data-ingest-key') || ''
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.16",
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.NEXT_PUBLIC_EVENT_INGEST_KEY = 'pk-live-from-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.NEXT_PUBLIC_EVENT_INGEST_KEY
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.NEXT_PUBLIC_EVENT_INGEST_KEY = 'pk-live-from-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.NEXT_PUBLIC_EVENT_INGEST_KEY
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.NEXT_PUBLIC_EVENT_INGEST_KEY = 'pk-live-one-org'
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.NEXT_PUBLIC_EVENT_INGEST_KEY
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.NEXT_PUBLIC_EVENT_INGEST_KEY = 'pk-live-one-org'
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.NEXT_PUBLIC_EVENT_INGEST_KEY
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: an explicit
242
- // config wins, else the inlined build-time env.
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
- // NEXT_PUBLIC_EVENT_INGEST_KEY is that env, and it is the name the fleet
245
- // ALREADY carries end to end KMS holds deploy/EVENT_INGEST_KEY, each
246
- // Dockerfile takes it as the EVENT_INGEST_KEY build-arg and re-exports it
247
- // with the NEXT_PUBLIC_ prefix Next needs to inline it. Reading anything
248
- // else here would add a fourth spelling of one value.
249
- //
250
- // Without this the key was the one piece of wiring a surface could not
251
- // declare the way it declares every other piece, so a surface that shipped
252
- // without passing it in code sent its beacons unattributed — and an
253
- // unattributed write is refused (401 ingest_key_required), which is silent
254
- // in the page and invisible until you read the warehouse and find the host
255
- // missing entirely.
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 ingest key as a bearer on fetch', () => {
234
- const r = runSnippet({ attrs: { 'data-ingest-key': 'pk-abc123' } })
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('presents the ingest key in the query on a headerless beacon', () => {
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('sends no credential when no key is declared', () => {
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 NEXT_PUBLIC_EVENT_INGEST_KEY from the inlined
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/EVENT_INGEST_KEY, and each
127
- * Dockerfile takes EVENT_INGEST_KEY as a build-arg and re-exports it with the
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.16'
4
+ export const VERSION = '0.3.18'