@hanzo/event 0.3.50 → 0.3.52
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/consent.d.ts +19 -0
- package/dist/consent.d.ts.map +1 -1
- package/dist/index.cjs +67 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +62 -22
- package/dist/index.mjs.map +1 -1
- package/dist/org.d.ts +10 -0
- package/dist/org.d.ts.map +1 -1
- package/dist/react.cjs +41 -10
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.mjs +41 -10
- package/dist/react.mjs.map +1 -1
- package/dist/tags.d.ts +0 -7
- package/dist/tags.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/consent.test.ts +60 -3
- package/src/consent.ts +67 -9
- package/src/index.ts +4 -4
- package/src/link.test.ts +4 -3
- package/src/org.test.ts +25 -5
- package/src/org.ts +38 -0
- package/src/react.tsx +6 -2
- package/src/tags.ts +5 -26
package/dist/tags.d.ts
CHANGED
|
@@ -15,13 +15,6 @@ export interface TagOptions {
|
|
|
15
15
|
/** Domains one visit crosses, so GA4 keeps it one session. */
|
|
16
16
|
domains?: string[];
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* The publishable key of each site's project, which is how cloud finds the site's
|
|
20
|
-
* tag set. A `pk-` is public by design (it ships in every page); it names a site,
|
|
21
|
-
* never a platform id. A host absent here has no tag config unless `start` is
|
|
22
|
-
* given a key.
|
|
23
|
-
*/
|
|
24
|
-
export declare const SITE_KEY: Readonly<Record<string, string>>;
|
|
25
18
|
/**
|
|
26
19
|
* Resolves once every tag this visitor's choice loads has taken the page view, or after
|
|
27
20
|
* `limit` ms, whichever is first: the tag set has answered, gtag.js has loaded or failed,
|
package/dist/tags.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../src/tags.ts"],"names":[],"mappings":"AAqCA,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;AAqCD;;;;;;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,CAoCpD;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.52",
|
|
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/consent.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Consent: opt-in outside the US, opt-out inside it, GPC always wins.
|
|
2
2
|
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
-
import { CONSENT_COOKIE, acceptAll, asks, read, region, rejectAll, save } from './consent'
|
|
4
|
+
import { CONSENT_COOKIE, CONSENT_VERSION_COOKIE, acceptAll, asks, notices, read, region, rejectAll, save, serve } from './consent'
|
|
5
5
|
import { touch, capture } from './touch'
|
|
6
6
|
|
|
7
7
|
const g = globalThis as Record<string, unknown>
|
|
@@ -42,6 +42,7 @@ function browser(zone: string, opts: { gpc?: boolean; cookie?: string; href?: st
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
afterEach(() => {
|
|
45
|
+
serve(null)
|
|
45
46
|
for (const k of ['window', 'document', 'Event']) delete g[k]
|
|
46
47
|
for (const [k, d] of Object.entries(original)) if (d) Object.defineProperty(globalThis, k, d)
|
|
47
48
|
vi.restoreAllMocks()
|
|
@@ -77,9 +78,9 @@ describe('the default before a choice', () => {
|
|
|
77
78
|
expect(asks()).toBe(false)
|
|
78
79
|
})
|
|
79
80
|
|
|
80
|
-
it('Global Privacy Control turns marketing off in the US', () => {
|
|
81
|
+
it('Global Privacy Control turns marketing and ads off in the US, analytics stays', () => {
|
|
81
82
|
browser('America/Denver', { gpc: true })
|
|
82
|
-
expect(read()).toEqual({ analytics:
|
|
83
|
+
expect(read()).toEqual({ analytics: true, marketing: false, ads: false })
|
|
83
84
|
})
|
|
84
85
|
|
|
85
86
|
it('GPC wins over a stored yes', () => {
|
|
@@ -138,3 +139,59 @@ describe('click ids', () => {
|
|
|
138
139
|
expect(touch({ analytics: true, marketing: false, ads: false })).not.toHaveProperty('gclid')
|
|
139
140
|
})
|
|
140
141
|
})
|
|
142
|
+
|
|
143
|
+
describe('the rule cloud serves', () => {
|
|
144
|
+
const rule = (mode: 'opt-in' | 'opt-out', version = 1) => ({
|
|
145
|
+
region: mode === 'opt-in' ? 'DE' : 'US',
|
|
146
|
+
mode,
|
|
147
|
+
version,
|
|
148
|
+
gpc: false,
|
|
149
|
+
defaults:
|
|
150
|
+
mode === 'opt-in'
|
|
151
|
+
? { analytics: false, marketing: false, ads: false }
|
|
152
|
+
: { analytics: true, marketing: true, ads: true },
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('beats the time zone, both ways', () => {
|
|
156
|
+
browser('America/New_York')
|
|
157
|
+
serve(rule('opt-in'))
|
|
158
|
+
expect(region()).toBe('opt-in')
|
|
159
|
+
expect(asks()).toBe(true)
|
|
160
|
+
expect(read()).toEqual({ analytics: false, marketing: false, ads: false })
|
|
161
|
+
browser('Europe/Berlin')
|
|
162
|
+
serve(rule('opt-out'))
|
|
163
|
+
expect(asks()).toBe(false)
|
|
164
|
+
expect(notices()).toBe(true)
|
|
165
|
+
expect(read()).toEqual({ analytics: true, marketing: true, ads: true })
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('tells the page when it arrives, so a banner drawn early redraws', () => {
|
|
169
|
+
const b = browser('Europe/Berlin')
|
|
170
|
+
serve(rule('opt-in'))
|
|
171
|
+
expect(b.fired).toContain('hzpolicy')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('a choice made under an older policy is asked again', () => {
|
|
175
|
+
const b = browser('Europe/Berlin', { cookie: 'analytics' })
|
|
176
|
+
b.jar.set(CONSENT_VERSION_COOKIE, '1')
|
|
177
|
+
serve(rule('opt-in', 1))
|
|
178
|
+
expect(asks()).toBe(false)
|
|
179
|
+
serve(rule('opt-in', 2))
|
|
180
|
+
expect(asks()).toBe(true)
|
|
181
|
+
expect(read().analytics).toBe(false)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('a choice is stored with the version it was made under', () => {
|
|
185
|
+
const b = browser('Europe/Berlin')
|
|
186
|
+
serve(rule('opt-in', 3))
|
|
187
|
+
acceptAll()
|
|
188
|
+
expect(b.jar.get(CONSENT_VERSION_COOKIE)).toBe('3')
|
|
189
|
+
expect(asks()).toBe(false)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('ignores an answer that is not a rule', () => {
|
|
193
|
+
browser('Europe/Berlin')
|
|
194
|
+
serve({ tags: [] })
|
|
195
|
+
expect(region()).toBe('opt-in')
|
|
196
|
+
})
|
|
197
|
+
})
|
package/src/consent.ts
CHANGED
|
@@ -15,10 +15,16 @@
|
|
|
15
15
|
// ads off in every region whatever is stored: California reads GPC as the opt-out
|
|
16
16
|
// of sale and sharing.
|
|
17
17
|
//
|
|
18
|
-
//
|
|
19
|
-
// the
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// The region comes from cloud. The site's tag config (`GET /v1/project/tags`) answers
|
|
19
|
+
// with the rule that binds this visitor, resolved at the edge from where the request
|
|
20
|
+
// came from (`serve`). Until it answers, and if it never does, the zone is the browser's
|
|
21
|
+
// own time zone and a zone that is not a US zone is treated as opt-in: wrong in the
|
|
22
|
+
// safe direction, a US visitor on a foreign clock is asked and a European is never
|
|
23
|
+
// presumed to have agreed.
|
|
24
|
+
//
|
|
25
|
+
// The policy has a version. A choice is stored beside the version it was made under
|
|
26
|
+
// (`hz_consent_v`); when the policy changes, the stored choice no longer counts and the
|
|
27
|
+
// visitor is asked again.
|
|
22
28
|
//
|
|
23
29
|
// The choice is a first-party cookie, `hz_consent`, on the registrable domain
|
|
24
30
|
// for 13 months: the granted categories, comma separated. cloud reads the same
|
|
@@ -28,6 +34,12 @@ import { get, set } from './cookie'
|
|
|
28
34
|
|
|
29
35
|
export const CONSENT_COOKIE = 'hz_consent'
|
|
30
36
|
|
|
37
|
+
/** The policy version the stored choice was made under. */
|
|
38
|
+
export const CONSENT_VERSION_COOKIE = 'hz_consent_v'
|
|
39
|
+
|
|
40
|
+
/** Fired on `window` when cloud's rule for this visitor arrives. */
|
|
41
|
+
export const POLICY_EVENT = 'hzpolicy'
|
|
42
|
+
|
|
31
43
|
/** Fired on `window` whenever a choice is stored. */
|
|
32
44
|
export const CONSENT_EVENT = 'hzconsent'
|
|
33
45
|
|
|
@@ -39,6 +51,40 @@ export interface Choice {
|
|
|
39
51
|
|
|
40
52
|
export type Region = 'opt-in' | 'opt-out'
|
|
41
53
|
|
|
54
|
+
/** The rule cloud resolved for this visitor. */
|
|
55
|
+
export interface Policy {
|
|
56
|
+
region: string
|
|
57
|
+
mode: Region
|
|
58
|
+
defaults: Choice
|
|
59
|
+
gpc: boolean
|
|
60
|
+
version: number
|
|
61
|
+
notice?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let served: Policy | undefined
|
|
65
|
+
|
|
66
|
+
/** Takes cloud's rule for this visitor and tells the page, so a banner drawn early redraws. */
|
|
67
|
+
export function serve(p: unknown): void {
|
|
68
|
+
if (p == null) {
|
|
69
|
+
served = undefined
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
const d = p as Partial<Policy>
|
|
73
|
+
if ( (d.mode !== 'opt-in' && d.mode !== 'opt-out') || !d.defaults) return
|
|
74
|
+
served = {
|
|
75
|
+
region: String(d.region ?? ''),
|
|
76
|
+
mode: d.mode,
|
|
77
|
+
defaults: { analytics: !!d.defaults.analytics, marketing: !!d.defaults.marketing, ads: !!d.defaults.ads },
|
|
78
|
+
gpc: d.gpc === true,
|
|
79
|
+
version: Number(d.version) || 0,
|
|
80
|
+
notice: typeof d.notice === 'string' ? d.notice : undefined,
|
|
81
|
+
}
|
|
82
|
+
if (typeof window !== 'undefined') window.dispatchEvent(new Event(POLICY_EVENT))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The rule cloud resolved, once it has answered. */
|
|
86
|
+
export const policy = (): Policy | undefined => served
|
|
87
|
+
|
|
42
88
|
type Nav = { doNotTrack?: string; msDoNotTrack?: string; globalPrivacyControl?: boolean }
|
|
43
89
|
|
|
44
90
|
const US =
|
|
@@ -60,6 +106,7 @@ export function refused(): boolean {
|
|
|
60
106
|
|
|
61
107
|
/** Which rule applies to this visitor. */
|
|
62
108
|
export function region(): Region {
|
|
109
|
+
if (served) return served.mode
|
|
63
110
|
try {
|
|
64
111
|
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? ''
|
|
65
112
|
return US.test(zone) ? 'opt-out' : 'opt-in'
|
|
@@ -70,18 +117,23 @@ export function region(): Region {
|
|
|
70
117
|
|
|
71
118
|
/** Whether the visitor has stored a choice. */
|
|
72
119
|
export function stored(): boolean {
|
|
73
|
-
|
|
120
|
+
if (get(CONSENT_COOKIE) === undefined) return false
|
|
121
|
+
return !served || get(CONSENT_VERSION_COOKIE) === String(served.version)
|
|
74
122
|
}
|
|
75
123
|
|
|
76
124
|
/** The visitor's choice: the stored one, else the regional default, with GPC applied. */
|
|
77
125
|
export function read(): Choice {
|
|
78
|
-
const kept = get(CONSENT_COOKIE)
|
|
126
|
+
const kept = stored() ? get(CONSENT_COOKIE) : undefined
|
|
79
127
|
const on =
|
|
80
128
|
kept !== undefined
|
|
81
129
|
? kept.split(',')
|
|
82
|
-
:
|
|
83
|
-
? []
|
|
84
|
-
:
|
|
130
|
+
: served
|
|
131
|
+
? (['analytics', 'marketing', 'ads'] as const).filter((k) => served!.defaults[k])
|
|
132
|
+
: region() === 'opt-in'
|
|
133
|
+
? []
|
|
134
|
+
: gpc()
|
|
135
|
+
? ['analytics']
|
|
136
|
+
: ['analytics', 'marketing', 'ads']
|
|
85
137
|
const has = (k: string) => on.includes(k)
|
|
86
138
|
return { analytics: has('analytics'), marketing: has('marketing') && !gpc(), ads: has('ads') && !gpc() }
|
|
87
139
|
}
|
|
@@ -91,6 +143,11 @@ export function asks(): boolean {
|
|
|
91
143
|
return typeof document !== 'undefined' && region() === 'opt-in' && !stored()
|
|
92
144
|
}
|
|
93
145
|
|
|
146
|
+
/** Whether a small notice must be shown: an opt-out visitor who has not yet seen it. */
|
|
147
|
+
export function notices(): boolean {
|
|
148
|
+
return typeof document !== 'undefined' && region() === 'opt-out' && !stored()
|
|
149
|
+
}
|
|
150
|
+
|
|
94
151
|
/** The choice as the `consent` property and the cookie spell it. */
|
|
95
152
|
export function render(c: Choice): string {
|
|
96
153
|
return (['analytics', 'marketing', 'ads'] as const).filter((k) => c[k]).join(',')
|
|
@@ -106,6 +163,7 @@ export function render(c: Choice): string {
|
|
|
106
163
|
export function save(c: Choice): void {
|
|
107
164
|
const out: Choice = { analytics: c.analytics, marketing: c.marketing && !gpc(), ads: c.ads && !gpc() }
|
|
108
165
|
set(CONSENT_COOKIE, render(out), 395)
|
|
166
|
+
set(CONSENT_VERSION_COOKIE, String(served?.version ?? 0), 395)
|
|
109
167
|
if (typeof window !== 'undefined') window.dispatchEvent(new Event(CONSENT_EVENT))
|
|
110
168
|
}
|
|
111
169
|
|
package/src/index.ts
CHANGED
|
@@ -24,12 +24,12 @@ export { EVENTS, EXCEPTION, PAGEVIEW } from './events'
|
|
|
24
24
|
export { exceptionEntry, exceptionProperties, fingerprint, digest } from './exception'
|
|
25
25
|
export type { EventName } from './events'
|
|
26
26
|
export {
|
|
27
|
-
CONSENT_COOKIE, CONSENT_EVENT, acceptAll, asks, gpc, read as readConsent, refused, region,
|
|
27
|
+
CONSENT_COOKIE, CONSENT_EVENT, CONSENT_VERSION_COOKIE, POLICY_EVENT, acceptAll, asks, gpc, notices, policy as consentPolicy, serve as serveConsent, read as readConsent, refused, region,
|
|
28
28
|
rejectAll, render as renderConsent, save as saveConsent, stored as storedConsent,
|
|
29
29
|
} from './consent'
|
|
30
|
-
export type { Choice, Region } from './consent'
|
|
30
|
+
export type { Choice, Policy as ConsentRule, Region } from './consent'
|
|
31
31
|
export { CLICK_IDS, capture as captureClick, touch } from './touch'
|
|
32
|
-
export { start as startTags, ready as tagsReady, track, mirror, reach, visit
|
|
32
|
+
export { start as startTags, ready as tagsReady, track, mirror, reach, visit } 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'
|
|
@@ -64,5 +64,5 @@ export type {
|
|
|
64
64
|
* correctly while configuring nothing. `keyFor` takes an optional keyring so a
|
|
65
65
|
* runtime that receives one (hanzo.id serves every brand from one image) resolves
|
|
66
66
|
* through this same function rather than a second copy of it. */
|
|
67
|
-
export { ORG_DOMAIN, ORG_KEY, orgOf, keyFor, keyForPage } from './org'
|
|
67
|
+
export { ORG_DOMAIN, ORG_KEY, SITE_KEY, siteKey, orgOf, keyFor, keyForPage } from './org'
|
|
68
68
|
export type { Keyring } from './org'
|
package/src/link.test.ts
CHANGED
|
@@ -204,11 +204,12 @@ describe('Analytics.link / authorize / identify', () => {
|
|
|
204
204
|
})
|
|
205
205
|
})
|
|
206
206
|
|
|
207
|
-
describe('one
|
|
208
|
-
it('hanzo.ai
|
|
207
|
+
describe('one visitor across the journey, each site its own project', () => {
|
|
208
|
+
it('www.hanzo.ai is hanzo.ai; every site owns a key; a runtime keyring stays per org', () => {
|
|
209
209
|
const k = keyFor('hanzo.ai')
|
|
210
210
|
expect(k).toBeTruthy()
|
|
211
|
-
|
|
211
|
+
expect(keyFor('www.hanzo.ai')).toBe(k)
|
|
212
|
+
for (const h of ['hanzo.id', 'hanzo.app', 'hanzo.team', 'hanzo.bot']) expect(keyFor(h)).not.toBe(k)
|
|
212
213
|
const ring = { hanzo: 'pk-runtime' }
|
|
213
214
|
expect(keyFor('hanzo.id', ring)).toBe(keyFor('hanzo.ai', ring))
|
|
214
215
|
})
|
package/src/org.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import { createAnalytics } from './core'
|
|
4
|
-
import { ORG_KEY, keyFor, keyForPage, orgOf } from './org'
|
|
4
|
+
import { ORG_KEY, SITE_KEY, keyFor, keyForPage, orgOf, siteKey } from './org'
|
|
5
5
|
|
|
6
6
|
const ENV = 'NEXT_PUBLIC_PUBLISHABLE_KEY'
|
|
7
7
|
|
|
@@ -45,7 +45,6 @@ describe('the org that owns a host', () => {
|
|
|
45
45
|
'hanzo.codes',
|
|
46
46
|
'hanzoskills.com',
|
|
47
47
|
'hanzo.ventures',
|
|
48
|
-
'hanzo.team',
|
|
49
48
|
]) {
|
|
50
49
|
expect(orgOf(host)).toBe('hanzo')
|
|
51
50
|
expect(keyFor(host)).toBe(ORG_KEY.hanzo)
|
|
@@ -128,11 +127,32 @@ describe('a surface that configures nothing', () => {
|
|
|
128
127
|
})
|
|
129
128
|
})
|
|
130
129
|
|
|
131
|
-
describe('the hanzo org
|
|
132
|
-
it('names hanzo.network, hanzo.market, hanzo.industries and hanzo.agency', () => {
|
|
130
|
+
describe('the hanzo org names its sites', () => {
|
|
131
|
+
it('names hanzo.network, hanzo.market, hanzo.industries and hanzo.agency, each with its own project key', () => {
|
|
133
132
|
for (const host of ['hanzo.network', 'hanzo.market', 'hanzo.industries', 'hanzo.agency']) {
|
|
134
133
|
expect(orgOf(host)).toBe('hanzo')
|
|
135
|
-
expect(keyFor(host)).toBe(
|
|
134
|
+
expect(keyFor(host)).toBe(SITE_KEY[host])
|
|
136
135
|
}
|
|
137
136
|
})
|
|
138
137
|
})
|
|
138
|
+
|
|
139
|
+
describe('a site files its events under its own project', () => {
|
|
140
|
+
it('each first-party site resolves to its own key, and its tag set follows', () => {
|
|
141
|
+
const keys = Object.values(SITE_KEY)
|
|
142
|
+
expect(keyFor('docs.hanzo.ai')).toBe(SITE_KEY['docs.hanzo.ai'])
|
|
143
|
+
expect(keyFor('hanzo.chat')).toBe(SITE_KEY['hanzo.chat'])
|
|
144
|
+
expect(keyFor('hanzo.team')).not.toBe(keyFor('hanzo.ai'))
|
|
145
|
+
expect(keyFor('hanzo.ai')).toBe(ORG_KEY.hanzo)
|
|
146
|
+
expect(keys.every((k) => k.startsWith('pk-'))).toBe(true)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('www. and a hanzo.app subdomain reach their site', () => {
|
|
150
|
+
expect(siteKey('www.hanzo.ai')).toBe(SITE_KEY['hanzo.ai'])
|
|
151
|
+
expect(siteKey('demo.hanzo.app')).toBe(SITE_KEY['hanzo.app'])
|
|
152
|
+
expect(siteKey('evil-hanzo.ai')).toBeUndefined()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('a host with no site of its own falls back to its org', () => {
|
|
156
|
+
expect(keyFor('hanzo.works')).toBe(ORG_KEY.hanzo)
|
|
157
|
+
})
|
|
158
|
+
})
|
package/src/org.ts
CHANGED
|
@@ -98,6 +98,40 @@ export const ORG_KEY: Keyring = Object.freeze({
|
|
|
98
98
|
zoo: 'pk-3TKpKnERV9AQSsBUERWkZejC1O1mUxc1jRzsP3MPbs4',
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The publishable key of each site's project, which is how cloud finds the site's
|
|
103
|
+
* tag set and the project its events are filed under. Each site is ONE project with its
|
|
104
|
+
* own key. A `pk-` is public by design (it ships in every page); it names a site,
|
|
105
|
+
* never a platform id. A host absent here falls back to its org's key for events and
|
|
106
|
+
* has no tag config unless `start` is given a key.
|
|
107
|
+
*/
|
|
108
|
+
export const SITE_KEY: Readonly<Record<string, string>> = Object.freeze({
|
|
109
|
+
'hanzo.ai': 'pk-CmfLA2K6kvsPflrS9DSkt06H_kSoQB_21sjedt6VJdc',
|
|
110
|
+
'www.hanzo.ai': 'pk-CmfLA2K6kvsPflrS9DSkt06H_kSoQB_21sjedt6VJdc',
|
|
111
|
+
'hanzo.app': 'pk-wlnXN2a9_vmCm60yTFtQ629Q8TyuaxBNZbY1RWT72gQ',
|
|
112
|
+
'hanzo.team': 'pk-NCzD2FiHpZv8KUpkCX4olT1LJOJsMxBC_Z8NkiQsOFQ',
|
|
113
|
+
'pay.hanzo.ai': 'pk-eX6kv7JZNoiYn1WkeJH3tT_8OvkVYScmCXnLMwxTKf8',
|
|
114
|
+
'platform.hanzo.ai': 'pk-My1RpZLEUnTj8vAdPbWKYdDUuhxZJ4dVjHcwjZN4rZ8',
|
|
115
|
+
'docs.hanzo.ai': 'pk-jukhtjMT2ymoeBDAeFjINQWlBv-v9sNn1TPztiCrrwk',
|
|
116
|
+
'hanzo.bot': 'pk-W5d7Mn7ZukT7igyscIy6Pqe8JpA0Ge604Yn4xNR4JCU',
|
|
117
|
+
'hanzo.chat': 'pk-ATxWeB2jNyRSxDgqnkGoDj3CCbzrmTxSktUh9zlrO-E',
|
|
118
|
+
'hanzo.id': 'pk-_GVF3KlaqprlPHDumBMRFeFU1lIGOaarQfgimNVDIo8',
|
|
119
|
+
'hanzo.industries': 'pk-Vh1wvL2HqIAv9qpVvmrznTI9uMHrTD-jyIC-vnlBsgY',
|
|
120
|
+
'hanzo.market': 'pk-DN3xQFa0H61epUVg9OkiOYLzCdteNt9hE6f-S-ednjQ',
|
|
121
|
+
'hanzo.network': 'pk-YJ2eiA_7ckLStMPte8Ar8FsmRp_ycmeIeRbpGprZYmM',
|
|
122
|
+
'hanzo.agency': 'pk-26a2clOaJFe24aKJZ0PkI-GWR9F4EEa8rthHzgy_Efw',
|
|
123
|
+
'console.hanzo.ai': 'pk-oFNXoiO1wOD8PqGdHYyiSq6jaNCdrd7TpztGpwM2ZiQ',
|
|
124
|
+
'admin.hanzo.ai': 'pk-RYZwxkomW-tlAnEu2DKo_kAzcxFbSF2YiYcsUY48QL0',
|
|
125
|
+
'bot.hanzo.ai': 'pk-W5d7Mn7ZukT7igyscIy6Pqe8JpA0Ge604Yn4xNR4JCU',
|
|
126
|
+
'cloud.hanzo.ai': 'pk-RAfEGHPoNdCEU9fnA_cPd_Xo9Tci44rlYQV9xuJ1Ob0',
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
/** The project key of the site a host is: its own entry, `www.` stripped, a `*.hanzo.app` site under hanzo.app. */
|
|
130
|
+
export function siteKey(host: string): string | undefined {
|
|
131
|
+
const h = normalize(host).replace(/^www\./, '')
|
|
132
|
+
return SITE_KEY[h] ?? (h.endsWith('.hanzo.app') ? SITE_KEY['hanzo.app'] : undefined)
|
|
133
|
+
}
|
|
134
|
+
|
|
101
135
|
/** `pk-` is publishable; `sk-` is not, and there is no third thing. Checked at the
|
|
102
136
|
* one point a key becomes something a page will send, so a mistyped entry fails
|
|
103
137
|
* closed rather than putting a secret in every visitor's tab. */
|
|
@@ -142,6 +176,10 @@ export function orgOf(host: string): string | undefined {
|
|
|
142
176
|
* this SAME function rather than a second copy of it.
|
|
143
177
|
*/
|
|
144
178
|
export function keyFor(host: string, keyring: Keyring = ORG_KEY): string | undefined {
|
|
179
|
+
if (keyring === ORG_KEY) {
|
|
180
|
+
const site = siteKey(host)
|
|
181
|
+
if (site) return site
|
|
182
|
+
}
|
|
145
183
|
const org = orgOf(host)
|
|
146
184
|
if (!org) return undefined
|
|
147
185
|
const key = keyring[org]
|
package/src/react.tsx
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from 'react'
|
|
23
23
|
import { Analytics, createAnalytics } from './core'
|
|
24
24
|
import type { AnalyticsConfig } from './types'
|
|
25
|
-
import { CONSENT_EVENT, read, render, type Choice } from './consent'
|
|
25
|
+
import { CONSENT_EVENT, POLICY_EVENT, read, render, type Choice } from './consent'
|
|
26
26
|
|
|
27
27
|
const Ctx = createContext<Analytics | null>(null)
|
|
28
28
|
|
|
@@ -125,7 +125,11 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
125
125
|
|
|
126
126
|
const listen = (on: () => void) => {
|
|
127
127
|
window.addEventListener(CONSENT_EVENT, on)
|
|
128
|
-
|
|
128
|
+
window.addEventListener(POLICY_EVENT, on)
|
|
129
|
+
return () => {
|
|
130
|
+
window.removeEventListener(CONSENT_EVENT, on)
|
|
131
|
+
window.removeEventListener(POLICY_EVENT, on)
|
|
132
|
+
}
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
/**
|
package/src/tags.ts
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
// reads the same table.
|
|
33
33
|
|
|
34
34
|
import { namesOn } from '@hanzo/events'
|
|
35
|
-
import { CONSENT_EVENT, read, render, type Choice } from './consent'
|
|
35
|
+
import { CONSENT_EVENT, read, render, serve, type Choice } from './consent'
|
|
36
|
+
import { siteKey } from './org'
|
|
36
37
|
import { capture, touch } from './touch'
|
|
37
38
|
import type { Analytics } from './core'
|
|
38
39
|
|
|
@@ -54,29 +55,6 @@ export interface TagOptions {
|
|
|
54
55
|
domains?: string[]
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
/**
|
|
58
|
-
* The publishable key of each site's project, which is how cloud finds the site's
|
|
59
|
-
* tag set. A `pk-` is public by design (it ships in every page); it names a site,
|
|
60
|
-
* never a platform id. A host absent here has no tag config unless `start` is
|
|
61
|
-
* given a key.
|
|
62
|
-
*/
|
|
63
|
-
export const SITE_KEY: Readonly<Record<string, string>> = Object.freeze({
|
|
64
|
-
'hanzo.ai': 'pk-CmfLA2K6kvsPflrS9DSkt06H_kSoQB_21sjedt6VJdc',
|
|
65
|
-
'www.hanzo.ai': 'pk-CmfLA2K6kvsPflrS9DSkt06H_kSoQB_21sjedt6VJdc',
|
|
66
|
-
'hanzo.app': 'pk-wlnXN2a9_vmCm60yTFtQ629Q8TyuaxBNZbY1RWT72gQ',
|
|
67
|
-
'hanzo.team': 'pk-NCzD2FiHpZv8KUpkCX4olT1LJOJsMxBC_Z8NkiQsOFQ',
|
|
68
|
-
'pay.hanzo.ai': 'pk-eX6kv7JZNoiYn1WkeJH3tT_8OvkVYScmCXnLMwxTKf8',
|
|
69
|
-
'platform.hanzo.ai': 'pk-My1RpZLEUnTj8vAdPbWKYdDUuhxZJ4dVjHcwjZN4rZ8',
|
|
70
|
-
'docs.hanzo.ai': 'pk-jukhtjMT2ymoeBDAeFjINQWlBv-v9sNn1TPztiCrrwk',
|
|
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',
|
|
77
|
-
'cloud.hanzo.ai': 'pk-RAfEGHPoNdCEU9fnA_cPd_Xo9Tci44rlYQV9xuJ1Ob0',
|
|
78
|
-
})
|
|
79
|
-
|
|
80
58
|
type Call = (...args: unknown[]) => void
|
|
81
59
|
type Page = {
|
|
82
60
|
dataLayer?: unknown[]
|
|
@@ -328,7 +306,7 @@ export function start(o: TagOptions = {}): () => void {
|
|
|
328
306
|
const base = (o.base ?? 'https://api.hanzo.ai').replace(/\/$/, '')
|
|
329
307
|
const q = new URLSearchParams()
|
|
330
308
|
const host = o.host ?? window.location.hostname
|
|
331
|
-
const key = o.key ??
|
|
309
|
+
const key = o.key ?? siteKey(host)
|
|
332
310
|
if (key) q.set('key', key)
|
|
333
311
|
q.set('host', host)
|
|
334
312
|
const answer = (list: BrowserTag[]) => {
|
|
@@ -340,8 +318,9 @@ export function start(o: TagOptions = {}): () => void {
|
|
|
340
318
|
const cap = setTimeout(() => answer([]), 3000)
|
|
341
319
|
fetch(`${base}/v1/project/tags?${q}`)
|
|
342
320
|
.then((r) => (r.ok ? r.json() : { tags: [] }))
|
|
343
|
-
.then((j: { tags?: BrowserTag[] }) => {
|
|
321
|
+
.then((j: { tags?: BrowserTag[]; consent?: unknown }) => {
|
|
344
322
|
clearTimeout(cap)
|
|
323
|
+
serve(j.consent)
|
|
345
324
|
answer(Array.isArray(j.tags) ? j.tags : [])
|
|
346
325
|
})
|
|
347
326
|
.catch(() => {
|