bitfab 0.28.10 → 0.29.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
@@ -291,6 +291,22 @@ var init_asyncStorage = __esm({
291
291
  }
292
292
  });
293
293
 
294
+ // src/mockOverride.ts
295
+ function resolveMockValue(value, ctx) {
296
+ return typeof value === "function" ? value(ctx) : value;
297
+ }
298
+ function normalizeMockOverrides(mockOverride) {
299
+ if (mockOverride === void 0) {
300
+ return [];
301
+ }
302
+ return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
303
+ }
304
+ var init_mockOverride = __esm({
305
+ "src/mockOverride.ts"() {
306
+ "use strict";
307
+ }
308
+ });
309
+
294
310
  // src/replayContext.ts
295
311
  function getReplayContext() {
296
312
  return replayContextStorage?.getStore() ?? null;
@@ -366,6 +382,7 @@ function buildMockTree(rootNode) {
366
382
  counters.set(counterKey, index + 1);
367
383
  spans.set(`${counterKey}:${index}`, {
368
384
  sourceSpanId: node.sourceSpanId,
385
+ externalSpanId: node.externalSpanId,
369
386
  output: node.output,
370
387
  outputMeta: node.outputMeta
371
388
  });
@@ -379,7 +396,7 @@ function buildMockTree(rootNode) {
379
396
  }
380
397
  return { spans };
381
398
  }
382
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment, adaptInputs) {
399
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, environment, adaptInputs) {
383
400
  const lease = environment ? serverItem.dbBranchLease : void 0;
384
401
  let inputs = [];
385
402
  let originalOutput;
@@ -398,28 +415,45 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
398
415
  sourceSpanId: serverItem.externalSpanId
399
416
  });
400
417
  }
418
+ const hasOverrides = resolvedOverrides.length > 0;
419
+ const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
420
+ const includeOutputs = mockStrategy === "all";
401
421
  let mockTree;
