@bitfab/sdk 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/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,7 +394,7 @@ 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, environment, adaptInputs) {
381
398
  const lease = environment ? serverItem.dbBranchLease : void 0;
382
399
  let inputs = [];
383
400
  let originalOutput;
@@ -396,28 +413,45 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
396
413
  sourceSpanId: serverItem.externalSpanId
397
414
  });
398
415
  }
416
+ const hasOverrides = resolvedOverrides.length > 0;
417
+ const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
418
+ const includeOutputs = mockStrategy === "all";
399
419
  let mockTree;
400
- if (mockStrategy === "all" || mockStrategy === "marked") {
420
+ if (needTree) {
401
421
  try {
402
422
  const treeResponse = await httpClient.getSpanTree(
403
- serverItem.externalSpanId
423
+ serverItem.externalSpanId,
424
+ { includeOutputs }
404
425
  );
405
426
  if (treeResponse.root) {
406
427
  mockTree = buildMockTree(treeResponse.root);
407
- } else if (mockStrategy === "all") {
428
+ } else if (mockStrategy === "all" || hasOverrides) {
408
429
  throw new BitfabError(
409
- `Replay mock strategy "all" requires a span tree root for source span ${serverItem.externalSpanId}.`
430
+ `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for source span ${serverItem.externalSpanId}.`
410
431
  );
411
432
  } else {
412
433
  mockTree = void 0;
413
434
  }
414
435
  } catch (e) {
415
- if (mockStrategy === "all") {
436
+ if (mockStrategy === "all" || hasOverrides) {
416
437
  throw e;
417
438
  }
418
439
  mockTree = void 0;
419
440
  }
420
441
  }
442
+ const outputCache = /* @__PURE__ */ new Map();
443
+ const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
444
+ let pending = outputCache.get(externalSpanId);
445
+ if (!pending) {
446
+ pending = httpClient.getExternalSpan(externalSpanId).then(
447
+ (s) => deserializeOutput(
448
+ s.rawData?.span_data ?? {}
449
+ )
450
+ );
451
+ outputCache.set(externalSpanId, pending);
452
+ }
453
+ return pending;
454
+ } : void 0;
421
455
  const maybePromise = runWithReplayContext(
422
456
  {
423
457
  testRunId,
@@ -428,6 +462,8 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
428
462
  mockTree,
429
463
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
430
464
  mockStrategy,
465
+ mockOverrides: hasOverrides ? resolvedOverrides : void 0,
466
+ fetchSpanOutput,
431
467
  dbBranchLease: lease,
432
468
  pendingPersistence
433
469
  },
@@ -484,7 +520,7 @@ async function mapWithConcurrency(tasks, maxConcurrency, onSettled) {
484
520
  await Promise.all(workers);
485
521
  return results;
486
522
  }
487
- async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
523
+ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
488
524
  if (options?.traceIds !== void 0) {
489
525
  if (options.traceIds.length === 0) {
490
526
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -524,6 +560,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
524
560
  );
525
561
  const mockStrategy = options?.mock ?? "marked";
526
562
  const maxConcurrency = options?.maxConcurrency ?? 10;
563
+ const resolvedOverrides = [
564
+ ...normalizeMockOverrides(options?.mockOverride),
565
+ ...registeredOverrides
566
+ ];
527
567
  const tasks = serverItems.map(
528
568
  (serverItem) => () => processItem(
529
569
  httpClient,
@@ -531,6 +571,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
531
571
  fn,
532
572
  testRunId,
533
573
  mockStrategy,
574
+ resolvedOverrides,
534
575
  options?.environment,
535
576
  options?.adaptInputs
536
577
  )
@@ -657,6 +698,7 @@ var init_replay = __esm({
657
698
  "src/replay.ts"() {
658
699
  "use strict";
659
700
  init_errors();
701
+ init_mockOverride();
660
702
  init_randomUuid();
661
703
  init_replayContext();
662
704
  init_serialize();
@@ -697,7 +739,7 @@ registerAsyncLocalStorageClass(
697
739
  );
698
740
 
699
741
  // src/version.generated.ts
700
- var __version__ = "0.28.10";
742
+ var __version__ = "0.29.0";
701
743
 
702
744
  // src/constants.ts
703
745
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
@@ -937,6 +979,50 @@ var HttpClient = class {
937
979
  async lookupFunction(name) {
938
980
  return this.request("/api/sdk/functions/lookup", { name });
939
981
  }
982
+ async getTraceSpan(traceId, lookup) {
983
+ const searchParams = new URLSearchParams();
984
+ if (lookup.id !== void 0) {
985
+ searchParams.set("id", lookup.id);
986
+ } else {
987
+ searchParams.set("name", lookup.name);
988
+ searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
989
+ }
990
+ const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
991
+ const response = await this.get(endpoint);
992
+ return response.span;
993
+ }
994
+ async get(endpoint) {
995
+ const url = `${this.serviceUrl}${endpoint}`;
996
+ const controller = new AbortController();
997
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
998
+ try {
999
+ const response = await fetch(url, {
1000
+ method: "GET",
1001
+ headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
1002
+ signal: controller.signal
1003
+ });
1004
+ if (!response.ok) {
1005
+ const errorText = await response.text();
1006
+ throw new BitfabError(
1007
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1008
+ );
1009
+ }
1010
+ return await response.json();
1011
+ } catch (error) {
1012
+ if (error instanceof BitfabError) {
1013
+ throw error;
1014
+ }
1015
+ if (error instanceof Error) {
1016
+ if (error.name === "AbortError") {
1017
+ throw new BitfabError(`Request timed out after ${this.timeout}ms`);
1018
+ }
1019
+ throw new BitfabError(error.message);
1020
+ }
1021
+ throw new BitfabError("Unknown error occurred");
1022
+ } finally {
1023
+ clearTimeout(timeoutId);
1024
+ }
1025
+ }
940
1026
  /**
941
1027
  * Send an internal trace (from BAML execution).
942
1028
  * Fire-and-forget with awaitOnExit - doesn't block the caller.
@@ -993,12 +1079,12 @@ var HttpClient = class {
993
1079
  });
994
1080
  }
995
1081
  /**
996
- * Partial update of an existing external trace identified by sourceTraceId.
1082
+ * Partial update of an existing trace identified by its Bitfab trace ID.
997
1083
  * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
998
1084
  * returns a tracked promise that callers may optionally await.
999
1085
  */
1000
- patchTrace(sourceTraceId, payload) {
1001
- const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
1086
+ patchTrace(traceId, payload) {
1087
+ const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
1002
1088
  return awaitOnExit(
1003
1089
  this.request(endpoint, payload, { method: "PATCH" })
1004
1090
  ).catch((error) => {
@@ -1082,9 +1168,14 @@ var HttpClient = class {
1082
1168
  /**
1083
1169
  * Fetch the span tree for a root span.
1084
1170
  * Blocking GET request.
1171
+ *
1172
+ * Pass `includeOutputs: false` for a payload-free tree (structure +
1173
+ * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
1174
+ * span instead of all up front. Omit it (default eager) for `mock: "all"`.
1085
1175
  */
1086
- async getSpanTree(externalSpanId) {
1087
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
1176
+ async getSpanTree(externalSpanId, options) {
1177
+ const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
1178
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
1088
1179
  const controller = new AbortController();
1089
1180
  const timeoutId = setTimeout(() => controller.abort(), 3e4);
1090
1181
  try {
@@ -1336,6 +1427,7 @@ var BitfabClaudeAgentHandler = class {
1336
1427
  const traceId = this.ensureTrace();
1337
1428
  const { safe: safeInput, dropped: inputDropped } = toJsonSafeReport(inputData);
1338
1429
  const spanInfo = {
1430
+ id: randomUuid(),
1339
1431
  spanId,
1340
1432
  traceId,
1341
1433
  parentId: parentId ?? null,
@@ -1399,6 +1491,8 @@ var BitfabClaudeAgentHandler = class {
1399
1491
  rawSpan.parent_id = spanInfo.parentId;
1400
1492
  }
1401
1493
  const payload = {
1494
+ id: spanInfo.id,
1495
+ traceId: spanInfo.traceId,
1402
1496
  type: "sdk-function",
1403
1497
  source: "typescript-sdk-claude-agent-sdk",
1404
1498
  traceFunctionKey: this.traceFunctionKey,
@@ -1428,6 +1522,7 @@ var BitfabClaudeAgentHandler = class {
1428
1522
  externalTrace.metadata = metadata;
1429
1523
  }
1430
1524
  const traceData = {
1525
+ id: traceId,
1431
1526
  type: "sdk-function",
1432
1527
  source: "typescript-sdk-claude-agent-sdk",
1433
1528
  traceFunctionKey: this.traceFunctionKey,
@@ -1696,6 +1791,7 @@ var BitfabClaudeAgentHandler = class {
1696
1791
  }
1697
1792
  Object.assign(llmContext, this.currentLlmUsage);
1698
1793
  const spanInfo = {
1794
+ id: randomUuid(),
1699
1795
  spanId,
1700
1796
  traceId,
1701
1797
  parentId,
@@ -2342,6 +2438,7 @@ var BitfabLangGraphCallbackHandler = class {
2342
2438
  const contexts = Object.keys(lgMetadata).length > 0 ? [lgMetadata] : [];
2343
2439
  const { safe: safeInput, dropped: inputDropped } = toJsonSafeReport(inputData);
2344
2440
  const spanInfo = {
2441
+ id: randomUuid(),
2345
2442
  spanId: runId,
2346
2443
  traceId: invocation.traceId,
2347
2444
  rootRunId: invocation.rootRunId,
@@ -2420,6 +2517,8 @@ var BitfabLangGraphCallbackHandler = class {
2420
2517
  rawSpan.parent_id = spanInfo.parentId;
2421
2518
  }
2422
2519
  const payload = {
2520
+ id: spanInfo.id,
2521
+ traceId: spanInfo.traceId,
2423
2522
  type: "sdk-function",
2424
2523
  source: "typescript-sdk-langgraph",
2425
2524
  traceFunctionKey: this.traceFunctionKey,
@@ -2435,6 +2534,7 @@ var BitfabLangGraphCallbackHandler = class {
2435
2534
  sendTraceCompletion(rootSpan, activeContext) {
2436
2535
  const completed = activeContext === null;
2437
2536
  const traceData = {
2537
+ id: rootSpan.traceId,
2438
2538
  type: "sdk-function",
2439
2539
  source: "typescript-sdk-langgraph",
2440
2540
  traceFunctionKey: this.traceFunctionKey,
@@ -2454,6 +2554,7 @@ var BitfabLangGraphCallbackHandler = class {
2454
2554
  }
2455
2555
  sendTraceStart(rootSpan) {
2456
2556
  const traceData = {
2557
+ id: rootSpan.traceId,
2457
2558
  type: "sdk-function",
2458
2559
  source: "typescript-sdk-langgraph",
2459
2560
  traceFunctionKey: this.traceFunctionKey,
@@ -2660,6 +2761,9 @@ var BitfabLangGraphCallbackHandler = class {
2660
2761
  }
2661
2762
  };
2662
2763
 
2764
+ // src/client.ts
2765
+ init_mockOverride();
2766
+
2663
2767
  // src/openaiAgentSdk.ts
2664
2768
  var BitfabOpenAIAgentHandler = class {
2665
2769
  constructor(config) {
@@ -2812,6 +2916,7 @@ var ReplayEnvironment = class {
2812
2916
  init_serialize();
2813
2917
 
2814
2918
  // src/tracing.ts
2919
+ init_randomUuid();
2815
2920
  var BitfabOpenAITracingProcessor = class {
2816
2921
  /**
2817
2922
  * Initialize the tracing processor.
@@ -2821,6 +2926,7 @@ var BitfabOpenAITracingProcessor = class {
2821
2926
  constructor(config) {
2822
2927
  this.activeTraces = {};
2823
2928
  this.activeSpanMappings = {};
2929
+ this.canonicalTraceIds = {};
2824
2930
  this.httpClient = new HttpClient({
2825
2931
  apiKey: config.apiKey,
2826
2932
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
@@ -2828,6 +2934,15 @@ var BitfabOpenAITracingProcessor = class {
2828
2934
  });
2829
2935
  this.getActiveSpanContext = config.getActiveSpanContext ?? null;
2830
2936
  }
2937
+ getCanonicalTraceId(sourceTraceId) {
2938
+ const existing = this.canonicalTraceIds[sourceTraceId];
2939
+ if (existing) {
2940
+ return existing;
2941
+ }
2942
+ const created = randomUuid();
2943
+ this.canonicalTraceIds[sourceTraceId] = created;
2944
+ return created;
2945
+ }
2831
2946
  /**
2832
2947
  * Called when a trace is started.
2833
2948
  * If there's an active withSpan context, the trace ID is remapped to the
@@ -2839,7 +2954,12 @@ var BitfabOpenAITracingProcessor = class {
2839
2954
  if (activeContext) {
2840
2955
  this.activeSpanMappings[trace.traceId] = activeContext;
2841
2956
  }
2842
- this.sendTrace(trace, activeContext ? { id: activeContext.traceId } : {});
2957
+ const canonicalTraceId = activeContext?.traceId ?? this.getCanonicalTraceId(trace.traceId);
2958
+ this.canonicalTraceIds[trace.traceId] = canonicalTraceId;
2959
+ this.sendTrace(trace, {
2960
+ id: canonicalTraceId,
2961
+ sourceTraceId: activeContext?.traceId
2962
+ });
2843
2963
  }
2844
2964
  /**
2845
2965
  * Called when a trace is ended.
@@ -2848,11 +2968,13 @@ var BitfabOpenAITracingProcessor = class {
2848
2968
  */
2849
2969
  async onTraceEnd(trace) {
2850
2970
  const mapping = this.activeSpanMappings[trace.traceId];
2851
- this.sendTrace(
2852
- trace,
2853
- mapping ? { id: mapping.traceId } : { completed: true }
2854
- );
2971
+ this.sendTrace(trace, {
2972
+ completed: mapping === void 0,
2973
+ id: mapping?.traceId ?? this.getCanonicalTraceId(trace.traceId),
2974
+ sourceTraceId: mapping?.traceId
2975
+ });
2855
2976
  delete this.activeSpanMappings[trace.traceId];
2977
+ delete this.canonicalTraceIds[trace.traceId];
2856
2978
  delete this.activeTraces[trace.traceId];
2857
2979
  }
2858
2980
  /**
@@ -2882,22 +3004,25 @@ var BitfabOpenAITracingProcessor = class {
2882
3004
  async shutdown(_timeout) {
2883
3005
  this.activeTraces = {};
2884
3006
  this.activeSpanMappings = {};
3007
+ this.canonicalTraceIds = {};
2885
3008
  }
2886
3009
  /**
2887
3010
  * Send trace to Bitfab API (fire-and-forget).
2888
3011
  * When traceIdOverride is provided, the trace ID is remapped to link
2889
3012
  * the OpenAI trace into an outer withSpan trace.
2890
3013
  */
2891
- sendTrace(trace, overrides = {}) {
3014
+ sendTrace(trace, options = {}) {
2892
3015
  try {
2893
- const { completed, ...traceOverrides } = overrides;
2894
3016
  const traceData = trace.toJSON();
2895
- Object.assign(traceData, traceOverrides);
3017
+ if (options.sourceTraceId) {
3018
+ traceData.id = options.sourceTraceId;
3019
+ }
2896
3020
  this.httpClient.sendExternalTrace({
3021
+ ...options.id && { id: options.id },
2897
3022
  type: "openai",
2898
3023
  source: "typescript-sdk-openai-tracing",
2899
3024
  externalTrace: traceData,
2900
- completed: completed ?? false
3025
+ completed: options.completed ?? false
2901
3026
  });
2902
3027
  } catch {
2903
3028
  }
@@ -2983,6 +3108,7 @@ var BitfabOpenAITracingProcessor = class {
2983
3108
  */
2984
3109
  buildSpanPayload(serializedSpan, errors) {
2985
3110
  const payload = {
3111
+ id: randomUuid(),
2986
3112
  type: "openai",
2987
3113
  source: "typescript-sdk-openai-tracing",
2988
3114
  sourceTraceId: serializedSpan.trace_id ?? "unknown",
@@ -3005,6 +3131,10 @@ var BitfabOpenAITracingProcessor = class {
3005
3131
  this.extractSpanInputResponse(span, serializedSpan, errors);
3006
3132
  this.applySpanOverrides(serializedSpan, span.traceId ?? "");
3007
3133
  const payload = this.buildSpanPayload(serializedSpan, errors);
3134
+ const canonicalTraceId = span.traceId ? this.getCanonicalTraceId(span.traceId) : void 0;
3135
+ if (canonicalTraceId) {
3136
+ payload.traceId = canonicalTraceId;
3137
+ }
3008
3138
  this.httpClient.sendExternalSpan(payload);
3009
3139
  }
3010
3140
  };
@@ -3321,24 +3451,19 @@ function extractContextFromCollector(collector) {
3321
3451
  return null;
3322
3452
  }
3323
3453
  }
3324
- var TRACE_ID_PATTERN = /^[a-zA-Z0-9_\-.:]+$/;
3325
- var TRACE_ID_MAX_LENGTH = 256;
3454
+ 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;
3326
3455
  function validateTraceId(traceId) {
3327
- if (typeof traceId !== "string" || traceId.length === 0) {
3328
- throw new BitfabError("traceId is required and must be a non-empty string");
3329
- }
3330
- if (traceId.length > TRACE_ID_MAX_LENGTH) {
3331
- throw new BitfabError(
3332
- `traceId must be ${TRACE_ID_MAX_LENGTH} characters or fewer`
3333
- );
3456
+ if (typeof traceId !== "string" || !UUID_PATTERN.test(traceId)) {
3457
+ throw new BitfabError("traceId must be a valid Bitfab trace ID");
3334
3458
  }
3335
- if (!TRACE_ID_PATTERN.test(traceId)) {
3336
- throw new BitfabError(
3337
- `traceId may only contain letters, digits, "_", "-", ".", ":"`
3338
- );
3459
+ }
3460
+ function validateSpanId(id) {
3461
+ if (typeof id !== "string" || !UUID_PATTERN.test(id)) {
3462
+ throw new BitfabError("id must be a valid Bitfab span ID");
3339
3463
  }
3340
3464
  }
3341
3465
  var noOpSpan = {
3466
+ id: "",
3342
3467
  traceId: "",
3343
3468
  addContext() {
3344
3469
  },
@@ -3362,6 +3487,7 @@ function getCurrentSpan() {
3362
3487
  return noOpSpan;
3363
3488
  }
3364
3489
  return {
3490
+ id: current.spanId,
3365
3491
  traceId: current.traceId,
3366
3492
  addContext(context) {
3367
3493
  try {
@@ -3453,6 +3579,12 @@ var Bitfab = class {
3453
3579
  constructor(config) {
3454
3580
  /** Gate the empty-key warning to fire at most once. */
3455
3581
  this.apiKeyWarned = false;
3582
+ /**
3583
+ * Mock overrides registered via {@link Bitfab.registerMockOverride}, applied
3584
+ * to every `replay` on this client (after any per-call `mockOverride`). In
3585
+ * registration order; first matcher wins within this list.
3586
+ */
3587
+ this.mockOverrides = [];
3456
3588
  this.apiKeyConfig = config.apiKey;
3457
3589
  this.serviceUrl = config.serviceUrl ?? DEFAULT_SERVICE_URL;
3458
3590
  this.timeout = config.timeout ?? 12e4;
@@ -4065,24 +4197,77 @@ var Bitfab = class {
4065
4197
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
4066
4198
  const callIndex = counters.get(counterKey) ?? 0;
4067
4199
  counters.set(counterKey, callIndex + 1);
4068
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4069
- if (shouldMock) {
4070
- const mockKey = `${counterKey}:${callIndex}`;
4071
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4072
- if (mockSpan) {
4073
- let output = mockSpan.output;
4074
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4075
- output = deserializeValue({
4076
- json: mockSpan.output,
4077
- meta: mockSpan.outputMeta
4078
- });
4079
- }
4200
+ const mockKey = `${counterKey}:${callIndex}`;
4201
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4202
+ const emitMock = (output) => {
4203
+ void sendSpan({ result: output, mocked: true });
4204
+ if (fnReturnsPromise) {
4205
+ return Promise.resolve(output);
4206
+ }
4207
+ return output;
4208
+ };
4209
+ const emitMockAsync = (pending) => {
4210
+ if (!fnReturnsPromise) {
4211
+ throw new BitfabError(
4212
+ `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.`
4213
+ );
4214
+ }
4215
+ return (async () => {
4216
+ const output = await pending;
4080
4217
  void sendSpan({ result: output, mocked: true });
4081
- if (fnReturnsPromise) {
4082
- return Promise.resolve(output);
4083
- }
4084
4218
  return output;
4219
+ })();
4220
+ };
4221
+ const resolveRecordedOutput = () => {
4222
+ const hasInlineOutput = mockSpan?.output !== void 0 || mockSpan?.outputMeta !== void 0;
4223
+ if (!hasInlineOutput && replayCtxForMock.fetchSpanOutput && mockSpan?.externalSpanId) {
4224
+ return replayCtxForMock.fetchSpanOutput(mockSpan.externalSpanId);
4225
+ }
4226
+ if (!mockSpan) {
4227
+ return Promise.reject(
4228
+ new BitfabError(
4229
+ `No recorded span to source output for "${traceFunctionKey}".`
4230
+ )
4231
+ );
4232
+ }
4233
+ let output = mockSpan.output;
4234
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4235
+ output = deserializeValue({
4236
+ json: mockSpan.output,
4237
+ meta: mockSpan.outputMeta
4238
+ });
4239
+ }
4240
+ return output;
4241
+ };
4242
+ if (replayCtxForMock.mockOverrides?.length) {
4243
+ const nodeMeta = {
4244
+ traceFunctionKey,
4245
+ spanName: baseSpanParams.spanName,
4246
+ type: options.type ?? "custom",
4247
+ originalSpanId: mockSpan?.sourceSpanId
4248
+ };
4249
+ const override = replayCtxForMock.mockOverrides.find(
4250
+ (o) => o.match(nodeMeta)
4251
+ );
4252
+ if (override) {
4253
+ const injected = resolveMockValue(override.value, {
4254
+ node: nodeMeta,
4255
+ inputs: args,
4256
+ getOriginalOutput: () => Promise.resolve(resolveRecordedOutput())
4257
+ });
4258
+ if (injected instanceof Promise) {
4259
+ return emitMockAsync(injected);
4260
+ }
4261
+ return emitMock(injected);
4262
+ }
4263
+ }
4264
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4265
+ if (shouldMock && mockSpan) {
4266
+ const recorded = resolveRecordedOutput();
4267
+ if (recorded instanceof Promise) {
4268
+ return emitMockAsync(recorded);
4085
4269
  }
4270
+ return emitMock(recorded);
4086
4271
  }
4087
4272
  }
4088
4273
  const recordSpan = (result) => {
@@ -4158,20 +4343,19 @@ var Bitfab = class {
4158
4343
  }
4159
4344
  /**
4160
4345
  * Get a detached handle to a previously-created trace, looked up by the
4161
- * caller-supplied id (the same id passed at trace creation).
4346
+ * canonical Bitfab trace ID.
4162
4347
  *
4163
4348
  * The returned handle is not tied to AsyncLocalStorage - each method sends
4164
4349
  * to the server immediately. Useful for adding context to a trace from a
4165
4350
  * different process or thread than the one that created it.
4166
4351
  *
4167
- * Throws synchronously if `traceId` is malformed (empty, too long, or
4168
- * contains characters outside `[a-zA-Z0-9_\-.:]`). Server returns 404 if
4169
- * no trace exists with that id in the org; the failure surfaces as a
4352
+ * Throws synchronously if `traceId` is not a valid Bitfab trace ID. The
4353
+ * server returns 404 if no trace exists with that ID in the org; the failure surfaces as a
4170
4354
  * logged warning (fire-and-forget) or via the awaited promise.
4171
4355
  *
4172
4356
  * Example:
4173
4357
  * ```typescript
4174
- * const trace = client.getTrace("order_abc_123");
4358
+ * const trace = client.getTrace(traceId);
4175
4359
  * await trace.addContext({ refund_status: "approved" });
4176
4360
  * await trace.setMetadata({ region: "us-west" });
4177
4361
  * ```
@@ -4211,6 +4395,33 @@ var Bitfab = class {
4211
4395
  }
4212
4396
  };
4213
4397
  }
4398
+ /**
4399
+ * Fetch one persisted span from a trace without loading the full trace.
4400
+ * Name lookups return the last matching span by default. Pass `occurrence`
4401
+ * as `"first"` or a zero-based index to select a different match.
4402
+ */
4403
+ async getTraceSpan(traceId, lookup) {
4404
+ validateTraceId(traceId);
4405
+ const hasId = lookup.id !== void 0;
4406
+ const hasName = lookup.name !== void 0;
4407
+ if (hasId === hasName) {
4408
+ throw new BitfabError("Provide exactly one of id or name");
4409
+ }
4410
+ if (hasId) {
4411
+ validateSpanId(lookup.id);
4412
+ } else {
4413
+ if (lookup.name.length === 0) {
4414
+ throw new BitfabError("name must be a non-empty string");
4415
+ }
4416
+ const occurrence = lookup.occurrence ?? "last";
4417
+ if (occurrence !== "first" && occurrence !== "last" && (!Number.isInteger(occurrence) || occurrence < 0)) {
4418
+ throw new BitfabError(
4419
+ 'occurrence must be "first", "last", or a non-negative integer'
4420
+ );
4421
+ }
4422
+ }
4423
+ return this.httpClient.getTraceSpan(traceId, lookup);
4424
+ }
4214
4425
  /**
4215
4426
  * Get a function wrapper for a specific trace function key.
4216
4427
  *
@@ -4269,6 +4480,7 @@ var Bitfab = class {
4269
4480
  };
4270
4481
  }
4271
4482
  return this.httpClient.sendExternalTrace({
4483
+ id: params.traceId,
4272
4484
  type: "sdk-function",
4273
4485
  source: "typescript-sdk-function",
4274
4486
  traceFunctionKey: params.traceFunctionKey,
@@ -4324,6 +4536,8 @@ var Bitfab = class {
4324
4536
  externalSpan.input_source_span_id = params.inputSourceSpanId;
4325
4537
  }
4326
4538
  return this.httpClient.sendExternalSpan({
4539
+ id: params.spanId,
4540
+ traceId: params.traceId,
4327
4541
  type: "sdk-function",
4328
4542
  source: "typescript-sdk-function",
4329
4543
  sourceTraceId: params.traceId,
@@ -4333,26 +4547,14 @@ var Bitfab = class {
4333
4547
  ...params.mocked && { mocked: true }
4334
4548
  });
4335
4549
  }
4336
- /**
4337
- * Replay historical traces through a function and create a test run.
4338
- *
4339
- * Fetches the last N traces for the given trace function key, re-runs each
4340
- * through the provided function, and returns comparison data.
4341
- *
4342
- * Accepts either a `withSpan`-wrapped function (under the same key) or any
4343
- * plain callable: plain callables are wrapped internally so each replayed
4344
- * invocation records a trace tied to the test run. The plain-callable form
4345
- * is how handler-instrumented workflows (LangGraph/LangChain, Claude Agent
4346
- * SDK) replay - those record traces under a key with no `withSpan`-wrapped
4347
- * root in the app.
4348
- *
4349
- * @param traceFunctionKey - The trace function key to replay
4350
- * @param fn - The function to run recorded inputs through
4351
- * @param options - Optional replay options. When `traceIds` is passed,
4352
- * `limit` is ignored (with a warning): an explicit ID list already
4353
- * determines how many traces replay.
4354
- * @returns ReplayResult with items, testRunId, and testRunUrl
4355
- */
4550
+ registerMockOverride(overrideOrMatch, value) {
4551
+ const override = typeof overrideOrMatch === "function" ? { match: overrideOrMatch, value } : overrideOrMatch;
4552
+ this.mockOverrides.push(override);
4553
+ }
4554
+ /** Remove all overrides registered via {@link registerMockOverride}. */
4555
+ clearMockOverrides() {
4556
+ this.mockOverrides.length = 0;
4557
+ }
4356
4558
  async replay(traceFunctionKey, fn, options) {
4357
4559
  const wrappedKey = fn._bitfabTraceFunctionKey;
4358
4560
  let replayFn = fn;
@@ -4373,7 +4575,8 @@ var Bitfab = class {
4373
4575
  this.serviceUrl,
4374
4576
  traceFunctionKey,
4375
4577
  replayFn,
4376
- options
4578
+ options,
4579
+ this.mockOverrides
4377
4580
  );
4378
4581
  }
4379
4582
  };