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