402
- if (mockStrategy === "all" || mockStrategy === "marked") {
422
+ if (needTree) {
403
423
  try {
404
424
  const treeResponse = await httpClient.getSpanTree(
405
- serverItem.externalSpanId
425
+ serverItem.externalSpanId,
426
+ { includeOutputs }
406
427
  );
407
428
  if (treeResponse.root) {
408
429
  mockTree = buildMockTree(treeResponse.root);
409
- } else if (mockStrategy === "all") {
430
+ } else if (mockStrategy === "all" || hasOverrides) {
410
431
  throw new BitfabError(
411
- `Replay mock strategy "all" requires a span tree root for source span ${serverItem.externalSpanId}.`
432
+ `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for source span ${serverItem.externalSpanId}.`
412
433
  );
413
434
  } else {
414
435
  mockTree = void 0;
415
436
  }
416
437
  } catch (e) {
417
- if (mockStrategy === "all") {
438
+ if (mockStrategy === "all" || hasOverrides) {
418
439
  throw e;
419
440
  }
420
441
  mockTree = void 0;
421
442
  }
422
443
  }
444
+ const outputCache = /* @__PURE__ */ new Map();
445
+ const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
446
+ let pending = outputCache.get(externalSpanId);
447
+ if (!pending) {
448
+ pending = httpClient.getExternalSpan(externalSpanId).then(
449
+ (s) => deserializeOutput(
450
+ s.rawData?.span_data ?? {}
451
+ )
452
+ );
453
+ outputCache.set(externalSpanId, pending);
454
+ }
455
+ return pending;
456
+ } : void 0;
423
457
  const maybePromise = runWithReplayContext(
424
458
  {
425
459
  testRunId,
@@ -430,6 +464,8 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
430
464
  mockTree,
431
465
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
432
466
  mockStrategy,
467
+ mockOverrides: hasOverrides ? resolvedOverrides : void 0,
468
+ fetchSpanOutput,
433
469
  dbBranchLease: lease,
434
470
  pendingPersistence
435
471
  },
@@ -486,7 +522,7 @@ async function mapWithConcurrency(tasks, maxConcurrency, onSettled) {
486
522
  await Promise.all(workers);
487
523
  return results;
488
524
  }
489
- async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
525
+ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
490
526
  if (options?.traceIds !== void 0) {
491
527
  if (options.traceIds.length === 0) {
492
528
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -526,6 +562,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
526
562
  );
527
563
  const mockStrategy = options?.mock ?? "marked";
528
564
  const maxConcurrency = options?.maxConcurrency ?? 10;
565
+ const resolvedOverrides = [
566
+ ...normalizeMockOverrides(options?.mockOverride),
567
+ ...registeredOverrides
568
+ ];
529
569
  const tasks = serverItems.map(
530
570
  (serverItem) => () => processItem(
531
571
  httpClient,
@@ -533,6 +573,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
533
573
  fn,
534
574
  testRunId,
535
575
  mockStrategy,
576
+ resolvedOverrides,
536
577
  options?.environment,
537
578
  options?.adaptInputs
538
579
  )
@@ -659,6 +700,7 @@ var init_replay = __esm({
659
700
  "src/replay.ts"() {
660
701
  "use strict";
661
702
  init_errors();
703
+ init_mockOverride();
662
704
  init_randomUuid();
663
705
  init_replayContext();
664
706
  init_serialize();
@@ -692,7 +734,7 @@ __export(index_exports, {
692
734
  module.exports = __toCommonJS(index_exports);
693
735
 
694
736
  // src/version.generated.ts
695
- var __version__ = "0.28.10";
737
+ var __version__ = "0.29.0";
696
738
 
697
739
  // src/constants.ts
698
740
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
@@ -932,6 +974,50 @@ var HttpClient = class {
932
974
  async lookupFunction(name) {
933
975
  return this.request("/api/sdk/functions/lookup", { name });
934
976
  }
977
+ async getTraceSpan(traceId, lookup) {
978
+ const searchParams = new URLSearchParams();
979
+ if (lookup.id !== void 0) {
980
+ searchParams.set("id", lookup.id);
981
+ } else {
982
+ searchParams.set("name", lookup.name);
983
+ searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
984
+ }
985
+ const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
986
+ const response = await this.get(endpoint);
987
+ return response.span;
988
+ }
989
+ async get(endpoint) {
990
+ const url = `${this.serviceUrl}${endpoint}`;
991
+ const controller = new AbortController();
992
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
993
+ try {
994
+ const response = await fetch(url, {
995
+ method: "GET",
996
+ headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
997
+ signal: controller.signal
998
+ });
999
+ if (!response.ok) {
1000
+ const errorText = await response.text();
1001
+ throw new BitfabError(
1002
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1003
+ );
1004
+ }
1005
+ return await response.json();
1006
+ } catch (error) {
1007
+ if (error instanceof BitfabError) {
1008
+ throw error;
1009
+ }
1010
+ if (error instanceof Error) {
1011
+ if (error.name === "AbortError") {
1012
+ throw new BitfabError(`Request timed out after ${this.timeout}ms`);
1013
+ }
1014
+ throw new BitfabError(error.message);
1015
+ }
1016
+ throw new BitfabError("Unknown error occurred");
1017
+ } finally {
1018
+ clearTimeout(timeoutId);
1019
+ }
1020
+ }
935
1021
  /**
936
1022
  * Send an internal trace (from BAML execution).
937
1023
  * Fire-and-forget with awaitOnExit - doesn't block the caller.
@@ -988,12 +1074,12 @@ var HttpClient = class {
988
1074
  });
989
1075
  }
990
1076
  /**
991
- * Partial update of an existing external trace identified by sourceTraceId.
1077
+ * Partial update of an existing trace identified by its Bitfab trace ID.
992
1078
  * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
993
1079
  * returns a tracked promise that callers may optionally await.
994
1080
  */
995
- patchTrace(sourceTraceId, payload) {
996
- const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
1081
+ patchTrace(traceId, payload) {
1082
+ const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
997
1083
  return awaitOnExit(
998
1084
  this.request(endpoint, payload, { method: "PATCH" })
999
1085
  ).catch((error) => {
@@ -1077,9 +1163,14 @@ var HttpClient = class {
1077
1163
  /**
1078
1164
  * Fetch the span tree for a root span.
1079
1165
  * Blocking GET request.
1166
+ *
1167
+ * Pass `includeOutputs: false` for a payload-free tree (structure +
1168
+ * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
1169
+ * span instead of all up front. Omit it (default eager) for `mock: "all"`.
1080
1170
  */
1081
- async getSpanTree(externalSpanId) {
1082
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
1171
+ async getSpanTree(externalSpanId, options) {
1172
+ const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
1173
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
1083
1174
  const controller = new AbortController();
1084
1175
  const timeoutId = setTimeout(() => controller.abort(), 3e4);
1085
1176
  try {
@@ -1331,6 +1422,7 @@ var BitfabClaudeAgentHandler = class {
1331
1422
  const traceId = this.ensureTrace();
1332
1423
  const { safe: safeInput, dropped: inputDropped } = toJsonSafeReport(inputData);
1333
1424
  const spanInfo = {
1425
+ id: randomUuid(),
1334
1426
  spanId,
1335
1427
  traceId,
1336
1428
  parentId: parentId ?? null,
@@ -1394,6 +1486,8 @@ var BitfabClaudeAgentHandler = class {
1394
1486
  rawSpan.parent_id = spanInfo.parentId;
1395
1487
  }
1396
1488
  const payload = {
1489
+ id: spanInfo.id,
1490
+ traceId: spanInfo.traceId,
1397
1491
  type: "sdk-function",
1398
1492
  source: "typescript-sdk-claude-agent-sdk",
1399
1493
  traceFunctionKey: this.traceFunctionKey,
@@ -1423,6 +1517,7 @@ var BitfabClaudeAgentHandler = class {
1423
1517
  externalTrace.metadata = metadata;
1424
1518
  }
1425
1519
  const traceData = {
1520
+ id: traceId,
1426
1521
  type: "sdk-function",
1427
1522
  source: "typescript-sdk-claude-agent-sdk",
1428
1523
  traceFunctionKey: this.traceFunctionKey,
@@ -1691,6 +1786,7 @@ var BitfabClaudeAgentHandler = class {
1691
1786
  }
1692
1787
  Object.assign(llmContext, this.currentLlmUsage);
1693
1788
  const spanInfo = {
1789
+ id: randomUuid(),
1694
1790
  spanId,
1695
1791
  traceId,
1696
1792
  parentId,
@@ -2337,6 +2433,7 @@ var BitfabLangGraphCallbackHandler = class {
2337
2433
  const contexts = Object.keys(lgMetadata).length > 0 ? [lgMetadata] : [];
2338
2434
  const { safe: safeInput, dropped: inputDropped } = toJsonSafeReport(inputData);
2339
2435
  const spanInfo = {
2436
+ id: randomUuid(),
2340
2437
  spanId: runId,
2341
2438
  traceId: invocation.traceId,
2342
2439
  rootRunId: invocation.rootRunId,
@@ -2415,6 +2512,8 @@ var BitfabLangGraphCallbackHandler = class {
2415
2512
  rawSpan.parent_id = spanInfo.parentId;
2416
2513
  }
2417
2514
  const payload = {
2515
+ id: spanInfo.id,
2516
+ traceId: spanInfo.traceId,
2418
2517
  type: "sdk-function",
2419
2518
  source: "typescript-sdk-langgraph",
2420
2519
  traceFunctionKey: this.traceFunctionKey,
@@ -2430,6 +2529,7 @@ var BitfabLangGraphCallbackHandler = class {
2430
2529
  sendTraceCompletion(rootSpan, activeContext) {
2431
2530
  const completed = activeContext === null;
2432
2531
  const traceData = {
2532
+ id: rootSpan.traceId,
2433
2533
  type: "sdk-function",
2434
2534
  source: "typescript-sdk-langgraph",
2435
2535
  traceFunctionKey: this.traceFunctionKey,
@@ -2449,6 +2549,7 @@ var BitfabLangGraphCallbackHandler = class {
2449
2549
  }
2450
2550
  sendTraceStart(rootSpan) {
2451
2551
  const traceData = {
2552
+ id: rootSpan.traceId,
2452
2553
  type: "sdk-function",
2453
2554
  source: "typescript-sdk-langgraph",
2454
2555
  traceFunctionKey: this.traceFunctionKey,
@@ -2655,6 +2756,9 @@ var BitfabLangGraphCallbackHandler = class {
2655
2756
  }
2656
2757
  };
2657
2758
 
2759
+ // src/client.ts
2760
+ init_mockOverride();
2761
+
2658
2762
  // src/openaiAgentSdk.ts
2659
2763
  var BitfabOpenAIAgentHandler = class {
2660
2764
  constructor(config) {
@@ -2807,6 +2911,7 @@ var ReplayEnvironment = class {
2807
2911
  init_serialize();
2808
2912
 
2809
2913
  // src/tracing.ts
2914
+ init_randomUuid();
2810
2915
  var BitfabOpenAITracingProcessor = class {
2811
2916
  /**
2812
2917
  * Initialize the tracing processor.
@@ -2816,6 +2921,7 @@ var BitfabOpenAITracingProcessor = class {
2816
2921
  constructor(config) {
2817
2922
  this.activeTraces = {};
2818
2923
  this.activeSpanMappings = {};
2924
+ this.canonicalTraceIds = {};
2819
2925
  this.httpClient = new HttpClient({
2820
2926
  apiKey: config.apiKey,
2821
2927
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
@@ -2823,6 +2929,15 @@ var BitfabOpenAITracingProcessor = class {
2823
2929
  });
2824
2930
  this.getActiveSpanContext = config.getActiveSpanContext ?? null;
2825
2931
  }
2932
+ getCanonicalTraceId(sourceTraceId) {
2933
+ const existing = this.canonicalTraceIds[sourceTraceId];
2934
+ if (existing) {
2935
+ return existing;
2936
+ }
2937
+ const created = randomUuid();
2938
+ this.canonicalTraceIds[sourceTraceId] = created;
2939
+ return created;
2940
+ }
2826
2941
  /**
2827
2942
  * Called when a trace is started.
2828
2943
  * If there's an active withSpan context, the trace ID is remapped to the
@@ -2834,7 +2949,12 @@ var BitfabOpenAITracingProcessor = class {
2834
2949
  if (activeContext) {
2835
2950
  this.activeSpanMappings[trace.traceId] = activeContext;
2836
2951
  }
2837
- this.sendTrace(trace, activeContext ? { id: activeContext.traceId } : {});
2952
+ const canonicalTraceId = activeContext?.traceId ?? this.getCanonicalTraceId(trace.traceId);
2953
+ this.canonicalTraceIds[trace.traceId] = canonicalTraceId;
2954
+ this.sendTrace(trace, {
2955
+ id: canonicalTraceId,
2956
+ sourceTraceId: activeContext?.traceId
2957
+ });
2838
2958
  }
2839
2959
  /**
2840
2960
  * Called when a trace is ended.
@@ -2843,11 +2963,13 @@ var BitfabOpenAITracingProcessor = class {
2843
2963
  */
2844
2964
  async onTraceEnd(trace) {
2845
2965
  const mapping = this.activeSpanMappings[trace.traceId];
2846
- this.sendTrace(
2847
- trace,
2848
- mapping ? { id: mapping.traceId } : { completed: true }
2849
- );
2966
+ this.sendTrace(trace, {
2967
+ completed: mapping === void 0,
2968
+ id: mapping?.traceId ?? this.getCanonicalTraceId(trace.traceId),
2969
+ sourceTraceId: mapping?.traceId
2970
+ });
2850
2971
  delete this.activeSpanMappings[trace.traceId];
2972
+ delete this.canonicalTraceIds[trace.traceId];
2851
2973
  delete this.activeTraces[trace.traceId];
2852
2974
  }
2853
2975
  /**
@@ -2877,22 +2999,25 @@ var BitfabOpenAITracingProcessor = class {
2877
2999
  async shutdown(_timeout) {
2878
3000
  this.activeTraces = {};
2879
3001
  this.activeSpanMappings = {};
3002
+ this.canonicalTraceIds = {};
2880
3003
  }
2881
3004
  /**
2882
3005
  * Send trace to Bitfab API (fire-and-forget).
2883
3006
  * When traceIdOverride is provided, the trace ID is remapped to link
2884
3007
  * the OpenAI trace into an outer withSpan trace.
2885
3008
  */
2886
- sendTrace(trace, overrides = {}) {
3009
+ sendTrace(trace, options = {}) {
2887
3010
  try {
2888
- const { completed, ...traceOverrides } = overrides;
2889
3011
  const traceData = trace.toJSON();
2890
- Object.assign(traceData, traceOverrides);
3012
+ if (options.sourceTraceId) {
3013
+ traceData.id = options.sourceTraceId;
3014
+ }
2891
3015
  this.httpClient.sendExternalTrace({
3016
+ ...options.id && { id: options.id },
2892
3017
  type: "openai",
2893
3018
  source: "typescript-sdk-openai-tracing",
2894
3019
  externalTrace: traceData,
2895
- completed: completed ?? false
3020
+ completed: options.completed ?? false
2896
3021
  });
2897
3022
  } catch {
2898
3023
  }
@@ -2978,6 +3103,7 @@ var BitfabOpenAITracingProcessor = class {
2978
3103
  */
2979
3104
  buildSpanPayload(serializedSpan, errors) {
2980
3105
  const payload = {
3106
+ id: randomUuid(),
2981
3107
  type: "openai",
2982
3108
  source: "typescript-sdk-openai-tracing",
2983
3109
  sourceTraceId: serializedSpan.trace_id ?? "unknown",
@@ -3000,6 +3126,10 @@ var BitfabOpenAITracingProcessor = class {
3000
3126
  this.extractSpanInputResponse(span, serializedSpan, errors);
3001
3127
  this.applySpanOverrides(serializedSpan, span.traceId ?? "");
3002
3128
  const payload = this.buildSpanPayload(serializedSpan, errors);
3129
+ const canonicalTraceId = span.traceId ? this.getCanonicalTraceId(span.traceId) : void 0;
3130
+ if (canonicalTraceId) {
3131
+ payload.traceId = canonicalTraceId;
3132
+ }
3003
3133
  this.httpClient.sendExternalSpan(payload);
3004
3134
  }
3005
3135
  };
@@ -3316,24 +3446,19 @@ function extractContextFromCollector(collector) {
3316
3446
  return null;
3317
3447
  }
3318
3448
  }
3319
- var TRACE_ID_PATTERN = /^[a-zA-Z0-9_\-.:]+$/;
3320
- var TRACE_ID_MAX_LENGTH = 256;
3449
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
3321
3450
  function validateTraceId(traceId) {
3322
- if (typeof traceId !== "string" || traceId.length === 0) {
3323
- throw new BitfabError("traceId is required and must be a non-empty string");
3324
- }
3325
- if (traceId.length > TRACE_ID_MAX_LENGTH) {
3326
- throw new BitfabError(
3327
- `traceId must be ${TRACE_ID_MAX_LENGTH} characters or fewer`
3328
- );
3451
+ if (typeof traceId !== "string" || !UUID_PATTERN.test(traceId)) {
3452
+ throw new BitfabError("traceId must be a valid Bitfab trace ID");
3329
3453
  }
3330
- if (!TRACE_ID_PATTERN.test(traceId)) {
3331
- throw new BitfabError(
3332
- `traceId may only contain letters, digits, "_", "-", ".", ":"`
3333
- );
3454
+ }
3455
+ function validateSpanId(id) {
3456
+ if (typeof id !== "string" || !UUID_PATTERN.test(id)) {
3457
+ throw new BitfabError("id must be a valid Bitfab span ID");
3334
3458
  }
3335
3459
  }
3336
3460
  var noOpSpan = {
3461
+ id: "",
3337
3462
  traceId: "",
3338
3463
  addContext() {
3339
3464
  },
@@ -3357,6 +3482,7 @@ function getCurrentSpan() {
3357
3482
  return noOpSpan;
3358
3483
  }
3359
3484
  return {
3485
+ id: current.spanId,
3360
3486
  traceId: current.traceId,
3361
3487
  addContext(context) {
3362
3488
  try {
@@ -3448,6 +3574,12 @@ var Bitfab = class {
3448
3574
  constructor(config) {
3449
3575
  /** Gate the empty-key warning to fire at most once. */
3450
3576
  this.apiKeyWarned = false;
3577
+ /**
3578
+ * Mock overrides registered via {@link Bitfab.registerMockOverride}, applied
3579
+ * to every `replay` on this client (after any per-call `mockOverride`). In
3580
+ * registration order; first matcher wins within this list.
3581
+ */
3582
+ this.mockOverrides = [];
3451
3583
  this.apiKeyConfig = config.apiKey;
3452
3584
  this.serviceUrl = config.serviceUrl ?? DEFAULT_SERVICE_URL;
3453
3585
  this.timeout = config.timeout ?? 12e4;
@@ -4060,24 +4192,77 @@ var Bitfab = class {
4060
4192
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
4061
4193
  const callIndex = counters.get(counterKey) ?? 0;
4062
4194
  counters.set(counterKey, callIndex + 1);
4063
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4064
- if (shouldMock) {
4065
- const mockKey = `${counterKey}:${callIndex}`;
4066
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4067
- if (mockSpan) {
4068
- let output = mockSpan.output;
4069
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4070
- output = deserializeValue({
4071
- json: mockSpan.output,
4072
- meta: mockSpan.outputMeta
4073
- });
4074
- }
4195
+ const mockKey = `${counterKey}:${callIndex}`;
4196
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4197
+ const emitMock = (output) => {
4198
+ void sendSpan({ result: output, mocked: true });
4199
+ if (fnReturnsPromise) {
4200
+ return Promise.resolve(output);
4201
+ }
4202
+ return output;
4203
+ };
4204
+ const emitMockAsync = (pending) => {
4205
+ if (!fnReturnsPromise) {
4206
+ throw new BitfabError(
4207
+ `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.`
4208
+ );
4209
+ }
4210
+ return (async () => {
4211
+ const output = await pending;
4075
4212
  void sendSpan({ result: output, mocked: true });
4076
- if (fnReturnsPromise) {
4077
- return Promise.resolve(output);
4078
- }
4079
4213
  return output;
4214
+ })();
4215
+ };
4216
+ const resolveRecordedOutput = () => {
4217
+ const hasInlineOutput = mockSpan?.output !== void 0 || mockSpan?.outputMeta !== void 0;
4218
+ if (!hasInlineOutput && replayCtxForMock.fetchSpanOutput && mockSpan?.externalSpanId) {
4219
+ return replayCtxForMock.fetchSpanOutput(mockSpan.externalSpanId);
4220
+ }
4221
+ if (!mockSpan) {
4222
+ return Promise.reject(
4223
+ new BitfabError(
4224
+ `No recorded span to source output for "${traceFunctionKey}".`
4225
+ )
4226
+ );
4227
+ }
4228
+ let output = mockSpan.output;
4229
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4230
+ output = deserializeValue({
4231
+ json: mockSpan.output,
4232
+ meta: mockSpan.outputMeta
4233
+ });
4234
+ }
4235
+ return output;
4236
+ };
4237
+ if (replayCtxForMock.mockOverrides?.length) {
4238
+ const nodeMeta = {
4239
+ traceFunctionKey,
4240
+ spanName: baseSpanParams.spanName,
4241
+ type: options.type ?? "custom",
4242
+ originalSpanId: mockSpan?.sourceSpanId
4243
+ };
4244
+ const override = replayCtxForMock.mockOverrides.find(
4245
+ (o) => o.match(nodeMeta)
4246
+ );
4247
+ if (override) {
4248
+ const injected = resolveMockValue(override.value, {
4249
+ node: nodeMeta,
4250
+ inputs: args,
4251
+ getOriginalOutput: () => Promise.resolve(resolveRecordedOutput())
4252
+ });
4253
+ if (injected instanceof Promise) {
4254
+ return emitMockAsync(injected);
4255
+ }
4256
+ return emitMock(injected);
4257
+ }
4258
+ }
4259
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4260
+ if (shouldMock && mockSpan) {
4261
+ const recorded = resolveRecordedOutput();
4262
+ if (recorded instanceof Promise) {
4263
+ return emitMockAsync(recorded);
4080
4264
  }
4265
+ return emitMock(recorded);
4081
4266
  }
4082
4267
  }
4083
4268
  const recordSpan = (result) => {
@@ -4153,20 +4338,19 @@ var Bitfab = class {
4153
4338
  }
4154
4339
  /**
4155
4340
  * Get a detached handle to a previously-created trace, looked up by the
4156
- * caller-supplied id (the same id passed at trace creation).
4341
+ * canonical Bitfab trace ID.
4157
4342
  *
4158
4343
  * The returned handle is not tied to AsyncLocalStorage - each method sends
4159
4344
  * to the server immediately. Useful for adding context to a trace from a
4160
4345
  * different process or thread than the one that created it.
4161
4346
  *
4162
- * Throws synchronously if `traceId` is malformed (empty, too long, or
4163
- * contains characters outside `[a-zA-Z0-9_\-.:]`). Server returns 404 if
4164
- * no trace exists with that id in the org; the failure surfaces as a
4347
+ * Throws synchronously if `traceId` is not a valid Bitfab trace ID. The
4348
+ * server returns 404 if no trace exists with that ID in the org; the failure surfaces as a
4165
4349
  * logged warning (fire-and-forget) or via the awaited promise.
4166
4350
  *
4167
4351
  * Example:
4168
4352
  * ```typescript
4169
- * const trace = client.getTrace("order_abc_123");
4353
+ * const trace = client.getTrace(traceId);
4170
4354
  * await trace.addContext({ refund_status: "approved" });
4171
4355
  * await trace.setMetadata({ region: "us-west" });
4172
4356
  * ```
@@ -4206,6 +4390,33 @@ var Bitfab = class {
4206
4390
  }
4207
4391
  };
4208
4392
  }
4393
+ /**
4394
+ * Fetch one persisted span from a trace without loading the full trace.
4395
+ * Name lookups return the last matching span by default. Pass `occurrence`
4396
+ * as `"first"` or a zero-based index to select a different match.
4397
+ */
4398
+ async getTraceSpan(traceId, lookup) {
4399
+ validateTraceId(traceId);
4400
+ const hasId = lookup.id !== void 0;
4401
+ const hasName = lookup.name !== void 0;
4402
+ if (hasId === hasName) {
4403
+ throw new BitfabError("Provide exactly one of id or name");
4404
+ }
4405
+ if (hasId) {
4406
+ validateSpanId(lookup.id);
4407
+ } else {
4408
+ if (lookup.name.length === 0) {
4409
+ throw new BitfabError("name must be a non-empty string");
4410
+ }
4411
+ const occurrence = lookup.occurrence ?? "last";
4412
+ if (occurrence !== "first" && occurrence !== "last" && (!Number.isInteger(occurrence) || occurrence < 0)) {
4413
+ throw new BitfabError(
4414
+ 'occurrence must be "first", "last", or a non-negative integer'
4415
+ );
4416
+ }
4417
+ }
4418
+ return this.httpClient.getTraceSpan(traceId, lookup);
4419
+ }
4209
4420
  /**
4210
4421
  * Get a function wrapper for a specific trace function key.
4211
4422
  *
@@ -4264,6 +4475,7 @@ var Bitfab = class {
4264
4475
  };
4265
4476
  }
4266
4477
  return this.httpClient.sendExternalTrace({
4478
+ id: params.traceId,
4267
4479
  type: "sdk-function",
4268
4480
  source: "typescript-sdk-function",
4269
4481
  traceFunctionKey: params.traceFunctionKey,
@@ -4319,6 +4531,8 @@ var Bitfab = class {
4319
4531
  externalSpan.input_source_span_id = params.inputSourceSpanId;
4320
4532
  }
4321
4533
  return this.httpClient.sendExternalSpan({
4534
+ id: params.spanId,
4535
+ traceId: params.traceId,
4322
4536
  type: "sdk-function",
4323
4537
  source: "typescript-sdk-function",
4324
4538
  sourceTraceId: params.traceId,
@@ -4328,26 +4542,14 @@ var Bitfab = class {
4328
4542
  ...params.mocked && { mocked: true }
4329
4543
  });
4330
4544
  }
4331
- /**
4332
- * Replay historical traces through a function and create a test run.
4333
- *
4334
- * Fetches the last N traces for the given trace function key, re-runs each
4335
- * through the provided function, and returns comparison data.
4336
- *
4337
- * Accepts either a `withSpan`-wrapped function (under the same key) or any
4338
- * plain callable: plain callables are wrapped internally so each replayed
4339
- * invocation records a trace tied to the test run. The plain-callable form
4340
- * is how handler-instrumented workflows (LangGraph/LangChain, Claude Agent
4341
- * SDK) replay - those record traces under a key with no `withSpan`-wrapped
4342
- * root in the app.
4343
- *
4344
- * @param traceFunctionKey - The trace function key to replay
4345
- * @param fn - The function to run recorded inputs through
4346
- * @param options - Optional replay options. When `traceIds` is passed,
4347
- * `limit` is ignored (with a warning): an explicit ID list already
4348
- * determines how many traces replay.
4349
- * @returns ReplayResult with items, testRunId, and testRunUrl
4350
- */
4545
+ registerMockOverride(overrideOrMatch, value) {
4546
+ const override = typeof overrideOrMatch === "function" ? { match: overrideOrMatch, value } : overrideOrMatch;
4547
+ this.mockOverrides.push(override);
4548
+ }
4549
+ /** Remove all overrides registered via {@link registerMockOverride}. */
4550
+ clearMockOverrides() {
4551
+ this.mockOverrides.length = 0;
4552
+ }
4351
4553
  async replay(traceFunctionKey, fn, options) {
4352
4554
  const wrappedKey = fn._bitfabTraceFunctionKey;
4353
4555
  let replayFn = fn;
@@ -4368,7 +4570,8 @@ var Bitfab = class {
4368
4570
  this.serviceUrl,
4369
4571
  traceFunctionKey,
4370
4572
  replayFn,
4371
- options
4573
+ options,
4574
+ this.mockOverrides
4372
4575
  );
4373
4576
  }
4374
4577
  };