@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/index.cjs CHANGED
@@ -282,6 +282,22 @@ var init_asyncStorage = __esm({
282
282
  }
283
283
  });
284
284
 
285
+ // src/mockOverride.ts
286
+ function resolveMockValue(value, ctx) {
287
+ return typeof value === "function" ? value(ctx) : value;
288
+ }
289
+ function normalizeMockOverrides(mockOverride) {
290
+ if (mockOverride === void 0) {
291
+ return [];
292
+ }
293
+ return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
294
+ }
295
+ var init_mockOverride = __esm({
296
+ "src/mockOverride.ts"() {
297
+ "use strict";
298
+ }
299
+ });
300
+
285
301
  // src/replayContext.ts
286
302
  function getReplayContext() {
287
303
  return replayContextStorage?.getStore() ?? null;
@@ -357,6 +373,7 @@ function buildMockTree(rootNode) {
357
373
  counters.set(counterKey, index + 1);
358
374
  spans.set(`${counterKey}:${index}`, {
359
375
  sourceSpanId: node.sourceSpanId,
376
+ externalSpanId: node.externalSpanId,
360
377
  output: node.output,
361
378
  outputMeta: node.outputMeta
362
379
  });
@@ -370,7 +387,7 @@ function buildMockTree(rootNode) {
370
387
  }
371
388
  return { spans };
372
389
  }
373
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment, adaptInputs) {
390
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, environment, adaptInputs) {
374
391
  const lease = environment ? serverItem.dbBranchLease : void 0;
375
392
  let inputs = [];
376
393
  let originalOutput;
@@ -389,28 +406,45 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
389
406
  sourceSpanId: serverItem.externalSpanId
390
407
  });
391
408
  }
409
+ const hasOverrides = resolvedOverrides.length > 0;
410
+ const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
411
+ const includeOutputs = mockStrategy === "all";
392
412
  let mockTree;
