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