@hanzo/event 0.3.32 → 0.3.34

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/src/stack.ts ADDED
@@ -0,0 +1,144 @@
1
+ // Pure error parsing: coerce an unknown throwable into {name, message, stack},
2
+ // and parse a browser stack string into structured frames. No I/O, no globals —
3
+ // core.ts wires these to identity and transport.
4
+ //
5
+ // These frames ride the error Event to POST /v1/event so the warehouse stores
6
+ // WHERE a crash happened, not just that one did. Nothing here talks to Sentry:
7
+ // the client has one door and one credential. The frame shape is deliberately the
8
+ // conventional one (function/filename/abs_path/lineno/colno/in_app) because it is
9
+ // what every stack tool already speaks — including a future grouper built over
10
+ // the warehouse.
11
+
12
+ /** Max frames kept — well under the server's cap, plenty to identify a crash. */
13
+ const MAX_FRAMES = 50
14
+ /** Max stack lines examined, and max length of a line worth examining. Guards the
15
+ * frame regexes against a hostile `stack` string (see framesFromStack). */
16
+ const MAX_LINES = 500
17
+ const MAX_LINE_LEN = 2048
18
+
19
+ /** One parsed stack frame. */
20
+ export interface Frame {
21
+ filename?: string
22
+ function?: string
23
+ abs_path?: string
24
+ lineno?: number
25
+ colno?: number
26
+ /** The app's own code, as opposed to vendor/runtime — the useful default filter. */
27
+ in_app?: boolean
28
+ }
29
+
30
+ const V8_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+)|([^)]+))\)?\s*$/
31
+ const MOZ_FRAME = /^\s*(?:(.*?)@)?(.+?):(\d+):(\d+)\s*$/
32
+
33
+ /** inApp marks a frame as the app's own code (vs vendor/runtime). */
34
+ function inApp(file: string): boolean {
35
+ if (!file) return false
36
+ return !(
37
+ file.includes('node_modules') ||
38
+ file.startsWith('webpack-internal') ||
39
+ file.startsWith('webpack://') ||
40
+ file.startsWith('chrome-extension://') ||
41
+ file.startsWith('moz-extension://')
42
+ )
43
+ }
44
+
45
+ /**
46
+ * framesFromStack parses a browser Error.stack into frames, OLDEST-FIRST (caller
47
+ * → callee, so the crash site is LAST). Handles both V8 ("at fn (file:li:co)")
48
+ * and Firefox/Safari ("fn@file:li:co"). Unparseable lines are skipped.
49
+ */
50
+ export function framesFromStack(stack: string | undefined): Frame[] {
51
+ if (!stack) return []
52
+ // Both frame regexes use lazy nested quantifiers, which backtrack badly on a
53
+ // long line that never matches. A stack is attacker-influenced (a thrown value
54
+ // can carry any `stack` string), so bound the work: skip absurd lines and stop
55
+ // after MAX_LINES. Only the innermost MAX_FRAMES are kept anyway.
56
+ const lines = stack.split('\n', MAX_LINES)
57
+ const frames: Frame[] = []
58
+ for (const raw of lines) {
59
+ if (raw.length > MAX_LINE_LEN) continue
60
+ const line = raw.trimEnd()
61
+ if (!line) continue
62
+ // Header lines like "TypeError: x is not a function" match neither frame
63
+ // regex (no "at " prefix, no trailing :line:col) and are skipped naturally.
64
+ let fn: string | undefined
65
+ let file = ''
66
+ let lineno = 0
67
+ let colno = 0
68
+ const v = V8_FRAME.exec(line)
69
+ if (v) {
70
+ fn = v[1]
71
+ if (v[2]) {
72
+ file = v[2]
73
+ lineno = Number(v[3]) || 0
74
+ colno = Number(v[4]) || 0
75
+ } else {
76
+ file = (v[5] || '').trim()
77
+ }
78
+ } else {
79
+ const f = MOZ_FRAME.exec(line)
80
+ if (!f) continue
81
+ fn = f[1]
82
+ file = f[2]
83
+ lineno = Number(f[3]) || 0
84
+ colno = Number(f[4]) || 0
85
+ }
86
+ if (!file && !fn) continue
87
+ frames.push({
88
+ function: fn || '<anonymous>',
89
+ filename: file,
90
+ abs_path: file,
91
+ lineno,
92
+ colno,
93
+ in_app: inApp(file),
94
+ })
95
+ }
96
+ // Reverse to oldest-first and cap to the innermost MAX_FRAMES.
97
+ frames.reverse()
98
+ if (frames.length > MAX_FRAMES) return frames.slice(frames.length - MAX_FRAMES)
99
+ return frames
100
+ }
101
+
102
+ /** read pulls a property off a value that may be hostile — `name`, `message` and
103
+ * `stack` are ordinary getters that a thrown object is free to define as
104
+ * throwing. The thrown value is the least trustworthy input this library
105
+ * handles; losing the whole report to one of them is not acceptable. */
106
+ function read(o: unknown, k: string): unknown {
107
+ try {
108
+ return (o as Record<string, unknown>)[k]
109
+ } catch {
110
+ return undefined
111
+ }
112
+ }
113
+
114
+ /** str coerces to a string without letting a throwing toString/Symbol.toPrimitive
115
+ * escape. */
116
+ function str(v: unknown): string {
117
+ try {
118
+ return String(v)
119
+ } catch {
120
+ return '[unstringifiable]'
121
+ }
122
+ }
123
+
124
+ /** normalizeError coerces an unknown throwable into {name, message, stack}.
125
+ * TOTAL: it returns a usable record for ANY input, including an object
126
+ * engineered to throw on property access. */
127
+ export function normalizeError(err: unknown): { name: string; message: string; stack?: string } {
128
+ if (err instanceof Error) {
129
+ const name = read(err, 'name')
130
+ const message = read(err, 'message')
131
+ const stack = read(err, 'stack')
132
+ return {
133
+ name: typeof name === 'string' && name ? name : 'Error',
134
+ message: typeof message === 'string' && message ? message : str(err),
135
+ stack: typeof stack === 'string' ? stack : undefined,
136
+ }
137
+ }
138
+ if (typeof err === 'string') return { name: 'Error', message: err }
139
+ try {
140
+ return { name: 'Error', message: JSON.stringify(err) ?? str(err) }
141
+ } catch {
142
+ return { name: 'Error', message: str(err) }
143
+ }
144
+ }
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { describe, it, expect, vi } from 'vitest'
10
10
 