393
- if (mockStrategy === "all" || mockStrategy === "marked") {
413
+ if (needTree) {
394
414
  try {
395
415
  const treeResponse = await httpClient.getSpanTree(
396
- serverItem.externalSpanId
416
+ serverItem.externalSpanId,
417
+ { includeOutputs }
397
418
  );
398
419
  if (treeResponse.root) {
399
420
  mockTree = buildMockTree(treeResponse.root);
400
- } else if (mockStrategy === "all") {
421
+ } else if (mockStrategy === "all" || hasOverrides) {
401
422
  throw new BitfabError(
402
- `Replay mock strategy "all" requires a span tree root for source span ${serverItem.externalSpanId}.`
423
+ `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for source span ${serverItem.externalSpanId}.`
403
424
  );
404
425
  } else {
405
426
  mockTree = void 0;
406
427
  }
407
428
  } catch (e) {
408
- if (mockStrategy === "all") {
429
+ if (mockStrategy === "all" || hasOverrides) {
409
430
  throw e;
410
431
  }
411
432
  mockTree = void 0;
412
433
  }
413
434
  }
435
+ const outputCache = /* @__PURE__ */ new Map();
436
+ const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
437
+ let pending = outputCache.get(externalSpanId);
438
+ if (!pending) {
439
+ pending = httpClient.getExternalSpan(externalSpanId).then(
440
+ (s) => deserializeOutput(
441
+ s.rawData?.span_data ?? {}
442
+ )
443
+ );
444
+ outputCache.set(externalSpanId, pending);
445
+ }
446
+ return pending;
447
+ } : void 0;
414
448
  const maybePromise = runWithReplayContext(
415
449
  {
416
450
  testRunId,
@@ -421,6 +455,8 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
421
455
  mockTree,
422
456
  callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
423
457
  mockStrategy,
458
+ mockOverrides: hasOverrides ? resolvedOverrides : void 0,
459
+ fetchSpanOutput,
424
460
  dbBranchLease: lease,
425
461
  pendingPersistence
426
462
  },
@@ -477,7 +513,7 @@ async function mapWithConcurrency(tasks, maxConcurrency, onSettled) {
477
513
  await Promise.all(workers);
478
514
  return results;
479
515
  }
480
- async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
516
+ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
481
517
  if (options?.traceIds !== void 0) {
482
518
  if (options.traceIds.length === 0) {
483
519
  throw new BitfabError("traceIds must contain at least one trace ID.");
@@ -517,6 +553,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
517
553
  );
518
554
  const mockStrategy = options?.mock ?? "marked";
519
555
  const maxConcurrency = options?.maxConcurrency ?? 10;
556
+ const resolvedOverrides = [
557
+ ...normalizeMockOverrides(options?.mockOverride),
558
+ ...registeredOverrides
559
+ ];
520
560
  const tasks = serverItems.map(
521
561
  (serverItem) => () => processItem(
522
562
  httpClient,
@@ -524,6 +564,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
524
564
  fn,
525
565
  testRunId,
526
566
  mockStrategy,
567
+ resolvedOverrides,
527
568
  options?.environment,
528
569
  options?.adaptInputs
529
570
  )
@@ -650,6 +691,7 @@ var init_replay = __esm({
650
691
  "src/replay.ts"() {
651
692
  "use strict";
652
693
  init_errors();
694
+ init_mockOverride();
653
695
  init_randomUuid();
654
696
  init_replayContext();
655
697
  init_serialize();
@@ -683,7 +725,7 @@ __export(index_exports, {
683
725
  module.exports = __toCommonJS(index_exports);
684
726
 
685
727
  // src/version.generated.ts
686
- var __version__ = "0.28.10";
728
+ var __version__ = "0.29.0";
687
729
 
688
730
  // src/constants.ts
689
731
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
@@ -923,6 +965,50 @@ var HttpClient = class {
923
965
  async lookupFunction(name) {
924
966
  return this.request("/api/sdk/functions/lookup", { name });
925
967
  }
968
+ async getTraceSpan(traceId, lookup) {
969
+ const searchParams = new URLSearchParams();
970
+ if (lookup.id !== void 0) {
971
+ searchParams.set("id", lookup.id);
972
+ } else {
973
+ searchParams.set("name", lookup.name);
974
+ searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
975
+ }
976
+ const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
977
+ const response = await this.get(endpoint);
978
+ return response.span;
979
+ }
980
+ async get(endpoint) {
981
+ const url = `${this.serviceUrl}${endpoint}`;
982
+ const controller = new AbortController();
983
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
984
+ try {
985
+ const response = await fetch(url, {
986
+ method: "GET",
987
+ headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
988
+ signal: controller.signal
989
+ });
990
+ if (!response.ok) {
991
+ const errorText = await response.text();
992
+ throw new BitfabError(
993
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
994
+ );
995
+ }
996
+ return await response.json();
997
+ } catch (error) {
998
+ if (error instanceof BitfabError) {
999
+ throw error;
1000
+ }
1001
+ if (error instanceof Error) {
1002
+ if (error.name === "AbortError") {
1003
+ throw new BitfabError(`Request timed out after ${this.timeout}ms`);
1004
+ }
1005
+ throw new BitfabError(error.message);
1006
+ }
1007
+ throw new BitfabError("Unknown error occurred");
1008
+ } finally {
1009
+ clearTimeout(timeoutId);
1010
+ }
1011
+ }
926
1012
  /**
927
1013
  * Send an internal trace (from BAML execution).
928
1014
  * Fire-and-forget with awaitOnExit - doesn't block the caller.
@@ -979,12 +1065,12 @@ var HttpClient = class {
979
1065
  });
980
1066
  }
981
1067
  /**
982
- * Partial update of an existing external trace identified by sourceTraceId.
1068
+ * Partial update of an existing trace identified by its Bitfab trace ID.
983
1069
  * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
984
1070
  * returns a tracked promise that callers may optionally await.
985
1071
  */
986
- patchTrace(sourceTraceId, payload) {
987
- const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
1072
+ patchTrace(traceId, payload) {
1073
+ const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
988
1074
  return awaitOnExit(
989
1075
  this.request(endpoint, payload, { method: "PATCH" })
990
1076
  ).catch((error) => {
@@ -1068,9 +1154,14 @@ var HttpClient = class {
1068
1154
  /**
1069
1155
  * Fetch the span tree for a root span.
1070
1156
  * Blocking GET request.
1157
+ *
1158
+ * Pass `includeOutputs: false` for a payload-free tree (structure +
1159
+ * `externalSpanId` only), so recorded outputs are fetched lazily per mocked
1160
+ * span instead of all up front. Omit it (default eager) for `mock: "all"`.
1071
1161
  */
1072
- async getSpanTree(externalSpanId) {
1073
- const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
1162
+ async getSpanTree(externalSpanId, options) {
1163
+ const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
1164
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
1074
1165
  const controller = new AbortController();
1075
1166
  const timeoutId = setTimeout(() => controller.abort(), 3e4);
1076
1167
  try {
@@ -1322,6 +1413,7 @@ var BitfabClaudeAgentHandler = class {
1322
1413
  const traceId = this.ensureTrace();
1323
1414
  const { safe: safeInput, dropped: inputDropped } = toJsonSafeReport(inputData);
1324
1415
  const spanInfo = {
1416
+ id: randomUuid(),
1325
1417
  spanId,
1326
1418
  traceId,
1327
1419
  parentId: parentId ?? null,
@@ -1385,6 +1477,8 @@ var BitfabClaudeAgentHandler = class {
1385
1477
  rawSpan.parent_id = spanInfo.parentId;
1386
1478
  }
1387
1479
  const payload = {
1480
+ id: spanInfo.id,
1481
+ traceId: spanInfo.traceId,
1388
1482
  type: "sdk-function",
1389
1483
  source: "typescript-sdk-claude-agent-sdk",
1390
1484
  traceFunctionKey: this.traceFunctionKey,
@@ -1414,6 +1508,7 @@ var BitfabClaudeAgentHandler = class {
1414
1508
  externalTrace.metadata = metadata;
1415
1509
  }
1416
1510
  const traceData = {
1511
+ id: traceId,
1417
1512
  type: "sdk-function",
1418
1513
  source: "typescript-sdk-claude-agent-sdk",
1419
1514
  traceFunctionKey: this.traceFunctionKey,
@@ -1682,6 +1777,7 @@ var BitfabClaudeAgentHandler = class {
1682
1777
  }
1683
1778
  Object.assign(llmContext, this.currentLlmUsage);
1684
1779
  const spanInfo = {
1780
+ id: randomUuid(),
1685
1781
  spanId,
1686
1782
  traceId,
1687
1783
  parentId,
@@ -2328,6 +2424,7 @@ var BitfabLangGraphCallbackHandler = class {
2328
2424
  const contexts = Object.keys(lgMetadata).length > 0 ? [lgMetadata] : [];
2329
2425
  const { safe: safeInput, dropped: inputDropped } = toJsonSafeReport(inputData);
2330
2426
  const spanInfo = {
2427
+ id: randomUuid(),
2331
2428
  spanId: runId,
2332
2429
  traceId: invocation.traceId,
2333
2430
  rootRunId: invocation.rootRunId,
@@ -2406,6 +2503,8 @@ var BitfabLangGraphCallbackHandler = class {
2406
2503
  rawSpan.parent_id = spanInfo.parentId;
2407
2504
  }
2408
2505
  const payload = {
2506
+ id: spanInfo.id,
2507
+ traceId: spanInfo.traceId,
2409
2508
  type: "sdk-function",
2410
2509
  source: "typescript-sdk-langgraph",
2411
2510
  traceFunctionKey: this.traceFunctionKey,
@@ -2421,6 +2520,7 @@ var BitfabLangGraphCallbackHandler = class {
2421
2520
  sendTraceCompletion(rootSpan, activeContext) {
2422
2521
  const completed = activeContext === null;
2423
2522
  const traceData = {
2523
+ id: rootSpan.traceId,
2424
2524
  type: "sdk-function",
2425
2525
  source: "typescript-sdk-langgraph",
2426
2526
  traceFunctionKey: this.traceFunctionKey,
@@ -2440,6 +2540,7 @@ var BitfabLangGraphCallbackHandler = class {
2440
2540
  }
2441
2541
  sendTraceStart(rootSpan) {
2442
2542
  const traceData = {
2543
+ id: rootSpan.traceId,
2443
2544
  type: "sdk-function",
2444
2545
  source: "typescript-sdk-langgraph",
2445
2546
  traceFunctionKey: this.traceFunctionKey,
@@ -2646,6 +2747,9 @@ var BitfabLangGraphCallbackHandler = class {
2646
2747
  }
2647
2748
  };
2648
2749
 
2750
+ // src/client.ts
2751
+ init_mockOverride();
2752
+
2649
2753
  // src/openaiAgentSdk.ts
2650
2754
  var BitfabOpenAIAgentHandler = class {
2651
2755
  constructor(config) {
@@ -2798,6 +2902,7 @@ var ReplayEnvironment = class {
2798
2902
  init_serialize();
2799
2903
 
2800
2904
  // src/tracing.ts
2905
+ init_randomUuid();
2801
2906
  var BitfabOpenAITracingProcessor = class {
2802
2907
  /**
2803
2908
  * Initialize the tracing processor.
@@ -2807,6 +2912,7 @@ var BitfabOpenAITracingProcessor = class {
2807
2912
  constructor(config) {
2808
2913
  this.activeTraces = {};
2809
2914
  this.activeSpanMappings = {};
2915
+ this.canonicalTraceIds = {};
2810
2916
  this.httpClient = new HttpClient({
2811
2917
  apiKey: config.apiKey,
2812
2918
  serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
@@ -2814,6 +2920,15 @@ var BitfabOpenAITracingProcessor = class {
2814
2920
  });
2815
2921
  this.getActiveSpanContext = config.getActiveSpanContext ?? null;
2816
2922
  }
2923
+ getCanonicalTraceId(sourceTraceId) {
2924
+ const existing = this.canonicalTraceIds[sourceTraceId];
2925
+ if (existing) {
2926
+ return existing;
2927
+ }
2928
+ const created = randomUuid();
2929
+ this.canonicalTraceIds[sourceTraceId] = created;
2930
+ return created;
2931
+ }
2817
2932
  /**
2818
2933
  * Called when a trace is started.
2819
2934
  * If there's an active withSpan context, the trace ID is remapped to the
@@ -2825,7 +2940,12 @@ var BitfabOpenAITracingProcessor = class {
2825
2940
  if (activeContext) {
2826
2941
  this.activeSpanMappings[trace.traceId] = activeContext;
2827
2942
  }
2828
- this.sendTrace(trace, activeContext ? { id: activeContext.traceId } : {});
2943
+ const canonicalTraceId = activeContext?.traceId ?? this.getCanonicalTraceId(trace.traceId);
2944
+ this.canonicalTraceIds[trace.traceId] = canonicalTraceId;
2945
+ this.sendTrace(trace, {
2946
+ id: canonicalTraceId,
2947
+ sourceTraceId: activeContext?.traceId
2948
+ });
2829
2949
  }
2830
2950
  /**
2831
2951
  * Called when a trace is ended.
@@ -2834,11 +2954,13 @@ var BitfabOpenAITracingProcessor = class {
2834
2954
  */
2835
2955
  async onTraceEnd(trace) {
2836
2956
  const mapping = this.activeSpanMappings[trace.traceId];
2837
- this.sendTrace(
2838
- trace,
2839
- mapping ? { id: mapping.traceId } : { completed: true }
2840
- );
2957
+ this.sendTrace(trace, {
2958
+ completed: mapping === void 0,
2959
+ id: mapping?.traceId ?? this.getCanonicalTraceId(trace.traceId),
2960
+ sourceTraceId: mapping?.traceId
2961
+ });
2841
2962
  delete this.activeSpanMappings[trace.traceId];
2963
+ delete this.canonicalTraceIds[trace.traceId];
2842
2964
  delete this.activeTraces[trace.traceId];
2843
2965
  }
2844
2966
  /**
@@ -2868,22 +2990,25 @@ var BitfabOpenAITracingProcessor = class {
2868
2990
  async shutdown(_timeout) {
2869
2991
  this.activeTraces = {};
2870
2992
  this.activeSpanMappings = {};
2993
+ this.canonicalTraceIds = {};
2871
2994
  }
2872
2995
  /**
2873
2996
  * Send trace to Bitfab API (fire-and-forget).
2874
2997
  * When traceIdOverride is provided, the trace ID is remapped to link
2875
2998
  * the OpenAI trace into an outer withSpan trace.
2876
2999
  */
2877
- sendTrace(trace, overrides = {}) {
3000
+ sendTrace(trace, options = {}) {
2878
3001
  try {
2879
- const { completed, ...traceOverrides } = overrides;
2880
3002
  const traceData = trace.toJSON();
2881
- Object.assign(traceData, traceOverrides);
3003
+ if (options.sourceTraceId) {
3004
+ traceData.id = options.sourceTraceId;
3005
+ }
2882
3006
  this.httpClient.sendExternalTrace({
3007
+ ...options.id && { id: options.id },
2883
3008
  type: "openai",
2884
3009
  source: "typescript-sdk-openai-tracing",
2885
3010
  externalTrace: traceData,
2886
- completed: completed ?? false
3011
+ completed: options.completed ?? false
2887
3012
  });
2888
3013
  } catch {
2889
3014
  }
@@ -2969,6 +3094,7 @@ var BitfabOpenAITracingProcessor = class {
2969
3094
  */
2970
3095
  buildSpanPayload(serializedSpan, errors) {
2971
3096
  const payload = {
3097
+ id: randomUuid(),
2972
3098
  type: "openai",
2973
3099
  source: "typescript-sdk-openai-tracing",
2974
3100
  sourceTraceId: serializedSpan.trace_id ?? "unknown",
@@ -2991,6 +3117,10 @@ var BitfabOpenAITracingProcessor = class {
2991
3117
  this.extractSpanInputResponse(span, serializedSpan, errors);
2992
3118
  this.applySpanOverrides(serializedSpan, span.traceId ?? "");
2993
3119
  const payload = this.buildSpanPayload(serializedSpan, errors);
3120
+ const canonicalTraceId = span.traceId ? this.getCanonicalTraceId(span.traceId) : void 0;
3121
+ if (canonicalTraceId) {
3122
+ payload.traceId = canonicalTraceId;
3123
+ }
2994
3124
  this.httpClient.sendExternalSpan(payload);
2995
3125
  }
2996
3126
  };
@@ -3307,24 +3437,19 @@ function extractContextFromCollector(collector) {
3307
3437
  return null;
3308
3438
  }
3309
3439
  }
3310
- var TRACE_ID_PATTERN = /^[a-zA-Z0-9_\-.:]+$/;
3311
- var TRACE_ID_MAX_LENGTH = 256;
3440
+ 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;
3312
3441
  function validateTraceId(traceId) {
3313
- if (typeof traceId !== "string" || traceId.length === 0) {
3314
- throw new BitfabError("traceId is required and must be a non-empty string");
3315
- }
3316
- if (traceId.length > TRACE_ID_MAX_LENGTH) {
3317
- throw new BitfabError(
3318
- `traceId must be ${TRACE_ID_MAX_LENGTH} characters or fewer`
3319
- );
3442
+ if (typeof traceId !== "string" || !UUID_PATTERN.test(traceId)) {
3443
+ throw new BitfabError("traceId must be a valid Bitfab trace ID");
3320
3444
  }
3321
- if (!TRACE_ID_PATTERN.test(traceId)) {
3322
- throw new BitfabError(
3323
- `traceId may only contain letters, digits, "_", "-", ".", ":"`
3324
- );
3445
+ }
3446
+ function validateSpanId(id) {
3447
+ if (typeof id !== "string" || !UUID_PATTERN.test(id)) {
3448
+ throw new BitfabError("id must be a valid Bitfab span ID");
3325
3449
  }
3326
3450
  }
3327
3451
  var noOpSpan = {
3452
+ id: "",
3328
3453
  traceId: "",
3329
3454
  addContext() {
3330
3455
  },
@@ -3348,6 +3473,7 @@ function getCurrentSpan() {
3348
3473
  return noOpSpan;
3349
3474
  }
3350
3475
  return {
3476
+ id: current.spanId,
3351
3477
  traceId: current.traceId,
3352
3478
  addContext(context) {
3353
3479
  try {
@@ -3439,6 +3565,12 @@ var Bitfab = class {
3439
3565
  constructor(config) {
3440
3566
  /** Gate the empty-key warning to fire at most once. */
3441
3567
  this.apiKeyWarned = false;
3568
+ /**
3569
+ * Mock overrides registered via {@link Bitfab.registerMockOverride}, applied
3570
+ * to every `replay` on this client (after any per-call `mockOverride`). In
3571
+ * registration order; first matcher wins within this list.
3572
+ */
3573
+ this.mockOverrides = [];
3442
3574
  this.apiKeyConfig = config.apiKey;
3443
3575
  this.serviceUrl = config.serviceUrl ?? DEFAULT_SERVICE_URL;
3444
3576
  this.timeout = config.timeout ?? 12e4;
@@ -4051,24 +4183,77 @@ var Bitfab = class {
4051
4183
  const counterKey = `${traceFunctionKey}:${baseSpanParams.spanName}`;
4052
4184
  const callIndex = counters.get(counterKey) ?? 0;
4053
4185
  counters.set(counterKey, callIndex + 1);
4054
- const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4055
- if (shouldMock) {
4056
- const mockKey = `${counterKey}:${callIndex}`;
4057
- const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4058
- if (mockSpan) {
4059
- let output = mockSpan.output;
4060
- if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4061
- output = deserializeValue({
4062
- json: mockSpan.output,
4063
- meta: mockSpan.outputMeta
4064
- });
4065
- }
4186
+ const mockKey = `${counterKey}:${callIndex}`;
4187
+ const mockSpan = replayCtxForMock.mockTree.spans.get(mockKey);
4188
+ const emitMock = (output) => {
4189
+ void sendSpan({ result: output, mocked: true });
4190
+ if (fnReturnsPromise) {
4191
+ return Promise.resolve(output);
4192
+ }
4193
+ return output;
4194
+ };
4195
+ const emitMockAsync = (pending) => {
4196
+ if (!fnReturnsPromise) {
4197
+ throw new BitfabError(
4198
+ `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.`
4199
+ );
4200
+ }
4201
+ return (async () => {
4202
+ const output = await pending;
4066
4203
  void sendSpan({ result: output, mocked: true });
4067
- if (fnReturnsPromise) {
4068
- return Promise.resolve(output);
4069
- }
4070
4204
  return output;
4205
+ })();
4206
+ };
4207
+ const resolveRecordedOutput = () => {
4208
+ const hasInlineOutput = mockSpan?.output !== void 0 || mockSpan?.outputMeta !== void 0;
4209
+ if (!hasInlineOutput && replayCtxForMock.fetchSpanOutput && mockSpan?.externalSpanId) {
4210
+ return replayCtxForMock.fetchSpanOutput(mockSpan.externalSpanId);
4211
+ }
4212
+ if (!mockSpan) {
4213
+ return Promise.reject(
4214
+ new BitfabError(
4215
+ `No recorded span to source output for "${traceFunctionKey}".`
4216
+ )
4217
+ );
4218
+ }
4219
+ let output = mockSpan.output;
4220
+ if (mockSpan.outputMeta !== void 0 && mockSpan.outputMeta !== null) {
4221
+ output = deserializeValue({
4222
+ json: mockSpan.output,
4223
+ meta: mockSpan.outputMeta
4224
+ });
4225
+ }
4226
+ return output;
4227
+ };
4228
+ if (replayCtxForMock.mockOverrides?.length) {
4229
+ const nodeMeta = {
4230
+ traceFunctionKey,
4231
+ spanName: baseSpanParams.spanName,
4232
+ type: options.type ?? "custom",
4233
+ originalSpanId: mockSpan?.sourceSpanId
4234
+ };
4235
+ const override = replayCtxForMock.mockOverrides.find(
4236
+ (o) => o.match(nodeMeta)
4237
+ );
4238
+ if (override) {
4239
+ const injected = resolveMockValue(override.value, {
4240
+ node: nodeMeta,
4241
+ inputs: args,
4242
+ getOriginalOutput: () => Promise.resolve(resolveRecordedOutput())
4243
+ });
4244
+ if (injected instanceof Promise) {
4245
+ return emitMockAsync(injected);
4246
+ }
4247
+ return emitMock(injected);
4248
+ }
4249
+ }
4250
+ const shouldMock = replayCtxForMock.mockStrategy === "all" || replayCtxForMock.mockStrategy === "marked" && options.mockOnReplay === true;
4251
+ if (shouldMock && mockSpan) {
4252
+ const recorded = resolveRecordedOutput();
4253
+ if (recorded instanceof Promise) {
4254
+ return emitMockAsync(recorded);
4071
4255
  }
4256
+ return emitMock(recorded);
4072
4257
  }
4073
4258
  }
4074
4259
  const recordSpan = (result) => {
@@ -4144,20 +4329,19 @@ var Bitfab = class {
4144
4329
  }
4145
4330
  /**
4146
4331
  * Get a detached handle to a previously-created trace, looked up by the
4147
- * caller-supplied id (the same id passed at trace creation).
4332
+ * canonical Bitfab trace ID.
4148
4333
  *
4149
4334
  * The returned handle is not tied to AsyncLocalStorage - each method sends
4150
4335
  * to the server immediately. Useful for adding context to a trace from a
4151
4336
  * different process or thread than the one that created it.
4152
4337
  *
4153
- * Throws synchronously if `traceId` is malformed (empty, too long, or
4154
- * contains characters outside `[a-zA-Z0-9_\-.:]`). Server returns 404 if
4155
- * no trace exists with that id in the org; the failure surfaces as a
4338
+ * Throws synchronously if `traceId` is not a valid Bitfab trace ID. The
4339
+ * server returns 404 if no trace exists with that ID in the org; the failure surfaces as a
4156
4340
  * logged warning (fire-and-forget) or via the awaited promise.
4157
4341
  *
4158
4342
  * Example:
4159
4343
  * ```typescript
4160
- * const trace = client.getTrace("order_abc_123");
4344
+ * const trace = client.getTrace(traceId);
4161
4345
  * await trace.addContext({ refund_status: "approved" });
4162
4346
  * await trace.setMetadata({ region: "us-west" });
4163
4347
  * ```
@@ -4197,6 +4381,33 @@ var Bitfab = class {
4197
4381
  }
4198
4382
  };
4199
4383
  }
4384
+ /**
4385
+ * Fetch one persisted span from a trace without loading the full trace.
4386
+ * Name lookups return the last matching span by default. Pass `occurrence`
4387
+ * as `"first"` or a zero-based index to select a different match.
4388
+ */
4389
+ async getTraceSpan(traceId, lookup) {
4390
+ validateTraceId(traceId);
4391
+ const hasId = lookup.id !== void 0;
4392
+ const hasName = lookup.name !== void 0;
4393
+ if (hasId === hasName) {
4394
+ throw new BitfabError("Provide exactly one of id or name");
4395
+ }
4396
+ if (hasId) {
4397
+ validateSpanId(lookup.id);
4398
+ } else {
4399
+ if (lookup.name.length === 0) {
4400
+ throw new BitfabError("name must be a non-empty string");
4401
+ }
4402
+ const occurrence = lookup.occurrence ?? "last";
4403
+ if (occurrence !== "first" && occurrence !== "last" && (!Number.isInteger(occurrence) || occurrence < 0)) {
4404
+ throw new BitfabError(
4405
+ 'occurrence must be "first", "last", or a non-negative integer'
4406
+ );
4407
+ }
4408
+ }
4409
+ return this.httpClient.getTraceSpan(traceId, lookup);
4410
+ }
4200
4411
  /**
4201
4412
  * Get a function wrapper for a specific trace function key.
4202
4413
  *
@@ -4255,6 +4466,7 @@ var Bitfab = class {
4255
4466
  };
4256
4467
  }
4257
4468
  return this.httpClient.sendExternalTrace({
4469
+ id: params.traceId,
4258
4470
  type: "sdk-function",
4259
4471
  source: "typescript-sdk-function",
4260
4472
  traceFunctionKey: params.traceFunctionKey,
@@ -4310,6 +4522,8 @@ var Bitfab = class {
4310
4522
  externalSpan.input_source_span_id = params.inputSourceSpanId;
4311
4523
  }
4312
4524
  return this.httpClient.sendExternalSpan({
4525
+ id: params.spanId,
4526
+ traceId: params.traceId,
4313
4527
  type: "sdk-function",
4314
4528
  source: "typescript-sdk-function",
4315
4529
  sourceTraceId: params.traceId,
@@ -4319,26 +4533,14 @@ var Bitfab = class {
4319
4533
  ...params.mocked && { mocked: true }
4320
4534
  });
4321
4535
  }
4322
- /**
4323
- * Replay historical traces through a function and create a test run.
4324
- *
4325
- * Fetches the last N traces for the given trace function key, re-runs each
4326
- * through the provided function, and returns comparison data.
4327
- *
4328
- * Accepts either a `withSpan`-wrapped function (under the same key) or any
4329
- * plain callable: plain callables are wrapped internally so each replayed
4330
- * invocation records a trace tied to the test run. The plain-callable form
4331
- * is how handler-instrumented workflows (LangGraph/LangChain, Claude Agent
4332
- * SDK) replay - those record traces under a key with no `withSpan`-wrapped
4333
- * root in the app.
4334
- *
4335
- * @param traceFunctionKey - The trace function key to replay
4336
- * @param fn - The function to run recorded inputs through
4337
- * @param options - Optional replay options. When `traceIds` is passed,
4338
- * `limit` is ignored (with a warning): an explicit ID list already
4339
- * determines how many traces replay.
4340
- * @returns ReplayResult with items, testRunId, and testRunUrl
4341
- */
4536
+ registerMockOverride(overrideOrMatch, value) {
4537
+ const override = typeof overrideOrMatch === "function" ? { match: overrideOrMatch, value } : overrideOrMatch;
4538
+ this.mockOverrides.push(override);
4539
+ }
4540
+ /** Remove all overrides registered via {@link registerMockOverride}. */
4541
+ clearMockOverrides() {
4542
+ this.mockOverrides.length = 0;
4543
+ }
4342
4544
  async replay(traceFunctionKey, fn, options) {
4343
4545
  const wrappedKey = fn._bitfabTraceFunctionKey;
4344
4546
  let replayFn = fn;
@@ -4359,7 +4561,8 @@ var Bitfab = class {
4359
4561
  this.serviceUrl,
4360
4562
  traceFunctionKey,
4361
4563
  replayFn,
4362
- options
4564
+ options,
4565
+ this.mockOverrides
4363
4566
  );
4364
4567
  }
4365
4568
  };