@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/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ declare function getFirstTouch(): Attribution | undefined;
|
|
|
6
6
|
/** Read persisted cohort dimensions. */
|
|
7
7
|
declare function getCohort(): Cohort | undefined;
|
|
8
8
|
|
|
9
|
-
declare const VERSION = "0.3.
|
|
9
|
+
declare const VERSION = "0.3.3";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* parseDsn parses "https://<version>:<hmac>@<host>/v1/sentry/<projectId>" into its
|
|
@@ -73,6 +73,12 @@ declare const EVENTS: {
|
|
|
73
73
|
readonly SIGNUP_SUBMITTED: "signup_submitted";
|
|
74
74
|
readonly SIGNUP_VERIFIED: "signup_verified";
|
|
75
75
|
readonly SIGNUP_COMPLETED: "signup_completed";
|
|
76
|
+
/** A RETURNING user authenticated — the non-signup half of the IAM callback.
|
|
77
|
+
* Keeping it distinct is what stops returning logins from inflating signups. */
|
|
78
|
+
readonly LOGIN_COMPLETED: "login_completed";
|
|
79
|
+
/** Activation: the first moment of real value. ONE event for every product —
|
|
80
|
+
* the product-specific moment is the `action` property (api_call, app_live,
|
|
81
|
+
* chat_reply), never a new event name. */
|
|
76
82
|
readonly FIRST_ACTION: "first_action";
|
|
77
83
|
readonly WAITLIST_JOINED: "waitlist_joined";
|
|
78
84
|
readonly WAITLIST_SHARED: "waitlist_shared";
|
|
@@ -85,24 +91,127 @@ declare const EVENTS: {
|
|
|
85
91
|
readonly FEATURE_USED: "feature_used";
|
|
86
92
|
readonly API_KEY_CREATED: "api_key_created";
|
|
87
93
|
readonly APP_CREATED: "app_created";
|
|
88
|
-
readonly DEPLOY_STARTED: "deploy_started";
|
|
89
94
|
readonly PROJECT_CREATED: "project_created";
|
|
90
95
|
readonly AGENT_CREATED: "agent_created";
|
|
91
96
|
readonly CHAT_STARTED: "chat_started";
|
|
92
97
|
readonly CHAT_MESSAGE_SENT: "chat_message_sent";
|
|
98
|
+
/** The user switched model/endpoint — the single strongest quality signal a
|
|
99
|
+
* chat surface emits (a switch usually follows a bad answer). */
|
|
100
|
+
readonly MODEL_SWITCHED: "model_switched";
|
|
93
101
|
readonly TASK_STARTED: "task_started";
|
|
94
102
|
readonly TASK_COMPLETED: "task_completed";
|
|
103
|
+
readonly BUILD_STARTED: "build_started";
|
|
104
|
+
/** A model finished producing an artifact (an app build, a chat reply, an agent
|
|
105
|
+
* run). Carries `durationMs` — the outcome event owns its own duration, so no
|
|
106
|
+
* paired start event is needed. */
|
|
107
|
+
readonly GENERATION_COMPLETED: "generation_completed";
|
|
108
|
+
readonly GENERATION_FAILED: "generation_failed";
|
|
109
|
+
readonly DEPLOY_STARTED: "deploy_started";
|
|
110
|
+
readonly DEPLOY_SUCCEEDED: "deploy_succeeded";
|
|
111
|
+
readonly DEPLOY_FAILED: "deploy_failed";
|
|
95
112
|
};
|
|
96
113
|
type EventName = (typeof EVENTS)[keyof typeof EVENTS];
|
|
97
114
|
/** The reserved event name a pageview is stored under (server + read lens). */
|
|
98
115
|
declare const PAGEVIEW = "$pageview";
|
|
99
116
|
|
|
117
|
+
/** The emitting surfaces — the closed set of `AnalyticsConfig.product` values.
|
|
118
|
+
* `product` is on every event, so a funnel scopes by product instead of every
|
|
119
|
+
* surface prefixing its event names. */
|
|
120
|
+
declare const PRODUCTS: readonly ["site", "app", "chat", "console", "admin", "cloud"];
|
|
121
|
+
type ProductId = (typeof PRODUCTS)[number];
|
|
122
|
+
interface FunnelStep {
|
|
123
|
+
/** An EVENTS value (or PAGEVIEW). */
|
|
124
|
+
event: string;
|
|
125
|
+
/** Human label for the Insights step. */
|
|
126
|
+
label: string;
|
|
127
|
+
/** Property equality that qualifies the step, e.g. first_action{action:'api_call'}. */
|
|
128
|
+
where?: {
|
|
129
|
+
property: string;
|
|
130
|
+
equals: string;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
interface FunnelDef {
|
|
134
|
+
label: string;
|
|
135
|
+
/** Surface(s) the steps are emitted from — matched against the `product` field. */
|
|
136
|
+
products: ProductId[];
|
|
137
|
+
/**
|
|
138
|
+
* How steps are joined:
|
|
139
|
+
* • 'person' — steps join on distinctId (one browser, or one logged-in
|
|
140
|
+
* person across surfaces). The normal case.
|
|
141
|
+
* • 'aggregate' — steps are emitted on DIFFERENT origins by a LOGGED-OUT
|
|
142
|
+
* visitor, so there is no shared id: hanzo.ai, hanzo.app and
|
|
143
|
+
* hanzo.chat each mint their own anonymousId in their own
|
|
144
|
+
* storage. Read these as step-over-step COUNTS, never as a
|
|
145
|
+
* per-person conversion. Honest by construction.
|
|
146
|
+
*/
|
|
147
|
+
join: 'person' | 'aggregate';
|
|
148
|
+
steps: FunnelStep[];
|
|
149
|
+
}
|
|
150
|
+
declare const FUNNELS: {
|
|
151
|
+
/** hanzo.ai: land → sign up. IAM hosts the form, so `signup_submitted` is the
|
|
152
|
+
* redirect INTO IAM and `signup_completed` is the return at /auth/callback. */
|
|
153
|
+
readonly signup: {
|
|
154
|
+
readonly label: "Signup";
|
|
155
|
+
readonly products: ["site"];
|
|
156
|
+
readonly join: "person";
|
|
157
|
+
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
158
|
+
};
|
|
159
|
+
/** The developer activation path: an account is worth nothing until a key has
|
|
160
|
+
* made a call. `first_action{action:'api_call'}` is emitted SERVER-SIDE by
|
|
161
|
+
* Cloud on an org's first successful /v1 request — a browser cannot see it. */
|
|
162
|
+
readonly apiActivation: {
|
|
163
|
+
readonly label: "API activation";
|
|
164
|
+
readonly products: ["site", "cloud"];
|
|
165
|
+
readonly join: "person";
|
|
166
|
+
readonly steps: [FunnelStep, FunnelStep, FunnelStep];
|
|
167
|
+
};
|
|
168
|
+
/** Upgrade intent → revenue. `order_completed{kind:'plan'}` is the Sale goal. */
|
|
169
|
+
readonly upgrade: {
|
|
170
|
+
readonly label: "Upgrade";
|
|
171
|
+
readonly products: ["site", "app", "console"];
|
|
172
|
+
readonly join: "person";
|
|
173
|
+
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
174
|
+
};
|
|
175
|
+
/** hanzo.app: describe → build → deploy → live URL. The whole product thesis
|
|
176
|
+
* in five steps; `deploy_succeeded` is the moment a live URL exists. */
|
|
177
|
+
readonly appShip: {
|
|
178
|
+
readonly label: "Describe → ship";
|
|
179
|
+
readonly products: ["app"];
|
|
180
|
+
readonly join: "person";
|
|
181
|
+
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
182
|
+
};
|
|
183
|
+
/** hanzo.chat: visit → first message → answer. `generation_completed` is what
|
|
184
|
+
* separates "typed something" from "got value". */
|
|
185
|
+
readonly chatEngage: {
|
|
186
|
+
readonly label: "Chat engagement";
|
|
187
|
+
readonly products: ["chat"];
|
|
188
|
+
readonly join: "person";
|
|
189
|
+
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
190
|
+
};
|
|
191
|
+
/** The cross-surface handoff: the hanzo.ai composer forwards its prompt to
|
|
192
|
+
* hanzo.chat. Two origins, two anonymousIds — so this is an AGGREGATE funnel.
|
|
193
|
+
* The join is the `referrerProduct` property hanzo.chat reads off `?hz_ref=`,
|
|
194
|
+
* which makes the drop-off measurable without any cross-domain identity. */
|
|
195
|
+
readonly siteToChat: {
|
|
196
|
+
readonly label: "Site → Chat handoff";
|
|
197
|
+
readonly products: ["site", "chat"];
|
|
198
|
+
readonly join: "aggregate";
|
|
199
|
+
readonly steps: [FunnelStep, FunnelStep, FunnelStep];
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
type FunnelId = keyof typeof FUNNELS;
|
|
203
|
+
/** eventsOf flattens a funnel to its ordered event names — what a goal's `funnel`
|
|
204
|
+
* field carries, so the steps are defined exactly once (here). */
|
|
205
|
+
declare function eventsOf(id: FunnelId): string[];
|
|
206
|
+
|
|
100
207
|
interface GoalDef {
|
|
101
208
|
/** Human label shown in Insights. */
|
|
102
209
|
label: string;
|
|
103
210
|
/** The event whose occurrence counts as the goal conversion. */
|
|
104
211
|
event: string;
|
|
105
|
-
/**
|
|
212
|
+
/** The funnel leading to the goal — an id into FUNNELS (see funnels.ts). */
|
|
213
|
+
funnelId?: FunnelId;
|
|
214
|
+
/** The ordered event names of `funnelId`, derived — never hand-written. */
|
|
106
215
|
funnel?: string[];
|
|
107
216
|
/** Optional property equality filter that qualifies the conversion. */
|
|
108
217
|
filter?: {
|
|
@@ -110,7 +219,7 @@ interface GoalDef {
|
|
|
110
219
|
equals: string;
|
|
111
220
|
};
|
|
112
221
|
}
|
|
113
|
-
declare const GOALS: Record<'signup' | 'sale' | 'upgradeIntent', GoalDef>;
|
|
222
|
+
declare const GOALS: Record<'signup' | 'sale' | 'upgradeIntent' | 'activation', GoalDef>;
|
|
114
223
|
interface CohortDef {
|
|
115
224
|
/** The hanzo.events column the cohort dimension maps to. */
|
|
116
225
|
field: string;
|
|
@@ -131,4 +240,4 @@ declare function hasAttribution(a: Attribution): boolean;
|
|
|
131
240
|
/** isoWeek returns the ISO-8601 week label, e.g. "2026-W28". */
|
|
132
241
|
declare function isoWeek(d: Date): string;
|
|
133
242
|
|
|
134
|
-
export { Attribution, COHORTS, CaptureErrorOptions, Cohort, type CohortDef, Dsn, EVENTS, type ErrorIdentity, type EventName, GOALS, type GoalDef, PAGEVIEW, SentryEvent, SentryFrame, VERSION, buildEnvelope, buildSentryEvent, deriveChannel, framesFromStack, getCohort, getFirstTouch, hasAttribution, hostOf, isoWeek, parseAttribution, parseDsn, redactSecrets, scrubPII, scrubText };
|
|
243
|
+
export { Attribution, COHORTS, CaptureErrorOptions, Cohort, type CohortDef, Dsn, EVENTS, type ErrorIdentity, type EventName, FUNNELS, type FunnelDef, type FunnelId, type FunnelStep, GOALS, type GoalDef, PAGEVIEW, PRODUCTS, type ProductId, SentryEvent, SentryFrame, VERSION, buildEnvelope, buildSentryEvent, deriveChannel, eventsOf, framesFromStack, getCohort, getFirstTouch, hasAttribution, hostOf, isoWeek, parseAttribution, parseDsn, redactSecrets, scrubPII, scrubText };
|
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(
|
|
@@ -78,6 +78,12 @@ var EVENTS = {
|
|
|
78
78
|
SIGNUP_SUBMITTED: "signup_submitted",
|
|
79
79
|
SIGNUP_VERIFIED: "signup_verified",
|
|
80
80
|
SIGNUP_COMPLETED: "signup_completed",
|
|
81
|
+
/** A RETURNING user authenticated — the non-signup half of the IAM callback.
|
|
82
|
+
* Keeping it distinct is what stops returning logins from inflating signups. */
|
|
83
|
+
LOGIN_COMPLETED: "login_completed",
|
|
84
|
+
/** Activation: the first moment of real value. ONE event for every product —
|
|
85
|
+
* the product-specific moment is the `action` property (api_call, app_live,
|
|
86
|
+
* chat_reply), never a new event name. */
|
|
81
87
|
FIRST_ACTION: "first_action",
|
|
82
88
|
// Waitlist + referral.
|
|
83
89
|
WAITLIST_JOINED: "waitlist_joined",
|
|
@@ -93,13 +99,27 @@ var EVENTS = {
|
|
|
93
99
|
FEATURE_USED: "feature_used",
|
|
94
100
|
API_KEY_CREATED: "api_key_created",
|
|
95
101
|
APP_CREATED: "app_created",
|
|
96
|
-
DEPLOY_STARTED: "deploy_started",
|
|
97
102
|
PROJECT_CREATED: "project_created",
|
|
98
103
|
AGENT_CREATED: "agent_created",
|
|
99
104
|
CHAT_STARTED: "chat_started",
|
|
100
105
|
CHAT_MESSAGE_SENT: "chat_message_sent",
|
|
106
|
+
/** The user switched model/endpoint — the single strongest quality signal a
|
|
107
|
+
* chat surface emits (a switch usually follows a bad answer). */
|
|
108
|
+
MODEL_SWITCHED: "model_switched",
|
|
101
109
|
TASK_STARTED: "task_started",
|
|
102
|
-
TASK_COMPLETED: "task_completed"
|
|
110
|
+
TASK_COMPLETED: "task_completed",
|
|
111
|
+
// Build → ship. `build_*` is a MODEL producing an artifact; `deploy_*` is that
|
|
112
|
+
// artifact going live. Intent (build_started) is never the same event as the
|
|
113
|
+
// artifact existing (app_created) — conflating them makes the funnel lie.
|
|
114
|
+
BUILD_STARTED: "build_started",
|
|
115
|
+
/** A model finished producing an artifact (an app build, a chat reply, an agent
|
|
116
|
+
* run). Carries `durationMs` — the outcome event owns its own duration, so no
|
|
117
|
+
* paired start event is needed. */
|
|
118
|
+
GENERATION_COMPLETED: "generation_completed",
|
|
119
|
+
GENERATION_FAILED: "generation_failed",
|
|
120
|
+
DEPLOY_STARTED: "deploy_started",
|
|
121
|
+
DEPLOY_SUCCEEDED: "deploy_succeeded",
|
|
122
|
+
DEPLOY_FAILED: "deploy_failed"
|
|
103
123
|
};
|
|
104
124
|
var PAGEVIEW = "$pageview";
|
|
105
125
|
|
|
@@ -151,40 +171,40 @@ function luhn(digits) {
|
|
|
151
171
|
}
|
|
152
172
|
return sum % 10 === 0;
|
|
153
173
|
}
|
|
154
|
-
function redactPAN(
|
|
155
|
-
return
|
|
174
|
+
function redactPAN(s2) {
|
|
175
|
+
return s2.replace(RE_PAN, (m) => {
|
|
156
176
|
const digits = m.replace(/[ -]/g, "");
|
|
157
177
|
if (digits.length < 13 || digits.length > 19) return m;
|
|
158
178
|
return luhn(digits) ? REDACTED : m;
|
|
159
179
|
});
|
|
160
180
|
}
|
|
161
|
-
var RE_EMAIL = /[A-Za-z0-9._%+-]
|
|
181
|
+
var RE_EMAIL = /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/g;
|
|
162
182
|
var RE_IPV4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
|
|
163
183
|
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(
|
|
184
|
+
function redactSecrets(s2) {
|
|
185
|
+
for (const re of SECRET_PATTERNS) s2 = s2.replace(re, REDACTED);
|
|
186
|
+
return redactPAN(s2);
|
|
167
187
|
}
|
|
168
|
-
function scrubPII(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return
|
|
188
|
+
function scrubPII(s2) {
|
|
189
|
+
s2 = s2.replace(RE_EMAIL, EMAIL_MARK);
|
|
190
|
+
s2 = s2.replace(RE_IPV6, IP_MARK);
|
|
191
|
+
s2 = s2.replace(RE_IPV4, IP_MARK);
|
|
192
|
+
return s2;
|
|
173
193
|
}
|
|
174
194
|
var MAX_SCRUB_LEN = 8192;
|
|
175
|
-
function truncate(
|
|
176
|
-
return
|
|
195
|
+
function truncate(s2, max = MAX_SCRUB_LEN) {
|
|
196
|
+
return s2.length > max ? s2.slice(0, max) + "\u2026 [truncated]" : s2;
|
|
177
197
|
}
|
|
178
|
-
function scrubText(
|
|
179
|
-
if (!
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (!capturePII)
|
|
183
|
-
return
|
|
198
|
+
function scrubText(s2, capturePII = false) {
|
|
199
|
+
if (!s2) return s2 ?? "";
|
|
200
|
+
s2 = truncate(s2);
|
|
201
|
+
s2 = redactSecrets(s2);
|
|
202
|
+
if (!capturePII) s2 = scrubPII(s2);
|
|
203
|
+
return s2;
|
|
184
204
|
}
|
|
185
205
|
|
|
186
206
|
// src/version.ts
|
|
187
|
-
var VERSION = "0.3.
|
|
207
|
+
var VERSION = "0.3.3";
|
|
188
208
|
|
|
189
209
|
// src/sentry.ts
|
|
190
210
|
var MAX_FRAMES = 50;
|
|
@@ -195,14 +215,14 @@ var MAX_TAGS = 50;
|
|
|
195
215
|
function eventId() {
|
|
196
216
|
const c = typeof crypto !== "undefined" ? crypto : void 0;
|
|
197
217
|
if (c && "randomUUID" in c) return c.randomUUID().replace(/-/g, "");
|
|
198
|
-
let
|
|
199
|
-
for (let i = 0; i < 32; i++)
|
|
200
|
-
return
|
|
218
|
+
let s2 = "";
|
|
219
|
+
for (let i = 0; i < 32; i++) s2 += Math.floor(Math.random() * 16).toString(16);
|
|
220
|
+
return s2;
|
|
201
221
|
}
|
|
202
|
-
function byteLen(
|
|
203
|
-
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(
|
|
204
|
-
if (typeof Buffer !== "undefined") return Buffer.byteLength(
|
|
205
|
-
return
|
|
222
|
+
function byteLen(s2) {
|
|
223
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s2).length;
|
|
224
|
+
if (typeof Buffer !== "undefined") return Buffer.byteLength(s2, "utf8");
|
|
225
|
+
return s2.length;
|
|
206
226
|
}
|
|
207
227
|
function parseDsn(dsn) {
|
|
208
228
|
if (!dsn) return null;
|
|
@@ -272,17 +292,38 @@ function framesFromStack(stack) {
|
|
|
272
292
|
}
|
|
273
293
|
function normalizeError(err) {
|
|
274
294
|
if (err instanceof Error) {
|
|
275
|
-
|
|
295
|
+
const name = read(err, "name");
|
|
296
|
+
const message = read(err, "message");
|
|
297
|
+
const stack = read(err, "stack");
|
|
298
|
+
return {
|
|
299
|
+
name: typeof name === "string" && name ? name : "Error",
|
|
300
|
+
message: typeof message === "string" && message ? message : str(err),
|
|
301
|
+
stack: typeof stack === "string" ? stack : void 0
|
|
302
|
+
};
|
|
276
303
|
}
|
|
277
304
|
if (typeof err === "string") return { name: "Error", message: err };
|
|
278
305
|
try {
|
|
279
|
-
return { name: "Error", message: JSON.stringify(err) };
|
|
306
|
+
return { name: "Error", message: JSON.stringify(err) ?? str(err) };
|
|
307
|
+
} catch {
|
|
308
|
+
return { name: "Error", message: str(err) };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function read(o, k) {
|
|
312
|
+
try {
|
|
313
|
+
return o[k];
|
|
280
314
|
} catch {
|
|
281
|
-
return
|
|
315
|
+
return void 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function str(v) {
|
|
319
|
+
try {
|
|
320
|
+
return String(v);
|
|
321
|
+
} catch {
|
|
322
|
+
return "[unstringifiable]";
|
|
282
323
|
}
|
|
283
324
|
}
|
|
284
325
|
function coerceTag(v) {
|
|
285
|
-
const
|
|
326
|
+
const s2 = typeof v === "string" ? v : (() => {
|
|
286
327
|
try {
|
|
287
328
|
return JSON.stringify(v) ?? String(v);
|
|
288
329
|
} catch {
|
|
@@ -293,7 +334,7 @@ function coerceTag(v) {
|
|
|
293
334
|
}
|
|
294
335
|
}
|
|
295
336
|
})();
|
|
296
|
-
return
|
|
337
|
+
return s2.length > MAX_TAG_LEN ? s2.slice(0, MAX_TAG_LEN) : s2;
|
|
297
338
|
}
|
|
298
339
|
function buildSentryEvent(input) {
|
|
299
340
|
const { error, options = {}, identity, capturePII = false } = input;
|
|
@@ -382,21 +423,21 @@ function uid() {
|
|
|
382
423
|
return "a-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
383
424
|
}
|
|
384
425
|
function anonId() {
|
|
385
|
-
const
|
|
386
|
-
if (!
|
|
387
|
-
let v =
|
|
426
|
+
const s2 = ls();
|
|
427
|
+
if (!s2) return void 0;
|
|
428
|
+
let v = s2.getItem(KEY.anon);
|
|
388
429
|
if (!v) {
|
|
389
430
|
v = uid();
|
|
390
|
-
|
|
431
|
+
s2.setItem(KEY.anon, v);
|
|
391
432
|
}
|
|
392
433
|
return v;
|
|
393
434
|
}
|
|
394
435
|
function sessionId(now = Date.now()) {
|
|
395
|
-
const
|
|
396
|
-
if (!
|
|
436
|
+
const s2 = ls();
|
|
437
|
+
if (!s2) return void 0;
|
|
397
438
|
let state = null;
|
|
398
439
|
try {
|
|
399
|
-
state = JSON.parse(
|
|
440
|
+
state = JSON.parse(s2.getItem(KEY.session) || "null");
|
|
400
441
|
} catch {
|
|
401
442
|
state = null;
|
|
402
443
|
}
|
|
@@ -405,45 +446,45 @@ function sessionId(now = Date.now()) {
|
|
|
405
446
|
} else {
|
|
406
447
|
state.last = now;
|
|
407
448
|
}
|
|
408
|
-
|
|
449
|
+
s2.setItem(KEY.session, JSON.stringify(state));
|
|
409
450
|
return state.id;
|
|
410
451
|
}
|
|
411
452
|
function getFirstTouch() {
|
|
412
|
-
const
|
|
413
|
-
if (!
|
|
453
|
+
const s2 = ls();
|
|
454
|
+
if (!s2) return void 0;
|
|
414
455
|
try {
|
|
415
|
-
const v =
|
|
456
|
+
const v = s2.getItem(KEY.firstTouch);
|
|
416
457
|
return v ? JSON.parse(v) : void 0;
|
|
417
458
|
} catch {
|
|
418
459
|
return void 0;
|
|
419
460
|
}
|
|
420
461
|
}
|
|
421
462
|
function setFirstTouchOnce(a) {
|
|
422
|
-
const
|
|
463
|
+
const s2 = ls();
|
|
423
464
|
const existing = getFirstTouch();
|
|
424
465
|
if (existing) return existing;
|
|
425
|
-
if (
|
|
466
|
+
if (s2) s2.setItem(KEY.firstTouch, JSON.stringify(a));
|
|
426
467
|
return a;
|
|
427
468
|
}
|
|
428
469
|
function getCohort() {
|
|
429
|
-
const
|
|
430
|
-
if (!
|
|
470
|
+
const s2 = ls();
|
|
471
|
+
if (!s2) return void 0;
|
|
431
472
|
try {
|
|
432
|
-
const v =
|
|
473
|
+
const v = s2.getItem(KEY.cohort);
|
|
433
474
|
return v ? JSON.parse(v) : void 0;
|
|
434
475
|
} catch {
|
|
435
476
|
return void 0;
|
|
436
477
|
}
|
|
437
478
|
}
|
|
438
479
|
function mergeCohort(patch) {
|
|
439
|
-
const
|
|
480
|
+
const s2 = ls();
|
|
440
481
|
const cur = getCohort() || {};
|
|
441
482
|
const next = {
|
|
442
483
|
signupWeek: cur.signupWeek || patch.signupWeek,
|
|
443
484
|
channel: patch.channel || cur.channel,
|
|
444
485
|
refCode: cur.refCode || patch.refCode
|
|
445
486
|
};
|
|
446
|
-
if (
|
|
487
|
+
if (s2) s2.setItem(KEY.cohort, JSON.stringify(next));
|
|
447
488
|
return next;
|
|
448
489
|
}
|
|
449
490
|
|
|
@@ -476,15 +517,8 @@ function uid2() {
|
|
|
476
517
|
return "m-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
477
518
|
}
|
|
478
519
|
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
|
-
}
|
|
520
|
+
const n = normalizeError(err);
|
|
521
|
+
return { type: n.name, message: n.message, stack: n.stack };
|
|
488
522
|
}
|
|
489
523
|
var isBrowser = () => typeof window !== "undefined";
|
|
490
524
|
function serializeBatch(batch) {
|
|
@@ -771,30 +805,139 @@ function createAnalytics(config) {
|
|
|
771
805
|
return new Analytics(config);
|
|
772
806
|
}
|
|
773
807
|
|
|
808
|
+
// src/funnels.ts
|
|
809
|
+
var PRODUCTS = ["site", "app", "chat", "console", "admin", "cloud"];
|
|
810
|
+
var s = (event, label, where) => ({
|
|
811
|
+
event,
|
|
812
|
+
label,
|
|
813
|
+
...where ? { where } : {}
|
|
814
|
+
});
|
|
815
|
+
var FUNNELS = {
|
|
816
|
+
/** hanzo.ai: land → sign up. IAM hosts the form, so `signup_submitted` is the
|
|
817
|
+
* redirect INTO IAM and `signup_completed` is the return at /auth/callback. */
|
|
818
|
+
signup: {
|
|
819
|
+
label: "Signup",
|
|
820
|
+
products: ["site"],
|
|
821
|
+
join: "person",
|
|
822
|
+
steps: [
|
|
823
|
+
s(PAGEVIEW, "Landed"),
|
|
824
|
+
s(EVENTS.SIGNUP_VIEWED, "Opened signup"),
|
|
825
|
+
s(EVENTS.SIGNUP_SUBMITTED, "Redirected to Hanzo ID"),
|
|
826
|
+
s(EVENTS.SIGNUP_COMPLETED, "Account created"),
|
|
827
|
+
s(EVENTS.FIRST_ACTION, "First action")
|
|
828
|
+
]
|
|
829
|
+
},
|
|
830
|
+
/** The developer activation path: an account is worth nothing until a key has
|
|
831
|
+
* made a call. `first_action{action:'api_call'}` is emitted SERVER-SIDE by
|
|
832
|
+
* Cloud on an org's first successful /v1 request — a browser cannot see it. */
|
|
833
|
+
apiActivation: {
|
|
834
|
+
label: "API activation",
|
|
835
|
+
products: ["site", "cloud"],
|
|
836
|
+
join: "person",
|
|
837
|
+
steps: [
|
|
838
|
+
s(EVENTS.SIGNUP_COMPLETED, "Account created"),
|
|
839
|
+
s(EVENTS.API_KEY_CREATED, "Key minted"),
|
|
840
|
+
s(EVENTS.FIRST_ACTION, "First successful API call", {
|
|
841
|
+
property: "action",
|
|
842
|
+
equals: "api_call"
|
|
843
|
+
})
|
|
844
|
+
]
|
|
845
|
+
},
|
|
846
|
+
/** Upgrade intent → revenue. `order_completed{kind:'plan'}` is the Sale goal. */
|
|
847
|
+
upgrade: {
|
|
848
|
+
label: "Upgrade",
|
|
849
|
+
products: ["site", "app", "console"],
|
|
850
|
+
join: "person",
|
|
851
|
+
steps: [
|
|
852
|
+
s(EVENTS.PRICING_VIEWED, "Viewed pricing"),
|
|
853
|
+
s(EVENTS.PLAN_CLICKED, "Chose a plan"),
|
|
854
|
+
s(EVENTS.CHECKOUT_STARTED, "Started checkout"),
|
|
855
|
+
s(EVENTS.ORDER_COMPLETED, "Paid")
|
|
856
|
+
]
|
|
857
|
+
},
|
|
858
|
+
/** hanzo.app: describe → build → deploy → live URL. The whole product thesis
|
|
859
|
+
* in five steps; `deploy_succeeded` is the moment a live URL exists. */
|
|
860
|
+
appShip: {
|
|
861
|
+
label: "Describe \u2192 ship",
|
|
862
|
+
products: ["app"],
|
|
863
|
+
join: "person",
|
|
864
|
+
steps: [
|
|
865
|
+
s(PAGEVIEW, "Landed"),
|
|
866
|
+
s(EVENTS.BUILD_STARTED, "Described an app"),
|
|
867
|
+
s(EVENTS.GENERATION_COMPLETED, "Got a working build"),
|
|
868
|
+
s(EVENTS.DEPLOY_STARTED, "Hit publish"),
|
|
869
|
+
s(EVENTS.DEPLOY_SUCCEEDED, "Live URL")
|
|
870
|
+
]
|
|
871
|
+
},
|
|
872
|
+
/** hanzo.chat: visit → first message → answer. `generation_completed` is what
|
|
873
|
+
* separates "typed something" from "got value". */
|
|
874
|
+
chatEngage: {
|
|
875
|
+
label: "Chat engagement",
|
|
876
|
+
products: ["chat"],
|
|
877
|
+
join: "person",
|
|
878
|
+
steps: [
|
|
879
|
+
s(PAGEVIEW, "Landed"),
|
|
880
|
+
s(EVENTS.CHAT_STARTED, "Started a conversation"),
|
|
881
|
+
s(EVENTS.CHAT_MESSAGE_SENT, "Sent a message"),
|
|
882
|
+
s(EVENTS.GENERATION_COMPLETED, "Got an answer")
|
|
883
|
+
]
|
|
884
|
+
},
|
|
885
|
+
/** The cross-surface handoff: the hanzo.ai composer forwards its prompt to
|
|
886
|
+
* hanzo.chat. Two origins, two anonymousIds — so this is an AGGREGATE funnel.
|
|
887
|
+
* The join is the `referrerProduct` property hanzo.chat reads off `?hz_ref=`,
|
|
888
|
+
* which makes the drop-off measurable without any cross-domain identity. */
|
|
889
|
+
siteToChat: {
|
|
890
|
+
label: "Site \u2192 Chat handoff",
|
|
891
|
+
products: ["site", "chat"],
|
|
892
|
+
join: "aggregate",
|
|
893
|
+
steps: [
|
|
894
|
+
s(EVENTS.CHAT_STARTED, "Submitted the hanzo.ai composer", {
|
|
895
|
+
property: "source",
|
|
896
|
+
equals: "composer"
|
|
897
|
+
}),
|
|
898
|
+
s(EVENTS.CHAT_STARTED, "Landed in hanzo.chat", {
|
|
899
|
+
property: "referrerProduct",
|
|
900
|
+
equals: "site"
|
|
901
|
+
}),
|
|
902
|
+
s(EVENTS.GENERATION_COMPLETED, "Got an answer")
|
|
903
|
+
]
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
function eventsOf(id) {
|
|
907
|
+
return FUNNELS[id].steps.map((step) => step.event);
|
|
908
|
+
}
|
|
909
|
+
|
|
774
910
|
// src/goals.ts
|
|
775
911
|
var GOALS = {
|
|
776
|
-
// Signup: the conversion is signup_completed
|
|
912
|
+
// Signup: the conversion is signup_completed, along the site signup funnel.
|
|
777
913
|
signup: {
|
|
778
914
|
label: "Signup",
|
|
779
915
|
event: EVENTS.SIGNUP_COMPLETED,
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
EVENTS.SIGNUP_SUBMITTED,
|
|
783
|
-
EVENTS.SIGNUP_VERIFIED,
|
|
784
|
-
EVENTS.FIRST_ACTION
|
|
785
|
-
]
|
|
916
|
+
funnelId: "signup",
|
|
917
|
+
funnel: eventsOf("signup")
|
|
786
918
|
},
|
|
787
919
|
// Sale: a completed order qualified as a plan purchase (kind=plan).
|
|
788
920
|
sale: {
|
|
789
921
|
label: "Sale",
|
|
790
922
|
event: EVENTS.ORDER_COMPLETED,
|
|
923
|
+
funnelId: "upgrade",
|
|
924
|
+
funnel: eventsOf("upgrade"),
|
|
791
925
|
filter: { property: "kind", equals: "plan" }
|
|
792
926
|
},
|
|
793
927
|
// Upgrade intent: a plan click; pricing_viewed is the top of its funnel.
|
|
794
928
|
upgradeIntent: {
|
|
795
929
|
label: "Upgrade Intent",
|
|
796
930
|
event: EVENTS.PLAN_CLICKED,
|
|
797
|
-
|
|
931
|
+
funnelId: "upgrade",
|
|
932
|
+
funnel: eventsOf("upgrade")
|
|
933
|
+
},
|
|
934
|
+
// Activation: the ONE north-star conversion — an account that did the first
|
|
935
|
+
// valuable thing (a successful API call, a live app, a chat answer).
|
|
936
|
+
activation: {
|
|
937
|
+
label: "Activation",
|
|
938
|
+
event: EVENTS.FIRST_ACTION,
|
|
939
|
+
funnelId: "apiActivation",
|
|
940
|
+
funnel: eventsOf("apiActivation")
|
|
798
941
|
}
|
|
799
942
|
};
|
|
800
943
|
var COHORTS = {
|
|
@@ -803,6 +946,6 @@ var COHORTS = {
|
|
|
803
946
|
refCode: { field: "ref_code", label: "Referral code" }
|
|
804
947
|
};
|
|
805
948
|
|
|
806
|
-
export { Analytics, COHORTS, EVENTS, GOALS, PAGEVIEW, VERSION, buildEnvelope, buildSentryEvent, createAnalytics, deriveChannel, framesFromStack, getCohort, getFirstTouch, hasAttribution, hostOf, isoWeek, parseAttribution, parseDsn, redactSecrets, scrubPII, scrubText };
|
|
949
|
+
export { Analytics, COHORTS, EVENTS, FUNNELS, GOALS, PAGEVIEW, PRODUCTS, VERSION, buildEnvelope, buildSentryEvent, createAnalytics, deriveChannel, eventsOf, framesFromStack, getCohort, getFirstTouch, hasAttribution, hostOf, isoWeek, parseAttribution, parseDsn, redactSecrets, scrubPII, scrubText };
|
|
807
950
|
//# sourceMappingURL=index.mjs.map
|
|
808
951
|
//# sourceMappingURL=index.mjs.map
|