@hanzo/event 0.3.17 → 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
@@ -55,7 +55,7 @@
55
55
  }
56
56
 
57
57
  var LIB = 'hz.js'
58
- var VERSION = '0.3.17'
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'
@@ -68,8 +68,12 @@
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.
72
- var key = s.getAttribute('data-publishable-key') || s.getAttribute('data-ingest-key') || ''
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' : '')
73
77
 
74
78
  // ── the shared anonymous-identity chain ───────────────────────────────────
75
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.17",
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' }
@@ -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: 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_PUBLISHABLE_KEY is that env, and it is the name the fleet
245
- // ALREADY carries end to end KMS holds deploy/PUBLISHABLE_KEY, each
246
- // Dockerfile takes it as the PUBLISHABLE_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_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.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
@@ -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/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.17'
4
+ export const VERSION = '0.3.18'