@hanzo/event 0.3.2 → 0.3.3
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/dist/index.cjs +225 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -5
- package/dist/index.d.ts +114 -5
- package/dist/index.mjs +223 -80
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +28 -14
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +28 -14
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core.ts +14 -10
- 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 +2 -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/README.md
CHANGED
|
@@ -149,6 +149,26 @@ createAnalytics({
|
|
|
149
149
|
})
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
## Taxonomy, funnels & goals
|
|
153
|
+
|
|
154
|
+
**[TAXONOMY.md](./TAXONOMY.md) is the canonical spec** — naming convention,
|
|
155
|
+
property rules, `identify`/`group` semantics, the funnels for hanzo.ai /
|
|
156
|
+
hanzo.app / hanzo.chat, and the exact emit site (file:line) of every event on
|
|
157
|
+
each surface. Read it before adding an event.
|
|
158
|
+
|
|
159
|
+
`FUNNELS` (see `funnels.ts`) is the one funnel registry: each journey is an
|
|
160
|
+
ordered list of steps naming `EVENTS` values, scoped by `product`. A funnel that
|
|
161
|
+
spans origins while logged out is marked `join: 'aggregate'` — two origins mean
|
|
162
|
+
two `anonymousId`s, so a per-person rate across them would be a lie.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { FUNNELS, GOALS } from '@hanzo/event'
|
|
166
|
+
|
|
167
|
+
FUNNELS.appShip.steps.map((s) => s.event)
|
|
168
|
+
// ['$pageview','build_started','generation_completed','deploy_started','deploy_succeeded']
|
|
169
|
+
GOALS.signup.funnel // derived from FUNNELS.signup — never restated
|
|
170
|
+
```
|
|
171
|
+
|
|
152
172
|
## Goals & cohorts
|
|
153
173
|
|
|
154
174
|
`GOALS` and `COHORTS` (see `goals.ts`) are the shared, machine-readable insights
|
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(
|
|
@@ -80,6 +80,12 @@ var EVENTS = {
|
|
|
80
80
|
SIGNUP_SUBMITTED: "signup_submitted",
|
|
81
81
|
SIGNUP_VERIFIED: "signup_verified",
|
|
82
82
|
SIGNUP_COMPLETED: "signup_completed",
|
|
83
|
+
/** A RETURNING user authenticated — the non-signup half of the IAM callback.
|
|
84
|
+
* Keeping it distinct is what stops returning logins from inflating signups. */
|
|
85
|
+
LOGIN_COMPLETED: "login_completed",
|
|
86
|
+
/** Activation: the first moment of real value. ONE event for every product —
|
|
87
|
+
* the product-specific moment is the `action` property (api_call, app_live,
|
|
88
|
+
* chat_reply), never a new event name. */
|
|
83
89
|
FIRST_ACTION: "first_action",
|
|
84
90
|
// Waitlist + referral.
|
|
85
91
|
WAITLIST_JOINED: "waitlist_joined",
|
|
@@ -95,13 +101,27 @@ var EVENTS = {
|
|
|
95
101
|
FEATURE_USED: "feature_used",
|
|
96
102
|
API_KEY_CREATED: "api_key_created",
|
|
97
103
|
APP_CREATED: "app_created",
|
|
98
|
-
DEPLOY_STARTED: "deploy_started",
|
|
99
104
|
PROJECT_CREATED: "project_created",
|
|
100
105
|
AGENT_CREATED: "agent_created",
|
|
101
106
|
CHAT_STARTED: "chat_started",
|
|
102
107
|
CHAT_MESSAGE_SENT: "chat_message_sent",
|
|
108
|
+
/** The user switched model/endpoint — the single strongest quality signal a
|
|
109
|
+
* chat surface emits (a switch usually follows a bad answer). */
|
|
110
|
+
MODEL_SWITCHED: "model_switched",
|
|
103
111
|
TASK_STARTED: "task_started",
|
|
104
|
-
TASK_COMPLETED: "task_completed"
|
|
112
|
+
TASK_COMPLETED: "task_completed",
|
|
113
|
+
// Build → ship. `build_*` is a MODEL producing an artifact; `deploy_*` is that
|
|
114
|
+
// artifact going live. Intent (build_started) is never the same event as the
|
|
115
|
+
// artifact existing (app_created) — conflating them makes the funnel lie.
|
|
116
|
+
BUILD_STARTED: "build_started",
|
|
117
|
+
/** A model finished producing an artifact (an app build, a chat reply, an agent
|
|
118
|
+
* run). Carries `durationMs` — the outcome event owns its own duration, so no
|
|
119
|
+
* paired start event is needed. */
|
|
120
|
+
GENERATION_COMPLETED: "generation_completed",
|
|
121
|
+
GENERATION_FAILED: "generation_failed",
|
|
122
|
+
DEPLOY_STARTED: "deploy_started",
|
|
123
|
+
DEPLOY_SUCCEEDED: "deploy_succeeded",
|
|
124
|
+
DEPLOY_FAILED: "deploy_failed"
|
|
105
125
|
};
|
|
106
126
|
var PAGEVIEW = "$pageview";
|
|
107
127
|
|
|
@@ -153,40 +173,40 @@ function luhn(digits) {
|
|
|
153
173
|
}
|
|
154
174
|
return sum % 10 === 0;
|
|
155
175
|
}
|
|
156
|
-
function redactPAN(
|
|
157
|
-
return
|
|
176
|
+
function redactPAN(s2) {
|
|
177
|
+
return s2.replace(RE_PAN, (m) => {
|
|
158
178
|
const digits = m.replace(/[ -]/g, "");
|
|
159
179
|
if (digits.length < 13 || digits.length > 19) return m;
|
|
160
180
|
return luhn(digits) ? REDACTED : m;
|
|
161
181
|
});
|
|
162
182
|
}
|
|
163
|
-
var RE_EMAIL = /[A-Za-z0-9._%+-]
|
|
183
|
+
var RE_EMAIL = /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g;
|
|
164
184
|
var RE_IPV4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
|
|
165
185
|
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(
|
|
186
|
+
function redactSecrets(s2) {
|
|
187
|
+
for (const re of SECRET_PATTERNS) s2 = s2.replace(re, REDACTED);
|
|
188
|
+
return redactPAN(s2);
|
|
169
189
|
}
|
|
170
|
-
function scrubPII(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return
|
|
190
|
+
function scrubPII(s2) {
|
|
191
|
+
s2 = s2.replace(RE_EMAIL, EMAIL_MARK);
|
|
192
|
+
s2 = s2.replace(RE_IPV6, IP_MARK);
|
|
193
|
+
s2 = s2.replace(RE_IPV4, IP_MARK);
|
|
194
|
+
return s2;
|
|
175
195
|
}
|
|
176
196
|
var MAX_SCRUB_LEN = 8192;
|
|
177
|
-
function truncate(
|
|
178
|
-
return
|
|
197
|
+
function truncate(s2, max = MAX_SCRUB_LEN) {
|
|
198
|
+
return s2.length > max ? s2.slice(0, max) + "\u2026 [truncated]" : s2;
|
|
179
199
|
}
|
|
180
|
-
function scrubText(
|
|
181
|
-
if (!
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (!capturePII)
|
|
185
|
-
return
|
|
200
|
+
function scrubText(s2, capturePII = false) {
|
|
201
|
+
if (!s2) return s2 ?? "";
|
|
202
|
+
s2 = truncate(s2);
|
|
203
|
+
s2 = redactSecrets(s2);
|
|
204
|
+
if (!capturePII) s2 = scrubPII(s2);
|
|
205
|
+
return s2;
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
// src/version.ts
|
|
189
|
-
var VERSION = "0.3.
|
|
209
|
+
var VERSION = "0.3.3";
|
|
190
210
|
|
|
191
211
|
// src/sentry.ts
|
|
192
212
|
var MAX_FRAMES = 50;
|
|
@@ -197,14 +217,14 @@ var MAX_TAGS = 50;
|
|
|
197
217
|
function eventId() {
|
|
198
218
|
const c = typeof crypto !== "undefined" ? crypto : void 0;
|
|
199
219
|
if (c && "randomUUID" in c) return c.randomUUID().replace(/-/g, "");
|
|
200
|
-
let
|
|
201
|
-
for (let i = 0; i < 32; i++)
|
|
202
|
-
return
|
|
220
|
+
let s2 = "";
|
|
221
|
+
for (let i = 0; i < 32; i++) s2 += Math.floor(Math.random() * 16).toString(16);
|
|
222
|
+
return s2;
|
|
203
223
|
}
|
|
204
|
-
function byteLen(
|
|
205
|
-
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(
|
|
206
|
-
if (typeof Buffer !== "undefined") return Buffer.byteLength(
|
|
207
|
-
return
|
|
224
|
+
function byteLen(s2) {
|
|
225
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s2).length;
|
|
226
|
+
if (typeof Buffer !== "undefined") return Buffer.byteLength(s2, "utf8");
|
|
227
|
+
return s2.length;
|
|
208
228
|
}
|
|
209
229
|
function parseDsn(dsn) {
|
|
210
230
|
if (!dsn) return null;
|
|
@@ -274,17 +294,38 @@ function framesFromStack(stack) {
|
|
|
274
294
|
}
|
|
275
295
|
function normalizeError(err) {
|
|
276
296
|
if (err instanceof Error) {
|
|
277
|
-
|
|
297
|
+
const name = read(err, "name");
|
|
298
|
+
const message = read(err, "message");
|
|
299
|
+
const stack = read(err, "stack");
|
|
300
|
+
return {
|
|
301
|
+
name: typeof name === "string" && name ? name : "Error",
|
|
302
|
+
message: typeof message === "string" && message ? message : str(err),
|
|
303
|
+
stack: typeof stack === "string" ? stack : void 0
|
|
304
|
+
};
|
|
278
305
|
}
|
|
279
306
|
if (typeof err === "string") return { name: "Error", message: err };
|
|
280
307
|
try {
|
|
281
|
-
return { name: "Error", message: JSON.stringify(err) };
|
|
308
|
+
return { name: "Error", message: JSON.stringify(err) ?? str(err) };
|
|
309
|
+
} catch {
|
|
310
|
+
return { name: "Error", message: str(err) };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function read(o, k) {
|
|
314
|
+
try {
|
|
315
|
+
return o[k];
|
|
282
316
|
} catch {
|
|
283
|
-
return
|
|
317
|
+
return void 0;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function str(v) {
|
|
321
|
+
try {
|
|
322
|
+
return String(v);
|
|
323
|
+
} catch {
|
|
324
|
+
return "[unstringifiable]";
|
|
284
325
|
}
|
|
285
326
|
}
|
|
286
327
|
function coerceTag(v) {
|
|
287
|
-
const
|
|
328
|
+
const s2 = typeof v === "string" ? v : (() => {
|
|
288
329
|
try {
|
|
289
330
|
return JSON.stringify(v) ?? String(v);
|
|
290
331
|
} catch {
|
|
@@ -295,7 +336,7 @@ function coerceTag(v) {
|
|
|
295
336
|
}
|
|
296
337
|
}
|
|
297
338
|
})();
|
|
298
|
-
return
|
|
339
|
+
return s2.length > MAX_TAG_LEN ? s2.slice(0, MAX_TAG_LEN) : s2;
|
|
299
340
|
}
|
|
300
341
|
function buildSentryEvent(input) {
|
|
301
342
|
const { error, options = {}, identity, capturePII = false } = input;
|
|
@@ -384,21 +425,21 @@ function uid() {
|
|
|
384
425
|
return "a-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
385
426
|
}
|
|
386
427
|
function anonId() {
|
|
387
|
-
const
|
|
388
|
-
if (!
|
|
389
|
-
let v =
|
|
428
|
+
const s2 = ls();
|
|
429
|
+
if (!s2) return void 0;
|
|
430
|
+
let v = s2.getItem(KEY.anon);
|
|
390
431
|
if (!v) {
|
|
391
432
|
v = uid();
|
|
392
|
-
|
|
433
|
+
s2.setItem(KEY.anon, v);
|
|
393
434
|
}
|
|
394
435
|
return v;
|
|
395
436
|
}
|
|
396
437
|
function sessionId(now = Date.now()) {
|
|
397
|
-
const
|
|
398
|
-
if (!
|
|
438
|
+
const s2 = ls();
|
|
439
|
+
if (!s2) return void 0;
|
|
399
440
|
let state = null;
|
|
400
441
|
try {
|
|
401
|
-
state = JSON.parse(
|
|
442
|
+
state = JSON.parse(s2.getItem(KEY.session) || "null");
|
|
402
443
|
} catch {
|
|
403
444
|
state = null;
|
|
404
445
|
}
|
|
@@ -407,45 +448,45 @@ function sessionId(now = Date.now()) {
|
|
|
407
448
|
} else {
|
|
408
449
|
state.last = now;
|
|
409
450
|
}
|
|
410
|
-
|
|
451
|
+
s2.setItem(KEY.session, JSON.stringify(state));
|
|
411
452
|
return state.id;
|
|
412
453
|
}
|
|
413
454
|
function getFirstTouch() {
|
|
414
|
-
const
|
|
415
|
-
if (!
|
|
455
|
+
const s2 = ls();
|
|
456
|
+
if (!s2) return void 0;
|
|
416
457
|
try {
|
|
417
|
-
const v =
|
|
458
|
+
const v = s2.getItem(KEY.firstTouch);
|
|
418
459
|
return v ? JSON.parse(v) : void 0;
|
|
419
460
|
} catch {
|
|
420
461
|
return void 0;
|
|
421
462
|
}
|
|
422
463
|
}
|
|
423
464
|
function setFirstTouchOnce(a) {
|
|
424
|
-
const
|
|
465
|
+
const s2 = ls();
|
|
425
466
|
const existing = getFirstTouch();
|
|
426
467
|
if (existing) return existing;
|
|
427
|
-
if (
|
|
468
|
+
if (s2) s2.setItem(KEY.firstTouch, JSON.stringify(a));
|
|
428
469
|
return a;
|
|
429
470
|
}
|
|
430
471
|
function getCohort() {
|
|
431
|
-
const
|
|
432
|
-
if (!
|
|
472
|
+
const s2 = ls();
|
|
473
|
+
if (!s2) return void 0;
|
|
433
474
|
try {
|
|
434
|
-
const v =
|
|
475
|
+
const v = s2.getItem(KEY.cohort);
|
|
435
476
|
return v ? JSON.parse(v) : void 0;
|
|
436
477
|
} catch {
|
|
437
478
|
return void 0;
|
|
438
479
|
}
|
|
439
480
|
}
|
|
440
481
|
function mergeCohort(patch) {
|
|
441
|
-
const
|
|
482
|
+
const s2 = ls();
|
|
442
483
|
const cur = getCohort() || {};
|
|
443
484
|
const next = {
|
|
444
485
|
signupWeek: cur.signupWeek || patch.signupWeek,
|
|
445
486
|
channel: patch.channel || cur.channel,
|
|
446
487
|
refCode: cur.refCode || patch.refCode
|
|
447
488
|
};
|
|
448
|
-
if (
|
|
489
|
+
if (s2) s2.setItem(KEY.cohort, JSON.stringify(next));
|
|
449
490
|
return next;
|
|
450
491
|
}
|
|
451
492
|
|
|
@@ -478,15 +519,8 @@ function uid2() {
|
|
|
478
519
|
return "m-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
479
520
|
}
|
|
480
521
|
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
|
-
}
|
|
522
|
+
const n = normalizeError(err);
|
|
523
|
+
return { type: n.name, message: n.message, stack: n.stack };
|
|
490
524
|
}
|
|
491
525
|
var isBrowser = () => typeof window !== "undefined";
|
|
492
526
|
function serializeBatch(batch) {
|
|
@@ -773,30 +807,139 @@ function createAnalytics(config) {
|
|
|
773
807
|
return new Analytics(config);
|
|
774
808
|
}
|
|
775
809
|
|
|
810
|
+
// src/funnels.ts
|
|
811
|
+
var PRODUCTS = ["site", "app", "chat", "console", "admin", "cloud"];
|
|
812
|
+
var s = (event, label, where) => ({
|
|
813
|
+
event,
|
|
814
|
+
label,
|
|
815
|
+
...where ? { where } : {}
|
|
816
|
+
});
|
|
817
|
+
var FUNNELS = {
|
|
818
|
+
/** hanzo.ai: land → sign up. IAM hosts the form, so `signup_submitted` is the
|
|
819
|
+
* redirect INTO IAM and `signup_completed` is the return at /auth/callback. */
|
|
820
|
+
signup: {
|
|
821
|
+
label: "Signup",
|
|
822
|
+
products: ["site"],
|
|
823
|
+
join: "person",
|
|
824
|
+
steps: [
|
|
825
|
+
s(PAGEVIEW, "Landed"),
|
|
826
|
+
s(EVENTS.SIGNUP_VIEWED, "Opened signup"),
|
|
827
|
+
s(EVENTS.SIGNUP_SUBMITTED, "Redirected to Hanzo ID"),
|
|
828
|
+
s(EVENTS.SIGNUP_COMPLETED, "Account created"),
|
|
829
|
+
s(EVENTS.FIRST_ACTION, "First action")
|
|
830
|
+
]
|
|
831
|
+
},
|
|
832
|
+
/** The developer activation path: an account is worth nothing until a key has
|
|
833
|
+
* made a call. `first_action{action:'api_call'}` is emitted SERVER-SIDE by
|
|
834
|
+
* Cloud on an org's first successful /v1 request — a browser cannot see it. */
|
|
835
|
+
apiActivation: {
|
|
836
|
+
label: "API activation",
|
|
837
|
+
products: ["site", "cloud"],
|
|
838
|
+
join: "person",
|
|
839
|
+
steps: [
|
|
840
|
+
s(EVENTS.SIGNUP_COMPLETED, "Account created"),
|
|
841
|
+
s(EVENTS.API_KEY_CREATED, "Key minted"),
|
|
842
|
+
s(EVENTS.FIRST_ACTION, "First successful API call", {
|
|
843
|
+
property: "action",
|
|
844
|
+
equals: "api_call"
|
|
845
|
+
})
|
|
846
|
+
]
|
|
847
|
+
},
|
|
848
|
+
/** Upgrade intent → revenue. `order_completed{kind:'plan'}` is the Sale goal. */
|
|
849
|
+
upgrade: {
|
|
850
|
+
label: "Upgrade",
|
|
851
|
+
products: ["site", "app", "console"],
|
|
852
|
+
join: "person",
|
|
853
|
+
steps: [
|
|
854
|
+
s(EVENTS.PRICING_VIEWED, "Viewed pricing"),
|
|
855
|
+
s(EVENTS.PLAN_CLICKED, "Chose a plan"),
|
|
856
|
+
s(EVENTS.CHECKOUT_STARTED, "Started checkout"),
|
|
857
|
+
s(EVENTS.ORDER_COMPLETED, "Paid")
|
|
858
|
+
]
|
|
859
|
+
},
|
|
860
|
+
/** hanzo.app: describe → build → deploy → live URL. The whole product thesis
|
|
861
|
+
* in five steps; `deploy_succeeded` is the moment a live URL exists. */
|
|
862
|
+
appShip: {
|
|
863
|
+
label: "Describe \u2192 ship",
|
|
864
|
+
products: ["app"],
|
|
865
|
+
join: "person",
|
|
866
|
+
steps: [
|
|
867
|
+
s(PAGEVIEW, "Landed"),
|
|
868
|
+
s(EVENTS.BUILD_STARTED, "Described an app"),
|
|
869
|
+
s(EVENTS.GENERATION_COMPLETED, "Got a working build"),
|
|
870
|
+
s(EVENTS.DEPLOY_STARTED, "Hit publish"),
|
|
871
|
+
s(EVENTS.DEPLOY_SUCCEEDED, "Live URL")
|
|
872
|
+
]
|
|
873
|
+
},
|
|
874
|
+
/** hanzo.chat: visit → first message → answer. `generation_completed` is what
|
|
875
|
+
* separates "typed something" from "got value". */
|
|
876
|
+
chatEngage: {
|
|
877
|
+
label: "Chat engagement",
|
|
878
|
+
products: ["chat"],
|
|
879
|
+
join: "person",
|
|
880
|
+
steps: [
|
|
881
|
+
s(PAGEVIEW, "Landed"),
|
|
882
|
+
s(EVENTS.CHAT_STARTED, "Started a conversation"),
|
|
883
|
+
s(EVENTS.CHAT_MESSAGE_SENT, "Sent a message"),
|
|
884
|
+
s(EVENTS.GENERATION_COMPLETED, "Got an answer")
|
|
885
|
+
]
|
|
886
|
+
},
|
|
887
|
+
/** The cross-surface handoff: the hanzo.ai composer forwards its prompt to
|
|
888
|
+
* hanzo.chat. Two origins, two anonymousIds — so this is an AGGREGATE funnel.
|
|
889
|
+
* The join is the `referrerProduct` property hanzo.chat reads off `?hz_ref=`,
|
|
890
|
+
* which makes the drop-off measurable without any cross-domain identity. */
|
|
891
|
+
siteToChat: {
|
|
892
|
+
label: "Site \u2192 Chat handoff",
|
|
893
|
+
products: ["site", "chat"],
|
|
894
|
+
join: "aggregate",
|
|
895
|
+
steps: [
|
|
896
|
+
s(EVENTS.CHAT_STARTED, "Submitted the hanzo.ai composer", {
|
|
897
|
+
property: "source",
|
|
898
|
+
equals: "composer"
|
|
899
|
+
}),
|
|
900
|
+
s(EVENTS.CHAT_STARTED, "Landed in hanzo.chat", {
|
|
901
|
+
property: "referrerProduct",
|
|
902
|
+
equals: "site"
|
|
903
|
+
}),
|
|
904
|
+
s(EVENTS.GENERATION_COMPLETED, "Got an answer")
|
|
905
|
+
]
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
function eventsOf(id) {
|
|
909
|
+
return FUNNELS[id].steps.map((step) => step.event);
|
|
910
|
+
}
|
|
911
|
+
|
|
776
912
|
// src/goals.ts
|
|
777
913
|
var GOALS = {
|
|
778
|
-
// Signup: the conversion is signup_completed
|
|
914
|
+
// Signup: the conversion is signup_completed, along the site signup funnel.
|
|
779
915
|
signup: {
|
|
780
916
|
label: "Signup",
|
|
781
917
|
event: EVENTS.SIGNUP_COMPLETED,
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
EVENTS.SIGNUP_SUBMITTED,
|
|
785
|
-
EVENTS.SIGNUP_VERIFIED,
|
|
786
|
-
EVENTS.FIRST_ACTION
|
|
787
|
-
]
|
|
918
|
+
funnelId: "signup",
|
|
919
|
+
funnel: eventsOf("signup")
|
|
788
920
|
},
|
|
789
921
|
// Sale: a completed order qualified as a plan purchase (kind=plan).
|
|
790
922
|
sale: {
|
|
791
923
|
label: "Sale",
|
|
792
924
|
event: EVENTS.ORDER_COMPLETED,
|
|
925
|
+
funnelId: "upgrade",
|
|
926
|
+
funnel: eventsOf("upgrade"),
|
|
793
927
|
filter: { property: "kind", equals: "plan" }
|
|
794
928
|
},
|
|
795
929
|
// Upgrade intent: a plan click; pricing_viewed is the top of its funnel.
|
|
796
930
|
upgradeIntent: {
|
|
797
931
|
label: "Upgrade Intent",
|
|
798
932
|
event: EVENTS.PLAN_CLICKED,
|
|
799
|
-
|
|
933
|
+
funnelId: "upgrade",
|
|
934
|
+
funnel: eventsOf("upgrade")
|
|
935
|
+
},
|
|
936
|
+
// Activation: the ONE north-star conversion — an account that did the first
|
|
937
|
+
// valuable thing (a successful API call, a live app, a chat answer).
|
|
938
|
+
activation: {
|
|
939
|
+
label: "Activation",
|
|
940
|
+
event: EVENTS.FIRST_ACTION,
|
|
941
|
+
funnelId: "apiActivation",
|
|
942
|
+
funnel: eventsOf("apiActivation")
|
|
800
943
|
}
|
|
801
944
|
};
|
|
802
945
|
var COHORTS = {
|
|
@@ -808,13 +951,16 @@ var COHORTS = {
|
|
|
808
951
|
exports.Analytics = Analytics;
|
|
809
952
|
exports.COHORTS = COHORTS;
|
|
810
953
|
exports.EVENTS = EVENTS;
|
|
954
|
+
exports.FUNNELS = FUNNELS;
|
|
811
955
|
exports.GOALS = GOALS;
|
|
812
956
|
exports.PAGEVIEW = PAGEVIEW;
|
|
957
|
+
exports.PRODUCTS = PRODUCTS;
|
|
813
958
|
exports.VERSION = VERSION;
|
|
814
959
|
exports.buildEnvelope = buildEnvelope;
|
|
815
960
|
exports.buildSentryEvent = buildSentryEvent;
|
|
816
961
|
exports.createAnalytics = createAnalytics;
|
|
817
962
|
exports.deriveChannel = deriveChannel;
|
|
963
|
+
exports.eventsOf = eventsOf;
|
|
818
964
|
exports.framesFromStack = framesFromStack;
|
|
819
965
|
exports.getCohort = getCohort;
|
|
820
966
|
exports.getFirstTouch = getFirstTouch;
|