@hanzo/event 0.3.2 → 0.3.4

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.mjs CHANGED
@@ -46,16 +46,16 @@ function deriveChannel(a) {
46
46
  }
47
47
  function hostOf(raw) {
48
48
  if (!raw) return "";
49
- let s = raw.trim();
50
- const scheme = s.indexOf("://");
51
- if (scheme >= 0) s = s.slice(scheme + 3);
52
- const cut = s.search(/[/?#]/);
53
- if (cut >= 0) s = s.slice(0, cut);
54
- const at = s.indexOf("@");
55
- if (at >= 0) s = s.slice(at + 1);
56
- const colon = s.indexOf(":");
57
- if (colon >= 0) s = s.slice(0, colon);
58
- return s.toLowerCase().trim();
49
+ let s2 = raw.trim();
50
+ const scheme = s2.indexOf("://");
51
+ if (scheme >= 0) s2 = s2.slice(scheme + 3);
52
+ const cut = s2.search(/[/?#]/);
53
+ if (cut >= 0) s2 = s2.slice(0, cut);
54
+ const at = s2.indexOf("@");
55
+ if (at >= 0) s2 = s2.slice(at + 1);
56
+ const colon = s2.indexOf(":");
57
+ if (colon >= 0) s2 = s2.slice(0, colon);
58
+ return s2.toLowerCase().trim();
59
59
  }
60
60
  function hasAttribution(a) {
61
61
  return Boolean(
@@ -71,6 +71,20 @@ function isoWeek(d) {
71
71
  return `${date.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
72
72
  }
73
73
 
74
+ // src/dsn.ts
75
+ var PRODUCT_DSN = Object.freeze({
76
+ // hanzo-console — console.hanzo.ai (also served embedded by the cloud binary)
77
+ console: "https://1:0c8054dbde157f4f420c56b58660052b2ad782293c4de1d606ef8fbc46a0bf34@api.hanzo.ai/v1/sentry/019fa40b-94ae-7f1d-8f7b-e92f123fad42",
78
+ // hanzo-app — hanzo.app
79
+ app: "https://1:b3e1173125568c80f91ef4b1fabbbd2d7e22341de02b33ce7e22ef4fc16a196e@api.hanzo.ai/v1/sentry/019f9b1e-57eb-7171-9d92-72c0b85e4b4b",
80
+ // hanzo-ai — hanzo.ai (the marketing site; `site` is the product name it declares)
81
+ site: "https://1:d9cbfb844958bd7ef2a455600f00fbf237fbd71b75c1504f137773096d6aa53f@api.hanzo.ai/v1/sentry/019f9b1e-5785-7359-ad0b-f75db8e58c99"
82
+ });
83
+ function dsnForProduct(product) {
84
+ if (!product) return void 0;
85
+ return PRODUCT_DSN[product];
86
+ }
87
+
74
88
  // src/events.ts
75
89
  var EVENTS = {
76
90
  // Signup funnel: view -> submit -> verify -> completed -> first action.
@@ -78,6 +92,12 @@ var EVENTS = {
78
92
  SIGNUP_SUBMITTED: "signup_submitted",
79
93
  SIGNUP_VERIFIED: "signup_verified",
80
94
  SIGNUP_COMPLETED: "signup_completed",
95
+ /** A RETURNING user authenticated — the non-signup half of the IAM callback.
96
+ * Keeping it distinct is what stops returning logins from inflating signups. */
97
+ LOGIN_COMPLETED: "login_completed",
98
+ /** Activation: the first moment of real value. ONE event for every product —
99
+ * the product-specific moment is the `action` property (api_call, app_live,
100
+ * chat_reply), never a new event name. */
81
101
  FIRST_ACTION: "first_action",
82
102
  // Waitlist + referral.
83
103
  WAITLIST_JOINED: "waitlist_joined",
@@ -93,13 +113,27 @@ var EVENTS = {
93
113
  FEATURE_USED: "feature_used",
94
114
  API_KEY_CREATED: "api_key_created",
95
115
  APP_CREATED: "app_created",
96
- DEPLOY_STARTED: "deploy_started",
97
116
  PROJECT_CREATED: "project_created",
98
117
  AGENT_CREATED: "agent_created",
99
118
  CHAT_STARTED: "chat_started",
100
119
  CHAT_MESSAGE_SENT: "chat_message_sent",
120
+ /** The user switched model/endpoint — the single strongest quality signal a
121
+ * chat surface emits (a switch usually follows a bad answer). */
122
+ MODEL_SWITCHED: "model_switched",
101
123
  TASK_STARTED: "task_started",
102
- TASK_COMPLETED: "task_completed"
124
+ TASK_COMPLETED: "task_completed",
125
+ // Build → ship. `build_*` is a MODEL producing an artifact; `deploy_*` is that
126
+ // artifact going live. Intent (build_started) is never the same event as the
127
+ // artifact existing (app_created) — conflating them makes the funnel lie.
128
+ BUILD_STARTED: "build_started",
129
+ /** A model finished producing an artifact (an app build, a chat reply, an agent
130
+ * run). Carries `durationMs` — the outcome event owns its own duration, so no
131
+ * paired start event is needed. */
132
+ GENERATION_COMPLETED: "generation_completed",
133
+ GENERATION_FAILED: "generation_failed",
134
+ DEPLOY_STARTED: "deploy_started",
135
+ DEPLOY_SUCCEEDED: "deploy_succeeded",
136
+ DEPLOY_FAILED: "deploy_failed"
103
137
  };
104
138
  var PAGEVIEW = "$pageview";
105
139
 
@@ -151,40 +185,40 @@ function luhn(digits) {
151
185
  }
152
186
  return sum % 10 === 0;
153
187
  }
154
- function redactPAN(s) {
155
- return s.replace(RE_PAN, (m) => {
188
+ function redactPAN(s2) {
189
+ return s2.replace(RE_PAN, (m) => {
156
190
  const digits = m.replace(/[ -]/g, "");
157
191
  if (digits.length < 13 || digits.length > 19) return m;
158
192
  return luhn(digits) ? REDACTED : m;
159
193
  });
160
194
  }
161
- var RE_EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
195
+ var RE_EMAIL = /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g;
162
196
  var RE_IPV4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
163
197
  var RE_IPV6 = /\b(?:[0-9A-Fa-f]{1,4}:){2,7}[0-9A-Fa-f]{1,4}\b/g;
164
- function redactSecrets(s) {
165
- for (const re of SECRET_PATTERNS) s = s.replace(re, REDACTED);
166
- return redactPAN(s);
198
+ function redactSecrets(s2) {
199
+ for (const re of SECRET_PATTERNS) s2 = s2.replace(re, REDACTED);
200
+ return redactPAN(s2);
167
201
  }
168
- function scrubPII(s) {
169
- s = s.replace(RE_EMAIL, EMAIL_MARK);
170
- s = s.replace(RE_IPV6, IP_MARK);
171
- s = s.replace(RE_IPV4, IP_MARK);
172
- return s;
202
+ function scrubPII(s2) {
203
+ s2 = s2.replace(RE_EMAIL, EMAIL_MARK);
204
+ s2 = s2.replace(RE_IPV6, IP_MARK);
205
+ s2 = s2.replace(RE_IPV4, IP_MARK);
206
+ return s2;
173
207
  }
174
208
  var MAX_SCRUB_LEN = 8192;
175
- function truncate(s, max = MAX_SCRUB_LEN) {
176
- return s.length > max ? s.slice(0, max) + "\u2026 [truncated]" : s;
209
+ function truncate(s2, max = MAX_SCRUB_LEN) {
210
+ return s2.length > max ? s2.slice(0, max) + "\u2026 [truncated]" : s2;
177
211
  }
178
- function scrubText(s, capturePII = false) {
179
- if (!s) return s ?? "";
180
- s = truncate(s);
181
- s = redactSecrets(s);
182
- if (!capturePII) s = scrubPII(s);
183
- return s;
212
+ function scrubText(s2, capturePII = false) {
213
+ if (!s2) return s2 ?? "";
214
+ s2 = truncate(s2);
215
+ s2 = redactSecrets(s2);
216
+ if (!capturePII) s2 = scrubPII(s2);
217
+ return s2;
184
218
  }
185
219
 
186
220
  // src/version.ts
187
- var VERSION = "0.3.2";
221
+ var VERSION = "0.3.3";
188
222
 
189
223
  // src/sentry.ts
190
224
  var MAX_FRAMES = 50;
@@ -195,14 +229,14 @@ var MAX_TAGS = 50;
195
229
  function eventId() {
196
230
  const c = typeof crypto !== "undefined" ? crypto : void 0;
197
231
  if (c && "randomUUID" in c) return c.randomUUID().replace(/-/g, "");
198
- let s = "";
199
- for (let i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16);
200
- return s;
232
+ let s2 = "";
233
+ for (let i = 0; i < 32; i++) s2 += Math.floor(Math.random() * 16).toString(16);
234
+ return s2;
201
235
  }
202
- function byteLen(s) {
203
- if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s).length;
204
- if (typeof Buffer !== "undefined") return Buffer.byteLength(s, "utf8");
205
- return s.length;
236
+ function byteLen(s2) {
237
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s2).length;
238
+ if (typeof Buffer !== "undefined") return Buffer.byteLength(s2, "utf8");
239
+ return s2.length;
206
240
  }
207
241
  function parseDsn(dsn) {
208
242
  if (!dsn) return null;
@@ -272,17 +306,38 @@ function framesFromStack(stack) {
272
306
  }
273
307
  function normalizeError(err) {
274
308
  if (err instanceof Error) {
275
- return { name: err.name || "Error", message: err.message || String(err), stack: err.stack };
309
+ const name = read(err, "name");
310
+ const message = read(err, "message");
311
+ const stack = read(err, "stack");
312
+ return {
313
+ name: typeof name === "string" && name ? name : "Error",
314
+ message: typeof message === "string" && message ? message : str(err),
315
+ stack: typeof stack === "string" ? stack : void 0
316
+ };
276
317
  }
277
318
  if (typeof err === "string") return { name: "Error", message: err };
278
319
  try {
279
- return { name: "Error", message: JSON.stringify(err) };
320
+ return { name: "Error", message: JSON.stringify(err) ?? str(err) };
321
+ } catch {
322
+ return { name: "Error", message: str(err) };
323
+ }
324
+ }
325
+ function read(o, k) {
326
+ try {
327
+ return o[k];
280
328
  } catch {
281
- return { name: "Error", message: String(err) };
329
+ return void 0;
330
+ }
331
+ }
332
+ function str(v) {
333
+ try {
334
+ return String(v);
335
+ } catch {
336
+ return "[unstringifiable]";
282
337
  }
283
338
  }
284
339
  function coerceTag(v) {
285
- const s = typeof v === "string" ? v : (() => {
340
+ const s2 = typeof v === "string" ? v : (() => {
286
341
  try {
287
342
  return JSON.stringify(v) ?? String(v);
288
343
  } catch {
@@ -293,7 +348,7 @@ function coerceTag(v) {
293
348
  }
294
349
  }
295
350
  })();
296
- return s.length > MAX_TAG_LEN ? s.slice(0, MAX_TAG_LEN) : s;
351
+ return s2.length > MAX_TAG_LEN ? s2.slice(0, MAX_TAG_LEN) : s2;
297
352
  }
298
353
  function buildSentryEvent(input) {
299
354
  const { error, options = {}, identity, capturePII = false } = input;
@@ -382,21 +437,21 @@ function uid() {
382
437
  return "a-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
383
438
  }
384
439
  function anonId() {
385
- const s = ls();
386
- if (!s) return void 0;
387
- let v = s.getItem(KEY.anon);
440
+ const s2 = ls();
441
+ if (!s2) return void 0;
442
+ let v = s2.getItem(KEY.anon);
388
443
  if (!v) {
389
444
  v = uid();
390
- s.setItem(KEY.anon, v);
445
+ s2.setItem(KEY.anon, v);
391
446
  }
392
447
  return v;
393
448
  }
394
449
  function sessionId(now = Date.now()) {
395
- const s = ls();
396
- if (!s) return void 0;
450
+ const s2 = ls();
451
+ if (!s2) return void 0;
397
452
  let state = null;
398
453
  try {
399
- state = JSON.parse(s.getItem(KEY.session) || "null");
454
+ state = JSON.parse(s2.getItem(KEY.session) || "null");
400
455
  } catch {
401
456
  state = null;
402
457
  }
@@ -405,45 +460,45 @@ function sessionId(now = Date.now()) {
405
460
  } else {
406
461
  state.last = now;
407
462
  }
408
- s.setItem(KEY.session, JSON.stringify(state));
463
+ s2.setItem(KEY.session, JSON.stringify(state));
409
464
  return state.id;
410
465
  }
411
466
  function getFirstTouch() {
412
- const s = ls();
413
- if (!s) return void 0;
467
+ const s2 = ls();
468
+ if (!s2) return void 0;
414
469
  try {
415
- const v = s.getItem(KEY.firstTouch);
470
+ const v = s2.getItem(KEY.firstTouch);
416
471
  return v ? JSON.parse(v) : void 0;
417
472
  } catch {
418
473
  return void 0;
419
474
  }
420
475
  }
421
476
  function setFirstTouchOnce(a) {
422
- const s = ls();
477
+ const s2 = ls();
423
478
  const existing = getFirstTouch();
424
479
  if (existing) return existing;
425
- if (s) s.setItem(KEY.firstTouch, JSON.stringify(a));
480
+ if (s2) s2.setItem(KEY.firstTouch, JSON.stringify(a));
426
481
  return a;
427
482
  }
428
483
  function getCohort() {
429
- const s = ls();
430
- if (!s) return void 0;
484
+ const s2 = ls();
485
+ if (!s2) return void 0;
431
486
  try {
432
- const v = s.getItem(KEY.cohort);
487
+ const v = s2.getItem(KEY.cohort);
433
488
  return v ? JSON.parse(v) : void 0;
434
489
  } catch {
435
490
  return void 0;
436
491
  }
437
492
  }
438
493
  function mergeCohort(patch) {
439
- const s = ls();
494
+ const s2 = ls();
440
495
  const cur = getCohort() || {};
441
496
  const next = {
442
497
  signupWeek: cur.signupWeek || patch.signupWeek,
443
498
  channel: patch.channel || cur.channel,
444
499
  refCode: cur.refCode || patch.refCode
445
500
  };
446
- if (s) s.setItem(KEY.cohort, JSON.stringify(next));
501
+ if (s2) s2.setItem(KEY.cohort, JSON.stringify(next));
447
502
  return next;
448
503
  }
449
504
 
@@ -476,15 +531,8 @@ function uid2() {
476
531
  return "m-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
477
532
  }
478
533
  function normalizeError2(err) {
479
- if (err instanceof Error) {
480
- return { type: err.name, message: err.message, stack: err.stack };
481
- }
482
- if (typeof err === "string") return { message: err };
483
- try {
484
- return { message: JSON.stringify(err) };
485
- } catch {
486
- return { message: String(err) };
487
- }
534
+ const n = normalizeError(err);
535
+ return { type: n.name, message: n.message, stack: n.stack };
488
536
  }
489
537
  var isBrowser = () => typeof window !== "undefined";
490
538
  function serializeBatch(batch) {
@@ -557,7 +605,7 @@ var Analytics = class {
557
605
  ...config
558
606
  };
559
607
  this.transport = config.transport ?? new DefaultTransport();
560
- this.dsn = parseDsn(config.dsn ?? readEnvDsn());
608
+ this.dsn = parseDsn(config.dsn ?? readEnvDsn() ?? dsnForProduct(this.cfg.product));
561
609
  }
562
610
  /** errorPlaneEnabled reports whether captured exceptions can actually reach the
563
611
  * error host. False means a DSN was never configured — the documented
@@ -771,30 +819,139 @@ function createAnalytics(config) {
771
819
  return new Analytics(config);
772
820
  }
773
821
 
822
+ // src/funnels.ts
823
+ var PRODUCTS = ["site", "app", "chat", "console", "admin", "cloud"];
824
+ var s = (event, label, where) => ({
825
+ event,
826
+ label,
827
+ ...where ? { where } : {}
828
+ });
829
+ var FUNNELS = {
830
+ /** hanzo.ai: land → sign up. IAM hosts the form, so `signup_submitted` is the
831
+ * redirect INTO IAM and `signup_completed` is the return at /auth/callback. */
832
+ signup: {
833
+ label: "Signup",
834
+ products: ["site"],
835
+ join: "person",
836
+ steps: [
837
+ s(PAGEVIEW, "Landed"),
838
+ s(EVENTS.SIGNUP_VIEWED, "Opened signup"),
839
+ s(EVENTS.SIGNUP_SUBMITTED, "Redirected to Hanzo ID"),
840
+ s(EVENTS.SIGNUP_COMPLETED, "Account created"),
841
+ s(EVENTS.FIRST_ACTION, "First action")
842
+ ]
843
+ },
844
+ /** The developer activation path: an account is worth nothing until a key has
845
+ * made a call. `first_action{action:'api_call'}` is emitted SERVER-SIDE by
846
+ * Cloud on an org's first successful /v1 request — a browser cannot see it. */
847
+ apiActivation: {
848
+ label: "API activation",
849
+ products: ["site", "cloud"],
850
+ join: "person",
851
+ steps: [
852
+ s(EVENTS.SIGNUP_COMPLETED, "Account created"),
853
+ s(EVENTS.API_KEY_CREATED, "Key minted"),
854
+ s(EVENTS.FIRST_ACTION, "First successful API call", {
855
+ property: "action",
856
+ equals: "api_call"
857
+ })
858
+ ]
859
+ },
860
+ /** Upgrade intent → revenue. `order_completed{kind:'plan'}` is the Sale goal. */
861
+ upgrade: {
862
+ label: "Upgrade",
863
+ products: ["site", "app", "console"],
864
+ join: "person",
865
+ steps: [
866
+ s(EVENTS.PRICING_VIEWED, "Viewed pricing"),
867
+ s(EVENTS.PLAN_CLICKED, "Chose a plan"),
868
+ s(EVENTS.CHECKOUT_STARTED, "Started checkout"),
869
+ s(EVENTS.ORDER_COMPLETED, "Paid")
870
+ ]
871
+ },
872
+ /** hanzo.app: describe → build → deploy → live URL. The whole product thesis
873
+ * in five steps; `deploy_succeeded` is the moment a live URL exists. */
874
+ appShip: {
875
+ label: "Describe \u2192 ship",
876
+ products: ["app"],
877
+ join: "person",
878
+ steps: [
879
+ s(PAGEVIEW, "Landed"),
880
+ s(EVENTS.BUILD_STARTED, "Described an app"),
881
+ s(EVENTS.GENERATION_COMPLETED, "Got a working build"),
882
+ s(EVENTS.DEPLOY_STARTED, "Hit publish"),
883
+ s(EVENTS.DEPLOY_SUCCEEDED, "Live URL")
884
+ ]
885
+ },
886
+ /** hanzo.chat: visit → first message → answer. `generation_completed` is what
887
+ * separates "typed something" from "got value". */
888
+ chatEngage: {
889
+ label: "Chat engagement",
890
+ products: ["chat"],
891
+ join: "person",
892
+ steps: [
893
+ s(PAGEVIEW, "Landed"),
894
+ s(EVENTS.CHAT_STARTED, "Started a conversation"),
895
+ s(EVENTS.CHAT_MESSAGE_SENT, "Sent a message"),
896
+ s(EVENTS.GENERATION_COMPLETED, "Got an answer")
897
+ ]
898
+ },
899
+ /** The cross-surface handoff: the hanzo.ai composer forwards its prompt to
900
+ * hanzo.chat. Two origins, two anonymousIds — so this is an AGGREGATE funnel.
901
+ * The join is the `referrerProduct` property hanzo.chat reads off `?hz_ref=`,
902
+ * which makes the drop-off measurable without any cross-domain identity. */
903
+ siteToChat: {
904
+ label: "Site \u2192 Chat handoff",
905
+ products: ["site", "chat"],
906
+ join: "aggregate",
907
+ steps: [
908
+ s(EVENTS.CHAT_STARTED, "Submitted the hanzo.ai composer", {
909
+ property: "source",
910
+ equals: "composer"
911
+ }),
912
+ s(EVENTS.CHAT_STARTED, "Landed in hanzo.chat", {
913
+ property: "referrerProduct",
914
+ equals: "site"
915
+ }),
916
+ s(EVENTS.GENERATION_COMPLETED, "Got an answer")
917
+ ]
918
+ }
919
+ };
920
+ function eventsOf(id) {
921
+ return FUNNELS[id].steps.map((step) => step.event);
922
+ }
923
+
774
924
  // src/goals.ts
775
925
  var GOALS = {
776
- // Signup: the conversion is signup_completed; the funnel is the four steps.
926
+ // Signup: the conversion is signup_completed, along the site signup funnel.
777
927
  signup: {
778
928
  label: "Signup",
779
929
  event: EVENTS.SIGNUP_COMPLETED,
780
- funnel: [
781
- EVENTS.SIGNUP_VIEWED,
782
- EVENTS.SIGNUP_SUBMITTED,
783
- EVENTS.SIGNUP_VERIFIED,
784
- EVENTS.FIRST_ACTION
785
- ]
930
+ funnelId: "signup",
931
+ funnel: eventsOf("signup")
786
932
  },
787
933
  // Sale: a completed order qualified as a plan purchase (kind=plan).
788
934
  sale: {
789
935
  label: "Sale",
790
936
  event: EVENTS.ORDER_COMPLETED,
937
+ funnelId: "upgrade",
938
+ funnel: eventsOf("upgrade"),
791
939
  filter: { property: "kind", equals: "plan" }
792
940
  },
793
941
  // Upgrade intent: a plan click; pricing_viewed is the top of its funnel.
794
942
  upgradeIntent: {
795
943
  label: "Upgrade Intent",
796
944
  event: EVENTS.PLAN_CLICKED,
797
- funnel: [EVENTS.PRICING_VIEWED, EVENTS.PLAN_CLICKED, EVENTS.CHECKOUT_STARTED]
945
+ funnelId: "upgrade",
946
+ funnel: eventsOf("upgrade")
947
+ },
948
+ // Activation: the ONE north-star conversion — an account that did the first
949
+ // valuable thing (a successful API call, a live app, a chat answer).
950
+ activation: {
951
+ label: "Activation",
952
+ event: EVENTS.FIRST_ACTION,
953
+ funnelId: "apiActivation",
954
+ funnel: eventsOf("apiActivation")
798
955
  }
799
956
  };
800
957
  var COHORTS = {
@@ -803,6 +960,6 @@ var COHORTS = {
803
960
  refCode: { field: "ref_code", label: "Referral code" }
804
961
  };
805
962
 
806
- export { Analytics, COHORTS, EVENTS, GOALS, PAGEVIEW, VERSION, buildEnvelope, buildSentryEvent, createAnalytics, deriveChannel, framesFromStack, getCohort, getFirstTouch, hasAttribution, hostOf, isoWeek, parseAttribution, parseDsn, redactSecrets, scrubPII, scrubText };
963
+ export { Analytics, COHORTS, EVENTS, FUNNELS, GOALS, PAGEVIEW, PRODUCTS, PRODUCT_DSN, VERSION, buildEnvelope, buildSentryEvent, createAnalytics, deriveChannel, dsnForProduct, eventsOf, framesFromStack, getCohort, getFirstTouch, hasAttribution, hostOf, isoWeek, parseAttribution, parseDsn, redactSecrets, scrubPII, scrubText };
807
964
  //# sourceMappingURL=index.mjs.map
808
965
  //# sourceMappingURL=index.mjs.map