@hanzo/event 0.3.49 → 0.3.50
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/core.d.ts.map +1 -1
- package/dist/index.cjs +29 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +29 -8
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +8 -7
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +8 -7
- package/dist/react.mjs.map +1 -1
- package/dist/tags.d.ts +8 -0
- package/dist/tags.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core.test.ts +5 -5
- package/src/core.ts +15 -17
- package/src/index.ts +1 -1
- package/src/org.test.ts +9 -0
- package/src/org.ts +1 -1
- package/src/tags.ts +28 -0
package/dist/tags.d.ts
CHANGED
|
@@ -22,6 +22,14 @@ export interface TagOptions {
|
|
|
22
22
|
* given a key.
|
|
23
23
|
*/
|
|
24
24
|
export declare const SITE_KEY: Readonly<Record<string, string>>;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves once every tag this visitor's choice loads has taken the page view, or after
|
|
27
|
+
* `limit` ms, whichever is first: the tag set has answered, gtag.js has loaded or failed,
|
|
28
|
+
* and Meta's library has replaced the stub that queued the call. A page that is about to
|
|
29
|
+
* leave waits on this so the visit is counted where it arrived. A tag the choice does not
|
|
30
|
+
* load is not waited for.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ready(limit?: number): Promise<void>;
|
|
25
33
|
/**
|
|
26
34
|
* Starts the tag manager: fetches the site's tag set and loads what consent
|
|
27
35
|
* allows, again on every consent change. Safe to call on every page load, and
|
package/dist/tags.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAEvC,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA;AAEvC,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAepD,CAAA;AAqCF;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,KAAK,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjD;AAoLD;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,CAAC,GAAE,UAAe,GAAG,MAAM,IAAI,CAmCpD;AAOD,uFAAuF;AACvF,wBAAgB,KAAK,IAAI,MAAM,CAc9B;AA+CD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,SAAU,GAAG,IAAI,CAiCpG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CACnB,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;AACpC,kGAAkG;AAClG,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CAsBN;AAED,gGAAgG;AAChG,wBAAgB,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAG/C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/event",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.50",
|
|
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
|
@@ -396,17 +396,17 @@ describe('Analytics capture', () => {
|
|
|
396
396
|
expect(tx.sent[0].ingestKey).toBeUndefined()
|
|
397
397
|
})
|
|
398
398
|
|
|
399
|
-
it('a signed-in
|
|
400
|
-
// The
|
|
401
|
-
//
|
|
402
|
-
//
|
|
399
|
+
it('a signed-in page sends the key AND the bearer; the bearer never replaces the key', () => {
|
|
400
|
+
// The person is the bearer and the project is the key. The server files the event
|
|
401
|
+
// under the key's project only when it belongs to the bearer's own org, so a key
|
|
402
|
+
// from the build env cannot re-file a person's events under another org.
|
|
403
403
|
process.env.NEXT_PUBLIC_PUBLISHABLE_KEY = 'pk-live-one-org'
|
|
404
404
|
try {
|
|
405
405
|
const a = mk({ getToken: () => 'jwt-of-a-real-person' })
|
|
406
406
|
a.capture('x')
|
|
407
407
|
a.flush()
|
|
408
408
|
expect(tx.sent[0].token).toBe('jwt-of-a-real-person')
|
|
409
|
-
expect(tx.sent[0].ingestKey).
|
|
409
|
+
expect(tx.sent[0].ingestKey).toBe('pk-live-one-org')
|
|
410
410
|
} finally {
|
|
411
411
|
delete process.env.NEXT_PUBLIC_PUBLISHABLE_KEY
|
|
412
412
|
}
|
package/src/core.ts
CHANGED
|
@@ -105,6 +105,12 @@ function normalizeError(err: unknown): Exception {
|
|
|
105
105
|
// unload listeners RN doesn't have), while `capture()`/`flush()` — which are NOT
|
|
106
106
|
// gated on this and post through plain `fetch` — still emit. So the client is
|
|
107
107
|
// inert-safe under SSR/Node and live on RN, without a second transport.
|
|
108
|
+
/** The screen and language a page view carries, for the readers that derive device and locale. */
|
|
109
|
+
const viewport = (): Record<string, string> =>
|
|
110
|
+
isBrowser()
|
|
111
|
+
? { screen: `${window.screen?.width ?? 0}x${window.screen?.height ?? 0}`, language: navigator.language || '' }
|
|
112
|
+
: {}
|
|
113
|
+
|
|
108
114
|
const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
109
115
|
|
|
110
116
|
/** serializeBatch stringifies a batch, salvaging what it can. `properties` is
|
|
@@ -186,13 +192,15 @@ class DefaultTransport implements Transport {
|
|
|
186
192
|
// for an origin that cannot pass one. A JWT is first-party by construction, so
|
|
187
193
|
// it keeps the header and the session it travels with, and so does the error
|
|
188
194
|
// plane, which names its own envelope type.
|
|
189
|
-
const simple = opts.ingestKey !== undefined && opts.contentType === undefined
|
|
195
|
+
const simple = opts.ingestKey !== undefined && opts.token === undefined && opts.contentType === undefined
|
|
190
196
|
const headers: Record<string, string> = {
|
|
191
197
|
'Content-Type': simple ? BEACON_CONTENT_TYPE : (opts.contentType ?? 'application/json'),
|
|
192
198
|
}
|
|
193
|
-
|
|
199
|
+
// A signed-in page sends the key (which project) in the query AND the person's bearer
|
|
200
|
+
// (who) in the header; the bearer never replaces the key.
|
|
201
|
+
const bearer = simple ? undefined : (opts.token ?? opts.ingestKey)
|
|
194
202
|
if (bearer) headers.Authorization = `Bearer ${bearer}`
|
|
195
|
-
void fetch(
|
|
203
|
+
void fetch(opts.ingestKey ? appendQuery(url, 'ingest_key', opts.ingestKey) : url, {
|
|
196
204
|
method: 'POST',
|
|
197
205
|
headers,
|
|
198
206
|
body,
|
|
@@ -375,7 +383,7 @@ export class Analytics {
|
|
|
375
383
|
const view = (p ?? '') + '\0' + (isBrowser() ? window.location.href : '')
|
|
376
384
|
if (view === this.counted) return
|
|
377
385
|
this.counted = view
|
|
378
|
-
this.enqueue('pageview', PAGEVIEW, { path: p, properties })
|
|
386
|
+
this.enqueue('pageview', PAGEVIEW, { path: p, properties: { ...viewport(), ...properties } })
|
|
379
387
|
}
|
|
380
388
|
|
|
381
389
|
/** capture records a named product event with optional properties. Commerce
|
|
@@ -441,20 +449,10 @@ export class Analytics {
|
|
|
441
449
|
this.queue = []
|
|
442
450
|
this.clearTimer()
|
|
443
451
|
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
// a pk- names one org for everybody holding it. So the key is what attributes
|
|
447
|
-
// a visitor nobody has vouched for, and it must never displace someone who
|
|
448
|
-
// has been.
|
|
449
|
-
//
|
|
450
|
-
// The precedence used to run the other way, which was survivable only while
|
|
451
|
-
// the key had to be passed in code. Once it also resolves from the build env,
|
|
452
|
-
// key-wins means setting one variable silently blanks every signed-in user's
|
|
453
|
-
// token and re-files their events under whichever org minted the key — on a
|
|
454
|
-
// console served to several brands from one bundle, that is a cross-tenant
|
|
455
|
-
// leak introduced by an env var.
|
|
452
|
+
// Two kinds of credential, and both ride when both are held: the publishable key
|
|
453
|
+
// names the project, the bearer names the person. The bearer never replaces the key.
|
|
456
454
|
const token = this.cfg.getToken?.() ?? undefined
|
|
457
|
-
const key =
|
|
455
|
+
const key = this.cfg.ingestKey?.trim() || undefined
|
|
458
456
|
// Only a bearer JWT blocks the beacon: sendBeacon cannot set an Authorization
|
|
459
457
|
// header. A pk- rides ?ingest_key; a cookie rides credentials.
|
|
460
458
|
const useBeacon = beacon && !token
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export {
|
|
|
29
29
|
} from './consent'
|
|
30
30
|
export type { Choice, Region } from './consent'
|
|
31
31
|
export { CLICK_IDS, capture as captureClick, touch } from './touch'
|
|
32
|
-
export { start as startTags, track, mirror, reach, visit, SITE_KEY } from './tags'
|
|
32
|
+
export { start as startTags, ready as tagsReady, track, mirror, reach, visit, SITE_KEY } from './tags'
|
|
33
33
|
export type { BrowserTag, TagOptions } from './tags'
|
|
34
34
|
export { commerceItem, worth } from './items'
|
|
35
35
|
export type { CommerceItem, PlanLine } from './items'
|
package/src/org.test.ts
CHANGED
|
@@ -127,3 +127,12 @@ describe('a surface that configures nothing', () => {
|
|
|
127
127
|
expect((none as unknown as { cfg: { ingestKey?: string } }).cfg.ingestKey).toBeUndefined()
|
|
128
128
|
})
|
|
129
129
|
})
|
|
130
|
+
|
|
131
|
+
describe('the hanzo org files under the hanzo.ai project', () => {
|
|
132
|
+
it('names hanzo.network, hanzo.market, hanzo.industries and hanzo.agency', () => {
|
|
133
|
+
for (const host of ['hanzo.network', 'hanzo.market', 'hanzo.industries', 'hanzo.agency']) {
|
|
134
|
+
expect(orgOf(host)).toBe('hanzo')
|
|
135
|
+
expect(keyFor(host)).toBe('pk-CmfLA2K6kvsPflrS9DSkt06H_kSoQB_21sjedt6VJdc')
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
})
|
package/src/org.ts
CHANGED
|
@@ -93,7 +93,7 @@ export const ORG_DOMAIN: Readonly<Record<string, readonly string[]>> = Object.fr
|
|
|
93
93
|
* recreate a project, move BOTH.
|
|
94
94
|
*/
|
|
95
95
|
export const ORG_KEY: Keyring = Object.freeze({
|
|
96
|
-
hanzo: 'pk-
|
|
96
|
+
hanzo: 'pk-CmfLA2K6kvsPflrS9DSkt06H_kSoQB_21sjedt6VJdc',
|
|
97
97
|
lux: 'pk-gUZp6ZVfhJzSwK-rb4oLbVkpCnMBx5uSCpxf_5yEhQk',
|
|
98
98
|
zoo: 'pk-3TKpKnERV9AQSsBUERWkZejC1O1mUxc1jRzsP3MPbs4',
|
|
99
99
|
})
|
package/src/tags.ts
CHANGED
|
@@ -69,6 +69,11 @@ export const SITE_KEY: Readonly<Record<string, string>> = Object.freeze({
|
|
|
69
69
|
'platform.hanzo.ai': 'pk-My1RpZLEUnTj8vAdPbWKYdDUuhxZJ4dVjHcwjZN4rZ8',
|
|
70
70
|
'docs.hanzo.ai': 'pk-jukhtjMT2ymoeBDAeFjINQWlBv-v9sNn1TPztiCrrwk',
|
|
71
71
|
'hanzo.bot': 'pk-W5d7Mn7ZukT7igyscIy6Pqe8JpA0Ge604Yn4xNR4JCU',
|
|
72
|
+
'hanzo.chat': 'pk-ATxWeB2jNyRSxDgqnkGoDj3CCbzrmTxSktUh9zlrO-E',
|
|
73
|
+
'hanzo.id': 'pk-_GVF3KlaqprlPHDumBMRFeFU1lIGOaarQfgimNVDIo8',
|
|
74
|
+
'hanzo.industries': 'pk-Vh1wvL2HqIAv9qpVvmrznTI9uMHrTD-jyIC-vnlBsgY',
|
|
75
|
+
'hanzo.market': 'pk-DN3xQFa0H61epUVg9OkiOYLzCdteNt9hE6f-S-ednjQ',
|
|
76
|
+
'hanzo.network': 'pk-YJ2eiA_7ckLStMPte8Ar8FsmRp_ycmeIeRbpGprZYmM',
|
|
72
77
|
'cloud.hanzo.ai': 'pk-RAfEGHPoNdCEU9fnA_cPd_Xo9Tci44rlYQV9xuJ1Ob0',
|
|
73
78
|
})
|
|
74
79
|
|
|
@@ -107,6 +112,29 @@ function flush(): void {
|
|
|
107
112
|
if (settled() || leaving) held.splice(0).forEach((send) => send())
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Resolves once every tag this visitor's choice loads has taken the page view, or after
|
|
117
|
+
* `limit` ms, whichever is first: the tag set has answered, gtag.js has loaded or failed,
|
|
118
|
+
* and Meta's library has replaced the stub that queued the call. A page that is about to
|
|
119
|
+
* leave waits on this so the visit is counted where it arrived. A tag the choice does not
|
|
120
|
+
* load is not waited for.
|
|
121
|
+
*/
|
|
122
|
+
export function ready(limit = 3000): Promise<void> {
|
|
123
|
+
if (typeof window === 'undefined') return Promise.resolve()
|
|
124
|
+
const start = Date.now()
|
|
125
|
+
const meta = () => {
|
|
126
|
+
const fbq = page().fbq
|
|
127
|
+
return ![...loaded].some((k) => k.startsWith('meta:')) || typeof fbq?.callMethod === 'function'
|
|
128
|
+
}
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const tick = () => {
|
|
131
|
+
if ((configured && settled() && meta()) || Date.now() - start >= limit) return resolve()
|
|
132
|
+
setTimeout(tick, 100)
|
|
133
|
+
}
|
|
134
|
+
tick()
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
110
138
|
function script(src: string, onload?: () => void, onerror?: () => void): void {
|
|
111
139
|
const s = document.createElement('script')
|
|
112
140
|
s.async = true
|