@hanzo/event 0.3.22 → 0.3.23
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/LICENSE.md +21 -0
- package/dist/index.cjs +80 -173
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -93
- package/dist/index.d.ts +4 -93
- package/dist/index.mjs +67 -169
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/hz.js +1 -1
- package/package.json +11 -11
- package/src/funnels.ts +14 -154
- package/src/goals.test.ts +33 -0
- package/src/version.ts +1 -1
- package/src/funnels.test.ts +0 -92
package/dist/index.d.cts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { C as Cohort, A as Attribution, S as SentryEvent, D as Dsn, a as CaptureErrorOptions, b as SentryFrame, E as ExceptionEntry, c as SentryLevel, d as ExceptionProperties } from './core-CHLpJy51.cjs';
|
|
2
2
|
export { e as Analytics, f as AnalyticsConfig, g as EventKind, h as Exception, i as ExceptionFrame, T as Transport, W as WireEvent, j as createAnalytics } from './core-CHLpJy51.cjs';
|
|
3
|
-
|
|
3
|
+
import { FunnelId } from '@hanzo/events';
|
|
4
|
+
export { EVENTS, EXCEPTION, EventName, FUNNELS, FunnelDef, FunnelId, FunnelStep, PAGEVIEW, PRODUCTS, ProductId, eventsOf } from '@hanzo/events';
|
|
4
5
|
|
|
5
6
|
/** Read the persisted first-touch attribution. */
|
|
6
7
|
declare function getFirstTouch(): Attribution | undefined;
|
|
7
8
|
/** Read persisted cohort dimensions. */
|
|
8
9
|
declare function getCohort(): Cohort | undefined;
|
|
9
10
|
|
|
10
|
-
declare const VERSION = "0.3.
|
|
11
|
+
declare const VERSION = "0.3.23";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* parseDsn parses "https://<version>:<hmac>@<host>/v1/sentry/<projectId>" into its
|
|
@@ -178,96 +179,6 @@ declare function exceptionProperties(err: unknown, opts: {
|
|
|
178
179
|
level?: SentryLevel;
|
|
179
180
|
}): ExceptionProperties;
|
|
180
181
|
|
|
181
|
-
/** The emitting surfaces — the closed set of `AnalyticsConfig.product` values.
|
|
182
|
-
* `product` is on every event, so a funnel scopes by product instead of every
|
|
183
|
-
* surface prefixing its event names. */
|
|
184
|
-
declare const PRODUCTS: readonly ["site", "app", "chat", "console", "admin", "cloud"];
|
|
185
|
-
type ProductId = (typeof PRODUCTS)[number];
|
|
186
|
-
interface FunnelStep {
|
|
187
|
-
/** An EVENTS value (or PAGEVIEW). */
|
|
188
|
-
event: string;
|
|
189
|
-
/** Human label for the Insights step. */
|
|
190
|
-
label: string;
|
|
191
|
-
/** Property equality that qualifies the step, e.g. first_action{action:'api_call'}. */
|
|
192
|
-
where?: {
|
|
193
|
-
property: string;
|
|
194
|
-
equals: string;
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
interface FunnelDef {
|
|
198
|
-
label: string;
|
|
199
|
-
/** Surface(s) the steps are emitted from — matched against the `product` field. */
|
|
200
|
-
products: ProductId[];
|
|
201
|
-
/**
|
|
202
|
-
* How steps are joined:
|
|
203
|
-
* • 'person' — steps join on distinctId (one browser, or one logged-in
|
|
204
|
-
* person across surfaces). The normal case.
|
|
205
|
-
* • 'aggregate' — steps are emitted on DIFFERENT origins by a LOGGED-OUT
|
|
206
|
-
* visitor, so there is no shared id: hanzo.ai, hanzo.app and
|
|
207
|
-
* hanzo.chat each mint their own anonymousId in their own
|
|
208
|
-
* storage. Read these as step-over-step COUNTS, never as a
|
|
209
|
-
* per-person conversion. Honest by construction.
|
|
210
|
-
*/
|
|
211
|
-
join: 'person' | 'aggregate';
|
|
212
|
-
steps: FunnelStep[];
|
|
213
|
-
}
|
|
214
|
-
declare const FUNNELS: {
|
|
215
|
-
/** hanzo.ai: land → sign up. IAM hosts the form, so `signup_submitted` is the
|
|
216
|
-
* redirect INTO IAM and `signup_completed` is the return at /auth/callback. */
|
|
217
|
-
readonly signup: {
|
|
218
|
-
readonly label: "Signup";
|
|
219
|
-
readonly products: ["site"];
|
|
220
|
-
readonly join: "person";
|
|
221
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
222
|
-
};
|
|
223
|
-
/** The developer activation path: an account is worth nothing until a key has
|
|
224
|
-
* made a call. `first_action{action:'api_call'}` is emitted SERVER-SIDE by
|
|
225
|
-
* Cloud on an org's first successful /v1 request — a browser cannot see it. */
|
|
226
|
-
readonly apiActivation: {
|
|
227
|
-
readonly label: "API activation";
|
|
228
|
-
readonly products: ["site", "cloud"];
|
|
229
|
-
readonly join: "person";
|
|
230
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep];
|
|
231
|
-
};
|
|
232
|
-
/** Upgrade intent → revenue. `order_completed{kind:'plan'}` is the Sale goal. */
|
|
233
|
-
readonly upgrade: {
|
|
234
|
-
readonly label: "Upgrade";
|
|
235
|
-
readonly products: ["site", "app", "console"];
|
|
236
|
-
readonly join: "person";
|
|
237
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
238
|
-
};
|
|
239
|
-
/** hanzo.app: describe → build → deploy → live URL. The whole product thesis
|
|
240
|
-
* in five steps; `deploy_succeeded` is the moment a live URL exists. */
|
|
241
|
-
readonly appShip: {
|
|
242
|
-
readonly label: "Describe → ship";
|
|
243
|
-
readonly products: ["app"];
|
|
244
|
-
readonly join: "person";
|
|
245
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
246
|
-
};
|
|
247
|
-
/** hanzo.chat: visit → first message → answer. `generation_completed` is what
|
|
248
|
-
* separates "typed something" from "got value". */
|
|
249
|
-
readonly chatEngage: {
|
|
250
|
-
readonly label: "Chat engagement";
|
|
251
|
-
readonly products: ["chat"];
|
|
252
|
-
readonly join: "person";
|
|
253
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
254
|
-
};
|
|
255
|
-
/** The cross-surface handoff: the hanzo.ai composer forwards its prompt to
|
|
256
|
-
* hanzo.chat. Two origins, two anonymousIds — so this is an AGGREGATE funnel.
|
|
257
|
-
* The join is the `referrerProduct` property hanzo.chat reads off `?hz_ref=`,
|
|
258
|
-
* which makes the drop-off measurable without any cross-domain identity. */
|
|
259
|
-
readonly siteToChat: {
|
|
260
|
-
readonly label: "Site → Chat handoff";
|
|
261
|
-
readonly products: ["site", "chat"];
|
|
262
|
-
readonly join: "aggregate";
|
|
263
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep];
|
|
264
|
-
};
|
|
265
|
-
};
|
|
266
|
-
type FunnelId = keyof typeof FUNNELS;
|
|
267
|
-
/** eventsOf flattens a funnel to its ordered event names — what a goal's `funnel`
|
|
268
|
-
* field carries, so the steps are defined exactly once (here). */
|
|
269
|
-
declare function eventsOf(id: FunnelId): string[];
|
|
270
|
-
|
|
271
182
|
interface GoalDef {
|
|
272
183
|
/** Human label shown in Insights. */
|
|
273
184
|
label: string;
|
|
@@ -304,4 +215,4 @@ declare function hasAttribution(a: Attribution): boolean;
|
|
|
304
215
|
/** isoWeek returns the ISO-8601 week label, e.g. "2026-W28". */
|
|
305
216
|
declare function isoWeek(d: Date): string;
|
|
306
217
|
|
|
307
|
-
export { Attribution, COHORTS, CaptureErrorOptions, Cohort, type CohortDef, Dsn, type ErrorIdentity, ExceptionEntry, ExceptionProperties,
|
|
218
|
+
export { Attribution, COHORTS, CaptureErrorOptions, Cohort, type CohortDef, Dsn, type ErrorIdentity, ExceptionEntry, ExceptionProperties, GOALS, type GoalDef, PRODUCT_PROJECT, SentryEvent, SentryFrame, SentryLevel, VERSION, buildEnvelope, buildSentryEvent, deriveChannel, digest, dsnForProduct, exceptionEntry, exceptionProperties, fingerprint, framesFromStack, getCohort, getFirstTouch, hasAttribution, hostOf, isoWeek, parseAttribution, parseDsn, redactSecrets, scrubPII, scrubText, hzUuidv7 as uuidv7, uuidv7Time };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { C as Cohort, A as Attribution, S as SentryEvent, D as Dsn, a as CaptureErrorOptions, b as SentryFrame, E as ExceptionEntry, c as SentryLevel, d as ExceptionProperties } from './core-CHLpJy51.js';
|
|
2
2
|
export { e as Analytics, f as AnalyticsConfig, g as EventKind, h as Exception, i as ExceptionFrame, T as Transport, W as WireEvent, j as createAnalytics } from './core-CHLpJy51.js';
|
|
3
|
-
|
|
3
|
+
import { FunnelId } from '@hanzo/events';
|
|
4
|
+
export { EVENTS, EXCEPTION, EventName, FUNNELS, FunnelDef, FunnelId, FunnelStep, PAGEVIEW, PRODUCTS, ProductId, eventsOf } from '@hanzo/events';
|
|
4
5
|
|
|
5
6
|
/** Read the persisted first-touch attribution. */
|
|
6
7
|
declare function getFirstTouch(): Attribution | undefined;
|
|
7
8
|
/** Read persisted cohort dimensions. */
|
|
8
9
|
declare function getCohort(): Cohort | undefined;
|
|
9
10
|
|
|
10
|
-
declare const VERSION = "0.3.
|
|
11
|
+
declare const VERSION = "0.3.23";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* parseDsn parses "https://<version>:<hmac>@<host>/v1/sentry/<projectId>" into its
|
|
@@ -178,96 +179,6 @@ declare function exceptionProperties(err: unknown, opts: {
|
|
|
178
179
|
level?: SentryLevel;
|
|
179
180
|
}): ExceptionProperties;
|
|
180
181
|
|
|
181
|
-
/** The emitting surfaces — the closed set of `AnalyticsConfig.product` values.
|
|
182
|
-
* `product` is on every event, so a funnel scopes by product instead of every
|
|
183
|
-
* surface prefixing its event names. */
|
|
184
|
-
declare const PRODUCTS: readonly ["site", "app", "chat", "console", "admin", "cloud"];
|
|
185
|
-
type ProductId = (typeof PRODUCTS)[number];
|
|
186
|
-
interface FunnelStep {
|
|
187
|
-
/** An EVENTS value (or PAGEVIEW). */
|
|
188
|
-
event: string;
|
|
189
|
-
/** Human label for the Insights step. */
|
|
190
|
-
label: string;
|
|
191
|
-
/** Property equality that qualifies the step, e.g. first_action{action:'api_call'}. */
|
|
192
|
-
where?: {
|
|
193
|
-
property: string;
|
|
194
|
-
equals: string;
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
interface FunnelDef {
|
|
198
|
-
label: string;
|
|
199
|
-
/** Surface(s) the steps are emitted from — matched against the `product` field. */
|
|
200
|
-
products: ProductId[];
|
|
201
|
-
/**
|
|
202
|
-
* How steps are joined:
|
|
203
|
-
* • 'person' — steps join on distinctId (one browser, or one logged-in
|
|
204
|
-
* person across surfaces). The normal case.
|
|
205
|
-
* • 'aggregate' — steps are emitted on DIFFERENT origins by a LOGGED-OUT
|
|
206
|
-
* visitor, so there is no shared id: hanzo.ai, hanzo.app and
|
|
207
|
-
* hanzo.chat each mint their own anonymousId in their own
|
|
208
|
-
* storage. Read these as step-over-step COUNTS, never as a
|
|
209
|
-
* per-person conversion. Honest by construction.
|
|
210
|
-
*/
|
|
211
|
-
join: 'person' | 'aggregate';
|
|
212
|
-
steps: FunnelStep[];
|
|
213
|
-
}
|
|
214
|
-
declare const FUNNELS: {
|
|
215
|
-
/** hanzo.ai: land → sign up. IAM hosts the form, so `signup_submitted` is the
|
|
216
|
-
* redirect INTO IAM and `signup_completed` is the return at /auth/callback. */
|
|
217
|
-
readonly signup: {
|
|
218
|
-
readonly label: "Signup";
|
|
219
|
-
readonly products: ["site"];
|
|
220
|
-
readonly join: "person";
|
|
221
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
222
|
-
};
|
|
223
|
-
/** The developer activation path: an account is worth nothing until a key has
|
|
224
|
-
* made a call. `first_action{action:'api_call'}` is emitted SERVER-SIDE by
|
|
225
|
-
* Cloud on an org's first successful /v1 request — a browser cannot see it. */
|
|
226
|
-
readonly apiActivation: {
|
|
227
|
-
readonly label: "API activation";
|
|
228
|
-
readonly products: ["site", "cloud"];
|
|
229
|
-
readonly join: "person";
|
|
230
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep];
|
|
231
|
-
};
|
|
232
|
-
/** Upgrade intent → revenue. `order_completed{kind:'plan'}` is the Sale goal. */
|
|
233
|
-
readonly upgrade: {
|
|
234
|
-
readonly label: "Upgrade";
|
|
235
|
-
readonly products: ["site", "app", "console"];
|
|
236
|
-
readonly join: "person";
|
|
237
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
238
|
-
};
|
|
239
|
-
/** hanzo.app: describe → build → deploy → live URL. The whole product thesis
|
|
240
|
-
* in five steps; `deploy_succeeded` is the moment a live URL exists. */
|
|
241
|
-
readonly appShip: {
|
|
242
|
-
readonly label: "Describe → ship";
|
|
243
|
-
readonly products: ["app"];
|
|
244
|
-
readonly join: "person";
|
|
245
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
246
|
-
};
|
|
247
|
-
/** hanzo.chat: visit → first message → answer. `generation_completed` is what
|
|
248
|
-
* separates "typed something" from "got value". */
|
|
249
|
-
readonly chatEngage: {
|
|
250
|
-
readonly label: "Chat engagement";
|
|
251
|
-
readonly products: ["chat"];
|
|
252
|
-
readonly join: "person";
|
|
253
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep, FunnelStep];
|
|
254
|
-
};
|
|
255
|
-
/** The cross-surface handoff: the hanzo.ai composer forwards its prompt to
|
|
256
|
-
* hanzo.chat. Two origins, two anonymousIds — so this is an AGGREGATE funnel.
|
|
257
|
-
* The join is the `referrerProduct` property hanzo.chat reads off `?hz_ref=`,
|
|
258
|
-
* which makes the drop-off measurable without any cross-domain identity. */
|
|
259
|
-
readonly siteToChat: {
|
|
260
|
-
readonly label: "Site → Chat handoff";
|
|
261
|
-
readonly products: ["site", "chat"];
|
|
262
|
-
readonly join: "aggregate";
|
|
263
|
-
readonly steps: [FunnelStep, FunnelStep, FunnelStep];
|
|
264
|
-
};
|
|
265
|
-
};
|
|
266
|
-
type FunnelId = keyof typeof FUNNELS;
|
|
267
|
-
/** eventsOf flattens a funnel to its ordered event names — what a goal's `funnel`
|
|
268
|
-
* field carries, so the steps are defined exactly once (here). */
|
|
269
|
-
declare function eventsOf(id: FunnelId): string[];
|
|
270
|
-
|
|
271
182
|
interface GoalDef {
|
|
272
183
|
/** Human label shown in Insights. */
|
|
273
184
|
label: string;
|
|
@@ -304,4 +215,4 @@ declare function hasAttribution(a: Attribution): boolean;
|
|
|
304
215
|
/** isoWeek returns the ISO-8601 week label, e.g. "2026-W28". */
|
|
305
216
|
declare function isoWeek(d: Date): string;
|
|
306
217
|
|
|
307
|
-
export { Attribution, COHORTS, CaptureErrorOptions, Cohort, type CohortDef, Dsn, type ErrorIdentity, ExceptionEntry, ExceptionProperties,
|
|
218
|
+
export { Attribution, COHORTS, CaptureErrorOptions, Cohort, type CohortDef, Dsn, type ErrorIdentity, ExceptionEntry, ExceptionProperties, GOALS, type GoalDef, PRODUCT_PROJECT, SentryEvent, SentryFrame, SentryLevel, VERSION, buildEnvelope, buildSentryEvent, deriveChannel, digest, dsnForProduct, exceptionEntry, exceptionProperties, fingerprint, framesFromStack, getCohort, getFirstTouch, hasAttribution, hostOf, isoWeek, parseAttribution, parseDsn, redactSecrets, scrubPII, scrubText, hzUuidv7 as uuidv7, uuidv7Time };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EVENTS, PAGEVIEW, EXCEPTION } from '@hanzo/events';
|
|
2
|
-
export { EVENTS, EXCEPTION, PAGEVIEW } from '@hanzo/events';
|
|
1
|
+
import { eventsOf, EVENTS, PAGEVIEW, EXCEPTION } from '@hanzo/events';
|
|
2
|
+
export { EVENTS, EXCEPTION, FUNNELS, PAGEVIEW, PRODUCTS, eventsOf } from '@hanzo/events';
|
|
3
3
|
|
|
4
4
|
// src/attribution.ts
|
|
5
5
|
var SOCIAL_HOSTS = [
|
|
@@ -49,16 +49,16 @@ function deriveChannel(a) {
|
|
|
49
49
|
}
|
|
50
50
|
function hostOf(raw) {
|
|
51
51
|
if (!raw) return "";
|
|
52
|
-
let
|
|
53
|
-
const scheme =
|
|
54
|
-
if (scheme >= 0)
|
|
55
|
-
const cut =
|
|
56
|
-
if (cut >= 0)
|
|
57
|
-
const at =
|
|
58
|
-
if (at >= 0)
|
|
59
|
-
const colon =
|
|
60
|
-
if (colon >= 0)
|
|
61
|
-
return
|
|
52
|
+
let s = raw.trim();
|
|
53
|
+
const scheme = s.indexOf("://");
|
|
54
|
+
if (scheme >= 0) s = s.slice(scheme + 3);
|
|
55
|
+
const cut = s.search(/[/?#]/);
|
|
56
|
+
if (cut >= 0) s = s.slice(0, cut);
|
|
57
|
+
const at = s.indexOf("@");
|
|
58
|
+
if (at >= 0) s = s.slice(at + 1);
|
|
59
|
+
const colon = s.indexOf(":");
|
|
60
|
+
if (colon >= 0) s = s.slice(0, colon);
|
|
61
|
+
return s.toLowerCase().trim();
|
|
62
62
|
}
|
|
63
63
|
function hasAttribution(a) {
|
|
64
64
|
return Boolean(
|
|
@@ -138,8 +138,8 @@ function luhn(digits) {
|
|
|
138
138
|
}
|
|
139
139
|
return sum % 10 === 0;
|
|
140
140
|
}
|
|
141
|
-
function redactPAN(
|
|
142
|
-
return
|
|
141
|
+
function redactPAN(s) {
|
|
142
|
+
return s.replace(RE_PAN, (m) => {
|
|
143
143
|
const digits = m.replace(/[ -]/g, "");
|
|
144
144
|
if (digits.length < 13 || digits.length > 19) return m;
|
|
145
145
|
return luhn(digits) ? REDACTED : m;
|
|
@@ -184,30 +184,30 @@ var RE_CREDENTIAL_PARAM = new RegExp(
|
|
|
184
184
|
"([?&#;](?:" + CREDENTIAL_PARAMS.join("|") + ")=)[^&#;\\s]{1,4096}",
|
|
185
185
|
"gi"
|
|
186
186
|
);
|
|
187
|
-
function redactCredentialParams(
|
|
188
|
-
return
|
|
187
|
+
function redactCredentialParams(s) {
|
|
188
|
+
return s.replace(RE_CREDENTIAL_PARAM, (_m, prefix) => prefix + REDACTED);
|
|
189
189
|
}
|
|
190
|
-
function redactSecrets(
|
|
191
|
-
|
|
192
|
-
for (const re of SECRET_PATTERNS)
|
|
193
|
-
return redactPAN(
|
|
190
|
+
function redactSecrets(s) {
|
|
191
|
+
s = redactCredentialParams(s);
|
|
192
|
+
for (const re of SECRET_PATTERNS) s = s.replace(re, REDACTED);
|
|
193
|
+
return redactPAN(s);
|
|
194
194
|
}
|
|
195
|
-
function scrubPII(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return
|
|
195
|
+
function scrubPII(s) {
|
|
196
|
+
s = s.replace(RE_EMAIL, EMAIL_MARK);
|
|
197
|
+
s = s.replace(RE_IPV6, IP_MARK);
|
|
198
|
+
s = s.replace(RE_IPV4, IP_MARK);
|
|
199
|
+
return s;
|
|
200
200
|
}
|
|
201
201
|
var MAX_SCRUB_LEN = 8192;
|
|
202
|
-
function truncate(
|
|
203
|
-
return
|
|
202
|
+
function truncate(s, max = MAX_SCRUB_LEN) {
|
|
203
|
+
return s.length > max ? s.slice(0, max) + "\u2026 [truncated]" : s;
|
|
204
204
|
}
|
|
205
|
-
function scrubText(
|
|
206
|
-
if (!
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (!capturePII)
|
|
210
|
-
return
|
|
205
|
+
function scrubText(s, capturePII = false) {
|
|
206
|
+
if (!s) return s ?? "";
|
|
207
|
+
s = truncate(s);
|
|
208
|
+
s = redactSecrets(s);
|
|
209
|
+
if (!capturePII) s = scrubPII(s);
|
|
210
|
+
return s;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
// src/anon.js
|
|
@@ -301,12 +301,12 @@ function hzAnonWrite(name, value) {
|
|
|
301
301
|
}
|
|
302
302
|
function hzAnonId() {
|
|
303
303
|
if (typeof window === "undefined") return "";
|
|
304
|
-
var
|
|
305
|
-
var id = hzAnonCookie(HZ_ANON_KEY) || hzAnonItem(
|
|
304
|
+
var s = hzAnonStore();
|
|
305
|
+
var id = hzAnonCookie(HZ_ANON_KEY) || hzAnonItem(s, HZ_ANON_KEY) || hzAnonItem(s, HZ_ANON_LEGACY_KEY) || hzAnonMemo || hzUuidv7();
|
|
306
306
|
hzAnonMemo = id;
|
|
307
307
|
hzAnonWrite(HZ_ANON_KEY, id);
|
|
308
308
|
try {
|
|
309
|
-
if (
|
|
309
|
+
if (s && s.getItem(HZ_ANON_KEY) !== id) s.setItem(HZ_ANON_KEY, id);
|
|
310
310
|
} catch (e) {
|
|
311
311
|
}
|
|
312
312
|
return id;
|
|
@@ -318,7 +318,7 @@ function uuidv7Time(id) {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
// src/version.ts
|
|
321
|
-
var VERSION = "0.3.
|
|
321
|
+
var VERSION = "0.3.23";
|
|
322
322
|
|
|
323
323
|
// src/sentry.ts
|
|
324
324
|
var MAX_FRAMES = 50;
|
|
@@ -329,10 +329,10 @@ var MAX_TAGS = 50;
|
|
|
329
329
|
function eventId() {
|
|
330
330
|
return hzUuidv7().replace(/-/g, "");
|
|
331
331
|
}
|
|
332
|
-
function byteLen(
|
|
333
|
-
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(
|
|
334
|
-
if (typeof Buffer !== "undefined") return Buffer.byteLength(
|
|
335
|
-
return
|
|
332
|
+
function byteLen(s) {
|
|
333
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s).length;
|
|
334
|
+
if (typeof Buffer !== "undefined") return Buffer.byteLength(s, "utf8");
|
|
335
|
+
return s.length;
|
|
336
336
|
}
|
|
337
337
|
function parseDsn(dsn) {
|
|
338
338
|
if (!dsn) return null;
|
|
@@ -433,7 +433,7 @@ function str(v) {
|
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
function coerceTag(v) {
|
|
436
|
-
const
|
|
436
|
+
const s = typeof v === "string" ? v : (() => {
|
|
437
437
|
try {
|
|
438
438
|
return JSON.stringify(v) ?? String(v);
|
|
439
439
|
} catch {
|
|
@@ -444,7 +444,7 @@ function coerceTag(v) {
|
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
})();
|
|
447
|
-
return
|
|
447
|
+
return s.length > MAX_TAG_LEN ? s.slice(0, MAX_TAG_LEN) : s;
|
|
448
448
|
}
|
|
449
449
|
function buildSentryEvent(input) {
|
|
450
450
|
const { error, options = {}, identity, capturePII = false } = input;
|
|
@@ -511,12 +511,12 @@ ${payload}
|
|
|
511
511
|
`;
|
|
512
512
|
}
|
|
513
513
|
var MAX_VALUE = 4096;
|
|
514
|
-
function digest(
|
|
514
|
+
function digest(s) {
|
|
515
515
|
let out = "";
|
|
516
516
|
for (const seed of [2166136261, 16777619, 2654435769, 2246822507]) {
|
|
517
517
|
let h = seed >>> 0;
|
|
518
|
-
for (let i = 0; i <
|
|
519
|
-
h ^=
|
|
518
|
+
for (let i = 0; i < s.length; i++) {
|
|
519
|
+
h ^= s.charCodeAt(i);
|
|
520
520
|
h = h + (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24) >>> 0;
|
|
521
521
|
}
|
|
522
522
|
out += h.toString(16).padStart(8, "0");
|
|
@@ -545,12 +545,12 @@ function frameOf(f) {
|
|
|
545
545
|
module: null
|
|
546
546
|
};
|
|
547
547
|
}
|
|
548
|
-
function truncate2(
|
|
549
|
-
if (
|
|
548
|
+
function truncate2(s, max) {
|
|
549
|
+
if (s.length <= max) return s;
|
|
550
550
|
let end = max;
|
|
551
|
-
const c =
|
|
551
|
+
const c = s.charCodeAt(end - 1);
|
|
552
552
|
if (c >= 55296 && c <= 56319) end--;
|
|
553
|
-
return
|
|
553
|
+
return s.slice(0, end);
|
|
554
554
|
}
|
|
555
555
|
function exceptionEntry(err, opts) {
|
|
556
556
|
const n = normalizeError(err);
|
|
@@ -594,8 +594,8 @@ function exceptionProperties(err, opts) {
|
|
|
594
594
|
$exception_synthetic: entry.mechanism?.synthetic ?? false,
|
|
595
595
|
$exception_types: [entry.type],
|
|
596
596
|
$exception_values: [entry.value],
|
|
597
|
-
$exception_sources: frames.map((f) => f.source).filter((
|
|
598
|
-
$exception_functions: frames.map((f) => f.resolved_name ?? f.mangled_name).filter((
|
|
597
|
+
$exception_sources: frames.map((f) => f.source).filter((s) => !!s),
|
|
598
|
+
$exception_functions: frames.map((f) => f.resolved_name ?? f.mangled_name).filter((s) => !!s)
|
|
599
599
|
};
|
|
600
600
|
}
|
|
601
601
|
|
|
@@ -618,11 +618,11 @@ function anonId() {
|
|
|
618
618
|
return hzAnonId() || void 0;
|
|
619
619
|
}
|
|
620
620
|
function sessionId(now = Date.now()) {
|
|
621
|
-
const
|
|
622
|
-
if (!
|
|
621
|
+
const s = ls();
|
|
622
|
+
if (!s) return void 0;
|
|
623
623
|
let state = null;
|
|
624
624
|
try {
|
|
625
|
-
state = JSON.parse(
|
|
625
|
+
state = JSON.parse(s.getItem(KEY.session) || "null");
|
|
626
626
|
} catch {
|
|
627
627
|
state = null;
|
|
628
628
|
}
|
|
@@ -631,45 +631,45 @@ function sessionId(now = Date.now()) {
|
|
|
631
631
|
} else {
|
|
632
632
|
state.last = now;
|
|
633
633
|
}
|
|
634
|
-
|
|
634
|
+
s.setItem(KEY.session, JSON.stringify(state));
|
|
635
635
|
return state.id;
|
|
636
636
|
}
|
|
637
637
|
function getFirstTouch() {
|
|
638
|
-
const
|
|
639
|
-
if (!
|
|
638
|
+
const s = ls();
|
|
639
|
+
if (!s) return void 0;
|
|
640
640
|
try {
|
|
641
|
-
const v =
|
|
641
|
+
const v = s.getItem(KEY.firstTouch);
|
|
642
642
|
return v ? JSON.parse(v) : void 0;
|
|
643
643
|
} catch {
|
|
644
644
|
return void 0;
|
|
645
645
|
}
|
|
646
646
|
}
|
|
647
647
|
function setFirstTouchOnce(a) {
|
|
648
|
-
const
|
|
648
|
+
const s = ls();
|
|
649
649
|
const existing = getFirstTouch();
|
|
650
650
|
if (existing) return existing;
|
|
651
|
-
if (
|
|
651
|
+
if (s) s.setItem(KEY.firstTouch, JSON.stringify(a));
|
|
652
652
|
return a;
|
|
653
653
|
}
|
|
654
654
|
function getCohort() {
|
|
655
|
-
const
|
|
656
|
-
if (!
|
|
655
|
+
const s = ls();
|
|
656
|
+
if (!s) return void 0;
|
|
657
657
|
try {
|
|
658
|
-
const v =
|
|
658
|
+
const v = s.getItem(KEY.cohort);
|
|
659
659
|
return v ? JSON.parse(v) : void 0;
|
|
660
660
|
} catch {
|
|
661
661
|
return void 0;
|
|
662
662
|
}
|
|
663
663
|
}
|
|
664
664
|
function mergeCohort(patch) {
|
|
665
|
-
const
|
|
665
|
+
const s = ls();
|
|
666
666
|
const cur = getCohort() || {};
|
|
667
667
|
const next = {
|
|
668
668
|
signupWeek: cur.signupWeek || patch.signupWeek,
|
|
669
669
|
channel: patch.channel || cur.channel,
|
|
670
670
|
refCode: cur.refCode || patch.refCode
|
|
671
671
|
};
|
|
672
|
-
if (
|
|
672
|
+
if (s) s.setItem(KEY.cohort, JSON.stringify(next));
|
|
673
673
|
return next;
|
|
674
674
|
}
|
|
675
675
|
|
|
@@ -1029,108 +1029,6 @@ function createAnalytics(config) {
|
|
|
1029
1029
|
return new Analytics(config);
|
|
1030
1030
|
}
|
|
1031
1031
|
|
|
1032
|
-
// src/funnels.ts
|
|
1033
|
-
var PRODUCTS = ["site", "app", "chat", "console", "admin", "cloud"];
|
|
1034
|
-
var s = (event, label, where) => ({
|
|
1035
|
-
event,
|
|
1036
|
-
label,
|
|
1037
|
-
...where ? { where } : {}
|
|
1038
|
-
});
|
|
1039
|
-
var FUNNELS = {
|
|
1040
|
-
/** hanzo.ai: land → sign up. IAM hosts the form, so `signup_submitted` is the
|
|
1041
|
-
* redirect INTO IAM and `signup_completed` is the return at /auth/callback. */
|
|
1042
|
-
signup: {
|
|
1043
|
-
label: "Signup",
|
|
1044
|
-
products: ["site"],
|
|
1045
|
-
join: "person",
|
|
1046
|
-
steps: [
|
|
1047
|
-
s(PAGEVIEW, "Landed"),
|
|
1048
|
-
s(EVENTS.SIGNUP_VIEWED, "Opened signup"),
|
|
1049
|
-
s(EVENTS.SIGNUP_SUBMITTED, "Redirected to Hanzo ID"),
|
|
1050
|
-
s(EVENTS.SIGNUP_COMPLETED, "Account created"),
|
|
1051
|
-
s(EVENTS.FIRST_ACTION, "First action")
|
|
1052
|
-
]
|
|
1053
|
-
},
|
|
1054
|
-
/** The developer activation path: an account is worth nothing until a key has
|
|
1055
|
-
* made a call. `first_action{action:'api_call'}` is emitted SERVER-SIDE by
|
|
1056
|
-
* Cloud on an org's first successful /v1 request — a browser cannot see it. */
|
|
1057
|
-
apiActivation: {
|
|
1058
|
-
label: "API activation",
|
|
1059
|
-
products: ["site", "cloud"],
|
|
1060
|
-
join: "person",
|
|
1061
|
-
steps: [
|
|
1062
|
-
s(EVENTS.SIGNUP_COMPLETED, "Account created"),
|
|
1063
|
-
s(EVENTS.API_KEY_CREATED, "Key minted"),
|
|
1064
|
-
s(EVENTS.FIRST_ACTION, "First successful API call", {
|
|
1065
|
-
property: "action",
|
|
1066
|
-
equals: "api_call"
|
|
1067
|
-
})
|
|
1068
|
-
]
|
|
1069
|
-
},
|
|
1070
|
-
/** Upgrade intent → revenue. `order_completed{kind:'plan'}` is the Sale goal. */
|
|
1071
|
-
upgrade: {
|
|
1072
|
-
label: "Upgrade",
|
|
1073
|
-
products: ["site", "app", "console"],
|
|
1074
|
-
join: "person",
|
|
1075
|
-
steps: [
|
|
1076
|
-
s(EVENTS.PRICING_VIEWED, "Viewed pricing"),
|
|
1077
|
-
s(EVENTS.PLAN_CLICKED, "Chose a plan"),
|
|
1078
|
-
s(EVENTS.CHECKOUT_STARTED, "Started checkout"),
|
|
1079
|
-
s(EVENTS.ORDER_COMPLETED, "Paid")
|
|
1080
|
-
]
|
|
1081
|
-
},
|
|
1082
|
-
/** hanzo.app: describe → build → deploy → live URL. The whole product thesis
|
|
1083
|
-
* in five steps; `deploy_succeeded` is the moment a live URL exists. */
|
|
1084
|
-
appShip: {
|
|
1085
|
-
label: "Describe \u2192 ship",
|
|
1086
|
-
products: ["app"],
|
|
1087
|
-
join: "person",
|
|
1088
|
-
steps: [
|
|
1089
|
-
s(PAGEVIEW, "Landed"),
|
|
1090
|
-
s(EVENTS.BUILD_STARTED, "Described an app"),
|
|
1091
|
-
s(EVENTS.GENERATION_COMPLETED, "Got a working build"),
|
|
1092
|
-
s(EVENTS.DEPLOY_STARTED, "Hit publish"),
|
|
1093
|
-
s(EVENTS.DEPLOY_SUCCEEDED, "Live URL")
|
|
1094
|
-
]
|
|
1095
|
-
},
|
|
1096
|
-
/** hanzo.chat: visit → first message → answer. `generation_completed` is what
|
|
1097
|
-
* separates "typed something" from "got value". */
|
|
1098
|
-
chatEngage: {
|
|
1099
|
-
label: "Chat engagement",
|
|
1100
|
-
products: ["chat"],
|
|
1101
|
-
join: "person",
|
|
1102
|
-
steps: [
|
|
1103
|
-
s(PAGEVIEW, "Landed"),
|
|
1104
|
-
s(EVENTS.CHAT_STARTED, "Started a conversation"),
|
|
1105
|
-
s(EVENTS.CHAT_MESSAGE_SENT, "Sent a message"),
|
|
1106
|
-
s(EVENTS.GENERATION_COMPLETED, "Got an answer")
|
|
1107
|
-
]
|
|
1108
|
-
},
|
|
1109
|
-
/** The cross-surface handoff: the hanzo.ai composer forwards its prompt to
|
|
1110
|
-
* hanzo.chat. Two origins, two anonymousIds — so this is an AGGREGATE funnel.
|
|
1111
|
-
* The join is the `referrerProduct` property hanzo.chat reads off `?hz_ref=`,
|
|
1112
|
-
* which makes the drop-off measurable without any cross-domain identity. */
|
|
1113
|
-
siteToChat: {
|
|
1114
|
-
label: "Site \u2192 Chat handoff",
|
|
1115
|
-
products: ["site", "chat"],
|
|
1116
|
-
join: "aggregate",
|
|
1117
|
-
steps: [
|
|
1118
|
-
s(EVENTS.CHAT_STARTED, "Submitted the hanzo.ai composer", {
|
|
1119
|
-
property: "source",
|
|
1120
|
-
equals: "composer"
|
|
1121
|
-
}),
|
|
1122
|
-
s(EVENTS.CHAT_STARTED, "Landed in hanzo.chat", {
|
|
1123
|
-
property: "referrerProduct",
|
|
1124
|
-
equals: "site"
|
|
1125
|
-
}),
|
|
1126
|
-
s(EVENTS.GENERATION_COMPLETED, "Got an answer")
|
|
1127
|
-
]
|
|
1128
|
-
}
|
|
1129
|
-
};
|
|
1130
|
-
function eventsOf(id) {
|
|
1131
|
-
return FUNNELS[id].steps.map((step) => step.event);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
1032
|
// src/goals.ts
|
|
1135
1033
|
var GOALS = {
|
|
1136
1034
|
// Signup: the conversion is signup_completed, along the site signup funnel.
|
|
@@ -1191,6 +1089,6 @@ var COHORTS = {
|
|
|
1191
1089
|
* reformat of one copy is a diff against the other.
|
|
1192
1090
|
*/
|
|
1193
1091
|
|
|
1194
|
-
export { Analytics, COHORTS,
|
|
1092
|
+
export { Analytics, COHORTS, GOALS, PRODUCT_PROJECT, VERSION, buildEnvelope, buildSentryEvent, createAnalytics, deriveChannel, digest, dsnForProduct, exceptionEntry, exceptionProperties, fingerprint, framesFromStack, getCohort, getFirstTouch, hasAttribution, hostOf, isoWeek, parseAttribution, parseDsn, redactSecrets, scrubPII, scrubText, hzUuidv7 as uuidv7, uuidv7Time };
|
|
1195
1093
|
//# sourceMappingURL=index.mjs.map
|
|
1196
1094
|
//# sourceMappingURL=index.mjs.map
|