@blaxel/core 0.3.12-preview.255 → 0.3.12-preview.256
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/h2stats.js +3 -0
- package/dist/cjs/common/sentry.js +163 -2
- package/dist/cjs/common/sentry.test.js +165 -10
- package/dist/cjs/common/settings.js +2 -2
- package/dist/cjs/types/common/sentry.d.ts +4 -0
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/common/h2stats.js +3 -0
- package/dist/cjs-browser/common/sentry.js +163 -2
- package/dist/cjs-browser/common/sentry.test.js +165 -10
- package/dist/cjs-browser/common/settings.js +2 -2
- package/dist/cjs-browser/types/common/sentry.d.ts +4 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/common/h2stats.js +3 -0
- package/dist/esm/common/sentry.js +162 -2
- package/dist/esm/common/sentry.test.js +165 -10
- package/dist/esm/common/settings.js +2 -2
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/common/h2stats.js +3 -0
- package/dist/esm-browser/common/sentry.js +162 -2
- package/dist/esm-browser/common/sentry.test.js +165 -10
- package/dist/esm-browser/common/settings.js +2 -2
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ exports.resetH2TransportStats = resetH2TransportStats;
|
|
|
5
5
|
exports.recordH2EstablishFailure = recordH2EstablishFailure;
|
|
6
6
|
exports.recordH2Fallback = recordH2Fallback;
|
|
7
7
|
const logger_js_1 = require("./logger.js");
|
|
8
|
+
const sentry_js_1 = require("./sentry.js");
|
|
8
9
|
const emptyReasons = () => ({
|
|
9
10
|
"no-session": 0,
|
|
10
11
|
"request-rejected": 0,
|
|
@@ -54,6 +55,7 @@ class H2TransportStatsStore {
|
|
|
54
55
|
catch {
|
|
55
56
|
// Diagnostics must never change transport behavior.
|
|
56
57
|
}
|
|
58
|
+
(0, sentry_js_1.reportH2TransportDegradation)(domain, "establish-failure");
|
|
57
59
|
publishDebugSnapshot(this.snapshot());
|
|
58
60
|
}
|
|
59
61
|
/** @internal */
|
|
@@ -69,6 +71,7 @@ class H2TransportStatsStore {
|
|
|
69
71
|
catch {
|
|
70
72
|
// Diagnostics must never change transport behavior.
|
|
71
73
|
}
|
|
74
|
+
(0, sentry_js_1.reportH2TransportDegradation)(domain, reason);
|
|
72
75
|
publishDebugSnapshot(this.snapshot());
|
|
73
76
|
}
|
|
74
77
|
forDomain(domain) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reportH2TransportDegradation = reportH2TransportDegradation;
|
|
3
4
|
exports.initSentry = initSentry;
|
|
4
5
|
exports.flushSentry = flushSentry;
|
|
5
6
|
exports.isSentryInitialized = isSentryInitialized;
|
|
7
|
+
const node_js_1 = require("./node.js");
|
|
6
8
|
const settings_js_1 = require("./settings.js");
|
|
7
9
|
const PACKAGE_LAYOUT_MARKERS = [
|
|
8
10
|
"/src/common/sentry.ts",
|
|
@@ -29,7 +31,11 @@ const SAFE_ERROR_NAMES = new Set([
|
|
|
29
31
|
"AggregateError",
|
|
30
32
|
]);
|
|
31
33
|
const MAX_IN_FLIGHT_EVENTS = 20;
|
|
34
|
+
const MAX_ACTIVE_H2_ROLLUPS = 20;
|
|
35
|
+
const MAX_CACHED_DOMAIN_TAGS = 100;
|
|
36
|
+
const MAX_H2_EVENTS_PER_PROCESS = 100;
|
|
32
37
|
const DELIVERY_TIMEOUT_MS = 500;
|
|
38
|
+
const H2_ROLLUP_WINDOW_MS = 60_000;
|
|
33
39
|
const SAFE_FILENAME_SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
34
40
|
let sentryInitialized = false;
|
|
35
41
|
let handlersRegistered = false;
|
|
@@ -37,6 +43,9 @@ let sentryConfig = null;
|
|
|
37
43
|
let flushPromise = null;
|
|
38
44
|
const capturedExceptions = new WeakSet();
|
|
39
45
|
const inFlightDeliveries = new Set();
|
|
46
|
+
const domainTagCache = new Map();
|
|
47
|
+
const h2DegradationRollups = new Map();
|
|
48
|
+
let h2EventsEmitted = 0;
|
|
40
49
|
/**
|
|
41
50
|
* Normalize stack filenames without resolving or exposing host filesystem data.
|
|
42
51
|
*/
|
|
@@ -209,6 +218,91 @@ function errorToSentryEvent(error, ownedFrames) {
|
|
|
209
218
|
},
|
|
210
219
|
};
|
|
211
220
|
}
|
|
221
|
+
function runtimeTag() {
|
|
222
|
+
const host = globalThis;
|
|
223
|
+
if (host.process?.versions?.bun)
|
|
224
|
+
return `bun/${host.process.versions.bun}`;
|
|
225
|
+
if (host.Deno?.version?.deno)
|
|
226
|
+
return `deno/${host.Deno.version.deno}`;
|
|
227
|
+
if (host.process?.versions?.node)
|
|
228
|
+
return `node/${host.process.versions.node}`;
|
|
229
|
+
return "browser";
|
|
230
|
+
}
|
|
231
|
+
function normalizeDomain(domain) {
|
|
232
|
+
const normalized = domain.trim().toLowerCase().replace(/\.$/, "");
|
|
233
|
+
return normalized.length > 0 ? normalized : null;
|
|
234
|
+
}
|
|
235
|
+
function blaxelEdgeDomainTag(domain) {
|
|
236
|
+
const edgeSuffixes = [
|
|
237
|
+
{ environment: "prod", suffix: ".bl.run" },
|
|
238
|
+
{ environment: "dev", suffix: ".runv2.blaxel.dev" },
|
|
239
|
+
];
|
|
240
|
+
for (const { environment, suffix } of edgeSuffixes) {
|
|
241
|
+
if (!domain.endsWith(suffix))
|
|
242
|
+
continue;
|
|
243
|
+
const region = domain.slice(0, -suffix.length).split(".").at(-1);
|
|
244
|
+
if (region && /^[a-z0-9-]+$/.test(region)) {
|
|
245
|
+
return `blaxel-edge:${environment}:${region}`;
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
function uncachedDomainTag(domain) {
|
|
252
|
+
const stableBlaxelDomains = {
|
|
253
|
+
"api.blaxel.ai": "blaxel-api:prod",
|
|
254
|
+
"api.blaxel.dev": "blaxel-api:dev",
|
|
255
|
+
"run.blaxel.ai": "blaxel-run:prod",
|
|
256
|
+
"run.blaxel.dev": "blaxel-run:dev",
|
|
257
|
+
};
|
|
258
|
+
const stableTag = stableBlaxelDomains[domain];
|
|
259
|
+
if (stableTag)
|
|
260
|
+
return stableTag;
|
|
261
|
+
const edgeTag = blaxelEdgeDomainTag(domain);
|
|
262
|
+
if (edgeTag)
|
|
263
|
+
return edgeTag;
|
|
264
|
+
if (!node_js_1.crypto)
|
|
265
|
+
return null;
|
|
266
|
+
try {
|
|
267
|
+
return `sha256:${node_js_1.crypto.createHash("sha256").update(domain).digest("hex")}`;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function domainToTag(domain) {
|
|
274
|
+
if (domainTagCache.has(domain))
|
|
275
|
+
return domainTagCache.get(domain) ?? null;
|
|
276
|
+
const domainTag = uncachedDomainTag(domain);
|
|
277
|
+
if (domainTagCache.size >= MAX_CACHED_DOMAIN_TAGS) {
|
|
278
|
+
const oldestDomain = domainTagCache.keys().next().value;
|
|
279
|
+
if (oldestDomain !== undefined)
|
|
280
|
+
domainTagCache.delete(oldestDomain);
|
|
281
|
+
}
|
|
282
|
+
domainTagCache.set(domain, domainTag);
|
|
283
|
+
return domainTag;
|
|
284
|
+
}
|
|
285
|
+
function h2DegradationToSentryEvent(reason, domainTag, count) {
|
|
286
|
+
return {
|
|
287
|
+
event_id: generateEventId(),
|
|
288
|
+
timestamp: Date.now() / 1000,
|
|
289
|
+
platform: "javascript",
|
|
290
|
+
level: "warning",
|
|
291
|
+
environment: settings_js_1.settings.env,
|
|
292
|
+
release: `sdk-typescript@${settings_js_1.settings.version}`,
|
|
293
|
+
message: `h2 transport degradation: ${reason}`,
|
|
294
|
+
fingerprint: ["h2-degradation", reason, domainTag],
|
|
295
|
+
tags: {
|
|
296
|
+
"blaxel.version": settings_js_1.settings.version,
|
|
297
|
+
"blaxel.commit": settings_js_1.settings.commit,
|
|
298
|
+
"blaxel.error_source": "h2-transport-degradation",
|
|
299
|
+
"blaxel.runtime": runtimeTag(),
|
|
300
|
+
reason,
|
|
301
|
+
domainTag,
|
|
302
|
+
},
|
|
303
|
+
extra: { count },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
212
306
|
async function sendToSentry(event) {
|
|
213
307
|
if (!sentryConfig)
|
|
214
308
|
return;
|
|
@@ -250,12 +344,71 @@ async function sendToSentry(event) {
|
|
|
250
344
|
}
|
|
251
345
|
function scheduleDelivery(event) {
|
|
252
346
|
if (inFlightDeliveries.size >= MAX_IN_FLIGHT_EVENTS)
|
|
253
|
-
return;
|
|
347
|
+
return false;
|
|
254
348
|
// Contain rejections from setup that occurs before sendToSentry's internal
|
|
255
349
|
// fetch guard (for example, a host-provided AbortController implementation).
|
|
256
350
|
const delivery = sendToSentry(event).catch(() => undefined);
|
|
257
351
|
inFlightDeliveries.add(delivery);
|
|
258
352
|
void delivery.then(() => inFlightDeliveries.delete(delivery));
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
function emitH2DegradationRollup(key) {
|
|
356
|
+
const rollup = h2DegradationRollups.get(key);
|
|
357
|
+
if (!rollup)
|
|
358
|
+
return;
|
|
359
|
+
h2DegradationRollups.delete(key);
|
|
360
|
+
clearTimeout(rollup.timer);
|
|
361
|
+
if (h2EventsEmitted >= MAX_H2_EVENTS_PER_PROCESS)
|
|
362
|
+
return;
|
|
363
|
+
if (scheduleDelivery(h2DegradationToSentryEvent(rollup.reason, rollup.domainTag, rollup.count))) {
|
|
364
|
+
h2EventsEmitted++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function flushH2DegradationRollups() {
|
|
368
|
+
for (const key of [...h2DegradationRollups.keys()]) {
|
|
369
|
+
emitH2DegradationRollup(key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** @internal */
|
|
373
|
+
function reportH2TransportDegradation(domain, reason) {
|
|
374
|
+
if (!sentryInitialized || !sentryConfig)
|
|
375
|
+
return;
|
|
376
|
+
try {
|
|
377
|
+
if (h2EventsEmitted >= MAX_H2_EVENTS_PER_PROCESS)
|
|
378
|
+
return;
|
|
379
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
380
|
+
if (!normalizedDomain)
|
|
381
|
+
return;
|
|
382
|
+
const domainTag = domainToTag(normalizedDomain);
|
|
383
|
+
if (!domainTag)
|
|
384
|
+
return;
|
|
385
|
+
const key = `${reason}\0${domainTag}`;
|
|
386
|
+
const existing = h2DegradationRollups.get(key);
|
|
387
|
+
if (existing) {
|
|
388
|
+
existing.count++;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (h2DegradationRollups.size >= MAX_ACTIVE_H2_ROLLUPS)
|
|
392
|
+
return;
|
|
393
|
+
const timer = setTimeout(() => {
|
|
394
|
+
try {
|
|
395
|
+
emitH2DegradationRollup(key);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// Telemetry must never change transport behavior.
|
|
399
|
+
}
|
|
400
|
+
}, H2_ROLLUP_WINDOW_MS);
|
|
401
|
+
timer.unref?.();
|
|
402
|
+
h2DegradationRollups.set(key, {
|
|
403
|
+
count: 1,
|
|
404
|
+
domainTag,
|
|
405
|
+
reason,
|
|
406
|
+
timer,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
// Telemetry must never change transport behavior.
|
|
411
|
+
}
|
|
259
412
|
}
|
|
260
413
|
function captureException(error) {
|
|
261
414
|
if (!sentryInitialized || !sentryConfig || capturedExceptions.has(error)) {
|
|
@@ -323,7 +476,15 @@ function initSentry() {
|
|
|
323
476
|
* Events are sent exactly once; flush never re-enqueues them.
|
|
324
477
|
*/
|
|
325
478
|
async function flushSentry(timeout = DELIVERY_TIMEOUT_MS) {
|
|
326
|
-
if (!sentryInitialized
|
|
479
|
+
if (!sentryInitialized)
|
|
480
|
+
return;
|
|
481
|
+
try {
|
|
482
|
+
flushH2DegradationRollups();
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// Flushing telemetry must never affect application shutdown.
|
|
486
|
+
}
|
|
487
|
+
if (inFlightDeliveries.size === 0)
|
|
327
488
|
return;
|
|
328
489
|
if (flushPromise) {
|
|
329
490
|
await flushPromise;
|
|
@@ -76,12 +76,28 @@ function makeTraversalStackError() {
|
|
|
76
76
|
error.stack = ["Error: application secret", forgedFrame].join("\n");
|
|
77
77
|
return error;
|
|
78
78
|
}
|
|
79
|
+
function createFetchMock() {
|
|
80
|
+
return vitest_1.vi.fn((input, init) => {
|
|
81
|
+
void input;
|
|
82
|
+
void init;
|
|
83
|
+
return Promise.resolve({ ok: true });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
79
86
|
function eventFromFetch(fetchMock) {
|
|
80
87
|
const request = fetchMock.mock.calls[0]?.[1];
|
|
81
88
|
if (typeof request?.body !== "string")
|
|
82
89
|
throw new Error("Expected a string Sentry envelope");
|
|
83
90
|
return JSON.parse(request.body.split("\n")[2]);
|
|
84
91
|
}
|
|
92
|
+
function eventsFromFetch(fetchMock) {
|
|
93
|
+
return fetchMock.mock.calls.map((call) => {
|
|
94
|
+
const request = call[1];
|
|
95
|
+
if (typeof request?.body !== "string") {
|
|
96
|
+
throw new Error("Expected a string Sentry envelope");
|
|
97
|
+
}
|
|
98
|
+
return JSON.parse(request.body.split("\n")[2]);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
85
101
|
function emitUncaughtExceptionMonitor(error) {
|
|
86
102
|
const processWithStringEvents = process;
|
|
87
103
|
return processWithStringEvents.emit("uncaughtExceptionMonitor", error, "uncaughtException");
|
|
@@ -96,6 +112,8 @@ function emitUncaughtExceptionMonitor(error) {
|
|
|
96
112
|
originalMonitorListeners = process.listeners("uncaughtExceptionMonitor");
|
|
97
113
|
});
|
|
98
114
|
(0, vitest_1.afterEach)(() => {
|
|
115
|
+
vitest_1.vi.useRealTimers();
|
|
116
|
+
vitest_1.vi.doUnmock("./node.js");
|
|
99
117
|
vitest_1.vi.unstubAllGlobals();
|
|
100
118
|
for (const listener of process.listeners("uncaughtExceptionMonitor")) {
|
|
101
119
|
if (!originalMonitorListeners.includes(listener)) {
|
|
@@ -105,7 +123,7 @@ function emitUncaughtExceptionMonitor(error) {
|
|
|
105
123
|
vitest_1.vi.restoreAllMocks();
|
|
106
124
|
});
|
|
107
125
|
(0, vitest_1.it)("does not replace console.error or report caught errors", async () => {
|
|
108
|
-
const fetchMock =
|
|
126
|
+
const fetchMock = createFetchMock();
|
|
109
127
|
const hostConsoleError = vitest_1.vi.fn();
|
|
110
128
|
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
111
129
|
vitest_1.vi.spyOn(console, "error").mockImplementation(hostConsoleError);
|
|
@@ -118,7 +136,7 @@ function emitUncaughtExceptionMonitor(error) {
|
|
|
118
136
|
(0, vitest_1.expect)(fetchMock).not.toHaveBeenCalled();
|
|
119
137
|
});
|
|
120
138
|
(0, vitest_1.it)("composes with host handlers and reports one sanitized SDK-owned event", async () => {
|
|
121
|
-
const fetchMock =
|
|
139
|
+
const fetchMock = createFetchMock();
|
|
122
140
|
const hostMonitor = vitest_1.vi.fn();
|
|
123
141
|
const hostRejection = vitest_1.vi.fn();
|
|
124
142
|
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
@@ -136,7 +154,7 @@ function emitUncaughtExceptionMonitor(error) {
|
|
|
136
154
|
(0, vitest_1.expect)(process.listeners("unhandledRejection")).toEqual(rejectionListeners);
|
|
137
155
|
(0, vitest_1.expect)(fetchMock).toHaveBeenCalledOnce();
|
|
138
156
|
const event = eventFromFetch(fetchMock);
|
|
139
|
-
(0, vitest_1.expect)(event.exception
|
|
157
|
+
(0, vitest_1.expect)(event.exception?.values[0]).toMatchObject({
|
|
140
158
|
type: "TypeError",
|
|
141
159
|
value: "Unhandled SDK exception",
|
|
142
160
|
});
|
|
@@ -146,7 +164,7 @@ function emitUncaughtExceptionMonitor(error) {
|
|
|
146
164
|
"blaxel.error_source": "unhandled-sdk-exception",
|
|
147
165
|
});
|
|
148
166
|
(0, vitest_1.expect)(event.tags).not.toHaveProperty("blaxel.workspace");
|
|
149
|
-
const frames = event.exception
|
|
167
|
+
const frames = event.exception?.values[0]?.stacktrace.frames ?? [];
|
|
150
168
|
(0, vitest_1.expect)(frames.length).toBeGreaterThan(0);
|
|
151
169
|
for (const frame of frames) {
|
|
152
170
|
(0, vitest_1.expect)(frame.filename).toBe("@blaxel/core/src/common/sentry.test.ts");
|
|
@@ -163,7 +181,7 @@ function emitUncaughtExceptionMonitor(error) {
|
|
|
163
181
|
}
|
|
164
182
|
});
|
|
165
183
|
(0, vitest_1.it)("does not attribute an application-owned exception with a later SDK frame", async () => {
|
|
166
|
-
const fetchMock =
|
|
184
|
+
const fetchMock = createFetchMock();
|
|
167
185
|
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
168
186
|
const { initSentry } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
169
187
|
initSentry();
|
|
@@ -171,7 +189,7 @@ function emitUncaughtExceptionMonitor(error) {
|
|
|
171
189
|
(0, vitest_1.expect)(fetchMock).not.toHaveBeenCalled();
|
|
172
190
|
});
|
|
173
191
|
(0, vitest_1.it)("rejects a forged owned path containing parent traversal", async () => {
|
|
174
|
-
const fetchMock =
|
|
192
|
+
const fetchMock = createFetchMock();
|
|
175
193
|
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
176
194
|
const { initSentry } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
177
195
|
initSentry();
|
|
@@ -179,7 +197,7 @@ function emitUncaughtExceptionMonitor(error) {
|
|
|
179
197
|
(0, vitest_1.expect)(fetchMock).not.toHaveBeenCalled();
|
|
180
198
|
});
|
|
181
199
|
(0, vitest_1.it)("contains delivery setup failures without creating an unhandled rejection", async () => {
|
|
182
|
-
const fetchMock =
|
|
200
|
+
const fetchMock = createFetchMock();
|
|
183
201
|
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
184
202
|
vitest_1.vi.stubGlobal("AbortController", class FailingAbortController {
|
|
185
203
|
constructor() {
|
|
@@ -193,7 +211,7 @@ function emitUncaughtExceptionMonitor(error) {
|
|
|
193
211
|
(0, vitest_1.expect)(fetchMock).not.toHaveBeenCalled();
|
|
194
212
|
});
|
|
195
213
|
(0, vitest_1.it)("composes with browser handlers and ignores primitive rejections", async () => {
|
|
196
|
-
const fetchMock =
|
|
214
|
+
const fetchMock = createFetchMock();
|
|
197
215
|
const listeners = new Map();
|
|
198
216
|
const addEventListener = vitest_1.vi.fn((type, listener) => {
|
|
199
217
|
listeners.set(type, listener);
|
|
@@ -212,13 +230,150 @@ function emitUncaughtExceptionMonitor(error) {
|
|
|
212
230
|
(0, vitest_1.expect)(JSON.stringify(eventFromFetch(fetchMock))).not.toContain("raw rejection secret");
|
|
213
231
|
});
|
|
214
232
|
(0, vitest_1.it)("does not initialize when tracking is disabled", async () => {
|
|
215
|
-
const fetchMock =
|
|
233
|
+
const fetchMock = createFetchMock();
|
|
216
234
|
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
217
235
|
mockSettings.tracking = false;
|
|
218
|
-
const { initSentry, isSentryInitialized } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
236
|
+
const { flushSentry, initSentry, isSentryInitialized, reportH2TransportDegradation, } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
219
237
|
initSentry();
|
|
220
238
|
emitUncaughtExceptionMonitor(makeSdkError());
|
|
239
|
+
reportH2TransportDegradation("sbx-test-workspace.us-pdx-1.bl.run", "no-session");
|
|
240
|
+
await flushSentry();
|
|
221
241
|
(0, vitest_1.expect)(isSentryInitialized()).toBe(false);
|
|
222
242
|
(0, vitest_1.expect)(fetchMock).not.toHaveBeenCalled();
|
|
223
243
|
});
|
|
244
|
+
(0, vitest_1.it)("rolls a 100-fallback burst into one warning event", async () => {
|
|
245
|
+
vitest_1.vi.useFakeTimers();
|
|
246
|
+
const fetchMock = createFetchMock();
|
|
247
|
+
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
248
|
+
const { flushSentry, initSentry, reportH2TransportDegradation } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
249
|
+
initSentry();
|
|
250
|
+
const domain = "sbx-test-workspace.us-pdx-1.bl.run";
|
|
251
|
+
const domainTag = "blaxel-edge:prod:us-pdx-1";
|
|
252
|
+
for (let index = 0; index < 100; index++) {
|
|
253
|
+
reportH2TransportDegradation(domain, "no-session");
|
|
254
|
+
}
|
|
255
|
+
(0, vitest_1.expect)(fetchMock).not.toHaveBeenCalled();
|
|
256
|
+
await vitest_1.vi.advanceTimersByTimeAsync(60_000);
|
|
257
|
+
await flushSentry();
|
|
258
|
+
(0, vitest_1.expect)(fetchMock).toHaveBeenCalledOnce();
|
|
259
|
+
(0, vitest_1.expect)(eventFromFetch(fetchMock)).toMatchObject({
|
|
260
|
+
level: "warning",
|
|
261
|
+
message: "h2 transport degradation: no-session",
|
|
262
|
+
fingerprint: ["h2-degradation", "no-session", domainTag],
|
|
263
|
+
tags: {
|
|
264
|
+
"blaxel.version": "9.9.9",
|
|
265
|
+
"blaxel.commit": "abcdef0",
|
|
266
|
+
"blaxel.error_source": "h2-transport-degradation",
|
|
267
|
+
"blaxel.runtime": `node/${process.versions.node}`,
|
|
268
|
+
reason: "no-session",
|
|
269
|
+
domainTag,
|
|
270
|
+
},
|
|
271
|
+
extra: { count: 100 },
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
(0, vitest_1.it)("groups keys independently and hashes external domains", async () => {
|
|
275
|
+
const fetchMock = createFetchMock();
|
|
276
|
+
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
277
|
+
const { flushSentry, initSentry, reportH2TransportDegradation } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
278
|
+
initSentry();
|
|
279
|
+
const externalDomain = "customer.internal.example";
|
|
280
|
+
const blaxelDomain = "sbx-test-workspace.us-pdx-1.bl.run";
|
|
281
|
+
reportH2TransportDegradation(externalDomain, "no-session");
|
|
282
|
+
reportH2TransportDegradation(externalDomain, "no-session");
|
|
283
|
+
reportH2TransportDegradation(blaxelDomain, "request-rejected");
|
|
284
|
+
await flushSentry();
|
|
285
|
+
(0, vitest_1.expect)(fetchMock).toHaveBeenCalledTimes(2);
|
|
286
|
+
const events = eventsFromFetch(fetchMock);
|
|
287
|
+
const externalEvent = events.find((event) => event.tags.reason === "no-session");
|
|
288
|
+
const blaxelEvent = events.find((event) => event.tags.reason === "request-rejected");
|
|
289
|
+
(0, vitest_1.expect)(externalEvent).toMatchObject({ extra: { count: 2 } });
|
|
290
|
+
(0, vitest_1.expect)(externalEvent?.tags.domainTag).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
291
|
+
(0, vitest_1.expect)(JSON.stringify(externalEvent)).not.toContain(externalDomain);
|
|
292
|
+
(0, vitest_1.expect)(blaxelEvent).toMatchObject({
|
|
293
|
+
tags: { domainTag: "blaxel-edge:prod:us-pdx-1" },
|
|
294
|
+
extra: { count: 1 },
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
(0, vitest_1.it)("groups Blaxel edge hosts by environment and region", async () => {
|
|
298
|
+
const fetchMock = createFetchMock();
|
|
299
|
+
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
300
|
+
const { flushSentry, initSentry, reportH2TransportDegradation } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
301
|
+
initSentry();
|
|
302
|
+
reportH2TransportDegradation("sbx-first-workspace.us-pdx-1.bl.run", "no-session");
|
|
303
|
+
reportH2TransportDegradation("sbx-second-workspace.us-pdx-1.bl.run", "no-session");
|
|
304
|
+
await flushSentry();
|
|
305
|
+
(0, vitest_1.expect)(fetchMock).toHaveBeenCalledOnce();
|
|
306
|
+
(0, vitest_1.expect)(eventFromFetch(fetchMock)).toMatchObject({
|
|
307
|
+
fingerprint: [
|
|
308
|
+
"h2-degradation",
|
|
309
|
+
"no-session",
|
|
310
|
+
"blaxel-edge:prod:us-pdx-1",
|
|
311
|
+
],
|
|
312
|
+
extra: { count: 2 },
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
(0, vitest_1.it)("keeps stable Blaxel control-plane domains identifiable", async () => {
|
|
316
|
+
const fetchMock = createFetchMock();
|
|
317
|
+
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
318
|
+
const { flushSentry, initSentry, reportH2TransportDegradation } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
319
|
+
initSentry();
|
|
320
|
+
reportH2TransportDegradation("api.blaxel.ai", "no-session");
|
|
321
|
+
await flushSentry();
|
|
322
|
+
(0, vitest_1.expect)(eventFromFetch(fetchMock)).toMatchObject({
|
|
323
|
+
tags: { domainTag: "blaxel-api:prod" },
|
|
324
|
+
fingerprint: ["h2-degradation", "no-session", "blaxel-api:prod"],
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
(0, vitest_1.it)("caps active rollup keys at 20", async () => {
|
|
328
|
+
const fetchMock = createFetchMock();
|
|
329
|
+
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
330
|
+
const { flushSentry, initSentry, reportH2TransportDegradation } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
331
|
+
initSentry();
|
|
332
|
+
for (let index = 0; index < 21; index++) {
|
|
333
|
+
reportH2TransportDegradation(`sbx-test-workspace.us-test-${index}.bl.run`, "no-session");
|
|
334
|
+
}
|
|
335
|
+
await flushSentry();
|
|
336
|
+
(0, vitest_1.expect)(fetchMock).toHaveBeenCalledTimes(20);
|
|
337
|
+
});
|
|
338
|
+
(0, vitest_1.it)("caps H2 degradation events at 100 per process", async () => {
|
|
339
|
+
const fetchMock = createFetchMock();
|
|
340
|
+
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
341
|
+
const { flushSentry, initSentry, reportH2TransportDegradation } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
342
|
+
initSentry();
|
|
343
|
+
for (let index = 0; index < 101; index++) {
|
|
344
|
+
reportH2TransportDegradation("sbx-test-workspace.us-pdx-1.bl.run", "no-session");
|
|
345
|
+
await flushSentry();
|
|
346
|
+
}
|
|
347
|
+
(0, vitest_1.expect)(fetchMock).toHaveBeenCalledTimes(100);
|
|
348
|
+
});
|
|
349
|
+
(0, vitest_1.it)("fails closed when an external domain cannot be hashed", async () => {
|
|
350
|
+
const fetchMock = createFetchMock();
|
|
351
|
+
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
352
|
+
vitest_1.vi.doMock("./node.js", () => ({
|
|
353
|
+
crypto: {
|
|
354
|
+
createHash: () => {
|
|
355
|
+
throw new Error("hash unavailable");
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
}));
|
|
359
|
+
const { flushSentry, initSentry, reportH2TransportDegradation } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
360
|
+
initSentry();
|
|
361
|
+
(0, vitest_1.expect)(() => reportH2TransportDegradation("private.customer.example", "no-session")).not.toThrow();
|
|
362
|
+
await flushSentry();
|
|
363
|
+
(0, vitest_1.expect)(fetchMock).not.toHaveBeenCalled();
|
|
364
|
+
});
|
|
365
|
+
(0, vitest_1.it)("reports degradations through the H2 statistics hooks", async () => {
|
|
366
|
+
const fetchMock = createFetchMock();
|
|
367
|
+
vitest_1.vi.stubGlobal("fetch", fetchMock);
|
|
368
|
+
const { flushSentry, initSentry } = await Promise.resolve().then(() => __importStar(require("./sentry.js")));
|
|
369
|
+
initSentry();
|
|
370
|
+
const { recordH2Fallback } = await Promise.resolve().then(() => __importStar(require("./h2stats.js")));
|
|
371
|
+
recordH2Fallback("sbx-test-workspace.us-pdx-1.bl.run", "unsupported-body");
|
|
372
|
+
await flushSentry();
|
|
373
|
+
(0, vitest_1.expect)(fetchMock).toHaveBeenCalledOnce();
|
|
374
|
+
(0, vitest_1.expect)(eventFromFetch(fetchMock)).toMatchObject({
|
|
375
|
+
message: "h2 transport degradation: unsupported-body",
|
|
376
|
+
extra: { count: 1 },
|
|
377
|
+
});
|
|
378
|
+
});
|
|
224
379
|
});
|
|
@@ -30,8 +30,8 @@ function missingCredentialsMessage() {
|
|
|
30
30
|
return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
|
|
31
31
|
}
|
|
32
32
|
// Build info - these placeholders are replaced at build time by build:replace-imports
|
|
33
|
-
const BUILD_VERSION = "0.3.12-preview.
|
|
34
|
-
const BUILD_COMMIT = "
|
|
33
|
+
const BUILD_VERSION = "0.3.12-preview.256";
|
|
34
|
+
const BUILD_COMMIT = "d1beb333d98608709a5a26ace333ff0db5673694";
|
|
35
35
|
const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
|
|
36
36
|
const BLAXEL_API_VERSION = "2026-04-28";
|
|
37
37
|
// Bun < 1.3.11 never sends connection-level WINDOW_UPDATE: the pooled h2
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { H2FallbackReason } from "./h2stats.js";
|
|
2
|
+
export type H2DegradationReason = "establish-failure" | H2FallbackReason;
|
|
3
|
+
/** @internal */
|
|
4
|
+
export declare function reportH2TransportDegradation(domain: string, reason: H2DegradationReason): void;
|
|
1
5
|
/**
|
|
2
6
|
* Initialize opt-in SDK error tracking. Registration composes with host global
|
|
3
7
|
* handlers and never replaces console, process, or browser callbacks.
|