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