@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/README.md +20 -0
- package/TAXONOMY.md +257 -0
- package/dist/index.cjs +242 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +147 -5
- package/dist/index.d.ts +147 -5
- package/dist/index.mjs +238 -81
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +43 -15
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +43 -15
- package/dist/react.mjs.map +1 -1
- package/package.json +4 -3
- package/src/core.test.ts +9 -4
- package/src/core.ts +21 -13
- package/src/dsn.test.ts +62 -0
- package/src/dsn.ts +45 -0
- package/src/events.ts +30 -1
- package/src/funnels.test.ts +92 -0
- package/src/funnels.ts +154 -0
- package/src/goals.ts +25 -11
- package/src/index.ts +3 -0
- package/src/scrub.ts +6 -1
- package/src/sentry.test.ts +52 -0
- package/src/sentry.ts +33 -3
- package/src/version.ts +1 -1
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
|
|
50
|
-
const scheme =
|
|
51
|
-
if (scheme >= 0)
|
|
52
|
-
const cut =
|
|
53
|
-
if (cut >= 0)
|
|
54
|
-
const at =
|
|
55
|
-
if (at >= 0)
|
|
56
|
-
const colon =
|
|
57
|
-
if (colon >= 0)
|
|
58
|
-
return
|
|
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(
|
|
155
|
-
return
|
|
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._%+-]
|
|
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(
|
|
165
|
-
for (const re of SECRET_PATTERNS)
|
|
166
|
-
return redactPAN(
|
|
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(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return
|
|
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(
|
|
176
|
-
return
|
|
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(
|
|
179
|
-
if (!
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (!capturePII)
|
|
183
|
-
return
|
|
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.
|
|
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
|
|
199
|
-
for (let i = 0; i < 32; i++)
|
|
200
|
-
return
|
|
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(
|
|
203
|
-
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(
|
|
204
|
-
if (typeof Buffer !== "undefined") return Buffer.byteLength(
|
|
205
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
386
|
-
if (!
|
|
387
|
-
let v =
|
|
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
|
-
|
|
445
|
+
s2.setItem(KEY.anon, v);
|
|
391
446
|
}
|
|
392
447
|
return v;
|
|
393
448
|
}
|
|
394
449
|
function sessionId(now = Date.now()) {
|
|
395
|
-
const
|
|
396
|
-
if (!
|
|
450
|
+
const s2 = ls();
|
|
451
|
+
if (!s2) return void 0;
|
|
397
452
|
let state = null;
|
|
398
453
|
try {
|
|
399
|
-
state = JSON.parse(
|
|
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
|
-
|
|
463
|
+
s2.setItem(KEY.session, JSON.stringify(state));
|
|
409
464
|
return state.id;
|
|
410
465
|
}
|
|
411
466
|
function getFirstTouch() {
|
|
412
|
-
const
|
|
413
|
-
if (!
|
|
467
|
+
const s2 = ls();
|
|
468
|
+
if (!s2) return void 0;
|
|
414
469
|
try {
|
|
415
|
-
const v =
|
|
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
|
|
477
|
+
const s2 = ls();
|
|
423
478
|
const existing = getFirstTouch();
|
|
424
479
|
if (existing) return existing;
|
|
425
|
-
if (
|
|
480
|
+
if (s2) s2.setItem(KEY.firstTouch, JSON.stringify(a));
|
|
426
481
|
return a;
|
|
427
482
|
}
|
|
428
483
|
function getCohort() {
|
|
429
|
-
const
|
|
430
|
-
if (!
|
|
484
|
+
const s2 = ls();
|
|
485
|
+
if (!s2) return void 0;
|
|
431
486
|
try {
|
|
432
|
-
const v =
|
|
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
|
|
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 (
|
|
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
|
-
|
|
480
|
-
|
|
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
|
|
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
|
-
|
|
781
|
-
|
|
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
|
-
|
|
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
|