@hanzo/event 0.3.32 → 0.3.34

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/README.md CHANGED
@@ -27,29 +27,27 @@ starve the other:
27
27
  > plane is **inert**: nothing is sent, nothing throws, analytics is unaffected.
28
28
  > Assert `client.errorPlaneEnabled` if you want to know which you have.
29
29
 
30
- ### No bundler? Use the `hz.js` tag — same client, same wire
30
+ ### No bundler? Use the hosted tag
31
31
 
32
32
  ```html
33
- <script async src="https://unpkg.com/@hanzo/event/hz.js"
34
- data-product="hanzo.ai"
35
- data-ingest-key="pk-…"
36
- data-capture="1"></script>
33
+ <script defer src="https://api.hanzo.ai/v1/event.js" data-key="pk-…"></script>
37
34
  ```
38
35
 
39
- > **`data-ingest-key` is required off the door's own origin**, and through
40
- > 0.3.11 this file could not present one at all no header, no query. A keyed
41
- > static surface therefore sent UNATTRIBUTED writes, which the door refuses
42
- > (`401 ingest_key_required`) silently, because nothing here reads the response.
43
- > The tag measured fine in the browser and filed nothing. It now rides
44
- > `Authorization: Bearer pk-…` on fetch and `?ingest_key=pk-…` on a headerless
45
- > unload beacon the same pair the npm client uses.
46
-
47
- `hz.js` (in this package) is the **script-tag distribution** of this client, for
48
- surfaces with no build step. It posts the SAME `{ batch: [WireEvent, …] }` to the
49
- SAME `POST {host}/v1/event`, and adds DOM **autocapture** `$click` (with an
50
- element locator), `$outbound`, `$scroll`, `$form`, `$vitals` which a bundled app
51
- does not need and a plain page cannot get. Manual API: `window.hanzo.track()` /
52
- `identify()` / `page()`.
36
+ That one line is the whole install, and it is the same line for a published site,
37
+ hanzo.team and a customer's own page. The tag is served by the door that eats the
38
+ events, so a caller allowlists ONE host and the tag can never drift from the wire
39
+ it posts to. It adds DOM **autocapture** `$click` (with an element locator),
40
+ `$outbound`, `$scroll`, `$form`, `$vitals` which a bundled app does not need and
41
+ a plain page cannot get, and it carries the site's tag config from `/v1/tags`, so a
42
+ pixel is configured on the project rather than pasted into the page.
43
+
44
+ > **`data-key` is a publishable `pk-`, and without one the tag is INERT.** A write
45
+ > the door cannot attribute is refused (`401 ingest_key_required`) silently, so a
46
+ > keyless tag measures fine in the browser and files nothing. Mint one with
47
+ > `POST /v1/keys {"type":"publishable"}`.
48
+
49
+ This package is the client for a surface that **builds**. There is no second
50
+ script-tag distribution here: one wire, one door, one tag.
53
51
 
54
52
  It honours the same consent sources the bundled stack does: an explicit stored
55
53
  choice (`hz_consent`, the key a Hanzo consent banner writes) outranks the browser
package/dist/index.cjs CHANGED
@@ -91,6 +91,56 @@ function dsnForProduct(product, key) {
91
91
  return `https://${key}@api.hanzo.ai/v1/event/${projectId}`;
92
92
  }
93
93
 
94
+ // src/org.ts
95
+ var ORG_DOMAIN = Object.freeze({
96
+ hanzo: Object.freeze(["hanzo.ai", "hanzo.app", "hanzo.chat", "hanzo.id", "hanzo.bot", "hanzo.sh"]),
97
+ lux: Object.freeze([
98
+ "lux.network",
99
+ "lux.exchange",
100
+ "lux.market",
101
+ "lux.finance",
102
+ "lux.financial",
103
+ "lux.credit",
104
+ "lux.fund",
105
+ "lux.id"
106
+ ]),
107
+ zoo: Object.freeze(["zoo.ngo", "zoo.network", "zoolabs.id"])
108
+ });
109
+ var ORG_KEY = Object.freeze({
110
+ hanzo: "pk-rM_CdaF2MQckGCrla113SrR1oH4zvqN8xh2I95Z9tY8",
111
+ lux: "pk-gUZp6ZVfhJzSwK-rb4oLbVkpCnMBx5uSCpxf_5yEhQk",
112
+ zoo: "pk-3TKpKnERV9AQSsBUERWkZejC1O1mUxc1jRzsP3MPbs4"
113
+ });
114
+ var PUBLISHABLE = "pk-";
115
+ var normalize = (host) => host.trim().toLowerCase().replace(/\.$/, "").split(":")[0] ?? "";
116
+ function orgOf(host) {
117
+ const h = normalize(host);
118
+ if (!h) return void 0;
119
+ let best;
120
+ let bestLen = 0;
121
+ for (const [org, domains] of Object.entries(ORG_DOMAIN)) {
122
+ for (const d of domains) {
123
+ if ((h === d || h.endsWith(`.${d}`)) && d.length > bestLen) {
124
+ best = org;
125
+ bestLen = d.length;
126
+ }
127
+ }
128
+ }
129
+ return best;
130
+ }
131
+ function keyFor(host, keyring = ORG_KEY) {
132
+ const org = orgOf(host);
133
+ if (!org) return void 0;
134
+ const key = keyring[org];
135
+ if (typeof key !== "string") return void 0;
136
+ const trimmed = key.trim();
137
+ return trimmed.startsWith(PUBLISHABLE) ? trimmed : void 0;
138
+ }
139
+ function keyForPage(keyring = ORG_KEY) {
140
+ if (typeof location === "undefined") return void 0;
141
+ return keyFor(location.hostname, keyring);
142
+ }
143
+
94
144
  // src/scrub.ts
