@hanzo/event 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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.2";
9
+ declare const VERSION = "0.3.3";
10
10
 
11
11
  /**
12
12
  * parseDsn parses "https://<version>:<hmac>@<host>/v1/sentry/<projectId>" into its
@@ -60,6 +60,39 @@ declare function buildSentryEvent(input: BuildEventInput): SentryEvent;
60
60
  */
61
61
  declare function buildEnvelope(event: SentryEvent, dsn: Dsn, sentAt?: string): string;
62
62
 
63
+ /**
64
+ * The product → Sentry DSN registry.
65
+ *
66
+ * An app declares WHAT it is (`product: 'console'`); this module knows WHERE its
67
+ * errors go. That split is the whole point: no surface has to learn a DSN, carry
68
+ * a build argument, or grow a config file to report errors — declaring the
69
+ * product it already declares is enough.
70
+ *
71
+ * A Sentry DSN is PUBLIC by construction. It ships inside the client bundle and
72
+ * is readable in devtools on any deployed page, and it grants exactly one
73
+ * capability: submitting new events. It cannot read issues, projects, or any
74
+ * other data. So committing it is not leaking a secret — it is recording a public
75
+ * identifier next to the code that needs it. (Contrast the server-side collector
76
+ * DSN in the `team-analytics-sentry` Secret, which is HMAC-derived and revocable
77
+ * precisely because a server-side credential is NOT public.)
78
+ *
79
+ * Why a literal map instead of deriving `hanzo-${product}`: the projects predate
80
+ * this registry and do not derive cleanly — `site` lives in `hanzo-ai`, not
81
+ * `hanzo-site`. An explicit map is honest about that; a derivation rule plus an
82
+ * exception table is the same data with a trap in it.
83
+ *
84
+ * Projects are org-scoped and named `<org>-<app>`. To add one: create the project
85
+ * (POST /v1/sentry/projects with X-Org-Id), then add its `dsn` here keyed by the
86
+ * product name the app passes to `createAnalytics`.
87
+ */
88
+ /** PRODUCT_DSN maps a `product` to the DSN its errors are submitted to. */
89
+ declare const PRODUCT_DSN: Readonly<Record<string, string>>;
90
+ /** dsnForProduct resolves the registered DSN for a product, or undefined when the
91
+ * product has no project yet — which leaves the error plane inert rather than
92
+ * guessing a destination and silently posting a surface's errors into the wrong
93
+ * project. */
94
+ declare function dsnForProduct(product: string | undefined): string | undefined;
95
+
63
96
  /** redactSecrets removes known secret shapes. Always applied. */
64
97
  declare function redactSecrets(s: string): string;
65
98
  /** scrubPII masks emails and IPs. Applied unless PII capture is enabled. */