11
- const ANON = 'hz_anon_id'
11
+ const ANON = 'iam-anon-id'
12
12
  const LEGACY = '01920000-0000-7000-8000-0000000000aa'
13
13
  const OTHER = '01920000-0000-7000-8000-0000000000bb'
14
14
 
@@ -113,8 +113,8 @@ describe('anonId', () => {
113
113
  }
114
114
  })
115
115
 
116
- it("adopts hz.js's `hz_id` when there is no canonical id to find", async () => {
117
- // The no-build tag minted into a key of its own, so a browser that met hz.js
116
+ it("adopts a legacy `hz_id` when there is no canonical id to find", async () => {
117
+ // A no-build tag once minted into a key of its own, so a browser that met it
118
118
  // first already carries an identity — under a different name. Minting here
119
119
  // would make that visitor a stranger the moment they reach a bundled surface,
120
120
  // which is precisely the split this migration closes.
package/src/storage.ts CHANGED
@@ -30,7 +30,7 @@ function ls(): Storage | undefined {
30
30
 
31
31
  /**
32
32
  * Stable anonymous id, shared by every *.hanzo.ai surface AND by every Hanzo
33
- * client on the page — the npm client, hz.js and the hosted tag all run the one
33
+ * client on the page — the npm client and the hosted tag both run the one
34
34
  * chain in ./anon.js, so which snippet a surface loaded no longer decides who the
35
35
  * visitor is.
36
36
  *
package/src/uid.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // The implementation is `hzUuidv7` in ./anon.js and this file only re-exports it.
4
4
  // It lives there because the anonymous-id chain has to mint too, and that chain is
5
- // inlined verbatim by two distributions that have no bundler (hz.js, and the tag
5
+ // inlined verbatim by the distribution that has no bundler (the tag
6
6
  // the door hosts) — a minter here as well would be a second implementation, and
7
7
  // the version nibble it produces is exactly the thing that must never diverge.
8
8
  //
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  // The library version, stamped on every event (`libraryVersion`) and on the
2
2
  // Sentry `sdk` block. It lives alone so `sentry.ts` can read it without importing
3
3
  // `core.ts` — core imports sentry, so the reverse would be an import cycle.
4
- export const VERSION = '0.3.32'
4
+ export const VERSION = '0.3.34'