bitfab 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.d.cts CHANGED
@@ -1,2 +1,2 @@
1
- export { ActiveSpanContext, AllowedEnvVars, BamlExecutionResult, Bitfab, BitfabClaudeAgentHandler, BitfabConfig, BitfabError, BitfabFunction, BitfabLangGraphCallbackHandler, BitfabOpenAITracingProcessor, CodeChangeFile, CurrentSpan, CurrentTrace, DEFAULT_SERVICE_URL, DbSnapshotConfig, DbSnapshotProvider, DbSnapshotRef, DetachedTrace, MockStrategy, ProviderDefinition, ReplayEnvironment, ReplayEnvironmentSnapshot, ReplayItem, ReplayOptions, ReplayResult, SUPPORTED_PROVIDERS, SpanOptions, SpanType, TokenUsage, TraceResponse, TracingProcessor, WrapBAMLOptions, WrappedBamlFn, __version__, flushTraces, getCurrentSpan, getCurrentTrace } from './index.cjs';
1
+ export { ActiveSpanContext, AdaptContext, AdaptInputsFn, AllowedEnvVars, BamlExecutionResult, Bitfab, BitfabClaudeAgentHandler, BitfabConfig, BitfabError, BitfabFunction, BitfabLangGraphCallbackHandler, BitfabOpenAITracingProcessor, CodeChangeFile, CurrentSpan, CurrentTrace, DEFAULT_SERVICE_URL, DbSnapshotConfig, DbSnapshotProvider, DbSnapshotRef, DetachedTrace, MockStrategy, ProviderDefinition, ReplayEnvironment, ReplayEnvironmentSnapshot, ReplayItem, ReplayOptions, ReplayResult, SUPPORTED_PROVIDERS, SpanOptions, SpanType, TokenUsage, TraceResponse, TracingProcessor, WrapBAMLOptions, WrappedBamlFn, __version__, flushTraces, getCurrentSpan, getCurrentTrace } from './index.cjs';
2
2
  import '@openai/agents';
package/dist/node.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { ActiveSpanContext, AllowedEnvVars, BamlExecutionResult, Bitfab, BitfabClaudeAgentHandler, BitfabConfig, BitfabError, BitfabFunction, BitfabLangGraphCallbackHandler, BitfabOpenAITracingProcessor, CodeChangeFile, CurrentSpan, CurrentTrace, DEFAULT_SERVICE_URL, DbSnapshotConfig, DbSnapshotProvider, DbSnapshotRef, DetachedTrace, MockStrategy, ProviderDefinition, ReplayEnvironment, ReplayEnvironmentSnapshot, ReplayItem, ReplayOptions, ReplayResult, SUPPORTED_PROVIDERS, SpanOptions, SpanType, TokenUsage, TraceResponse, TracingProcessor, WrapBAMLOptions, WrappedBamlFn, __version__, flushTraces, getCurrentSpan, getCurrentTrace } from './index.js';
1
+ export { ActiveSpanContext, AdaptContext, AdaptInputsFn, AllowedEnvVars, BamlExecutionResult, Bitfab, BitfabClaudeAgentHandler, BitfabConfig, BitfabError, BitfabFunction, BitfabLangGraphCallbackHandler, BitfabOpenAITracingProcessor, CodeChangeFile, CurrentSpan, CurrentTrace, DEFAULT_SERVICE_URL, DbSnapshotConfig, DbSnapshotProvider, DbSnapshotRef, DetachedTrace, MockStrategy, ProviderDefinition, ReplayEnvironment, ReplayEnvironmentSnapshot, ReplayItem, ReplayOptions, ReplayResult, SUPPORTED_PROVIDERS, SpanOptions, SpanType, TokenUsage, TraceResponse, TracingProcessor, WrapBAMLOptions, WrappedBamlFn, __version__, flushTraces, getCurrentSpan, getCurrentTrace } from './index.js';
2
2
  import '@openai/agents';
package/dist/node.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  flushTraces,
21
21
  getCurrentSpan,
22
22
  getCurrentTrace
23
- } from "./chunk-YPG3XIG4.js";
23
+ } from "./chunk-P4WFJ5O3.js";
24
24
  import {
25
25
  BitfabError,
26
26
  assertAsyncStorageRegistered,
@@ -54,7 +54,7 @@ function buildMockTree(rootNode) {
54
54
  }
55
55
  return { spans };
56
56
  }
57
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment) {
57
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, environment, adaptInputs) {
58
58
  const lease = environment ? serverItem.dbBranchLease : void 0;
59
59
  let inputs = [];
60
60
  let originalOutput;
@@ -67,6 +67,12 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
67
67
  const spanData = span.rawData?.span_data ?? {};
68
68
  inputs = deserializeInputs(spanData);
69
69
  originalOutput = deserializeOutput(spanData);
70
+ if (adaptInputs) {
71
+ inputs = adaptInputs(inputs, {
72
+ traceId: serverItem.traceId,
73
+ sourceSpanId: serverItem.externalSpanId
74
+ });
75
+ }
70
76
  let mockTree;
71
77
  if (mockStrategy === "all" || mockStrategy === "marked") {
72
78
  const treeResponse = await httpClient.getSpanTree(
@@ -147,9 +153,12 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
147
153
  }
148
154
  }
149
155
  if (options?.limit !== void 0 && options?.traceIds !== void 0) {
150
- throw new BitfabError(
151
- "Pass either limit or traceIds, not both: an explicit trace ID list already determines how many traces replay."
152
- );
156
+ try {
157
+ console.warn(
158
+ "Bitfab: limit is ignored when traceIds is passed: the explicit trace ID list already determines how many traces replay."
159
+ );
160
+ } catch {
161
+ }
153
162
  }
154
163
  await replayContextReady;
155
164
  const {
@@ -177,7 +186,8 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
177
186
  fn,
178
187
  testRunId,
179
188
  mockStrategy,
180
- options?.environment
189
+ options?.environment,
190
+ options?.adaptInputs
181
191
  )
182
192
  );
