@hanzo/event 0.3.20 → 0.3.22
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 +61 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -58
- package/dist/index.d.ts +12 -58
- package/dist/index.mjs +21 -78
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +20 -30
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +18 -28
- package/dist/react.mjs.map +1 -1
- package/hz.js +7 -8
- package/package.json +4 -1
- package/src/core.test.ts +3 -37
- package/src/core.ts +18 -24
- package/src/dsn.test.ts +28 -18
- package/src/dsn.ts +14 -34
- package/src/events.ts +21 -76
- package/src/hz.test.ts +4 -10
- package/src/version.ts +1 -1
package/hz.js
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
var LIB = 'hz.js'
|
|
58
|
-
var VERSION = '0.3.
|
|
58
|
+
var VERSION = '0.3.21'
|
|
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,13 +67,12 @@
|
|
|
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
|
-
// data-publishable-key is the name;
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
var key = s.getAttribute('data-publishable-key') || s.getAttribute('data-ingest-key') ||
|
|
76
|
-
(host === 'https://api.hanzo.ai' ? 'pk-live-c88649f1085fb6ad441d8a0072933a9b' : '')
|
|
70
|
+
// data-publishable-key is the name; data-ingest-key is the retiring spelling.
|
|
71
|
+
// The value is the surface's own KMS-sourced key, stamped into the tag by the
|
|
72
|
+
// deploy — never a literal baked in here. With neither attribute the tag is
|
|
73
|
+
// keyless and its writes file under $public; the fix is to stamp the key, not
|
|
74
|
+
// to hardcode the org's credential into this file.
|
|
75
|
+
var key = s.getAttribute('data-publishable-key') || s.getAttribute('data-ingest-key') || ''
|
|
77
76
|
|
|
78
77
|
// ── the shared anonymous-identity chain ───────────────────────────────────
|
|
79
78
|
// 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.22",
|
|
4
4
|
"description": "Hanzo Event \u2014 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 \u2014 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/",
|
|
@@ -81,5 +81,8 @@
|
|
|
81
81
|
"tsup": "^8.5.1",
|
|
82
82
|
"typescript": "^5.9.3",
|
|
83
83
|
"vitest": "^4.1.0"
|
|
84
|
+
},
|
|
85
|
+
"dependencies": {
|
|
86
|
+
"@hanzo/events": "^0.2.0"
|
|
84
87
|
}
|
|
85
88
|
}
|
package/src/core.test.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
2
|
import { Analytics, VERSION } from './core'
|
|
3
|
-
import { HANZO_PUBLISHABLE_KEY } from './dsn'
|
|
4
3
|
import { EVENTS, PAGEVIEW } from './events'
|
|
5
4
|
import type { Transport, WireEvent } from './types'
|
|
6
5
|
import pkg from '../package.json' with { type: 'json' }
|
|
@@ -384,48 +383,15 @@ describe('Analytics capture', () => {
|
|
|
384
383
|
}
|
|
385
384
|
})
|
|
386
385
|
|
|
387
|
-
it('stays keyless when neither config nor env names a key
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
a.capture('x')
|
|
391
|
-
a.flush(true)
|
|
392
|
-
expect(tx.sent[0].ingestKey).toBeUndefined()
|
|
393
|
-
})
|
|
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.
|
|
386
|
+
it('stays keyless when neither config nor env names a key — no baked literal', () => {
|
|
387
|
+
// The key comes from ONE live source (config or the KMS-sourced env); nothing
|
|
388
|
+
// is hardcoded, so a surface that provides neither is honestly keyless.
|
|
399
389
|
const a = mk({ host: 'https://api.hanzo.ai' })
|
|
400
390
|
a.capture('x')
|
|
401
391
|
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
392
|
expect(tx.sent[0].ingestKey).toBeUndefined()
|
|
410
393
|
})
|
|
411
394
|
|
|
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
|
-
|
|
429
395
|
it('a signed-in bearer WINS over a key from the build env', () => {
|
|
430
396
|
// The leak this closes: one console bundle is served to several brands, and a
|
|
431
397
|
// 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
|
|
46
|
+
import { dsnForProduct } from './dsn'
|
|
47
47
|
import { EXCEPTION, PAGEVIEW } from './events'
|
|
48
48
|
import { exceptionProperties } from './exception'
|
|
49
49
|
import { scrubText } from './scrub'
|
|
@@ -239,35 +239,29 @@ export class Analytics {
|
|
|
239
239
|
enabled: true,
|
|
240
240
|
captureErrors: true,
|
|
241
241
|
...config,
|
|
242
|
-
// The publishable key resolves
|
|
243
|
-
//
|
|
244
|
-
// enough. Most specific first:
|
|
242
|
+
// The publishable key resolves from ONE live source, never a literal baked
|
|
243
|
+
// beside the code. Most specific first:
|
|
245
244
|
// 1. an explicit `ingestKey` in config;
|
|
246
245
|
// 2. NEXT_PUBLIC_PUBLISHABLE_KEY, the inlined build-time env the fleet
|
|
247
|
-
// carries end to end
|
|
248
|
-
// build-arg -> the NEXT_PUBLIC_ prefix Next inlines
|
|
249
|
-
// 3. the hanzo org key baked in `dsn.ts`, on the hanzo cloud only.
|
|
246
|
+
// carries end to end — KMS `deploy/PUBLISHABLE_KEY` -> the PUBLISHABLE_KEY
|
|
247
|
+
// build-arg -> the NEXT_PUBLIC_ prefix Next inlines.
|
|
250
248
|
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
// its own cloud (a non-default host) gets no default, so the org this
|
|
258
|
-
// attributes to is never the wrong one.
|
|
259
|
-
ingestKey:
|
|
260
|
-
config.ingestKey ??
|
|
261
|
-
readEnv('NEXT_PUBLIC_PUBLISHABLE_KEY') ??
|
|
262
|
-
defaultPublishableKey(config.host ?? DEFAULT_HOST),
|
|
249
|
+
// A surface that provides neither has no key, and its anonymous traffic
|
|
250
|
+
// files under `$public` (which drops track/identify/group and answers 200);
|
|
251
|
+
// the fix is to give it the KMS-sourced env, not to hardcode the org key
|
|
252
|
+
// here. The key is a credential-class value: its home is KMS, and the ONE
|
|
253
|
+
// build reads it from there.
|
|
254
|
+
ingestKey: config.ingestKey ?? readEnv('NEXT_PUBLIC_PUBLISHABLE_KEY'),
|
|
263
255
|
}
|
|
264
256
|
this.transport = config.transport ?? new DefaultTransport()
|
|
265
257
|
// Error plane, most specific source first: an explicit DSN wins, then the
|
|
266
|
-
// inlined build-time env (a per-deploy override), then the product registry
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
// throwing into the host app.
|
|
270
|
-
this.dsn = parseDsn(
|
|
258
|
+
// inlined build-time env (a per-deploy override), then the product registry —
|
|
259
|
+
// whose DSN carries the SAME resolved key (not a baked one), so declaring
|
|
260
|
+
// `product` + providing the key is enough to report errors. Malformed or
|
|
261
|
+
// absent => null => inert, never throwing into the host app.
|
|
262
|
+
this.dsn = parseDsn(
|
|
263
|
+
config.dsn ?? readEnvDsn() ?? dsnForProduct(this.cfg.product, this.cfg.ingestKey)
|
|
264
|
+
)
|
|
271
265
|
}
|
|
272
266
|
|
|
273
267
|
/** errorPlaneEnabled reports whether captured exceptions can actually reach the
|
package/src/dsn.test.ts
CHANGED
|
@@ -1,64 +1,74 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import { createAnalytics } from './core'
|
|
4
|
-
import { PRODUCT_PROJECT, dsnForProduct
|
|
4
|
+
import { PRODUCT_PROJECT, dsnForProduct } from './dsn'
|
|
5
5
|
import { parseDsn } from './sentry'
|
|
6
6
|
|
|
7
7
|
const ENV = 'NEXT_PUBLIC_HANZO_EVENT_DSN'
|
|
8
8
|
const OVERRIDE = 'https://1:aaaa@api.hanzo.ai/v1/sentry/env-project'
|
|
9
9
|
const EXPLICIT = 'https://1:bbbb@api.hanzo.ai/v1/sentry/explicit-project'
|
|
10
|
+
// A stand-in for the surface's own resolved key — the DSN carries whatever key
|
|
11
|
+
// the caller resolved (config or the KMS-sourced env), never a literal.
|
|
12
|
+
const KEY = 'pk-test-resolved-key'
|
|
10
13
|
|
|
11
14
|
afterEach(() => {
|
|
12
15
|
delete process.env[ENV]
|
|
13
16
|
})
|
|
14
17
|
|
|
15
18
|
describe('the product registry', () => {
|
|
16
|
-
it('builds a product DSN
|
|
17
|
-
expect(dsnForProduct('console')).toBe(
|
|
18
|
-
`https://${
|
|
19
|
+
it('builds a product DSN from the CALLER key + the project — nothing baked', () => {
|
|
20
|
+
expect(dsnForProduct('console', KEY)).toBe(
|
|
21
|
+
`https://${KEY}@api.hanzo.ai/v1/sentry/${PRODUCT_PROJECT.console}`
|
|
19
22
|
)
|
|
20
|
-
expect(dsnForProduct('site')).toBe(
|
|
21
|
-
`https://${
|
|
23
|
+
expect(dsnForProduct('site', KEY)).toBe(
|
|
24
|
+
`https://${KEY}@api.hanzo.ai/v1/sentry/${PRODUCT_PROJECT.site}`
|
|
22
25
|
)
|
|
23
26
|
})
|
|
24
27
|
|
|
25
|
-
it('returns undefined
|
|
26
|
-
expect(dsnForProduct('
|
|
27
|
-
expect(dsnForProduct(
|
|
28
|
-
expect(dsnForProduct(
|
|
28
|
+
it('returns undefined without a key or without a project, rather than guessing', () => {
|
|
29
|
+
expect(dsnForProduct('console', undefined)).toBeUndefined() // no key -> inert
|
|
30
|
+
expect(dsnForProduct('not-a-product', KEY)).toBeUndefined()
|
|
31
|
+
expect(dsnForProduct(undefined, KEY)).toBeUndefined()
|
|
32
|
+
expect(dsnForProduct('', KEY)).toBeUndefined()
|
|
29
33
|
})
|
|
30
34
|
|
|
31
|
-
it('every product DSN parses to the
|
|
35
|
+
it('every product DSN parses to the caller key + its own project id', () => {
|
|
32
36
|
for (const [product, projectId] of Object.entries(PRODUCT_PROJECT)) {
|
|
33
|
-
const parsed = parseDsn(dsnForProduct(product))
|
|
37
|
+
const parsed = parseDsn(dsnForProduct(product, KEY))
|
|
34
38
|
expect(parsed, `${product} DSN must parse`).not.toBeNull()
|
|
35
39
|
expect(parsed!.projectId, `${product} project id`).toBe(projectId)
|
|
36
|
-
expect(parsed!.publicKey, `${product} carries the
|
|
40
|
+
expect(parsed!.publicKey, `${product} carries the resolved key`).toBe(KEY)
|
|
37
41
|
}
|
|
38
42
|
})
|
|
39
43
|
})
|
|
40
44
|
|
|
41
45
|
describe('DSN precedence — most specific source wins', () => {
|
|
42
|
-
it('lights up the error plane from `product`
|
|
43
|
-
const a = createAnalytics({ product: 'console', enabled: false })
|
|
46
|
+
it('lights up the error plane from `product` + a resolved key, with no explicit dsn', () => {
|
|
47
|
+
const a = createAnalytics({ product: 'console', ingestKey: KEY, enabled: false })
|
|
44
48
|
expect(a.errorPlaneEnabled).toBe(true)
|
|
45
49
|
expect(a.errorIngestUrl).toContain(PRODUCT_PROJECT.console)
|
|
46
50
|
})
|
|
47
51
|
|
|
48
52
|
it('prefers an explicit dsn over both the env and the registry', () => {
|
|
49
53
|
process.env[ENV] = OVERRIDE
|
|
50
|
-
const a = createAnalytics({ product: 'console', dsn: EXPLICIT, enabled: false })
|
|
54
|
+
const a = createAnalytics({ product: 'console', ingestKey: KEY, dsn: EXPLICIT, enabled: false })
|
|
51
55
|
expect(a.errorIngestUrl).toContain('explicit-project')
|
|
52
56
|
})
|
|
53
57
|
|
|
54
58
|
it('prefers the env override over the registry, so a deploy can repoint a surface', () => {
|
|
55
59
|
process.env[ENV] = OVERRIDE
|
|
56
|
-
const a = createAnalytics({ product: 'console', enabled: false })
|
|
60
|
+
const a = createAnalytics({ product: 'console', ingestKey: KEY, enabled: false })
|
|
57
61
|
expect(a.errorIngestUrl).toContain('env-project')
|
|
58
62
|
})
|
|
59
63
|
|
|
60
64
|
it('stays inert for an unregistered product — never posts one surface into another project', () => {
|
|
61
|
-
const a = createAnalytics({ product: 'not-a-product', enabled: false })
|
|
65
|
+
const a = createAnalytics({ product: 'not-a-product', ingestKey: KEY, enabled: false })
|
|
66
|
+
expect(a.errorPlaneEnabled).toBe(false)
|
|
67
|
+
expect(a.errorIngestUrl).toBeUndefined()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('stays inert with a product but no key — the error plane needs the live key', () => {
|
|
71
|
+
const a = createAnalytics({ product: 'console', enabled: false })
|
|
62
72
|
expect(a.errorPlaneEnabled).toBe(false)
|
|
63
73
|
expect(a.errorIngestUrl).toBeUndefined()
|
|
64
74
|
})
|
package/src/dsn.ts
CHANGED
|
@@ -37,40 +37,20 @@ export const PRODUCT_PROJECT: Readonly<Record<string, string>> = Object.freeze({
|
|
|
37
37
|
site: '019f9b1e-5785-7359-ad0b-f75db8e58c99', // hanzo.ai (marketing; product `site`)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
/** dsnForProduct builds the product's Sentry DSN
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
/** dsnForProduct builds the product's Sentry DSN from the caller's resolved
|
|
41
|
+
* publishable `key` and the product's project — the SAME key the event stream
|
|
42
|
+
* carries, at the product's envelope endpoint. Returns undefined when there is no
|
|
43
|
+
* key or no project for the product, leaving the error plane inert rather than
|
|
44
|
+
* posting into the wrong one. The key is NOT baked here: it is the value the
|
|
45
|
+
* surface resolved (an explicit `ingestKey`, or the KMS-sourced
|
|
46
|
+
* NEXT_PUBLIC_PUBLISHABLE_KEY the build inlines) — the ONE live source, never a
|
|
47
|
+
* literal committed beside the code. */
|
|
48
|
+
export function dsnForProduct(
|
|
49
|
+
product: string | undefined,
|
|
50
|
+
key: string | undefined
|
|
51
|
+
): string | undefined {
|
|
52
|
+
if (!product || !key) return undefined
|
|
47
53
|
const projectId = PRODUCT_PROJECT[product]
|
|
48
54
|
if (!projectId) return undefined
|
|
49
|
-
return `https://${
|
|
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
|
|
55
|
+
return `https://${key}@api.hanzo.ai/v1/sentry/${projectId}`
|
|
76
56
|
}
|
package/src/events.ts
CHANGED
|
@@ -1,76 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
LOGIN_COMPLETED: 'login_completed',
|
|
23
|
-
/** Activation: the first moment of real value. ONE event for every product —
|
|
24
|
-
* the product-specific moment is the `action` property (api_call, app_live,
|
|
25
|
-
* chat_reply), never a new event name. */
|
|
26
|
-
FIRST_ACTION: 'first_action',
|
|
27
|
-
|
|
28
|
-
// Waitlist + referral.
|
|
29
|
-
WAITLIST_JOINED: 'waitlist_joined',
|
|
30
|
-
WAITLIST_SHARED: 'waitlist_shared',
|
|
31
|
-
REFERRAL_USED: 'referral_used',
|
|
32
|
-
REFERRAL_CLAIMED: 'referral_claimed',
|
|
33
|
-
|
|
34
|
-
// Upgrade-intent + purchase.
|
|
35
|
-
PRICING_VIEWED: 'pricing_viewed',
|
|
36
|
-
PLAN_CLICKED: 'plan_clicked',
|
|
37
|
-
CHECKOUT_STARTED: 'checkout_started',
|
|
38
|
-
ORDER_COMPLETED: 'order_completed',
|
|
39
|
-
|
|
40
|
-
// Feature usage — generic + the common key surfaces across products.
|
|
41
|
-
FEATURE_USED: 'feature_used',
|
|
42
|
-
API_KEY_CREATED: 'api_key_created',
|
|
43
|
-
APP_CREATED: 'app_created',
|
|
44
|
-
PROJECT_CREATED: 'project_created',
|
|
45
|
-
AGENT_CREATED: 'agent_created',
|
|
46
|
-
CHAT_STARTED: 'chat_started',
|
|
47
|
-
CHAT_MESSAGE_SENT: 'chat_message_sent',
|
|
48
|
-
/** The user switched model/endpoint — the single strongest quality signal a
|
|
49
|
-
* chat surface emits (a switch usually follows a bad answer). */
|
|
50
|
-
MODEL_SWITCHED: 'model_switched',
|
|
51
|
-
TASK_STARTED: 'task_started',
|
|
52
|
-
TASK_COMPLETED: 'task_completed',
|
|
53
|
-
|
|
54
|
-
// Build → ship. `build_*` is a MODEL producing an artifact; `deploy_*` is that
|
|
55
|
-
// artifact going live. Intent (build_started) is never the same event as the
|
|
56
|
-
// artifact existing (app_created) — conflating them makes the funnel lie.
|
|
57
|
-
BUILD_STARTED: 'build_started',
|
|
58
|
-
/** A model finished producing an artifact (an app build, a chat reply, an agent
|
|
59
|
-
* run). Carries `durationMs` — the outcome event owns its own duration, so no
|
|
60
|
-
* paired start event is needed. */
|
|
61
|
-
GENERATION_COMPLETED: 'generation_completed',
|
|
62
|
-
GENERATION_FAILED: 'generation_failed',
|
|
63
|
-
DEPLOY_STARTED: 'deploy_started',
|
|
64
|
-
DEPLOY_SUCCEEDED: 'deploy_succeeded',
|
|
65
|
-
DEPLOY_FAILED: 'deploy_failed',
|
|
66
|
-
} as const
|
|
67
|
-
|
|
68
|
-
export type EventName = (typeof EVENTS)[keyof typeof EVENTS]
|
|
69
|
-
|
|
70
|
-
/** The reserved event name a pageview is stored under (server + read lens). */
|
|
71
|
-
export const PAGEVIEW = '$pageview'
|
|
72
|
-
|
|
73
|
-
/** The reserved name every captured exception is emitted under. Error Tracking
|
|
74
|
-
* reads exactly this name; an exception emitted under its own message instead
|
|
75
|
-
* makes every distinct message a permanent entry in the event taxonomy. */
|
|
76
|
-
export const EXCEPTION = '$exception'
|
|
1
|
+
/**
|
|
2
|
+
* The vocabulary, re-exported from where it is now defined.
|
|
3
|
+
*
|
|
4
|
+
* These names used to be declared here. They moved to @hanzo/events — the
|
|
5
|
+
* plural package is the catalog (many events, what each means, what each
|
|
6
|
+
* carries), this one is the client (one call sends one event). Splitting them
|
|
7
|
+
* that way is what lets the ingest door read the same catalog: the door is Go
|
|
8
|
+
* and cannot import TypeScript, so @hanzo/events also ships the data as JSON,
|
|
9
|
+
* and there is still exactly one definition.
|
|
10
|
+
*
|
|
11
|
+
* Nothing about the API moved. Every existing import keeps working, the names
|
|
12
|
+
* and their values are unchanged, and `pkgs/events/test/catalog.mjs` fails the
|
|
13
|
+
* build if a name and its meaning ever disagree.
|
|
14
|
+
*
|
|
15
|
+
* The naming convention lives with the names, in @hanzo/events and TAXONOMY.md:
|
|
16
|
+
* snake_case `<object>_<verb-past>`, a dimension is a PROPERTY and never part
|
|
17
|
+
* of the name, one name per user-visible moment shared by every surface.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { EVENTS, PAGEVIEW, EXCEPTION } from '@hanzo/events'
|
|
21
|
+
export type { EventName } from '@hanzo/events'
|
package/src/hz.test.ts
CHANGED
|
@@ -247,18 +247,12 @@ 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('
|
|
251
|
-
//
|
|
252
|
-
//
|
|
250
|
+
it('sends no credential when no key is declared — no baked literal', () => {
|
|
251
|
+
// The key is the surface's own, stamped into the tag by its deploy from KMS;
|
|
252
|
+
// a bare tag carries nothing, so it is honestly keyless rather than borrowing
|
|
253
|
+
// a hardcoded org credential.
|
|
253
254
|
run.api!.flush()
|
|
254
255
|
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)!
|
|
262
256
|
expect(post.headers.authorization).toBeUndefined()
|
|
263
257
|
expect(post.url).not.toContain('ingest_key')
|
|
264
258
|
})
|
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.21'
|