@@ -73,6 +106,12 @@ declare const EVENTS: {
73
106
  readonly SIGNUP_SUBMITTED: "signup_submitted";
74
107
  readonly SIGNUP_VERIFIED: "signup_verified";
75
108
  readonly SIGNUP_COMPLETED: "signup_completed";
109
+ /** A RETURNING user authenticated — the non-signup half of the IAM callback.
110
+ * Keeping it distinct is what stops returning logins from inflating signups. */
111
+ readonly LOGIN_COMPLETED: "login_completed";
112
+ /** Activation: the first moment of real value. ONE event for every product —
113
+ * the product-specific moment is the `action` property (api_call, app_live,
114
+ * chat_reply), never a new event name. */
76
115
  readonly FIRST_ACTION: "first_action";
77
116
  readonly WAITLIST_JOINED: "waitlist_joined";
78
117
  readonly WAITLIST_SHARED: "waitlist_shared";
@@ -85,24 +124,127 @@ declare const EVENTS: {
85
124
  readonly FEATURE_USED: "feature_used";
86
125
  readonly API_KEY_CREATED: "api_key_created";
87
126
  readonly APP_CREATED: "app_created";
88
- readonly DEPLOY_STARTED: "deploy_started";
89
127
  readonly PROJECT_CREATED: "project_created";
90
128
  readonly AGENT_CREATED: "agent_created";
91
129
  readonly CHAT_STARTED: "chat_started";
92
130
  readonly CHAT_MESSAGE_SENT: "chat_message_sent";
131
+ /** The user switched model/endpoint — the single strongest quality signal a
132
+ * chat surface emits (a switch usually follows a bad answer). */
133
+ readonly MODEL_SWITCHED: "model_switched";
93
134
  readonly TASK_STARTED: "task_started";
94
135
  readonly TASK_COMPLETED: "task_completed";
136
+ readonly BUILD_STARTED: "build_started";
137
+ /** A model finished producing an artifact (an app build, a chat reply, an agent
138
+ * run). Carries `durationMs` — the outcome event owns its own duration, so no
139
+ * paired start event is needed. */
140
+ readonly GENERATION_COMPLETED: "generation_completed";
141
+ readonly GENERATION_FAILED: "generation_failed";
142
+ readonly DEPLOY_STARTED: "deploy_started";
143
+ readonly DEPLOY_SUCCEEDED: "deploy_succeeded";
144
+ readonly DEPLOY_FAILED: "deploy_failed";
95
145
  };
96
146
  type EventName = (typeof EVENTS)[keyof typeof EVENTS];
97
147
  /** The reserved event name a pageview is stored under (server + read lens). */
98
148
  declare const PAGEVIEW = "$pageview";
99
149
 
150
+ /** The emitting surfaces — the closed set of `AnalyticsConfig.product` values.
151
+ * `product` is on every event, so a funnel scopes by product instead of every
152
+ * surface prefixing its event names. */
153
+ declare const PRODUCTS: readonly ["site", "app", "chat", "console", "admin", "cloud"];
154
+ type ProductId = (typeof PRODUCTS)[number];
155
+ interface FunnelStep {
156
+ /** An EVENTS value (or PAGEVIEW). */
157
+ event: string;
158
+ /** Human label for the Insights step. */
159
+ label: string;
160
+ /** Property equality that qualifies the step, e.g. first_action{action:'api_call'}. */
161
+ where?: {
162
+ property: string;
163
+ equals: string;
164
+ };
165
+ }
166
+ interface FunnelDef {
167
+ label: string;
168
+ /** Surface(s) the steps are emitted from — matched against the `product` field. */
169
+ products: ProductId[];
170
+ /**
171
+ * How steps are joined:
172
+ * • 'person' — steps join on distinctId (one browser, or one logged-in
173
+ * person across surfaces). The normal case.
174
+ * • 'aggregate' — steps are emitted on DIFFERENT origins by a LOGGED-OUT
175
+ * visitor, so there is no shared id: hanzo.ai, hanzo.app and
176
+ * hanzo.chat each mint their own anonymousId in their own
177
+ * storage. Read these as step-over-step COUNTS, never as a
178
+ * per-person conversion. Honest by construction.
179
+ */
180
+ join: 'person' | 'aggregate';
181
+ steps: FunnelStep[];
182
+ }
183
+ declare const FUNNELS: {
184
+ /** hanzo.ai: land → sign up. IAM hosts the form, so `signup_submitted` is the
185
+ * redirect INTO IAM and `signup_completed` is the return at /auth/callback. */
186
+ readonly signup: {
187
+ readonly label: "Signup";
188
+ readonly products: ["site"];
189
+ readonly join: "person";
190
+ readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep, FunnelStep];
191
+ };
192
+ /** The developer activation path: an account is worth nothing until a key has
193
+ * made a call. `first_action{action:'api_call'}` is emitted SERVER-SIDE by
194
+ * Cloud on an org's first successful /v1 request — a browser cannot see it. */
195
+ readonly apiActivation: {
196
+ readonly label: "API activation";
197
+ readonly products: ["site", "cloud"];
198
+ readonly join: "person";
199
+ readonly steps: [FunnelStep, FunnelStep, FunnelStep];
200
+ };
201
+ /** Upgrade intent → revenue. `order_completed{kind:'plan'}` is the Sale goal. */
202
+ readonly upgrade: {
203
+ readonly label: "Upgrade";
204
+ readonly products: ["site", "app", "console"];
205
+ readonly join: "person";
206
+ readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep];
207
+ };
208
+ /** hanzo.app: describe → build → deploy → live URL. The whole product thesis
209
+ * in five steps; `deploy_succeeded` is the moment a live URL exists. */
210
+ readonly appShip: {
211
+ readonly label: "Describe → ship";
212
+ readonly products: ["app"];
213
+ readonly join: "person";
214
+ readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep, FunnelStep];
215
+ };
216
+ /** hanzo.chat: visit → first message → answer. `generation_completed` is what
217
+ * separates "typed something" from "got value". */
218
+ readonly chatEngage: {
219
+ readonly label: "Chat engagement";
220
+ readonly products: ["chat"];
221
+ readonly join: "person";
222
+ readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep];
223
+ };
224
+ /** The cross-surface handoff: the hanzo.ai composer forwards its prompt to
225
+ * hanzo.chat. Two origins, two anonymousIds — so this is an AGGREGATE funnel.
226
+ * The join is the `referrerProduct` property hanzo.chat reads off `?hz_ref=`,
227
+ * which makes the drop-off measurable without any cross-domain identity. */
228
+ readonly siteToChat: {
229
+ readonly label: "Site → Chat handoff";
230
+ readonly products: ["site", "chat"];
231
+ readonly join: "aggregate";
232
+ readonly steps: [FunnelStep, FunnelStep, FunnelStep];
233
+ };
234
+ };
235
+ type FunnelId = keyof typeof FUNNELS;
236
+ /** eventsOf flattens a funnel to its ordered event names — what a goal's `funnel`
237
+ * field carries, so the steps are defined exactly once (here). */
238
+ declare function eventsOf(id: FunnelId): string[];
239
+
100
240
  interface GoalDef {
101
241
  /** Human label shown in Insights. */
102
242
  label: string;
103
243
  /** The event whose occurrence counts as the goal conversion. */
104
244
  event: string;
105
- /** Optional ordered funnel leading to the goal (for funnel insights). */
245
+ /** The funnel leading to the goal — an id into FUNNELS (see funnels.ts). */
246
+ funnelId?: FunnelId;
247
+ /** The ordered event names of `funnelId`, derived — never hand-written. */
106
248
  funnel?: string[];
107
249
  /** Optional property equality filter that qualifies the conversion. */
108
250
  filter?: {
@@ -110,7 +252,7 @@ interface GoalDef {
110
252
  equals: string;
111
253
  };
112
254
  }
113
- declare const GOALS: Record<'signup' | 'sale' | 'upgradeIntent', GoalDef>;
255
+ declare const GOALS: Record<'signup' | 'sale' | 'upgradeIntent' | 'activation', GoalDef>;
114
256
  interface CohortDef {
115
257
  /** The hanzo.events column the cohort dimension maps to. */
116
258
  field: string;
@@ -131,4 +273,4 @@ declare function hasAttribution(a: Attribution): boolean;
131
273
  /** isoWeek returns the ISO-8601 week label, e.g. "2026-W28". */
132
274
  declare function isoWeek(d: Date): string;
133
275
 
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 };
276
+ 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, PRODUCT_DSN, type ProductId, SentryEvent, SentryFrame, VERSION, buildEnvelope, buildSentryEvent, deriveChannel, dsnForProduct, eventsOf, framesFromStack, getCohort, getFirstTouch, hasAttribution, hostOf, isoWeek, parseAttribution, parseDsn, redactSecrets, scrubPII, scrubText };