bitfab 0.28.11 → 0.29.1

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/node.cjs CHANGED
@@ -298,6 +298,22 @@ var init_randomUuid = __esm({
298
298
  }
299
299
  });
300
300
 
301
+ // src/mockOverride.ts
302
+ function resolveMockValue(value, ctx) {
303
+ return typeof value === "function" ? value(ctx) : value;
304
+ }
305
+ function normalizeMockOverrides(mockOverride) {
306
+ if (mockOverride === void 0) {
307
+ return [];
308
+ }
309
+ return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
310
+ }
311
+ var init_mockOverride = __esm({
312
+ "src/mockOverride.ts"() {
313
+ "use strict";
314
+ }
315
+ });
316
+
301
317
  // src/replayContext.ts
302
318
  function getReplayContext() {
303
319
  return replayContextStorage?.getStore() ?? null;
@@ -373,6 +389,7 @@ function buildMockTree(rootNode) {
373
389
  counters.set(counterKey, index + 1);
374
390
  spans.set(`${counterKey}:${index}`, {
375
391
  sourceSpanId: node.sourceSpanId,
392
+ externalSpanId: node.externalSpanId,
376
393
  output: node.output,
377
394
  outputMeta: node.outputMeta
378
395
  });
@@ -386,57 +403,79 @@ function buildMockTree(rootNode) {
386
403
  }
387
404
  return { spans };
388
405
  }
389
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment, adaptInputs) {
406
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, environment, adaptInputs) {
390
407
  const lease = environment ? serverItem.dbBranchLease : void 0;
391
408
  let inputs = [];
392
409
  let originalOutput;
393
410
  let result;
394
411
  let error = null;
395
- const replayedTraceId = randomUuid();
396
412
  const pendingPersistence = [];
413
+ const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
414
+ const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
397
415
  try {
398
- const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
416
+ const span = await httpClient.getExternalSpan(originalSpanId);
399
417
  const spanData = span.rawData?.span_data ?? {};
400
418
  inputs = deserializeInputs(spanData);
401
419
  originalOutput = deserializeOutput(spanData);
402
420
  if (adaptInputs) {
403
421
  inputs = adaptInputs(inputs, {
404
- traceId: serverItem.traceId,
405
- sourceSpanId: serverItem.externalSpanId
422
+ originalTraceId,
423
+ originalSpanId,
424
+ // Deprecated aliases for originalTraceId/originalSpanId.
425
+ sourceTraceId: originalTraceId,
426
+ sourceSpanId: originalSpanId
406
427
  });
407
428
  }
429
+ const hasOverrides = resolvedOverrides.length > 0;
430
+ const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
431
+ const includeOutputs = mockStrategy === "all";
408
432
  let mockTree;
409
- if (mockStrategy === "all" || mockStrategy === "marked") {
433
+ if (needTree) {
410
434
  try {
411
- const treeResponse = await httpClient.getSpanTree(
412
- serverItem.externalSpanId
413
- );
435
+ const treeResponse = await httpClient.getSpanTree(originalSpanId, {
436
+ includeOutputs
437
+ });
414
438
  if (treeResponse.root) {
415
439
  mockTree = buildMockTree(treeResponse.root);
416
- } else if (mockStrategy === "all") {
440
+ } else if (mockStrategy === "all" || hasOverrides) {
417
441
  throw new BitfabError(
418
- `Replay mock strategy "all" requires a span tree root for source span ${serverItem.externalSpanId}.`
442
+ `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for original span ${originalSpanId}.`
419
443
  );
420
444
  } else {
421
445
  mockTree = void 0;
422
446
  }
423
447
  } catch (e) {
424
- if (mockStrategy === "all") {
448
+ if (mockStrategy === "all" || hasOverrides) {
425
449
  throw e;
426
450
  }
427
451
  mockTree = void 0;
428
452
  }
429
453
  }
454
+ const outputCache = /* @__PURE__ */ new Map();
455
+ const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
456
+ let pending = outputCache.get(externalSpanId);
457
+ if (!pending) {
458
+ pending = httpClient.getExternalSpan(externalSpanId).then(
459
+ (s) => deserializeOutput(
460
+ s.rawData?.span_data ?? {}
461
+ )
462
+ );
463
+ outputCache.set(externalSpanId, pending);
464
+ }
465
+ return pending;
466
+ } : void 0;
430
467
  const maybePromise = runWithReplayContext(
431
468
  {
432
469
  testRunId,
433
470
  traceId: replayedTraceId,
434
471
  inputSourceSpanId: span.id,
435
472
  inputSourceTraceId: span.externalTraceId,
436
- sourceBitfabTraceId: serverItem.traceId,
473
+ sourceBitfabTraceId: originalTraceId,
437
474
  mockTree,
438
475
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
439
476
  mockStrategy,
477
+ mockOverrides: hasOverrides ? resolvedOverrides : void 0,
478
+ fetchSpanOutput,
440
479
  dbBranchLease: lease,
441
480
  pendingPersistence
442
481
  },
@@ -461,7 +500,15 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
461
500
  }
462
501
  }
463
502
  return {
464
- traceId: replayedTraceId,
503
+ // Written in by replay() from the complete-replay response once the server
504
+ // has minted this replay trace's row. Null until then: the client-side
505
+ // correlation id (replayedTraceId) is never surfaced as the item's traceId.
506
+ traceId: null,
507
+ originalTraceId,
508
+ originalSpanId,
509
+ // Deprecated aliases for originalTraceId/originalSpanId.
510
+ sourceTraceId: originalTraceId,
511
+ sourceSpanId: originalSpanId,
465
512
  input: inputs,
466
513
  result,
467
514
  originalOutput,
@@ -493,7 +540,7 @@ async function mapWithConcurrency(tasks, maxConcurrency, onSettled) {
493
540
  await Promise.all(workers);
494
541
  return results;
495
542
  }
496
- async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
543
+ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
497
544
  if (options?.traceIds !== void 0) {
498
545
  if (options.traceIds.length === 0) {
499
546
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -533,13 +580,20 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
533
580
  );
534
581
  const mockStrategy = options?.mock ?? "marked";
535
582
  const maxConcurrency = options?.maxConcurrency ?? 10;
583
+ const resolvedOverrides = [
584
+ ...normalizeMockOverrides(options?.mockOverride),
585
+ ...registeredOverrides
586
+ ];
587
+ const replayedTraceIds = serverItems.map(() => randomUuid());
536
588
  const tasks = serverItems.map(
537
- (serverItem) => () => processItem(
589
+ (serverItem, index) => () => processItem(
538
590
  httpClient,
539
591
  serverItem,
540
592
  fn,
541
593
  testRunId,
542
594
  mockStrategy,
595
+ resolvedOverrides,
596
+ replayedTraceIds[index],
543
597
  options?.environment,
544
598
  options?.adaptInputs
545
599
  )
@@ -566,11 +620,17 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
566
620
  succeeded,
567
621
  errored,
568
622
  item: {
569
- // Source (historical) trace id, so a UI can identify the trace
570
- // that just settled. The item's own traceId is the new replay
571
- // trace and is assigned later (below), so use the server item.
572
- traceId: serverItems[index]?.traceId ?? null,
573
- replayTraceId: item.traceId,
623
+ // The server replay trace id isn't known until completeReplay
624
+ // runs (below), so it can't be reported mid-run and we never
625
+ // emit the client-side placeholder. originalTraceId (the
626
+ // historical trace) is known now and is what a UI keys on to
627
+ // identify what just settled.
628
+ traceId: null,
629
+ originalTraceId: item.originalTraceId ?? null,
630
+ originalSpanId: item.originalSpanId ?? null,
631
+ // Deprecated aliases for originalTraceId/originalSpanId.
632
+ sourceTraceId: item.originalTraceId ?? null,
633
+ sourceSpanId: item.originalSpanId ?? null,
574
634
  input: item.input,
575
635
  result: item.result,
576
636
  originalOutput: item.originalOutput,
@@ -588,56 +648,58 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
588
648
  const completeResult = await httpClient.completeReplay(testRunId);
589
649
  const serverTraceIds = completeResult.traceIds;
590
650
  const replayTokens = completeResult.tokens;
591
- if (serverTraceIds === void 0) {
592
- try {
593
- console.warn(
594
- "Bitfab: server did not return replay trace IDs; item.traceId will be null (server upgrade required for verdict persistence)"
595
- );
596
- } catch {
597
- }
598
- for (const item of resultItems) {
599
- item.traceId = null;
600
- }
601
- } else {
651
+ if (serverTraceIds !== void 0) {
602
652
  const missing = [];
603
653
  let completedCount = 0;
604
- for (const item of resultItems) {
605
- if (item.traceId) {
606
- const mapped = serverTraceIds[item.traceId];
607
- if (item.error === null) {
608
- completedCount += 1;
609
- if (mapped === void 0) {
610
- missing.push(item.traceId);
611
- }
612
- }
613
- if (mapped !== void 0) {
614
- item.tokens = replayTokens?.[mapped] ?? null;
654
+ for (let index = 0; index < resultItems.length; index += 1) {
655
+ const item = resultItems[index];
656
+ const localId = replayedTraceIds[index];
657
+ const mapped = localId ? serverTraceIds[localId] : void 0;
658
+ item.traceId = mapped ?? null;
659
+ if (item.error === null) {
660
+ completedCount += 1;
661
+ if (mapped === void 0) {
662
+ missing.push(localId ?? item.originalTraceId);
615
663
  }
616
- item.traceId = mapped ?? null;
664
+ }
665
+ if (mapped !== void 0) {
666
+ item.tokens = replayTokens?.[mapped] ?? null;
617
667
  }
618
668
  }
619
- if (missing.length > 0) {
669
+ if (completedCount > 0 && missing.length === completedCount) {
620
670
  const serverCount = completeResult.traceCount !== void 0 ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.` : "";
621
- if (missing.length === completedCount) {
622
- throw new BitfabError(
623
- `Replay completed but the server has no persisted trace for any of the ${completedCount} completed item(s) (testRunId ${testRunId}).${serverCount} Trace uploads were awaited, so either the uploads failed (check for "Bitfab: Failed to create" errors above) or the replayed function is not wrapped with withSpan.`
624
- );
625
- }
671
+ throw new BitfabError(
672
+ `Replay completed but the server has no persisted trace for any of the ${completedCount} completed item(s) (testRunId ${testRunId}).${serverCount} Trace uploads were awaited, so either the uploads failed (check for "Bitfab: Failed to create" errors above) or the replayed function is not wrapped with withSpan.`
673
+ );
674
+ }
675
+ if (missing.length > 0) {
626
676
  try {
627
677
  console.error(
628
- `Bitfab: server has no persisted trace for ${missing.length} of ${completedCount} completed replay item(s) (testRunId ${testRunId}).${serverCount} Their traceId is null and verdicts cannot be persisted for them. Missing: ${missing.join(", ")}`
678
+ `Bitfab: server has no persisted trace for ${missing.length} of ${completedCount} completed replay item(s) (testRunId ${testRunId}). Their replay token usage is unavailable and they cannot be labeled.`
629
679
  );
630
680
  } catch {
631
681
  }
632
682
  }
633
683
  }
634
- const replayResult = {
684
+ const result = {
635
685
  items: resultItems,
636
686
  testRunId,
637
687
  testRunUrl: `${serviceUrl}${testRunUrl}`
638
688
  };
639
- await writeReplayResultFile(replayResult);
640
- return replayResult;
689
+ await writeReplayResultFile(result);
690
+ try {
691
+ options?.onProgress?.({
692
+ type: "complete",
693
+ testRunId,
694
+ completed: total,
695
+ total,
696
+ succeeded,
697
+ errored,
698
+ result
699
+ });
700
+ } catch {
701
+ }
702
+ return result;
641
703
  }
642
704
  async function writeReplayResultFile(result) {
643
705
  const resultPath = typeof process !== "undefined" ? process.env?.BITFAB_REPLAY_RESULT_PATH : void 0;
@@ -666,6 +728,7 @@ var init_replay = __esm({
666
728
  "src/replay.ts"() {
667
729
  "use strict";
668
730
  init_errors();
731
+ init_mockOverride();
669
732
  init_randomUuid();
670
733
  init_replayContext();
671
734
  init_serialize();
@@ -706,7 +769,7 @@ registerAsyncLocalStorageClass(
706
769
  );
707
770
 
708
771
  // src/version.generated.ts
709
- var __version__ = "0.28.11";
772
+ var __version__ = "0.29.1";
710
773
 
711
774
  // src/constants.ts
712
775
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
@@ -1135,9 +1198,14 @@ var HttpClient = class {
1135
1198
  /**
1136
1199
  * Fetch the span tree for a root span.
1137
1200
  * Blocking GET request.
1201
+ *
1202
+ * Pass `includeOutputs: false` for a payload-free tree (structure +
1203
+ * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
1204
+ * span instead of all up front. Omit it (default eager) for `mock: "all"`.
1138
1205
  */
1139
- async getSpanTree(externalSpanId) {
1140
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
1206
+ async getSpanTree(externalSpanId, options) {
1207
+ const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
1208
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
1141
1209
  const controller = new AbortController();
1142
1210
  const timeoutId = setTimeout(() => controller.abort(), 3e4);
1143
1211
  try {
@@ -2723,6 +2791,9 @@ var BitfabLangGraphCallbackHandler = class {
2723
2791
  }
2724
2792
  };
2725
2793
 
2794
+ // src/client.ts
2795
+ init_mockOverride();
2796
+
2726
2797
  // src/openaiAgentSdk.ts
2727
2798
  var BitfabOpenAIAgentHandler = class {
2728
2799
  constructor(config) {
@@ -3538,6 +3609,12 @@ var Bitfab = class {
3538
3609
  constructor(config) {
3539
3610
  /** Gate the empty-key warning to fire at most once. */
3540
3611
  this.apiKeyWarned = false;
3612
+ /**
3613
+ * Mock overrides registered via {@link Bitfab.registerMockOverride}, applied
3614
+ * to every `replay` on this client (after any per-call `mockOverride`). In
3615
+ * registration order; first matcher wins within this list.
3616
+ */
3617
+ this.mockOverrides = [];
3541
3618
  this.apiKeyConfig = config.apiKey;
3542
3619
  this.serviceUrl = config.serviceUrl ?? DEFAULT_SERVICE_URL;
3543
3620
  this.timeout = config.timeout ?? 12e4;
@@ -4122,7 +4199,7 @@ var Bitfab = class {
4122
4199
  dbSnapshotUsage: {
4123
4200
  neonBranchId: replayCtx.dbBranchLease.neonBranchId,
4124
4201
  snapshotTimestamp: replayCtx.dbBranchLease.snapshotTimestamp,
4125
- sourceTraceId: replayCtx.sourceBitfabTraceId,
4202
+ originalTraceId: replayCtx.sourceBitfabTraceId,
4126
4203
  accessed: replayCtx.dbSnapshotAccessed === true
4127
4204
  }
4128
4205
  }
@@ -4150,24 +4227,77 @@ var Bitfab = class {
4150
4227
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
4151
4228
  const callIndex = counters.get(counterKey) ?? 0;
4152
4229
  counters.set(counterKey, callIndex + 1);
4153
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4154
- if (shouldMock) {
4155
- const mockKey = `${counterKey}:${callIndex}`;
4156
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4157
- if (mockSpan) {
4158
- let output = mockSpan.output;
4159
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4160
- output = deserializeValue({
4161
- json: mockSpan.output,
4162
- meta: mockSpan.outputMeta
4163
- });
4164
- }
4230
+ const mockKey = `${counterKey}:${callIndex}`;
4231
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4232
+ const emitMock = (output) => {
4233
+ void sendSpan({ result: output, mocked: true });
4234
+ if (fnReturnsPromise) {
4235
+ return Promise.resolve(output);
4236
+ }
4237
+ return output;
4238
+ };
4239
+ const emitMockAsync = (pending) => {
4240
+ if (!fnReturnsPromise) {
4241
+ throw new BitfabError(
4242
+ `Cannot mock synchronous span "${traceFunctionKey}" with an asynchronously-resolved value (lazy recorded-output fetch or an async value function). Make the wrapped function async, or use mock: "all" so recorded outputs are fetched eagerly.`
4243
+ );
4244
+ }
4245
+ return (async () => {
4246
+ const output = await pending;
4165
4247
  void sendSpan({ result: output, mocked: true });
4166
- if (fnReturnsPromise) {
4167
- return Promise.resolve(output);
4168
- }
4169
4248
  return output;
4249
+ })();
4250
+ };
4251
+ const resolveRecordedOutput = () => {
4252
+ const hasInlineOutput = mockSpan?.output !== void 0 || mockSpan?.outputMeta !== void 0;
4253
+ if (!hasInlineOutput && replayCtxForMock.fetchSpanOutput && mockSpan?.externalSpanId) {
4254
+ return replayCtxForMock.fetchSpanOutput(mockSpan.externalSpanId);
4255
+ }
4256
+ if (!mockSpan) {
4257
+ return Promise.reject(
4258
+ new BitfabError(
4259
+ `No recorded span to source output for "${traceFunctionKey}".`
4260
+ )
4261
+ );
4170
4262
  }
4263
+ let output = mockSpan.output;
4264
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4265
+ output = deserializeValue({
4266
+ json: mockSpan.output,
4267
+ meta: mockSpan.outputMeta
4268
+ });
4269
+ }
4270
+ return output;
4271
+ };
4272
+ if (replayCtxForMock.mockOverrides?.length) {
4273
+ const nodeMeta = {
4274
+ traceFunctionKey,
4275
+ spanName: baseSpanParams.spanName,
4276
+ type: options.type ?? "custom",
4277
+ originalSpanId: mockSpan?.sourceSpanId
4278
+ };
4279
+ const override = replayCtxForMock.mockOverrides.find(
4280
+ (o) => o.match(nodeMeta)
4281
+ );
4282
+ if (override) {
4283
+ const injected = resolveMockValue(override.value, {
4284
+ node: nodeMeta,
4285
+ inputs: args,
4286
+ getOriginalOutput: () => Promise.resolve(resolveRecordedOutput())
4287
+ });
4288
+ if (injected instanceof Promise) {
4289
+ return emitMockAsync(injected);
4290
+ }
4291
+ return emitMock(injected);
4292
+ }
4293
+ }
4294
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4295
+ if (shouldMock && mockSpan) {
4296
+ const recorded = resolveRecordedOutput();
4297
+ if (recorded instanceof Promise) {
4298
+ return emitMockAsync(recorded);
4299
+ }
4300
+ return emitMock(recorded);
4171
4301
  }
4172
4302
  }
4173
4303
  const recordSpan = (result) => {
@@ -4373,8 +4503,11 @@ var Bitfab = class {
4373
4503
  ...params.dbSnapshotUsage.snapshotTimestamp && {
4374
4504
  snapshot_timestamp: params.dbSnapshotUsage.snapshotTimestamp
4375
4505
  },
4376
- ...params.dbSnapshotUsage.sourceTraceId && {
4377
- source_trace_id: params.dbSnapshotUsage.sourceTraceId
4506
+ ...params.dbSnapshotUsage.originalTraceId && {
4507
+ original_trace_id: params.dbSnapshotUsage.originalTraceId,
4508
+ // Deprecated wire alias, kept so this SDK still reports usage
4509
+ // against servers that predate the rename.
4510
+ source_trace_id: params.dbSnapshotUsage.originalTraceId
4378
4511
  },
4379
4512
  accessed: params.dbSnapshotUsage.accessed
4380
4513
  };
@@ -4447,26 +4580,14 @@ var Bitfab = class {
4447
4580
  ...params.mocked && { mocked: true }
4448
4581
  });
4449
4582
  }
4450
- /**
4451
- * Replay historical traces through a function and create a test run.
4452
- *
4453
- * Fetches the last N traces for the given trace function key, re-runs each
4454
- * through the provided function, and returns comparison data.
4455
- *
4456
- * Accepts either a `withSpan`-wrapped function (under the same key) or any
4457
- * plain callable: plain callables are wrapped internally so each replayed
4458
- * invocation records a trace tied to the test run. The plain-callable form
4459
- * is how handler-instrumented workflows (LangGraph/LangChain, Claude Agent
4460
- * SDK) replay - those record traces under a key with no `withSpan`-wrapped
4461
- * root in the app.
4462
- *
4463
- * @param traceFunctionKey - The trace function key to replay
4464
- * @param fn - The function to run recorded inputs through
4465
- * @param options - Optional replay options. When `traceIds` is passed,
4466
- * `limit` is ignored (with a warning): an explicit ID list already
4467
- * determines how many traces replay.
4468
- * @returns ReplayResult with items, testRunId, and testRunUrl
4469
- */
4583
+ registerMockOverride(overrideOrMatch, value) {
4584
+ const override = typeof overrideOrMatch === "function" ? { match: overrideOrMatch, value } : overrideOrMatch;
4585
+ this.mockOverrides.push(override);
4586
+ }
4587
+ /** Remove all overrides registered via {@link registerMockOverride}. */
4588
+ clearMockOverrides() {
4589
+ this.mockOverrides.length = 0;
4590
+ }
4470
4591
  async replay(traceFunctionKey, fn, options) {
4471
4592
  const wrappedKey = fn._bitfabTraceFunctionKey;
4472
4593
  let replayFn = fn;
@@ -4487,7 +4608,8 @@ var Bitfab = class {
4487
4608
  this.serviceUrl,
4488
4609
  traceFunctionKey,
4489
4610
  replayFn,
4490
- options
4611
+ options,
4612
+ this.mockOverrides
4491
4613
  );
4492
4614
  }
4493
4615
  };