95
145
  var REDACTED = "[redacted]";
96
146
  var EMAIL_MARK = "[email]";
@@ -212,8 +262,8 @@ function scrubText(s, capturePII = false) {
212
262
  }
213
263
 
214
264
  // src/anon.js
215
- var HZ_ANON_KEY = "hz_anon_id";
216
- var HZ_ANON_LEGACY_KEY = "hz_id";
265
+ var HZ_ANON_KEY = "iam-anon-id";
266
+ var HZ_ANON_ADOPT = ["hz_anon_id", "hz_id"];
217
267
  var HZ_ANON_DOMAIN = "hanzo.ai";
218
268
  var HZ_ANON_MAX_AGE = 2 * 365 * 24 * 60 * 60;
219
269
  var hzAnonMemo;
@@ -300,10 +350,17 @@ function hzAnonWrite(name, value) {
300
350
  } catch (e) {
301
351
  }
302
352
  }
353
+ function hzAnonAdopted(s) {
354
+ for (var i = 0; i < HZ_ANON_ADOPT.length; i++) {
355
+ var id = hzAnonCookie(HZ_ANON_ADOPT[i]) || hzAnonItem(s, HZ_ANON_ADOPT[i]);
356
+ if (id) return id;
357
+ }
358
+ return "";
359
+ }
303
360
  function hzAnonId() {
304
361
  if (typeof window === "undefined") return "";
305
362
  var s = hzAnonStore();
306
- var id = hzAnonCookie(HZ_ANON_KEY) || hzAnonItem(s, HZ_ANON_KEY) || hzAnonItem(s, HZ_ANON_LEGACY_KEY) || hzAnonMemo || hzUuidv7();
363
+ var id = hzAnonCookie(HZ_ANON_KEY) || hzAnonItem(s, HZ_ANON_KEY) || hzAnonAdopted(s) || hzAnonMemo || hzUuidv7();
307
364
  hzAnonMemo = id;
308
365
  hzAnonWrite(HZ_ANON_KEY, id);
309
366
  try {
@@ -319,7 +376,7 @@ function uuidv7Time(id) {
319
376
  }
320
377
 
321
378
  // src/version.ts
322
- var VERSION = "0.3.32";
379
+ var VERSION = "0.3.34";
323
380
 
324
381
  // src/sentry.ts
325
382
  var MAX_FRAMES = 50;
@@ -781,12 +838,20 @@ var Analytics = class {
781
838
  // carries end to end — KMS `deploy/PUBLISHABLE_KEY` -> the PUBLISHABLE_KEY
782
839
  // build-arg -> the NEXT_PUBLIC_ prefix Next inlines.
783
840
  //
784
- // A surface that provides neither has no key, and its anonymous traffic
785
- // files under `$public` (which drops track/identify/group and answers 200);
786
- // the fix is to give it the KMS-sourced env, not to hardcode the org key
787
- // here. The key is a credential-class value: its home is KMS, and the ONE
788
- // build reads it from there.
789
- ingestKey: config.ingestKey ?? readEnv("NEXT_PUBLIC_PUBLISHABLE_KEY")
841
+ // 3. the org that owns the HOST this page is served from (`org.ts`).
842
+ //
843
+ // Step 3 fires only when a surface configured NOTHING, so it never overrides
844
+ // the KMS chain above it replaces going dark with reporting to the right
845
+ // brand. That is what a static export needs: `output: export` inlines no env,
846
+ // so before this the only way to report was to commit a key literal, and the
847
+ // fleet grew one copy per site of a value with a single source.
848
+ //
849
+ // A surface no brand claims still has no key, and the door REFUSES an
850
+ // unattributed event (401 `ingest_key_required`) — the reserved `$public`
851
+ // tenant that once caught keyless beacons is retired, and anonymous ingest is
852
+ // refused at every door on every brand host. So there is no quiet fallback to
853
+ // rely on: an event lands in the org a credential names, or it does not land.
854
+ ingestKey: config.ingestKey ?? readEnv("NEXT_PUBLIC_PUBLISHABLE_KEY") ?? keyForPage()
790
855
  };
791
856
  this.transport = config.transport ?? new DefaultTransport();
792
857
  this.dsn = parseDsn(
@@ -1110,26 +1175,26 @@ var COHORTS = {
1110
1175
  channel: { field: "channel", label: "Acquisition channel" },
1111
1176
  refCode: { field: "ref_code", label: "Referral code" }
1112
1177
  };
1113
- /*! anon.js — THE anonymous-identity chain. ONE implementation, three distributions.
1178
+ /*! anon.js — THE anonymous-identity chain. ONE implementation, two distributions.
1114
1179
  *
1115
1180
  * One browser is ONE person on every Hanzo surface, whichever client a page
1116
- * happens to have loaded. There were three implementations writing TWO keys —
1117
- * `hz_anon_id` (the npm client, the hosted tag) and `hz_id` (hz.js) — so the same
1118
- * visitor was several people depending on which snippet the surface shipped.
1181
+ * happens to have loaded. There were once three implementations writing TWO keys —
1182
+ * `hz_anon_id` and `hz_id` — so the same visitor was several people depending on
1183
+ * which snippet the surface shipped.
1119
1184
  *
1120
- * The three call sites:
1185
+ * The two call sites:
1121
1186
  * 1. src/storage.ts — the bundled npm client; IMPORTS this file.
1122
- * 2. hz.js — the no-build script tag; INLINES the marked region.
1123
- * 3. hanzoai/cloud apps/analytics/tag.js — the tag the door hosts at
1187
+ * 2. hanzoai/cloud apps/analytics/tag.js — the tag the door hosts at
1124
1188
  * /v1/event.js; vendors this file and its tag.go serves the marked region
1125
1189
  * with the tag as one asset, so the door holds no second copy either.
1126
1190
  *
1127
- * (2) and (3) have no bundler and cannot import anything, which is why the chain
1128
- * lives in a file that is plain ES5 rather than in a .ts: the region between the
1129
- * BEGIN and END markers is COPIED VERBATIM, and src/anon.test.ts fails if hz.js's
1130
- * copy is so much as a byte different. Keep the region ES5, dependency-free,
1131
- * `hz`-prefixed (it is spliced into other people's scopes) and unformatted — a
1132
- * reformat of one copy is a diff against the other.
1191
+ * (2) has no bundler and cannot import anything, which is why the chain lives in a
1192
+ * file that is plain ES5 rather than in a .ts: the region between the BEGIN and END
1193
+ * markers is COPIED VERBATIM. Keep it ES5, dependency-free, `hz`-prefixed (it is
1194
+ * spliced into other people's scopes) and unformatted a reformat here is a diff
1195
+ * against the vendored copy. After editing, resync the door:
1196
+ *
1197
+ * curl -fsSL https://unpkg.com/@hanzo/event/src/anon.js -o apps/analytics/anon.js
1133
1198
  */
1134
1199
 
1135
1200
  Object.defineProperty(exports, "EVENTS", {
@@ -1159,6 +1224,8 @@ Object.defineProperty(exports, "eventsOf", {
1159
1224
  exports.Analytics = Analytics;
1160
1225
  exports.COHORTS = COHORTS;
1161
1226
  exports.GOALS = GOALS;
1227
+ exports.ORG_DOMAIN = ORG_DOMAIN;
1228
+ exports.ORG_KEY = ORG_KEY;
1162
1229
  exports.PRODUCT_PROJECT = PRODUCT_PROJECT;
1163
1230
  exports.VERSION = VERSION;
1164
1231
  exports.buildEnvelope = buildEnvelope;
@@ -1176,6 +1243,9 @@ exports.getFirstTouch = getFirstTouch;
1176
1243
  exports.hasAttribution = hasAttribution;
1177
1244
  exports.hostOf = hostOf;
1178
1245
  exports.isoWeek = isoWeek;
1246
+ exports.keyFor = keyFor;
1247
+ exports.keyForPage = keyForPage;
1248
+ exports.orgOf = orgOf;
1179
1249
  exports.parseAttribution = parseAttribution;
1180
1250
  exports.parseDsn = parseDsn;
1181
1251
  exports.redactSecrets = redactSecrets;