@hanzo/event 0.3.1 → 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.cjs +399 -18
- package/dist/index.cjs.map +1 -1
- 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.cjs +392 -18
- package/dist/react.cjs.map +1 -1
- 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 +2 -2
- 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.cjs
CHANGED
|
@@ -105,6 +105,263 @@ var EVENTS = {
|
|
|
105
105
|
};
|
|
106
106
|
var PAGEVIEW = "$pageview";
|
|
107
107
|
|
|
108
|
+
// src/scrub.ts
|
|
109
|
+
var REDACTED = "[redacted]";
|
|
110
|
+
var EMAIL_MARK = "[email]";
|
|
111
|
+
var IP_MARK = "[ip]";
|
|
112
|
+
var SECRET_PATTERNS = [
|
|
113
|
+
/-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?-----END[ A-Z]*PRIVATE KEY-----/g,
|
|
114
|
+
/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}/g,
|
|
115
|
+
// JWT
|
|
116
|
+
/\bbearer\s+[A-Za-z0-9._~+/-]{12,}=*/gi,
|
|
117
|
+
// bearer token
|
|
118
|
+
/\b(?:sk|pk|rk)-[A-Za-z0-9]{2,}-?[A-Za-z0-9]{12,}/g,
|
|
119
|
+
// openai-style
|
|
120
|
+
/\b(?:sk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g,
|
|
121
|
+
// stripe
|
|
122
|
+
/\bhk-[A-Za-z0-9]{16,}/g,
|
|
123
|
+
// hanzo key
|
|
124
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
125
|
+
// aws access key id
|
|
126
|
+
/\bASIA[0-9A-Z]{16}\b/g,
|
|
127
|
+
// aws sts key id
|
|
128
|
+
/\bAIza[0-9A-Za-z_-]{20,}/g,
|
|
129
|
+
// google api key
|
|
130
|
+
/\bgh[posru]_[A-Za-z0-9]{20,}/g,
|
|
131
|
+
// github token
|
|
132
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
133
|
+
// slack token
|
|
134
|
+
// Creds in a URL/DSN. The repetition is BOUNDED on purpose: the unbounded form
|
|
135
|
+
// ([^\s:@/]+:[^\s@/]+@) backtracks quadratically on colon-rich text with no
|
|
136
|
+
// terminating '@' — an HTML error page pasted into an error message took 4.9s
|
|
137
|
+
// at 32KB and >60s at 128KB, freezing the main thread from inside captureError.
|
|
138
|
+
// Real userinfo is far below these caps, so bounding costs nothing.
|
|
139
|
+
/[a-zA-Z][a-zA-Z0-9+.-]{0,32}:\/\/[^\s:@/]{1,256}:[^\s@/]{1,256}@/g
|
|
140
|
+
];
|
|
141
|
+
var RE_PAN = /\b(?:\d[ -]?){13,19}\b/g;
|
|
142
|
+
function luhn(digits) {
|
|
143
|
+
let sum = 0;
|
|
144
|
+
let alt = false;
|
|
145
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
146
|
+
let d = digits.charCodeAt(i) - 48;
|
|
147
|
+
if (alt) {
|
|
148
|
+
d *= 2;
|
|
149
|
+
if (d > 9) d -= 9;
|
|
150
|
+
}
|
|
151
|
+
sum += d;
|
|
152
|
+
alt = !alt;
|
|
153
|
+
}
|
|
154
|
+
return sum % 10 === 0;
|
|
155
|
+
}
|
|
156
|
+
function redactPAN(s) {
|
|
157
|
+
return s.replace(RE_PAN, (m) => {
|
|
158
|
+
const digits = m.replace(/[ -]/g, "");
|
|
159
|
+
if (digits.length < 13 || digits.length > 19) return m;
|
|
160
|
+
return luhn(digits) ? REDACTED : m;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
var RE_EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
164
|
+
var RE_IPV4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
|
|
165
|
+
var RE_IPV6 = /\b(?:[0-9A-Fa-f]{1,4}:){2,7}[0-9A-Fa-f]{1,4}\b/g;
|
|
166
|
+
function redactSecrets(s) {
|
|
167
|
+
for (const re of SECRET_PATTERNS) s = s.replace(re, REDACTED);
|
|
168
|
+
return redactPAN(s);
|
|
169
|
+
}
|
|
170
|
+
function scrubPII(s) {
|
|
171
|
+
s = s.replace(RE_EMAIL, EMAIL_MARK);
|
|
172
|
+
s = s.replace(RE_IPV6, IP_MARK);
|
|
173
|
+
s = s.replace(RE_IPV4, IP_MARK);
|
|
174
|
+
return s;
|
|
175
|
+
}
|
|
176
|
+
var MAX_SCRUB_LEN = 8192;
|
|
177
|
+
function truncate(s, max = MAX_SCRUB_LEN) {
|
|
178
|
+
return s.length > max ? s.slice(0, max) + "\u2026 [truncated]" : s;
|
|
179
|
+
}
|
|
180
|
+
function scrubText(s, capturePII = false) {
|
|
181
|
+
if (!s) return s ?? "";
|
|
182
|
+
s = truncate(s);
|
|
183
|
+
s = redactSecrets(s);
|
|
184
|
+
if (!capturePII) s = scrubPII(s);
|
|
185
|
+
return s;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/version.ts
|
|
189
|
+
var VERSION = "0.3.2";
|
|
190
|
+
|
|
191
|
+
// src/sentry.ts
|
|
192
|
+
var MAX_FRAMES = 50;
|
|
193
|
+
var MAX_LINES = 500;
|
|
194
|
+
var MAX_LINE_LEN = 2048;
|
|
195
|
+
var MAX_TAG_LEN = 1024;
|
|
196
|
+
var MAX_TAGS = 50;
|
|
197
|
+
function eventId() {
|
|
198
|
+
const c = typeof crypto !== "undefined" ? crypto : void 0;
|
|
199
|
+
if (c && "randomUUID" in c) return c.randomUUID().replace(/-/g, "");
|
|
200
|
+
let s = "";
|
|
201
|
+
for (let i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16);
|
|
202
|
+
return s;
|
|
203
|
+
}
|
|
204
|
+
function byteLen(s) {
|
|
205
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(s).length;
|
|
206
|
+
if (typeof Buffer !== "undefined") return Buffer.byteLength(s, "utf8");
|
|
207
|
+
return s.length;
|
|
208
|
+
}
|
|
209
|
+
function parseDsn(dsn) {
|
|
210
|
+
if (!dsn) return null;
|
|
211
|
+
const m = /^(https?):\/\/([^@]+)@([^/]+)(\/.*)?$/.exec(dsn.trim());
|
|
212
|
+
if (!m) return null;
|
|
213
|
+
const scheme = m[1];
|
|
214
|
+
const publicKey = m[2];
|
|
215
|
+
const host = m[3];
|
|
216
|
+
const path = m[4] ?? "";
|
|
217
|
+
if (!publicKey || !host) return null;
|
|
218
|
+
const segs = path.split("/").filter(Boolean);
|
|
219
|
+
const projectId = segs.length > 0 ? segs[segs.length - 1] : "";
|
|
220
|
+
if (!projectId) return null;
|
|
221
|
+
const origin = `${scheme}://${host}`;
|
|
222
|
+
const ingestUrl = `${origin}/v1/sentry/${encodeURIComponent(projectId)}/envelope/?sentry_key=${encodeURIComponent(publicKey)}`;
|
|
223
|
+
return { publicKey, origin, projectId, ingestUrl };
|
|
224
|
+
}
|
|
225
|
+
var V8_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+)|([^)]+))\)?\s*$/;
|
|
226
|
+
var MOZ_FRAME = /^\s*(?:(.*?)@)?(.+?):(\d+):(\d+)\s*$/;
|
|
227
|
+
function inApp(file) {
|
|
228
|
+
if (!file) return false;
|
|
229
|
+
return !(file.includes("node_modules") || file.startsWith("webpack-internal") || file.startsWith("webpack://") || file.startsWith("chrome-extension://") || file.startsWith("moz-extension://"));
|
|
230
|
+
}
|
|
231
|
+
function framesFromStack(stack) {
|
|
232
|
+
if (!stack) return [];
|
|
233
|
+
const lines = stack.split("\n", MAX_LINES);
|
|
234
|
+
const frames = [];
|
|
235
|
+
for (const raw of lines) {
|
|
236
|
+
if (raw.length > MAX_LINE_LEN) continue;
|
|
237
|
+
const line = raw.trimEnd();
|
|
238
|
+
if (!line) continue;
|
|
239
|
+
let fn;
|
|
240
|
+
let file = "";
|
|
241
|
+
let lineno = 0;
|
|
242
|
+
let colno = 0;
|
|
243
|
+
const v = V8_FRAME.exec(line);
|
|
244
|
+
if (v) {
|
|
245
|
+
fn = v[1];
|
|
246
|
+
if (v[2]) {
|
|
247
|
+
file = v[2];
|
|
248
|
+
lineno = Number(v[3]) || 0;
|
|
249
|
+
colno = Number(v[4]) || 0;
|
|
250
|
+
} else {
|
|
251
|
+
file = (v[5] || "").trim();
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
const f = MOZ_FRAME.exec(line);
|
|
255
|
+
if (!f) continue;
|
|
256
|
+
fn = f[1];
|
|
257
|
+
file = f[2];
|
|
258
|
+
lineno = Number(f[3]) || 0;
|
|
259
|
+
colno = Number(f[4]) || 0;
|
|
260
|
+
}
|
|
261
|
+
if (!file && !fn) continue;
|
|
262
|
+
frames.push({
|
|
263
|
+
function: fn || "<anonymous>",
|
|
264
|
+
filename: file,
|
|
265
|
+
abs_path: file,
|
|
266
|
+
lineno,
|
|
267
|
+
colno,
|
|
268
|
+
in_app: inApp(file)
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
frames.reverse();
|
|
272
|
+
if (frames.length > MAX_FRAMES) return frames.slice(frames.length - MAX_FRAMES);
|
|
273
|
+
return frames;
|
|
274
|
+
}
|
|
275
|
+
function normalizeError(err) {
|
|
276
|
+
if (err instanceof Error) {
|
|
277
|
+
return { name: err.name || "Error", message: err.message || String(err), stack: err.stack };
|
|
278
|
+
}
|
|
279
|
+
if (typeof err === "string") return { name: "Error", message: err };
|
|
280
|
+
try {
|
|
281
|
+
return { name: "Error", message: JSON.stringify(err) };
|
|
282
|
+
} catch {
|
|
283
|
+
return { name: "Error", message: String(err) };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function coerceTag(v) {
|
|
287
|
+
const s = typeof v === "string" ? v : (() => {
|
|
288
|
+
try {
|
|
289
|
+
return JSON.stringify(v) ?? String(v);
|
|
290
|
+
} catch {
|
|
291
|
+
try {
|
|
292
|
+
return String(v);
|
|
293
|
+
} catch {
|
|
294
|
+
return "[unstringifiable]";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
})();
|
|
298
|
+
return s.length > MAX_TAG_LEN ? s.slice(0, MAX_TAG_LEN) : s;
|
|
299
|
+
}
|
|
300
|
+
function buildSentryEvent(input) {
|
|
301
|
+
const { error, options = {}, identity, capturePII = false } = input;
|
|
302
|
+
const norm = normalizeError(error);
|
|
303
|
+
const handled = options.handled !== false;
|
|
304
|
+
const level = options.level ?? (handled ? "error" : "fatal");
|
|
305
|
+
const tags = { handled: String(handled) };
|
|
306
|
+
if (identity.product) tags.product = identity.product;
|
|
307
|
+
if (identity.sessionId) tags.session = identity.sessionId;
|
|
308
|
+
try {
|
|
309
|
+
const props = options.properties ?? {};
|
|
310
|
+
let n = 0;
|
|
311
|
+
for (const k of Object.keys(props)) {
|
|
312
|
+
if (n >= MAX_TAGS) break;
|
|
313
|
+
try {
|
|
314
|
+
const val = props[k];
|
|
315
|
+
if (val === void 0 || val === null) continue;
|
|
316
|
+
tags[k] = scrubText(coerceTag(val), capturePII);
|
|
317
|
+
n++;
|
|
318
|
+
} catch {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
const event = {
|
|
325
|
+
event_id: input.id ?? eventId(),
|
|
326
|
+
timestamp: (input.now ?? Date.now()) / 1e3,
|
|
327
|
+
platform: "javascript",
|
|
328
|
+
level,
|
|
329
|
+
logger: identity.product,
|
|
330
|
+
environment: identity.environment,
|
|
331
|
+
release: identity.release,
|
|
332
|
+
exception: {
|
|
333
|
+
values: [
|
|
334
|
+
{
|
|
335
|
+
type: norm.name,
|
|
336
|
+
value: scrubText(norm.message, capturePII),
|
|
337
|
+
stacktrace: { frames: framesFromStack(norm.stack) }
|
|
338
|
+
}
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
tags,
|
|
342
|
+
sdk: { name: "@hanzo/event", version: VERSION }
|
|
343
|
+
};
|
|
344
|
+
if (identity.userId) event.user = { id: identity.userId };
|
|
345
|
+
return event;
|
|
346
|
+
}
|
|
347
|
+
function buildEnvelope(event, dsn, sentAt) {
|
|
348
|
+
const payload = JSON.stringify(event);
|
|
349
|
+
const header = JSON.stringify({
|
|
350
|
+
event_id: event.event_id,
|
|
351
|
+
dsn: `${dsn.origin}/v1/sentry/${dsn.projectId}`,
|
|
352
|
+
sent_at: sentAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
353
|
+
});
|
|
354
|
+
const itemHeader = JSON.stringify({
|
|
355
|
+
type: "event",
|
|
356
|
+
content_type: "application/json",
|
|
357
|
+
length: byteLen(payload)
|
|
358
|
+
});
|
|
359
|
+
return `${header}
|
|
360
|
+
${itemHeader}
|
|
361
|
+
${payload}
|
|
362
|
+
`;
|
|
363
|
+
}
|
|
364
|
+
|
|
108
365
|
// src/storage.ts
|
|
109
366
|
var KEY = {
|
|
110
367
|
anon: "hz_anon_id",
|
|
@@ -193,9 +450,25 @@ function mergeCohort(patch) {
|
|
|
193
450
|
}
|
|
194
451
|
|
|
195
452
|
// src/core.ts
|
|
196
|
-
var VERSION = "0.3.0";
|
|
197
453
|
var EVENT_PATH = "/v1/event";
|
|
198
454
|
var DEFAULT_HOST = "https://api.hanzo.ai";
|
|
455
|
+
var ENVELOPE_CONTENT_TYPE = "application/x-sentry-envelope";
|
|
456
|
+
function readEnvDsn() {
|
|
457
|
+
try {
|
|
458
|
+
if (typeof process !== "undefined" && process.env) {
|
|
459
|
+
return process.env.NEXT_PUBLIC_HANZO_EVENT_DSN || process.env.HANZO_EVENT_DSN || void 0;
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
return void 0;
|
|
464
|
+
}
|
|
465
|
+
function readEnv(name) {
|
|
466
|
+
try {
|
|
467
|
+
if (typeof process !== "undefined" && process.env) return process.env[name] || void 0;
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
return void 0;
|
|
471
|
+
}
|
|
199
472
|
function appendQuery(url, key, value) {
|
|
200
473
|
return url + (url.includes("?") ? "&" : "?") + key + "=" + encodeURIComponent(value);
|
|
201
474
|
}
|
|
@@ -204,7 +477,7 @@ function uid2() {
|
|
|
204
477
|
if (c && "randomUUID" in c) return c.randomUUID();
|
|
205
478
|
return "m-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
206
479
|
}
|
|
207
|
-
function
|
|
480
|
+
function normalizeError2(err) {
|
|
208
481
|
if (err instanceof Error) {
|
|
209
482
|
return { type: err.name, message: err.message, stack: err.stack };
|
|
210
483
|
}
|
|
@@ -216,18 +489,37 @@ function normalizeError(err) {
|
|
|
216
489
|
}
|
|
217
490
|
}
|
|
218
491
|
var isBrowser = () => typeof window !== "undefined";
|
|
492
|
+
function serializeBatch(batch) {
|
|
493
|
+
try {
|
|
494
|
+
return JSON.stringify({ batch });
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
const parts = [];
|
|
498
|
+
for (const e of batch) {
|
|
499
|
+
try {
|
|
500
|
+
parts.push(JSON.stringify(e));
|
|
501
|
+
} catch {
|
|
502
|
+
try {
|
|
503
|
+
parts.push(JSON.stringify({ ...e, properties: { $unserializable: true } }));
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return parts.length > 0 ? '{"batch":[' + parts.join(",") + "]}" : null;
|
|
509
|
+
}
|
|
219
510
|
var DefaultTransport = class {
|
|
220
511
|
send(url, body, opts) {
|
|
512
|
+
const contentType = opts.contentType ?? "application/json";
|
|
221
513
|
if (opts.beacon && isBrowser() && typeof navigator.sendBeacon === "function") {
|
|
222
514
|
const beaconUrl = opts.ingestKey ? appendQuery(url, "ingest_key", opts.ingestKey) : url;
|
|
223
515
|
try {
|
|
224
|
-
navigator.sendBeacon(beaconUrl, new Blob([body], { type:
|
|
516
|
+
navigator.sendBeacon(beaconUrl, new Blob([body], { type: contentType }));
|
|
225
517
|
return;
|
|
226
518
|
} catch {
|
|
227
519
|
}
|
|
228
520
|
}
|
|
229
521
|
if (typeof fetch !== "function") return;
|
|
230
|
-
const headers = { "Content-Type":
|
|
522
|
+
const headers = { "Content-Type": contentType };
|
|
231
523
|
const bearer = opts.ingestKey ?? opts.token;
|
|
232
524
|
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
233
525
|
void fetch(url, {
|
|
@@ -236,7 +528,12 @@ var DefaultTransport = class {
|
|
|
236
528
|
body,
|
|
237
529
|
keepalive: true,
|
|
238
530
|
credentials: "include"
|
|
239
|
-
}).
|
|
531
|
+
}).then((res) => {
|
|
532
|
+
if (!res.ok && opts.debug) {
|
|
533
|
+
console.warn("[event] ingest rejected", res.status, url.split("?")[0]);
|
|
534
|
+
}
|
|
535
|
+
}).catch((e) => {
|
|
536
|
+
if (opts.debug) console.warn("[event] ingest failed", url.split("?")[0], e);
|
|
240
537
|
});
|
|
241
538
|
}
|
|
242
539
|
};
|
|
@@ -247,6 +544,8 @@ var Analytics = class {
|
|
|
247
544
|
this.attribution = { utm: {} };
|
|
248
545
|
this.cohort = {};
|
|
249
546
|
this.started = false;
|
|
547
|
+
/** Guards against an error thrown *inside* the error path re-entering it. */
|
|
548
|
+
this.reentrant = false;
|
|
250
549
|
/** track is an alias of capture (Segment familiarity). */
|
|
251
550
|
this.track = this.capture.bind(this);
|
|
252
551
|
/** captureException — @sentry-familiar alias of captureError. */
|
|
@@ -260,6 +559,19 @@ var Analytics = class {
|
|
|
260
559
|
...config
|
|
261
560
|
};
|
|
262
561
|
this.transport = config.transport ?? new DefaultTransport();
|
|
562
|
+
this.dsn = parseDsn(config.dsn ?? readEnvDsn());
|
|
563
|
+
}
|
|
564
|
+
/** errorPlaneEnabled reports whether captured exceptions can actually reach the
|
|
565
|
+
* error host. False means a DSN was never configured — the documented
|
|
566
|
+
* fail-safe. Exposed so an app (or a test) can assert its wiring instead of
|
|
567
|
+
* discovering months later that nothing was ever reported. */
|
|
568
|
+
get errorPlaneEnabled() {
|
|
569
|
+
return this.dsn !== null;
|
|
570
|
+
}
|
|
571
|
+
/** errorIngestUrl is the fully-derived envelope endpoint, or undefined when the
|
|
572
|
+
* plane is inert. Diagnostics only. */
|
|
573
|
+
get errorIngestUrl() {
|
|
574
|
+
return this.dsn?.ingestUrl;
|
|
263
575
|
}
|
|
264
576
|
/** init is idempotent and browser-only for its side effects: capture first-touch
|
|
265
577
|
* attribution, hydrate cohort, register the unload flush, and (unless opted out)
|
|
@@ -310,18 +622,38 @@ var Analytics = class {
|
|
|
310
622
|
capture(event, properties, commerce) {
|
|
311
623
|
this.enqueue("event", event, { properties, ...commerce });
|
|
312
624
|
}
|
|
313
|
-
/** captureError
|
|
314
|
-
* error
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
625
|
+
/** captureError reports a caught error, an unhandled rejection, a React render
|
|
626
|
+
* error, or a manual report to BOTH planes, from one call:
|
|
627
|
+
*
|
|
628
|
+
* - the ERROR PLANE — a real Sentry envelope to the DSN host. This is the one
|
|
629
|
+
* that produces an issue in sentry.hanzo.ai (grouping, stack frames, AST).
|
|
630
|
+
* Inert when no DSN is configured.
|
|
631
|
+
* - the EVENT STREAM — a `type:'error'` row in the cloud event warehouse, so
|
|
632
|
+
* an error stays correlated with the session's pageviews for product
|
|
633
|
+
* analysis (readable via GET /v1/errors).
|
|
634
|
+
*
|
|
635
|
+
* Both carry the SAME session and subject id, so an error and the pageview
|
|
636
|
+
* before it join up. Never throws back into the app; errors are higher-signal
|
|
637
|
+
* than pageviews, so both planes flush promptly (a crash may unload the page
|
|
638
|
+
* moments later). */
|
|
320
639
|
captureError(err, context) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
640
|
+
if (this.reentrant) return;
|
|
641
|
+
this.reentrant = true;
|
|
642
|
+
try {
|
|
643
|
+
try {
|
|
644
|
+
this.sendError(err, context);
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const ex = normalizeError2(err);
|
|
649
|
+
ex.handled = context?.handled ?? true;
|
|
650
|
+
this.enqueue("error", ex.message, { error: ex, properties: context?.properties });
|
|
651
|
+
this.flush();
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
654
|
+
} finally {
|
|
655
|
+
this.reentrant = false;
|
|
656
|
+
}
|
|
325
657
|
}
|
|
326
658
|
/** setCohort persists cohort dimensions (e.g. signupWeek at signup) so they ride
|
|
327
659
|
* every subsequent event. */
|
|
@@ -347,11 +679,53 @@ var Analytics = class {
|
|
|
347
679
|
const key = this.cfg.ingestKey?.trim() || void 0;
|
|
348
680
|
const token = key ? void 0 : this.cfg.getToken?.() ?? void 0;
|
|
349
681
|
const useBeacon = beacon && !token;
|
|
350
|
-
const body =
|
|
682
|
+
const body = serializeBatch(batch);
|
|
683
|
+
if (body === null) {
|
|
684
|
+
if (this.cfg.debug) console.debug("[event] flush \u2192 dropped, batch unserializable");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
351
687
|
if (this.cfg.debug) console.debug("[event] flush \u2192", EVENT_PATH, batch.length);
|
|
352
|
-
this.transport.send(this.cfg.host + EVENT_PATH, body, {
|
|
688
|
+
this.transport.send(this.cfg.host + EVENT_PATH, body, {
|
|
689
|
+
beacon: useBeacon,
|
|
690
|
+
token,
|
|
691
|
+
ingestKey: key,
|
|
692
|
+
debug: this.cfg.debug
|
|
693
|
+
});
|
|
353
694
|
}
|
|
354
695
|
// ── internals ────────────────────────────────────────────────────────────
|
|
696
|
+
/** sendError frames one exception as a Sentry envelope and posts it to the DSN's
|
|
697
|
+
* ingest URL. The DSN's own key rides ?sentry_key= (the credential channel the
|
|
698
|
+
* server trusts, and the only one a headerless beacon can carry), so NO bearer
|
|
699
|
+
* or publishable key is attached here — the two planes authenticate
|
|
700
|
+
* independently. Errors are sent one envelope per event, immediately: batching
|
|
701
|
+
* a crash report is how you lose it. */
|
|
702
|
+
sendError(err, options) {
|
|
703
|
+
if (!this.cfg.enabled || !this.dsn) return;
|
|
704
|
+
const event = buildSentryEvent({
|
|
705
|
+
error: err,
|
|
706
|
+
options,
|
|
707
|
+
identity: this.errorIdentity(),
|
|
708
|
+
capturePII: this.cfg.capturePII ?? false
|
|
709
|
+
});
|
|
710
|
+
const body = buildEnvelope(event, this.dsn);
|
|
711
|
+
if (this.cfg.debug) console.debug("[event] error \u2192", this.dsn.ingestUrl, event.event_id);
|
|
712
|
+
this.transport.send(this.dsn.ingestUrl, body, {
|
|
713
|
+
beacon: false,
|
|
714
|
+
contentType: ENVELOPE_CONTENT_TYPE,
|
|
715
|
+
debug: this.cfg.debug
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/** errorIdentity is the SAME identity the event stream stamps — the OIDC subject
|
|
719
|
+
* once identify() has run, else the anon id. Never email/PII. */
|
|
720
|
+
errorIdentity() {
|
|
721
|
+
return {
|
|
722
|
+
userId: this.personId ?? anonId(),
|
|
723
|
+
sessionId: sessionId(),
|
|
724
|
+
product: this.cfg.product,
|
|
725
|
+
release: this.cfg.release ?? readEnv("NEXT_PUBLIC_HANZO_RELEASE"),
|
|
726
|
+
environment: this.cfg.environment ?? readEnv("NODE_ENV")
|
|
727
|
+
};
|
|
728
|
+
}
|
|
355
729
|
enqueue(kind, event, extra) {
|
|
356
730
|
if (!this.cfg.enabled) return;
|
|
357
731
|
if (!this.started) this.init();
|
|
@@ -437,13 +811,20 @@ exports.EVENTS = EVENTS;
|
|
|
437
811
|
exports.GOALS = GOALS;
|
|
438
812
|
exports.PAGEVIEW = PAGEVIEW;
|
|
439
813
|
exports.VERSION = VERSION;
|
|
814
|
+
exports.buildEnvelope = buildEnvelope;
|
|
815
|
+
exports.buildSentryEvent = buildSentryEvent;
|
|
440
816
|
exports.createAnalytics = createAnalytics;
|
|
441
817
|
exports.deriveChannel = deriveChannel;
|
|
818
|
+
exports.framesFromStack = framesFromStack;
|
|
442
819
|
exports.getCohort = getCohort;
|
|
443
820
|
exports.getFirstTouch = getFirstTouch;
|
|
444
821
|
exports.hasAttribution = hasAttribution;
|
|
445
822
|
exports.hostOf = hostOf;
|
|
446
823
|
exports.isoWeek = isoWeek;
|
|
447
824
|
exports.parseAttribution = parseAttribution;
|
|
825
|
+
exports.parseDsn = parseDsn;
|
|
826
|
+
exports.redactSecrets = redactSecrets;
|
|
827
|
+
exports.scrubPII = scrubPII;
|
|
828
|
+
exports.scrubText = scrubText;
|
|
448
829
|
//# sourceMappingURL=index.cjs.map
|
|
449
830
|
//# sourceMappingURL=index.cjs.map
|