@hanzo/event 0.3.19 → 0.3.21

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.
@@ -18,6 +18,61 @@ interface Exception {
18
18
  * true = a caught error the app chose to report. Defaults true. */
19
19
  handled?: boolean;
20
20
  }
21
+ /** One stack frame as Error Tracking renders it. The key names are the product's
22
+ * POST-symbolication vocabulary (`mangled_name`/`source`/`line`/`column`), which
23
+ * is what the issue view reads straight off `$exception_list`. */
24
+ interface ExceptionFrame {
25
+ /** "<hash>/<part>" — stable per code location; the frame's identity. */
26
+ raw_id: string;
27
+ /** The function name as it appears in the shipped bundle. */
28
+ mangled_name: string;
29
+ /** File/URL the frame is in. */
30
+ source: string;
31
+ line: number;
32
+ column: number;
33
+ /** First-party code. Frames without this are hidden by default in the product. */
34
+ in_app: boolean;
35
+ lang: string;
36
+ /** Whether a symbol set mapped this frame back to original source. */
37
+ resolved: boolean;
38
+ resolve_failure?: string;
39
+ resolved_name?: string | null;
40
+ module?: string | null;
41
+ }
42
+ /** One exception in `$exception_list`. */
43
+ interface ExceptionEntry {
44
+ id: string;
45
+ type: string;
46
+ value: string;
47
+ mechanism?: {
48
+ type: 'generic';
49
+ handled: boolean;
50
+ synthetic?: boolean;
51
+ };
52
+ /** `type` MUST be 'resolved' — the renderer draws frames on no other value. */
53
+ stacktrace?: {
54
+ type: 'resolved';
55
+ frames: ExceptionFrame[];
56
+ };
57
+ }
58
+ /** The `$exception_*` property bag Error Tracking reads off a `$exception` event. */
59
+ interface ExceptionProperties {
60
+ $exception_list: ExceptionEntry[];
61
+ /** Issue grouping key. An event without one is dropped by the issue query. */
62
+ $exception_fingerprint: string;
63
+ $exception_fingerprint_record: {
64
+ type: 'manual';
65
+ }[];
66
+ $exception_type: string;
67
+ $exception_message: string;
68
+ $exception_level: SentryLevel;
69
+ $exception_handled: boolean;
70
+ $exception_synthetic: boolean;
71
+ $exception_types: string[];
72
+ $exception_values: string[];
73
+ $exception_sources: string[];
74
+ $exception_functions: string[];
75
+ }
21
76
  /** First-touch marketing attribution, parsed once and persisted. */
