bitfab 0.11.6 → 0.12.0

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/index.cjs CHANGED
@@ -35,7 +35,7 @@ var __version__;
35
35
  var init_version_generated = __esm({
36
36
  "src/version.generated.ts"() {
37
37
  "use strict";
38
- __version__ = "0.11.6";
38
+ __version__ = "0.12.0";
39
39
  }
40
40
  });
41
41
 
@@ -290,6 +290,42 @@ var init_http = __esm({
290
290
  clearTimeout(timeoutId);
291
291
  }
292
292
  }
293
+ /**
294
+ * Fetch the span tree for a root span.
295
+ * Blocking GET request.
296
+ */
297
+ async getSpanTree(externalSpanId) {
298
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
299
+ const controller = new AbortController();
300
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
301
+ try {
302
+ const response = await fetch(url, {
303
+ method: "GET",
304
+ headers: { Authorization: `Bearer ${this.apiKey}` },
305
+ signal: controller.signal
306
+ });
307
+ if (!response.ok) {
308
+ const errorText = await response.text();
309
+ throw new BitfabError(
310
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
311
+ );
312
+ }
313
+ return await response.json();
314
+ } catch (error) {
315
+ if (error instanceof BitfabError) {
316
+ throw error;
317
+ }
318
+ if (error instanceof Error) {
319
+ if (error.name === "AbortError") {
320
+ throw new BitfabError("Request timed out after 30000ms");
321
+ }
322
+ throw new BitfabError(error.message);
323
+ }
324
+ throw new BitfabError("Unknown error occurred");
325
+ } finally {
326
+ clearTimeout(timeoutId);
327
+ }
328
+ }
293
329
  /**
294
330
  * Mark a replay test run as completed.
295
331
  * Blocking call.
@@ -424,11 +460,39 @@ function deserializeOutput(spanData) {
424
460
  }
425
461
  return rawOutput;
426
462
  }
427
- async function processItem(httpClient, serverItem, fn, testRunId) {
463
+ function buildMockTree(rootNode) {
464
+ const spans = /* @__PURE__ */ new Map();
465
+ const counters = /* @__PURE__ */ new Map();
466
+ function walk(node) {
467
+ const key = node.traceFunctionKey;
468
+ if (key) {
469
+ const index = counters.get(key) ?? 0;
470
+ counters.set(key, index + 1);
471
+ spans.set(`${key}:${index}`, {
472
+ sourceSpanId: node.sourceSpanId,
473
+ output: node.output,
474
+ outputMeta: node.outputMeta
475
+ });
476
+ }
477
+ for (const child of node.children) {
478
+ walk(child);
479
+ }
480
+ }
481
+ for (const child of rootNode.children) {
482
+ walk(child);
483
+ }
484
+ return { spans };
485
+ }
486
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy) {
428
487
  const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
429
488
  const spanData = span.rawData?.span_data ?? {};
430
489
  const inputs = deserializeInputs(spanData);
431
490
  const originalOutput = deserializeOutput(spanData);
491
+ let mockTree;
492
+ if (mockStrategy === "all" || mockStrategy === "marked") {
493
+ const treeResponse = await httpClient.getSpanTree(serverItem.externalSpanId);
494
+ mockTree = buildMockTree(treeResponse.root);
495
+ }
432
496
  let result;
433
497
  let error = null;
434
498
  try {
@@ -436,7 +500,10 @@ async function processItem(httpClient, serverItem, fn, testRunId) {
436
500
  {
437
501
  testRunId,
438
502
  inputSourceSpanId: span.id,
439
- inputSourceTraceId: span.externalTraceId
503
+ inputSourceTraceId: span.externalTraceId,
504
+ mockTree,
505
+ callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
506
+ mockStrategy
440
507
  },
441
508
  () => fn(...inputs)
442
509
  );
@@ -483,9 +550,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
483
550
  options?.codeChangeDescription,
484
551
  options?.codeChangeFiles
485
552
  );
553
+ const mockStrategy = options?.mock ?? "none";
486
554
  const maxConcurrency = options?.maxConcurrency ?? 10;
487
555
  const tasks = serverItems.map(
488
- (serverItem) => () => processItem(httpClient, serverItem, fn, testRunId)
556
+ (serverItem) => () => processItem(httpClient, serverItem, fn, testRunId, mockStrategy)
489
557
  );
490
558
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
491
559
  await flushTraces();
@@ -2625,6 +2693,28 @@ var Bitfab = class {
2625
2693
  } catch {
2626
2694
  }
2627
2695
  };
2696
+ const replayCtxForMock = getReplayContext();
2697
+ if (replayCtxForMock?.mockTree && !isRootSpan) {
2698
+ const counters = replayCtxForMock.callCounters;
2699
+ const callIndex = counters.get(traceFunctionKey) ?? 0;
2700
+ counters.set(traceFunctionKey, callIndex + 1);
2701
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
2702
+ if (shouldMock) {
2703
+ const mockKey = `${traceFunctionKey}:${callIndex}`;
2704
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
2705
+ if (mockSpan) {
2706
+ let output = mockSpan.output;
2707
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
2708
+ output = deserializeValue({
2709
+ json: mockSpan.output,
2710
+ meta: mockSpan.outputMeta
2711
+ });
2712
+ }
2713
+ void sendSpan({ result: output });
2714
+ return output;
2715
+ }
2716
+ }
2717
+ }
2628
2718
  const executeWithContext = () => {
2629
2719
  const result = fn(...args);
2630
2720
  if (result instanceof Promise) {