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/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,7 +403,7 @@ 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, environment, adaptInputs) {
390
407
  const lease = environment ? serverItem.dbBranchLease : void 0;
391
408
  let inputs = [];
392
409
  let originalOutput;
@@ -405,28 +422,45 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
405
422
  sourceSpanId: serverItem.externalSpanId
406
423
  });
407
424
  }
425
+ const hasOverrides = resolvedOverrides.length > 0;
426
+ const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
427
+ const includeOutputs = mockStrategy === "all";
408
428
  let mockTree;
409
- if (mockStrategy === "all" || mockStrategy === "marked") {
429
+ if (needTree) {
410
430
  try {
411
431
  const treeResponse = await httpClient.getSpanTree(
412
- serverItem.externalSpanId
432
+ serverItem.externalSpanId,
433
+ { includeOutputs }
413
434
  );
414
435
  if (treeResponse.root) {
415
436
  mockTree = buildMockTree(treeResponse.root);
416
- } else if (mockStrategy === "all") {
437
+ } else if (mockStrategy === "all" || hasOverrides) {
417
438
  throw new BitfabError(
418
- `Replay mock strategy "all" requires a span tree root for source span ${serverItem.externalSpanId}.`
439
+ `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for source span ${serverItem.externalSpanId}.`
419
440
  );
420
441
  } else {
421
442
  mockTree = void 0;
422
443
  }
423
444
  } catch (e) {
424
- if (mockStrategy === "all") {
445
+ if (mockStrategy === "all" || hasOverrides) {
425
446
  throw e;
426
447
  }
427
448
  mockTree = void 0;
428
449
  }
429
450
  }
451
+ const outputCache = /* @__PURE__ */ new Map();
452
+ const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
453
+ let pending = outputCache.get(externalSpanId);
454
+ if (!pending) {
455
+ pending = httpClient.getExternalSpan(externalSpanId).then(
456
+ (s) => deserializeOutput(
457
+ s.rawData?.span_data ?? {}
458
+ )
459
+ );
460
+ outputCache.set(externalSpanId, pending);
461
+ }
462
+ return pending;
463
+ } : void 0;
430
464
  const maybePromise = runWithReplayContext(
431
465
  {
432
466
  testRunId,
@@ -437,6 +471,8 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
437
471
  mockTree,
438
472
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
439
473
  mockStrategy,
474
+ mockOverrides: hasOverrides ? resolvedOverrides : void 0,
475
+ fetchSpanOutput,
440
476
  dbBranchLease: lease,
441
477
  pendingPersistence
442
478
  },
@@ -493,7 +529,7 @@ async function mapWithConcurrency(tasks, maxConcurrency, onSettled) {
493
529
  await Promise.all(workers);
494
530
  return results;
495
531
  }
496
- async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
532
+ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
497
533
  if (options?.traceIds !== void 0) {
498
534
  if (options.traceIds.length === 0) {
499
535
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -533,6 +569,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
533
569
  );
534
570
  const mockStrategy = options?.mock ?? "marked";
535
571
  const maxConcurrency = options?.maxConcurrency ?? 10;
572
+ const resolvedOverrides = [
573
+ ...normalizeMockOverrides(options?.mockOverride),
574
+ ...registeredOverrides
575
+ ];
536
576
  const tasks = serverItems.map(
537
577
  (serverItem) => () => processItem(
538
578
  httpClient,
@@ -540,6 +580,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
540
580
  fn,
541
581
  testRunId,
542
582
  mockStrategy,
583
+ resolvedOverrides,
543
584
  options?.environment,
544
585
  options?.adaptInputs
545
586
  )
@@ -666,6 +707,7 @@ var init_replay = __esm({
666
707
  "src/replay.ts"() {
667
708
  "use strict";
668
709
  init_errors();
710
+ init_mockOverride();
669
711
  init_randomUuid();
670
712
  init_replayContext();
671
713
  init_serialize();
@@ -706,7 +748,7 @@ registerAsyncLocalStorageClass(
706
748
  );
707
749
 
708
750
  // src/version.generated.ts
709
- var __version__ = "0.28.10";
751
+ var __version__ = "0.29.0";
710
752
 
711
753
  // src/constants.ts
712
754
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
@@ -946,6 +988,50 @@ var HttpClient = class {
946
988
  async lookupFunction(name) {
947
989
  return this.request("/api/sdk/functions/lookup", { name });
948
990
  }
991
+ async getTraceSpan(traceId, lookup) {
992
+ const searchParams = new URLSearchParams();
993
+ if (lookup.id !== void 0) {
994
+ searchParams.set("id", lookup.id);
995
+ } else {
996
+ searchParams.set("name", lookup.name);
997
+ searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
998
+ }
999
+ const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
1000
+ const response = await this.get(endpoint);
1001
+ return response.span;
1002
+ }
1003
+ async get(endpoint) {
1004
+ const url = `${this.serviceUrl}${endpoint}`;
1005
+ const controller = new AbortController();
1006
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1007
+ try {
1008
+ const response = await fetch(url, {
1009
+ method: "GET",
1010
+ headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
1011
+ signal: controller.signal
1012
+ });
1013
+ if (!response.ok) {
1014
+ const errorText = await response.text();
1015
+ throw new BitfabError(
1016
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1017
+ );
1018
+ }
1019
+ return await response.json();
1020
+ } catch (error) {
1021
+ if (error instanceof BitfabError) {
1022
+ throw error;
1023
+ }
1024
+ if (error instanceof Error) {
1025
+ if (error.name === "AbortError") {
1026
+ throw new BitfabError(`Request timed out after ${this.timeout}ms`);
1027
+ }
1028
+ throw new BitfabError(error.message);
1029
+ }
1030
+ throw new BitfabError("Unknown error occurred");
1031
+ } finally {
1032
+ clearTimeout(timeoutId);
1033
+ }
1034
+ }
949
1035
  /**
950
1036
  * Send an internal trace (from BAML execution).
951
1037
  * Fire-and-forget with awaitOnExit - doesn't block the caller.
@@ -1002,12 +1088,12 @@ var HttpClient = class {
1002
1088
  });
1003
1089
  }
1004
1090
  /**
1005
- * Partial update of an existing external trace identified by sourceTraceId.
1091
+ * Partial update of an existing trace identified by its Bitfab trace ID.
1006
1092
  * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
1007
1093
  * returns a tracked promise that callers may optionally await.
1008
1094
  */
1009
- patchTrace(sourceTraceId, payload) {
1010
- const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
1095
+ patchTrace(traceId, payload) {
1096
+ const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
1011
1097
  return awaitOnExit(
1012
1098
  this.request(endpoint, payload, { method: "PATCH" })
1013
1099
  ).catch((error) => {
@@ -1091,9 +1177,14 @@ var HttpClient = class {
1091
1177
  /**
1092
1178
  * Fetch the span tree for a root span.
1093
1179
  * Blocking GET request.
1180
+ *
1181
+ * Pass `includeOutputs: false` for a payload-free tree (structure +
1182
+ * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
1183
+ * span instead of all up front. Omit it (default eager) for `mock: "all"`.
1094
1184
  */
1095
- async getSpanTree(externalSpanId) {
1096
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
1185
+ async getSpanTree(externalSpanId, options) {
1186
+ const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
1187
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
1097
1188
  const controller = new AbortController();
1098
1189
  const timeoutId = setTimeout(() => controller.abort(), 3e4);
1099
1190
  try {
@@ -1345,6 +1436,7 @@ var BitfabClaudeAgentHandler = class {
1345
1436
  const traceId = this.ensureTrace();
1346
1437
  const { safe: safeInput, dropped: inputDropped } = toJsonSafeReport(inputData);
1347
1438
  const spanInfo = {
1439
+ id: randomUuid(),
1348
1440
  spanId,
1349
1441
  traceId,
1350
1442
  parentId: parentId ?? null,
@@ -1408,6 +1500,8 @@ var BitfabClaudeAgentHandler = class {
1408
1500
  rawSpan.parent_id = spanInfo.parentId;
1409
1501
  }
1410
1502
  const payload = {
1503
+ id: spanInfo.id,
1504
+ traceId: spanInfo.traceId,
1411
1505
  type: "sdk-function",
1412
1506
  source: "typescript-sdk-claude-agent-sdk",
1413
1507
  traceFunctionKey: this.traceFunctionKey,
@@ -1437,6 +1531,7 @@ var BitfabClaudeAgentHandler = class {
1437
1531
  externalTrace.metadata = metadata;
1438
1532
  }
1439
1533
  const traceData = {
1534
+ id: traceId,
1440
1535
  type: "sdk-function",
1441
1536
  source: "typescript-sdk-claude-agent-sdk",
1442
1537
  traceFunctionKey: this.traceFunctionKey,
@@ -1705,6 +1800,7 @@ var BitfabClaudeAgentHandler = class {
1705
1800
  }
1706
1801
  Object.assign(llmContext, this.currentLlmUsage);
1707
1802
  const spanInfo = {
1803
+ id: randomUuid(),
1708
1804
  spanId,
1709
1805
  traceId,
1710
1806
  parentId,
@@ -2351,6 +2447,7 @@ var BitfabLangGraphCallbackHandler = class {
2351
2447
  const contexts = Object.keys(lgMetadata).length > 0 ? [lgMetadata] : [];
2352
2448
  const { safe: safeInput, dropped: inputDropped } = toJsonSafeReport(inputData);
2353
2449
  const spanInfo = {
2450
+ id: randomUuid(),
2354
2451
  spanId: runId,
2355
2452
  traceId: invocation.traceId,
2356
2453
  rootRunId: invocation.rootRunId,
@@ -2429,6 +2526,8 @@ var BitfabLangGraphCallbackHandler = class {
2429
2526
  rawSpan.parent_id = spanInfo.parentId;
2430
2527
  }
2431
2528
  const payload = {
2529
+ id: spanInfo.id,
2530
+ traceId: spanInfo.traceId,
2432
2531
  type: "sdk-function",
2433
2532
  source: "typescript-sdk-langgraph",
2434
2533
  traceFunctionKey: this.traceFunctionKey,
@@ -2444,6 +2543,7 @@ var BitfabLangGraphCallbackHandler = class {
2444
2543
  sendTraceCompletion(rootSpan, activeContext) {
2445
2544
  const completed = activeContext === null;
2446
2545
  const traceData = {
2546
+ id: rootSpan.traceId,
2447
2547
  type: "sdk-function",
2448
2548
  source: "typescript-sdk-langgraph",
2449
2549
  traceFunctionKey: this.traceFunctionKey,
@@ -2463,6 +2563,7 @@ var BitfabLangGraphCallbackHandler = class {
2463
2563
  }
2464
2564
  sendTraceStart(rootSpan) {
2465
2565
  const traceData = {
2566
+ id: rootSpan.traceId,
2466
2567
  type: "sdk-function",
2467
2568
  source: "typescript-sdk-langgraph",
2468
2569
  traceFunctionKey: this.traceFunctionKey,
@@ -2669,6 +2770,9 @@ var BitfabLangGraphCallbackHandler = class {
2669
2770
  }
2670
2771
  };
2671
2772
 
2773
+ // src/client.ts
2774
+ init_mockOverride();
2775
+
2672
2776
  // src/openaiAgentSdk.ts
2673
2777
  var BitfabOpenAIAgentHandler = class {
2674
2778
  constructor(config) {
@@ -2821,6 +2925,7 @@ var ReplayEnvironment = class {
2821
2925
  init_serialize();
2822
2926
 
2823
2927
  // src/tracing.ts
2928
+ init_randomUuid();
2824
2929
  var BitfabOpenAITracingProcessor = class {
2825
2930
  /**
2826
2931
  * Initialize the tracing processor.
@@ -2830,6 +2935,7 @@ var BitfabOpenAITracingProcessor = class {
2830
2935
  constructor(config) {
2831
2936
  this.activeTraces = {};
2832
2937
  this.activeSpanMappings = {};
2938
+ this.canonicalTraceIds = {};
2833
2939
  this.httpClient = new HttpClient({
2834
2940
  apiKey: config.apiKey,
2835
2941
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
@@ -2837,6 +2943,15 @@ var BitfabOpenAITracingProcessor = class {
2837
2943
  });
2838
2944
  this.getActiveSpanContext = config.getActiveSpanContext ?? null;
2839
2945
  }
2946
+ getCanonicalTraceId(sourceTraceId) {
2947
+ const existing = this.canonicalTraceIds[sourceTraceId];
2948
+ if (existing) {
2949
+ return existing;
2950
+ }
2951
+ const created = randomUuid();
2952
+ this.canonicalTraceIds[sourceTraceId] = created;
2953
+ return created;
2954
+ }
2840
2955
  /**
2841
2956
  * Called when a trace is started.
2842
2957
  * If there's an active withSpan context, the trace ID is remapped to the
@@ -2848,7 +2963,12 @@ var BitfabOpenAITracingProcessor = class {
2848
2963
  if (activeContext) {
2849
2964
  this.activeSpanMappings[trace.traceId] = activeContext;
2850
2965
  }
2851
- this.sendTrace(trace, activeContext ? { id: activeContext.traceId } : {});
2966
+ const canonicalTraceId = activeContext?.traceId ?? this.getCanonicalTraceId(trace.traceId);
2967
+ this.canonicalTraceIds[trace.traceId] = canonicalTraceId;
2968
+ this.sendTrace(trace, {
2969
+ id: canonicalTraceId,
2970
+ sourceTraceId: activeContext?.traceId
2971
+ });
2852
2972
  }
2853
2973
  /**
2854
2974
  * Called when a trace is ended.
@@ -2857,11 +2977,13 @@ var BitfabOpenAITracingProcessor = class {
2857
2977
  */
2858
2978
  async onTraceEnd(trace) {
2859
2979
  const mapping = this.activeSpanMappings[trace.traceId];
2860
- this.sendTrace(
2861
- trace,
2862
- mapping ? { id: mapping.traceId } : { completed: true }
2863
- );
2980
+ this.sendTrace(trace, {
2981
+ completed: mapping === void 0,
2982
+ id: mapping?.traceId ?? this.getCanonicalTraceId(trace.traceId),
2983
+ sourceTraceId: mapping?.traceId
2984
+ });
2864
2985
  delete this.activeSpanMappings[trace.traceId];
2986
+ delete this.canonicalTraceIds[trace.traceId];
2865
2987
  delete this.activeTraces[trace.traceId];
2866
2988
  }
2867
2989
  /**
@@ -2891,22 +3013,25 @@ var BitfabOpenAITracingProcessor = class {
2891
3013
  async shutdown(_timeout) {
2892
3014
  this.activeTraces = {};
2893
3015
  this.activeSpanMappings = {};
3016
+ this.canonicalTraceIds = {};
2894
3017
  }
2895
3018
  /**
2896
3019
  * Send trace to Bitfab API (fire-and-forget).
2897
3020
  * When traceIdOverride is provided, the trace ID is remapped to link
2898
3021
  * the OpenAI trace into an outer withSpan trace.
2899
3022
  */
2900
- sendTrace(trace, overrides = {}) {
3023
+ sendTrace(trace, options = {}) {
2901
3024
  try {
2902
- const { completed, ...traceOverrides } = overrides;
2903
3025
  const traceData = trace.toJSON();
2904
- Object.assign(traceData, traceOverrides);
3026
+ if (options.sourceTraceId) {
3027
+ traceData.id = options.sourceTraceId;
3028
+ }
2905
3029
  this.httpClient.sendExternalTrace({
3030
+ ...options.id && { id: options.id },
2906
3031
  type: "openai",
2907
3032
  source: "typescript-sdk-openai-tracing",
2908
3033
  externalTrace: traceData,
2909
- completed: completed ?? false
3034
+ completed: options.completed ?? false
2910
3035
  });
2911
3036
  } catch {
2912
3037
  }
@@ -2992,6 +3117,7 @@ var BitfabOpenAITracingProcessor = class {
2992
3117
  */
2993
3118
  buildSpanPayload(serializedSpan, errors) {
2994
3119
  const payload = {
3120
+ id: randomUuid(),
2995
3121
  type: "openai",
2996
3122
  source: "typescript-sdk-openai-tracing",
2997
3123
  sourceTraceId: serializedSpan.trace_id ?? "unknown",
@@ -3014,6 +3140,10 @@ var BitfabOpenAITracingProcessor = class {
3014
3140
  this.extractSpanInputResponse(span, serializedSpan, errors);
3015
3141
  this.applySpanOverrides(serializedSpan, span.traceId ?? "");
3016
3142
  const payload = this.buildSpanPayload(serializedSpan, errors);
3143
+ const canonicalTraceId = span.traceId ? this.getCanonicalTraceId(span.traceId) : void 0;
3144
+ if (canonicalTraceId) {
3145
+ payload.traceId = canonicalTraceId;
3146
+ }
3017
3147
  this.httpClient.sendExternalSpan(payload);
3018
3148
  }
3019
3149
  };
@@ -3330,24 +3460,19 @@ function extractContextFromCollector(collector) {
3330
3460
  return null;
3331
3461
  }
3332
3462
  }
3333
- var TRACE_ID_PATTERN = /^[a-zA-Z0-9_\-.:]+$/;
3334
- var TRACE_ID_MAX_LENGTH = 256;
3463
+ 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;
3335
3464
  function validateTraceId(traceId) {
3336
- if (typeof traceId !== "string" || traceId.length === 0) {
3337
- throw new BitfabError("traceId is required and must be a non-empty string");
3338
- }
3339
- if (traceId.length > TRACE_ID_MAX_LENGTH) {
3340
- throw new BitfabError(
3341
- `traceId must be ${TRACE_ID_MAX_LENGTH} characters or fewer`
3342
- );
3465
+ if (typeof traceId !== "string" || !UUID_PATTERN.test(traceId)) {
3466
+ throw new BitfabError("traceId must be a valid Bitfab trace ID");
3343
3467
  }
3344
- if (!TRACE_ID_PATTERN.test(traceId)) {
3345
- throw new BitfabError(
3346
- `traceId may only contain letters, digits, "_", "-", ".", ":"`
3347
- );
3468
+ }
3469
+ function validateSpanId(id) {
3470
+ if (typeof id !== "string" || !UUID_PATTERN.test(id)) {
3471
+ throw new BitfabError("id must be a valid Bitfab span ID");
3348
3472
  }
3349
3473
  }
3350
3474
  var noOpSpan = {
3475
+ id: "",
3351
3476
  traceId: "",
3352
3477
  addContext() {
3353
3478
  },
@@ -3371,6 +3496,7 @@ function getCurrentSpan() {
3371
3496
  return noOpSpan;
3372
3497
  }
3373
3498
  return {
3499
+ id: current.spanId,
3374
3500
  traceId: current.traceId,
3375
3501
  addContext(context) {
3376
3502
  try {
@@ -3462,6 +3588,12 @@ var Bitfab = class {
3462
3588
  constructor(config) {
3463
3589
  /** Gate the empty-key warning to fire at most once. */
3464
3590
  this.apiKeyWarned = false;
3591
+ /**
3592
+ * Mock overrides registered via {@link Bitfab.registerMockOverride}, applied
3593
+ * to every `replay` on this client (after any per-call `mockOverride`). In
3594
+ * registration order; first matcher wins within this list.
3595
+ */
3596
+ this.mockOverrides = [];
3465
3597
  this.apiKeyConfig = config.apiKey;
3466
3598
  this.serviceUrl = config.serviceUrl ?? DEFAULT_SERVICE_URL;
3467
3599
  this.timeout = config.timeout ?? 12e4;
@@ -4074,24 +4206,77 @@ var Bitfab = class {
4074
4206
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
4075
4207
  const callIndex = counters.get(counterKey) ?? 0;
4076
4208
  counters.set(counterKey, callIndex + 1);
4077
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4078
- if (shouldMock) {
4079
- const mockKey = `${counterKey}:${callIndex}`;
4080
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4081
- if (mockSpan) {
4082
- let output = mockSpan.output;
4083
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4084
- output = deserializeValue({
4085
- json: mockSpan.output,
4086
- meta: mockSpan.outputMeta
4087
- });
4088
- }
4209
+ const mockKey = `${counterKey}:${callIndex}`;
4210
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4211
+ const emitMock = (output) => {
4212
+ void sendSpan({ result: output, mocked: true });
4213
+ if (fnReturnsPromise) {
4214
+ return Promise.resolve(output);
4215
+ }
4216
+ return output;
4217
+ };
4218
+ const emitMockAsync = (pending) => {
4219
+ if (!fnReturnsPromise) {
4220
+ throw new BitfabError(
4221
+ `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.`
4222
+ );
4223
+ }
4224
+ return (async () => {
4225
+ const output = await pending;
4089
4226
  void sendSpan({ result: output, mocked: true });
4090
- if (fnReturnsPromise) {
4091
- return Promise.resolve(output);
4092
- }
4093
4227
  return output;
4228
+ })();
4229
+ };
4230
+ const resolveRecordedOutput = () => {
4231
+ const hasInlineOutput = mockSpan?.output !== void 0 || mockSpan?.outputMeta !== void 0;
4232
+ if (!hasInlineOutput && replayCtxForMock.fetchSpanOutput && mockSpan?.externalSpanId) {
4233
+ return replayCtxForMock.fetchSpanOutput(mockSpan.externalSpanId);
4234
+ }
4235
+ if (!mockSpan) {
4236
+ return Promise.reject(
4237
+ new BitfabError(
4238
+ `No recorded span to source output for "${traceFunctionKey}".`
4239
+ )
4240
+ );
4241
+ }
4242
+ let output = mockSpan.output;
4243
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4244
+ output = deserializeValue({
4245
+ json: mockSpan.output,
4246
+ meta: mockSpan.outputMeta
4247
+ });
4248
+ }
4249
+ return output;
4250
+ };
4251
+ if (replayCtxForMock.mockOverrides?.length) {
4252
+ const nodeMeta = {
4253
+ traceFunctionKey,
4254
+ spanName: baseSpanParams.spanName,
4255
+ type: options.type ?? "custom",
4256
+ originalSpanId: mockSpan?.sourceSpanId
4257
+ };
4258
+ const override = replayCtxForMock.mockOverrides.find(
4259
+ (o) => o.match(nodeMeta)
4260
+ );
4261
+ if (override) {
4262
+ const injected = resolveMockValue(override.value, {
4263
+ node: nodeMeta,
4264
+ inputs: args,
4265
+ getOriginalOutput: () => Promise.resolve(resolveRecordedOutput())
4266
+ });
4267
+ if (injected instanceof Promise) {
4268
+ return emitMockAsync(injected);
4269
+ }
4270
+ return emitMock(injected);
4271
+ }
4272
+ }
4273
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4274
+ if (shouldMock && mockSpan) {
4275
+ const recorded = resolveRecordedOutput();
4276
+ if (recorded instanceof Promise) {
4277
+ return emitMockAsync(recorded);
4094
4278
  }
4279
+ return emitMock(recorded);
4095
4280
  }
4096
4281
  }
4097
4282
  const recordSpan = (result) => {
@@ -4167,20 +4352,19 @@ var Bitfab = class {
4167
4352
  }
4168
4353
  /**
4169
4354
  * Get a detached handle to a previously-created trace, looked up by the
4170
- * caller-supplied id (the same id passed at trace creation).
4355
+ * canonical Bitfab trace ID.
4171
4356
  *
4172
4357
  * The returned handle is not tied to AsyncLocalStorage - each method sends
4173
4358
  * to the server immediately. Useful for adding context to a trace from a
4174
4359
  * different process or thread than the one that created it.
4175
4360
  *
4176
- * Throws synchronously if `traceId` is malformed (empty, too long, or
4177
- * contains characters outside `[a-zA-Z0-9_\-.:]`). Server returns 404 if
4178
- * no trace exists with that id in the org; the failure surfaces as a
4361
+ * Throws synchronously if `traceId` is not a valid Bitfab trace ID. The
4362
+ * server returns 404 if no trace exists with that ID in the org; the failure surfaces as a
4179
4363
  * logged warning (fire-and-forget) or via the awaited promise.
4180
4364
  *
4181
4365
  * Example:
4182
4366
  * ```typescript
4183
- * const trace = client.getTrace("order_abc_123");
4367
+ * const trace = client.getTrace(traceId);
4184
4368
  * await trace.addContext({ refund_status: "approved" });
4185
4369
  * await trace.setMetadata({ region: "us-west" });
4186
4370
  * ```
@@ -4220,6 +4404,33 @@ var Bitfab = class {
4220
4404
  }
4221
4405
  };
4222
4406
  }
4407
+ /**
4408
+ * Fetch one persisted span from a trace without loading the full trace.
4409
+ * Name lookups return the last matching span by default. Pass `occurrence`
4410
+ * as `"first"` or a zero-based index to select a different match.
4411
+ */
4412
+ async getTraceSpan(traceId, lookup) {
4413
+ validateTraceId(traceId);
4414
+ const hasId = lookup.id !== void 0;
4415
+ const hasName = lookup.name !== void 0;
4416
+ if (hasId === hasName) {
4417
+ throw new BitfabError("Provide exactly one of id or name");
4418
+ }
4419
+ if (hasId) {
4420
+ validateSpanId(lookup.id);
4421
+ } else {
4422
+ if (lookup.name.length === 0) {
4423
+ throw new BitfabError("name must be a non-empty string");
4424
+ }
4425
+ const occurrence = lookup.occurrence ?? "last";
4426
+ if (occurrence !== "first" && occurrence !== "last" && (!Number.isInteger(occurrence) || occurrence < 0)) {
4427
+ throw new BitfabError(
4428
+ 'occurrence must be "first", "last", or a non-negative integer'
4429
+ );
4430
+ }
4431
+ }
4432
+ return this.httpClient.getTraceSpan(traceId, lookup);
4433
+ }
4223
4434
  /**
4224
4435
  * Get a function wrapper for a specific trace function key.
4225
4436
  *
@@ -4278,6 +4489,7 @@ var Bitfab = class {
4278
4489
  };
4279
4490
  }
4280
4491
  return this.httpClient.sendExternalTrace({
4492
+ id: params.traceId,
4281
4493
  type: "sdk-function",
4282
4494
  source: "typescript-sdk-function",
4283
4495
  traceFunctionKey: params.traceFunctionKey,
@@ -4333,6 +4545,8 @@ var Bitfab = class {
4333
4545
  externalSpan.input_source_span_id = params.inputSourceSpanId;
4334
4546
  }
4335
4547
  return this.httpClient.sendExternalSpan({
4548
+ id: params.spanId,
4549
+ traceId: params.traceId,
4336
4550
  type: "sdk-function",
4337
4551
  source: "typescript-sdk-function",
4338
4552
  sourceTraceId: params.traceId,
@@ -4342,26 +4556,14 @@ var Bitfab = class {
4342
4556
  ...params.mocked && { mocked: true }
4343
4557
  });
4344
4558
  }
4345
- /**
4346
- * Replay historical traces through a function and create a test run.
4347
- *
4348
- * Fetches the last N traces for the given trace function key, re-runs each
4349
- * through the provided function, and returns comparison data.
4350
- *
4351
- * Accepts either a `withSpan`-wrapped function (under the same key) or any
4352
- * plain callable: plain callables are wrapped internally so each replayed
4353
- * invocation records a trace tied to the test run. The plain-callable form
4354
- * is how handler-instrumented workflows (LangGraph/LangChain, Claude Agent
4355
- * SDK) replay - those record traces under a key with no `withSpan`-wrapped
4356
- * root in the app.
4357
- *
4358
- * @param traceFunctionKey - The trace function key to replay
4359
- * @param fn - The function to run recorded inputs through
4360
- * @param options - Optional replay options. When `traceIds` is passed,
4361
- * `limit` is ignored (with a warning): an explicit ID list already
4362
- * determines how many traces replay.
4363
- * @returns ReplayResult with items, testRunId, and testRunUrl
4364
- */
4559
+ registerMockOverride(overrideOrMatch, value) {
4560
+ const override = typeof overrideOrMatch === "function" ? { match: overrideOrMatch, value } : overrideOrMatch;
4561
+ this.mockOverrides.push(override);
4562
+ }
4563
+ /** Remove all overrides registered via {@link registerMockOverride}. */
4564
+ clearMockOverrides() {
4565
+ this.mockOverrides.length = 0;
4566
+ }
4365
4567
  async replay(traceFunctionKey, fn, options) {
4366
4568
  const wrappedKey = fn._bitfabTraceFunctionKey;
4367
4569
  let replayFn = fn;
@@ -4382,7 +4584,8 @@ var Bitfab = class {
4382
4584
  this.serviceUrl,
4383
4585
  traceFunctionKey,
4384
4586
  replayFn,
4385
- options
4587
+ options,
4588
+ this.mockOverrides
4386
4589
  );
4387
4590
  }
4388
4591
  };