@hanzo/event 0.3.0 → 0.3.2
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 +61 -17
- package/dist/core-B1XEdWLd.d.cts +296 -0
- package/dist/core-B1XEdWLd.d.ts +296 -0
- package/dist/{index.js → index.cjs} +401 -20
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +65 -3
- package/dist/index.d.ts +65 -3
- package/dist/index.mjs +393 -19
- package/dist/index.mjs.map +1 -1
- package/dist/{react.js → react.cjs} +394 -20
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.mjs +392 -18
- package/dist/react.mjs.map +1 -1
- package/package.json +19 -9
- package/src/core.test.ts +258 -5
- package/src/core.ts +225 -37
- package/src/index.ts +8 -0
- package/src/scrub.test.ts +96 -0
- package/src/scrub.ts +112 -0
- package/src/sentry.test.ts +260 -0
- package/src/sentry.ts +279 -0
- package/src/types.ts +117 -15
- package/src/version.ts +4 -0
- package/dist/core-CrbiAQhN.d.cts +0 -186
- package/dist/core-CrbiAQhN.d.ts +0 -186
- package/dist/index.js.map +0 -1
- package/dist/react.js.map +0 -1
package/src/sentry.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Pure builders for the error path: parse a Hanzo-minted DSN, turn a JS Error into
|
|
2
|
+
// a Sentry `event`, and frame it into a Sentry envelope. No I/O, no globals — the
|
|
3
|
+
// Analytics client (core.ts) wires these to identity + transport.
|
|
4
|
+
//
|
|
5
|
+
// The wire shapes are a from-scratch model of the PUBLIC, documented Sentry ingest
|
|
6
|
+
// protocol (develop.sentry.dev), verified against the Hanzo /v1/sentry ingest
|
|
7
|
+
// (o11y pkg/modules/errortracking/implerrortracking: parseEnvelope, SentryEvent,
|
|
8
|
+
// normalizeEvent, computeFingerprint). No upstream (FSL) code is used.
|
|
9
|
+
|
|
10
|
+
import { scrubText } from './scrub'
|
|
11
|
+
import { VERSION } from './version'
|
|
12
|
+
import type {
|
|
13
|
+
CaptureErrorOptions,
|
|
14
|
+
Dsn,
|
|
15
|
+
SentryEvent,
|
|
16
|
+
SentryFrame,
|
|
17
|
+
SentryLevel,
|
|
18
|
+
} from './types'
|
|
19
|
+
|
|
20
|
+
/** Max stack frames kept — well under the server's 250 cap, plenty for grouping. */
|
|
21
|
+
const MAX_FRAMES = 50
|
|
22
|
+
/** Max stack lines examined, and max length of a line worth examining. Guards the
|
|
23
|
+
* frame regexes against a hostile `stack` string (see framesFromStack). */
|
|
24
|
+
const MAX_LINES = 500
|
|
25
|
+
const MAX_LINE_LEN = 2048
|
|
26
|
+
/** Max tag string length, so one huge property can't bloat the envelope. */
|
|
27
|
+
const MAX_TAG_LEN = 1024
|
|
28
|
+
/** Max tags copied from properties. */
|
|
29
|
+
const MAX_TAGS = 50
|
|
30
|
+
|
|
31
|
+
/** eventId mints a 32-hex-char id (no dashes) — the Sentry event_id shape. */
|
|
32
|
+
export function eventId(): string {
|
|
33
|
+
const c = typeof crypto !== 'undefined' ? crypto : undefined
|
|
34
|
+
if (c && 'randomUUID' in c) return c.randomUUID().replace(/-/g, '')
|
|
35
|
+
let s = ''
|
|
36
|
+
for (let i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16)
|
|
37
|
+
return s
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** byteLen returns the UTF-8 byte length used for envelope item framing. */
|
|
41
|
+
function byteLen(s: string): number {
|
|
42
|
+
if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(s).length
|
|
43
|
+
// Node fallback.
|
|
44
|
+
if (typeof Buffer !== 'undefined') return Buffer.byteLength(s, 'utf8')
|
|
45
|
+
return s.length
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* parseDsn parses "https://<version>:<hmac>@<host>/v1/sentry/<projectId>" into its
|
|
50
|
+
* public key + the derived envelope ingest URL. The key (which itself contains a
|
|
51
|
+
* ':') is taken verbatim as the userinfo — we do NOT split it as user:pass. The
|
|
52
|
+
* key rides ?sentry_key= (not the DSN in the body) because that is the credential
|
|
53
|
+
* channel the server trusts AND the only one sendBeacon can carry on unload.
|
|
54
|
+
* Returns null for anything malformed (fail-safe: the caller then stays inert).
|
|
55
|
+
*/
|
|
56
|
+
export function parseDsn(dsn: string | undefined | null): Dsn | null {
|
|
57
|
+
if (!dsn) return null
|
|
58
|
+
const m = /^(https?):\/\/([^@]+)@([^/]+)(\/.*)?$/.exec(dsn.trim())
|
|
59
|
+
if (!m) return null
|
|
60
|
+
const scheme = m[1]
|
|
61
|
+
const publicKey = m[2]
|
|
62
|
+
const host = m[3]
|
|
63
|
+
const path = m[4] ?? ''
|
|
64
|
+
if (!publicKey || !host) return null
|
|
65
|
+
const segs = path.split('/').filter(Boolean)
|
|
66
|
+
const projectId = segs.length > 0 ? segs[segs.length - 1] : ''
|
|
67
|
+
if (!projectId) return null
|
|
68
|
+
const origin = `${scheme}://${host}`
|
|
69
|
+
const ingestUrl =
|
|
70
|
+
`${origin}/v1/sentry/${encodeURIComponent(projectId)}/envelope/` +
|
|
71
|
+
`?sentry_key=${encodeURIComponent(publicKey)}`
|
|
72
|
+
return { publicKey, origin, projectId, ingestUrl }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const V8_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+)|([^)]+))\)?\s*$/
|
|
76
|
+
const MOZ_FRAME = /^\s*(?:(.*?)@)?(.+?):(\d+):(\d+)\s*$/
|
|
77
|
+
|
|
78
|
+
/** inApp marks a frame as the app's own code (vs vendor/runtime) for grouping. */
|
|
79
|
+
function inApp(file: string): boolean {
|
|
80
|
+
if (!file) return false
|
|
81
|
+
return !(
|
|
82
|
+
file.includes('node_modules') ||
|
|
83
|
+
file.startsWith('webpack-internal') ||
|
|
84
|
+
file.startsWith('webpack://') ||
|
|
85
|
+
file.startsWith('chrome-extension://') ||
|
|
86
|
+
file.startsWith('moz-extension://')
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* framesFromStack parses a browser Error.stack into Sentry frames, OLDEST-FIRST
|
|
92
|
+
* (Sentry orders caller->callee; the crash site is last — matching the server's
|
|
93
|
+
* pickCrashFrame). Handles both V8 ("at fn (file:li:co)") and
|
|
94
|
+
* Firefox/Safari ("fn@file:li:co"). Unparseable lines are skipped.
|
|
95
|
+
*/
|
|
96
|
+
export function framesFromStack(stack: string | undefined): SentryFrame[] {
|
|
97
|
+
if (!stack) return []
|
|
98
|
+
// Both frame regexes use lazy nested quantifiers, which backtrack badly on a
|
|
99
|
+
// long line that never matches. A stack is attacker-influenced (a thrown value
|
|
100
|
+
// can carry any `stack` string), so bound the work: skip absurd lines and stop
|
|
101
|
+
// after MAX_LINES. Only the innermost MAX_FRAMES are kept anyway.
|
|
102
|
+
const lines = stack.split('\n', MAX_LINES)
|
|
103
|
+
const frames: SentryFrame[] = []
|
|
104
|
+
for (const raw of lines) {
|
|
105
|
+
if (raw.length > MAX_LINE_LEN) continue
|
|
106
|
+
const line = raw.trimEnd()
|
|
107
|
+
if (!line) continue
|
|
108
|
+
// Header lines like "TypeError: x is not a function" match neither frame
|
|
109
|
+
// regex (no "at " prefix, no trailing :line:col) and are skipped naturally.
|
|
110
|
+
let fn: string | undefined
|
|
111
|
+
let file = ''
|
|
112
|
+
let lineno = 0
|
|
113
|
+
let colno = 0
|
|
114
|
+
const v = V8_FRAME.exec(line)
|
|
115
|
+
if (v) {
|
|
116
|
+
fn = v[1]
|
|
117
|
+
if (v[2]) {
|
|
118
|
+
file = v[2]
|
|
119
|
+
lineno = Number(v[3]) || 0
|
|
120
|
+
colno = Number(v[4]) || 0
|
|
121
|
+
} else {
|
|
122
|
+
file = (v[5] || '').trim()
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
const f = MOZ_FRAME.exec(line)
|
|
126
|
+
if (!f) continue
|
|
127
|
+
fn = f[1]
|
|
128
|
+
file = f[2]
|
|
129
|
+
lineno = Number(f[3]) || 0
|
|
130
|
+
colno = Number(f[4]) || 0
|
|
131
|
+
}
|
|
132
|
+
if (!file && !fn) continue
|
|
133
|
+
frames.push({
|
|
134
|
+
function: fn || '<anonymous>',
|
|
135
|
+
filename: file,
|
|
136
|
+
abs_path: file,
|
|
137
|
+
lineno,
|
|
138
|
+
colno,
|
|
139
|
+
in_app: inApp(file),
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
// Reverse to oldest-first and cap to the innermost MAX_FRAMES.
|
|
143
|
+
frames.reverse()
|
|
144
|
+
if (frames.length > MAX_FRAMES) return frames.slice(frames.length - MAX_FRAMES)
|
|
145
|
+
return frames
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** normalizeError coerces an unknown throwable into {name, message, stack}. */
|
|
149
|
+
export function normalizeError(err: unknown): { name: string; message: string; stack?: string } {
|
|
150
|
+
if (err instanceof Error) {
|
|
151
|
+
return { name: err.name || 'Error', message: err.message || String(err), stack: err.stack }
|
|
152
|
+
}
|
|
153
|
+
if (typeof err === 'string') return { name: 'Error', message: err }
|
|
154
|
+
try {
|
|
155
|
+
return { name: 'Error', message: JSON.stringify(err) }
|
|
156
|
+
} catch {
|
|
157
|
+
return { name: 'Error', message: String(err) }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Identity carried onto every error event — the SAME ids analytics uses. */
|
|
162
|
+
export interface ErrorIdentity {
|
|
163
|
+
/** OIDC sub (post-identify) or anon id. NEVER email/PII. */
|
|
164
|
+
userId?: string
|
|
165
|
+
sessionId?: string
|
|
166
|
+
product?: string
|
|
167
|
+
release?: string
|
|
168
|
+
environment?: string
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface BuildEventInput {
|
|
172
|
+
error: unknown
|
|
173
|
+
options?: CaptureErrorOptions
|
|
174
|
+
identity: ErrorIdentity
|
|
175
|
+
capturePII?: boolean
|
|
176
|
+
/** Injectable for deterministic tests. */
|
|
177
|
+
now?: number
|
|
178
|
+
id?: string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function coerceTag(v: unknown): string {
|
|
182
|
+
const s = typeof v === 'string' ? v : (() => {
|
|
183
|
+
try {
|
|
184
|
+
return JSON.stringify(v) ?? String(v)
|
|
185
|
+
} catch {
|
|
186
|
+
try {
|
|
187
|
+
return String(v) // circular — fall back to the primitive coercion
|
|
188
|
+
} catch {
|
|
189
|
+
return '[unstringifiable]' // ...which a throwing toString can also refuse
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
})()
|
|
193
|
+
return s.length > MAX_TAG_LEN ? s.slice(0, MAX_TAG_LEN) : s
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* buildSentryEvent turns a throwable + identity into a Sentry `event`. The message
|
|
198
|
+
* (the leak surface) is scrubbed client-side; the user is ONLY the stable subject
|
|
199
|
+
* id — never email/username/ip. Level defaults to error, or fatal for uncaught.
|
|
200
|
+
*/
|
|
201
|
+
export function buildSentryEvent(input: BuildEventInput): SentryEvent {
|
|
202
|
+
const { error, options = {}, identity, capturePII = false } = input
|
|
203
|
+
const norm = normalizeError(error)
|
|
204
|
+
const handled = options.handled !== false
|
|
205
|
+
const level: SentryLevel = options.level ?? (handled ? 'error' : 'fatal')
|
|
206
|
+
|
|
207
|
+
const tags: Record<string, string> = { handled: String(handled) }
|
|
208
|
+
if (identity.product) tags.product = identity.product
|
|
209
|
+
if (identity.sessionId) tags.session = identity.sessionId
|
|
210
|
+
// Tags come from arbitrary caller `properties`. Read each key in isolation:
|
|
211
|
+
// Object.entries() invokes every getter at once, so one throwing getter would
|
|
212
|
+
// cost us the entire envelope — losing the stack trace to save a tag. Keys are
|
|
213
|
+
// optional; the exception is not.
|
|
214
|
+
try {
|
|
215
|
+
const props = (options.properties ?? {}) as Record<string, unknown>
|
|
216
|
+
let n = 0
|
|
217
|
+
for (const k of Object.keys(props)) {
|
|
218
|
+
if (n >= MAX_TAGS) break
|
|
219
|
+
try {
|
|
220
|
+
const val = props[k]
|
|
221
|
+
if (val === undefined || val === null) continue
|
|
222
|
+
tags[k] = scrubText(coerceTag(val), capturePII)
|
|
223
|
+
n++
|
|
224
|
+
} catch {
|
|
225
|
+
continue // throwing getter or unstringifiable value — skip this key only
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
/* unenumerable/exotic properties object — ship the exception without tags */
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const event: SentryEvent = {
|
|
233
|
+
event_id: input.id ?? eventId(),
|
|
234
|
+
timestamp: (input.now ?? Date.now()) / 1000,
|
|
235
|
+
platform: 'javascript',
|
|
236
|
+
level,
|
|
237
|
+
logger: identity.product,
|
|
238
|
+
environment: identity.environment,
|
|
239
|
+
release: identity.release,
|
|
240
|
+
exception: {
|
|
241
|
+
values: [
|
|
242
|
+
{
|
|
243
|
+
type: norm.name,
|
|
244
|
+
value: scrubText(norm.message, capturePII),
|
|
245
|
+
stacktrace: { frames: framesFromStack(norm.stack) },
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
tags,
|
|
250
|
+
sdk: { name: '@hanzo/event', version: VERSION },
|
|
251
|
+
}
|
|
252
|
+
if (identity.userId) event.user = { id: identity.userId }
|
|
253
|
+
return event
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* buildEnvelope frames a Sentry event into a newline-delimited envelope:
|
|
258
|
+
*
|
|
259
|
+
* {"event_id","dsn","sent_at"}\n
|
|
260
|
+
* {"type":"event","content_type":"application/json","length":N}\n
|
|
261
|
+
* <event json>\n
|
|
262
|
+
*
|
|
263
|
+
* The item is length-delimited (N = UTF-8 byte length) — the framing the server's
|
|
264
|
+
* parseEnvelope reads first (falling back to newline-delimited otherwise).
|
|
265
|
+
*/
|
|
266
|
+
export function buildEnvelope(event: SentryEvent, dsn: Dsn, sentAt?: string): string {
|
|
267
|
+
const payload = JSON.stringify(event)
|
|
268
|
+
const header = JSON.stringify({
|
|
269
|
+
event_id: event.event_id,
|
|
270
|
+
dsn: `${dsn.origin}/v1/sentry/${dsn.projectId}`,
|
|
271
|
+
sent_at: sentAt ?? new Date().toISOString(),
|
|
272
|
+
})
|
|
273
|
+
const itemHeader = JSON.stringify({
|
|
274
|
+
type: 'event',
|
|
275
|
+
content_type: 'application/json',
|
|
276
|
+
length: byteLen(payload),
|
|
277
|
+
})
|
|
278
|
+
return `${header}\n${itemHeader}\n${payload}\n`
|
|
279
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// Public types for the Hanzo Event client.
|
|
2
2
|
|
|
3
|
-
/** The event kinds — the closed set the server understands.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
/** The event kinds — the closed set the server understands. `error` marks the
|
|
4
|
+
* breadcrumb an exception leaves on the event stream (Cloud stamps
|
|
5
|
+
* event_type='error' for the warehouse, GET /v1/errors). It does NOT reach the
|
|
6
|
+
* Sentry dashboard — the envelope on the error plane does that. */
|
|
6
7
|
export type EventKind = 'pageview' | 'event' | 'identify' | 'group' | 'error'
|
|
7
8
|
|
|
8
|
-
/** A captured exception
|
|
9
|
-
*
|
|
10
|
-
* error
|
|
9
|
+
/** A captured exception as it rides the EVENT STREAM — Cloud folds it into
|
|
10
|
+
* properties.$exception for the warehouse. The richer copy (parsed stack frames,
|
|
11
|
+
* grouping, release) travels on the error plane as a Sentry envelope; see
|
|
12
|
+
* AnalyticsConfig.dsn. */
|
|
11
13
|
export interface Exception {
|
|
12
14
|
/** Constructor/class name, e.g. "TypeError". */
|
|
13
15
|
type?: string
|
|
@@ -70,7 +72,8 @@ export interface WireEvent {
|
|
|
70
72
|
revenue?: number
|
|
71
73
|
currency?: string
|
|
72
74
|
/** Set on `type:'error'` events — the captured exception. Cloud lifts it into
|
|
73
|
-
* properties.$exception (foldException) for the
|
|
75
|
+
* properties.$exception (foldException) for the event warehouse. Not a Sentry
|
|
76
|
+
* path: the envelope on the error plane is what feeds the dashboard. */
|
|
74
77
|
error?: Exception
|
|
75
78
|
properties?: Record<string, unknown>
|
|
76
79
|
library?: string
|
|
@@ -82,8 +85,21 @@ export interface WireEvent {
|
|
|
82
85
|
* Authorization on fetch; on a headerless beacon a publishable key rides the
|
|
83
86
|
* ?ingest_key query. */
|
|
84
87
|
export interface Transport {
|
|
85
|
-
/** Durable POST usable during page unload (fetch keepalive / sendBeacon).
|
|
86
|
-
|
|
88
|
+
/** Durable POST usable during page unload (fetch keepalive / sendBeacon).
|
|
89
|
+
* `contentType` defaults to application/json; the error plane overrides it with
|
|
90
|
+
* application/x-sentry-envelope. */
|
|
91
|
+
send(
|
|
92
|
+
url: string,
|
|
93
|
+
body: string,
|
|
94
|
+
opts: {
|
|
95
|
+
beacon: boolean
|
|
96
|
+
token?: string
|
|
97
|
+
ingestKey?: string
|
|
98
|
+
contentType?: string
|
|
99
|
+
/** Surface non-OK / failed ingest on the console. Never on by default. */
|
|
100
|
+
debug?: boolean
|
|
101
|
+
},
|
|
102
|
+
): void
|
|
87
103
|
}
|
|
88
104
|
|
|
89
105
|
export interface AnalyticsConfig {
|
|
@@ -99,10 +115,11 @@ export interface AnalyticsConfig {
|
|
|
99
115
|
/** Publishable ingest key (pk_…). When set, the client authenticates to the ONE
|
|
100
116
|
* front door `/v1/event` with this key instead of a bearer/cookie: it rides
|
|
101
117
|
* Authorization: Bearer pk_… on fetch and ?ingest_key=pk_… on a headerless
|
|
102
|
-
* page-unload beacon, so
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
118
|
+
* page-unload beacon, so anonymous traffic is accepted and unload beacons work
|
|
119
|
+
* without a bearer. The key is write-only (cannot read) and safe to ship in a
|
|
120
|
+
* bundle; mint one per org via POST /v1/ingest/keys. This authenticates the
|
|
121
|
+
* EVENT STREAM only — the error plane authenticates independently with `dsn`,
|
|
122
|
+
* and one does not stand in for the other. */
|
|
106
123
|
ingestKey?: string
|
|
107
124
|
/** Max events buffered before an automatic flush. */
|
|
108
125
|
batchSize?: number
|
|
@@ -111,11 +128,96 @@ export interface AnalyticsConfig {
|
|
|
111
128
|
/** Turn the client off entirely (e.g. opt-out / DNT). Defaults to enabled. */
|
|
112
129
|
enabled?: boolean
|
|
113
130
|
/** Auto-capture unhandled errors + promise rejections (window.onerror,
|
|
114
|
-
* unhandledrejection)
|
|
115
|
-
* this is what makes the client a drop-in @sentry replacement
|
|
131
|
+
* unhandledrejection). Browser-only, defaults to enabled. Together with `dsn`
|
|
132
|
+
* this is what makes the client a drop-in @sentry replacement — without a
|
|
133
|
+
* `dsn` the captures never reach the Sentry dashboard. */
|
|
116
134
|
captureErrors?: boolean
|
|
117
135
|
/** Override the transport (tests). */
|
|
118
136
|
transport?: Transport
|
|
119
137
|
/** Debug logging. */
|
|
120
138
|
debug?: boolean
|
|
139
|
+
|
|
140
|
+
// ── error plane (Sentry envelope -> sentry.hanzo.ai) ──────────────────────
|
|
141
|
+
|
|
142
|
+
/** Hanzo-minted Sentry DSN: "https://<version>:<hmac>@<host>/v1/sentry/<projectId>".
|
|
143
|
+
* Publishable — the key authorizes writes to ONE project and can read nothing,
|
|
144
|
+
* so it is safe in a browser bundle (same trust class as `ingestKey`). When
|
|
145
|
+
* absent the client reads NEXT_PUBLIC_HANZO_EVENT_DSN; when neither is set the
|
|
146
|
+
* error plane is inert (fail-safe: nothing sent, nothing thrown, analytics
|
|
147
|
+
* unaffected). Mint one per property: POST /v1/sentry/projects. */
|
|
148
|
+
dsn?: string
|
|
149
|
+
/** Release stamped on error events (a git SHA / app version). */
|
|
150
|
+
release?: string
|
|
151
|
+
/** Deployment environment for error events (production | staging | …). */
|
|
152
|
+
environment?: string
|
|
153
|
+
/** Retain end-user PII (emails/IPs) in error text. Default false = scrub
|
|
154
|
+
* client-side before anything leaves the device (the server scrubs again). */
|
|
155
|
+
capturePII?: boolean
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Sentry envelope wire types (a from-scratch model of the PUBLIC, documented
|
|
159
|
+
// Sentry ingest protocol — develop.sentry.dev; no upstream code) ───────────
|
|
160
|
+
|
|
161
|
+
export type SentryLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug'
|
|
162
|
+
|
|
163
|
+
export interface SentryFrame {
|
|
164
|
+
filename?: string
|
|
165
|
+
function?: string
|
|
166
|
+
module?: string
|
|
167
|
+
abs_path?: string
|
|
168
|
+
lineno?: number
|
|
169
|
+
colno?: number
|
|
170
|
+
in_app?: boolean
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface SentryExceptionValue {
|
|
174
|
+
type?: string
|
|
175
|
+
value?: string
|
|
176
|
+
module?: string
|
|
177
|
+
stacktrace?: { frames: SentryFrame[] }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface SentryUser {
|
|
181
|
+
/** Stable subject id (OIDC sub / anon id). NEVER email/username/ip. */
|
|
182
|
+
id?: string
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface SentryEvent {
|
|
186
|
+
event_id: string
|
|
187
|
+
timestamp: number
|
|
188
|
+
platform: 'javascript'
|
|
189
|
+
level: SentryLevel
|
|
190
|
+
logger?: string
|
|
191
|
+
environment?: string
|
|
192
|
+
release?: string
|
|
193
|
+
transaction?: string
|
|
194
|
+
fingerprint?: string[]
|
|
195
|
+
message?: string
|
|
196
|
+
exception?: { values: SentryExceptionValue[] }
|
|
197
|
+
tags?: Record<string, string>
|
|
198
|
+
user?: SentryUser
|
|
199
|
+
contexts?: Record<string, Record<string, unknown>>
|
|
200
|
+
sdk?: { name: string; version: string }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Parsed DSN — the public key + the derived ingest URL. */
|
|
204
|
+
export interface Dsn {
|
|
205
|
+
/** "<version>:<hmac>" public key presented via ?sentry_key= (beacon-safe). */
|
|
206
|
+
publicKey: string
|
|
207
|
+
/** Ingest origin, e.g. "https://sentry.hanzo.ai". */
|
|
208
|
+
origin: string
|
|
209
|
+
/** Project id segment. */
|
|
210
|
+
projectId: string
|
|
211
|
+
/** Fully-derived envelope ingest URL incl. ?sentry_key=. */
|
|
212
|
+
ingestUrl: string
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Options for Analytics.captureError. */
|
|
216
|
+
export interface CaptureErrorOptions {
|
|
217
|
+
/** false => uncaught (window.onerror / unhandledrejection / render crash). */
|
|
218
|
+
handled?: boolean
|
|
219
|
+
/** Severity + free-form context; merged into the event's tags. */
|
|
220
|
+
properties?: Record<string, unknown>
|
|
221
|
+
/** Override the event level (default: error, or fatal when handled === false). */
|
|
222
|
+
level?: SentryLevel
|
|
121
223
|
}
|
package/src/version.ts
ADDED
package/dist/core-CrbiAQhN.d.cts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/** The event kinds — the closed set the server understands. An error is just
|
|
2
|
-
* another event on the one stream (Cloud stamps type:'error' → event_type='error',
|
|
3
|
-
* the key the error-tracking lens filters on). */
|
|
4
|
-
type EventKind = 'pageview' | 'event' | 'identify' | 'group' | 'error';
|
|
5
|
-
/** A captured exception. Carried on a `type:'error'` event's top-level `error`
|
|
6
|
-
* field; Cloud folds it into properties.$exception and lenses the event into the
|
|
7
|
-
* error-tracking view (sentry.hanzo.ai). */
|
|
8
|
-
interface Exception {
|
|
9
|
-
/** Constructor/class name, e.g. "TypeError". */
|
|
10
|
-
type?: string;
|
|
11
|
-
/** The error message. */
|
|
12
|
-
message: string;
|
|
13
|
-
/** Stack trace when available. */
|
|
14
|
-
stack?: string;
|
|
15
|
-
/** false = an unhandled/global error (window.onerror, unhandledrejection);
|
|
16
|
-
* true = a caught error the app chose to report. Defaults true. */
|
|
17
|
-
handled?: boolean;
|
|
18
|
-
}
|
|
19
|
-
/** First-touch marketing attribution, parsed once and persisted. */
|
|
20
|
-
interface Attribution {
|
|
21
|
-
utm: {
|
|
22
|
-
source?: string;
|
|
23
|
-
medium?: string;
|
|
24
|
-
campaign?: string;
|
|
25
|
-
term?: string;
|
|
26
|
-
content?: string;
|
|
27
|
-
};
|
|
28
|
-
referrer?: string;
|
|
29
|
-
refCode?: string;
|
|
30
|
-
/** Derived acquisition channel: direct | organic | paid | social | referral. */
|
|
31
|
-
channel?: string;
|
|
32
|
-
}
|
|
33
|
-
/** Cohort dimensions carried on every event once known (see goals.ts COHORTS). */
|
|
34
|
-
interface Cohort {
|
|
35
|
-
/** ISO week the person first signed up, e.g. "2026-W28". */
|
|
36
|
-
signupWeek?: string;
|
|
37
|
-
channel?: string;
|
|
38
|
-
refCode?: string;
|
|
39
|
-
}
|
|
40
|
-
/** One event as sent on the wire — the canonical Hanzo Cloud event. Maps 1:1 to
|
|
41
|
-
* the cloud `CaptureEvent` (camelCase JSON keys); a batch of these is POSTed to
|
|
42
|
-
* the ONE front door `/v1/event` as `{ batch: [WireEvent, …] }`. tenant/org is
|
|
43
|
-
* NEVER a field here — the server stamps it from the validated session/key. */
|
|
44
|
-
interface WireEvent {
|
|
45
|
-
messageId: string;
|
|
46
|
-
type: EventKind;
|
|
47
|
-
event?: string;
|
|
48
|
-
timestamp: string;
|
|
49
|
-
distinctId?: string;
|
|
50
|
-
anonymousId?: string;
|
|
51
|
-
personId?: string;
|
|
52
|
-
sessionId?: string;
|
|
53
|
-
product?: string;
|
|
54
|
-
url?: string;
|
|
55
|
-
path?: string;
|
|
56
|
-
referrer?: string;
|
|
57
|
-
utm?: Attribution['utm'];
|
|
58
|
-
refCode?: string;
|
|
59
|
-
channel?: string;
|
|
60
|
-
groupId?: string;
|
|
61
|
-
signupWeek?: string;
|
|
62
|
-
productId?: string;
|
|
63
|
-
quantity?: number;
|
|
64
|
-
revenue?: number;
|
|
65
|
-
currency?: string;
|
|
66
|
-
/** Set on `type:'error'` events — the captured exception. Cloud lifts it into
|
|
67
|
-
* properties.$exception (foldException) for the error-tracking lens. */
|
|
68
|
-
error?: Exception;
|
|
69
|
-
properties?: Record<string, unknown>;
|
|
70
|
-
library?: string;
|
|
71
|
-
libraryVersion?: string;
|
|
72
|
-
}
|
|
73
|
-
/** Injectable transports — overridden in tests; the default in core.ts uses fetch
|
|
74
|
-
* (keepalive) and sendBeacon. A bearer JWT or a publishable pk_ key rides
|
|
75
|
-
* Authorization on fetch; on a headerless beacon a publishable key rides the
|
|
76
|
-
* ?ingest_key query. */
|
|
77
|
-
interface Transport {
|
|
78
|
-
/** Durable POST usable during page unload (fetch keepalive / sendBeacon). */
|
|
79
|
-
send(url: string, body: string, opts: {
|
|
80
|
-
beacon: boolean;
|
|
81
|
-
token?: string;
|
|
82
|
-
ingestKey?: string;
|
|
83
|
-
}): void;
|
|
84
|
-
}
|
|
85
|
-
interface AnalyticsConfig {
|
|
86
|
-
/** Cloud base URL. Defaults to "https://api.hanzo.ai" (the one edge). Set to
|
|
87
|
-
* same-origin ("") for cookie-auth apps served behind the same edge
|
|
88
|
-
* (console/admin/chat), so the browser rides the session cookie. */
|
|
89
|
-
host?: string;
|
|
90
|
-
/** Emitting surface: console | chat | app | site | admin. */
|
|
91
|
-
product: string;
|
|
92
|
-
/** Bearer token provider for token-auth apps. Omit for cookie/session apps
|
|
93
|
-
* (the client then relies on same-origin credentials). */
|
|
94
|
-
getToken?: () => string | undefined | null;
|
|
95
|
-
/** Publishable ingest key (pk_…). When set, the client authenticates to the ONE
|
|
96
|
-
* front door `/v1/event` with this key instead of a bearer/cookie: it rides
|
|
97
|
-
* Authorization: Bearer pk_… on fetch and ?ingest_key=pk_… on a headerless
|
|
98
|
-
* page-unload beacon, so ALL THREE lenses (web + product + error) light up with
|
|
99
|
-
* no bearer and unload beacons work anonymously. The key is write-only (cannot
|
|
100
|
-
* read) and safe to ship in a bundle; mint one per org via POST /v1/ingest/keys.
|
|
101
|
-
* Recommended for marketing/public pages and the full sentry-subsuming setup. */
|
|
102
|
-
ingestKey?: string;
|
|
103
|
-
/** Max events buffered before an automatic flush. */
|
|
104
|
-
batchSize?: number;
|
|
105
|
-
/** Auto-flush cadence in ms. */
|
|
106
|
-
flushIntervalMs?: number;
|
|
107
|
-
/** Turn the client off entirely (e.g. opt-out / DNT). Defaults to enabled. */
|
|
108
|
-
enabled?: boolean;
|
|
109
|
-
/** Auto-capture unhandled errors + promise rejections (window.onerror,
|
|
110
|
-
* unhandledrejection) as error events. Browser-only. Defaults to enabled —
|
|
111
|
-
* this is what makes the client a drop-in @sentry replacement. */
|
|
112
|
-
captureErrors?: boolean;
|
|
113
|
-
/** Override the transport (tests). */
|
|
114
|
-
transport?: Transport;
|
|
115
|
-
/** Debug logging. */
|
|
116
|
-
debug?: boolean;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
declare const VERSION = "0.3.0";
|
|
120
|
-
declare class Analytics {
|
|
121
|
-
private cfg;
|
|
122
|
-
private transport;
|
|
123
|
-
private queue;
|
|
124
|
-
private timer;
|
|
125
|
-
private personId?;
|
|
126
|
-
private attribution;
|
|
127
|
-
private cohort;
|
|
128
|
-
private started;
|
|
129
|
-
constructor(config: AnalyticsConfig);
|
|
130
|
-
/** init is idempotent and browser-only for its side effects: capture first-touch
|
|
131
|
-
* attribution, hydrate cohort, register the unload flush, and (unless opted out)
|
|
132
|
-
* auto-capture unhandled errors. Safe to call from a React effect on every
|
|
133
|
-
* render. */
|
|
134
|
-
init(): void;
|
|
135
|
-
/** identify binds the current visitor to a stable person id (post-login). */
|
|
136
|
-
identify(personId: string, traits?: Record<string, unknown>): void;
|
|
137
|
-
/** group associates the visitor with an org/team (analytics grouping, not the
|
|
138
|
-
* server tenant — the server still derives tenant from the session). */
|
|
139
|
-
group(groupId: string, traits?: Record<string, unknown>): void;
|
|
140
|
-
/** pageview records a $pageview for the current (or given) location. */
|
|
141
|
-
pageview(path?: string, properties?: Record<string, unknown>): void;
|
|
142
|
-
/** capture records a named product event with optional properties. Commerce
|
|
143
|
-
* fields (productId/quantity/revenue/currency) may be passed for order events. */
|
|
144
|
-
capture(event: string, properties?: Record<string, unknown>, commerce?: Pick<WireEvent, 'productId' | 'quantity' | 'revenue' | 'currency'>): void;
|
|
145
|
-
/** track is an alias of capture (Segment familiarity). */
|
|
146
|
-
track: (event: string, properties?: Record<string, unknown>, commerce?: Pick<WireEvent, "productId" | "quantity" | "revenue" | "currency">) => void;
|
|
147
|
-
/** captureError records an exception as a first-class error event — the ONE
|
|
148
|
-
* error path (subsumes @sentry). A caught error, an unhandled rejection, a
|
|
149
|
-
* React render error, or a manual report all become a type:'error' event on the
|
|
150
|
-
* same stream; Cloud folds the exception into properties.$exception and stamps
|
|
151
|
-
* event_type='error', so it surfaces in the error-tracking lens. Never throws
|
|
152
|
-
* back into the app; errors are higher-signal than pageviews, so it flushes
|
|
153
|
-
* promptly (a crash may unload the page moments later). */
|
|
154
|
-
captureError(err: unknown, context?: {
|
|
155
|
-
handled?: boolean;
|
|
156
|
-
properties?: Record<string, unknown>;
|
|
157
|
-
}): void;
|
|
158
|
-
/** captureException — @sentry-familiar alias of captureError. */
|
|
159
|
-
captureException: (err: unknown, context?: {
|
|
160
|
-
handled?: boolean;
|
|
161
|
-
properties?: Record<string, unknown>;
|
|
162
|
-
}) => void;
|
|
163
|
-
/** setCohort persists cohort dimensions (e.g. signupWeek at signup) so they ride
|
|
164
|
-
* every subsequent event. */
|
|
165
|
-
setCohort(patch: Cohort): void;
|
|
166
|
-
/** flush drains the buffer to the server as ONE batch through the ONE ingest
|
|
167
|
-
* front door POST /v1/event, body { batch: [Event…] }. beacon=true selects the
|
|
168
|
-
* unload-safe transport. Auth is orthogonal to the wire:
|
|
169
|
-
*
|
|
170
|
-
* • publishable key set → rides Authorization: Bearer pk_… (fetch) or
|
|
171
|
-
* ?ingest_key=pk_… (beacon), so unload beacons work anonymously.
|
|
172
|
-
* • else a bearer JWT rides Authorization (fetch only — sendBeacon cannot
|
|
173
|
-
* carry a header, so token apps fall back to keepalive fetch on unload).
|
|
174
|
-
* • else a cookie app rides same-origin credentials (beacon carries the
|
|
175
|
-
* cookie fine).
|
|
176
|
-
*/
|
|
177
|
-
flush(beacon?: boolean): void;
|
|
178
|
-
private enqueue;
|
|
179
|
-
private build;
|
|
180
|
-
private schedule;
|
|
181
|
-
private clearTimer;
|
|
182
|
-
}
|
|
183
|
-
/** createAnalytics builds a client instance. Most apps use one shared instance. */
|
|
184
|
-
declare function createAnalytics(config: AnalyticsConfig): Analytics;
|
|
185
|
-
|
|
186
|
-
export { type Attribution as A, type Cohort as C, type EventKind as E, type Transport as T, VERSION as V, type WireEvent as W, Analytics as a, type AnalyticsConfig as b, type Exception as c, createAnalytics as d };
|