183
193
  const resultItems = await mapWithConcurrency(tasks, maxConcurrency);
@@ -232,4 +242,4 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options) {
232
242
  export {
233
243
  replay
234
244
  };
235
- //# sourceMappingURL=replay-3MQS22GS.js.map
245
+ //# sourceMappingURL=replay-BIPIDXX6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/replay.ts"],"sourcesContent":["/**\n * Replay historical traces through a function and create a test run.\n *\n * The replay flow has three phases:\n * 1. Start — fetches historical traces from the server and creates a test run\n * 2. Execute — re-runs each trace's inputs through the provided function locally\n * 3. Complete — marks the test run as completed on the server\n */\n\nimport type { DbSnapshotRef } from \"./dbSnapshot.js\"\nimport { BitfabError } from \"./errors.js\"\nimport type {\n CodeChangeFile,\n HttpClient,\n SpanTreeNode,\n TokenUsage,\n} from \"./http.js\"\nimport type { DbBranchLease, MockTree } from \"./replayContext.js\"\nimport { replayContextReady, runWithReplayContext } from \"./replayContext.js\"\nimport type { ReplayEnvironment } from \"./replayEnvironment.js\"\nimport { deserializeValue } from \"./serialize.js\"\n\nexport type MockStrategy = \"none\" | \"all\" | \"marked\"\n\nexport interface ReplayOptions {\n /**\n * Maximum number of traces to replay (1-100, default 5). Ignored when\n * `traceIds` is passed (with a warning): an explicit ID list already\n * determines how many traces replay.\n */\n limit?: number\n /** Optional list of specific trace IDs to replay (max 100). */\n traceIds?: string[]\n /** Maximum number of items to process in parallel. Set to 1 for sequential. Default 10. */\n maxConcurrency?: number\n /**\n * Description of the code change being tested in this replay. Stored on\n * the resulting experiment so the change can be reviewed alongside results.\n */\n codeChangeDescription?: string\n /**\n * Files edited as part of this code change. Each entry holds the file path\n * and the full `before`/`after` contents — the agent reads each file before\n * and after editing and passes the two strings. Use `\"\"` for newly created\n * files (`before`) or deleted files (`after`).\n */\n codeChangeFiles?: CodeChangeFile[]\n /**\n * Mock strategy for child spans during replay.\n * - \"none\": everything runs real code (default)\n * - \"all\": every child withSpan returns historical output\n * - \"marked\": only spans tagged with { mockOnReplay: true } in SpanOptions are mocked\n */\n mock?: MockStrategy\n /**\n * Per-trace environment. When the source trace carries a DB branching\n * snapshot, the SDK populates `environment.databaseUrl` before invoking\n * `fn` for that item and resets it after. Customer code reads from the\n * environment to pick up the per-trace branch URL.\n */\n environment?: ReplayEnvironment\n /** Group ID to associate this replay with an experiment group for live streaming in Studio. */\n experimentGroupId?: string\n /**\n * Reshape recorded inputs before they are spread into `fn`.\n *\n * Replay pulls each trace's inputs exactly as they were captured against the\n * function's signature AT TRACE TIME. When the function's shape has since\n * changed (params renamed, reordered, collapsed into an options object, etc.),\n * the recorded inputs no longer line up and `fn(...inputs)` throws. This hook\n * lets a caller map the recorded inputs onto the current signature so replay\n * can still run.\n *\n * Receives the deserialized recorded inputs and a per-trace {@link AdaptContext}\n * (so a table-driven adapter can look up the adapted inputs by `traceId`), and\n * returns the array actually spread into `fn`. The returned array is also what\n * `ReplayItem.input` reports, so the experiment shows what was really run.\n *\n * Runs per item, inside the same try/catch as `fn`: if the adapter throws, the\n * failure is surfaced on that item's `error` rather than crashing the run.\n * Omit it to spread the recorded inputs unchanged.\n */\n adaptInputs?: (inputs: unknown[], ctx: AdaptContext) => unknown[]\n}\n\n/** Per-trace context passed to {@link ReplayOptions.adaptInputs}. */\nexport interface AdaptContext {\n /** Bitfab trace ID of the historical trace being replayed. */\n traceId: string\n /** External span ID the recorded inputs were read from. */\n sourceSpanId: string\n}\n\n/**\n * The shape an adapter module must export as `adaptInputs`.\n *\n * Author an adapter in its own file (e.g. `scripts/replay-adapters/<name>.ts`),\n * import it in your replay script, and pass it to `replay({ adaptInputs })`:\n *\n * ```ts\n * import type { AdaptInputsFn } from \"@bitfab/sdk\"\n * export const adaptInputs: AdaptInputsFn = (inputs, ctx) => [reshape(inputs)]\n * ```\n */\nexport type AdaptInputsFn = (inputs: unknown[], ctx: AdaptContext) => unknown[]\n\nexport interface ReplayItem<T> {\n /** Trace ID of the new trace created during replay. */\n traceId: string | null\n /** Deserialized inputs from the original trace. */\n input: unknown[]\n /** The result returned by the function during replay, or undefined on error. */\n result: T | undefined\n /** The original output from the historical trace. */\n originalOutput: unknown\n /** Error message if the function threw, or null on success. */\n error: string | null\n /** Original trace duration in milliseconds, or null if timestamps are missing. */\n durationMs: number | null\n /** Token usage from the original trace, or null if not captured. */\n tokens: TokenUsage | null\n /** Model name from the original trace, or null if not captured. */\n model: string | null\n /**\n * The DB snapshot ref the SDK captured at trace open. Useful for debugging\n * (\"what state was this trace pinned to?\") and for customers building\n * their own resolvers. Undefined when the source trace was captured\n * without `dbSnapshot` configured.\n */\n dbSnapshotRef: DbSnapshotRef | null\n}\n\nexport type { CodeChangeFile, TokenUsage }\n\nexport interface ReplayResult<T> {\n /** Individual replay items with inputs, results, and comparison data. */\n items: ReplayItem<T>[]\n /** The test run ID created on the server. */\n testRunId: string\n /** Full URL to view the test run in the dashboard. */\n testRunUrl: string\n}\n\n/**\n * Deserialize inputs from a historical span's rawData.\n *\n * Prefers superjson-serialized `input_meta` for type preservation,\n * falls back to the raw `input` field.\n */\nfunction deserializeInputs(spanData: Record<string, unknown>): unknown[] {\n const inputMeta = spanData.input_meta as unknown\n const rawInput = spanData.input\n\n // If superjson meta is available, deserialize with type reconstruction\n if (inputMeta !== undefined && inputMeta !== null) {\n const deserialized = deserializeValue({ json: rawInput, meta: inputMeta })\n if (Array.isArray(deserialized)) {\n return deserialized\n }\n return deserialized !== undefined && deserialized !== null\n ? [deserialized]\n : []\n }\n\n // Fall back to raw input\n if (Array.isArray(rawInput)) {\n return rawInput\n }\n return rawInput !== undefined && rawInput !== null ? [rawInput] : []\n}\n\n/**\n * Deserialize the original output from a historical span's rawData.\n */\nfunction deserializeOutput(spanData: Record<string, unknown>): unknown {\n const outputMeta = spanData.output_meta as unknown\n const rawOutput = spanData.output\n\n if (outputMeta !== undefined && outputMeta !== null) {\n return deserializeValue({ json: rawOutput, meta: outputMeta })\n }\n\n return rawOutput\n}\n\n/**\n * Walk the children of a root span tree node in depth-first order and build\n * a MockTree keyed by `${traceFunctionKey}:${spanName}:${callIndex}`.\n *\n * The historical root itself is NOT walked. At replay time the runtime root\n * span has `isRootSpan === true` and never queries the mockTree (mock\n * interception is skipped for root spans by design), so the root has nothing\n * to look up. Walking it would just leave an unreachable entry in the table.\n *\n * The (key, name) compound match is what disambiguates same-key spans:\n * - A wrapped function's children commonly share its traceFunctionKey via\n * the fluent `getFunction(key).withSpan({ name }, ...)` pattern. They\n * disambiguate by `name`, never colliding with each other.\n * - Recursion: same (key, name) at every depth. callIndex per (key, name)\n * orders them correctly without leaking the historical root into the\n * nested call's slot.\n * - Outer-wrapper replay scripts: the outer wrapper's `name` is distinct\n * from anything in the historical tree (it only exists at replay), so\n * its presence never disturbs counters or lookups for spans that do\n * exist in the historical tree.\n */\nfunction buildMockTree(rootNode: SpanTreeNode): MockTree {\n const spans = new Map<\n string,\n { sourceSpanId: string; output: unknown; outputMeta?: unknown }\n >()\n const counters = new Map<string, number>()\n\n function walk(node: SpanTreeNode): void {\n const key = node.traceFunctionKey\n if (key) {\n const name = node.spanName\n const counterKey = `${key}:${name}`\n const index = counters.get(counterKey) ?? 0\n counters.set(counterKey, index + 1)\n spans.set(`${counterKey}:${index}`, {\n sourceSpanId: node.sourceSpanId,\n output: node.output,\n outputMeta: node.outputMeta,\n })\n }\n for (const child of node.children) {\n walk(child)\n }\n }\n\n for (const child of rootNode.children) {\n walk(child)\n }\n\n return { spans }\n}\n\n/**\n * Execute a single replay item: fetch span data, deserialize inputs, call\n * the function within a replay context that injects testRunId into new spans.\n */\nasync function processItem<TReturn>(\n httpClient: HttpClient,\n serverItem: {\n traceId: string\n externalSpanId: string\n durationMs: number | null\n tokens: TokenUsage | null\n model: string | null\n dbSnapshotRef?: DbSnapshotRef\n dbBranchLease?: DbBranchLease\n },\n // biome-ignore lint/suspicious/noExplicitAny: replay deserializes inputs from historical data\n fn: (...args: any[]) => TReturn | Promise<TReturn>,\n testRunId: string,\n mockStrategy: MockStrategy,\n environment: ReplayEnvironment | undefined,\n adaptInputs:\n | ((inputs: unknown[], ctx: AdaptContext) => unknown[])\n | undefined,\n): Promise<ReplayItem<TReturn>> {\n // The server-side resolver materializes a Neon preview branch per item\n // during `/api/sdk/replay/start` (when the customer passed `environment`,\n // which triggers `includeDbBranchLease: true`). The lease arrives on the\n // server item; we just attach it to the replay context and release the\n // branch in `finally` so any throw — in fetch, mock-tree build, or the\n // customer fn — frees the Neon resource. Items whose source trace had\n // no snapshot ref, or whose resolve failed server-side, arrive without\n // a lease; `env.active` will be `false` for those.\n const lease = environment ? serverItem.dbBranchLease : undefined\n\n let inputs: unknown[] = []\n let originalOutput: unknown\n let result: TReturn | undefined\n let error: string | null = null\n const replayedTraceId = crypto.randomUUID()\n // Collects the root span's full persistence chain (span uploads + trace\n // completion). Awaited below so this item's trace is on the server before\n // replay() calls completeReplay — otherwise the server's trace-ID mapping\n // races the uploads and item.traceId nulls out.\n const pendingPersistence: Promise<unknown>[] = []\n\n try {\n const span = await httpClient.getExternalSpan(serverItem.externalSpanId)\n const spanData = (span.rawData?.span_data ?? {}) as Record<string, unknown>\n\n inputs = deserializeInputs(spanData)\n originalOutput = deserializeOutput(spanData)\n\n // Reshape the recorded inputs onto the current signature when an adapter\n // is supplied. Runs before the mock tree / fn call so the adapted array is\n // what fn receives and what `item.input` reports.\n if (adaptInputs) {\n inputs = adaptInputs(inputs, {\n traceId: serverItem.traceId,\n sourceSpanId: serverItem.externalSpanId,\n })\n }\n\n // Build mock tree when mocking is active\n let mockTree: MockTree | undefined\n if (mockStrategy === \"all\" || mockStrategy === \"marked\") {\n const treeResponse = await httpClient.getSpanTree(\n serverItem.externalSpanId,\n )\n mockTree = buildMockTree(treeResponse.root)\n }\n\n const maybePromise = runWithReplayContext(\n {\n testRunId,\n traceId: replayedTraceId,\n inputSourceSpanId: span.id,\n inputSourceTraceId: span.externalTraceId,\n sourceBitfabTraceId: serverItem.traceId,\n mockTree,\n callCounters: mockTree ? new Map() : undefined,\n mockStrategy,\n dbBranchLease: lease,\n pendingPersistence,\n },\n () => fn(...inputs),\n )\n result = maybePromise instanceof Promise ? await maybePromise : maybePromise\n } catch (e) {\n error = e instanceof Error ? e.message : String(e)\n } finally {\n // Wait for this item's trace (spans + completion) to be fully persisted\n // before the item resolves. Runs on the error path too — a throwing fn\n // still emits a root span whose trace must land before completeReplay.\n await Promise.allSettled(pendingPersistence)\n if (lease) {\n try {\n await httpClient.releaseDbBranchLease(lease.neonBranchId)\n } catch (e) {\n try {\n console.warn(\n `Bitfab: failed to release DB branch ${lease.neonBranchId} (TTL janitor will catch it): ${\n e instanceof Error ? e.message : String(e)\n }`,\n )\n } catch {\n // Never crash the host\n }\n }\n }\n }\n\n return {\n traceId: replayedTraceId,\n input: inputs,\n result,\n originalOutput,\n error,\n durationMs: serverItem.durationMs ?? null,\n tokens: serverItem.tokens ?? null,\n model: serverItem.model ?? null,\n dbSnapshotRef: serverItem.dbSnapshotRef ?? null,\n }\n}\n\n/**\n * Run async tasks with a concurrency limit.\n * Each task factory is called when a slot opens; results preserve input order.\n */\nasync function mapWithConcurrency<T>(\n tasks: Array<() => Promise<T>>,\n maxConcurrency: number,\n): Promise<T[]> {\n const results: T[] = new Array(tasks.length)\n let nextIndex = 0\n\n async function worker(): Promise<void> {\n while (nextIndex < tasks.length) {\n const index = nextIndex++\n results[index] = await tasks[index]()\n }\n }\n\n const workers = Array.from(\n { length: Math.min(maxConcurrency, tasks.length) },\n () => worker(),\n )\n await Promise.all(workers)\n return results\n}\n\n/**\n * Replay historical traces through a function and create a test run.\n *\n * @internal Called by Bitfab.replay — not part of the public API.\n */\nexport async function replay<TReturn>(\n httpClient: HttpClient,\n serviceUrl: string,\n traceFunctionKey: string,\n // biome-ignore lint/suspicious/noExplicitAny: replay deserializes inputs from historical data\n fn: (...args: any[]) => TReturn | Promise<TReturn>,\n options?: ReplayOptions,\n): Promise<ReplayResult<TReturn>> {\n if (options?.traceIds !== undefined) {\n if (options.traceIds.length === 0) {\n throw new BitfabError(\"traceIds must contain at least one trace ID.\")\n }\n if (options.traceIds.length > 100) {\n throw new BitfabError(\n `traceIds supports at most 100 trace IDs per replay (got ${options.traceIds.length}).`,\n )\n }\n }\n if (options?.limit !== undefined && options?.traceIds !== undefined) {\n try {\n console.warn(\n \"Bitfab: limit is ignored when traceIds is passed: the explicit trace ID list already determines how many traces replay.\",\n )\n } catch {\n // Never crash the host app\n }\n }\n\n await replayContextReady\n\n const {\n testRunId,\n testRunUrl,\n items: serverItems,\n } = await httpClient.startReplay(\n traceFunctionKey,\n // limit is meaningless with explicit traceIds (the ID list determines\n // the count), so it's omitted from the request entirely.\n options?.traceIds ? undefined : (options?.limit ?? 5),\n options?.traceIds,\n options?.codeChangeDescription,\n options?.codeChangeFiles,\n options?.environment !== undefined, // includeDbBranchLease\n options?.experimentGroupId,\n )\n\n const mockStrategy: MockStrategy = options?.mock ?? \"none\"\n const maxConcurrency = options?.maxConcurrency ?? 10\n\n const tasks = serverItems.map(\n (serverItem) => () =>\n processItem(\n httpClient,\n serverItem,\n fn,\n testRunId,\n mockStrategy,\n options?.environment,\n options?.adaptInputs,\n ),\n )\n const resultItems = await mapWithConcurrency(tasks, maxConcurrency)\n\n // Every item awaited its own trace persistence (spans + completion) in\n // processItem, so all replay traces are on the server by now — no flush\n // needed, and completeReplay's trace-ID mapping is deterministic.\n // completeReplay failures propagate: a missing mapping means verdicts\n // can't be persisted, which callers must hear about loudly.\n const completeResult = await httpClient.completeReplay(testRunId)\n const serverTraceIds = completeResult.traceIds\n\n if (serverTraceIds === undefined) {\n // Older servers don't return the mapping. Preserve the legacy\n // null-traceId behavior but say why.\n try {\n console.warn(\n \"Bitfab: server did not return replay trace IDs; item.traceId will be null (server upgrade required for verdict persistence)\",\n )\n } catch {\n // Never crash the host app\n }\n for (const item of resultItems) {\n item.traceId = null\n }\n } else {\n // Map each item's locally-generated trace ID to the server's trace row\n // ID. A completed item with no mapping means its trace was sent but the\n // server has no record — a null traceId blocks verdict persistence and\n // the Studio experiments view downstream, so this must never be silent.\n //\n // Severity splits on scope:\n // - ALL completed items missing → systemic (the replayed function isn't\n // wrapped with withSpan, or uploads are wholesale broken). Throw; the\n // run's results are unusable for persistence and silence here is the\n // exact bug this guarantee exists to prevent.\n // - SOME completed items missing → per-item upload failure (transient\n // network blip, one oversized payload). Null those items and log an\n // unmissable error, but return the run — callers can persist verdicts\n // for the items that landed instead of losing all the compute.\n const missing: string[] = []\n let completedCount = 0\n for (const item of resultItems) {\n if (item.traceId) {\n const mapped = serverTraceIds[item.traceId]\n if (item.error === null) {\n completedCount += 1\n if (mapped === undefined) {\n missing.push(item.traceId)\n }\n }\n item.traceId = mapped ?? null\n }\n }\n if (missing.length > 0) {\n const serverCount =\n completeResult.traceCount !== undefined\n ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.`\n : \"\"\n if (missing.length === completedCount) {\n throw new BitfabError(\n `Replay completed but the server has no persisted trace for any of the ${completedCount} completed item(s) (testRunId ${testRunId}).${serverCount} ` +\n `Trace uploads were awaited, so either the uploads failed (check for \"Bitfab: Failed to create\" errors above) or the replayed function is not wrapped with withSpan.`,\n )\n }\n try {\n console.error(\n `Bitfab: server has no persisted trace for ${missing.length} of ${completedCount} completed replay item(s) (testRunId ${testRunId}).${serverCount} ` +\n `Their traceId is null and verdicts cannot be persisted for them. Missing: ${missing.join(\", \")}`,\n )\n } catch {\n // Never crash the host app\n }\n }\n }\n\n return {\n items: resultItems,\n testRunId,\n testRunUrl: `${serviceUrl}${testRunUrl}`,\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitfab",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Bitfab client for provider-based API calls with local BAML execution",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/replay.ts"],"sourcesContent":["/**\n * Replay historical traces through a function and create a test run.\n *\n * The replay flow has three phases:\n * 1. Start — fetches historical traces from the server and creates a test run\n * 2. Execute — re-runs each trace's inputs through the provided function locally\n * 3. Complete — marks the test run as completed on the server\n */\n\nimport type { DbSnapshotRef } from \"./dbSnapshot.js\"\nimport { BitfabError } from \"./errors.js\"\nimport type {\n CodeChangeFile,\n HttpClient,\n SpanTreeNode,\n TokenUsage,\n} from \"./http.js\"\nimport type { DbBranchLease, MockTree } from \"./replayContext.js\"\nimport { replayContextReady, runWithReplayContext } from \"./replayContext.js\"\nimport type { ReplayEnvironment } from \"./replayEnvironment.js\"\nimport { deserializeValue } from \"./serialize.js\"\n\nexport type MockStrategy = \"none\" | \"all\" | \"marked\"\n\nexport interface ReplayOptions {\n /**\n * Maximum number of traces to replay (1–100, default 5). Mutually\n * exclusive with `traceIds` — an explicit ID list already determines how\n * many traces replay, so passing both throws.\n */\n limit?: number\n /** Optional list of specific trace IDs to replay (max 100). */\n traceIds?: string[]\n /** Maximum number of items to process in parallel. Set to 1 for sequential. Default 10. */\n maxConcurrency?: number\n /**\n * Description of the code change being tested in this replay. Stored on\n * the resulting experiment so the change can be reviewed alongside results.\n */\n codeChangeDescription?: string\n /**\n * Files edited as part of this code change. Each entry holds the file path\n * and the full `before`/`after` contents — the agent reads each file before\n * and after editing and passes the two strings. Use `\"\"` for newly created\n * files (`before`) or deleted files (`after`).\n */\n codeChangeFiles?: CodeChangeFile[]\n /**\n * Mock strategy for child spans during replay.\n * - \"none\": everything runs real code (default)\n * - \"all\": every child withSpan returns historical output\n * - \"marked\": only spans tagged with { mockOnReplay: true } in SpanOptions are mocked\n */\n mock?: MockStrategy\n /**\n * Per-trace environment. When the source trace carries a DB branching\n * snapshot, the SDK populates `environment.databaseUrl` before invoking\n * `fn` for that item and resets it after. Customer code reads from the\n * environment to pick up the per-trace branch URL.\n */\n environment?: ReplayEnvironment\n /** Group ID to associate this replay with an experiment group for live streaming in Studio. */\n experimentGroupId?: string\n}\n\nexport interface ReplayItem<T> {\n /** Trace ID of the new trace created during replay. */\n traceId: string | null\n /** Deserialized inputs from the original trace. */\n input: unknown[]\n /** The result returned by the function during replay, or undefined on error. */\n result: T | undefined\n /** The original output from the historical trace. */\n originalOutput: unknown\n /** Error message if the function threw, or null on success. */\n error: string | null\n /** Original trace duration in milliseconds, or null if timestamps are missing. */\n durationMs: number | null\n /** Token usage from the original trace, or null if not captured. */\n tokens: TokenUsage | null\n /** Model name from the original trace, or null if not captured. */\n model: string | null\n /**\n * The DB snapshot ref the SDK captured at trace open. Useful for debugging\n * (\"what state was this trace pinned to?\") and for customers building\n * their own resolvers. Undefined when the source trace was captured\n * without `dbSnapshot` configured.\n */\n dbSnapshotRef: DbSnapshotRef | null\n}\n\nexport type { CodeChangeFile, TokenUsage }\n\nexport interface ReplayResult<T> {\n /** Individual replay items with inputs, results, and comparison data. */\n items: ReplayItem<T>[]\n /** The test run ID created on the server. */\n testRunId: string\n /** Full URL to view the test run in the dashboard. */\n testRunUrl: string\n}\n\n/**\n * Deserialize inputs from a historical span's rawData.\n *\n * Prefers superjson-serialized `input_meta` for type preservation,\n * falls back to the raw `input` field.\n */\nfunction deserializeInputs(spanData: Record<string, unknown>): unknown[] {\n const inputMeta = spanData.input_meta as unknown\n const rawInput = spanData.input\n\n // If superjson meta is available, deserialize with type reconstruction\n if (inputMeta !== undefined && inputMeta !== null) {\n const deserialized = deserializeValue({ json: rawInput, meta: inputMeta })\n if (Array.isArray(deserialized)) {\n return deserialized\n }\n return deserialized !== undefined && deserialized !== null\n ? [deserialized]\n : []\n }\n\n // Fall back to raw input\n if (Array.isArray(rawInput)) {\n return rawInput\n }\n return rawInput !== undefined && rawInput !== null ? [rawInput] : []\n}\n\n/**\n * Deserialize the original output from a historical span's rawData.\n */\nfunction deserializeOutput(spanData: Record<string, unknown>): unknown {\n const outputMeta = spanData.output_meta as unknown\n const rawOutput = spanData.output\n\n if (outputMeta !== undefined && outputMeta !== null) {\n return deserializeValue({ json: rawOutput, meta: outputMeta })\n }\n\n return rawOutput\n}\n\n/**\n * Walk the children of a root span tree node in depth-first order and build\n * a MockTree keyed by `${traceFunctionKey}:${spanName}:${callIndex}`.\n *\n * The historical root itself is NOT walked. At replay time the runtime root\n * span has `isRootSpan === true` and never queries the mockTree (mock\n * interception is skipped for root spans by design), so the root has nothing\n * to look up. Walking it would just leave an unreachable entry in the table.\n *\n * The (key, name) compound match is what disambiguates same-key spans:\n * - A wrapped function's children commonly share its traceFunctionKey via\n * the fluent `getFunction(key).withSpan({ name }, ...)` pattern. They\n * disambiguate by `name`, never colliding with each other.\n * - Recursion: same (key, name) at every depth. callIndex per (key, name)\n * orders them correctly without leaking the historical root into the\n * nested call's slot.\n * - Outer-wrapper replay scripts: the outer wrapper's `name` is distinct\n * from anything in the historical tree (it only exists at replay), so\n * its presence never disturbs counters or lookups for spans that do\n * exist in the historical tree.\n */\nfunction buildMockTree(rootNode: SpanTreeNode): MockTree {\n const spans = new Map<\n string,\n { sourceSpanId: string; output: unknown; outputMeta?: unknown }\n >()\n const counters = new Map<string, number>()\n\n function walk(node: SpanTreeNode): void {\n const key = node.traceFunctionKey\n if (key) {\n const name = node.spanName\n const counterKey = `${key}:${name}`\n const index = counters.get(counterKey) ?? 0\n counters.set(counterKey, index + 1)\n spans.set(`${counterKey}:${index}`, {\n sourceSpanId: node.sourceSpanId,\n output: node.output,\n outputMeta: node.outputMeta,\n })\n }\n for (const child of node.children) {\n walk(child)\n }\n }\n\n for (const child of rootNode.children) {\n walk(child)\n }\n\n return { spans }\n}\n\n/**\n * Execute a single replay item: fetch span data, deserialize inputs, call\n * the function within a replay context that injects testRunId into new spans.\n */\nasync function processItem<TReturn>(\n httpClient: HttpClient,\n serverItem: {\n traceId: string\n externalSpanId: string\n durationMs: number | null\n tokens: TokenUsage | null\n model: string | null\n dbSnapshotRef?: DbSnapshotRef\n dbBranchLease?: DbBranchLease\n },\n // biome-ignore lint/suspicious/noExplicitAny: replay deserializes inputs from historical data\n fn: (...args: any[]) => TReturn | Promise<TReturn>,\n testRunId: string,\n mockStrategy: MockStrategy,\n environment: ReplayEnvironment | undefined,\n): Promise<ReplayItem<TReturn>> {\n // The server-side resolver materializes a Neon preview branch per item\n // during `/api/sdk/replay/start` (when the customer passed `environment`,\n // which triggers `includeDbBranchLease: true`). The lease arrives on the\n // server item; we just attach it to the replay context and release the\n // branch in `finally` so any throw — in fetch, mock-tree build, or the\n // customer fn — frees the Neon resource. Items whose source trace had\n // no snapshot ref, or whose resolve failed server-side, arrive without\n // a lease; `env.active` will be `false` for those.\n const lease = environment ? serverItem.dbBranchLease : undefined\n\n let inputs: unknown[] = []\n let originalOutput: unknown\n let result: TReturn | undefined\n let error: string | null = null\n const replayedTraceId = crypto.randomUUID()\n // Collects the root span's full persistence chain (span uploads + trace\n // completion). Awaited below so this item's trace is on the server before\n // replay() calls completeReplay — otherwise the server's trace-ID mapping\n // races the uploads and item.traceId nulls out.\n const pendingPersistence: Promise<unknown>[] = []\n\n try {\n const span = await httpClient.getExternalSpan(serverItem.externalSpanId)\n const spanData = (span.rawData?.span_data ?? {}) as Record<string, unknown>\n\n inputs = deserializeInputs(spanData)\n originalOutput = deserializeOutput(spanData)\n\n // Build mock tree when mocking is active\n let mockTree: MockTree | undefined\n if (mockStrategy === \"all\" || mockStrategy === \"marked\") {\n const treeResponse = await httpClient.getSpanTree(\n serverItem.externalSpanId,\n )\n mockTree = buildMockTree(treeResponse.root)\n }\n\n const maybePromise = runWithReplayContext(\n {\n testRunId,\n traceId: replayedTraceId,\n inputSourceSpanId: span.id,\n inputSourceTraceId: span.externalTraceId,\n sourceBitfabTraceId: serverItem.traceId,\n mockTree,\n callCounters: mockTree ? new Map() : undefined,\n mockStrategy,\n dbBranchLease: lease,\n pendingPersistence,\n },\n () => fn(...inputs),\n )\n result = maybePromise instanceof Promise ? await maybePromise : maybePromise\n } catch (e) {\n error = e instanceof Error ? e.message : String(e)\n } finally {\n // Wait for this item's trace (spans + completion) to be fully persisted\n // before the item resolves. Runs on the error path too — a throwing fn\n // still emits a root span whose trace must land before completeReplay.\n await Promise.allSettled(pendingPersistence)\n if (lease) {\n try {\n await httpClient.releaseDbBranchLease(lease.neonBranchId)\n } catch (e) {\n try {\n console.warn(\n `Bitfab: failed to release DB branch ${lease.neonBranchId} (TTL janitor will catch it): ${\n e instanceof Error ? e.message : String(e)\n }`,\n )\n } catch {\n // Never crash the host\n }\n }\n }\n }\n\n return {\n traceId: replayedTraceId,\n input: inputs,\n result,\n originalOutput,\n error,\n durationMs: serverItem.durationMs ?? null,\n tokens: serverItem.tokens ?? null,\n model: serverItem.model ?? null,\n dbSnapshotRef: serverItem.dbSnapshotRef ?? null,\n }\n}\n\n/**\n * Run async tasks with a concurrency limit.\n * Each task factory is called when a slot opens; results preserve input order.\n */\nasync function mapWithConcurrency<T>(\n tasks: Array<() => Promise<T>>,\n maxConcurrency: number,\n): Promise<T[]> {\n const results: T[] = new Array(tasks.length)\n let nextIndex = 0\n\n async function worker(): Promise<void> {\n while (nextIndex < tasks.length) {\n const index = nextIndex++\n results[index] = await tasks[index]()\n }\n }\n\n const workers = Array.from(\n { length: Math.min(maxConcurrency, tasks.length) },\n () => worker(),\n )\n await Promise.all(workers)\n return results\n}\n\n/**\n * Replay historical traces through a function and create a test run.\n *\n * @internal Called by Bitfab.replay — not part of the public API.\n */\nexport async function replay<TReturn>(\n httpClient: HttpClient,\n serviceUrl: string,\n traceFunctionKey: string,\n // biome-ignore lint/suspicious/noExplicitAny: replay deserializes inputs from historical data\n fn: (...args: any[]) => TReturn | Promise<TReturn>,\n options?: ReplayOptions,\n): Promise<ReplayResult<TReturn>> {\n if (options?.traceIds !== undefined) {\n if (options.traceIds.length === 0) {\n throw new BitfabError(\"traceIds must contain at least one trace ID.\")\n }\n if (options.traceIds.length > 100) {\n throw new BitfabError(\n `traceIds supports at most 100 trace IDs per replay (got ${options.traceIds.length}).`,\n )\n }\n }\n if (options?.limit !== undefined && options?.traceIds !== undefined) {\n throw new BitfabError(\n \"Pass either limit or traceIds, not both: an explicit trace ID list already determines how many traces replay.\",\n )\n }\n\n await replayContextReady\n\n const {\n testRunId,\n testRunUrl,\n items: serverItems,\n } = await httpClient.startReplay(\n traceFunctionKey,\n // limit is meaningless with explicit traceIds (the ID list determines\n // the count), so it's omitted from the request entirely.\n options?.traceIds ? undefined : (options?.limit ?? 5),\n options?.traceIds,\n options?.codeChangeDescription,\n options?.codeChangeFiles,\n options?.environment !== undefined, // includeDbBranchLease\n options?.experimentGroupId,\n )\n\n const mockStrategy: MockStrategy = options?.mock ?? \"none\"\n const maxConcurrency = options?.maxConcurrency ?? 10\n\n const tasks = serverItems.map(\n (serverItem) => () =>\n processItem(\n httpClient,\n serverItem,\n fn,\n testRunId,\n mockStrategy,\n options?.environment,\n ),\n )\n const resultItems = await mapWithConcurrency(tasks, maxConcurrency)\n\n // Every item awaited its own trace persistence (spans + completion) in\n // processItem, so all replay traces are on the server by now — no flush\n // needed, and completeReplay's trace-ID mapping is deterministic.\n // completeReplay failures propagate: a missing mapping means verdicts\n // can't be persisted, which callers must hear about loudly.\n const completeResult = await httpClient.completeReplay(testRunId)\n const serverTraceIds = completeResult.traceIds\n\n if (serverTraceIds === undefined) {\n // Older servers don't return the mapping. Preserve the legacy\n // null-traceId behavior but say why.\n try {\n console.warn(\n \"Bitfab: server did not return replay trace IDs; item.traceId will be null (server upgrade required for verdict persistence)\",\n )\n } catch {\n // Never crash the host app\n }\n for (const item of resultItems) {\n item.traceId = null\n }\n } else {\n // Map each item's locally-generated trace ID to the server's trace row\n // ID. A completed item with no mapping means its trace was sent but the\n // server has no record — a null traceId blocks verdict persistence and\n // the Studio experiments view downstream, so this must never be silent.\n //\n // Severity splits on scope:\n // - ALL completed items missing → systemic (the replayed function isn't\n // wrapped with withSpan, or uploads are wholesale broken). Throw; the\n // run's results are unusable for persistence and silence here is the\n // exact bug this guarantee exists to prevent.\n // - SOME completed items missing → per-item upload failure (transient\n // network blip, one oversized payload). Null those items and log an\n // unmissable error, but return the run — callers can persist verdicts\n // for the items that landed instead of losing all the compute.\n const missing: string[] = []\n let completedCount = 0\n for (const item of resultItems) {\n if (item.traceId) {\n const mapped = serverTraceIds[item.traceId]\n if (item.error === null) {\n completedCount += 1\n if (mapped === undefined) {\n missing.push(item.traceId)\n }\n }\n item.traceId = mapped ?? null\n }\n }\n if (missing.length > 0) {\n const serverCount =\n completeResult.traceCount !== undefined\n ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.`\n : \"\"\n if (missing.length === completedCount) {\n throw new BitfabError(\n `Replay completed but the server has no persisted trace for any of the ${completedCount} completed item(s) (testRunId ${testRunId}).${serverCount} ` +\n `Trace uploads were awaited, so either the uploads failed (check for \"Bitfab: Failed to create\" errors above) or the replayed function is not wrapped with withSpan.`,\n )\n }\n try {\n console.error(\n `Bitfab: server has no persisted trace for ${missing.length} of ${completedCount} completed replay item(s) (testRunId ${testRunId}).${serverCount} ` +\n `Their traceId is null and verdicts cannot be persisted for them. Missing: ${missing.join(\", \")}`,\n )\n } catch {\n // Never crash the host app\n }\n }\n }\n\n return {\n items: resultItems,\n testRunId,\n testRunUrl: `${serviceUrl}${testRunUrl}`,\n }\n}\n"],"mappings":"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","names":[]}