@devkitio/faultlens 0.1.5

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.
@@ -0,0 +1,154 @@
1
+ import { createMonitorRuntime } from './chunk-LRHXSFK2.js';
2
+
3
+ // src/browser-queue.ts
4
+ var DATABASE_NAME = "faultlens";
5
+ var STORE_NAME = "events";
6
+ var MAX_BYTES = 5 * 1024 * 1024;
7
+ var MAX_ITEMS = 50;
8
+ var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
9
+ var IndexedDbEventQueue = class {
10
+ databasePromise;
11
+ static available() {
12
+ return typeof indexedDB !== "undefined";
13
+ }
14
+ database() {
15
+ this.databasePromise ??= new Promise((resolve, reject) => {
16
+ const request = indexedDB.open(DATABASE_NAME, 1);
17
+ request.onupgradeneeded = () => {
18
+ const database = request.result;
19
+ if (!database.objectStoreNames.contains(STORE_NAME)) {
20
+ const store = database.createObjectStore(STORE_NAME, { keyPath: "id" });
21
+ store.createIndex("enqueuedAt", "enqueuedAt");
22
+ }
23
+ };
24
+ request.onsuccess = () => resolve(request.result);
25
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB \u6253\u5F00\u5931\u8D25"));
26
+ });
27
+ return this.databasePromise;
28
+ }
29
+ async all() {
30
+ const database = await this.database();
31
+ return new Promise((resolve, reject) => {
32
+ const request = database.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).getAll();
33
+ request.onsuccess = () => resolve(request.result.sort((a, b) => a.enqueuedAt - b.enqueuedAt));
34
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB \u8BFB\u53D6\u5931\u8D25"));
35
+ });
36
+ }
37
+ async load(now) {
38
+ const items = await this.all();
39
+ const expired = items.filter((item) => now - item.enqueuedAt > MAX_AGE_MS).map((item) => item.id);
40
+ if (expired.length > 0) await this.remove(expired);
41
+ return items.filter((item) => now - item.enqueuedAt <= MAX_AGE_MS).slice(-MAX_ITEMS);
42
+ }
43
+ async put(item) {
44
+ const database = await this.database();
45
+ await new Promise((resolve, reject) => {
46
+ const request = database.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(item);
47
+ request.onsuccess = () => resolve();
48
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB \u5199\u5165\u5931\u8D25"));
49
+ });
50
+ const items = await this.all();
51
+ let bytes = items.reduce((total, current) => total + current.bytes, 0);
52
+ const removeIds = [];
53
+ for (const current of items) {
54
+ if (items.length - removeIds.length <= MAX_ITEMS && bytes <= MAX_BYTES) break;
55
+ removeIds.push(current.id);
56
+ bytes -= current.bytes;
57
+ }
58
+ if (removeIds.length > 0) await this.remove(removeIds);
59
+ }
60
+ async remove(ids) {
61
+ if (ids.length === 0) return;
62
+ const database = await this.database();
63
+ await new Promise((resolve, reject) => {
64
+ const transaction = database.transaction(STORE_NAME, "readwrite");
65
+ const store = transaction.objectStore(STORE_NAME);
66
+ for (const id of ids) store.delete(id);
67
+ transaction.oncomplete = () => resolve();
68
+ transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB \u5220\u9664\u5931\u8D25"));
69
+ });
70
+ }
71
+ async clear() {
72
+ const database = await this.database();
73
+ await new Promise((resolve, reject) => {
74
+ const request = database.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).clear();
75
+ request.onsuccess = () => resolve();
76
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB \u6E05\u7A7A\u5931\u8D25"));
77
+ });
78
+ }
79
+ };
80
+ var BrowserSendLease = class {
81
+ owner = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
82
+ key = "faultlens:send-lease";
83
+ static available() {
84
+ try {
85
+ return typeof localStorage !== "undefined";
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+ acquire(now, durationMs = 1e4) {
91
+ try {
92
+ const current = JSON.parse(localStorage.getItem(this.key) ?? "null");
93
+ if (current && current.owner !== this.owner && current.expiresAt > now) return false;
94
+ localStorage.setItem(this.key, JSON.stringify({ owner: this.owner, expiresAt: now + durationMs }));
95
+ const confirmed = JSON.parse(localStorage.getItem(this.key) ?? "null");
96
+ return confirmed?.owner === this.owner;
97
+ } catch {
98
+ return true;
99
+ }
100
+ }
101
+ release() {
102
+ try {
103
+ const current = JSON.parse(localStorage.getItem(this.key) ?? "null");
104
+ if (current?.owner === this.owner) localStorage.removeItem(this.key);
105
+ } catch {
106
+ }
107
+ }
108
+ };
109
+
110
+ // src/global-handlers.ts
111
+ function installGlobalHandlers(monitor, transportPath) {
112
+ if (typeof window === "undefined") return () => void 0;
113
+ const onError = (event) => {
114
+ if (event.filename?.includes(transportPath)) return;
115
+ if (event.error) {
116
+ monitor.captureException(event.error, { handled: false, mechanism: "window.error" });
117
+ return;
118
+ }
119
+ const target = event.target;
120
+ if (target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement) {
121
+ const resource = target instanceof HTMLLinkElement ? target.href : target.src;
122
+ if (!resource.includes(transportPath)) {
123
+ monitor.captureMessage("\u524D\u7AEF\u8D44\u6E90\u52A0\u8F7D\u5931\u8D25", {
124
+ handled: false,
125
+ mechanism: "resource.error",
126
+ context: { tagName: target.tagName, resource }
127
+ });
128
+ }
129
+ }
130
+ };
131
+ const onUnhandledRejection = (event) => {
132
+ monitor.captureException(event.reason, {
133
+ handled: false,
134
+ mechanism: "unhandledrejection"
135
+ });
136
+ };
137
+ window.addEventListener("error", onError, true);
138
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
139
+ return () => {
140
+ window.removeEventListener("error", onError, true);
141
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
142
+ };
143
+ }
144
+
145
+ // src/index.ts
146
+ function createErrorMonitor(options) {
147
+ return createMonitorRuntime(options, {
148
+ ...IndexedDbEventQueue.available() ? { persistentQueue: new IndexedDbEventQueue() } : {},
149
+ ...BrowserSendLease.available() ? { sendLease: new BrowserSendLease() } : {},
150
+ installGlobalHandlers
151
+ });
152
+ }
153
+
154
+ export { createErrorMonitor };
@@ -0,0 +1,535 @@
1
+ // ../security/dist/redaction.js
2
+ var REDACTED = "[\u5DF2\u8131\u654F]";
3
+ var MAX_STRING_LENGTH = 32768;
4
+ var SENSITIVE_KEY = /^(?:pass(?:word|wd)?|pwd|cookie|set[-_]?cookie|authorization|proxy[-_]?authorization|(?:access|refresh|id)?[-_]?token|api[-_]?key|secret|client[-_]?secret|private[-_]?key|card(?:number)?|pan|cvv|cvc|payment(?:data)?|oauth[-_]?(?:state|nonce)|state|nonce|registration[-_]?draft)$/i;
5
+ var DANGEROUS_OBJECT_KEY = /^(?:__proto__|constructor|prototype)$/;
6
+ var AUTH_VALUE = /^(?:Bearer|Basic|Digest)\s+\S+/i;
7
+ var PRIVATE_KEY_VALUE = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/;
8
+ var JWT_VALUE = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
9
+ var ABSOLUTE_HTTP_URL = /https?:\/\/[^\s<>"'`]+/gi;
10
+ var SENSITIVE_PATH = /(\/(?:token|secret|password|key|reset|invite|download|verify|verification|activate|activation|credential)s?\/)([^/?#\s]+)/gi;
11
+ var EMBEDDED_AUTH_VALUE = /\b((?:authorization|proxy[-_]?authorization)\s*[:=]\s*)?(?:Bearer|Basic|Digest)\s+[^\s,;"']+/gi;
12
+ var EMBEDDED_SENSITIVE_VALUE = /\b((?:(?:access|refresh|id)[-_]?token|token|api[-_]?key|secret|client[-_]?secret|password|passwd|pwd)\s*[:=]\s*)[^\s,;"']+/gi;
13
+ var EMBEDDED_JWT_VALUE = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g;
14
+ function looksLikePaymentCard(value) {
15
+ const digits = value.replace(/[\s-]/g, "");
16
+ if (!/^\d{13,19}$/.test(digits))
17
+ return false;
18
+ let sum = 0;
19
+ let alternate = false;
20
+ for (let index = digits.length - 1; index >= 0; index -= 1) {
21
+ let digit = Number(digits[index]);
22
+ if (alternate) {
23
+ digit *= 2;
24
+ if (digit > 9)
25
+ digit -= 9;
26
+ }
27
+ sum += digit;
28
+ alternate = !alternate;
29
+ }
30
+ return sum % 10 === 0;
31
+ }
32
+ function redactHttpUrl(value) {
33
+ try {
34
+ const url = new URL(value);
35
+ const segments = url.pathname.split("/");
36
+ for (let index = 0; index < segments.length - 1; index += 1) {
37
+ if (/^(?:token|secret|password|key|reset|invite|download|verify|verification|activate|activation|credential)s?$/i.test(segments[index] ?? "")) {
38
+ segments[index + 1] = REDACTED;
39
+ }
40
+ }
41
+ return `${url.origin}${segments.join("/")}`;
42
+ } catch {
43
+ return REDACTED;
44
+ }
45
+ }
46
+ function redactSensitiveString(value) {
47
+ if (AUTH_VALUE.test(value) || PRIVATE_KEY_VALUE.test(value) || JWT_VALUE.test(value) || looksLikePaymentCard(value)) {
48
+ return REDACTED;
49
+ }
50
+ return value.slice(0, MAX_STRING_LENGTH).replace(ABSOLUTE_HTTP_URL, redactHttpUrl).replace(SENSITIVE_PATH, `$1${REDACTED}`).replace(EMBEDDED_AUTH_VALUE, (_matched, prefix) => `${prefix ?? ""}${REDACTED}`).replace(EMBEDDED_SENSITIVE_VALUE, `$1${REDACTED}`).replace(EMBEDDED_JWT_VALUE, REDACTED);
51
+ }
52
+ function shouldRedactObjectKey(key) {
53
+ return SENSITIVE_KEY.test(key) || DANGEROUS_OBJECT_KEY.test(key);
54
+ }
55
+
56
+ // src/sanitize.ts
57
+ var REDACTED2 = "[\u5DF2\u8131\u654F]";
58
+ function clientRedactString(value) {
59
+ return redactSensitiveString(value);
60
+ }
61
+ function clientRedact(value) {
62
+ const seen = /* @__PURE__ */ new WeakSet();
63
+ let keys = 0;
64
+ function visit(current, depth) {
65
+ if (typeof current === "string") {
66
+ return clientRedactString(current);
67
+ }
68
+ if (current === null || typeof current !== "object") return current;
69
+ if (depth > 10 || seen.has(current)) return "[\u5DF2\u622A\u65AD]";
70
+ seen.add(current);
71
+ if (Array.isArray(current)) return current.slice(0, 100).map((item) => visit(item, depth + 1));
72
+ const result = {};
73
+ for (const [key, item] of Object.entries(current)) {
74
+ keys += 1;
75
+ if (keys > 400) {
76
+ result.__truncated__ = "[\u5DF2\u622A\u65AD]";
77
+ break;
78
+ }
79
+ Object.defineProperty(result, key, {
80
+ value: shouldRedactObjectKey(key) ? REDACTED2 : visit(item, depth + 1),
81
+ enumerable: true,
82
+ configurable: true,
83
+ writable: true
84
+ });
85
+ }
86
+ return result;
87
+ }
88
+ return visit(value, 0);
89
+ }
90
+
91
+ // src/transport.ts
92
+ function parseDirectDsn(dsn) {
93
+ const url = new URL(dsn);
94
+ const localHostname = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
95
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && localHostname)) {
96
+ throw new Error("Direct DSN \u5FC5\u987B\u4F7F\u7528 HTTPS");
97
+ }
98
+ if (!url.username || url.password || url.pathname !== "/v1/direct/envelope" || url.search || url.hash) {
99
+ throw new Error("Direct DSN \u683C\u5F0F\u65E0\u6548");
100
+ }
101
+ const publicDsn = decodeURIComponent(url.username);
102
+ url.username = "";
103
+ url.password = "";
104
+ return { endpoint: url.toString(), publicDsn };
105
+ }
106
+ function retryAfterMs(response, body) {
107
+ const value = response.headers.get("retry-after");
108
+ if (value && /^\d+$/.test(value)) return Number(value) * 1e3;
109
+ if (typeof body === "object" && body !== null && "retryAfterMs" in body && typeof body.retryAfterMs === "number") {
110
+ return Math.max(0, body.retryAfterMs);
111
+ }
112
+ return 1e3;
113
+ }
114
+ var FetchTransport = class {
115
+ constructor(ingest, fetcher = fetch) {
116
+ this.ingest = ingest;
117
+ this.fetcher = fetcher;
118
+ }
119
+ async send(envelope) {
120
+ try {
121
+ const direct = this.ingest.mode === "direct" ? parseDirectDsn(this.ingest.dsn) : void 0;
122
+ const endpoint = direct?.endpoint ?? this.ingest.endpoint;
123
+ const response = await this.fetcher(endpoint, {
124
+ method: "POST",
125
+ headers: {
126
+ "content-type": "application/json",
127
+ ...direct ? { "x-faultlens-dsn": direct.publicDsn } : {}
128
+ },
129
+ body: JSON.stringify(envelope),
130
+ credentials: "omit",
131
+ cache: "no-store",
132
+ redirect: "error",
133
+ keepalive: true
134
+ });
135
+ let body;
136
+ try {
137
+ body = await response.json();
138
+ } catch {
139
+ body = void 0;
140
+ }
141
+ if (response.status === 202) return { kind: "accepted" };
142
+ if (response.status === 429 || response.status >= 500) {
143
+ return { kind: "retryable", retryAfterMs: retryAfterMs(response, body) };
144
+ }
145
+ const code = typeof body === "object" && body !== null && "code" in body ? String(body.code) : `http_${response.status}`;
146
+ return { kind: "permanent_error", code };
147
+ } catch {
148
+ return { kind: "network_error" };
149
+ }
150
+ }
151
+ async loadRemoteConfig() {
152
+ try {
153
+ const direct = this.ingest.mode === "direct" ? parseDirectDsn(this.ingest.dsn) : void 0;
154
+ const base = direct?.endpoint ?? this.ingest.endpoint;
155
+ const endpoint = `${base.replace(/\/$/, "")}/config`;
156
+ const response = await this.fetcher(endpoint, {
157
+ method: "GET",
158
+ headers: direct ? { "x-faultlens-dsn": direct.publicDsn } : {},
159
+ credentials: "omit",
160
+ cache: "no-store",
161
+ redirect: "error"
162
+ });
163
+ if (!response.ok) return void 0;
164
+ const value = await response.json();
165
+ return {
166
+ ...typeof value.enabled === "boolean" ? { enabled: value.enabled } : {},
167
+ ...typeof value.sampleRate === "number" ? { sampleRate: value.sampleRate } : {},
168
+ ...Array.isArray(value.errorTypeAllowlist) ? { errorTypeAllowlist: value.errorTypeAllowlist.filter((item) => typeof item === "string") } : {},
169
+ ...typeof value.queueLimit === "number" ? { queueLimit: value.queueLimit } : {},
170
+ ...typeof value.retryBaseMs === "number" ? { retryBaseMs: value.retryBaseMs } : {}
171
+ };
172
+ } catch {
173
+ return void 0;
174
+ }
175
+ }
176
+ };
177
+
178
+ // src/monitor.ts
179
+ var SDK_VERSION = "0.1.5";
180
+ var PROTOCOL_VERSION = "1.0";
181
+ var REVOKED_CREDENTIAL_CODES = /* @__PURE__ */ new Set(["invalid_dsn", "relay_key_revoked", "credential_revoked"]);
182
+ function eventId() {
183
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
184
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
185
+ }
186
+ function normalizedFilename(filename) {
187
+ try {
188
+ const url = new URL(filename);
189
+ url.search = "";
190
+ url.hash = "";
191
+ return url.toString();
192
+ } catch {
193
+ return filename.split(/[?#]/, 1)[0] ?? filename;
194
+ }
195
+ }
196
+ function rawStackFrames(stack) {
197
+ return stack.split("\n").slice(1, 101).map((line) => {
198
+ const trimmed = line.trim();
199
+ const match = trimmed.match(/(?:at\s+(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
200
+ return {
201
+ filename: match?.[2] ?? trimmed.slice(0, 2048),
202
+ ...match?.[1] ? { function: match[1].slice(0, 1024) } : {},
203
+ ...match?.[3] ? { line: Number(match[3]) } : {},
204
+ ...match?.[4] ? { column: Number(match[4]) } : {},
205
+ inApp: match?.[2] ? !/node_modules|<anonymous>/.test(match[2]) : false
206
+ };
207
+ });
208
+ }
209
+ function runtimeDebugIds() {
210
+ const value = globalThis.__FAULTLENS_DEBUG_IDS__;
211
+ const result = /* @__PURE__ */ new Map();
212
+ if (!value || typeof value !== "object") return result;
213
+ for (const [stack, debugId] of Object.entries(value)) {
214
+ if (typeof debugId !== "string") continue;
215
+ const frame = rawStackFrames(stack).find((item) => item.filename);
216
+ if (frame) result.set(normalizedFilename(frame.filename), debugId);
217
+ }
218
+ return result;
219
+ }
220
+ function parseErrorStack(error) {
221
+ let stack = "";
222
+ try {
223
+ stack = typeof error.stack === "string" ? error.stack : "";
224
+ } catch {
225
+ return [];
226
+ }
227
+ const debugIds = runtimeDebugIds();
228
+ return stack ? rawStackFrames(stack).map((frame) => {
229
+ const debugId = debugIds.get(normalizedFilename(frame.filename));
230
+ return debugId ? { ...frame, debugId } : frame;
231
+ }) : [];
232
+ }
233
+ function errorShape(error) {
234
+ let isError = false;
235
+ try {
236
+ isError = error instanceof Error;
237
+ } catch {
238
+ isError = false;
239
+ }
240
+ if (isError) {
241
+ let name = "Error";
242
+ let message = "\u65E0\u6CD5\u5B89\u5168\u8BFB\u53D6\u5F02\u5E38\u5185\u5BB9";
243
+ try {
244
+ if (typeof error.name === "string" && error.name) {
245
+ name = error.name;
246
+ }
247
+ } catch {
248
+ name = "Error";
249
+ }
250
+ try {
251
+ if (typeof error.message === "string") {
252
+ message = clientRedactString(error.message);
253
+ }
254
+ } catch {
255
+ message = "\u65E0\u6CD5\u5B89\u5168\u8BFB\u53D6\u5F02\u5E38\u5185\u5BB9";
256
+ }
257
+ return {
258
+ type: name,
259
+ message,
260
+ stack: parseErrorStack(error)
261
+ };
262
+ }
263
+ if (typeof error === "string") return { type: "Error", message: clientRedactString(error), stack: [] };
264
+ return {
265
+ type: "NonErrorException",
266
+ message: "\u6355\u83B7\u5230\u975E Error \u7C7B\u578B\u5F02\u5E38",
267
+ stack: []
268
+ };
269
+ }
270
+ function shallowFingerprint(event) {
271
+ const frame = event.stack.find((item) => item.inApp) ?? event.stack[0];
272
+ return `${event.type}${event.message}${frame?.filename ?? ""}${frame?.line ?? ""}`;
273
+ }
274
+ function createMonitorRuntime(options, overrides = {}) {
275
+ const dependencies = {
276
+ transport: overrides.transport ?? new FetchTransport(options.ingest),
277
+ now: overrides.now ?? (() => /* @__PURE__ */ new Date()),
278
+ random: overrides.random ?? Math.random,
279
+ createEventId: overrides.createEventId ?? eventId
280
+ };
281
+ const queue = [];
282
+ const pending = /* @__PURE__ */ new Set();
283
+ const pendingEventIds = /* @__PURE__ */ new Set();
284
+ const breadcrumbs = [];
285
+ const namedContext = /* @__PURE__ */ new Map();
286
+ const seenErrors = /* @__PURE__ */ new WeakSet();
287
+ const recentFingerprints = /* @__PURE__ */ new Map();
288
+ const droppedReasons = {};
289
+ let queueLimit = Math.min(50, Math.max(1, options.queueLimit ?? 50));
290
+ let sampleRate = Math.min(1, Math.max(0, options.sampleRate ?? 1));
291
+ let errorTypeAllowlist;
292
+ let user = null;
293
+ let closed = false;
294
+ let sendTimer;
295
+ let recentSendStatus = "idle";
296
+ let lastRateLimitedAt = null;
297
+ let remotelyDisabled = false;
298
+ const persistentQueue = overrides.persistentQueue;
299
+ const sendLease = overrides.sendLease;
300
+ const restorePromise = persistentQueue?.load(dependencies.now().getTime()).then((items) => {
301
+ for (const item of items) {
302
+ if (!queue.some((queued) => queued.event.eventId === item.id)) {
303
+ queue.push({ event: item.event, enqueuedAt: item.enqueuedAt });
304
+ }
305
+ }
306
+ }).catch(() => drop("indexeddb_unavailable"));
307
+ void dependencies.transport.loadRemoteConfig?.().then((config) => {
308
+ if (!config) return;
309
+ if (config.enabled === false) remotelyDisabled = true;
310
+ if (typeof config.sampleRate === "number") sampleRate = Math.min(sampleRate, Math.max(0, config.sampleRate));
311
+ if (typeof config.queueLimit === "number") queueLimit = Math.min(queueLimit, Math.max(1, config.queueLimit));
312
+ if (config.errorTypeAllowlist) errorTypeAllowlist = new Set(config.errorTypeAllowlist);
313
+ }).catch(() => void 0);
314
+ function drop(reason) {
315
+ droppedReasons[reason] = (droppedReasons[reason] ?? 0) + 1;
316
+ }
317
+ function scheduleFlush() {
318
+ if (sendTimer || closed) return;
319
+ sendTimer = setTimeout(() => {
320
+ sendTimer = void 0;
321
+ void flushInternal();
322
+ }, 100);
323
+ }
324
+ async function prepare(event) {
325
+ try {
326
+ const beforeSendResult = options.beforeSend ? await options.beforeSend(event) : event;
327
+ if (!beforeSendResult) {
328
+ drop("before_send");
329
+ return;
330
+ }
331
+ const sanitized = clientRedact(beforeSendResult);
332
+ const size = new TextEncoder().encode(JSON.stringify(sanitized)).byteLength;
333
+ if (size > 128 * 1024) {
334
+ drop("payload_too_large");
335
+ return;
336
+ }
337
+ while (queue.length >= queueLimit) {
338
+ queue.shift();
339
+ drop("queue_limit");
340
+ }
341
+ queue.push({ event: sanitized, enqueuedAt: dependencies.now().getTime() });
342
+ void persistentQueue?.put({
343
+ id: sanitized.eventId,
344
+ event: sanitized,
345
+ enqueuedAt: dependencies.now().getTime(),
346
+ bytes: size
347
+ }).catch(() => drop("indexeddb_write_failed"));
348
+ scheduleFlush();
349
+ } catch {
350
+ drop("before_send_error");
351
+ }
352
+ }
353
+ function enqueue(event) {
354
+ pendingEventIds.add(event.eventId);
355
+ const promise = prepare(event).finally(() => {
356
+ pending.delete(promise);
357
+ pendingEventIds.delete(event.eventId);
358
+ });
359
+ pending.add(promise);
360
+ return event.eventId;
361
+ }
362
+ function baseEvent(shape, capture = {}) {
363
+ const context = {
364
+ ...user ? { user } : {},
365
+ contexts: Object.fromEntries(namedContext),
366
+ breadcrumbs: breadcrumbs.slice(-50),
367
+ ...capture.context === void 0 ? {} : { captured: capture.context },
368
+ contextMode: options.contextMode ?? "minimal"
369
+ };
370
+ return {
371
+ eventId: dependencies.createEventId(),
372
+ occurredAt: dependencies.now().toISOString(),
373
+ type: shape.type,
374
+ message: shape.message,
375
+ handled: capture.handled ?? false,
376
+ mechanism: capture.mechanism ?? "manual",
377
+ level: capture.level ?? "error",
378
+ stack: shape.stack,
379
+ tags: clientRedact(capture.tags ?? {}),
380
+ context: clientRedact(context)
381
+ };
382
+ }
383
+ function shouldIgnore(error, event) {
384
+ if (closed || remotelyDisabled) {
385
+ drop(closed ? "closed" : "remote_disabled");
386
+ return true;
387
+ }
388
+ if (options.ignoreError?.(error)) {
389
+ drop("ignored");
390
+ return true;
391
+ }
392
+ if (errorTypeAllowlist && !errorTypeAllowlist.has(event.type)) {
393
+ drop("remote_type_filter");
394
+ return true;
395
+ }
396
+ if (typeof error === "object" && error !== null) {
397
+ if (seenErrors.has(error)) {
398
+ drop("duplicate");
399
+ return true;
400
+ }
401
+ seenErrors.add(error);
402
+ }
403
+ const fingerprint = shallowFingerprint(event);
404
+ const seenAt = recentFingerprints.get(fingerprint);
405
+ const now = dependencies.now().getTime();
406
+ if (seenAt !== void 0 && now - seenAt < 2e3) {
407
+ drop("duplicate");
408
+ return true;
409
+ }
410
+ recentFingerprints.set(fingerprint, now);
411
+ if (dependencies.random() >= sampleRate) {
412
+ drop("sampled");
413
+ return true;
414
+ }
415
+ return false;
416
+ }
417
+ function envelope(events) {
418
+ return {
419
+ protocolVersion: PROTOCOL_VERSION,
420
+ sdkName: "@devkitio/faultlens",
421
+ sdkVersion: SDK_VERSION,
422
+ clientType: options.ingest.mode === "relay" ? "relay" : "browser",
423
+ expectedEnvironment: options.expectedEnvironment,
424
+ release: options.release,
425
+ dist: options.dist ?? "default",
426
+ buildId: options.buildId ?? options.release,
427
+ clientBatchId: dependencies.createEventId(),
428
+ events
429
+ };
430
+ }
431
+ async function flushInternal() {
432
+ await restorePromise;
433
+ await Promise.all([...pending]);
434
+ const expiredBefore = dependencies.now().getTime() - 24 * 60 * 60 * 1e3;
435
+ while (queue[0] && queue[0].enqueuedAt < expiredBefore) {
436
+ queue.shift();
437
+ drop("queue_expired");
438
+ }
439
+ if (queue.length === 0) return true;
440
+ const batch = queue.slice(0, 20);
441
+ if (sendLease && !sendLease.acquire(dependencies.now().getTime())) return false;
442
+ recentSendStatus = "sending";
443
+ const outcome = await dependencies.transport.send(envelope(batch.map((item) => item.event)));
444
+ sendLease?.release();
445
+ if (outcome.kind === "accepted" || outcome.kind === "permanent_error") {
446
+ const credentialRevoked = outcome.kind === "permanent_error" && REVOKED_CREDENTIAL_CODES.has(outcome.code);
447
+ if (credentialRevoked) {
448
+ remotelyDisabled = true;
449
+ queue.splice(0, queue.length);
450
+ void persistentQueue?.clear().catch(() => void 0);
451
+ } else {
452
+ queue.splice(0, batch.length);
453
+ void persistentQueue?.remove(batch.map((item) => item.event.eventId)).catch(() => void 0);
454
+ }
455
+ recentSendStatus = outcome.kind === "accepted" ? "accepted" : "permanent_error";
456
+ if (outcome.kind === "permanent_error") drop(outcome.code);
457
+ return queue.length === 0;
458
+ }
459
+ recentSendStatus = "retryable";
460
+ if (outcome.kind === "retryable") lastRateLimitedAt = dependencies.now().toISOString();
461
+ return false;
462
+ }
463
+ async function flush(timeout) {
464
+ if (sendTimer) {
465
+ clearTimeout(sendTimer);
466
+ sendTimer = void 0;
467
+ }
468
+ let timer;
469
+ try {
470
+ return await Promise.race([
471
+ flushInternal(),
472
+ new Promise((resolve) => {
473
+ timer = setTimeout(() => resolve(false), Math.max(0, timeout));
474
+ })
475
+ ]);
476
+ } finally {
477
+ if (timer) clearTimeout(timer);
478
+ }
479
+ }
480
+ const monitor = {
481
+ captureException(error, capture = {}) {
482
+ const event = baseEvent(errorShape(error), capture);
483
+ if (shouldIgnore(error, event)) return event.eventId;
484
+ return enqueue(event);
485
+ },
486
+ captureMessage(message, capture = {}) {
487
+ const event = baseEvent({ type: "Message", message: message.slice(0, 32768), stack: [] }, capture);
488
+ if (shouldIgnore(message, event)) return event.eventId;
489
+ return enqueue(event);
490
+ },
491
+ addBreadcrumb(breadcrumb) {
492
+ breadcrumbs.push(
493
+ clientRedact({
494
+ ...breadcrumb,
495
+ timestamp: breadcrumb.timestamp ?? dependencies.now().toISOString()
496
+ })
497
+ );
498
+ if (breadcrumbs.length > 50) breadcrumbs.shift();
499
+ },
500
+ setUser(value) {
501
+ user = value ? clientRedact(value) : null;
502
+ },
503
+ setContext(name, value) {
504
+ namedContext.set(name.slice(0, 128), clientRedact(value));
505
+ },
506
+ getStatus() {
507
+ const queuedEventIds = new Set(queue.map((item) => item.event.eventId));
508
+ return {
509
+ queueLength: (/* @__PURE__ */ new Set([...queuedEventIds, ...pendingEventIds])).size,
510
+ recentSendStatus,
511
+ droppedReasons: { ...droppedReasons },
512
+ protocolVersion: PROTOCOL_VERSION,
513
+ sdkVersion: SDK_VERSION,
514
+ lastRateLimitedAt,
515
+ remotelyDisabled
516
+ };
517
+ },
518
+ flush,
519
+ async close() {
520
+ closed = true;
521
+ if (sendTimer) clearTimeout(sendTimer);
522
+ return flush(2e3);
523
+ }
524
+ };
525
+ const transportPath = options.ingest.mode === "relay" ? options.ingest.endpoint : new URL(options.ingest.dsn).pathname;
526
+ const removeGlobalHandlers = overrides.installGlobalHandlers?.(monitor, transportPath) ?? (() => void 0);
527
+ const originalClose = monitor.close;
528
+ monitor.close = async () => {
529
+ removeGlobalHandlers();
530
+ return originalClose();
531
+ };
532
+ return monitor;
533
+ }
534
+
535
+ export { SDK_VERSION, clientRedact, createMonitorRuntime, parseErrorStack };