bitfab 0.13.8 → 0.15.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.
@@ -0,0 +1,131 @@
1
+ // src/errors.ts
2
+ var BitfabError = class extends Error {
3
+ constructor(message, url) {
4
+ super(message);
5
+ this.url = url;
6
+ this.name = "BitfabError";
7
+ }
8
+ };
9
+
10
+ // src/asyncStorage.ts
11
+ var AsyncLocalStorageClass = null;
12
+ var initDone = false;
13
+ function registerAsyncLocalStorageClass(cls) {
14
+ if (!AsyncLocalStorageClass) {
15
+ AsyncLocalStorageClass = cls;
16
+ }
17
+ initDone = true;
18
+ }
19
+ function assertAsyncStorageRegistered() {
20
+ if (!AsyncLocalStorageClass) {
21
+ console.warn(
22
+ "Bitfab: AsyncLocalStorage not available \u2014 nested span context will not propagate."
23
+ );
24
+ }
25
+ }
26
+ var asyncStorageReady = (typeof process !== "undefined" && process.versions?.node ? (
27
+ // The join trick hides "node:async_hooks" from static analysis so
28
+ // bundlers that ban Node.js built-ins don't fail at build time.
29
+ // webpackIgnore tells webpack/turbopack to emit a native import()
30
+ // so Node.js can resolve the module at runtime.
31
+ import(
32
+ /* webpackIgnore: true */
33
+ ["node", "async_hooks"].join(":")
34
+ ).then(
35
+ (mod) => {
36
+ registerAsyncLocalStorageClass(mod.AsyncLocalStorage);
37
+ }
38
+ ).catch(() => {
39
+ })
40
+ ) : Promise.resolve()).then(() => {
41
+ initDone = true;
42
+ });
43
+ function isAsyncStorageInitDone() {
44
+ return initDone;
45
+ }
46
+ function createAsyncLocalStorage() {
47
+ return AsyncLocalStorageClass ? new AsyncLocalStorageClass() : null;
48
+ }
49
+
50
+ // src/replayContext.ts
51
+ var replayContextStorage = null;
52
+ var replayContextReady = asyncStorageReady.then(() => {
53
+ replayContextStorage = createAsyncLocalStorage();
54
+ });
55
+ function getReplayContext() {
56
+ return replayContextStorage?.getStore() ?? null;
57
+ }
58
+ function runWithReplayContext(ctx, fn) {
59
+ if (replayContextStorage) {
60
+ return replayContextStorage.run(ctx, fn);
61
+ }
62
+ return fn();
63
+ }
64
+
65
+ // src/serialize.ts
66
+ import superjson from "superjson";
67
+ var MAX_SERIALIZED_BYTES = 512e3;
68
+ function describeValue(value) {
69
+ try {
70
+ const ctorName = value?.constructor?.name;
71
+ if (ctorName && ctorName !== "Object") {
72
+ return ctorName;
73
+ }
74
+ } catch {
75
+ }
76
+ return typeof value;
77
+ }
78
+ function unserializableStub(value, reason) {
79
+ let summary;
80
+ try {
81
+ summary = `<unserializable: ${describeValue(value)} (${reason})>`;
82
+ } catch {
83
+ summary = `<unserializable (${reason})>`;
84
+ }
85
+ return { json: summary };
86
+ }
87
+ function serializeValue(value) {
88
+ try {
89
+ const { json, meta } = superjson.serialize(value);
90
+ let size;
91
+ try {
92
+ size = JSON.stringify(json).length;
93
+ } catch {
94
+ return unserializableStub(value, "stringify_failed_after_superjson");
95
+ }
96
+ if (size > MAX_SERIALIZED_BYTES) {
97
+ return unserializableStub(value, `too_large_${size}_bytes`);
98
+ }
99
+ return meta ? { json, meta } : { json };
100
+ } catch {
101
+ try {
102
+ return { json: JSON.parse(JSON.stringify(value)) };
103
+ } catch {
104
+ return unserializableStub(value, "json_stringify_failed");
105
+ }
106
+ }
107
+ }
108
+ function deserializeValue(serialized) {
109
+ if (serialized.meta === void 0) {
110
+ return serialized.json;
111
+ }
112
+ return superjson.deserialize({
113
+ json: serialized.json,
114
+ meta: serialized.meta
115
+ });
116
+ }
117
+
118
+ export {
119
+ BitfabError,
120
+ registerAsyncLocalStorageClass,
121
+ assertAsyncStorageRegistered,
122
+ asyncStorageReady,
123
+ isAsyncStorageInitDone,
124
+ createAsyncLocalStorage,
125
+ replayContextReady,
126
+ getReplayContext,
127
+ runWithReplayContext,
128
+ serializeValue,
129
+ deserializeValue
130
+ };
131
+ //# sourceMappingURL=chunk-QT7HWOKU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/asyncStorage.ts","../src/replayContext.ts","../src/serialize.ts"],"sourcesContent":["/**\n * Shared error type for Bitfab SDK runtime errors. Lives in its own\n * module to avoid import cycles between `http.ts` and modules that need\n * to throw structured errors (e.g. `dbSnapshot.ts` validation).\n */\n\nexport class BitfabError extends Error {\n constructor(\n message: string,\n public readonly url?: string,\n ) {\n super(message)\n this.name = \"BitfabError\"\n }\n}\n","/**\n * Shared AsyncLocalStorage loader.\n *\n * Provides two ways to initialize AsyncLocalStorage:\n *\n * 1. **Synchronous registration** (preferred for Node.js):\n * `asyncStorageNode.ts` calls `registerAsyncLocalStorageClass()` at module\n * evaluation time, so the class is available immediately — no async gap.\n * The `node.ts` entry point imports it before anything else.\n *\n * 2. **Async dynamic import** (fallback for the default entry point):\n * Loads `node:async_hooks` via a bundler-safe dynamic import. This is used\n * by the default `index.ts` entry point so the SDK works in browsers\n * (where the import silently fails) and in Node.js when imported via the\n * default entry point.\n *\n * ## Why the dynamic import looks like this\n *\n * We need to handle three environments:\n *\n * 1. **Pure Node.js** — `import(\"node:async_hooks\")` works natively.\n * 2. **Webpack/Turbopack (Next.js server)** — The bundler processes\n * `import()` calls at build time. The `webpackIgnore` magic comment tells\n * webpack (and turbopack) to emit a native `import()` call instead of\n * trying to resolve it, so Node.js handles it at runtime.\n * 3. **Browsers / Edge** — The `process.versions?.node` guard prevents\n * execution entirely. If it somehow runs, `.catch(() => {})` swallows\n * the failure.\n */\n\nexport interface AsyncLocalStorageLike<T> {\n getStore(): T | undefined\n run<R>(store: T, fn: () => R): R\n}\n\nlet AsyncLocalStorageClass: (new () => AsyncLocalStorageLike<unknown>) | null =\n null\nlet initDone = false\n\n/**\n * Register the AsyncLocalStorage class synchronously.\n *\n * Called by `asyncStorageNode.ts` at module evaluation time so the class\n * is available before any span is created — no async gap, no race condition.\n *\n * Safe to call multiple times; subsequent calls are no-ops.\n */\nexport function registerAsyncLocalStorageClass(\n cls: new () => AsyncLocalStorageLike<unknown>,\n): void {\n if (!AsyncLocalStorageClass) {\n AsyncLocalStorageClass = cls\n }\n initDone = true\n}\n\n/**\n * Assert that AsyncLocalStorage was registered successfully.\n *\n * Called by `node.ts` after importing `asyncStorageNode.ts` to catch\n * import-order bugs at startup rather than silently degrading to the\n * browser fallback (flat spans with no nesting).\n *\n * This should ONLY be called from the Node.js entry point where we\n * know `node:async_hooks` must be available.\n */\nexport function assertAsyncStorageRegistered(): void {\n if (!AsyncLocalStorageClass) {\n console.warn(\n \"Bitfab: AsyncLocalStorage not available — nested span context will not propagate.\",\n )\n }\n}\n\nexport const asyncStorageReady: Promise<void> = (\n typeof process !== \"undefined\" && process.versions?.node\n ? // The join trick hides \"node:async_hooks\" from static analysis so\n // bundlers that ban Node.js built-ins don't fail at build time.\n // webpackIgnore tells webpack/turbopack to emit a native import()\n // so Node.js can resolve the module at runtime.\n import(\n /* webpackIgnore: true */\n [\"node\", \"async_hooks\"].join(\":\")\n )\n .then(\n (mod: {\n AsyncLocalStorage: new () => AsyncLocalStorageLike<unknown>\n }) => {\n registerAsyncLocalStorageClass(mod.AsyncLocalStorage)\n },\n )\n .catch(() => {})\n : Promise.resolve()\n).then(() => {\n initDone = true\n})\n\nexport function isAsyncStorageInitDone(): boolean {\n return initDone\n}\n\nexport function createAsyncLocalStorage<T>(): AsyncLocalStorageLike<T> | null {\n return AsyncLocalStorageClass\n ? (new AsyncLocalStorageClass() as AsyncLocalStorageLike<T>)\n : null\n}\n","/**\n * Replay context propagation via AsyncLocalStorage.\n *\n * When set, the withSpan wrapper injects testRunId into the span payload\n * so that new spans created during replay are linked to the test run.\n * Optionally carries a mock tree so child spans can return historical\n * outputs instead of executing.\n */\n\nimport {\n type AsyncLocalStorageLike,\n asyncStorageReady,\n createAsyncLocalStorage,\n} from \"./asyncStorage.js\"\n\n/** A single span entry in the mock tree with its historical output. */\nexport interface MockSpan {\n sourceSpanId: string\n output: unknown\n outputMeta?: unknown\n}\n\n/**\n * Per-item DB branch resolved by the Bitfab service from the source\n * trace's `dbSnapshotRef`. Carried on the replay context so that\n * customer code reads `databaseUrl` through `ReplayEnvironment`, and so\n * the process-isolated replay runner can materialize it into a `.env`\n * overlay file before customer code initializes its DB client.\n *\n * `neonBranchId` is the literal Neon branch id; passing it to\n * `releaseDbBranchLease` deletes that branch.\n */\nexport interface DbBranchLease {\n neonBranchId: string\n /** Env var name the customer's app reads, e.g. \"DATABASE_URL\". */\n envKey: string\n databaseUrl: string\n expiresAt: string\n providerConsoleUrl?: string\n readOnly?: boolean\n}\n\n/**\n * Pre-built lookup table of historical span outputs.\n * Keys are `${traceFunctionKey}:${spanName}:${callIndex}` so that repeated\n * calls with the same (key, name) are matched by call order, but spans\n * sharing only the traceFunctionKey (different name) do not collide.\n */\nexport interface MockTree {\n spans: Map<string, MockSpan>\n}\n\nexport interface ReplayContext {\n testRunId: string\n traceId?: string\n inputSourceSpanId?: string\n /**\n * External trace ID from `external_traces.id`. Used for span-chain\n * lookup against the source platform's trace tree (Braintrust, etc.).\n * NOT the same as the Bitfab `traceId` — see `sourceBitfabTraceId`.\n */\n inputSourceTraceId?: string\n /**\n * The Bitfab `traces.id` of the historical trace that produced this\n * replay item's input. This is what customer-facing surfaces (e.g.\n * `ReplayEnvironment.traceId`) should expose, since it's the ID the\n * customer sees in the Bitfab dashboard.\n */\n sourceBitfabTraceId?: string\n mockTree?: MockTree\n callCounters?: Map<string, number>\n mockStrategy?: \"none\" | \"all\" | \"marked\"\n dbBranchLease?: DbBranchLease\n /**\n * Collector for the replay item's trace-persistence work. When present,\n * the root span's send path pushes a promise that resolves only after\n * every span upload AND the trace completion have been sent (registered\n * synchronously at send time, so the replay runner can await it after\n * the wrapped fn resolves). This is what lets replay guarantee traces\n * are persisted server-side before `completeReplay` builds the\n * trace-ID mapping. Absent outside replay, where sends stay\n * fire-and-forget.\n */\n pendingPersistence?: Promise<unknown>[]\n}\n\nlet replayContextStorage: AsyncLocalStorageLike<ReplayContext | null> | null =\n null\n\nexport const replayContextReady: Promise<void> = asyncStorageReady.then(() => {\n replayContextStorage = createAsyncLocalStorage<ReplayContext | null>()\n})\n\n/** Get the current replay context, if any. */\nexport function getReplayContext(): ReplayContext | null {\n return replayContextStorage?.getStore() ?? null\n}\n\n/** Run a function within a replay context. */\nexport function runWithReplayContext<T>(ctx: ReplayContext, fn: () => T): T {\n if (replayContextStorage) {\n return replayContextStorage.run(ctx, fn)\n }\n return fn()\n}\n","/**\n * Serialization utilities for Bitfab SDK.\n *\n * This module provides serialization with type metadata preservation,\n * using superjson for handling special JavaScript types like Date, Map,\n * Set, BigInt, undefined, etc.\n */\n\nimport superjson from \"superjson\"\n\n/**\n * Serialized value with JSON data and optional superjson meta for type preservation.\n *\n * The json field contains the JSON-serializable data.\n * The meta field (if present) contains superjson type information for deserializing\n * special types like Date, Map, Set, BigInt, etc.\n */\nexport interface SerializedValue {\n json: unknown\n meta?: unknown\n}\n\n// Cap on serialized payload size. superjson can succeed on values like SDK\n// client instances (OpenAI, etc.) and produce hundreds of KB to MB of useless\n// internal state. Anything beyond this is replaced with a stub so the span\n// still ships and the trace isn't dropped server-side.\nconst MAX_SERIALIZED_BYTES = 512_000\n\nfunction describeValue(value: unknown): string {\n try {\n const ctorName = (value as { constructor?: { name?: string } })?.constructor\n ?.name\n if (ctorName && ctorName !== \"Object\") {\n return ctorName\n }\n } catch {\n // Property access on `value` can throw (Proxy, poisoned getter).\n }\n return typeof value\n}\n\nfunction unserializableStub(value: unknown, reason: string): SerializedValue {\n let summary: string\n try {\n summary = `<unserializable: ${describeValue(value)} (${reason})>`\n } catch {\n summary = `<unserializable (${reason})>`\n }\n return { json: summary }\n}\n\n/**\n * Serialize a value using superjson for trace storage.\n *\n * Handles arbitrary JavaScript values including:\n * - Date, RegExp, Error\n * - Map, Set\n * - BigInt\n * - undefined (in objects/arrays)\n * - Circular references\n *\n * Guarantees:\n * - Never throws. Pathological inputs (SDK clients, proxies, poisoned\n * getters, circular graphs that defeat superjson) return a stub string.\n * - Never returns a payload larger than MAX_SERIALIZED_BYTES; oversized\n * inputs are replaced with a stub. Without this the wire-side\n * `JSON.stringify` in http.ts can produce a request that times out or\n * gets rejected, leaving a trace with zero spans.\n *\n * @param value - Any JavaScript value to serialize\n * @returns SerializedValue with 'json' field containing the data.\n * If type metadata is needed for reconstruction, includes 'meta' field.\n *\n * @example\n * ```typescript\n * const result = serializeValue(new Date('2024-01-15T10:30:00Z'))\n * // result.json contains the ISO string\n * // result.meta contains type info for Date reconstruction\n * ```\n */\nexport function serializeValue(value: unknown): SerializedValue {\n try {\n const { json, meta } = superjson.serialize(value)\n\n let size: number\n try {\n size = JSON.stringify(json).length\n } catch {\n return unserializableStub(value, \"stringify_failed_after_superjson\")\n }\n if (size > MAX_SERIALIZED_BYTES) {\n return unserializableStub(value, `too_large_${size}_bytes`)\n }\n\n return meta ? { json, meta } : { json }\n } catch {\n try {\n return { json: JSON.parse(JSON.stringify(value)) }\n } catch {\n return unserializableStub(value, \"json_stringify_failed\")\n }\n }\n}\n\n/**\n * Deserialize a value that was serialized with serializeValue.\n *\n * @param serialized - A SerializedValue object with 'json' and optional 'meta'\n * @returns The reconstructed JavaScript value\n *\n * @example\n * ```typescript\n * const serialized = serializeValue(new Date('2024-01-15'))\n * const date = deserializeValue(serialized)\n * // date is a Date object\n * ```\n */\nexport function deserializeValue(serialized: SerializedValue): unknown {\n if (serialized.meta === undefined) {\n // No metadata, return as-is\n return serialized.json\n }\n\n // Use superjson to deserialize with type reconstruction\n // Cast json to the expected superjson type\n type SuperJSONResult = Parameters<typeof superjson.deserialize>[0]\n return superjson.deserialize({\n json: serialized.json as SuperJSONResult[\"json\"],\n meta: serialized.meta as SuperJSONResult[\"meta\"],\n })\n}\n"],"mappings":";AAMO,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YACE,SACgB,KAChB;AACA,UAAM,OAAO;AAFG;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;;;ACqBA,IAAI,yBACF;AACF,IAAI,WAAW;AAUR,SAAS,+BACd,KACM;AACN,MAAI,CAAC,wBAAwB;AAC3B,6BAAyB;AAAA,EAC3B;AACA,aAAW;AACb;AAYO,SAAS,+BAAqC;AACnD,MAAI,CAAC,wBAAwB;AAC3B,YAAQ;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACF;AAEO,IAAM,qBACX,OAAO,YAAY,eAAe,QAAQ,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,EAKhD;AAAA;AAAA,IAEE,CAAC,QAAQ,aAAa,EAAE,KAAK,GAAG;AAAA,IAE/B;AAAA,IACC,CAAC,QAEK;AACJ,qCAA+B,IAAI,iBAAiB;AAAA,IACtD;AAAA,EACF,EACC,MAAM,MAAM;AAAA,EAAC,CAAC;AAAA,IACjB,QAAQ,QAAQ,GACpB,KAAK,MAAM;AACX,aAAW;AACb,CAAC;AAEM,SAAS,yBAAkC;AAChD,SAAO;AACT;AAEO,SAAS,0BAA8D;AAC5E,SAAO,yBACF,IAAI,uBAAuB,IAC5B;AACN;;;ACnBA,IAAI,uBACF;AAEK,IAAM,qBAAoC,kBAAkB,KAAK,MAAM;AAC5E,yBAAuB,wBAA8C;AACvE,CAAC;AAGM,SAAS,mBAAyC;AACvD,SAAO,sBAAsB,SAAS,KAAK;AAC7C;AAGO,SAAS,qBAAwB,KAAoB,IAAgB;AAC1E,MAAI,sBAAsB;AACxB,WAAO,qBAAqB,IAAI,KAAK,EAAE;AAAA,EACzC;AACA,SAAO,GAAG;AACZ;;;AChGA,OAAO,eAAe;AAkBtB,IAAM,uBAAuB;AAE7B,SAAS,cAAc,OAAwB;AAC7C,MAAI;AACF,UAAM,WAAY,OAA+C,aAC7D;AACJ,QAAI,YAAY,aAAa,UAAU;AACrC,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,OAAO;AAChB;AAEA,SAAS,mBAAmB,OAAgB,QAAiC;AAC3E,MAAI;AACJ,MAAI;AACF,cAAU,oBAAoB,cAAc,KAAK,CAAC,KAAK,MAAM;AAAA,EAC/D,QAAQ;AACN,cAAU,oBAAoB,MAAM;AAAA,EACtC;AACA,SAAO,EAAE,MAAM,QAAQ;AACzB;AA+BO,SAAS,eAAe,OAAiC;AAC9D,MAAI;AACF,UAAM,EAAE,MAAM,KAAK,IAAI,UAAU,UAAU,KAAK;AAEhD,QAAI;AACJ,QAAI;AACF,aAAO,KAAK,UAAU,IAAI,EAAE;AAAA,IAC9B,QAAQ;AACN,aAAO,mBAAmB,OAAO,kCAAkC;AAAA,IACrE;AACA,QAAI,OAAO,sBAAsB;AAC/B,aAAO,mBAAmB,OAAO,aAAa,IAAI,QAAQ;AAAA,IAC5D;AAEA,WAAO,OAAO,EAAE,MAAM,KAAK,IAAI,EAAE,KAAK;AAAA,EACxC,QAAQ;AACN,QAAI;AACF,aAAO,EAAE,MAAM,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,IACnD,QAAQ;AACN,aAAO,mBAAmB,OAAO,uBAAuB;AAAA,IAC1D;AAAA,EACF;AACF;AAeO,SAAS,iBAAiB,YAAsC;AACrE,MAAI,WAAW,SAAS,QAAW;AAEjC,WAAO,WAAW;AAAA,EACpB;AAKA,SAAO,UAAU,YAAY;AAAA,IAC3B,MAAM,WAAW;AAAA,IACjB,MAAM,WAAW;AAAA,EACnB,CAAC;AACH;","names":[]}
@@ -1,14 +1,350 @@
1
1
  import {
2
2
  BitfabError,
3
- DEFAULT_SERVICE_URL,
4
- HttpClient,
5
3
  asyncStorageReady,
6
4
  createAsyncLocalStorage,
7
5
  deserializeValue,
8
6
  getReplayContext,
9
7
  isAsyncStorageInitDone,
10
8
  serializeValue
11
- } from "./chunk-VFGUZWAV.js";
9
+ } from "./chunk-QT7HWOKU.js";
10
+
11
+ // src/version.generated.ts
12
+ var __version__ = "0.15.0";
13
+
14
+ // src/constants.ts
15
+ var DEFAULT_SERVICE_URL = "https://bitfab.ai";
16
+
17
+ // src/http.ts
18
+ var pendingTracePromises = /* @__PURE__ */ new Set();
19
+ function awaitOnExit(promise) {
20
+ pendingTracePromises.add(promise);
21
+ void promise.finally(() => {
22
+ pendingTracePromises.delete(promise);
23
+ }).catch(() => {
24
+ });
25
+ return promise;
26
+ }
27
+ async function flushTraces(timeoutMs = 5e3) {
28
+ if (pendingTracePromises.size === 0) {
29
+ return;
30
+ }
31
+ await Promise.race([
32
+ Promise.allSettled(Array.from(pendingTracePromises)),
33
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
34
+ ]);
35
+ }
36
+ if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
37
+ let isFlushing = false;
38
+ process.on("beforeExit", () => {
39
+ if (pendingTracePromises.size > 0 && !isFlushing) {
40
+ isFlushing = true;
41
+ Promise.allSettled(
42
+ Array.from(pendingTracePromises).map(
43
+ (p) => p.catch(() => {
44
+ })
45
+ )
46
+ ).then(() => {
47
+ isFlushing = false;
48
+ }).catch(() => {
49
+ isFlushing = false;
50
+ });
51
+ }
52
+ });
53
+ }
54
+ var HttpClient = class {
55
+ constructor(config) {
56
+ this.apiKey = config.apiKey;
57
+ this.serviceUrl = config.serviceUrl;
58
+ this.timeout = config.timeout ?? 12e4;
59
+ }
60
+ /**
61
+ * Make an HTTP request to the Bitfab API. Defaults to POST; pass
62
+ * `options.method` to use a different verb (e.g. "PATCH").
63
+ *
64
+ * @param endpoint - The API endpoint (without base URL)
65
+ * @param payload - The request body
66
+ * @param options - Optional request options
67
+ * @returns The parsed JSON response
68
+ * @throws {BitfabError} If the request fails
69
+ */
70
+ async request(endpoint, payload, options) {
71
+ const url = `${this.serviceUrl}${endpoint}`;
72
+ const timeout = options?.timeout ?? this.timeout;
73
+ const method = options?.method ?? "POST";
74
+ const controller = new AbortController();
75
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
76
+ let body;
77
+ let serializationError;
78
+ try {
79
+ body = JSON.stringify(payload);
80
+ } catch (error) {
81
+ serializationError = error instanceof Error ? error.message : String(error);
82
+ body = JSON.stringify({
83
+ ...Object.fromEntries(
84
+ Object.entries(payload).filter(
85
+ ([, v]) => typeof v === "string" || typeof v === "number"
86
+ )
87
+ ),
88
+ rawSpan: {},
89
+ errors: [
90
+ { source: "sdk", step: "json_serialize", error: serializationError }
91
+ ]
92
+ });
93
+ }
94
+ try {
95
+ const response = await fetch(url, {
96
+ method,
97
+ headers: {
98
+ "Content-Type": "application/json",
99
+ Authorization: `Bearer ${this.apiKey}`
100
+ },
101
+ body,
102
+ signal: controller.signal
103
+ });
104
+ if (!response.ok) {
105
+ const errorText = await response.text();
106
+ throw new BitfabError(
107
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
108
+ );
109
+ }
110
+ const result = await response.json();
111
+ if (result.error) {
112
+ if (result.url) {
113
+ throw new BitfabError(
114
+ `${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
115
+ result.url
116
+ );
117
+ }
118
+ throw new BitfabError(result.error);
119
+ }
120
+ return result;
121
+ } catch (error) {
122
+ if (error instanceof BitfabError) {
123
+ throw error;
124
+ }
125
+ if (error instanceof Error) {
126
+ if (error.name === "AbortError") {
127
+ throw new BitfabError(`Request timed out after ${timeout}ms`);
128
+ }
129
+ throw new BitfabError(error.message);
130
+ }
131
+ throw new BitfabError("Unknown error occurred");
132
+ } finally {
133
+ clearTimeout(timeoutId);
134
+ }
135
+ }
136
+ /**
137
+ * Look up a function by name.
138
+ * Blocks until complete - needed for function execution.
139
+ */
140
+ async lookupFunction(name) {
141
+ return this.request("/api/sdk/functions/lookup", { name });
142
+ }
143
+ /**
144
+ * Send an internal trace (from BAML execution).
145
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
146
+ */
147
+ sendInternalTrace(functionId, payload) {
148
+ void awaitOnExit(
149
+ this.request(`/api/sdk/functions/${functionId}/traces`, {
150
+ ...payload,
151
+ sdkVersion: __version__
152
+ })
153
+ ).catch((error) => {
154
+ try {
155
+ console.error("Bitfab: Failed to create trace:", error);
156
+ } catch {
157
+ }
158
+ });
159
+ }
160
+ /**
161
+ * Send an external span (from withSpan wrapper or OpenAI tracing).
162
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
163
+ * Returns the tracked promise so callers can optionally await it.
164
+ */
165
+ sendExternalSpan(payload) {
166
+ return awaitOnExit(
167
+ this.request("/api/sdk/externalSpans", {
168
+ ...payload,
169
+ sdkVersion: __version__
170
+ })
171
+ ).catch((error) => {
172
+ try {
173
+ console.error("Bitfab: Failed to create external span:", error);
174
+ } catch {
175
+ }
176
+ });
177
+ }
178
+ /**
179
+ * Send an external trace (from OpenAI tracing).
180
+ * Fire-and-forget with awaitOnExit - doesn't block the caller.
181
+ * Returns the tracked promise so callers can optionally await it
182
+ * (the replay path does, so trace completions are persisted before
183
+ * `completeReplay` builds the trace-ID mapping).
184
+ */
185
+ sendExternalTrace(payload) {
186
+ return awaitOnExit(
187
+ this.request("/api/sdk/externalTraces", {
188
+ ...payload,
189
+ sdkVersion: __version__
190
+ })
191
+ ).catch((error) => {
192
+ try {
193
+ console.error("Bitfab: Failed to create external trace:", error);
194
+ } catch {
195
+ }
196
+ });
197
+ }
198
+ /**
199
+ * Partial update of an existing external trace identified by sourceTraceId.
200
+ * Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
201
+ * returns a tracked promise that callers may optionally await.
202
+ */
203
+ patchTrace(sourceTraceId, payload) {
204
+ const endpoint = `/api/sdk/externalTraces/${encodeURIComponent(sourceTraceId)}`;
205
+ return awaitOnExit(
206
+ this.request(endpoint, payload, { method: "PATCH" })
207
+ ).catch((error) => {
208
+ try {
209
+ console.error("Bitfab: Failed to patch trace:", error);
210
+ } catch {
211
+ }
212
+ });
213
+ }
214
+ /**
215
+ * Start a replay session by fetching historical traces.
216
+ * Blocking call — creates a test run and returns lightweight item references.
217
+ */
218
+ async startReplay(traceFunctionKey, limit, traceIds, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId) {
219
+ const payload = { traceFunctionKey };
220
+ if (limit !== void 0) {
221
+ payload.limit = limit;
222
+ }
223
+ if (traceIds) {
224
+ payload.traceIds = traceIds;
225
+ }
226
+ if (codeChangeDescription !== void 0) {
227
+ payload.codeChangeDescription = codeChangeDescription;
228
+ }
229
+ if (codeChangeFiles !== void 0) {
230
+ payload.codeChangeFiles = codeChangeFiles;
231
+ }
232
+ if (includeDbBranchLease) {
233
+ payload.includeDbBranchLease = true;
234
+ }
235
+ if (experimentGroupId !== void 0) {
236
+ payload.experimentGroupId = experimentGroupId;
237
+ }
238
+ const timeout = includeDbBranchLease ? 18e4 : 3e4;
239
+ return this.request("/api/sdk/replay/start", payload, {
240
+ timeout
241
+ });
242
+ }
243
+ /**
244
+ * Fetch an external span by ID.
245
+ * Blocking GET request.
246
+ */
247
+ async getExternalSpan(spanId) {
248
+ const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
249
+ const controller = new AbortController();
250
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
251
+ try {
252
+ const response = await fetch(url, {
253
+ method: "GET",
254
+ headers: { Authorization: `Bearer ${this.apiKey}` },
255
+ signal: controller.signal
256
+ });
257
+ if (!response.ok) {
258
+ const errorText = await response.text();
259
+ throw new BitfabError(
260
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
261
+ );
262
+ }
263
+ return await response.json();
264
+ } catch (error) {
265
+ if (error instanceof BitfabError) {
266
+ throw error;
267
+ }
268
+ if (error instanceof Error) {
269
+ if (error.name === "AbortError") {
270
+ throw new BitfabError("Request timed out after 30000ms");
271
+ }
272
+ throw new BitfabError(error.message);
273
+ }
274
+ throw new BitfabError("Unknown error occurred");
275
+ } finally {
276
+ clearTimeout(timeoutId);
277
+ }
278
+ }
279
+ /**
280
+ * Fetch the span tree for a root span.
281
+ * Blocking GET request.
282
+ */
283
+ async getSpanTree(externalSpanId) {
284
+ const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}`;
285
+ const controller = new AbortController();
286
+ const timeoutId = setTimeout(() => controller.abort(), 3e4);
287
+ try {
288
+ const response = await fetch(url, {
289
+ method: "GET",
290
+ headers: { Authorization: `Bearer ${this.apiKey}` },
291
+ signal: controller.signal
292
+ });
293
+ if (!response.ok) {
294
+ const errorText = await response.text();
295
+ throw new BitfabError(
296
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`
297
+ );
298
+ }
299
+ return await response.json();
300
+ } catch (error) {
301
+ if (error instanceof BitfabError) {
302
+ throw error;
303
+ }
304
+ if (error instanceof Error) {
305
+ if (error.name === "AbortError") {
306
+ throw new BitfabError("Request timed out after 30000ms");
307
+ }
308
+ throw new BitfabError(error.message);
309
+ }
310
+ throw new BitfabError("Unknown error occurred");
311
+ } finally {
312
+ clearTimeout(timeoutId);
313
+ }
314
+ }
315
+ /**
316
+ * Mark a replay test run as completed.
317
+ * Blocking call.
318
+ */
319
+ async completeReplay(testRunId) {
320
+ return this.request(
321
+ "/api/sdk/replay/complete",
322
+ { testRunId },
323
+ { timeout: 3e4 }
324
+ );
325
+ }
326
+ /**
327
+ * Ask the server to materialize a per-trace DB branch lease from a
328
+ * captured `dbSnapshotRef`. Blocking — the resolver creates a Neon
329
+ * snapshot + preview branch and polls operations to readiness, which
330
+ * can take seconds.
331
+ */
332
+ async resolveDbBranchLease(testRunId, traceId, dbSnapshotRef) {
333
+ return this.request(
334
+ "/api/sdk/replay/resolveDbBranchLease",
335
+ { testRunId, traceId, dbSnapshotRef },
336
+ { timeout: 9e4 }
337
+ );
338
+ }
339
+ /** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
340
+ async releaseDbBranchLease(neonBranchId) {
341
+ await this.request(
342
+ "/api/sdk/replay/releaseDbBranchLease",
343
+ { neonBranchId },
344
+ { timeout: 3e4 }
345
+ );
346
+ }
347
+ };
12
348
 
13
349
  // src/claudeAgentSdk.ts
14
350
  function nowIso() {
@@ -2162,9 +2498,18 @@ var Bitfab = class {
2162
2498
  spanType: options.type ?? "custom"
2163
2499
  };
2164
2500
  const sendSpan = async (params) => {
2501
+ const replayCtx = getReplayContext();
2502
+ const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
2503
+ let resolvePersistence;
2504
+ if (persistenceCollector) {
2505
+ persistenceCollector.push(
2506
+ new Promise((resolve) => {
2507
+ resolvePersistence = resolve;
2508
+ })
2509
+ );
2510
+ }
2165
2511
  try {
2166
2512
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
2167
- const replayCtx = getReplayContext();
2168
2513
  const spanPromise = self.sendWrapperSpan({
2169
2514
  ...baseSpanParams,
2170
2515
  ...params,
@@ -2179,13 +2524,17 @@ var Bitfab = class {
2179
2524
  if (isRootSpan) {
2180
2525
  const pending = pendingSpanPromises.get(traceId) ?? [];
2181
2526
  pending.push(spanPromise);
2182
- await Promise.race([
2183
- Promise.allSettled(pending),
2184
- new Promise((resolve) => setTimeout(resolve, 5e3))
2185
- ]);
2527
+ if (persistenceCollector) {
2528
+ await Promise.allSettled(pending);
2529
+ } else {
2530
+ await Promise.race([
2531
+ Promise.allSettled(pending),
2532
+ new Promise((resolve) => setTimeout(resolve, 5e3))
2533
+ ]);
2534
+ }
2186
2535
  pendingSpanPromises.delete(traceId);
2187
2536
  const traceState = activeTraceStates.get(traceId);
2188
- self.sendTraceCompletion({
2537
+ const completionPromise = self.sendTraceCompletion({
2189
2538
  traceFunctionKey,
2190
2539
  traceId,
2191
2540
  startedAt: traceState?.startedAt ?? startedAt,
@@ -2198,6 +2547,9 @@ var Bitfab = class {
2198
2547
  dbSnapshotRef: traceState?.dbSnapshotRef
2199
2548
  });
2200
2549
  activeTraceStates.delete(traceId);
2550
+ if (persistenceCollector) {
2551
+ await completionPromise;
2552
+ }
2201
2553
  } else {
2202
2554
  const pending = pendingSpanPromises.get(traceId);
2203
2555
  if (pending) {
@@ -2207,6 +2559,8 @@ var Bitfab = class {
2207
2559
  }
2208
2560
  }
2209
2561
  } catch {
2562
+ } finally {
2563
+ resolvePersistence?.();
2210
2564
  }
2211
2565
  };
2212
2566
  const replayCtxForMock = getReplayContext();
@@ -2359,7 +2713,7 @@ var Bitfab = class {
2359
2713
  if (params.dbSnapshotRef) {
2360
2714
  rawTrace.db_snapshot_ref = params.dbSnapshotRef;
2361
2715
  }
2362
- this.httpClient.sendExternalTrace({
2716
+ return this.httpClient.sendExternalTrace({
2363
2717
  type: "sdk-function",
2364
2718
  source: "typescript-sdk-function",
2365
2719
  traceFunctionKey: params.traceFunctionKey,
@@ -2433,11 +2787,13 @@ var Bitfab = class {
2433
2787
  *
2434
2788
  * @param traceFunctionKey - The trace function key to replay
2435
2789
  * @param fn - The function to replay (must be the return value of `withSpan`)
2436
- * @param options - Optional replay options (limit, traceIds)
2790
+ * @param options - Optional replay options. `limit` and `traceIds` are
2791
+ * mutually exclusive — an explicit ID list already determines how many
2792
+ * traces replay, so passing both throws a BitfabError.
2437
2793
  * @returns ReplayResult with items, testRunId, and testRunUrl
2438
2794
  */
2439
2795
  async replay(traceFunctionKey, fn, options) {
2440
- const { replay: doReplay } = await import("./replay-F7K2JQCZ.js");
2796
+ const { replay: doReplay } = await import("./replay-3MQS22GS.js");
2441
2797
  return doReplay(
2442
2798
  this.httpClient,
2443
2799
  this.serviceUrl,
@@ -2503,6 +2859,9 @@ var BitfabFunction = class {
2503
2859
  };
2504
2860
 
2505
2861
  export {
2862
+ __version__,
2863
+ DEFAULT_SERVICE_URL,
2864
+ flushTraces,
2506
2865
  BitfabClaudeAgentHandler,
2507
2866
  SUPPORTED_PROVIDERS,
2508
2867
  BitfabLangGraphCallbackHandler,
@@ -2513,4 +2872,4 @@ export {
2513
2872
  Bitfab,
2514
2873
  BitfabFunction
2515
2874
  };
2516
- //# sourceMappingURL=chunk-4ANYHNQJ.js.map
2875
+ //# sourceMappingURL=chunk-YPG3XIG4.js.map