@bitfab/sdk 0.38.10 → 0.40.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.
@@ -3,218 +3,6 @@ import {
3
3
  createAsyncLocalStorage
4
4
  } from "./chunk-H6LZRFMN.js";
5
5
 
6
- // src/codeChange.ts
7
- var MAX_FILES = 60;
8
- var MAX_FILE_BYTES = 5e5;
9
- var MAX_TOTAL_BYTES = 2e6;
10
- var TRUNK_CANDIDATES = [
11
- "origin/HEAD",
12
- "origin/main",
13
- "origin/master",
14
- "main",
15
- "master"
16
- ];
17
- var NUL = String.fromCharCode(0);
18
- async function resolveAutoCodeChange(label) {
19
- if (typeof process === "undefined") {
20
- return null;
21
- }
22
- if (process.env?.BITFAB_DISABLE_CODE_CHANGE_CAPTURE) {
23
- return null;
24
- }
25
- const fromEnv = await readCodeChangeFile();
26
- if (fromEnv) {
27
- return fromEnv;
28
- }
29
- return captureCodeChangeFromGit(process.cwd?.() ?? ".", label);
30
- }
31
- async function readCodeChangeFile() {
32
- const path = process.env?.BITFAB_CODE_CHANGE_PATH;
33
- if (!path) {
34
- return null;
35
- }
36
- try {
37
- const { readFile } = await import("fs/promises");
38
- const parsed = JSON.parse(await readFile(path, "utf8"));
39
- const files = Array.isArray(parsed?.files) && parsed.files.every(
40
- (f) => typeof f === "object" && f !== null && !Array.isArray(f)
41
- ) ? parsed.files : void 0;
42
- const description = typeof parsed?.description === "string" ? parsed.description : void 0;
43
- if (!files && description === void 0) {
44
- return null;
45
- }
46
- return { description, files };
47
- } catch {
48
- return null;
49
- }
50
- }
51
- async function captureCodeChangeFromGit(cwd, label) {
52
- let execFile;
53
- let readFile;
54
- try {
55
- ;
56
- ({ execFile } = await import("child_process"));
57
- ({ readFile } = await import("fs/promises"));
58
- } catch {
59
- return null;
60
- }
61
- const git = (dir, args) => new Promise((resolve) => {
62
- execFile(
63
- "git",
64
- args,
65
- // 30s timeout so a hung git (e.g. a network-touching ref op) can't
66
- // block the whole replay indefinitely.
67
- { cwd: dir, maxBuffer: 64 * 1024 * 1024, timeout: 3e4 },
68
- (err, stdout) => resolve(err ? null : stdout)
69
- );
70
- });
71
- try {
72
- const root = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
73
- if (!root) {
74
- return null;
75
- }
76
- const resolved = await resolveBase(git, root);
77
- if (!resolved) {
78
- return null;
79
- }
80
- const { base, fromTrunk } = resolved;
81
- const blobBytes = async (ref, path) => {
82
- const out = await git(root, ["cat-file", "-s", `${ref}:${path}`]);
83
- const n = out ? Number.parseInt(out.trim(), 10) : Number.NaN;
84
- return Number.isFinite(n) ? n : 0;
85
- };
86
- const workingBytes = async (path) => {
87
- try {
88
- const { stat } = await import("fs/promises");
89
- const { join } = await import("path");
90
- return (await stat(join(root, path))).size;
91
- } catch {
92
- return 0;
93
- }
94
- };
95
- const tracked = await git(root, [
96
- "diff",
97
- "--name-status",
98
- "--find-renames",
99
- "-z",
100
- base,
101
- "--",
102
- ":!.bitfab"
103
- ]);
104
- const untracked = await git(root, [
105
- "ls-files",
106
- "--others",
107
- "--exclude-standard",
108
- "-z",
109
- "--",
110
- ":!.bitfab"
111
- ]);
112
- const entries = [
113
- ...parseNameStatusZ(tracked ?? ""),
114
- ...(untracked ?? "").split(NUL).filter((p) => p.length > 0).map((path) => ({ status: "A", beforePath: path, path }))
115
- ];
116
- if (entries.length === 0) {
117
- return null;
118
- }
119
- const files = [];
120
- let totalBytes = 0;
121
- for (const { status, beforePath, path } of entries) {
122
- if (files.length >= MAX_FILES) {
123
- break;
124
- }
125
- const beforeBytes = status === "A" ? 0 : await blobBytes(base, beforePath);
126
- const afterBytes = status === "D" ? 0 : await workingBytes(path);
127
- if (beforeBytes > MAX_FILE_BYTES || afterBytes > MAX_FILE_BYTES) {
128
- continue;
129
- }
130
- const before = (status === "A" ? "" : await git(root, ["show", `${base}:${beforePath}`]) ?? "").replace(/\r\n/g, "\n");
131
- const after = (status === "D" ? "" : await readWorkingFile(readFile, root, path)).replace(/\r\n/g, "\n");
132
- if (before === after) {
133
- continue;
134
- }
135
- const size = Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8");
136
- if (totalBytes + size > MAX_TOTAL_BYTES || looksBinary(before) || looksBinary(after)) {
137
- continue;
138
- }
139
- totalBytes += size;
140
- files.push({ path, before, after });
141
- }
142
- if (files.length === 0) {
143
- return null;
144
- }
145
- const subject = (await git(root, ["log", "-1", "--format=%s", "HEAD"]))?.trim();
146
- const fileWord = files.length === 1 ? "file" : "files";
147
- const head = label?.trim() || subject || "Working-tree change";
148
- const against = fromTrunk ? "vs trunk" : "uncommitted (vs HEAD)";
149
- return {
150
- description: `${head} (${files.length} ${fileWord} changed ${against})`,
151
- files
152
- };
153
- } catch {
154
- return null;
155
- }
156
- }
157
- async function resolveBase(git, root) {
158
- const forced = process.env?.BITFAB_CODE_CHANGE_BASE;
159
- if (forced && await refExists(git, root, forced)) {
160
- const base = (await git(root, ["merge-base", "HEAD", forced]))?.trim() || (await git(root, ["rev-parse", "--verify", forced]))?.trim() || null;
161
- return base ? { base, fromTrunk: true } : null;
162
- }
163
- for (const candidate of TRUNK_CANDIDATES) {
164
- if (!await refExists(git, root, candidate)) {
165
- continue;
166
- }
167
- const mb = (await git(root, ["merge-base", "HEAD", candidate]))?.trim();
168
- if (mb) {
169
- return { base: mb, fromTrunk: true };
170
- }
171
- }
172
- return await refExists(git, root, "HEAD") ? { base: "HEAD", fromTrunk: false } : null;
173
- }
174
- async function refExists(git, root, ref) {
175
- return await git(root, ["rev-parse", "--verify", `${ref}^{object}`]) !== null;
176
- }
177
- async function readWorkingFile(readFile, root, path) {
178
- try {
179
- const { join } = await import("path");
180
- return await readFile(join(root, path), "utf8");
181
- } catch {
182
- return "";
183
- }
184
- }
185
- function parseNameStatusZ(raw) {
186
- const parts = raw.split(NUL).filter((p) => p.length > 0);
187
- const out = [];
188
- let i = 0;
189
- while (i + 1 < parts.length) {
190
- const status = parts[i].charAt(0);
191
- i += 1;
192
- const beforePath = parts[i];
193
- i += 1;
194
- if ((status === "R" || status === "C") && i < parts.length) {
195
- out.push({ status, beforePath, path: parts[i] });
196
- i += 1;
197
- } else {
198
- out.push({ status, beforePath, path: beforePath });
199
- }
200
- }
201
- return out;
202
- }
203
- function looksBinary(s) {
204
- return s.slice(0, 8e3).includes(NUL);
205
- }
206
-
207
- // src/errors.ts
208
- var BitfabError = class extends Error {
209
- constructor(message, url, status, retryAfterMs) {
210
- super(message);
211
- this.url = url;
212
- this.status = status;
213
- this.retryAfterMs = retryAfterMs;
214
- this.name = "BitfabError";
215
- }
216
- };
217
-
218
6
  // src/readEnv.ts
