@bitfab/sdk 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
@@ -289,6 +289,22 @@ var init_randomUuid = __esm({
289
289
  }
290
290
  });
291
291
 
292
+ // src/mockOverride.ts
293
+ function resolveMockValue(value, ctx) {
294
+ return typeof value === "function" ? value(ctx) : value;
295
+ }
296
+ function normalizeMockOverrides(mockOverride) {
297
+ if (mockOverride === void 0) {
298
+ return [];
299
+ }
300
+ return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
301
+ }
302
+ var init_mockOverride = __esm({
303
+ "src/mockOverride.ts"() {
304
+ "use strict";
305
+ }
306
+ });
307
+
292
308
  // src/replayContext.ts
293
309
  function getReplayContext() {
294
310
  return replayContextStorage?.getStore() ?? null;
@@ -364,6 +380,7 @@ function buildMockTree(rootNode) {
364
380
  counters.set(counterKey, index + 1);
365
381
  spans.set(`${counterKey}:${index}`, {
366
382
  sourceSpanId: node.sourceSpanId,
383
+ externalSpanId: node.externalSpanId,
367
384
  output: node.output,
368
385
  outputMeta: node.outputMeta
369
386
  });
@@ -377,57 +394,79 @@ function buildMockTree(rootNode) {
377
394
  }
378
395
  return { spans };
379
396
  }
380
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment, adaptInputs) {
397
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, environment, adaptInputs) {
381
398
  const lease = environment ? serverItem.dbBranchLease : void 0;
382
399
  let inputs = [];
383
400
  let originalOutput;
384
401
  let result;
385
402
  let error = null;
386
- const replayedTraceId = randomUuid();
387
403
  const pendingPersistence = [];
404
+ const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
405
+ const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
388
406
  try {
389
- const span = await httpClient.getExternalSpan(serverItem.externalSpanId);
407
+ const span = await httpClient.getExternalSpan(originalSpanId);
390
408
  const spanData = span.rawData?.span_data ?? {};
391
409
  inputs = deserializeInputs(spanData);
392
410
  originalOutput = deserializeOutput(spanData);
393
411
  if (adaptInputs) {
394
412
  inputs = adaptInputs(inputs, {
395
- traceId: serverItem.traceId,
396
- sourceSpanId: serverItem.externalSpanId
413
+ originalTraceId,
414
+ originalSpanId,
415
+ // Deprecated aliases for originalTraceId/originalSpanId.
416
+ sourceTraceId: originalTraceId,
417
+ sourceSpanId: originalSpanId
397
418
  });
398
419
  }
420
+ const hasOverrides = resolvedOverrides.length > 0;
421
+ const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
422
+ const includeOutputs = mockStrategy === "all";
399
423
  let mockTree;
400
- if (mockStrategy === "all" || mockStrategy === "marked") {
424
+ if (needTree) {
401
425
  try {
402
- const treeResponse = await httpClient.getSpanTree(
403
- serverItem.externalSpanId
404
- );
426
+ const treeResponse = await httpClient.getSpanTree(originalSpanId, {
427
+ includeOutputs
428
+ });
405
429
  if (treeResponse.root) {
406
430
  mockTree = buildMockTree(treeResponse.root);
407
- } else if (mockStrategy === "all") {
431
+ } else if (mockStrategy === "all" || hasOverrides) {
408
432
  throw new BitfabError(
409
- `Replay mock strategy "all" requires a span tree root for source span ${serverItem.externalSpanId}.`
433
+ `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for original span ${originalSpanId}.`
410
434
  );
411
435
  } else {
412
436
  mockTree = void 0;
413
437
  }
414
438
  } catch (e) {
415
- if (mockStrategy === "all") {
439
+ if (mockStrategy === "all" || hasOverrides) {
416
440
  throw e;
417
441
  }
418
442
  mockTree = void 0;
419
443
  }
420
444
  }
445
+ const outputCache = /* @__PURE__ */ new Map();
446
+ const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
447
+ let pending = outputCache.get(externalSpanId);
448
+ if (!pending) {
449
+ pending = httpClient.getExternalSpan(externalSpanId).then(
450
+ (s) => deserializeOutput(
451
+ s.rawData?.span_data ?? {}
452
+ )
453
+ );
454
+ outputCache.set(externalSpanId, pending);
455
+ }
456
+ return pending;
457
+ } : void 0;
421
458
  const maybePromise = runWithReplayContext(
422
459
  {
423
460
  testRunId,
424
461
  traceId: replayedTraceId,
425
462
  inputSourceSpanId: span.id,
426
463
  inputSourceTraceId: span.externalTraceId,
427
- sourceBitfabTraceId: serverItem.traceId,
464
+ sourceBitfabTraceId: originalTraceId,
428
465
  mockTree,
429
466
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
430
467
  mockStrategy,
468
+ mockOverrides: hasOverrides ? resolvedOverrides : void 0,
469
+ fetchSpanOutput,
431
470
  dbBranchLease: lease,
432
471
  pendingPersistence
433
472
  },
@@ -452,7 +491,15 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
452
491
  }
453
492
  }
454
493
  return {
455
- traceId: replayedTraceId,
494
+ // Written in by replay() from the complete-replay response once the server
495
+ // has minted this replay trace's row. Null until then: the client-side
496
+ // correlation id (replayedTraceId) is never surfaced as the item's traceId.
497
+ traceId: null,
498
+ originalTraceId,
499
+ originalSpanId,
500
+ // Deprecated aliases for originalTraceId/originalSpanId.
501
+ sourceTraceId: originalTraceId,
502
+ sourceSpanId: originalSpanId,
456
503
  input: inputs,
457
504
  result,
458
505
  originalOutput,
@@ -484,7 +531,7 @@ async function mapWithConcurrency(tasks, maxConcurrency, onSettled) {
484
531
  await Promise.all(workers);
485
532
  return results;
486
533
  }
487
- async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
534
+ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
488
535
  if (options?.traceIds !== void 0) {
489
536
  if (options.traceIds.length === 0) {
490
537
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -524,13 +571,20 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
524
571
  );
525
572
  const mockStrategy = options?.mock ?? "marked";
526
573
  const maxConcurrency = options?.maxConcurrency ?? 10;
574
+ const resolvedOverrides = [
575
+ ...normalizeMockOverrides(options?.mockOverride),
576
+ ...registeredOverrides
577
+ ];
578
+ const replayedTraceIds = serverItems.map(() => randomUuid());
527
579
  const tasks = serverItems.map(
528
- (serverItem) => () => processItem(
580
+ (serverItem, index) => () => processItem(
529
581
  httpClient,
530
582
  serverItem,
531
583
  fn,
532
584
  testRunId,
533
585
  mockStrategy,
586
+ resolvedOverrides,
587
+ replayedTraceIds[index],
534
588
  options?.environment,
535
589
  options?.adaptInputs
536
590
  )
@@ -557,11 +611,17 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
557
611
  succeeded,
558
612
  errored,
559
613
  item: {
560
- // Source (historical) trace id, so a UI can identify the trace
561
- // that just settled. The item's own traceId is the new replay
562
- // trace and is assigned later (below), so use the server item.
563
- traceId: serverItems[index]?.traceId ?? null,
564
- replayTraceId: item.traceId,
614
+ // The server replay trace id isn't known until completeReplay
615
+ // runs (below), so it can't be reported mid-run and we never
616
+ // emit the client-side placeholder. originalTraceId (the
617
+ // historical trace) is known now and is what a UI keys on to
618
+ // identify what just settled.
619
+ traceId: null,
620
+ originalTraceId: item.originalTraceId ?? null,
621
+ originalSpanId: item.originalSpanId ?? null,
622
+ // Deprecated aliases for originalTraceId/originalSpanId.
623
+ sourceTraceId: item.originalTraceId ?? null,
624
+ sourceSpanId: item.originalSpanId ?? null,
565
625
  input: item.input,
566
626
  result: item.result,
567
627
  originalOutput: item.originalOutput,
@@ -579,56 +639,58 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
579
639
  const completeResult = await httpClient.completeReplay(testRunId);
580
640
  const serverTraceIds = completeResult.traceIds;
581
641
  const replayTokens = completeResult.tokens;
582
- if (serverTraceIds === void 0) {
583
- try {
584
- console.warn(
585
- "Bitfab: server did not return replay trace IDs; item.traceId will be null (server upgrade required for verdict persistence)"
586
- );
587
- } catch {
588
- }
589
- for (const item of resultItems) {
590
- item.traceId = null;
591
- }
592
- } else {
642
+ if (serverTraceIds !== void 0) {
593
643
  const missing = [];
594
644
  let completedCount = 0;
595
- for (const item of resultItems) {
596
- if (item.traceId) {
597
- const mapped = serverTraceIds[item.traceId];
598
- if (item.error === null) {
599
- completedCount += 1;
600
- if (mapped === void 0) {
601
- missing.push(item.traceId);
602
- }
603
- }
604
- if (mapped !== void 0) {
605
- item.tokens = replayTokens?.[mapped] ?? null;
645
+ for (let index = 0; index < resultItems.length; index += 1) {
646
+ const item = resultItems[index];
647
+ const localId = replayedTraceIds[index];
648
+ const mapped = localId ? serverTraceIds[localId] : void 0;
649
+ item.traceId = mapped ?? null;
650
+ if (item.error === null) {
651
+ completedCount += 1;
652
+ if (mapped === void 0) {
653
+ missing.push(localId ?? item.originalTraceId);
606
654
  }
607
- item.traceId = mapped ?? null;
655
+ }
656
+ if (mapped !== void 0) {
657
+ item.tokens = replayTokens?.[mapped] ?? null;
608
658
  }
609
659
  }
610
- if (missing.length > 0) {
660
+ if (completedCount > 0 && missing.length === completedCount) {
611
661
  const serverCount = completeResult.traceCount !== void 0 ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.` : "";
612
- if (missing.length === completedCount) {
613
- throw new BitfabError(
614
- `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.`
615
- );
616
- }
662
+ throw new BitfabError(
663
+ `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.`
664
+ );
665
+ }
666
+ if (missing.length > 0) {
617
667
  try {
618
668
  console.error(
619
- `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(", ")}`
669
+ `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.`
620
670
  );
621
671
  } catch {
622
672
  }
623
673
  }
624
674
  }
625
- const replayResult = {
675
+ const result = {
626
676
  items: resultItems,
627
677
  testRunId,
628
678
  testRunUrl: `${serviceUrl}${testRunUrl}`
629
679
  };
630
- await writeReplayResultFile(replayResult);
631
- return replayResult;
680
+ await writeReplayResultFile(result);
681
+ try {
682
+ options?.onProgress?.({
683
+ type: "complete",
684
+ testRunId,
685
+ completed: total,
686
+ total,
687
+ succeeded,
688
+ errored,
689
+ result
690
+ });
691
+ } catch {
692
+ }
693
+ return result;
632
694
  }
633
695
  async function writeReplayResultFile(result) {
634
696
  const resultPath = typeof process !== "undefined" ? process.env?.BITFAB_REPLAY_RESULT_PATH : void 0;
@@ -657,6 +719,7 @@ var init_replay = __esm({
657
719
  "src/replay.ts"() {
658
720
  "use strict";
659
721
  init_errors();
722
+ init_mockOverride();
660
723
  init_randomUuid();
661
724
  init_replayContext();
662
725
  init_serialize();
@@ -697,7 +760,7 @@ registerAsyncLocalStorageClass(
697
760
  );
698
761
 
699
762
  // src/version.generated.ts
700
- var __version__ = "0.28.11";
763
+ var __version__ = "0.29.1";
701
764
 
702
765
  // src/constants.ts
703
766
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
@@ -1126,9 +1189,14 @@ var HttpClient = class {
1126
1189
  /**
1127
1190
  * Fetch the span tree for a root span.
1128
1191
  * Blocking GET request.
1192
+ *
1193
+ * Pass `includeOutputs: false` for a payload-free tree (structure +
1194
+ * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
1195
+ * span instead of all up front. Omit it (default eager) for `mock: "all"`.
1129
1196
  */
1130
- async getSpanTree(externalSpanId) {
1131
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
1197
+ async getSpanTree(externalSpanId, options) {
1198
+ const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
1199
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
1132
1200
  const controller = new AbortController();
1133
1201
  const timeoutId = setTimeout(() => controller.abort(), 3e4);
1134
1202
  try {
@@ -2714,6 +2782,9 @@ var BitfabLangGraphCallbackHandler = class {
2714
2782
  }
2715
2783
  };
2716
2784
 
2785
+ // src/client.ts
2786
+ init_mockOverride();
2787
+
2717
2788
  // src/openaiAgentSdk.ts
2718
2789
  var BitfabOpenAIAgentHandler = class {
2719
2790
  constructor(config) {
@@ -3529,6 +3600,12 @@ var Bitfab = class {
3529
3600
  constructor(config) {
3530
3601
  /** Gate the empty-key warning to fire at most once. */
3531
3602
  this.apiKeyWarned = false;
3603
+ /**
3604
+ * Mock overrides registered via {@link Bitfab.registerMockOverride}, applied
3605
+ * to every `replay` on this client (after any per-call `mockOverride`). In
3606
+ * registration order; first matcher wins within this list.
3607
+ */
3608
+ this.mockOverrides = [];
3532
3609
  this.apiKeyConfig = config.apiKey;
3533
3610
  this.serviceUrl = config.serviceUrl ?? DEFAULT_SERVICE_URL;
3534
3611
  this.timeout = config.timeout ?? 12e4;
@@ -4113,7 +4190,7 @@ var Bitfab = class {
4113
4190
  dbSnapshotUsage: {
4114
4191
  neonBranchId: replayCtx.dbBranchLease.neonBranchId,
4115
4192
  snapshotTimestamp: replayCtx.dbBranchLease.snapshotTimestamp,
4116
- sourceTraceId: replayCtx.sourceBitfabTraceId,
4193
+ originalTraceId: replayCtx.sourceBitfabTraceId,
4117
4194
  accessed: replayCtx.dbSnapshotAccessed === true
4118
4195
  }
4119
4196
  }
@@ -4141,24 +4218,77 @@ var Bitfab = class {
4141
4218
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
4142
4219
  const callIndex = counters.get(counterKey) ?? 0;
4143
4220
  counters.set(counterKey, callIndex + 1);
4144
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4145
- if (shouldMock) {
4146
- const mockKey = `${counterKey}:${callIndex}`;
4147
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4148
- if (mockSpan) {
4149
- let output = mockSpan.output;
4150
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4151
- output = deserializeValue({
4152
- json: mockSpan.output,
4153
- meta: mockSpan.outputMeta
4154
- });
4155
- }
4221
+ const mockKey = `${counterKey}:${callIndex}`;
4222
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4223
+ const emitMock = (output) => {
4224
+ void sendSpan({ result: output, mocked: true });
4225
+ if (fnReturnsPromise) {
4226
+ return Promise.resolve(output);
4227
+ }
4228
+ return output;
4229
+ };
4230
+ const emitMockAsync = (pending) => {
4231
+ if (!fnReturnsPromise) {
4232
+ throw new BitfabError(
4233
+ `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.`
4234
+ );
4235
+ }
4236
+ return (async () => {
4237
+ const output = await pending;
4156
4238
  void sendSpan({ result: output, mocked: true });
4157
- if (fnReturnsPromise) {
4158
- return Promise.resolve(output);
4159
- }
4160
4239
  return output;
4240
+ })();
4241
+ };
4242
+ const resolveRecordedOutput = () => {
4243
+ const hasInlineOutput = mockSpan?.output !== void 0 || mockSpan?.outputMeta !== void 0;
4244
+ if (!hasInlineOutput && replayCtxForMock.fetchSpanOutput && mockSpan?.externalSpanId) {
4245
+ return replayCtxForMock.fetchSpanOutput(mockSpan.externalSpanId);
4246
+ }
4247
+ if (!mockSpan) {
4248
+ return Promise.reject(
4249
+ new BitfabError(
4250
+ `No recorded span to source output for "${traceFunctionKey}".`
4251
+ )
4252
+ );
4161
4253
  }
4254
+ let output = mockSpan.output;
4255
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4256
+ output = deserializeValue({
4257
+ json: mockSpan.output,
4258
+ meta: mockSpan.outputMeta
4259
+ });
4260
+ }
4261
+ return output;
4262
+ };
4263
+ if (replayCtxForMock.mockOverrides?.length) {
4264
+ const nodeMeta = {
4265
+ traceFunctionKey,
4266
+ spanName: baseSpanParams.spanName,
4267
+ type: options.type ?? "custom",
4268
+ originalSpanId: mockSpan?.sourceSpanId
4269
+ };
4270
+ const override = replayCtxForMock.mockOverrides.find(
4271
+ (o) => o.match(nodeMeta)
4272
+ );
4273
+ if (override) {
4274
+ const injected = resolveMockValue(override.value, {
4275
+ node: nodeMeta,
4276
+ inputs: args,
4277
+ getOriginalOutput: () => Promise.resolve(resolveRecordedOutput())
4278
+ });
4279
+ if (injected instanceof Promise) {
4280
+ return emitMockAsync(injected);
4281
+ }
4282
+ return emitMock(injected);
4283
+ }
4284
+ }
4285
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4286
+ if (shouldMock && mockSpan) {
4287
+ const recorded = resolveRecordedOutput();
4288
+ if (recorded instanceof Promise) {
4289
+ return emitMockAsync(recorded);
4290
+ }
4291
+ return emitMock(recorded);
4162
4292
  }
4163
4293
  }
4164
4294
  const recordSpan = (result) => {
@@ -4364,8 +4494,11 @@ var Bitfab = class {
4364
4494
  ...params.dbSnapshotUsage.snapshotTimestamp && {
4365
4495
  snapshot_timestamp: params.dbSnapshotUsage.snapshotTimestamp
4366
4496
  },
4367
- ...params.dbSnapshotUsage.sourceTraceId && {
4368
- source_trace_id: params.dbSnapshotUsage.sourceTraceId
4497
+ ...params.dbSnapshotUsage.originalTraceId && {
4498
+ original_trace_id: params.dbSnapshotUsage.originalTraceId,
4499
+ // Deprecated wire alias, kept so this SDK still reports usage
4500
+ // against servers that predate the rename.
4501
+ source_trace_id: params.dbSnapshotUsage.originalTraceId
4369
4502
  },
4370
4503
  accessed: params.dbSnapshotUsage.accessed
4371
4504
  };
@@ -4438,26 +4571,14 @@ var Bitfab = class {
4438
4571
  ...params.mocked && { mocked: true }
4439
4572
  });
4440
4573
  }
4441
- /**
4442
- * Replay historical traces through a function and create a test run.
4443
- *
4444
- * Fetches the last N traces for the given trace function key, re-runs each
4445
- * through the provided function, and returns comparison data.
4446
- *
4447
- * Accepts either a `withSpan`-wrapped function (under the same key) or any
4448
- * plain callable: plain callables are wrapped internally so each replayed
4449
- * invocation records a trace tied to the test run. The plain-callable form
4450
- * is how handler-instrumented workflows (LangGraph/LangChain, Claude Agent
4451
- * SDK) replay - those record traces under a key with no `withSpan`-wrapped
4452
- * root in the app.
4453
- *
4454
- * @param traceFunctionKey - The trace function key to replay
4455
- * @param fn - The function to run recorded inputs through
4456
- * @param options - Optional replay options. When `traceIds` is passed,
4457
- * `limit` is ignored (with a warning): an explicit ID list already
4458
- * determines how many traces replay.
4459
- * @returns ReplayResult with items, testRunId, and testRunUrl
4460
- */
4574
+ registerMockOverride(overrideOrMatch, value) {
4575
+ const override = typeof overrideOrMatch === "function" ? { match: overrideOrMatch, value } : overrideOrMatch;
4576
+ this.mockOverrides.push(override);
4577
+ }
4578
+ /** Remove all overrides registered via {@link registerMockOverride}. */
4579
+ clearMockOverrides() {
4580
+ this.mockOverrides.length = 0;
4581
+ }
4461
4582
  async replay(traceFunctionKey, fn, options) {
4462
4583
  const wrappedKey = fn._bitfabTraceFunctionKey;
4463
4584
  let replayFn = fn;
@@ -4478,7 +4599,8 @@ var Bitfab = class {
4478
4599
  this.serviceUrl,
4479
4600
  traceFunctionKey,
4480
4601
  replayFn,
4481
- options
4602
+ options,
4603
+ this.mockOverrides
4482
4604
  );
4483
4605
  }
4484
4606
  };