@blaxel/core 0.3.7-preview.229 → 0.3.7-preview.230
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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/common/lazyInit.js +2 -2
- package/dist/cjs/common/sentry.js +236 -190
- package/dist/cjs/common/sentry.test.js +224 -0
- package/dist/cjs/common/settings.js +2 -2
- package/dist/cjs/types/common/lazyInit.d.ts +2 -2
- package/dist/cjs/types/common/sentry.d.ts +4 -8
- package/dist/cjs/types/common/sentry.test.d.ts +1 -0
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/common/lazyInit.js +2 -2
- package/dist/cjs-browser/common/sentry.js +236 -190
- package/dist/cjs-browser/common/sentry.test.js +224 -0
- package/dist/cjs-browser/common/settings.js +2 -2
- package/dist/cjs-browser/types/common/lazyInit.d.ts +2 -2
- package/dist/cjs-browser/types/common/sentry.d.ts +4 -8
- package/dist/cjs-browser/types/common/sentry.test.d.ts +1 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/common/lazyInit.js +2 -2
- package/dist/esm/common/sentry.js +236 -190
- package/dist/esm/common/sentry.test.js +189 -0
- package/dist/esm/common/settings.js +2 -2
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/common/lazyInit.js +2 -2
- package/dist/esm-browser/common/sentry.js +236 -190
- package/dist/esm-browser/common/sentry.test.js +189 -0
- package/dist/esm-browser/common/settings.js +2 -2
- package/package.json +1 -1
|
@@ -49,8 +49,8 @@ let autoloaded = false;
|
|
|
49
49
|
* populates `BL_ENV` if the user has a dev workspace configured).
|
|
50
50
|
* - Re-apply the control-plane and sandbox clients' `baseUrl` so they
|
|
51
51
|
* target the now-env-aware endpoint.
|
|
52
|
-
* - Initialize the lightweight Sentry client (registers
|
|
53
|
-
* `uncaughtExceptionMonitor`
|
|
52
|
+
* - Initialize the lightweight Sentry client (registers a composable
|
|
53
|
+
* `uncaughtExceptionMonitor` handler in Node).
|
|
54
54
|
* - Pre-warm the edge H2 connection pool for `settings.region`.
|
|
55
55
|
*
|
|
56
56
|
* These used to run at module load, but that meant `import "@blaxel/core"`
|
|
@@ -4,39 +4,165 @@ exports.initSentry = initSentry;
|
|
|
4
4
|
exports.flushSentry = flushSentry;
|
|
5
5
|
exports.isSentryInitialized = isSentryInitialized;
|
|
6
6
|
const settings_js_1 = require("./settings.js");
|
|
7
|
-
|
|
7
|
+
const PACKAGE_LAYOUT_MARKERS = [
|
|
8
|
+
"/src/common/sentry.ts",
|
|
9
|
+
"/dist/cjs/common/sentry.js",
|
|
10
|
+
"/dist/esm/common/sentry.js",
|
|
11
|
+
"/dist/cjs-browser/common/sentry.js",
|
|
12
|
+
"/dist/esm-browser/common/sentry.js",
|
|
13
|
+
];
|
|
14
|
+
const OWNED_PACKAGE_DIRECTORIES = [
|
|
15
|
+
"/src/",
|
|
16
|
+
"/dist/cjs/",
|
|
17
|
+
"/dist/esm/",
|
|
18
|
+
"/dist/cjs-browser/",
|
|
19
|
+
"/dist/esm-browser/",
|
|
20
|
+
];
|
|
21
|
+
const SAFE_ERROR_NAMES = new Set([
|
|
22
|
+
"Error",
|
|
23
|
+
"EvalError",
|
|
24
|
+
"RangeError",
|
|
25
|
+
"ReferenceError",
|
|
26
|
+
"SyntaxError",
|
|
27
|
+
"TypeError",
|
|
28
|
+
"URIError",
|
|
29
|
+
"AggregateError",
|
|
30
|
+
]);
|
|
31
|
+
const MAX_IN_FLIGHT_EVENTS = 20;
|
|
32
|
+
const DELIVERY_TIMEOUT_MS = 500;
|
|
33
|
+
const SAFE_FILENAME_SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
8
34
|
let sentryInitialized = false;
|
|
9
|
-
const capturedExceptions = new Set();
|
|
10
35
|
let handlersRegistered = false;
|
|
11
|
-
// Parsed DSN components
|
|
12
36
|
let sentryConfig = null;
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
];
|
|
37
|
+
let flushPromise = null;
|
|
38
|
+
const capturedExceptions = new WeakSet();
|
|
39
|
+
const inFlightDeliveries = new Set();
|
|
40
|
+
/**
|
|
41
|
+
* Normalize stack filenames without resolving or exposing host filesystem data.
|
|
42
|
+
*/
|
|
43
|
+
function normalizeFilename(filename) {
|
|
44
|
+
return filename.replace(/\\/g, "/").replace(/[?#].*$/, "");
|
|
45
|
+
}
|
|
21
46
|
/**
|
|
22
|
-
*
|
|
23
|
-
* Returns true if the stack trace contains any SDK-related paths.
|
|
47
|
+
* Parse V8/Node and browser stack-frame formats in call order (newest first).
|
|
24
48
|
*/
|
|
25
|
-
function
|
|
26
|
-
const
|
|
27
|
-
|
|
49
|
+
function parseStackTrace(stack) {
|
|
50
|
+
const frames = [];
|
|
51
|
+
// V8 starts with an error header (which does not match a frame), while
|
|
52
|
+
// Firefox-style stacks can start directly with the first frame.
|
|
53
|
+
for (const line of stack.split("\n")) {
|
|
54
|
+
const v8WithFunction = line.match(/^\s*at\s+(.+?)\s+\((.+):(\d+):(\d+)\)\s*$/);
|
|
55
|
+
const v8WithoutFunction = line.match(/^\s*at\s+(.+):(\d+):(\d+)\s*$/);
|
|
56
|
+
const browser = line.match(/^\s*(.*?)@(.+):(\d+):(\d+)\s*$/);
|
|
57
|
+
const match = v8WithFunction ?? v8WithoutFunction ?? browser;
|
|
58
|
+
if (!match)
|
|
59
|
+
continue;
|
|
60
|
+
if (match === v8WithFunction) {
|
|
61
|
+
frames.push({
|
|
62
|
+
function: match[1],
|
|
63
|
+
filename: normalizeFilename(match[2]),
|
|
64
|
+
lineno: Number.parseInt(match[3], 10),
|
|
65
|
+
colno: Number.parseInt(match[4], 10),
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (match === browser) {
|
|
70
|
+
frames.push({
|
|
71
|
+
function: match[1] || "<anonymous>",
|
|
72
|
+
filename: normalizeFilename(match[2]),
|
|
73
|
+
lineno: Number.parseInt(match[3], 10),
|
|
74
|
+
colno: Number.parseInt(match[4], 10),
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
frames.push({
|
|
79
|
+
function: "<anonymous>",
|
|
80
|
+
filename: normalizeFilename(match[1]),
|
|
81
|
+
lineno: Number.parseInt(match[2], 10),
|
|
82
|
+
colno: Number.parseInt(match[3], 10),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return frames;
|
|
28
86
|
}
|
|
29
87
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
88
|
+
* Discover this exact @blaxel/core package root from this module's own frame.
|
|
89
|
+
* This avoids trusting a generic "@blaxel" substring in an arbitrary stack.
|
|
90
|
+
*/
|
|
91
|
+
function discoverPackageRoot(stack) {
|
|
92
|
+
for (const frame of parseStackTrace(stack)) {
|
|
93
|
+
for (const marker of PACKAGE_LAYOUT_MARKERS) {
|
|
94
|
+
if (frame.filename.endsWith(marker)) {
|
|
95
|
+
return frame.filename.slice(0, -marker.length);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const sdkPackageRoot = discoverPackageRoot(new Error().stack ?? "");
|
|
102
|
+
function isRuntimeFrame(filename) {
|
|
103
|
+
return filename.startsWith("node:") || filename.startsWith("internal/");
|
|
104
|
+
}
|
|
105
|
+
function ownedRelativeFilename(filename) {
|
|
106
|
+
if (!sdkPackageRoot)
|
|
107
|
+
return null;
|
|
108
|
+
for (const directory of OWNED_PACKAGE_DIRECTORIES) {
|
|
109
|
+
const prefix = `${sdkPackageRoot}${directory}`;
|
|
110
|
+
if (!filename.startsWith(prefix))
|
|
111
|
+
continue;
|
|
112
|
+
const relativeFilename = filename.slice(sdkPackageRoot.length).replace(/^\/+/, "");
|
|
113
|
+
const segments = relativeFilename.split("/");
|
|
114
|
+
if (segments.length > 0 &&
|
|
115
|
+
segments.every((segment) => segment !== "." && segment !== ".." && SAFE_FILENAME_SEGMENT.test(segment))) {
|
|
116
|
+
return relativeFilename;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function isOwnedFilename(filename) {
|
|
122
|
+
return ownedRelativeFilename(filename) !== null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Attribute an exception only when its first non-runtime frame is inside the
|
|
126
|
+
* exact installed @blaxel/core package. Application and dependency frames are
|
|
127
|
+
* never included in the event.
|
|
128
|
+
*/
|
|
129
|
+
function getOwnedFrames(error) {
|
|
130
|
+
const frames = parseStackTrace(error.stack ?? "");
|
|
131
|
+
const firstRelevantFrame = frames.find((frame) => !isRuntimeFrame(frame.filename));
|
|
132
|
+
if (!firstRelevantFrame || !isOwnedFilename(firstRelevantFrame.filename)) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
return frames.filter((frame) => isOwnedFilename(frame.filename));
|
|
136
|
+
}
|
|
137
|
+
function sanitizeFunctionName(name) {
|
|
138
|
+
const normalized = name.trim();
|
|
139
|
+
if (!normalized || normalized.length > 120 || !/^[\w.$<> /-]+$/u.test(normalized)) {
|
|
140
|
+
return "<sdk>";
|
|
141
|
+
}
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
function sanitizeOwnedFrame(frame) {
|
|
145
|
+
const relativeFilename = ownedRelativeFilename(frame.filename) ?? "unknown";
|
|
146
|
+
return {
|
|
147
|
+
function: sanitizeFunctionName(frame.function),
|
|
148
|
+
filename: `@blaxel/core/${relativeFilename}`,
|
|
149
|
+
lineno: frame.lineno,
|
|
150
|
+
colno: frame.colno,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function sanitizeErrorName(name) {
|
|
154
|
+
return SAFE_ERROR_NAMES.has(name) ? name : "Error";
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Parse a Sentry DSN into the public transport components used by envelopes.
|
|
32
158
|
*/
|
|
33
159
|
function parseDsn(dsn) {
|
|
34
160
|
try {
|
|
35
161
|
const url = new URL(dsn);
|
|
36
162
|
const publicKey = url.username;
|
|
37
163
|
const host = url.host;
|
|
38
|
-
const projectId = url.pathname.slice(1);
|
|
39
|
-
if (!publicKey || !host || !projectId) {
|
|
164
|
+
const projectId = url.pathname.slice(1);
|
|
165
|
+
if (!publicKey || !host || !projectId || !["http:", "https:"].includes(url.protocol)) {
|
|
40
166
|
return null;
|
|
41
167
|
}
|
|
42
168
|
return { publicKey, host, projectId };
|
|
@@ -45,21 +171,18 @@ function parseDsn(dsn) {
|
|
|
45
171
|
return null;
|
|
46
172
|
}
|
|
47
173
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Generate a UUID v4
|
|
50
|
-
*/
|
|
51
174
|
function generateEventId() {
|
|
52
|
-
return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, (
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
return
|
|
175
|
+
return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, (character) => {
|
|
176
|
+
const random = (Math.random() * 16) | 0;
|
|
177
|
+
const value = character === "x" ? random : (random & 0x3) | 0x8;
|
|
178
|
+
return value.toString(16);
|
|
56
179
|
});
|
|
57
180
|
}
|
|
58
181
|
/**
|
|
59
|
-
*
|
|
182
|
+
* Build an allowlisted event. Raw messages, response bodies, workspace/resource
|
|
183
|
+
* context, and absolute host paths are deliberately excluded.
|
|
60
184
|
*/
|
|
61
|
-
function errorToSentryEvent(error) {
|
|
62
|
-
const frames = parseStackTrace(error.stack || "");
|
|
185
|
+
function errorToSentryEvent(error, ownedFrames) {
|
|
63
186
|
return {
|
|
64
187
|
event_id: generateEventId(),
|
|
65
188
|
timestamp: Date.now() / 1000,
|
|
@@ -68,64 +191,43 @@ function errorToSentryEvent(error) {
|
|
|
68
191
|
environment: settings_js_1.settings.env,
|
|
69
192
|
release: `sdk-typescript@${settings_js_1.settings.version}`,
|
|
70
193
|
tags: {
|
|
71
|
-
"blaxel.workspace": settings_js_1.settings.workspace,
|
|
72
194
|
"blaxel.version": settings_js_1.settings.version,
|
|
73
195
|
"blaxel.commit": settings_js_1.settings.commit,
|
|
196
|
+
"blaxel.error_source": "unhandled-sdk-exception",
|
|
74
197
|
},
|
|
75
198
|
exception: {
|
|
76
199
|
values: [
|
|
77
200
|
{
|
|
78
|
-
type: error.name,
|
|
79
|
-
value:
|
|
201
|
+
type: sanitizeErrorName(error.name),
|
|
202
|
+
value: "Unhandled SDK exception",
|
|
80
203
|
stacktrace: {
|
|
81
|
-
|
|
204
|
+
// Sentry expects oldest-to-newest frame order.
|
|
205
|
+
frames: ownedFrames.map(sanitizeOwnedFrame).reverse(),
|
|
82
206
|
},
|
|
83
207
|
},
|
|
84
208
|
],
|
|
85
209
|
},
|
|
86
210
|
};
|
|
87
211
|
}
|
|
88
|
-
/**
|
|
89
|
-
* Parse a stack trace string into Sentry-compatible frames.
|
|
90
|
-
*/
|
|
91
|
-
function parseStackTrace(stack) {
|
|
92
|
-
const lines = stack.split("\n").slice(1); // Skip first line (error message)
|
|
93
|
-
const frames = [];
|
|
94
|
-
for (const line of lines) {
|
|
95
|
-
// Match patterns like "at functionName (filename:line:col)" or "at filename:line:col"
|
|
96
|
-
const match = line.match(/at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?/);
|
|
97
|
-
if (match) {
|
|
98
|
-
frames.unshift({
|
|
99
|
-
function: match[1] || "<anonymous>",
|
|
100
|
-
filename: match[2],
|
|
101
|
-
lineno: parseInt(match[3], 10),
|
|
102
|
-
colno: parseInt(match[4], 10),
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return frames;
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Send an event to Sentry using fetch.
|
|
110
|
-
*/
|
|
111
212
|
async function sendToSentry(event) {
|
|
112
213
|
if (!sentryConfig)
|
|
113
214
|
return;
|
|
114
215
|
const { publicKey, host, projectId } = sentryConfig;
|
|
115
216
|
const envelopeUrl = `https://${host}/api/${projectId}/envelope/`;
|
|
116
|
-
// Create envelope header
|
|
117
217
|
const envelopeHeader = JSON.stringify({
|
|
118
218
|
event_id: event.event_id,
|
|
119
219
|
sent_at: new Date().toISOString(),
|
|
120
220
|
dsn: `https://${publicKey}@${host}/${projectId}`,
|
|
121
221
|
});
|
|
122
|
-
// Create item header
|
|
123
222
|
const itemHeader = JSON.stringify({
|
|
124
223
|
type: "event",
|
|
125
224
|
content_type: "application/json",
|
|
126
225
|
});
|
|
127
|
-
// Create envelope body
|
|
128
226
|
const envelope = `${envelopeHeader}\n${itemHeader}\n${JSON.stringify(event)}`;
|
|
227
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
228
|
+
const abortTimer = controller
|
|
229
|
+
? setTimeout(() => controller.abort(), DELIVERY_TIMEOUT_MS)
|
|
230
|
+
: null;
|
|
129
231
|
try {
|
|
130
232
|
await fetch(envelopeUrl, {
|
|
131
233
|
method: "POST",
|
|
@@ -134,170 +236,114 @@ async function sendToSentry(event) {
|
|
|
134
236
|
"X-Sentry-Auth": `Sentry sentry_version=7, sentry_client=blaxel-sdk/${settings_js_1.settings.version}, sentry_key=${publicKey}`,
|
|
135
237
|
},
|
|
136
238
|
body: envelope,
|
|
239
|
+
keepalive: true,
|
|
240
|
+
signal: controller?.signal,
|
|
137
241
|
});
|
|
138
242
|
}
|
|
139
243
|
catch {
|
|
140
|
-
//
|
|
244
|
+
// Error reporting must never affect the host application.
|
|
141
245
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
let flushPromise = null;
|
|
146
|
-
/**
|
|
147
|
-
* Register browser/edge environment error handlers.
|
|
148
|
-
* Separated to isolate dynamic globalThis access.
|
|
149
|
-
*/
|
|
150
|
-
function registerBrowserHandlers() {
|
|
151
|
-
const g = globalThis;
|
|
152
|
-
if (g && typeof g.addEventListener === "function") {
|
|
153
|
-
g.addEventListener("error", (event) => {
|
|
154
|
-
const e = event;
|
|
155
|
-
if (e.error instanceof Error && isFromSDK(e.error)) {
|
|
156
|
-
captureException(e.error);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
g.addEventListener("unhandledrejection", (event) => {
|
|
160
|
-
const e = event;
|
|
161
|
-
const error = e.reason instanceof Error ? e.reason : new Error(String(e.reason));
|
|
162
|
-
if (isFromSDK(error)) {
|
|
163
|
-
captureException(error);
|
|
164
|
-
}
|
|
165
|
-
});
|
|
246
|
+
finally {
|
|
247
|
+
if (abortTimer)
|
|
248
|
+
clearTimeout(abortTimer);
|
|
166
249
|
}
|
|
167
250
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
251
|
+
function scheduleDelivery(event) {
|
|
252
|
+
if (inFlightDeliveries.size >= MAX_IN_FLIGHT_EVENTS)
|
|
253
|
+
return;
|
|
254
|
+
// Contain rejections from setup that occurs before sendToSentry's internal
|
|
255
|
+
// fetch guard (for example, a host-provided AbortController implementation).
|
|
256
|
+
const delivery = sendToSentry(event).catch(() => undefined);
|
|
257
|
+
inFlightDeliveries.add(delivery);
|
|
258
|
+
void delivery.then(() => inFlightDeliveries.delete(delivery));
|
|
259
|
+
}
|
|
260
|
+
function captureException(error) {
|
|
261
|
+
if (!sentryInitialized || !sentryConfig || capturedExceptions.has(error)) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
172
264
|
try {
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const dsn = settings_js_1.settings.sentryDsn;
|
|
178
|
-
if (!dsn) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
// Parse DSN
|
|
182
|
-
sentryConfig = parseDsn(dsn);
|
|
183
|
-
if (!sentryConfig) {
|
|
265
|
+
const ownedFrames = getOwnedFrames(error);
|
|
266
|
+
if (ownedFrames.length === 0)
|
|
184
267
|
return;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (settings_js_1.settings.env !== "dev" && settings_js_1.settings.env !== "prod") {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
sentryInitialized = true;
|
|
191
|
-
// Register error handlers only once
|
|
192
|
-
if (!handlersRegistered) {
|
|
193
|
-
handlersRegistered = true;
|
|
194
|
-
// Node.js specific handlers
|
|
195
|
-
if (typeof process !== "undefined" && typeof process.on === "function") {
|
|
196
|
-
// Monitor uncaught exceptions to capture SDK errors without
|
|
197
|
-
// preventing Node.js default crash behavior (print + exit).
|
|
198
|
-
const uncaughtExceptionMonitorHandler = (error) => {
|
|
199
|
-
if (isFromSDK(error)) {
|
|
200
|
-
captureException(error);
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
process.on("uncaughtExceptionMonitor", uncaughtExceptionMonitorHandler);
|
|
204
|
-
// Intercept console.error to capture SDK errors that are caught and logged
|
|
205
|
-
const originalConsoleError = console.error;
|
|
206
|
-
console.error = function (...args) {
|
|
207
|
-
originalConsoleError.apply(console, args);
|
|
208
|
-
for (const arg of args) {
|
|
209
|
-
if (arg instanceof Error && isFromSDK(arg)) {
|
|
210
|
-
captureException(arg);
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
// Browser/Edge environment handlers
|
|
218
|
-
registerBrowserHandlers();
|
|
219
|
-
}
|
|
220
|
-
}
|
|
268
|
+
capturedExceptions.add(error);
|
|
269
|
+
scheduleDelivery(errorToSentryEvent(error, ownedFrames));
|
|
221
270
|
}
|
|
222
|
-
catch
|
|
223
|
-
//
|
|
224
|
-
if (settings_js_1.settings.env !== "production") {
|
|
225
|
-
console.error("[Blaxel SDK] Error initializing Sentry:", error);
|
|
226
|
-
}
|
|
271
|
+
catch {
|
|
272
|
+
// Error reporting must never affect the host application.
|
|
227
273
|
}
|
|
228
274
|
}
|
|
275
|
+
function registerBrowserHandlers() {
|
|
276
|
+
const host = globalThis;
|
|
277
|
+
if (typeof host.addEventListener !== "function")
|
|
278
|
+
return;
|
|
279
|
+
host.addEventListener.call(host, "error", (event) => {
|
|
280
|
+
const error = event.error;
|
|
281
|
+
if (error instanceof Error)
|
|
282
|
+
captureException(error);
|
|
283
|
+
});
|
|
284
|
+
host.addEventListener.call(host, "unhandledrejection", (event) => {
|
|
285
|
+
const reason = event.reason;
|
|
286
|
+
// A primitive rejection carries no attributable stack, so do not guess.
|
|
287
|
+
if (reason instanceof Error)
|
|
288
|
+
captureException(reason);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
229
291
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* @param error - The error to capture
|
|
292
|
+
* Initialize opt-in SDK error tracking. Registration composes with host global
|
|
293
|
+
* handlers and never replaces console, process, or browser callbacks.
|
|
234
294
|
*/
|
|
235
|
-
function
|
|
236
|
-
if (!sentryInitialized || !sentryConfig) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
// Double-check that error is from SDK (defense in depth)
|
|
240
|
-
if (!isFromSDK(error)) {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
295
|
+
function initSentry() {
|
|
243
296
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
297
|
+
if (!settings_js_1.settings.tracking || !settings_js_1.settings.sentryDsn || !sdkPackageRoot)
|
|
298
|
+
return;
|
|
299
|
+
const config = parseDsn(settings_js_1.settings.sentryDsn);
|
|
300
|
+
if (!config || (settings_js_1.settings.env !== "dev" && settings_js_1.settings.env !== "prod"))
|
|
301
|
+
return;
|
|
302
|
+
sentryConfig = config;
|
|
303
|
+
sentryInitialized = true;
|
|
304
|
+
if (handlersRegistered)
|
|
305
|
+
return;
|
|
306
|
+
handlersRegistered = true;
|
|
307
|
+
if (typeof process !== "undefined" &&
|
|
308
|
+
process.versions?.node &&
|
|
309
|
+
typeof process.on === "function") {
|
|
310
|
+
process.on("uncaughtExceptionMonitor", (error) => {
|
|
311
|
+
captureException(error);
|
|
312
|
+
});
|
|
247
313
|
return;
|
|
248
314
|
}
|
|
249
|
-
|
|
250
|
-
// Clean up old exception keys to prevent memory leak
|
|
251
|
-
if (capturedExceptions.size > 1000) {
|
|
252
|
-
capturedExceptions.clear();
|
|
253
|
-
}
|
|
254
|
-
// Convert error to Sentry event and queue it
|
|
255
|
-
const event = errorToSentryEvent(error);
|
|
256
|
-
pendingEvents.push(event);
|
|
257
|
-
// Send immediately (fire and forget)
|
|
258
|
-
sendToSentry(event).catch(() => {
|
|
259
|
-
// Silently fail
|
|
260
|
-
});
|
|
315
|
+
registerBrowserHandlers();
|
|
261
316
|
}
|
|
262
317
|
catch {
|
|
263
|
-
//
|
|
318
|
+
// Initialization is intentionally silent and cannot break the SDK.
|
|
264
319
|
}
|
|
265
320
|
}
|
|
266
321
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* @param timeout - Maximum time in milliseconds to wait for flush (default: 2000)
|
|
322
|
+
* Await only deliveries already in flight, bounded by the supplied timeout.
|
|
323
|
+
* Events are sent exactly once; flush never re-enqueues them.
|
|
271
324
|
*/
|
|
272
|
-
async function flushSentry(timeout =
|
|
273
|
-
if (!sentryInitialized ||
|
|
325
|
+
async function flushSentry(timeout = DELIVERY_TIMEOUT_MS) {
|
|
326
|
+
if (!sentryInitialized || inFlightDeliveries.size === 0)
|
|
274
327
|
return;
|
|
275
|
-
}
|
|
276
|
-
// If already flushing, wait for it
|
|
277
328
|
if (flushPromise) {
|
|
278
329
|
await flushPromise;
|
|
279
330
|
return;
|
|
280
331
|
}
|
|
332
|
+
let timeoutHandle;
|
|
281
333
|
try {
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
new Promise((resolve) => setTimeout(resolve, timeout)),
|
|
288
|
-
]);
|
|
334
|
+
const deliveries = Promise.allSettled([...inFlightDeliveries]).then(() => undefined);
|
|
335
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
336
|
+
timeoutHandle = setTimeout(resolve, Math.max(0, timeout));
|
|
337
|
+
});
|
|
338
|
+
flushPromise = Promise.race([deliveries, timeoutPromise]);
|
|
289
339
|
await flushPromise;
|
|
290
340
|
}
|
|
291
|
-
catch {
|
|
292
|
-
// Silently fail
|
|
293
|
-
}
|
|
294
341
|
finally {
|
|
342
|
+
if (timeoutHandle)
|
|
343
|
+
clearTimeout(timeoutHandle);
|
|
295
344
|
flushPromise = null;
|
|
296
345
|
}
|
|
297
346
|
}
|
|
298
|
-
/**
|
|
299
|
-
* Check if Sentry is initialized and available.
|
|
300
|
-
*/
|
|
301
347
|
function isSentryInitialized() {
|
|
302
348
|
return sentryInitialized;
|
|
303
349
|
}
|