219
7
  function readEnv(name) {
220
8
  if (typeof process !== "undefined" && process.env) {
@@ -311,12 +99,23 @@ function encodeRequestBody(body) {
311
99
  }
312
100
 
313
101
  // src/version.generated.ts
314
- var __version__ = "0.38.10";
102
+ var __version__ = "0.40.0";
315
103
  var __packageName__ = "@bitfab/sdk";
316
104
 
317
105
  // src/constants.ts
318
106
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
319
107
 
108
+ // src/errors.ts
109
+ var BitfabError = class extends Error {
110
+ constructor(message, url, status, retryAfterMs) {
111
+ super(message);
112
+ this.url = url;
113
+ this.status = status;
114
+ this.retryAfterMs = retryAfterMs;
115
+ this.name = "BitfabError";
116
+ }
117
+ };
118
+
320
119
  // src/replayContext.ts
321
120
  var replayContextStorage = null;
322
121
  var REPLAY_CONTEXT_STORAGE_SYMBOL = /* @__PURE__ */ Symbol.for("bitfab.replayContextStorage");
@@ -1757,6 +1556,10 @@ var HttpClient = class {
1757
1556
  const response = await this.get(endpoint);
1758
1557
  return response.span;
1759
1558
  }
1559
+ /**
1560
+ * GET a JSON endpoint on the service with the client's API key. Throws a
1561
+ * `BitfabError` carrying the status text for any non-2xx response.
1562
+ */
1760
1563
  async get(endpoint) {
1761
1564
  const url = `${this.serviceUrl}${endpoint}`;
1762
1565
  const controller = new AbortController();
@@ -1770,7 +1573,10 @@ var HttpClient = class {
1770
1573
  if (!response.ok) {
1771
1574
  const errorText = await response.text();
1772
1575
  throw new BitfabError(
1773
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1576
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1577
+ void 0,
1578
+ response.status,
1579
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1774
1580
  );
1775
1581
  }
1776
1582
  return await response.json();
@@ -2032,900 +1838,20 @@ var HttpClient = class {
2032
1838
  }
2033
1839
  };
2034
1840
 
2035
- // src/mockOverride.ts
2036
- var NO_MOCK_OVERRIDE = /* @__PURE__ */ Symbol("bitfab.noMockOverride");
2037
- function resolveMockValue(value, ctx) {
2038
- return typeof value === "function" ? value(ctx) : value;
2039
- }
2040
- function normalizeMockOverrides(mockOverride) {
2041
- if (mockOverride === void 0) {
2042
- return [];
2043
- }
2044
- const overrides = Array.isArray(mockOverride) ? mockOverride : [mockOverride];
2045
- return overrides.map(
2046
- (override) => typeof override === "function" ? { match: () => true, value: override } : override
2047
- );
2048
- }
2049
-
2050
- // src/randomUuid.ts
2051
- function randomUuid() {
2052
- const globalCrypto = globalThis.crypto;
2053
- if (typeof globalCrypto?.randomUUID === "function") {
2054
- try {
2055
- return globalCrypto.randomUUID();
2056
- } catch {
2057
- }
2058
- }
2059
- warnOnce(
2060
- "crypto-unavailable",
2061
- "global crypto.randomUUID is unavailable; using a non-cryptographic fallback for trace/span ids. Tracing works normally (ids are correlation-only, not security-sensitive)."
2062
- );
2063
- return fallbackUuidV4();
2064
- }
2065
- function fallbackUuidV4() {
2066
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
2067
- const rand = Math.random() * 16 | 0;
2068
- const value = char === "x" ? rand : rand & 3 | 8;
2069
- return value.toString(16);
2070
- });
2071
- }
2072
-
2073
- // src/serialize.ts
2074
- import superjson from "superjson";
2075
- var MAX_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
2076
- var MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
2077
- function describeValue(value) {
2078
- try {
2079
- const ctorName = value?.constructor?.name;
2080
- if (ctorName && ctorName !== "Object") {
2081
- return ctorName;
2082
- }
2083
- } catch {
2084
- }
2085
- return typeof value;
2086
- }
2087
- function unserializableStub(value, reason) {
2088
- warnOnce(
2089
- `serialize:${reason.replace(/\d+/g, "N")}`,
2090
- `a value could not be fully serialized for a span (${reason}); it was replaced with a placeholder. The span still ships, but its captured input/output is incomplete.`
2091
- );
2092
- let summary;
2093
- try {
2094
- summary = `<unserializable: ${describeValue(value)} (${reason})>`;
2095
- } catch {
2096
- summary = `<unserializable (${reason})>`;
2097
- }
2098
- return { json: summary };
2099
- }
2100
- function serializeValue(value) {
2101
- try {
2102
- const { json, meta } = superjson.serialize(value);
2103
- let size;
2104
- try {
2105
- size = JSON.stringify(json).length;
2106
- } catch {
2107
- return unserializableStub(value, "stringify_failed_after_superjson");
2108
- }
2109
- if (size > MAX_SERIALIZED_BYTES) {
2110
- return unserializableStub(value, `too_large_${size}_bytes`);
2111
- }
2112
- return meta ? { json, meta } : { json };
2113
- } catch {
2114
- try {
2115
- return { json: JSON.parse(JSON.stringify(value)) };
2116
- } catch {
2117
- return unserializableStub(value, "json_stringify_failed");
2118
- }
2119
- }
2120
- }
2121
- function deserializeValue(serialized) {
2122
- if (serialized.meta === void 0) {
2123
- return serialized.json;
2124
- }
2125
- return superjson.deserialize({
2126
- json: serialized.json,
2127
- meta: serialized.meta
2128
- });
2129
- }
2130
- var MAX_SAFE_DEPTH = 6;
2131
- function toJsonSafe(value) {
2132
- return toJsonSafeReport(value).safe;
2133
- }
2134
- function toJsonSafeReport(value) {
2135
- const dropped = [];
2136
- const safe = toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet(), dropped);
2137
- try {
2138
- const size = JSON.stringify(safe)?.length ?? 0;
2139
- if (size > MAX_FRAMEWORK_SERIALIZED_BYTES) {
2140
- warnOnce(
2141
- "toJsonSafe:too_large",
2142
- `a framework payload exceeded ${MAX_FRAMEWORK_SERIALIZED_BYTES} bytes and was replaced with a placeholder so the span still ships. The captured state for this span is incomplete.`
2143
- );
2144
- return {
2145
- safe: `<unserializable: too_large_${size}_bytes>`,
2146
- dropped: [...dropped, `too_large_${size}_bytes`]
2147
- };
2148
- }
2149
- } catch {
2150
- }
2151
- return { safe, dropped };
2152
- }
2153
- function toJsonSafeInner(value, depth, seen, dropped) {
2154
- if (value === null || value === void 0) {
2155
- return value;
2156
- }
2157
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
2158
- return value;
2159
- }
2160
- const className = value?.constructor?.name ?? typeof value;
2161
- if (depth > MAX_SAFE_DEPTH) {
2162
- dropped.push(className);
2163
- return `<${className}>`;
2164
- }
2165
- if (typeof value !== "object") {
2166
- if (typeof value === "function" || typeof value === "symbol") {
2167
- dropped.push(className);
2168
- }
2169
- try {
2170
- return String(value);
2171
- } catch {
2172
- dropped.push(className);
2173
- return `<${className}>`;
2174
- }
2175
- }
2176
- if (seen.has(value)) {
2177
- dropped.push(className);
2178
- return `<cycle ${className}>`;
2179
- }
2180
- seen.add(value);
2181
- let result;
2182
- if (Array.isArray(value)) {
2183
- result = value.map(
2184
- (item) => toJsonSafeInner(item, depth + 1, seen, dropped)
2185
- );
2186
- } else if (typeof value.toJSON === "function") {
2187
- try {
2188
- result = toJsonSafeInner(
2189
- value.toJSON(),
2190
- depth + 1,
2191
- seen,
2192
- dropped
2193
- );
2194
- } catch {
2195
- dropped.push(className);
2196
- result = `<${className}>`;
2197
- }
2198
- } else {
2199
- try {
2200
- const obj = {};
2201
- for (const [k, v] of Object.entries(value)) {
2202
- if (!k.startsWith("_")) {
2203
- obj[k] = toJsonSafeInner(v, depth + 1, seen, dropped);
2204
- }
2205
- }
2206
- result = obj;
2207
- } catch {
2208
- dropped.push(className);
2209
- result = `<${className}>`;
2210
- }
2211
- }
2212
- seen.delete(value);
2213
- return result;
2214
- }
2215
-
2216
- // src/replay.ts
2217
- var REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
2218
- function dbBranchEnabled(dbBranch) {
2219
- return dbBranch !== void 0 && dbBranch !== false;
2220
- }
2221
- function resolveDbBranchSettings(dbBranch) {
2222
- if (!dbBranch || dbBranch === true) {
2223
- return void 0;
2224
- }
2225
- const { minCu, maxCu, warmupSql } = dbBranch;
2226
- const settings = {
2227
- ...minCu === void 0 ? {} : { minCu },
2228
- ...maxCu === void 0 ? {} : { maxCu },
2229
- ...warmupSql === void 0 ? {} : { warmupSql }
2230
- };
2231
- return Object.keys(settings).length === 0 ? void 0 : settings;
2232
- }
2233
- var BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
2234
- function reportReplayProgress(progress) {
2235
- const stderr = typeof process !== "undefined" ? process.stderr : void 0;
2236
- if (!stderr) {
2237
- return;
2238
- }
2239
- try {
2240
- stderr.write(
2241
- `${BITFAB_PROGRESS_PREFIX}${JSON.stringify(progress, replayJsonReplacer)}
2242
- `
2243
- );
2244
- } catch {
2245
- }
2246
- }
2247
- var ReplayError = class extends BitfabError {
2248
- constructor(message, items, testRunId, testRunUrl, cause) {
2249
- super(message, testRunUrl);
2250
- this.items = items;
2251
- this.testRunId = testRunId;
2252
- this.testRunUrl = testRunUrl;
2253
- this.cause = cause;
2254
- this.name = "ReplayError";
2255
- }
2256
- };
2257
- var DbBranchReplayError = class extends BitfabError {
2258
- constructor(code, message, originalTraceId, cause) {
2259
- super(message);
2260
- this.code = code;
2261
- this.originalTraceId = originalTraceId;
2262
- this.cause = cause;
2263
- this.name = "DbBranchReplayError";
2264
- }
2265
- };
2266
- function errorMessage(error) {
2267
- return error instanceof Error ? error.message : String(error);
2268
- }
2269
- function replayItemErrorMessage(error) {
2270
- if (error instanceof DbBranchReplayError) {
2271
- return `Replay requested a database branch for trace ${error.originalTraceId} but it could not be resolved (${error.code}): ${error.message}. The function was not run, because replaying it against the live database would produce a result that looks valid but did not use the historical data you asked for.`;
2272
- }
2273
- return errorMessage(error);
2274
- }
2275
- function replayJsonReplacer(_key, value) {
2276
- if (value instanceof Error) {
2277
- const serialized = {
2278
- name: value.name,
2279
- message: value.message,
2280
- stack: value.stack
2281
- };
2282
- if (value instanceof DbBranchReplayError) {
2283
- serialized.code = value.code;
2284
- serialized.originalTraceId = value.originalTraceId;
2285
- if (value.cause !== void 0) {
2286
- serialized.cause = value.cause;
2287
- }
2288
- }
2289
- return serialized;
2290
- }
2291
- return value;
2292
- }
2293
- function serializeReplayResult(result) {
2294
- return JSON.stringify(result, replayJsonReplacer, 2);
2295
- }
2296
- async function preserveReplayFailure(operation, items, testRunId, testRunUrl) {
2297
- try {
2298
- return await operation();
2299
- } catch (cause) {
2300
- if (cause instanceof ReplayError) {
2301
- throw cause;
2302
- }
2303
- throw new ReplayError(
2304
- errorMessage(cause),
2305
- items,
2306
- testRunId,
2307
- testRunUrl,
2308
- cause
2309
- );
2310
- }
2311
- }
2312
- function deserializeInputs(spanData) {
2313
- const inputMeta = spanData.input_meta;
2314
- const rawInput = spanData.input;
2315
- if (inputMeta !== void 0 && inputMeta !== null) {
2316
- const deserialized = deserializeValue({ json: rawInput, meta: inputMeta });
2317
- if (Array.isArray(deserialized)) {
2318
- return deserialized;
2319
- }
2320
- return deserialized !== void 0 && deserialized !== null ? [deserialized] : [];
2321
- }
2322
- if (Array.isArray(rawInput)) {
2323
- return rawInput;
2324
- }
2325
- return rawInput !== void 0 && rawInput !== null ? [rawInput] : [];
2326
- }
2327
- function deserializeOutput(spanData) {
2328
- const outputMeta = spanData.output_meta;
2329
- const rawOutput = spanData.output;
2330
- if (outputMeta !== void 0 && outputMeta !== null) {
2331
- return deserializeValue({ json: rawOutput, meta: outputMeta });
2332
- }
2333
- return rawOutput;
2334
- }
2335
- function buildMockTree(rootNode) {
2336
- const spans = /* @__PURE__ */ new Map();
2337
- const counters = /* @__PURE__ */ new Map();
2338
- function walk(node) {
2339
- const key = node.traceFunctionKey;
2340
- if (key) {
2341
- const name = node.spanName || key;
2342
- const counterKey = `${key}:${name}`;
2343
- const index = counters.get(counterKey) ?? 0;
2344
- counters.set(counterKey, index + 1);
2345
- spans.set(`${counterKey}:${index}`, {
2346
- sourceSpanId: node.sourceSpanId,
2347
- externalSpanId: node.externalSpanId,
2348
- output: node.output,
2349
- outputMeta: node.outputMeta
2350
- });
2351
- }
2352
- for (const child of node.children) {
2353
- walk(child);
2354
- }
2355
- }
2356
- for (const child of rootNode.children) {
2357
- walk(child);
2358
- }
2359
- return { spans };
2360
- }
2361
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, includeDbBranchLease, dbBranchSettings, adaptInputs) {
2362
- let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
2363
- let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
2364
- let dbSnapshotRef = serverItem.dbSnapshotRef;
2365
- let dbBranchTimings = includeDbBranchLease ? serverItem.dbBranchTimings : void 0;
2366
- let inputs = [];
2367
- let originalOutput;
2368
- let result;
2369
- let error = null;
2370
- let traceError = null;
2371
- let replayError = null;
2372
- let replayDurationMs = null;
2373
- let replayStarted = null;
2374
- const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
2375
- const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
2376
- try {
2377
- if (includeDbBranchLease && !lease && !leaseError) {
2378
- let resolved;
2379
- try {
2380
- resolved = await httpClient.resolveDbBranchLease(
2381
- testRunId,
2382
- originalTraceId,
2383
- dbBranchSettings
2384
- );
2385
- } catch (cause) {
2386
- throw new DbBranchReplayError(
2387
- "lease_request_failed",
2388
- `Bitfab could not request the database branch: ${errorMessage(cause)}`,
2389
- originalTraceId,
2390
- cause
2391
- );
2392
- }
2393
- lease = resolved.lease ?? void 0;
2394
- leaseError = resolved.leaseError ?? void 0;
2395
- dbSnapshotRef = resolved.dbSnapshotRef ?? dbSnapshotRef;
2396
- dbBranchTimings = resolved.timings ?? dbBranchTimings;
2397
- }
2398
- if (leaseError) {
2399
- throw new DbBranchReplayError(
2400
- leaseError.code,
2401
- leaseError.message,
2402
- originalTraceId
2403
- );
2404
- }
2405
- const span = await httpClient.getExternalSpan(originalSpanId, {
2406
- view: "replay"
2407
- });
2408
- const spanData = span.rawData?.span_data ?? {};
2409
- inputs = deserializeInputs(spanData);
2410
- originalOutput = deserializeOutput(spanData);
2411
- if (adaptInputs) {
2412
- inputs = adaptInputs(inputs, {
2413
- originalTraceId,
2414
- originalSpanId,
2415
- // Deprecated aliases for originalTraceId/originalSpanId.
2416
- sourceTraceId: originalTraceId,
2417
- sourceSpanId: originalSpanId
2418
- });
2419
- }
2420
- const hasOverrides = resolvedOverrides.length > 0;
2421
- const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
2422
- const includeOutputs = mockStrategy === "all";
2423
- let mockTree;
2424
- if (needTree) {
2425
- try {
2426
- const treeResponse = await httpClient.getSpanTree(originalSpanId, {
2427
- includeOutputs,
2428
- includeRootOutput: false
2429
- });
2430
- if (!treeResponse.root) {
2431
- throw new BitfabError(
2432
- `Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for original span ${originalSpanId}.`
2433
- );
2434
- }
2435
- mockTree = buildMockTree(treeResponse.root);
2436
- } catch (error2) {
2437
- if (mockStrategy !== "marked" || hasOverrides) {
2438
- throw error2;
2439
- }
2440
- mockTree = { spans: /* @__PURE__ */ new Map() };
2441
- }
2442
- }
2443
- const outputCache = /* @__PURE__ */ new Map();
2444
- const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
2445
- let pending = outputCache.get(externalSpanId);
2446
- if (!pending) {
2447
- pending = httpClient.getExternalSpan(externalSpanId, { view: "replay" }).then(
2448
- (s) => deserializeOutput(
2449
- s.rawData?.span_data ?? {}
2450
- )
2451
- );
2452
- outputCache.set(externalSpanId, pending);
2453
- }
2454
- return pending;
2455
- } : void 0;
2456
- try {
2457
- replayStarted = performance.now();
2458
- const maybePromise = runWithReplayContext(
2459
- {
2460
- testRunId,
2461
- traceId: replayedTraceId,
2462
- inputSourceSpanId: span.id,
2463
- inputSourceTraceId: span.externalTraceId,
2464
- sourceBitfabTraceId: originalTraceId,
2465
- mockTree,
2466
- callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
2467
- mockStrategy,
2468
- mockOverrides: hasOverrides ? resolvedOverrides : void 0,
2469
- fetchSpanOutput,
2470
- dbBranchLease: lease,
2471
- dbBranchTimings
2472
- },
2473
- () => fn(...inputs)
2474
- );
2475
- result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
2476
- replayDurationMs = Math.round(performance.now() - replayStarted);
2477
- } catch (e) {
2478
- if (replayStarted !== null) {
2479
- replayDurationMs = Math.round(performance.now() - replayStarted);
2480
- }
2481
- traceError = e;
2482
- error = errorMessage(e);
2483
- }
2484
- } catch (e) {
2485
- if (replayStarted !== null) {
2486
- replayDurationMs = Math.round(performance.now() - replayStarted);
2487
- }
2488
- replayError = e;
2489
- error = replayItemErrorMessage(e);
2490
- } finally {
2491
- if (lease) {
2492
- try {
2493
- await httpClient.releaseDbBranchLease(lease.neonBranchId);
2494
- } catch (e) {
2495
- try {
2496
- console.warn(
2497
- `Bitfab: failed to release DB branch ${lease.neonBranchId} (TTL janitor will catch it): ${e instanceof Error ? e.message : String(e)}`
2498
- );
2499
- } catch {
2500
- }
2501
- }
2502
- }
2503
- }
2504
- const originalDurationMs = serverItem.originalDurationMs ?? serverItem.durationMs ?? null;
2505
- const originalModel = serverItem.originalModel ?? serverItem.model ?? null;
2506
- return {
2507
- // Written in by replay() from the complete-replay response once the server
2508
- // has minted this replay trace's row. Null until then: the client-side
2509
- // correlation id (replayedTraceId) is never surfaced as the item's traceId.
2510
- traceId: null,
2511
- originalTraceId,
2512
- originalSpanId,
2513
- // Deprecated aliases for originalTraceId/originalSpanId.
2514
- sourceTraceId: originalTraceId,
2515
- sourceSpanId: originalSpanId,
2516
- input: inputs,
2517
- result,
2518
- originalOutput,
2519
- error,
2520
- traceError,
2521
- replayError,
2522
- durationMs: replayDurationMs,
2523
- originalDurationMs,
2524
- originalTokens: serverItem.originalTokens ?? serverItem.tokens ?? null,
2525
- originalModel,
2526
- // Filled in by replay() from the complete-replay response once the
2527
- // replay traces are persisted and their spans aggregated server-side.
2528
- // Null here (and on older servers) means "replay tokens not known".
2529
- tokens: null,
2530
- model: originalModel,
2531
- dbSnapshotRef: dbSnapshotRef ?? null,
2532
- dbBranchTimings: dbBranchTimings ?? null
2533
- };
2534
- }
2535
- async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds) {
2536
- const deferredSettled = await httpClient.settleDeferredWork(
2537
- REPLAY_PERSISTENCE_TIMEOUT_MS
2538
- );
2539
- if (!deferredSettled) {
2540
- httpClient.takeTraceDeliveries(replayedTraceIds);
2541
- throw new BitfabError(
2542
- `Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
2543
- );
2544
- }
2545
- if (!httpClient.hasClosedDeliveries(replayedTraceIds)) {
2546
- httpClient.takeTraceDeliveries(replayedTraceIds);
2547
- return {};
2548
- }
2549
- const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2550
- const deliveries = httpClient.takeTraceDeliveries(replayedTraceIds);
2551
- const expectedSpanCounts = {};
2552
- const readBackTraceIds = {};
2553
- let allDelivered = true;
2554
- for (const [traceId, delivery] of Object.entries(deliveries)) {
2555
- if (delivery.serverTraceId !== void 0) {
2556
- readBackTraceIds[traceId] = delivery.serverTraceId;
2557
- }
2558
- if (!delivery.closed) {
2559
- continue;
2560
- }
2561
- expectedSpanCounts[traceId] = delivery.spanCount;
2562
- allDelivered = allDelivered && delivery.delivered;
2563
- }
2564
- if (allDelivered) {
2565
- return readBackTraceIds;
2566
- }
2567
- const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
2568
- let missing = Object.keys(expectedSpanCounts).length;
2569
- while (true) {
2570
- const status = await httpClient.getReplayStatus(
2571
- testRunId,
2572
- expectedSpanCounts
2573
- );
2574
- const ready = status.traceIds ?? {};
2575
- missing = Object.keys(expectedSpanCounts).filter(
2576
- (traceId) => ready[traceId] === void 0
2577
- ).length;
2578
- if (missing === 0) {
2579
- return readBackTraceIds;
2580
- }
2581
- if (Date.now() >= deadline) {
2582
- break;
2583
- }
2584
- await sleepForReplayPersistence(
2585
- Math.min(100, Math.max(0, deadline - Date.now()))
2586
- );
2587
- }
2588
- const cause = flushed ? "" : " Delivery was also not confirmed before the flush deadline, so the spans likely never reached the server.";
2589
- throw new BitfabError(
2590
- `Replay traces were not fully persisted before the delivery deadline (testRunId ${testRunId}, missing ${missing} of ${Object.keys(expectedSpanCounts).length} trace(s)).${cause}`
2591
- );
2592
- }
2593
- function sleepForReplayPersistence(ms) {
2594
- return new Promise((resolve) => {
2595
- setTimeout(resolve, ms);
2596
- });
2597
- }
2598
- async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted) {
2599
- const results = new Array(tasks.length);
2600
- let nextIndex = 0;
2601
- async function worker() {
2602
- while (nextIndex < tasks.length) {
2603
- const index = nextIndex++;
2604
- onStarted?.(index);
2605
- const result = await tasks[index]();
2606
- results[index] = result;
2607
- await onSettled?.(result, index);
2608
- }
2609
- }
2610
- const workers = Array.from(
2611
- { length: Math.min(maxConcurrency, tasks.length) },
2612
- () => worker()
2613
- );
2614
- await Promise.all(workers);
2615
- return results;
2616
- }
2617
- async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
2618
- if (options?.traceIds !== void 0) {
2619
- if (options.traceIds.length === 0) {
2620
- throw new BitfabError("traceIds must contain at least one trace ID.");
2621
- }
2622
- if (options.traceIds.length > 100) {
2623
- throw new BitfabError(
2624
- `traceIds supports at most 100 trace IDs per replay (got ${options.traceIds.length}).`
2625
- );
2626
- }
2627
- }
2628
- if (options?.traceIds !== void 0 && options?.datasetId !== void 0) {
2629
- throw new BitfabError(
2630
- "traceIds and datasetId select different replay sources and cannot be used together."
2631
- );
2632
- }
2633
- if (options?.limit !== void 0 && options?.traceIds !== void 0) {
2634
- try {
2635
- console.warn(
2636
- "Bitfab: limit is ignored when traceIds is passed: the explicit trace ID list already determines how many traces replay."
2637
- );
2638
- } catch {
2639
- }
2640
- }
2641
- await replayContextReady;
2642
- let codeChangeDescription = options?.codeChangeDescription;
2643
- let codeChangeFiles = options?.codeChangeFiles;
2644
- if (codeChangeFiles === void 0) {
2645
- const captured = await resolveAutoCodeChange(options?.name);
2646
- if (captured) {
2647
- codeChangeFiles = captured.files;
2648
- if (codeChangeDescription === void 0) {
2649
- codeChangeDescription = captured.description;
2650
- }
2651
- }
2652
- }
2653
- const {
2654
- testRunId,
2655
- testRunUrl,
2656
- items: serverItems
2657
- } = await httpClient.startReplay(
2658
- traceFunctionKey,
2659
- // limit is meaningless with explicit traceIds (the ID list determines
2660
- // the count), so it's omitted from the request entirely.
2661
- options?.traceIds ? void 0 : options?.limit ?? 5,
2662
- options?.traceIds,
2663
- options?.name,
2664
- codeChangeDescription,
2665
- codeChangeFiles,
2666
- dbBranchEnabled(options?.dbBranch),
2667
- // includeDbBranchLease
2668
- options?.experimentGroupId,
2669
- options?.datasetId,
2670
- options?.graderIds,
2671
- resolveDbBranchSettings(options?.dbBranch)
2672
- );
2673
- const mockStrategy = options?.mock ?? "marked";
2674
- const maxConcurrency = options?.maxConcurrency ?? 10;
2675
- const fullTestRunUrl = `${serviceUrl}${testRunUrl}`;
2676
- const resolvedOverrides = [
2677
- ...normalizeMockOverrides(options?.mockOverride),
2678
- ...registeredOverrides
2679
- ];
2680
- const replayedTraceIds = serverItems.map(() => randomUuid());
2681
- httpClient.trackTraceDeliveries(replayedTraceIds);
2682
- const tasks = serverItems.map(
2683
- (serverItem, index) => () => processItem(
2684
- httpClient,
2685
- serverItem,
2686
- fn,
2687
- testRunId,
2688
- mockStrategy,
2689
- resolvedOverrides,
2690
- replayedTraceIds[index],
2691
- dbBranchEnabled(options?.dbBranch),
2692
- resolveDbBranchSettings(options?.dbBranch),
2693
- options?.adaptInputs
2694
- )
2695
- );
2696
- const total = tasks.length;
2697
- const onItemFinish = options?.onItemFinish ?? options?.onProgress;
2698
- let completed = 0;
2699
- let started = 0;
2700
- let succeeded = 0;
2701
- let errored = 0;
2702
- let flushInFlight = null;
2703
- let flushPending = false;
2704
- const flushFinishedItemTrace = () => {
2705
- flushPending = true;
2706
- if (!flushInFlight) {
2707
- flushInFlight = (async () => {
2708
- try {
2709
- while (flushPending) {
2710
- flushPending = false;
2711
- await httpClient.settleDeferredWork(REPLAY_PERSISTENCE_TIMEOUT_MS);
2712
- await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2713
- }
2714
- } finally {
2715
- flushInFlight = null;
2716
- }
2717
- })();
2718
- }
2719
- return flushInFlight;
2720
- };
2721
- const resultItems = await mapWithConcurrency2(
2722
- tasks,
2723
- maxConcurrency,
2724
- async (item, index) => {
2725
- let serverTraceId = null;
2726
- try {
2727
- await flushFinishedItemTrace();
2728
- serverTraceId = httpClient.peekServerTraceId(replayedTraceIds[index]) ?? null;
2729
- } catch {
2730
- }
2731
- item.traceId = serverTraceId;
2732
- completed += 1;
2733
- if (item.error === null) {
2734
- succeeded += 1;
2735
- } else {
2736
- errored += 1;
2737
- }
2738
- try {
2739
- onItemFinish?.({
2740
- testRunId,
2741
- completed,
2742
- total,
2743
- succeeded,
2744
- errored,
2745
- item: {
2746
- // The server's assigned traces.id, read back off the ingest
2747
- // response and surfaced as this item finishes. Null only if the
2748
- // per-item flush could not confirm delivery in time; the end-of-run
2749
- // barrier then fills the returned ReplayItem. The client-side
2750
- // placeholder is never surfaced.
2751
- traceId: serverTraceId,
2752
- originalTraceId: item.originalTraceId ?? null,
2753
- originalSpanId: item.originalSpanId ?? null,
2754
- // Deprecated aliases for originalTraceId/originalSpanId.
2755
- sourceTraceId: item.originalTraceId ?? null,
2756
- sourceSpanId: item.originalSpanId ?? null,
2757
- input: item.input,
2758
- result: item.result,
2759
- originalOutput: item.originalOutput,
2760
- error: item.error,
2761
- traceError: item.traceError,
2762
- replayError: item.replayError,
2763
- durationMs: item.durationMs,
2764
- originalDurationMs: item.originalDurationMs,
2765
- originalTokens: item.originalTokens,
2766
- originalModel: item.originalModel,
2767
- tokens: item.tokens,
2768
- model: item.model,
2769
- dbSnapshotRef: item.dbSnapshotRef,
2770
- dbBranchTimings: item.dbBranchTimings
2771
- }
2772
- });
2773
- } catch {
2774
- }
2775
- },
2776
- options?.onItemStart ? (index) => {
2777
- started += 1;
2778
- const serverItem = serverItems[index];
2779
- const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
2780
- const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
2781
- try {
2782
- options.onItemStart?.({
2783
- type: "started",
2784
- testRunId,
2785
- started,
2786
- completed,
2787
- total,
2788
- succeeded,
2789
- errored,
2790
- item: {
2791
- originalTraceId,
2792
- originalSpanId,
2793
- sourceTraceId: originalTraceId,
2794
- sourceSpanId: originalSpanId
2795
- }
2796
- });
2797
- } catch {
2798
- }
2799
- } : void 0
2800
- );
2801
- const deliveredTraceIds = await preserveReplayFailure(
2802
- () => waitForReplayPersistence(httpClient, testRunId, replayedTraceIds),
2803
- resultItems,
2804
- testRunId,
2805
- fullTestRunUrl
2806
- );
2807
- for (let index = 0; index < resultItems.length; index += 1) {
2808
- const localId = replayedTraceIds[index];
2809
- const readBack = localId ? deliveredTraceIds[localId] : void 0;
2810
- if (readBack !== void 0) {
2811
- resultItems[index].traceId = readBack;
2812
- }
2813
- }
2814
- const completeResult = await preserveReplayFailure(
2815
- () => httpClient.completeReplay(testRunId),
2816
- resultItems,
2817
- testRunId,
2818
- fullTestRunUrl
2819
- );
2820
- const serverTraceIds = completeResult.traceIds;
2821
- const replayTokens = completeResult.tokens;
2822
- if (serverTraceIds !== void 0) {
2823
- const missing = [];
2824
- let completedCount = 0;
2825
- for (let index = 0; index < resultItems.length; index += 1) {
2826
- const item = resultItems[index];
2827
- const localId = replayedTraceIds[index];
2828
- const mapped = localId ? serverTraceIds[localId] : void 0;
2829
- item.traceId = item.traceId ?? mapped ?? null;
2830
- if (item.error === null) {
2831
- completedCount += 1;
2832
- if (mapped === void 0) {
2833
- missing.push(localId ?? item.originalTraceId);
2834
- }
2835
- }
2836
- if (mapped !== void 0) {
2837
- item.tokens = replayTokens?.[mapped] ?? null;
2838
- }
2839
- }
2840
- if (completedCount > 0 && missing.length === completedCount) {
2841
- const serverCount = completeResult.traceCount !== void 0 ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.` : "";
2842
- const cause = new BitfabError(
2843
- `Replay completed but the server has no persisted trace for any of the ${completedCount} completed item(s) (testRunId ${testRunId}).${serverCount} 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.`
2844
- );
2845
- throw new ReplayError(
2846
- cause.message,
2847
- resultItems,
2848
- testRunId,
2849
- fullTestRunUrl,
2850
- cause
2851
- );
2852
- }
2853
- if (missing.length > 0) {
2854
- try {
2855
- console.error(
2856
- `Bitfab: server has no persisted trace for ${missing.length} of ${completedCount} completed replay item(s) (testRunId ${testRunId}). Their replay token usage is unavailable and they cannot be labeled.`
2857
- );
2858
- } catch {
2859
- }
2860
- }
2861
- }
2862
- const result = {
2863
- items: resultItems,
2864
- testRunId,
2865
- testRunUrl: fullTestRunUrl
2866
- };
2867
- await writeReplayResultFile(result);
2868
- if (!options?.onItemFinish) {
2869
- try {
2870
- options?.onProgress?.({
2871
- type: "complete",
2872
- testRunId,
2873
- completed: total,
2874
- total,
2875
- succeeded,
2876
- errored,
2877
- result
2878
- });
2879
- } catch {
2880
- }
2881
- }
2882
- return result;
2883
- }
2884
- async function writeReplayResultFile(result) {
2885
- const resultPath = typeof process !== "undefined" ? process.env?.BITFAB_REPLAY_RESULT_PATH : void 0;
2886
- if (!resultPath) {
2887
- return;
2888
- }
2889
- try {
2890
- const [{ dirname }, { mkdir, writeFile }] = await Promise.all([
2891
- import("path"),
2892
- import("fs/promises")
2893
- ]);
2894
- await mkdir(dirname(resultPath), { recursive: true });
2895
- await writeFile(resultPath, `${serializeReplayResult(result)}
2896
- `);
2897
- } catch (err) {
2898
- try {
2899
- console.warn(
2900
- `Bitfab: failed to write replay result to BITFAB_REPLAY_RESULT_PATH (${resultPath}): ${err instanceof Error ? err.message : String(err)}`
2901
- );
2902
- } catch {
2903
- }
2904
- }
2905
- }
2906
-
2907
1841
  export {
2908
1842
  __version__,
2909
1843
  DEFAULT_SERVICE_URL,
2910
1844
  BitfabError,
1845
+ replayContextReady,
2911
1846
  getReplayContext,
1847
+ runWithReplayContext,
1848
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES,
2912
1849
  warnOnce,
1850
+ serializePayloadBody,
1851
+ awaitOnExit,
2913
1852
  flushTraces,
2914
- HttpClient,
2915
- serializeValue,
2916
- deserializeValue,
2917
- toJsonSafe,
2918
- toJsonSafeReport,
2919
- randomUuid,
2920
- NO_MOCK_OVERRIDE,
2921
- resolveMockValue,
2922
- BITFAB_PROGRESS_PREFIX,
2923
- reportReplayProgress,
2924
- ReplayError,
2925
- DbBranchReplayError,
2926
- serializeReplayResult,
2927
- waitForReplayPersistence,
2928
- sleepForReplayPersistence,
2929
- replay
1853
+ awaitPendingRequests,
1854
+ parseRetryAfterMs,
1855
+ HttpClient
2930
1856
  };
2931
- //# sourceMappingURL=chunk-BNOVHUQB.js.map
1857
+ //# sourceMappingURL=chunk-A22EYRSY.js.map