22
77
  interface Attribution {
23
78
  utm: {
@@ -308,4 +363,4 @@ declare class Analytics {
308
363
  /** createAnalytics builds a client instance. Most apps use one shared instance. */
309
364
  declare function createAnalytics(config: AnalyticsConfig): Analytics;
310
365
 
311
- export { type Attribution as A, type Cohort as C, type Dsn as D, type EventKind as E, type SentryEvent as S, type Transport as T, type WireEvent as W, type CaptureErrorOptions as a, type SentryFrame as b, Analytics as c, type AnalyticsConfig as d, type Exception as e, type SentryLevel as f, createAnalytics as g };
366
+ export { type Attribution as A, type Cohort as C, type Dsn as D, type ExceptionEntry as E, type SentryEvent as S, type Transport as T, type WireEvent as W, type CaptureErrorOptions as a, type SentryFrame as b, type SentryLevel as c, type ExceptionProperties as d, Analytics as e, type AnalyticsConfig as f, type EventKind as g, type Exception as h, type ExceptionFrame as i, createAnalytics as j };
@@ -18,6 +18,61 @@ interface Exception {
18
18
  * true = a caught error the app chose to report. Defaults true. */
19
19
  handled?: boolean;
20
20
  }
21
+ /** One stack frame as Error Tracking renders it. The key names are the product's
22
+ * POST-symbolication vocabulary (`mangled_name`/`source`/`line`/`column`), which
23
+ * is what the issue view reads straight off `$exception_list`. */
24
+ interface ExceptionFrame {
25
+ /** "<hash>/<part>" — stable per code location; the frame's identity. */
26
+ raw_id: string;
27
+ /** The function name as it appears in the shipped bundle. */
28
+ mangled_name: string;
29
+ /** File/URL the frame is in. */
30
+ source: string;
31
+ line: number;
32
+ column: number;
33
+ /** First-party code. Frames without this are hidden by default in the product. */
34
+ in_app: boolean;
35
+ lang: string;
36
+ /** Whether a symbol set mapped this frame back to original source. */
37
+ resolved: boolean;
38
+ resolve_failure?: string;
39
+ resolved_name?: string | null;
40
+ module?: string | null;
41
+ }
42
+ /** One exception in `$exception_list`. */
43
+ interface ExceptionEntry {
44
+ id: string;
45
+ type: string;
46
+ value: string;
47
+ mechanism?: {
48
+ type: 'generic';
49
+ handled: boolean;
50
+ synthetic?: boolean;
51
+ };
52
+ /** `type` MUST be 'resolved' — the renderer draws frames on no other value. */
53
+ stacktrace?: {
54
+ type: 'resolved';
55
+ frames: ExceptionFrame[];
56
+ };
57
+ }
58
+ /** The `$exception_*` property bag Error Tracking reads off a `$exception` event. */
59
+ interface ExceptionProperties {
60
+ $exception_list: ExceptionEntry[];
61
+ /** Issue grouping key. An event without one is dropped by the issue query. */
62
+ $exception_fingerprint: string;
63
+ $exception_fingerprint_record: {
64
+ type: 'manual';
65
+ }[];
66
+ $exception_type: string;
67
+ $exception_message: string;
68
+ $exception_level: SentryLevel;
69
+ $exception_handled: boolean;
70
+ $exception_synthetic: boolean;
71
+ $exception_types: string[];
72
+ $exception_values: string[];
73
+ $exception_sources: string[];
74
+ $exception_functions: string[];
75
+ }
21
76
  /** First-touch marketing attribution, parsed once and persisted. */
22
77
  interface Attribution {
23
78
  utm: {
@@ -308,4 +363,4 @@ declare class Analytics {
308
363
  /** createAnalytics builds a client instance. Most apps use one shared instance. */
309
364
  declare function createAnalytics(config: AnalyticsConfig): Analytics;
310
365
 
311
- export { type Attribution as A, type Cohort as C, type Dsn as D, type EventKind as E, type SentryEvent as S, type Transport as T, type WireEvent as W, type CaptureErrorOptions as a, type SentryFrame as b, Analytics as c, type AnalyticsConfig as d, type Exception as e, type SentryLevel as f, createAnalytics as g };
366
+ export { type Attribution as A, type Cohort as C, type Dsn as D, type ExceptionEntry as E, type SentryEvent as S, type Transport as T, type WireEvent as W, type CaptureErrorOptions as a, type SentryFrame as b, type SentryLevel as c, type ExceptionProperties as d, Analytics as e, type AnalyticsConfig as f, type EventKind as g, type Exception as h, type ExceptionFrame as i, createAnalytics as j };
package/dist/index.cjs CHANGED
@@ -82,15 +82,11 @@ var PRODUCT_PROJECT = Object.freeze({
82
82
  site: "019f9b1e-5785-7359-ad0b-f75db8e58c99"
83
83
  // hanzo.ai (marketing; product `site`)
84
84
  });
85
- function dsnForProduct(product) {
86
- if (!product) return void 0;
85
+ function dsnForProduct(product, key) {
86
+ if (!product || !key) return void 0;
87
87
  const projectId = PRODUCT_PROJECT[product];
88
88
  if (!projectId) return void 0;
89
- return `https://${HANZO_PUBLISHABLE_KEY}@api.hanzo.ai/v1/sentry/${projectId}`;
90
- }
91
- var HANZO_PUBLISHABLE_KEY = "pk-live-c88649f1085fb6ad441d8a0072933a9b";
92
- function defaultPublishableKey(host) {
93
- return host === void 0 || host === "https://api.hanzo.ai" ? HANZO_PUBLISHABLE_KEY : void 0;
89
+ return `https://${key}@api.hanzo.ai/v1/sentry/${projectId}`;
94
90
  }
95
91
 
96
92
  // src/events.ts
@@ -144,6 +140,7 @@ var EVENTS = {
144
140
  DEPLOY_FAILED: "deploy_failed"
145
141
  };
146
142
  var PAGEVIEW = "$pageview";
143
+ var EXCEPTION = "$exception";
147
144
 
148
145
  // src/scrub.ts
149
146
  var REDACTED = "[redacted]";
@@ -373,7 +370,7 @@ function uuidv7Time(id) {
373
370
  }
374
371
 
375
372
  // src/version.ts
376
- var VERSION = "0.3.19";
373
+ var VERSION = "0.3.21";
377
374
 
378
375
  // src/sentry.ts
379
376
  var MAX_FRAMES = 50;
@@ -565,6 +562,94 @@ ${itemHeader}
565
562
  ${payload}
566
563
  `;
567
564
  }
565
+ var MAX_VALUE = 4096;
566
+ function digest(s2) {
567
+ let out = "";
568
+ for (const seed of [2166136261, 16777619, 2654435769, 2246822507]) {
569
+ let h = seed >>> 0;
570
+ for (let i = 0; i < s2.length; i++) {
571
+ h ^= s2.charCodeAt(i);
572
+ h = h + (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24) >>> 0;
573
+ }
574
+ out += h.toString(16).padStart(8, "0");
575
+ }
576
+ return out;
577
+ }
578
+ function frameOf(f) {
579
+ const source = f.filename ?? "";
580
+ const name = f.function ?? "<anonymous>";
581
+ const line = f.lineno ?? 0;
582
+ const column = f.colno ?? 0;
583
+ return {
584
+ // Stable per code location, so the same frame groups and re-renders under one
585
+ // key. The '/part' suffix is the product's format for one raw frame expanding
586
+ // into several resolved ones; a browser frame is always part 0.
587
+ raw_id: `${digest(`${source}|${name}|${line}|${column}`)}/0`,
588
+ mangled_name: name,
589
+ source,
590
+ line,
591
+ column,
592
+ in_app: f.in_app ?? false,
593
+ lang: "javascript",
594
+ resolved: false,
595
+ resolve_failure: "no symbol set uploaded for this release",
596
+ resolved_name: null,
597
+ module: null
598
+ };
599
+ }
600
+ function truncate2(s2, max) {
601
+ if (s2.length <= max) return s2;
602
+ let end = max;
603
+ const c = s2.charCodeAt(end - 1);
604
+ if (c >= 55296 && c <= 56319) end--;
605
+ return s2.slice(0, end);
606
+ }
607
+ function exceptionEntry(err, opts) {
608
+ const n = normalizeError(err);
609
+ const frames = framesFromStack(n.stack).slice(-50);
610
+ const entry = {
611
+ id: opts.id,
612
+ type: n.name,
613
+ value: truncate2(n.message, MAX_VALUE),
614
+ mechanism: {
615
+ type: "generic",
616
+ handled: opts.handled,
617
+ // An exception the app reported by hand was constructed, not thrown by the
618
+ // runtime at a real call site.
619
+ synthetic: !(err instanceof Error)
620
+ }
621
+ };
622
+ if (frames.length > 0) {
623
+ entry.stacktrace = { type: "resolved", frames: frames.map(frameOf) };
624
+ }
625
+ return entry;
626
+ }
627
+ function fingerprint(entry) {
628
+ const frames = entry.stacktrace?.frames ?? [];
629
+ const pieces = frames.filter((f) => f.in_app).map((f) => `${f.mangled_name}@${f.source}`);
630
+ return digest([entry.type, ...pieces].join("\n"));
631
+ }
632
+ function exceptionProperties(err, opts) {
633
+ const entry = exceptionEntry(err, opts);
634
+ const frames = entry.stacktrace?.frames ?? [];
635
+ const fp = fingerprint(entry);
636
+ return {
637
+ $exception_list: [entry],
638
+ $exception_fingerprint: fp,
639
+ // Records WHY this fingerprint holds. 'manual' is the honest value: the client
640
+ // supplied the key rather than a server grouper deriving one.
641
+ $exception_fingerprint_record: [{ type: "manual" }],
642
+ $exception_type: entry.type,
643
+ $exception_message: entry.value,
644
+ $exception_level: opts.level ?? "error",
645
+ $exception_handled: opts.handled,
646
+ $exception_synthetic: entry.mechanism?.synthetic ?? false,
647
+ $exception_types: [entry.type],
648
+ $exception_values: [entry.value],
649
+ $exception_sources: frames.map((f) => f.source).filter((s2) => !!s2),
650
+ $exception_functions: frames.map((f) => f.resolved_name ?? f.mangled_name).filter((s2) => !!s2)
651
+ };
652
+ }
568
653
 
569
654
  // src/storage.ts
570
655
  var KEY = {
@@ -736,27 +821,24 @@ var Analytics = class {
736
821
  enabled: true,
737
822
  captureErrors: true,
738
823
  ...config,
739
- // The publishable key resolves the SAME way the DSN below does, and now
740
- // ends the SAME way too — in a baked default, so declaring nothing is
741
- // enough. Most specific first:
824
+ // The publishable key resolves from ONE live source, never a literal baked
825
+ // beside the code. Most specific first:
742
826
  // 1. an explicit `ingestKey` in config;
743
827
  // 2. NEXT_PUBLIC_PUBLISHABLE_KEY, the inlined build-time env the fleet
744
- // carries end to end (KMS `deploy/PUBLISHABLE_KEY` -> the PUBLISHABLE_KEY
745
- // build-arg -> the NEXT_PUBLIC_ prefix Next inlines);
746
- // 3. the hanzo org key baked in `dsn.ts`, on the hanzo cloud only.
828
+ // carries end to end KMS `deploy/PUBLISHABLE_KEY` -> the PUBLISHABLE_KEY
829
+ // build-arg -> the NEXT_PUBLIC_ prefix Next inlines.
747
830
  //
748
- // (3) is why this is `publishable_key for all`: a hanzo surface that passes
749
- // no key in code and inlines no env still emits attributed, exactly as
750
- // declaring `product` is enough for the DSN. Without it that surface sent
751
- // its beacons to `$public` which drops every track/identify/group and
752
- // still answers 200 silent in the page and invisible until you read the
753
- // warehouse and find the host missing entirely. A white-label surface on
754
- // its own cloud (a non-default host) gets no default, so the org this
755
- // attributes to is never the wrong one.
756
- ingestKey: config.ingestKey ?? readEnv("NEXT_PUBLIC_PUBLISHABLE_KEY") ?? defaultPublishableKey(config.host ?? DEFAULT_HOST)
831
+ // A surface that provides neither has no key, and its anonymous traffic
832
+ // files under `$public` (which drops track/identify/group and answers 200);
833
+ // the fix is to give it the KMS-sourced env, not to hardcode the org key
834
+ // here. The key is a credential-class value: its home is KMS, and the ONE
835
+ // build reads it from there.
836
+ ingestKey: config.ingestKey ?? readEnv("NEXT_PUBLIC_PUBLISHABLE_KEY")
757
837
  };
758
838
  this.transport = config.transport ?? new DefaultTransport();
759
- this.dsn = parseDsn(config.dsn ?? readEnvDsn() ?? dsnForProduct(this.cfg.product));
839
+ this.dsn = parseDsn(
840
+ config.dsn ?? readEnvDsn() ?? dsnForProduct(this.cfg.product, this.cfg.ingestKey)
841
+ );
760
842
  }
761
843
  /** errorPlaneEnabled reports whether captured exceptions can actually reach the
762
844
  * error host. False means a DSN was never configured — the documented
@@ -841,9 +923,20 @@ var Analytics = class {
841
923
  } catch {
842
924
  }
843
925
  try {
926
+ const handled = context?.handled ?? true;
844
927
  const ex = normalizeError2(err);
845
- ex.handled = context?.handled ?? true;
846
- this.enqueue("error", ex.message, { error: ex, properties: context?.properties });
928
+ ex.handled = handled;
929
+ this.enqueue("event", EXCEPTION, {
930
+ error: ex,
931
+ properties: {
932
+ ...context?.properties,
933
+ ...exceptionProperties(err, {
934
+ handled,
935
+ id: hzUuidv7(),
936
+ level: context?.level
937
+ })
938
+ }
939
+ });
847
940
  this.flush();
848
941
  } catch {
849
942
  }
@@ -1153,6 +1246,7 @@ var COHORTS = {
1153
1246
  exports.Analytics = Analytics;
1154
1247
  exports.COHORTS = COHORTS;
1155
1248
  exports.EVENTS = EVENTS;
1249
+ exports.EXCEPTION = EXCEPTION;
1156
1250
  exports.FUNNELS = FUNNELS;
1157
1251
  exports.GOALS = GOALS;
1158
1252
  exports.PAGEVIEW = PAGEVIEW;
@@ -1163,8 +1257,12 @@ exports.buildEnvelope = buildEnvelope;
1163
1257
  exports.buildSentryEvent = buildSentryEvent;
1164
1258
  exports.createAnalytics = createAnalytics;
1165
1259
  exports.deriveChannel = deriveChannel;
1260
+ exports.digest = digest;
1166
1261
  exports.dsnForProduct = dsnForProduct;
1167
1262
  exports.eventsOf = eventsOf;
1263
+ exports.exceptionEntry = exceptionEntry;
1264
+ exports.exceptionProperties = exceptionProperties;
1265
+ exports.fingerprint = fingerprint;
1168
1266
  exports.framesFromStack = framesFromStack;
1169
1267
  exports.getCohort = getCohort;
1170
1268
  exports.getFirstTouch = getFirstTouch;