@blaxel/core 0.3.12-preview.254 → 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.
@@ -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 = vi.fn().mockResolvedValue({ ok: true });
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 = vi.fn().mockResolvedValue({ ok: true });
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.values[0]).toMatchObject({
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.values[0].stacktrace.frames;
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 = vi.fn().mockResolvedValue({ ok: true });
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 = vi.fn().mockResolvedValue({ ok: true });
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 = vi.fn().mockResolvedValue({ ok: true });
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 = vi.fn().mockResolvedValue({ ok: true });
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 = vi.fn().mockResolvedValue({ ok: true });
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.254";
28
- const BUILD_COMMIT = "c57f438aac364b3b4137e7e4ec37d446fec85006";
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