bitfab 0.33.7 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/chunk-4J36FZ4K.js +2299 -0
- package/dist/chunk-4J36FZ4K.js.map +1 -0
- package/dist/{chunk-DZJ5K75J.js → chunk-ESKBRHVG.js} +87 -604
- package/dist/chunk-ESKBRHVG.js.map +1 -0
- package/dist/index.cjs +2297 -1365
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +374 -28
- package/dist/index.d.ts +374 -28
- package/dist/index.js +5 -5
- package/dist/node.cjs +2287 -1355
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +5 -5
- package/dist/{replay-SRQI4QMY.js → replay-QFNYP7EY.js} +2 -2
- package/package.json +6 -1
- package/dist/chunk-DPV6PBWE.js +0 -916
- package/dist/chunk-DPV6PBWE.js.map +0 -1
- package/dist/chunk-DZJ5K75J.js.map +0 -1
- /package/dist/{replay-SRQI4QMY.js.map → replay-QFNYP7EY.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -46,219 +46,41 @@ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read fr
|
|
|
46
46
|
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
47
47
|
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
48
48
|
|
|
49
|
+
// src/version.generated.ts
|
|
50
|
+
var __version__;
|
|
51
|
+
var init_version_generated = __esm({
|
|
52
|
+
"src/version.generated.ts"() {
|
|
53
|
+
"use strict";
|
|
54
|
+
__version__ = "0.34.0";
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// src/constants.ts
|
|
59
|
+
var DEFAULT_SERVICE_URL;
|
|
60
|
+
var init_constants = __esm({
|
|
61
|
+
"src/constants.ts"() {
|
|
62
|
+
"use strict";
|
|
63
|
+
init_version_generated();
|
|
64
|
+
DEFAULT_SERVICE_URL = "https://bitfab.ai";
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
49
68
|
// src/errors.ts
|
|
50
69
|
var BitfabError;
|
|
51
70
|
var init_errors = __esm({
|
|
52
71
|
"src/errors.ts"() {
|
|
53
72
|
"use strict";
|
|
54
73
|
BitfabError = class extends Error {
|
|
55
|
-
constructor(message, url) {
|
|
74
|
+
constructor(message, url, status) {
|
|
56
75
|
super(message);
|
|
57
76
|
this.url = url;
|
|
77
|
+
this.status = status;
|
|
58
78
|
this.name = "BitfabError";
|
|
59
79
|
}
|
|
60
80
|
};
|
|
61
81
|
}
|
|
62
82
|
});
|
|
63
83
|
|
|
64
|
-
// src/warnOnce.ts
|
|
65
|
-
function warnOnce(key, message) {
|
|
66
|
-
if (warned.has(key)) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
warned.add(key);
|
|
70
|
-
try {
|
|
71
|
-
console.warn(`[bitfab] ${message}`);
|
|
72
|
-
} catch {
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
var warned;
|
|
76
|
-
var init_warnOnce = __esm({
|
|
77
|
-
"src/warnOnce.ts"() {
|
|
78
|
-
"use strict";
|
|
79
|
-
warned = /* @__PURE__ */ new Set();
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// src/serialize.ts
|
|
84
|
-
function describeValue(value) {
|
|
85
|
-
try {
|
|
86
|
-
const ctorName = value?.constructor?.name;
|
|
87
|
-
if (ctorName && ctorName !== "Object") {
|
|
88
|
-
return ctorName;
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
}
|
|
92
|
-
return typeof value;
|
|
93
|
-
}
|
|
94
|
-
function unserializableStub(value, reason) {
|
|
95
|
-
warnOnce(
|
|
96
|
-
`serialize:${reason.replace(/\d+/g, "N")}`,
|
|
97
|
-
`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.`
|
|
98
|
-
);
|
|
99
|
-
let summary;
|
|
100
|
-
try {
|
|
101
|
-
summary = `<unserializable: ${describeValue(value)} (${reason})>`;
|
|
102
|
-
} catch {
|
|
103
|
-
summary = `<unserializable (${reason})>`;
|
|
104
|
-
}
|
|
105
|
-
return { json: summary };
|
|
106
|
-
}
|
|
107
|
-
function serializeValue(value) {
|
|
108
|
-
try {
|
|
109
|
-
const { json, meta } = import_superjson.default.serialize(value);
|
|
110
|
-
let size;
|
|
111
|
-
try {
|
|
112
|
-
size = JSON.stringify(json).length;
|
|
113
|
-
} catch {
|
|
114
|
-
return unserializableStub(value, "stringify_failed_after_superjson");
|
|
115
|
-
}
|
|
116
|
-
if (size > MAX_SERIALIZED_BYTES) {
|
|
117
|
-
return unserializableStub(value, `too_large_${size}_bytes`);
|
|
118
|
-
}
|
|
119
|
-
return meta ? { json, meta } : { json };
|
|
120
|
-
} catch {
|
|
121
|
-
try {
|
|
122
|
-
return { json: JSON.parse(JSON.stringify(value)) };
|
|
123
|
-
} catch {
|
|
124
|
-
return unserializableStub(value, "json_stringify_failed");
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
function deserializeValue(serialized) {
|
|
129
|
-
if (serialized.meta === void 0) {
|
|
130
|
-
return serialized.json;
|
|
131
|
-
}
|
|
132
|
-
return import_superjson.default.deserialize({
|
|
133
|
-
json: serialized.json,
|
|
134
|
-
meta: serialized.meta
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
function toJsonSafe(value) {
|
|
138
|
-
return toJsonSafeReport(value).safe;
|
|
139
|
-
}
|
|
140
|
-
function toJsonSafeReport(value) {
|
|
141
|
-
const dropped = [];
|
|
142
|
-
const safe = toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet(), dropped);
|
|
143
|
-
try {
|
|
144
|
-
const size = JSON.stringify(safe)?.length ?? 0;
|
|
145
|
-
if (size > MAX_FRAMEWORK_SERIALIZED_BYTES) {
|
|
146
|
-
warnOnce(
|
|
147
|
-
"toJsonSafe:too_large",
|
|
148
|
-
`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.`
|
|
149
|
-
);
|
|
150
|
-
return {
|
|
151
|
-
safe: `<unserializable: too_large_${size}_bytes>`,
|
|
152
|
-
dropped: [...dropped, `too_large_${size}_bytes`]
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
} catch {
|
|
156
|
-
}
|
|
157
|
-
return { safe, dropped };
|
|
158
|
-
}
|
|
159
|
-
function toJsonSafeInner(value, depth, seen, dropped) {
|
|
160
|
-
if (value === null || value === void 0) {
|
|
161
|
-
return value;
|
|
162
|
-
}
|
|
163
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
164
|
-
return value;
|
|
165
|
-
}
|
|
166
|
-
const className = value?.constructor?.name ?? typeof value;
|
|
167
|
-
if (depth > MAX_SAFE_DEPTH) {
|
|
168
|
-
dropped.push(className);
|
|
169
|
-
return `<${className}>`;
|
|
170
|
-
}
|
|
171
|
-
if (typeof value !== "object") {
|
|
172
|
-
if (typeof value === "function" || typeof value === "symbol") {
|
|
173
|
-
dropped.push(className);
|
|
174
|
-
}
|
|
175
|
-
try {
|
|
176
|
-
return String(value);
|
|
177
|
-
} catch {
|
|
178
|
-
dropped.push(className);
|
|
179
|
-
return `<${className}>`;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (seen.has(value)) {
|
|
183
|
-
dropped.push(className);
|
|
184
|
-
return `<cycle ${className}>`;
|
|
185
|
-
}
|
|
186
|
-
seen.add(value);
|
|
187
|
-
let result;
|
|
188
|
-
if (Array.isArray(value)) {
|
|
189
|
-
result = value.map(
|
|
190
|
-
(item) => toJsonSafeInner(item, depth + 1, seen, dropped)
|
|
191
|
-
);
|
|
192
|
-
} else if (typeof value.toJSON === "function") {
|
|
193
|
-
try {
|
|
194
|
-
result = toJsonSafeInner(
|
|
195
|
-
value.toJSON(),
|
|
196
|
-
depth + 1,
|
|
197
|
-
seen,
|
|
198
|
-
dropped
|
|
199
|
-
);
|
|
200
|
-
} catch {
|
|
201
|
-
dropped.push(className);
|
|
202
|
-
result = `<${className}>`;
|
|
203
|
-
}
|
|
204
|
-
} else {
|
|
205
|
-
try {
|
|
206
|
-
const obj = {};
|
|
207
|
-
for (const [k, v] of Object.entries(value)) {
|
|
208
|
-
if (!k.startsWith("_")) {
|
|
209
|
-
obj[k] = toJsonSafeInner(v, depth + 1, seen, dropped);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
result = obj;
|
|
213
|
-
} catch {
|
|
214
|
-
dropped.push(className);
|
|
215
|
-
result = `<${className}>`;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
seen.delete(value);
|
|
219
|
-
return result;
|
|
220
|
-
}
|
|
221
|
-
var import_superjson, MAX_SERIALIZED_BYTES, MAX_FRAMEWORK_SERIALIZED_BYTES, MAX_SAFE_DEPTH;
|
|
222
|
-
var init_serialize = __esm({
|
|
223
|
-
"src/serialize.ts"() {
|
|
224
|
-
"use strict";
|
|
225
|
-
import_superjson = __toESM(require("superjson"), 1);
|
|
226
|
-
init_warnOnce();
|
|
227
|
-
MAX_SERIALIZED_BYTES = 512e3;
|
|
228
|
-
MAX_FRAMEWORK_SERIALIZED_BYTES = 2e6;
|
|
229
|
-
MAX_SAFE_DEPTH = 6;
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// src/randomUuid.ts
|
|
234
|
-
function randomUuid() {
|
|
235
|
-
const globalCrypto = globalThis.crypto;
|
|
236
|
-
if (typeof globalCrypto?.randomUUID === "function") {
|
|
237
|
-
try {
|
|
238
|
-
return globalCrypto.randomUUID();
|
|
239
|
-
} catch {
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
warnOnce(
|
|
243
|
-
"crypto-unavailable",
|
|
244
|
-
"global crypto.randomUUID is unavailable; using a non-cryptographic fallback for trace/span ids. Tracing works normally (ids are correlation-only, not security-sensitive)."
|
|
245
|
-
);
|
|
246
|
-
return fallbackUuidV4();
|
|
247
|
-
}
|
|
248
|
-
function fallbackUuidV4() {
|
|
249
|
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
|
|
250
|
-
const rand = Math.random() * 16 | 0;
|
|
251
|
-
const value = char === "x" ? rand : rand & 3 | 8;
|
|
252
|
-
return value.toString(16);
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
var init_randomUuid = __esm({
|
|
256
|
-
"src/randomUuid.ts"() {
|
|
257
|
-
"use strict";
|
|
258
|
-
init_warnOnce();
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
|
|
262
84
|
// src/asyncStorage.ts
|
|
263
85
|
function registerAsyncLocalStorageClass(cls) {
|
|
264
86
|
if (!AsyncLocalStorageClass) {
|
|
@@ -298,22 +120,6 @@ var init_asyncStorage = __esm({
|
|
|
298
120
|
}
|
|
299
121
|
});
|
|
300
122
|
|
|
301
|
-
// src/mockOverride.ts
|
|
302
|
-
function resolveMockValue(value, ctx) {
|
|
303
|
-
return typeof value === "function" ? value(ctx) : value;
|
|
304
|
-
}
|
|
305
|
-
function normalizeMockOverrides(mockOverride) {
|
|
306
|
-
if (mockOverride === void 0) {
|
|
307
|
-
return [];
|
|
308
|
-
}
|
|
309
|
-
return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
|
|
310
|
-
}
|
|
311
|
-
var init_mockOverride = __esm({
|
|
312
|
-
"src/mockOverride.ts"() {
|
|
313
|
-
"use strict";
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
|
|
317
123
|
// src/replayContext.ts
|
|
318
124
|
function getReplayContext() {
|
|
319
125
|
return replayContextStorage?.getStore() ?? null;
|
|
@@ -347,1184 +153,2306 @@ var init_replayContext = __esm({
|
|
|
347
153
|
}
|
|
348
154
|
});
|
|
349
155
|
|
|
350
|
-
// src/
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
return
|
|
354
|
-
}
|
|
355
|
-
if (process.env?.BITFAB_DISABLE_CODE_CHANGE_CAPTURE) {
|
|
356
|
-
return null;
|
|
156
|
+
// src/warnOnce.ts
|
|
157
|
+
function warnOnce(key, message) {
|
|
158
|
+
if (warned.has(key)) {
|
|
159
|
+
return;
|
|
357
160
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
161
|
+
warned.add(key);
|
|
162
|
+
try {
|
|
163
|
+
console.warn(`[bitfab] ${message}`);
|
|
164
|
+
} catch {
|
|
361
165
|
}
|
|
362
|
-
return captureCodeChangeFromGit(process.cwd?.() ?? ".", label);
|
|
363
166
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
167
|
+
var warned;
|
|
168
|
+
var init_warnOnce = __esm({
|
|
169
|
+
"src/warnOnce.ts"() {
|
|
170
|
+
"use strict";
|
|
171
|
+
warned = /* @__PURE__ */ new Set();
|
|
368
172
|
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// src/serializePayload.ts
|
|
176
|
+
function serializePayloadBody(payload) {
|
|
369
177
|
try {
|
|
370
|
-
|
|
371
|
-
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
372
|
-
const files = Array.isArray(parsed?.files) && parsed.files.every(
|
|
373
|
-
(f) => typeof f === "object" && f !== null && !Array.isArray(f)
|
|
374
|
-
) ? parsed.files : void 0;
|
|
375
|
-
const description = typeof parsed?.description === "string" ? parsed.description : void 0;
|
|
376
|
-
if (!files && description === void 0) {
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
return { description, files };
|
|
380
|
-
} catch {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
async function captureCodeChangeFromGit(cwd, label) {
|
|
385
|
-
let execFile;
|
|
386
|
-
let readFile;
|
|
387
|
-
try {
|
|
388
|
-
;
|
|
389
|
-
({ execFile } = await import("child_process"));
|
|
390
|
-
({ readFile } = await import("fs/promises"));
|
|
178
|
+
return { body: JSON.stringify(payload), dropped: [] };
|
|
391
179
|
} catch {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
args,
|
|
398
|
-
// 30s timeout so a hung git (e.g. a network-touching ref op) can't
|
|
399
|
-
// block the whole replay indefinitely.
|
|
400
|
-
{ cwd: dir, maxBuffer: 64 * 1024 * 1024, timeout: 3e4 },
|
|
401
|
-
(err, stdout) => resolve(err ? null : stdout)
|
|
402
|
-
);
|
|
403
|
-
});
|
|
404
|
-
try {
|
|
405
|
-
const root = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
|
|
406
|
-
if (!root) {
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
const resolved = await resolveBase(git, root);
|
|
410
|
-
if (!resolved) {
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
const { base, fromTrunk } = resolved;
|
|
414
|
-
const blobBytes = async (ref, path) => {
|
|
415
|
-
const out = await git(root, ["cat-file", "-s", `${ref}:${path}`]);
|
|
416
|
-
const n = out ? Number.parseInt(out.trim(), 10) : Number.NaN;
|
|
417
|
-
return Number.isFinite(n) ? n : 0;
|
|
418
|
-
};
|
|
419
|
-
const workingBytes = async (path) => {
|
|
420
|
-
try {
|
|
421
|
-
const { stat } = await import("fs/promises");
|
|
422
|
-
const { join } = await import("path");
|
|
423
|
-
return (await stat(join(root, path))).size;
|
|
424
|
-
} catch {
|
|
425
|
-
return 0;
|
|
180
|
+
const dropped = [];
|
|
181
|
+
const sanitize = (value, seen) => {
|
|
182
|
+
const t = typeof value;
|
|
183
|
+
if (value === null || t === "string" || t === "number" || t === "boolean") {
|
|
184
|
+
return value;
|
|
426
185
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
"--name-status",
|
|
431
|
-
"--no-renames",
|
|
432
|
-
"-z",
|
|
433
|
-
base,
|
|
434
|
-
"--",
|
|
435
|
-
":!.bitfab"
|
|
436
|
-
]);
|
|
437
|
-
const untracked = await git(root, [
|
|
438
|
-
"ls-files",
|
|
439
|
-
"--others",
|
|
440
|
-
"--exclude-standard",
|
|
441
|
-
"-z",
|
|
442
|
-
"--",
|
|
443
|
-
":!.bitfab"
|
|
444
|
-
]);
|
|
445
|
-
const entries = [
|
|
446
|
-
...parseNameStatusZ(tracked ?? ""),
|
|
447
|
-
...(untracked ?? "").split(NUL).filter((p) => p.length > 0).map((path) => ({ status: "A", path }))
|
|
448
|
-
];
|
|
449
|
-
if (entries.length === 0) {
|
|
450
|
-
return null;
|
|
451
|
-
}
|
|
452
|
-
const files = [];
|
|
453
|
-
let totalBytes = 0;
|
|
454
|
-
for (const { status, path } of entries) {
|
|
455
|
-
if (files.length >= MAX_FILES) {
|
|
456
|
-
break;
|
|
186
|
+
if (t === "bigint") {
|
|
187
|
+
dropped.push("BigInt");
|
|
188
|
+
return "<unserializable: BigInt>";
|
|
457
189
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
190
|
+
if (t === "function") {
|
|
191
|
+
const name = value.name || "Function";
|
|
192
|
+
dropped.push(name);
|
|
193
|
+
return `<unserializable: ${name}>`;
|
|
462
194
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
continue;
|
|
195
|
+
if (t === "symbol") {
|
|
196
|
+
dropped.push("Symbol");
|
|
197
|
+
return "<unserializable: Symbol>";
|
|
467
198
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
continue;
|
|
199
|
+
if (t !== "object") {
|
|
200
|
+
return void 0;
|
|
471
201
|
}
|
|
472
|
-
|
|
473
|
-
|
|
202
|
+
const obj = value;
|
|
203
|
+
const className = obj.constructor?.name || "object";
|
|
204
|
+
if (seen.has(obj)) {
|
|
205
|
+
dropped.push(className);
|
|
206
|
+
return `<cycle: ${className}>`;
|
|
207
|
+
}
|
|
208
|
+
seen.add(obj);
|
|
209
|
+
let result;
|
|
210
|
+
if (Array.isArray(obj)) {
|
|
211
|
+
result = obj.map((item) => sanitize(item, seen));
|
|
212
|
+
} else if (typeof obj.toJSON === "function") {
|
|
213
|
+
try {
|
|
214
|
+
result = sanitize(obj.toJSON(), seen);
|
|
215
|
+
} catch {
|
|
216
|
+
dropped.push(className);
|
|
217
|
+
result = `<unserializable: ${className}>`;
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
try {
|
|
221
|
+
const out = {};
|
|
222
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
223
|
+
out[k] = sanitize(v, seen);
|
|
224
|
+
}
|
|
225
|
+
result = out;
|
|
226
|
+
} catch {
|
|
227
|
+
warnOnce(
|
|
228
|
+
"payload:field-getter-threw",
|
|
229
|
+
"a value with a throwing getter/proxy could not be serialized into a span payload; it was replaced with a placeholder. The span still ships with its other fields intact."
|
|
230
|
+
);
|
|
231
|
+
dropped.push(className);
|
|
232
|
+
result = `<unserializable: ${className}>`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
seen.delete(obj);
|
|
236
|
+
return result;
|
|
237
|
+
};
|
|
238
|
+
let sanitized;
|
|
239
|
+
try {
|
|
240
|
+
sanitized = sanitize(payload, /* @__PURE__ */ new WeakSet());
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
243
|
+
return {
|
|
244
|
+
body: JSON.stringify({ error: `payload_serialize_failed: ${message}` }),
|
|
245
|
+
dropped
|
|
246
|
+
};
|
|
474
247
|
}
|
|
475
|
-
if (
|
|
476
|
-
|
|
248
|
+
if (dropped.length > 0 && typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
|
|
249
|
+
const obj = sanitized;
|
|
250
|
+
const existing = Array.isArray(obj.errors) ? obj.errors : [];
|
|
251
|
+
obj.errors = [
|
|
252
|
+
...existing,
|
|
253
|
+
{
|
|
254
|
+
source: "sdk",
|
|
255
|
+
step: "json_serialize",
|
|
256
|
+
error: `stubbed non-serializable value(s): ${[
|
|
257
|
+
...new Set(dropped)
|
|
258
|
+
].join(", ")}`
|
|
259
|
+
}
|
|
260
|
+
];
|
|
477
261
|
}
|
|
478
|
-
|
|
479
|
-
const fileWord = files.length === 1 ? "file" : "files";
|
|
480
|
-
const head = label?.trim() || subject || "Working-tree change";
|
|
481
|
-
const against = fromTrunk ? "vs trunk" : "uncommitted (vs HEAD)";
|
|
482
|
-
return {
|
|
483
|
-
description: `${head} (${files.length} ${fileWord} changed ${against})`,
|
|
484
|
-
files
|
|
485
|
-
};
|
|
486
|
-
} catch {
|
|
487
|
-
return null;
|
|
262
|
+
return { body: JSON.stringify(sanitized), dropped };
|
|
488
263
|
}
|
|
489
264
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return base ? { base, fromTrunk: true } : null;
|
|
265
|
+
var init_serializePayload = __esm({
|
|
266
|
+
"src/serializePayload.ts"() {
|
|
267
|
+
"use strict";
|
|
268
|
+
init_warnOnce();
|
|
495
269
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
return { base: mb, fromTrunk: true };
|
|
503
|
-
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// src/readEnv.ts
|
|
273
|
+
function readEnv(name) {
|
|
274
|
+
if (typeof process !== "undefined" && process.env) {
|
|
275
|
+
return process.env[name];
|
|
504
276
|
}
|
|
505
|
-
return
|
|
506
|
-
}
|
|
507
|
-
async function refExists(git, root, ref) {
|
|
508
|
-
return await git(root, ["rev-parse", "--verify", `${ref}^{object}`]) !== null;
|
|
277
|
+
return void 0;
|
|
509
278
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
return await readFile(join(root, path), "utf8");
|
|
514
|
-
} catch {
|
|
515
|
-
return "";
|
|
279
|
+
var init_readEnv = __esm({
|
|
280
|
+
"src/readEnv.ts"() {
|
|
281
|
+
"use strict";
|
|
516
282
|
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// src/unrefTimer.ts
|
|
286
|
+
function unrefTimer(timer) {
|
|
287
|
+
const handle = timer;
|
|
288
|
+
if (typeof handle.unref === "function") {
|
|
289
|
+
handle.unref();
|
|
523
290
|
}
|
|
524
|
-
return out;
|
|
525
291
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
var MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, TRUNK_CANDIDATES, NUL;
|
|
530
|
-
var init_codeChange = __esm({
|
|
531
|
-
"src/codeChange.ts"() {
|
|
292
|
+
var init_unrefTimer = __esm({
|
|
293
|
+
"src/unrefTimer.ts"() {
|
|
532
294
|
"use strict";
|
|
533
|
-
MAX_FILES = 60;
|
|
534
|
-
MAX_FILE_BYTES = 5e5;
|
|
535
|
-
MAX_TOTAL_BYTES = 2e6;
|
|
536
|
-
TRUNK_CANDIDATES = [
|
|
537
|
-
"origin/HEAD",
|
|
538
|
-
"origin/main",
|
|
539
|
-
"origin/master",
|
|
540
|
-
"main",
|
|
541
|
-
"master"
|
|
542
|
-
];
|
|
543
|
-
NUL = String.fromCharCode(0);
|
|
544
295
|
}
|
|
545
296
|
});
|
|
546
297
|
|
|
547
|
-
// src/
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
298
|
+
// src/otel.ts
|
|
299
|
+
function readBoundedIntEnv(name, max, fallback, warnKey) {
|
|
300
|
+
const raw = readEnv(name);
|
|
301
|
+
if (raw === void 0) {
|
|
302
|
+
return fallback;
|
|
303
|
+
}
|
|
304
|
+
const value = Number(raw);
|
|
305
|
+
if (Number.isInteger(value) && value > 0 && value <= max) {
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
warnOnce(
|
|
309
|
+
warnKey,
|
|
310
|
+
`${name} must be a positive integer no greater than ${max}; using ${fallback}`
|
|
311
|
+
);
|
|
312
|
+
return fallback;
|
|
556
313
|
}
|
|
557
|
-
function
|
|
558
|
-
|
|
559
|
-
|
|
314
|
+
function logError(message, error) {
|
|
315
|
+
try {
|
|
316
|
+
if (error === void 0) {
|
|
317
|
+
console.error(`[bitfab] ${message}`);
|
|
318
|
+
} else {
|
|
319
|
+
console.error(`[bitfab] ${message}`, error);
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
560
322
|
}
|
|
561
|
-
const { minCu, maxCu, warmupSql } = dbBranch;
|
|
562
|
-
const settings = {
|
|
563
|
-
...minCu === void 0 ? {} : { minCu },
|
|
564
|
-
...maxCu === void 0 ? {} : { maxCu },
|
|
565
|
-
...warmupSql === void 0 ? {} : { warmupSql }
|
|
566
|
-
};
|
|
567
|
-
return Object.keys(settings).length === 0 ? void 0 : settings;
|
|
568
323
|
}
|
|
569
|
-
function
|
|
570
|
-
const
|
|
571
|
-
if (
|
|
324
|
+
function recordTraceSubmission(operation, payload) {
|
|
325
|
+
const sourceTraceId = resolveSourceTraceId(payload);
|
|
326
|
+
if (sourceTraceId === void 0) {
|
|
572
327
|
return;
|
|
573
328
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
329
|
+
if (operation === "external_span") {
|
|
330
|
+
const rawSpan = asRecord(payload.rawSpan);
|
|
331
|
+
if (typeof rawSpan?.id !== "string") {
|
|
332
|
+
submissionCounter += 1;
|
|
333
|
+
}
|
|
334
|
+
const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
|
|
335
|
+
const existing = traceSubmissionSpanIds.get(sourceTraceId);
|
|
336
|
+
if (existing) {
|
|
337
|
+
existing.add(sourceSpanId);
|
|
338
|
+
} else {
|
|
339
|
+
traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (payload.completed !== true) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (typeof payload.testRunId === "string") {
|
|
347
|
+
replayTraceSubmissions.add(sourceTraceId);
|
|
348
|
+
if (!traceSubmissionSpanIds.has(sourceTraceId)) {
|
|
349
|
+
traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
traceSubmissionSpanIds.delete(sourceTraceId);
|
|
578
353
|
}
|
|
579
354
|
}
|
|
580
|
-
function
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (Array.isArray(deserialized)) {
|
|
586
|
-
return deserialized;
|
|
355
|
+
function takeReplaySpanCounts(traceIds) {
|
|
356
|
+
const counts = {};
|
|
357
|
+
for (const traceId of traceIds) {
|
|
358
|
+
if (!replayTraceSubmissions.has(traceId)) {
|
|
359
|
+
continue;
|
|
587
360
|
}
|
|
588
|
-
|
|
361
|
+
counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
|
|
362
|
+
traceSubmissionSpanIds.delete(traceId);
|
|
363
|
+
replayTraceSubmissions.delete(traceId);
|
|
589
364
|
}
|
|
590
|
-
|
|
591
|
-
|
|
365
|
+
return counts;
|
|
366
|
+
}
|
|
367
|
+
function asRecord(value) {
|
|
368
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
369
|
+
}
|
|
370
|
+
function resolveSourceTraceId(payload) {
|
|
371
|
+
if (typeof payload.sourceTraceId === "string") {
|
|
372
|
+
return payload.sourceTraceId;
|
|
592
373
|
}
|
|
593
|
-
|
|
374
|
+
const rawTrace = asRecord(payload.externalTrace) ?? asRecord(payload.rawTrace);
|
|
375
|
+
return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
|
|
594
376
|
}
|
|
595
|
-
function
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (outputMeta !== void 0 && outputMeta !== null) {
|
|
599
|
-
return deserializeValue({ json: rawOutput, meta: outputMeta });
|
|
377
|
+
function otlpValue(value) {
|
|
378
|
+
if (typeof value === "boolean") {
|
|
379
|
+
return { boolValue: value };
|
|
600
380
|
}
|
|
601
|
-
|
|
381
|
+
if (typeof value === "number") {
|
|
382
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
383
|
+
}
|
|
384
|
+
if (typeof value === "string") {
|
|
385
|
+
return { stringValue: value };
|
|
386
|
+
}
|
|
387
|
+
if (Array.isArray(value)) {
|
|
388
|
+
return { arrayValue: { values: value.map(otlpValue) } };
|
|
389
|
+
}
|
|
390
|
+
return { stringValue: String(value) };
|
|
602
391
|
}
|
|
603
|
-
function
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
function walk(node) {
|
|
607
|
-
const key = node.traceFunctionKey;
|
|
608
|
-
if (key) {
|
|
609
|
-
const name = node.spanName || key;
|
|
610
|
-
const counterKey = `${key}:${name}`;
|
|
611
|
-
const index = counters.get(counterKey) ?? 0;
|
|
612
|
-
counters.set(counterKey, index + 1);
|
|
613
|
-
spans.set(`${counterKey}:${index}`, {
|
|
614
|
-
sourceSpanId: node.sourceSpanId,
|
|
615
|
-
externalSpanId: node.externalSpanId,
|
|
616
|
-
output: node.output,
|
|
617
|
-
outputMeta: node.outputMeta
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
for (const child of node.children) {
|
|
621
|
-
walk(child);
|
|
622
|
-
}
|
|
392
|
+
function otlpAttributes(attributes) {
|
|
393
|
+
if (!attributes) {
|
|
394
|
+
return [];
|
|
623
395
|
}
|
|
624
|
-
|
|
625
|
-
|
|
396
|
+
return Object.entries(attributes).filter(([, value]) => value !== void 0).map(([key, value]) => ({ key, value: otlpValue(value) }));
|
|
397
|
+
}
|
|
398
|
+
function hrTimeToNanoString(time) {
|
|
399
|
+
if (!time) {
|
|
400
|
+
return "0";
|
|
626
401
|
}
|
|
627
|
-
return {
|
|
402
|
+
return `${time[0]}${String(time[1]).padStart(9, "0")}`;
|
|
628
403
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
);
|
|
655
|
-
}
|
|
656
|
-
const span = await httpClient.getExternalSpan(originalSpanId);
|
|
657
|
-
const spanData = span.rawData?.span_data ?? {};
|
|
658
|
-
inputs = deserializeInputs(spanData);
|
|
659
|
-
originalOutput = deserializeOutput(spanData);
|
|
660
|
-
if (adaptInputs) {
|
|
661
|
-
inputs = adaptInputs(inputs, {
|
|
662
|
-
originalTraceId,
|
|
663
|
-
originalSpanId,
|
|
664
|
-
// Deprecated aliases for originalTraceId/originalSpanId.
|
|
665
|
-
sourceTraceId: originalTraceId,
|
|
666
|
-
sourceSpanId: originalSpanId
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
const hasOverrides = resolvedOverrides.length > 0;
|
|
670
|
-
const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
|
|
671
|
-
const includeOutputs = mockStrategy === "all";
|
|
672
|
-
let mockTree;
|
|
673
|
-
if (needTree) {
|
|
674
|
-
try {
|
|
675
|
-
const treeResponse = await httpClient.getSpanTree(originalSpanId, {
|
|
676
|
-
includeOutputs
|
|
677
|
-
});
|
|
678
|
-
if (treeResponse.root) {
|
|
679
|
-
mockTree = buildMockTree(treeResponse.root);
|
|
680
|
-
} else if (mockStrategy === "all" || hasOverrides) {
|
|
681
|
-
throw new BitfabError(
|
|
682
|
-
`Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for original span ${originalSpanId}.`
|
|
683
|
-
);
|
|
684
|
-
} else {
|
|
685
|
-
mockTree = void 0;
|
|
686
|
-
}
|
|
687
|
-
} catch (e) {
|
|
688
|
-
if (mockStrategy === "all" || hasOverrides) {
|
|
689
|
-
throw e;
|
|
690
|
-
}
|
|
691
|
-
mockTree = void 0;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
const outputCache = /* @__PURE__ */ new Map();
|
|
695
|
-
const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
|
|
696
|
-
let pending = outputCache.get(externalSpanId);
|
|
697
|
-
if (!pending) {
|
|
698
|
-
pending = httpClient.getExternalSpan(externalSpanId).then(
|
|
699
|
-
(s) => deserializeOutput(
|
|
700
|
-
s.rawData?.span_data ?? {}
|
|
701
|
-
)
|
|
702
|
-
);
|
|
703
|
-
outputCache.set(externalSpanId, pending);
|
|
704
|
-
}
|
|
705
|
-
return pending;
|
|
706
|
-
} : void 0;
|
|
707
|
-
const maybePromise = runWithReplayContext(
|
|
708
|
-
{
|
|
709
|
-
testRunId,
|
|
710
|
-
traceId: replayedTraceId,
|
|
711
|
-
inputSourceSpanId: span.id,
|
|
712
|
-
inputSourceTraceId: span.externalTraceId,
|
|
713
|
-
sourceBitfabTraceId: originalTraceId,
|
|
714
|
-
mockTree,
|
|
715
|
-
callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
|
|
716
|
-
mockStrategy,
|
|
717
|
-
mockOverrides: hasOverrides ? resolvedOverrides : void 0,
|
|
718
|
-
fetchSpanOutput,
|
|
719
|
-
dbBranchLease: lease,
|
|
720
|
-
pendingPersistence
|
|
721
|
-
},
|
|
722
|
-
() => fn(...inputs)
|
|
723
|
-
);
|
|
724
|
-
result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
|
|
725
|
-
} catch (e) {
|
|
726
|
-
error = e instanceof Error ? e.message : String(e);
|
|
727
|
-
} finally {
|
|
728
|
-
await Promise.allSettled(pendingPersistence);
|
|
729
|
-
if (lease) {
|
|
730
|
-
try {
|
|
731
|
-
await httpClient.releaseDbBranchLease(lease.neonBranchId);
|
|
732
|
-
} catch (e) {
|
|
733
|
-
try {
|
|
734
|
-
console.warn(
|
|
735
|
-
`Bitfab: failed to release DB branch ${lease.neonBranchId} (TTL janitor will catch it): ${e instanceof Error ? e.message : String(e)}`
|
|
736
|
-
);
|
|
737
|
-
} catch {
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
404
|
+
function spanToOtlp(span) {
|
|
405
|
+
const spanContext = span.spanContext();
|
|
406
|
+
const result = {
|
|
407
|
+
traceId: spanContext.traceId,
|
|
408
|
+
spanId: spanContext.spanId,
|
|
409
|
+
name: span.name,
|
|
410
|
+
kind: span.kind + 1,
|
|
411
|
+
startTimeUnixNano: hrTimeToNanoString(span.startTime),
|
|
412
|
+
endTimeUnixNano: hrTimeToNanoString(span.endTime),
|
|
413
|
+
attributes: otlpAttributes(span.attributes),
|
|
414
|
+
droppedAttributesCount: span.droppedAttributesCount,
|
|
415
|
+
droppedEventsCount: span.droppedEventsCount,
|
|
416
|
+
droppedLinksCount: span.droppedLinksCount,
|
|
417
|
+
status: {
|
|
418
|
+
code: span.status.code,
|
|
419
|
+
...span.status.message ? { message: span.status.message } : {}
|
|
420
|
+
},
|
|
421
|
+
flags: spanContext.traceFlags
|
|
422
|
+
};
|
|
423
|
+
const parentSpanId = span.parentSpanContext?.spanId;
|
|
424
|
+
if (parentSpanId) {
|
|
425
|
+
result.parentSpanId = parentSpanId;
|
|
426
|
+
}
|
|
427
|
+
if (spanContext.traceState) {
|
|
428
|
+
result.traceState = spanContext.traceState.serialize();
|
|
741
429
|
}
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
function buildOtlpRequest(first, spans) {
|
|
433
|
+
const scope = first.instrumentationScope;
|
|
742
434
|
return {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
// replay traces are persisted and their spans aggregated server-side.
|
|
759
|
-
// Null here (and on older servers) means "replay tokens not known".
|
|
760
|
-
tokens: null,
|
|
761
|
-
model: serverItem.model ?? null,
|
|
762
|
-
dbSnapshotRef: dbSnapshotRef ?? null
|
|
435
|
+
resourceSpans: [
|
|
436
|
+
{
|
|
437
|
+
resource: {
|
|
438
|
+
attributes: otlpAttributes(
|
|
439
|
+
first.resource.attributes
|
|
440
|
+
)
|
|
441
|
+
},
|
|
442
|
+
scopeSpans: [
|
|
443
|
+
{
|
|
444
|
+
scope: { name: scope.name, version: scope.version ?? "" },
|
|
445
|
+
spans
|
|
446
|
+
}
|
|
447
|
+
]
|
|
448
|
+
}
|
|
449
|
+
]
|
|
763
450
|
};
|
|
764
451
|
}
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
452
|
+
function encodedSize(value) {
|
|
453
|
+
const json = JSON.stringify(value);
|
|
454
|
+
if (typeof TextEncoder !== "undefined") {
|
|
455
|
+
return new TextEncoder().encode(json).length;
|
|
456
|
+
}
|
|
457
|
+
return json.length;
|
|
458
|
+
}
|
|
459
|
+
function delay(ms) {
|
|
460
|
+
return new Promise((resolve) => {
|
|
461
|
+
const timer = setTimeout(resolve, ms);
|
|
462
|
+
unrefTimer(timer);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
async function withDeadline(work, timeoutMs) {
|
|
466
|
+
let timer;
|
|
467
|
+
try {
|
|
468
|
+
return await Promise.race([
|
|
469
|
+
work,
|
|
470
|
+
new Promise((resolve) => {
|
|
471
|
+
timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
|
|
472
|
+
unrefTimer(timer);
|
|
473
|
+
})
|
|
474
|
+
]);
|
|
475
|
+
} finally {
|
|
476
|
+
if (timer) {
|
|
477
|
+
clearTimeout(timer);
|
|
774
478
|
}
|
|
775
479
|
}
|
|
480
|
+
}
|
|
481
|
+
async function mapWithConcurrency(items, limit, task) {
|
|
482
|
+
const results = new Array(items.length);
|
|
483
|
+
let next = 0;
|
|
776
484
|
const workers = Array.from(
|
|
777
|
-
{ length: Math.min(
|
|
778
|
-
() =>
|
|
485
|
+
{ length: Math.min(Math.max(limit, 1), items.length) },
|
|
486
|
+
async () => {
|
|
487
|
+
while (next < items.length) {
|
|
488
|
+
const index = next;
|
|
489
|
+
next += 1;
|
|
490
|
+
results[index] = await task(items[index]);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
779
493
|
);
|
|
780
494
|
await Promise.all(workers);
|
|
781
495
|
return results;
|
|
782
496
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
`traceIds supports at most 100 trace IDs per replay (got ${options.traceIds.length}).`
|
|
791
|
-
);
|
|
792
|
-
}
|
|
497
|
+
function responseStatus(error) {
|
|
498
|
+
return error instanceof BitfabError ? error.status : void 0;
|
|
499
|
+
}
|
|
500
|
+
function isRetryable(error) {
|
|
501
|
+
const status = responseStatus(error);
|
|
502
|
+
if (status === void 0) {
|
|
503
|
+
return true;
|
|
793
504
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
505
|
+
return RETRYABLE_STATUSES.has(status) || status >= 500;
|
|
506
|
+
}
|
|
507
|
+
function normalizeCollectorEndpoint(endpoint) {
|
|
508
|
+
const trimmed = endpoint.replace(/\/+$/, "");
|
|
509
|
+
return trimmed.endsWith("/v1/traces") ? trimmed : `${trimmed}/v1/traces`;
|
|
510
|
+
}
|
|
511
|
+
function endSpan(span, endTime) {
|
|
512
|
+
span.end(endTime);
|
|
513
|
+
}
|
|
514
|
+
function spanName(operation, payload) {
|
|
515
|
+
if (operation === "external_span") {
|
|
516
|
+
const spanData = asRecord(asRecord(payload.rawSpan)?.span_data);
|
|
517
|
+
if (typeof spanData?.name === "string") {
|
|
518
|
+
return spanData.name;
|
|
800
519
|
}
|
|
801
520
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
let codeChangeFiles = options?.codeChangeFiles;
|
|
805
|
-
if (codeChangeFiles === void 0) {
|
|
806
|
-
const captured = await resolveAutoCodeChange(options?.name);
|
|
807
|
-
if (captured) {
|
|
808
|
-
codeChangeFiles = captured.files;
|
|
809
|
-
if (codeChangeDescription === void 0) {
|
|
810
|
-
codeChangeDescription = captured.description;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
521
|
+
if (typeof payload.traceFunctionKey === "string") {
|
|
522
|
+
return payload.traceFunctionKey;
|
|
813
523
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
replayedTraceIds[index],
|
|
850
|
-
dbBranchEnabled(options?.dbBranch),
|
|
851
|
-
resolveDbBranchSettings(options?.dbBranch),
|
|
852
|
-
options?.adaptInputs
|
|
524
|
+
return `bitfab.${operation}`;
|
|
525
|
+
}
|
|
526
|
+
function payloadTimestamp(payload, field) {
|
|
527
|
+
const rawSpan = asRecord(payload.rawSpan);
|
|
528
|
+
const rawTrace = asRecord(payload.externalTrace) ?? asRecord(payload.rawTrace);
|
|
529
|
+
const raw = rawSpan?.[field] ?? rawTrace?.[field];
|
|
530
|
+
if (typeof raw !== "string") {
|
|
531
|
+
return void 0;
|
|
532
|
+
}
|
|
533
|
+
const parsed = Date.parse(raw);
|
|
534
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
535
|
+
}
|
|
536
|
+
function hasError(payload) {
|
|
537
|
+
const spanData = asRecord(asRecord(payload.rawSpan)?.span_data);
|
|
538
|
+
if (spanData?.error != null) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
const errors = payload.errors;
|
|
542
|
+
return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
|
|
543
|
+
}
|
|
544
|
+
function createOtelTransport(options) {
|
|
545
|
+
return new OtelBatchTransport({
|
|
546
|
+
...options,
|
|
547
|
+
collectorEndpoint: readEnv(COLLECTOR_ENDPOINT_ENV) || void 0,
|
|
548
|
+
exportConcurrency: readBoundedIntEnv(
|
|
549
|
+
EXPORT_CONCURRENCY_ENV,
|
|
550
|
+
MAX_EXPORT_CONCURRENCY,
|
|
551
|
+
DEFAULT_EXPORT_CONCURRENCY,
|
|
552
|
+
"otel-export-concurrency-invalid"
|
|
553
|
+
),
|
|
554
|
+
maxRequestBytes: readBoundedIntEnv(
|
|
555
|
+
MAX_REQUEST_BYTES_ENV,
|
|
556
|
+
MAX_EXPORT_REQUEST_BYTES,
|
|
557
|
+
MAX_EXPORT_REQUEST_BYTES,
|
|
558
|
+
"otel-max-request-bytes-invalid"
|
|
853
559
|
)
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
async function forEachLiveTransport(timeoutMs, run) {
|
|
563
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
564
|
+
let succeeded = true;
|
|
565
|
+
for (const transport of [...liveTransports]) {
|
|
566
|
+
succeeded = await run(transport, Math.max(0, deadline - Date.now())) && succeeded;
|
|
567
|
+
}
|
|
568
|
+
return succeeded;
|
|
569
|
+
}
|
|
570
|
+
function flushOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
571
|
+
return forEachLiveTransport(
|
|
572
|
+
timeoutMs,
|
|
573
|
+
(transport, remaining) => transport.flush(remaining)
|
|
854
574
|
);
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
575
|
+
}
|
|
576
|
+
function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
577
|
+
return forEachLiveTransport(
|
|
578
|
+
timeoutMs,
|
|
579
|
+
(transport, remaining) => transport.shutdown(remaining)
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
var import_api, import_core, import_resources, import_sdk_trace_base, OPERATION_ATTRIBUTE, PAYLOAD_ATTRIBUTE, OTLP_TRACES_ENDPOINT, MAX_EXPORT_REQUEST_BYTES, MAX_REQUEST_BYTES_ENV, EXPORT_CONCURRENCY_ENV, COLLECTOR_ENDPOINT_ENV, MAX_QUEUE_SIZE, DIRECT_MAX_EXPORT_BATCH_SIZE, COLLECTOR_MAX_EXPORT_BATCH_SIZE, DIRECT_MAX_REQUEST_BATCH_SIZE, DEFAULT_EXPORT_CONCURRENCY, MAX_EXPORT_CONCURRENCY, SCHEDULE_DELAY_MILLIS, EXPORT_TIMEOUT_MILLIS, RETRY_DELAY_MILLIS, MAX_SEND_ATTEMPTS, DEFAULT_LIFECYCLE_TIMEOUT_MS, RETRYABLE_STATUSES, liveTransports, traceSubmissionSpanIds, replayTraceSubmissions, submissionCounter, OtlpPayloadTooLargeError, OtlpPartialSuccessError, BitfabSpanExporter, CollectorSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
|
|
583
|
+
var init_otel = __esm({
|
|
584
|
+
"src/otel.ts"() {
|
|
585
|
+
"use strict";
|
|
586
|
+
import_api = require("@opentelemetry/api");
|
|
587
|
+
import_core = require("@opentelemetry/core");
|
|
588
|
+
import_resources = require("@opentelemetry/resources");
|
|
589
|
+
import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
|
|
590
|
+
init_constants();
|
|
591
|
+
init_errors();
|
|
592
|
+
init_readEnv();
|
|
593
|
+
init_serializePayload();
|
|
594
|
+
init_unrefTimer();
|
|
595
|
+
init_warnOnce();
|
|
596
|
+
OPERATION_ATTRIBUTE = "bitfab.operation";
|
|
597
|
+
PAYLOAD_ATTRIBUTE = "bitfab.payload";
|
|
598
|
+
OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
|
|
599
|
+
MAX_EXPORT_REQUEST_BYTES = 3e6;
|
|
600
|
+
MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
|
|
601
|
+
EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
|
|
602
|
+
COLLECTOR_ENDPOINT_ENV = "BITFAB_OTEL_EXPORTER_ENDPOINT";
|
|
603
|
+
MAX_QUEUE_SIZE = 8192;
|
|
604
|
+
DIRECT_MAX_EXPORT_BATCH_SIZE = 512;
|
|
605
|
+
COLLECTOR_MAX_EXPORT_BATCH_SIZE = 32;
|
|
606
|
+
DIRECT_MAX_REQUEST_BATCH_SIZE = 8;
|
|
607
|
+
DEFAULT_EXPORT_CONCURRENCY = 32;
|
|
608
|
+
MAX_EXPORT_CONCURRENCY = 64;
|
|
609
|
+
SCHEDULE_DELAY_MILLIS = 5e3;
|
|
610
|
+
EXPORT_TIMEOUT_MILLIS = 3e4;
|
|
611
|
+
RETRY_DELAY_MILLIS = 100;
|
|
612
|
+
MAX_SEND_ATTEMPTS = 3;
|
|
613
|
+
DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
|
|
614
|
+
RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
615
|
+
liveTransports = /* @__PURE__ */ new Set();
|
|
616
|
+
traceSubmissionSpanIds = /* @__PURE__ */ new Map();
|
|
617
|
+
replayTraceSubmissions = /* @__PURE__ */ new Set();
|
|
618
|
+
submissionCounter = 0;
|
|
619
|
+
OtlpPayloadTooLargeError = class extends Error {
|
|
620
|
+
};
|
|
621
|
+
OtlpPartialSuccessError = class extends Error {
|
|
622
|
+
};
|
|
623
|
+
BitfabSpanExporter = class {
|
|
624
|
+
constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
|
|
625
|
+
this.directSender = directSender;
|
|
626
|
+
this.maxRequestBytes = maxRequestBytes;
|
|
627
|
+
this.maxRequestBatchSize = maxRequestBatchSize;
|
|
628
|
+
this.exportConcurrency = exportConcurrency;
|
|
868
629
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
630
|
+
export(spans, resultCallback) {
|
|
631
|
+
void this.exportAsync(spans).then(
|
|
632
|
+
(succeeded) => {
|
|
633
|
+
resultCallback({
|
|
634
|
+
code: succeeded ? import_core.ExportResultCode.SUCCESS : import_core.ExportResultCode.FAILED
|
|
635
|
+
});
|
|
636
|
+
},
|
|
637
|
+
(error) => {
|
|
638
|
+
resultCallback({ code: import_core.ExportResultCode.FAILED, error });
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
async exportAsync(spans) {
|
|
643
|
+
if (spans.length === 0) {
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
let encoded;
|
|
647
|
+
try {
|
|
648
|
+
encoded = spans.map(spanToOtlp);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
logError("failed to encode an OpenTelemetry span batch", error);
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
const first = spans[0];
|
|
654
|
+
const batches = this.buildRequestBatches(first, encoded);
|
|
655
|
+
const results = await mapWithConcurrency(
|
|
656
|
+
batches,
|
|
657
|
+
this.exportConcurrency,
|
|
658
|
+
(batch) => this.send(first, batch)
|
|
659
|
+
);
|
|
660
|
+
return results.every(Boolean);
|
|
661
|
+
}
|
|
662
|
+
buildRequestBatches(first, spans) {
|
|
663
|
+
const batches = [];
|
|
664
|
+
let current = [];
|
|
665
|
+
for (const span of spans) {
|
|
666
|
+
if (current.length >= this.maxRequestBatchSize) {
|
|
667
|
+
batches.push(current);
|
|
668
|
+
current = [];
|
|
669
|
+
}
|
|
670
|
+
const candidate = [...current, span];
|
|
671
|
+
if (current.length > 0 && encodedSize(buildOtlpRequest(first, candidate)) > this.maxRequestBytes) {
|
|
672
|
+
batches.push(current);
|
|
673
|
+
current = [span];
|
|
674
|
+
} else {
|
|
675
|
+
current = candidate;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (current.length > 0) {
|
|
679
|
+
batches.push(current);
|
|
680
|
+
}
|
|
681
|
+
return batches;
|
|
682
|
+
}
|
|
683
|
+
async send(first, spans) {
|
|
684
|
+
const payload = buildOtlpRequest(first, spans);
|
|
685
|
+
if (encodedSize(payload) > this.maxRequestBytes) {
|
|
686
|
+
logError(
|
|
687
|
+
"a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
|
|
688
|
+
);
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
await this.sendWithRetries(payload);
|
|
693
|
+
return true;
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (error instanceof OtlpPayloadTooLargeError) {
|
|
696
|
+
logError(
|
|
697
|
+
spans.length === 1 ? "a single OpenTelemetry span exceeded the ingestion request limit and could not be exported" : "an OpenTelemetry span batch exceeded the ingestion request limit and could not be exported"
|
|
698
|
+
);
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
if (error instanceof OtlpPartialSuccessError) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
logError("failed to export an OpenTelemetry span batch", error);
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Retries transient failures. Span and trace-completion carriers are safe to
|
|
710
|
+
* retry: the server keys them idempotently on `sourceSpanId`/`sourceTraceId`,
|
|
711
|
+
* so a duplicate delivery cannot create a duplicate row.
|
|
712
|
+
*
|
|
713
|
+
* KNOWN LIMITATION: an `internal_trace` (a `call()` BAML trace) carries no
|
|
714
|
+
* such key, so retrying a batch that holds one can create a duplicate trace -
|
|
715
|
+
* including when a request times out client-side but the server goes on to
|
|
716
|
+
* persist it. Accepted deliberately for now, matching the other SDKs, rather
|
|
717
|
+
* than skipping retries for a whole batch or inventing an idempotency scheme
|
|
718
|
+
* the server does not yet understand. The fix is a client-supplied
|
|
719
|
+
* idempotency key that ingestion dedupes on.
|
|
720
|
+
*/
|
|
721
|
+
async sendWithRetries(payload) {
|
|
722
|
+
for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
|
|
723
|
+
try {
|
|
724
|
+
const response = await this.directSender(
|
|
725
|
+
OTLP_TRACES_ENDPOINT,
|
|
726
|
+
payload,
|
|
727
|
+
EXPORT_TIMEOUT_MILLIS
|
|
728
|
+
);
|
|
729
|
+
const partialSuccess = asRecord(response?.partialSuccess);
|
|
730
|
+
const rejected = partialSuccess?.rejectedSpans;
|
|
731
|
+
if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
|
|
732
|
+
logError(
|
|
733
|
+
`OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
|
|
734
|
+
);
|
|
735
|
+
throw new OtlpPartialSuccessError();
|
|
736
|
+
}
|
|
737
|
+
return;
|
|
738
|
+
} catch (error) {
|
|
739
|
+
if (error instanceof OtlpPartialSuccessError) {
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
if (responseStatus(error) === 413) {
|
|
743
|
+
throw new OtlpPayloadTooLargeError();
|
|
744
|
+
}
|
|
745
|
+
if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
await delay(RETRY_DELAY_MILLIS);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async shutdown() {
|
|
753
|
+
}
|
|
754
|
+
async forceFlush() {
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
CollectorSpanExporter = class {
|
|
758
|
+
constructor(endpoint, apiKey, maxRequestBytes) {
|
|
759
|
+
this.endpoint = endpoint;
|
|
760
|
+
this.apiKey = apiKey;
|
|
761
|
+
this.maxRequestBytes = maxRequestBytes;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Loaded through a dynamic import rather than a top-level one so bundlers
|
|
765
|
+
* code-split it: Collector delivery is opt-in, and a consumer who never sets
|
|
766
|
+
* an endpoint should not pay for the exporter in their initial bundle. It is
|
|
767
|
+
* a hard dependency, so this cannot fail for want of the package.
|
|
768
|
+
*/
|
|
769
|
+
loadExporterModule() {
|
|
770
|
+
if (!this.pendingModule) {
|
|
771
|
+
this.pendingModule = import("@opentelemetry/exporter-trace-otlp-proto");
|
|
772
|
+
}
|
|
773
|
+
return this.pendingModule;
|
|
774
|
+
}
|
|
775
|
+
export(spans, resultCallback) {
|
|
776
|
+
void this.exportAsync(spans).then(
|
|
777
|
+
(succeeded) => {
|
|
778
|
+
resultCallback({
|
|
779
|
+
code: succeeded ? import_core.ExportResultCode.SUCCESS : import_core.ExportResultCode.FAILED
|
|
780
|
+
});
|
|
781
|
+
},
|
|
782
|
+
(error) => {
|
|
783
|
+
resultCallback({ code: import_core.ExportResultCode.FAILED, error });
|
|
784
|
+
}
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
async exportAsync(spans) {
|
|
788
|
+
if (spans.length === 0) {
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
let delegate;
|
|
792
|
+
try {
|
|
793
|
+
delegate = await this.resolveDelegate();
|
|
794
|
+
} catch (error) {
|
|
795
|
+
logError("failed to build the OTLP Collector exporter", error);
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
const results = await Promise.all(
|
|
799
|
+
this.partition(spans).map(
|
|
800
|
+
(batch) => new Promise((resolve) => {
|
|
801
|
+
try {
|
|
802
|
+
delegate.export(batch, (result) => {
|
|
803
|
+
resolve(result.code === import_core.ExportResultCode.SUCCESS);
|
|
804
|
+
});
|
|
805
|
+
} catch (error) {
|
|
806
|
+
logError("Collector export threw", error);
|
|
807
|
+
resolve(false);
|
|
808
|
+
}
|
|
809
|
+
})
|
|
810
|
+
)
|
|
811
|
+
);
|
|
812
|
+
return results.every(Boolean);
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Partition by the encoded JSON size of each carrier rather than its encoded
|
|
816
|
+
* protobuf size. Protobuf is strictly smaller than the equivalent JSON for
|
|
817
|
+
* these payloads, so the JSON figure is a conservative bound that keeps every
|
|
818
|
+
* request under the target without pulling `@opentelemetry/otlp-transformer`
|
|
819
|
+
* into the dependency set purely to measure bytes.
|
|
820
|
+
*/
|
|
821
|
+
partition(spans) {
|
|
822
|
+
const batches = [];
|
|
823
|
+
let current = [];
|
|
824
|
+
let currentSize = 0;
|
|
825
|
+
for (const span of spans) {
|
|
826
|
+
const size = encodedSize(spanToOtlp(span));
|
|
827
|
+
if (current.length > 0 && currentSize + size > this.maxRequestBytes) {
|
|
828
|
+
batches.push(current);
|
|
829
|
+
current = [];
|
|
830
|
+
currentSize = 0;
|
|
896
831
|
}
|
|
832
|
+
current.push(span);
|
|
833
|
+
currentSize += size;
|
|
834
|
+
}
|
|
835
|
+
if (current.length > 0) {
|
|
836
|
+
batches.push(current);
|
|
837
|
+
}
|
|
838
|
+
return batches;
|
|
839
|
+
}
|
|
840
|
+
async resolveDelegate() {
|
|
841
|
+
const apiKey = this.apiKey() ?? "";
|
|
842
|
+
if (this.delegate && this.delegateApiKey === apiKey) {
|
|
843
|
+
return this.delegate;
|
|
844
|
+
}
|
|
845
|
+
const { OTLPTraceExporter } = await this.loadExporterModule();
|
|
846
|
+
const previous = this.delegate;
|
|
847
|
+
this.delegate = new OTLPTraceExporter({
|
|
848
|
+
url: this.endpoint,
|
|
849
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
850
|
+
timeoutMillis: EXPORT_TIMEOUT_MILLIS
|
|
897
851
|
});
|
|
898
|
-
|
|
852
|
+
this.delegateApiKey = apiKey;
|
|
853
|
+
if (previous) {
|
|
854
|
+
void previous.shutdown().catch(() => {
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
return this.delegate;
|
|
899
858
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
859
|
+
async shutdown() {
|
|
860
|
+
await this.delegate?.shutdown();
|
|
861
|
+
}
|
|
862
|
+
async forceFlush() {
|
|
863
|
+
await this.delegate?.forceFlush?.();
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
DeliveryTrackingExporter = class {
|
|
867
|
+
constructor(exporter) {
|
|
868
|
+
this.exporter = exporter;
|
|
869
|
+
// Deliberately unscoped, matching the Python SDK. An export can outlive
|
|
870
|
+
// OTel's export timeout and report failure after the flush that was waiting
|
|
871
|
+
// on it already returned, so that failure surfaces on the NEXT flush instead.
|
|
872
|
+
// That over-reports: a good flush can inherit an older failure. The
|
|
873
|
+
// alternative - discarding failures from completed flush windows - under-
|
|
874
|
+
// reports, and `BatchSpanProcessor` also runs scheduled exports that belong
|
|
875
|
+
// to no flush at all, so their failures would vanish entirely. For a
|
|
876
|
+
// telemetry SDK a false "flush failed" is investigable; a false "flush
|
|
877
|
+
// succeeded" silently loses traces. We take the noisy direction on purpose.
|
|
878
|
+
this.failedExports = 0;
|
|
879
|
+
}
|
|
880
|
+
export(spans, resultCallback) {
|
|
881
|
+
try {
|
|
882
|
+
this.exporter.export(spans, (result) => {
|
|
883
|
+
if (result.code !== import_core.ExportResultCode.SUCCESS) {
|
|
884
|
+
this.failedExports += 1;
|
|
885
|
+
}
|
|
886
|
+
resultCallback(result);
|
|
887
|
+
});
|
|
888
|
+
} catch (error) {
|
|
889
|
+
this.failedExports += 1;
|
|
890
|
+
resultCallback({ code: import_core.ExportResultCode.FAILED, error });
|
|
917
891
|
}
|
|
918
892
|
}
|
|
919
|
-
|
|
920
|
-
|
|
893
|
+
takeFailedExports() {
|
|
894
|
+
const failed = this.failedExports;
|
|
895
|
+
this.failedExports = 0;
|
|
896
|
+
return failed;
|
|
921
897
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
898
|
+
shutdown() {
|
|
899
|
+
return this.exporter.shutdown();
|
|
900
|
+
}
|
|
901
|
+
forceFlush() {
|
|
902
|
+
return this.exporter.forceFlush?.() ?? Promise.resolve();
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
OtelBatchTransport = class {
|
|
906
|
+
constructor(options) {
|
|
907
|
+
this.closed = false;
|
|
908
|
+
const collectorEndpoint = options.collectorEndpoint;
|
|
909
|
+
const maxRequestBytes = options.maxRequestBytes ?? MAX_EXPORT_REQUEST_BYTES;
|
|
910
|
+
const maxRequestBatchSize = options.maxRequestBatchSize ?? DIRECT_MAX_REQUEST_BATCH_SIZE;
|
|
911
|
+
if (maxRequestBatchSize <= 0) {
|
|
912
|
+
throw new BitfabError("maxRequestBatchSize must be a positive integer");
|
|
913
|
+
}
|
|
914
|
+
this.deliveryTracker = new DeliveryTrackingExporter(
|
|
915
|
+
collectorEndpoint === void 0 ? new BitfabSpanExporter(
|
|
916
|
+
options.directSender,
|
|
917
|
+
maxRequestBytes,
|
|
918
|
+
maxRequestBatchSize,
|
|
919
|
+
options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
|
|
920
|
+
) : new CollectorSpanExporter(
|
|
921
|
+
normalizeCollectorEndpoint(collectorEndpoint),
|
|
922
|
+
options.apiKey,
|
|
923
|
+
maxRequestBytes
|
|
924
|
+
)
|
|
933
925
|
);
|
|
934
|
-
|
|
926
|
+
this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
|
|
927
|
+
maxQueueSize: options.maxQueueSize ?? MAX_QUEUE_SIZE,
|
|
928
|
+
maxExportBatchSize: options.maxExportBatchSize ?? (collectorEndpoint === void 0 ? DIRECT_MAX_EXPORT_BATCH_SIZE : COLLECTOR_MAX_EXPORT_BATCH_SIZE),
|
|
929
|
+
scheduledDelayMillis: SCHEDULE_DELAY_MILLIS,
|
|
930
|
+
exportTimeoutMillis: options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
|
|
931
|
+
});
|
|
932
|
+
this.provider = new import_sdk_trace_base.BasicTracerProvider({
|
|
933
|
+
sampler: new import_sdk_trace_base.AlwaysOnSampler(),
|
|
934
|
+
resource: (0, import_resources.resourceFromAttributes)({
|
|
935
|
+
"service.name": "bitfab-typescript-sdk",
|
|
936
|
+
"service.version": __version__
|
|
937
|
+
}),
|
|
938
|
+
spanLimits: {
|
|
939
|
+
attributeCountLimit: 2,
|
|
940
|
+
attributeValueLengthLimit: Number.POSITIVE_INFINITY
|
|
941
|
+
},
|
|
942
|
+
spanProcessors: [this.processor]
|
|
943
|
+
});
|
|
944
|
+
this.tracer = this.provider.getTracer("bitfab", __version__);
|
|
945
|
+
liveTransports.add(this);
|
|
935
946
|
}
|
|
936
|
-
|
|
947
|
+
submit(operation, payload) {
|
|
948
|
+
recordTraceSubmission(operation, payload);
|
|
949
|
+
if (this.closed) {
|
|
950
|
+
warnOnce(
|
|
951
|
+
"otel-submit-after-shutdown",
|
|
952
|
+
"OpenTelemetry transport is shut down; dropping spans"
|
|
953
|
+
);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
const { body, dropped } = serializePayloadBody(payload);
|
|
958
|
+
if (dropped.length > 0) {
|
|
959
|
+
warnOnce(
|
|
960
|
+
"otel-carrier-payload-stubbed",
|
|
961
|
+
`a span payload held non-serializable value(s) (${[
|
|
962
|
+
...new Set(dropped)
|
|
963
|
+
].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
const span = this.tracer.startSpan(spanName(operation, payload), {
|
|
967
|
+
attributes: {
|
|
968
|
+
[OPERATION_ATTRIBUTE]: operation,
|
|
969
|
+
[PAYLOAD_ATTRIBUTE]: body
|
|
970
|
+
},
|
|
971
|
+
startTime: payloadTimestamp(payload, "started_at")
|
|
972
|
+
});
|
|
973
|
+
if (hasError(payload)) {
|
|
974
|
+
span.setStatus({ code: import_api.SpanStatusCode.ERROR });
|
|
975
|
+
}
|
|
976
|
+
endSpan(span, payloadTimestamp(payload, "ended_at"));
|
|
977
|
+
} catch (error) {
|
|
978
|
+
logError("failed to queue an OpenTelemetry span", error);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
async flush(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
982
|
+
const pending = (this.pendingFlush ?? Promise.resolve(true)).then(
|
|
983
|
+
() => this.forceFlushOnce()
|
|
984
|
+
);
|
|
985
|
+
this.pendingFlush = pending.catch(() => false);
|
|
986
|
+
return withDeadline(pending, timeoutMs);
|
|
987
|
+
}
|
|
988
|
+
async forceFlushOnce() {
|
|
989
|
+
try {
|
|
990
|
+
await this.processor.forceFlush();
|
|
991
|
+
} catch (error) {
|
|
992
|
+
logError("failed to flush OpenTelemetry spans", error);
|
|
993
|
+
this.deliveryTracker.takeFailedExports();
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
return this.deliveryTracker.takeFailedExports() === 0;
|
|
997
|
+
}
|
|
998
|
+
async shutdown(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
|
|
999
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1000
|
+
this.closed = true;
|
|
1001
|
+
const flushed = await this.flush(Math.max(0, deadline - Date.now()));
|
|
1002
|
+
liveTransports.delete(this);
|
|
1003
|
+
const shutdownCompleted = await withDeadline(
|
|
1004
|
+
this.provider.shutdown().then(() => true).catch((error) => {
|
|
1005
|
+
logError("failed to shut down the OpenTelemetry transport", error);
|
|
1006
|
+
return false;
|
|
1007
|
+
}),
|
|
1008
|
+
Math.max(0, deadline - Date.now())
|
|
1009
|
+
);
|
|
1010
|
+
return flushed && shutdownCompleted;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
937
1013
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// src/transport.ts
|
|
1017
|
+
function createTraceTransport(options) {
|
|
1018
|
+
return createOtelTransport(options);
|
|
1019
|
+
}
|
|
1020
|
+
function flushTraceTransports(timeoutMs) {
|
|
1021
|
+
return flushOtelTransports(timeoutMs);
|
|
1022
|
+
}
|
|
1023
|
+
function shutdownTraceTransports(timeoutMs) {
|
|
1024
|
+
return shutdownOtelTransports(timeoutMs);
|
|
1025
|
+
}
|
|
1026
|
+
function takeReplaySpanCounts2(traceIds) {
|
|
1027
|
+
return takeReplaySpanCounts(traceIds);
|
|
1028
|
+
}
|
|
1029
|
+
var init_transport = __esm({
|
|
1030
|
+
"src/transport.ts"() {
|
|
1031
|
+
"use strict";
|
|
1032
|
+
init_otel();
|
|
955
1033
|
}
|
|
956
|
-
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
// src/http.ts
|
|
1037
|
+
function awaitOnExit(promise) {
|
|
1038
|
+
pendingTracePromises.add(promise);
|
|
1039
|
+
void promise.finally(() => {
|
|
1040
|
+
pendingTracePromises.delete(promise);
|
|
1041
|
+
}).catch(() => {
|
|
1042
|
+
});
|
|
1043
|
+
return promise;
|
|
957
1044
|
}
|
|
958
|
-
async function
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
1045
|
+
async function flushTraces(timeoutMs = 5e3) {
|
|
1046
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1047
|
+
const requestsFlushed = await awaitPendingRequests(timeoutMs);
|
|
1048
|
+
const transportsFlushed = await flushTraceTransports(
|
|
1049
|
+
Math.max(0, deadline - Date.now())
|
|
1050
|
+
);
|
|
1051
|
+
return requestsFlushed && transportsFlushed;
|
|
1052
|
+
}
|
|
1053
|
+
async function awaitPendingRequests(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1054
|
+
await replayContextReady.catch(() => {
|
|
1055
|
+
});
|
|
1056
|
+
return waitForPromises(Array.from(pendingTracePromises), timeoutMs);
|
|
1057
|
+
}
|
|
1058
|
+
async function waitForPromises(promises, timeoutMs) {
|
|
1059
|
+
if (promises.length === 0) {
|
|
1060
|
+
return true;
|
|
962
1061
|
}
|
|
1062
|
+
let timer;
|
|
963
1063
|
try {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1064
|
+
return await Promise.race([
|
|
1065
|
+
Promise.allSettled(promises).then(() => true),
|
|
1066
|
+
new Promise((resolve) => {
|
|
1067
|
+
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
1068
|
+
unrefTimer(timer);
|
|
1069
|
+
})
|
|
967
1070
|
]);
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
} catch (err) {
|
|
972
|
-
try {
|
|
973
|
-
console.warn(
|
|
974
|
-
`Bitfab: failed to write replay result to BITFAB_REPLAY_RESULT_PATH (${resultPath}): ${err instanceof Error ? err.message : String(err)}`
|
|
975
|
-
);
|
|
976
|
-
} catch {
|
|
1071
|
+
} finally {
|
|
1072
|
+
if (timer) {
|
|
1073
|
+
clearTimeout(timer);
|
|
977
1074
|
}
|
|
978
1075
|
}
|
|
979
1076
|
}
|
|
980
|
-
var
|
|
981
|
-
var
|
|
982
|
-
"src/
|
|
1077
|
+
var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, HttpClient;
|
|
1078
|
+
var init_http = __esm({
|
|
1079
|
+
"src/http.ts"() {
|
|
983
1080
|
"use strict";
|
|
984
|
-
|
|
1081
|
+
init_constants();
|
|
985
1082
|
init_errors();
|
|
986
|
-
init_mockOverride();
|
|
987
|
-
init_randomUuid();
|
|
988
1083
|
init_replayContext();
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
// src/http.ts
|
|
1026
|
-
init_errors();
|
|
1027
|
-
|
|
1028
|
-
// src/unrefTimer.ts
|
|
1029
|
-
function unrefTimer(timer) {
|
|
1030
|
-
const handle = timer;
|
|
1031
|
-
if (typeof handle.unref === "function") {
|
|
1032
|
-
handle.unref();
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// src/http.ts
|
|
1037
|
-
init_warnOnce();
|
|
1038
|
-
var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
|
|
1039
|
-
function serializePayloadBody(payload) {
|
|
1040
|
-
try {
|
|
1041
|
-
return { body: JSON.stringify(payload), dropped: [] };
|
|
1042
|
-
} catch {
|
|
1043
|
-
const dropped = [];
|
|
1044
|
-
const sanitize = (value, seen) => {
|
|
1045
|
-
const t = typeof value;
|
|
1046
|
-
if (value === null || t === "string" || t === "number" || t === "boolean") {
|
|
1047
|
-
return value;
|
|
1084
|
+
init_serializePayload();
|
|
1085
|
+
init_transport();
|
|
1086
|
+
init_unrefTimer();
|
|
1087
|
+
init_warnOnce();
|
|
1088
|
+
REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
|
|
1089
|
+
EXIT_FLUSH_TIMEOUT_MS = 5e3;
|
|
1090
|
+
DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
|
|
1091
|
+
pendingTracePromises = /* @__PURE__ */ new Set();
|
|
1092
|
+
if (typeof process !== "undefined" && process.versions != null && process.versions.node != null) {
|
|
1093
|
+
let isFlushing = false;
|
|
1094
|
+
process.on("beforeExit", () => {
|
|
1095
|
+
if (isFlushing) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
isFlushing = true;
|
|
1099
|
+
void Promise.allSettled([
|
|
1100
|
+
...Array.from(pendingTracePromises).map((p) => p.catch(() => {
|
|
1101
|
+
})),
|
|
1102
|
+
shutdownTraceTransports(EXIT_FLUSH_TIMEOUT_MS).catch(() => false)
|
|
1103
|
+
]).then(() => {
|
|
1104
|
+
isFlushing = false;
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
HttpClient = class {
|
|
1109
|
+
constructor(config) {
|
|
1110
|
+
// Deferred span work owned by THIS client. The module-global set backs the
|
|
1111
|
+
// process-wide `flushTraces()` and the exit hook, but per-client lifecycle
|
|
1112
|
+
// must not wait on another client's slow finalize: a false `close()` failure
|
|
1113
|
+
// caused by unrelated work is worse than no signal at all.
|
|
1114
|
+
this.deferredWork = /* @__PURE__ */ new Set();
|
|
1115
|
+
this.closed = false;
|
|
1116
|
+
this.apiKey = config.apiKey;
|
|
1117
|
+
this.serviceUrl = config.serviceUrl;
|
|
1118
|
+
this.timeout = config.timeout ?? 12e4;
|
|
1048
1119
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1120
|
+
/**
|
|
1121
|
+
* Resolve the API key at the moment it is needed (request time), invoking
|
|
1122
|
+
* the function form if one was supplied. Never read at construction.
|
|
1123
|
+
*/
|
|
1124
|
+
resolveApiKey() {
|
|
1125
|
+
return typeof this.apiKey === "function" ? this.apiKey() : this.apiKey;
|
|
1052
1126
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1127
|
+
/**
|
|
1128
|
+
* This client's span transport, built on first use.
|
|
1129
|
+
*
|
|
1130
|
+
* Lazy on purpose: a client that never sends a span must never start a batch
|
|
1131
|
+
* worker. Every framework integration created from a `Bitfab` client shares
|
|
1132
|
+
* the owning client's `HttpClient`, so handlers reuse this one worker instead
|
|
1133
|
+
* of each spinning up their own.
|
|
1134
|
+
*/
|
|
1135
|
+
getTraceTransport() {
|
|
1136
|
+
if (this.closed) {
|
|
1137
|
+
warnOnce(
|
|
1138
|
+
"http-client-closed",
|
|
1139
|
+
"the Bitfab client is closed; dropping spans"
|
|
1140
|
+
);
|
|
1141
|
+
return void 0;
|
|
1142
|
+
}
|
|
1143
|
+
if (!this.traceTransport) {
|
|
1144
|
+
this.traceTransport = createTraceTransport({
|
|
1145
|
+
apiKey: () => this.resolveApiKey(),
|
|
1146
|
+
directSender: (endpoint, payload, timeoutMs) => this.request(endpoint, payload, {
|
|
1147
|
+
timeout: timeoutMs
|
|
1148
|
+
})
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
return this.traceTransport;
|
|
1057
1152
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1153
|
+
/**
|
|
1154
|
+
* Track deferred span work so this client's own lifecycle waits for it, and
|
|
1155
|
+
* so the process-wide flush and exit hook do too.
|
|
1156
|
+
*/
|
|
1157
|
+
trackDeferred(promise) {
|
|
1158
|
+
this.deferredWork.add(promise);
|
|
1159
|
+
void promise.finally(() => this.deferredWork.delete(promise)).catch(() => {
|
|
1160
|
+
});
|
|
1161
|
+
return awaitOnExit(promise);
|
|
1061
1162
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1163
|
+
/**
|
|
1164
|
+
* Settle only THIS client's deferred span work. Scoped deliberately: the
|
|
1165
|
+
* global set can contain another client's long-running finalize, and
|
|
1166
|
+
* attributing its timeout here would fail a client whose own work succeeded.
|
|
1167
|
+
*/
|
|
1168
|
+
async settleDeferredWork(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1169
|
+
await replayContextReady.catch(() => {
|
|
1170
|
+
});
|
|
1171
|
+
return waitForPromises(Array.from(this.deferredWork), timeoutMs);
|
|
1064
1172
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1173
|
+
/**
|
|
1174
|
+
* Wait for spans queued by this client to be delivered, within one deadline.
|
|
1175
|
+
* Returns false on delivery failure or timeout.
|
|
1176
|
+
*/
|
|
1177
|
+
async waitForPendingRequests(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1178
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1179
|
+
const settled = await this.settleDeferredWork(timeoutMs);
|
|
1180
|
+
const flushed = await this.traceTransport?.flush(Math.max(0, deadline - Date.now())) ?? true;
|
|
1181
|
+
return settled && flushed;
|
|
1070
1182
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1183
|
+
/**
|
|
1184
|
+
* Flush and permanently close this client's tracing transport. Idempotent:
|
|
1185
|
+
* a second call joins the first rather than tearing down a pipeline the
|
|
1186
|
+
* first call already owns.
|
|
1187
|
+
*/
|
|
1188
|
+
close(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS2) {
|
|
1189
|
+
if (this.closing) {
|
|
1190
|
+
return this.closing;
|
|
1191
|
+
}
|
|
1192
|
+
const deadline = Date.now() + Math.max(timeoutMs, 0);
|
|
1193
|
+
this.closing = (async () => {
|
|
1194
|
+
const settled = await this.settleDeferredWork(
|
|
1195
|
+
Math.max(0, deadline - Date.now())
|
|
1196
|
+
);
|
|
1197
|
+
this.closed = true;
|
|
1198
|
+
const transport = this.traceTransport;
|
|
1199
|
+
this.traceTransport = void 0;
|
|
1200
|
+
const shutdownOk = await transport?.shutdown(Math.max(0, deadline - Date.now())) ?? true;
|
|
1201
|
+
return settled && shutdownOk;
|
|
1202
|
+
})();
|
|
1203
|
+
return this.closing;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Make an HTTP request to the Bitfab API. Defaults to POST; pass
|
|
1207
|
+
* `options.method` to use a different verb (e.g. "PATCH").
|
|
1208
|
+
*
|
|
1209
|
+
* @param endpoint - The API endpoint (without base URL)
|
|
1210
|
+
* @param payload - The request body
|
|
1211
|
+
* @param options - Optional request options
|
|
1212
|
+
* @returns The parsed JSON response
|
|
1213
|
+
* @throws {BitfabError} If the request fails
|
|
1214
|
+
*/
|
|
1215
|
+
async request(endpoint, payload, options) {
|
|
1216
|
+
const url = `${this.serviceUrl}${endpoint}`;
|
|
1217
|
+
const timeout = options?.timeout ?? this.timeout;
|
|
1218
|
+
const method = options?.method ?? "POST";
|
|
1219
|
+
const controller = new AbortController();
|
|
1220
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1221
|
+
const { body, dropped } = serializePayloadBody(payload);
|
|
1222
|
+
if (dropped.length > 0) {
|
|
1223
|
+
try {
|
|
1224
|
+
console.warn(
|
|
1225
|
+
`Bitfab: request body to ${endpoint} held ${dropped.length} non-serializable value(s) (${[...new Set(dropped)].join(", ")}); they were stubbed so the span still sends, but the trace may be incomplete or not replayable. Capture a JSON-safe projection of this input to make it replayable.`
|
|
1226
|
+
);
|
|
1227
|
+
} catch {
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1076
1230
|
try {
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1231
|
+
const response = await fetch(url, {
|
|
1232
|
+
method,
|
|
1233
|
+
headers: {
|
|
1234
|
+
"Content-Type": "application/json",
|
|
1235
|
+
Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
|
|
1236
|
+
},
|
|
1237
|
+
body,
|
|
1238
|
+
signal: controller.signal
|
|
1239
|
+
});
|
|
1240
|
+
if (!response.ok) {
|
|
1241
|
+
const errorText = await response.text();
|
|
1242
|
+
throw new BitfabError(
|
|
1243
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`,
|
|
1244
|
+
void 0,
|
|
1245
|
+
response.status
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
const result = await response.json();
|
|
1249
|
+
if (result.error) {
|
|
1250
|
+
if (result.url) {
|
|
1251
|
+
throw new BitfabError(
|
|
1252
|
+
`${result.error} Configure it at: ${this.serviceUrl}${result.url}`,
|
|
1253
|
+
result.url
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
throw new BitfabError(result.error);
|
|
1257
|
+
}
|
|
1258
|
+
return result;
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
if (error instanceof BitfabError) {
|
|
1261
|
+
throw error;
|
|
1262
|
+
}
|
|
1263
|
+
if (error instanceof Error) {
|
|
1264
|
+
if (error.name === "AbortError") {
|
|
1265
|
+
throw new BitfabError(`Request timed out after ${timeout}ms`);
|
|
1266
|
+
}
|
|
1267
|
+
throw new BitfabError(error.message);
|
|
1268
|
+
}
|
|
1269
|
+
throw new BitfabError("Unknown error occurred");
|
|
1270
|
+
} finally {
|
|
1271
|
+
clearTimeout(timeoutId);
|
|
1081
1272
|
}
|
|
1082
|
-
}
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Look up a function by name.
|
|
1276
|
+
* Blocks until complete - needed for function execution.
|
|
1277
|
+
*/
|
|
1278
|
+
async lookupFunction(name) {
|
|
1279
|
+
return this.request("/api/sdk/functions/lookup", { name });
|
|
1280
|
+
}
|
|
1281
|
+
async getTraceSpan(traceId, lookup) {
|
|
1282
|
+
const searchParams = new URLSearchParams();
|
|
1283
|
+
if (lookup.id !== void 0) {
|
|
1284
|
+
searchParams.set("id", lookup.id);
|
|
1285
|
+
} else {
|
|
1286
|
+
searchParams.set("name", lookup.name);
|
|
1287
|
+
searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
|
|
1288
|
+
}
|
|
1289
|
+
const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
|
|
1290
|
+
const response = await this.get(endpoint);
|
|
1291
|
+
return response.span;
|
|
1292
|
+
}
|
|
1293
|
+
async get(endpoint) {
|
|
1294
|
+
const url = `${this.serviceUrl}${endpoint}`;
|
|
1295
|
+
const controller = new AbortController();
|
|
1296
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1297
|
+
try {
|
|
1298
|
+
const response = await fetch(url, {
|
|
1299
|
+
method: "GET",
|
|
1300
|
+
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1301
|
+
signal: controller.signal
|
|
1302
|
+
});
|
|
1303
|
+
if (!response.ok) {
|
|
1304
|
+
const errorText = await response.text();
|
|
1305
|
+
throw new BitfabError(
|
|
1306
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
return await response.json();
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
if (error instanceof BitfabError) {
|
|
1312
|
+
throw error;
|
|
1313
|
+
}
|
|
1314
|
+
if (error instanceof Error) {
|
|
1315
|
+
if (error.name === "AbortError") {
|
|
1316
|
+
throw new BitfabError(`Request timed out after ${this.timeout}ms`);
|
|
1317
|
+
}
|
|
1318
|
+
throw new BitfabError(error.message);
|
|
1319
|
+
}
|
|
1320
|
+
throw new BitfabError("Unknown error occurred");
|
|
1321
|
+
} finally {
|
|
1322
|
+
clearTimeout(timeoutId);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Queue an internal trace (from local BAML execution via `call()`) onto this
|
|
1327
|
+
* client's batching transport. `functionId` moves into the payload because
|
|
1328
|
+
* the OTLP carrier has no path to carry it.
|
|
1329
|
+
*/
|
|
1330
|
+
sendInternalTrace(functionId, payload) {
|
|
1331
|
+
this.getTraceTransport()?.submit("internal_trace", {
|
|
1332
|
+
...payload,
|
|
1333
|
+
functionId,
|
|
1334
|
+
sdkVersion: __version__
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
|
|
1339
|
+
* client's batching transport. Fire-and-forget: the transport owns delivery,
|
|
1340
|
+
* so callers await `flushTraces()` or `close()` rather than a per-span
|
|
1341
|
+
* promise.
|
|
1342
|
+
*/
|
|
1343
|
+
sendExternalSpan(payload) {
|
|
1344
|
+
this.getTraceTransport()?.submit("external_span", {
|
|
1345
|
+
...payload,
|
|
1346
|
+
sdkVersion: __version__
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Queue an external trace completion (from OpenAI tracing) onto this
|
|
1351
|
+
* client's batching transport. Fire-and-forget for the same reason as
|
|
1352
|
+
* {@link HttpClient.sendExternalSpan}; replay confirms persistence with the
|
|
1353
|
+
* server-authoritative barrier in `replay.ts`, not by awaiting this call.
|
|
1354
|
+
*/
|
|
1355
|
+
sendExternalTrace(payload) {
|
|
1356
|
+
this.getTraceTransport()?.submit("external_trace", {
|
|
1357
|
+
...payload,
|
|
1358
|
+
sdkVersion: __version__
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Partial update of an existing trace identified by its Bitfab trace ID.
|
|
1363
|
+
* Used by the detached `client.getTrace(id)` handle.
|
|
1364
|
+
*
|
|
1365
|
+
* Blocking, like the other trace-API calls: it resolves once the server has
|
|
1366
|
+
* applied the change and rejects if the server refused it. A patch targets a
|
|
1367
|
+
* trace that is already closed, so there is no batch for it to ride along
|
|
1368
|
+
* with and no later signal that would reveal a silent failure.
|
|
1369
|
+
*/
|
|
1370
|
+
async patchTrace(traceId, payload) {
|
|
1371
|
+
const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
|
|
1372
|
+
await this.request(endpoint, payload, { method: "PATCH" });
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Start a replay session by fetching historical traces.
|
|
1376
|
+
* Blocking call - creates a test run and returns lightweight item references.
|
|
1377
|
+
*/
|
|
1378
|
+
async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetId, graderIds, dbBranchSettings) {
|
|
1379
|
+
const payload = { traceFunctionKey };
|
|
1380
|
+
if (limit !== void 0) {
|
|
1381
|
+
payload.limit = limit;
|
|
1382
|
+
}
|
|
1383
|
+
if (traceIds) {
|
|
1384
|
+
payload.traceIds = traceIds;
|
|
1385
|
+
}
|
|
1386
|
+
if (name !== void 0) {
|
|
1387
|
+
payload.name = name;
|
|
1388
|
+
}
|
|
1389
|
+
if (codeChangeDescription !== void 0) {
|
|
1390
|
+
payload.codeChangeDescription = codeChangeDescription;
|
|
1391
|
+
}
|
|
1392
|
+
if (codeChangeFiles !== void 0) {
|
|
1393
|
+
payload.codeChangeFiles = codeChangeFiles;
|
|
1394
|
+
}
|
|
1395
|
+
if (includeDbBranchLease) {
|
|
1396
|
+
payload.includeDbBranchLease = true;
|
|
1397
|
+
payload.lazyDbBranchLease = true;
|
|
1398
|
+
}
|
|
1399
|
+
if (experimentGroupId !== void 0) {
|
|
1400
|
+
payload.experimentGroupId = experimentGroupId;
|
|
1401
|
+
}
|
|
1402
|
+
if (datasetId !== void 0) {
|
|
1403
|
+
payload.datasetId = datasetId;
|
|
1404
|
+
}
|
|
1405
|
+
if (graderIds !== void 0) {
|
|
1406
|
+
payload.graderIds = graderIds;
|
|
1407
|
+
}
|
|
1408
|
+
if (dbBranchSettings !== void 0) {
|
|
1409
|
+
payload.dbBranchSettings = dbBranchSettings;
|
|
1410
|
+
}
|
|
1411
|
+
const timeout = includeDbBranchLease ? REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS : 3e4;
|
|
1412
|
+
return this.request("/api/sdk/replay/start", payload, {
|
|
1413
|
+
timeout
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Fetch an external span by ID.
|
|
1418
|
+
* Blocking GET request.
|
|
1419
|
+
*/
|
|
1420
|
+
async getExternalSpan(spanId) {
|
|
1421
|
+
const url = `${this.serviceUrl}/api/sdk/externalSpans/${spanId}`;
|
|
1422
|
+
const controller = new AbortController();
|
|
1423
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
1424
|
+
try {
|
|
1425
|
+
const response = await fetch(url, {
|
|
1426
|
+
method: "GET",
|
|
1427
|
+
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1428
|
+
signal: controller.signal
|
|
1429
|
+
});
|
|
1430
|
+
if (!response.ok) {
|
|
1431
|
+
const errorText = await response.text();
|
|
1432
|
+
throw new BitfabError(
|
|
1433
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
return await response.json();
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
if (error instanceof BitfabError) {
|
|
1439
|
+
throw error;
|
|
1440
|
+
}
|
|
1441
|
+
if (error instanceof Error) {
|
|
1442
|
+
if (error.name === "AbortError") {
|
|
1443
|
+
throw new BitfabError("Request timed out after 30000ms");
|
|
1444
|
+
}
|
|
1445
|
+
throw new BitfabError(error.message);
|
|
1446
|
+
}
|
|
1447
|
+
throw new BitfabError("Unknown error occurred");
|
|
1448
|
+
} finally {
|
|
1449
|
+
clearTimeout(timeoutId);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Fetch the span tree for a root span.
|
|
1454
|
+
* Blocking GET request.
|
|
1455
|
+
*
|
|
1456
|
+
* Pass `includeOutputs: false` for a payload-free tree (structure +
|
|
1457
|
+
* `externalSpanId` only), so recorded outputs are fetched lazily per mocked
|
|
1458
|
+
* span instead of all up front. Omit it (default eager) for `mock: "all"`.
|
|
1459
|
+
*/
|
|
1460
|
+
async getSpanTree(externalSpanId, options) {
|
|
1461
|
+
const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
|
|
1462
|
+
const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
|
|
1463
|
+
const controller = new AbortController();
|
|
1464
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
1465
|
+
try {
|
|
1466
|
+
const response = await fetch(url, {
|
|
1467
|
+
method: "GET",
|
|
1468
|
+
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1469
|
+
signal: controller.signal
|
|
1470
|
+
});
|
|
1471
|
+
if (!response.ok) {
|
|
1472
|
+
const errorText = await response.text();
|
|
1473
|
+
throw new BitfabError(
|
|
1474
|
+
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
return await response.json();
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
if (error instanceof BitfabError) {
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1482
|
+
if (error instanceof Error) {
|
|
1483
|
+
if (error.name === "AbortError") {
|
|
1484
|
+
throw new BitfabError("Request timed out after 30000ms");
|
|
1485
|
+
}
|
|
1486
|
+
throw new BitfabError(error.message);
|
|
1487
|
+
}
|
|
1488
|
+
throw new BitfabError("Unknown error occurred");
|
|
1489
|
+
} finally {
|
|
1490
|
+
clearTimeout(timeoutId);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Read which of a replay run's traces the server has fully persisted.
|
|
1495
|
+
*
|
|
1496
|
+
* With `expectedSpanCounts`, a trace appears in the response only once it
|
|
1497
|
+
* has a final status AND at least that many persisted spans, which is what
|
|
1498
|
+
* makes this a real barrier rather than a "the row exists" check.
|
|
1499
|
+
*/
|
|
1500
|
+
async getReplayStatus(testRunId, expectedSpanCounts) {
|
|
1501
|
+
return this.request(
|
|
1502
|
+
"/api/sdk/replay/status",
|
|
1503
|
+
{ testRunId, expectedSpanCounts },
|
|
1504
|
+
{ timeout: 3e4 }
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Mark a replay test run as completed.
|
|
1509
|
+
* Blocking call.
|
|
1510
|
+
*/
|
|
1511
|
+
async completeReplay(testRunId) {
|
|
1512
|
+
return this.request(
|
|
1513
|
+
"/api/sdk/replay/complete",
|
|
1514
|
+
{ testRunId },
|
|
1515
|
+
{ timeout: 3e4 }
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Ask the server to materialize a per-trace DB branch lease from a
|
|
1520
|
+
* captured `dbSnapshotRef`. Blocking - the resolver creates a Neon
|
|
1521
|
+
* snapshot + preview branch and polls operations to readiness, which
|
|
1522
|
+
* can take seconds.
|
|
1523
|
+
*/
|
|
1524
|
+
async resolveDbBranchLease(testRunId, traceId, dbBranchSettings) {
|
|
1525
|
+
return this.request(
|
|
1526
|
+
"/api/sdk/replay/resolveDbBranchLease",
|
|
1527
|
+
{ testRunId, traceId, dbBranchSettings },
|
|
1528
|
+
{ timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
/** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
|
|
1532
|
+
async releaseDbBranchLease(neonBranchId) {
|
|
1533
|
+
await this.request(
|
|
1534
|
+
"/api/sdk/replay/releaseDbBranchLease",
|
|
1535
|
+
{ neonBranchId },
|
|
1536
|
+
{ timeout: 3e4 }
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
// src/serialize.ts
|
|
1544
|
+
function describeValue(value) {
|
|
1545
|
+
try {
|
|
1546
|
+
const ctorName = value?.constructor?.name;
|
|
1547
|
+
if (ctorName && ctorName !== "Object") {
|
|
1548
|
+
return ctorName;
|
|
1549
|
+
}
|
|
1550
|
+
} catch {
|
|
1551
|
+
}
|
|
1552
|
+
return typeof value;
|
|
1553
|
+
}
|
|
1554
|
+
function unserializableStub(value, reason) {
|
|
1555
|
+
warnOnce(
|
|
1556
|
+
`serialize:${reason.replace(/\d+/g, "N")}`,
|
|
1557
|
+
`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.`
|
|
1558
|
+
);
|
|
1559
|
+
let summary;
|
|
1560
|
+
try {
|
|
1561
|
+
summary = `<unserializable: ${describeValue(value)} (${reason})>`;
|
|
1562
|
+
} catch {
|
|
1563
|
+
summary = `<unserializable (${reason})>`;
|
|
1564
|
+
}
|
|
1565
|
+
return { json: summary };
|
|
1566
|
+
}
|
|
1567
|
+
function serializeValue(value) {
|
|
1568
|
+
try {
|
|
1569
|
+
const { json, meta } = import_superjson.default.serialize(value);
|
|
1570
|
+
let size;
|
|
1571
|
+
try {
|
|
1572
|
+
size = JSON.stringify(json).length;
|
|
1573
|
+
} catch {
|
|
1574
|
+
return unserializableStub(value, "stringify_failed_after_superjson");
|
|
1575
|
+
}
|
|
1576
|
+
if (size > MAX_SERIALIZED_BYTES) {
|
|
1577
|
+
return unserializableStub(value, `too_large_${size}_bytes`);
|
|
1578
|
+
}
|
|
1579
|
+
return meta ? { json, meta } : { json };
|
|
1580
|
+
} catch {
|
|
1581
|
+
try {
|
|
1582
|
+
return { json: JSON.parse(JSON.stringify(value)) };
|
|
1583
|
+
} catch {
|
|
1584
|
+
return unserializableStub(value, "json_stringify_failed");
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function deserializeValue(serialized) {
|
|
1589
|
+
if (serialized.meta === void 0) {
|
|
1590
|
+
return serialized.json;
|
|
1591
|
+
}
|
|
1592
|
+
return import_superjson.default.deserialize({
|
|
1593
|
+
json: serialized.json,
|
|
1594
|
+
meta: serialized.meta
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
function toJsonSafe(value) {
|
|
1598
|
+
return toJsonSafeReport(value).safe;
|
|
1599
|
+
}
|
|
1600
|
+
function toJsonSafeReport(value) {
|
|
1601
|
+
const dropped = [];
|
|
1602
|
+
const safe = toJsonSafeInner(value, 0, /* @__PURE__ */ new WeakSet(), dropped);
|
|
1603
|
+
try {
|
|
1604
|
+
const size = JSON.stringify(safe)?.length ?? 0;
|
|
1605
|
+
if (size > MAX_FRAMEWORK_SERIALIZED_BYTES) {
|
|
1606
|
+
warnOnce(
|
|
1607
|
+
"toJsonSafe:too_large",
|
|
1608
|
+
`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.`
|
|
1609
|
+
);
|
|
1610
|
+
return {
|
|
1611
|
+
safe: `<unserializable: too_large_${size}_bytes>`,
|
|
1612
|
+
dropped: [...dropped, `too_large_${size}_bytes`]
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
} catch {
|
|
1616
|
+
}
|
|
1617
|
+
return { safe, dropped };
|
|
1618
|
+
}
|
|
1619
|
+
function toJsonSafeInner(value, depth, seen, dropped) {
|
|
1620
|
+
if (value === null || value === void 0) {
|
|
1621
|
+
return value;
|
|
1622
|
+
}
|
|
1623
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1624
|
+
return value;
|
|
1625
|
+
}
|
|
1626
|
+
const className = value?.constructor?.name ?? typeof value;
|
|
1627
|
+
if (depth > MAX_SAFE_DEPTH) {
|
|
1628
|
+
dropped.push(className);
|
|
1629
|
+
return `<${className}>`;
|
|
1630
|
+
}
|
|
1631
|
+
if (typeof value !== "object") {
|
|
1632
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
1633
|
+
dropped.push(className);
|
|
1634
|
+
}
|
|
1635
|
+
try {
|
|
1636
|
+
return String(value);
|
|
1637
|
+
} catch {
|
|
1638
|
+
dropped.push(className);
|
|
1639
|
+
return `<${className}>`;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
if (seen.has(value)) {
|
|
1643
|
+
dropped.push(className);
|
|
1644
|
+
return `<cycle ${className}>`;
|
|
1645
|
+
}
|
|
1646
|
+
seen.add(value);
|
|
1647
|
+
let result;
|
|
1648
|
+
if (Array.isArray(value)) {
|
|
1649
|
+
result = value.map(
|
|
1650
|
+
(item) => toJsonSafeInner(item, depth + 1, seen, dropped)
|
|
1651
|
+
);
|
|
1652
|
+
} else if (typeof value.toJSON === "function") {
|
|
1653
|
+
try {
|
|
1654
|
+
result = toJsonSafeInner(
|
|
1655
|
+
value.toJSON(),
|
|
1656
|
+
depth + 1,
|
|
1657
|
+
seen,
|
|
1658
|
+
dropped
|
|
1659
|
+
);
|
|
1660
|
+
} catch {
|
|
1661
|
+
dropped.push(className);
|
|
1662
|
+
result = `<${className}>`;
|
|
1663
|
+
}
|
|
1664
|
+
} else {
|
|
1665
|
+
try {
|
|
1666
|
+
const obj = {};
|
|
1667
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1668
|
+
if (!k.startsWith("_")) {
|
|
1669
|
+
obj[k] = toJsonSafeInner(v, depth + 1, seen, dropped);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
result = obj;
|
|
1673
|
+
} catch {
|
|
1674
|
+
dropped.push(className);
|
|
1675
|
+
result = `<${className}>`;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
seen.delete(value);
|
|
1679
|
+
return result;
|
|
1680
|
+
}
|
|
1681
|
+
var import_superjson, MAX_SERIALIZED_BYTES, MAX_FRAMEWORK_SERIALIZED_BYTES, MAX_SAFE_DEPTH;
|
|
1682
|
+
var init_serialize = __esm({
|
|
1683
|
+
"src/serialize.ts"() {
|
|
1684
|
+
"use strict";
|
|
1685
|
+
import_superjson = __toESM(require("superjson"), 1);
|
|
1686
|
+
init_warnOnce();
|
|
1687
|
+
MAX_SERIALIZED_BYTES = 512e3;
|
|
1688
|
+
MAX_FRAMEWORK_SERIALIZED_BYTES = 2e6;
|
|
1689
|
+
MAX_SAFE_DEPTH = 6;
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
// src/randomUuid.ts
|
|
1694
|
+
function randomUuid() {
|
|
1695
|
+
const globalCrypto = globalThis.crypto;
|
|
1696
|
+
if (typeof globalCrypto?.randomUUID === "function") {
|
|
1697
|
+
try {
|
|
1698
|
+
return globalCrypto.randomUUID();
|
|
1699
|
+
} catch {
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
warnOnce(
|
|
1703
|
+
"crypto-unavailable",
|
|
1704
|
+
"global crypto.randomUUID is unavailable; using a non-cryptographic fallback for trace/span ids. Tracing works normally (ids are correlation-only, not security-sensitive)."
|
|
1705
|
+
);
|
|
1706
|
+
return fallbackUuidV4();
|
|
1707
|
+
}
|
|
1708
|
+
function fallbackUuidV4() {
|
|
1709
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
|
|
1710
|
+
const rand = Math.random() * 16 | 0;
|
|
1711
|
+
const value = char === "x" ? rand : rand & 3 | 8;
|
|
1712
|
+
return value.toString(16);
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
var init_randomUuid = __esm({
|
|
1716
|
+
"src/randomUuid.ts"() {
|
|
1717
|
+
"use strict";
|
|
1718
|
+
init_warnOnce();
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
// src/mockOverride.ts
|
|
1723
|
+
function resolveMockValue(value, ctx) {
|
|
1724
|
+
return typeof value === "function" ? value(ctx) : value;
|
|
1725
|
+
}
|
|
1726
|
+
function normalizeMockOverrides(mockOverride) {
|
|
1727
|
+
if (mockOverride === void 0) {
|
|
1728
|
+
return [];
|
|
1729
|
+
}
|
|
1730
|
+
return Array.isArray(mockOverride) ? mockOverride : [mockOverride];
|
|
1731
|
+
}
|
|
1732
|
+
var init_mockOverride = __esm({
|
|
1733
|
+
"src/mockOverride.ts"() {
|
|
1734
|
+
"use strict";
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
// src/codeChange.ts
|
|
1739
|
+
async function resolveAutoCodeChange(label) {
|
|
1740
|
+
if (typeof process === "undefined") {
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
if (process.env?.BITFAB_DISABLE_CODE_CHANGE_CAPTURE) {
|
|
1744
|
+
return null;
|
|
1745
|
+
}
|
|
1746
|
+
const fromEnv = await readCodeChangeFile();
|
|
1747
|
+
if (fromEnv) {
|
|
1748
|
+
return fromEnv;
|
|
1749
|
+
}
|
|
1750
|
+
return captureCodeChangeFromGit(process.cwd?.() ?? ".", label);
|
|
1751
|
+
}
|
|
1752
|
+
async function readCodeChangeFile() {
|
|
1753
|
+
const path = process.env?.BITFAB_CODE_CHANGE_PATH;
|
|
1754
|
+
if (!path) {
|
|
1755
|
+
return null;
|
|
1756
|
+
}
|
|
1757
|
+
try {
|
|
1758
|
+
const { readFile } = await import("fs/promises");
|
|
1759
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1760
|
+
const files = Array.isArray(parsed?.files) && parsed.files.every(
|
|
1761
|
+
(f) => typeof f === "object" && f !== null && !Array.isArray(f)
|
|
1762
|
+
) ? parsed.files : void 0;
|
|
1763
|
+
const description = typeof parsed?.description === "string" ? parsed.description : void 0;
|
|
1764
|
+
if (!files && description === void 0) {
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1767
|
+
return { description, files };
|
|
1768
|
+
} catch {
|
|
1769
|
+
return null;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
async function captureCodeChangeFromGit(cwd, label) {
|
|
1773
|
+
let execFile;
|
|
1774
|
+
let readFile;
|
|
1775
|
+
try {
|
|
1776
|
+
;
|
|
1777
|
+
({ execFile } = await import("child_process"));
|
|
1778
|
+
({ readFile } = await import("fs/promises"));
|
|
1779
|
+
} catch {
|
|
1780
|
+
return null;
|
|
1781
|
+
}
|
|
1782
|
+
const git = (dir, args) => new Promise((resolve) => {
|
|
1783
|
+
execFile(
|
|
1784
|
+
"git",
|
|
1785
|
+
args,
|
|
1786
|
+
// 30s timeout so a hung git (e.g. a network-touching ref op) can't
|
|
1787
|
+
// block the whole replay indefinitely.
|
|
1788
|
+
{ cwd: dir, maxBuffer: 64 * 1024 * 1024, timeout: 3e4 },
|
|
1789
|
+
(err, stdout) => resolve(err ? null : stdout)
|
|
1790
|
+
);
|
|
1791
|
+
});
|
|
1792
|
+
try {
|
|
1793
|
+
const root = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
|
|
1794
|
+
if (!root) {
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
const resolved = await resolveBase(git, root);
|
|
1798
|
+
if (!resolved) {
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
const { base, fromTrunk } = resolved;
|
|
1802
|
+
const blobBytes = async (ref, path) => {
|
|
1803
|
+
const out = await git(root, ["cat-file", "-s", `${ref}:${path}`]);
|
|
1804
|
+
const n = out ? Number.parseInt(out.trim(), 10) : Number.NaN;
|
|
1805
|
+
return Number.isFinite(n) ? n : 0;
|
|
1806
|
+
};
|
|
1807
|
+
const workingBytes = async (path) => {
|
|
1808
|
+
try {
|
|
1809
|
+
const { stat } = await import("fs/promises");
|
|
1810
|
+
const { join } = await import("path");
|
|
1811
|
+
return (await stat(join(root, path))).size;
|
|
1812
|
+
} catch {
|
|
1813
|
+
return 0;
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
const tracked = await git(root, [
|
|
1817
|
+
"diff",
|
|
1818
|
+
"--name-status",
|
|
1819
|
+
"--no-renames",
|
|
1820
|
+
"-z",
|
|
1821
|
+
base,
|
|
1822
|
+
"--",
|
|
1823
|
+
":!.bitfab"
|
|
1824
|
+
]);
|
|
1825
|
+
const untracked = await git(root, [
|
|
1826
|
+
"ls-files",
|
|
1827
|
+
"--others",
|
|
1828
|
+
"--exclude-standard",
|
|
1829
|
+
"-z",
|
|
1830
|
+
"--",
|
|
1831
|
+
":!.bitfab"
|
|
1832
|
+
]);
|
|
1833
|
+
const entries = [
|
|
1834
|
+
...parseNameStatusZ(tracked ?? ""),
|
|
1835
|
+
...(untracked ?? "").split(NUL).filter((p) => p.length > 0).map((path) => ({ status: "A", path }))
|
|
1836
|
+
];
|
|
1837
|
+
if (entries.length === 0) {
|
|
1838
|
+
return null;
|
|
1839
|
+
}
|
|
1840
|
+
const files = [];
|
|
1841
|
+
let totalBytes = 0;
|
|
1842
|
+
for (const { status, path } of entries) {
|
|
1843
|
+
if (files.length >= MAX_FILES) {
|
|
1844
|
+
break;
|
|
1845
|
+
}
|
|
1846
|
+
const beforeBytes = status === "A" ? 0 : await blobBytes(base, path);
|
|
1847
|
+
const afterBytes = status === "D" ? 0 : await workingBytes(path);
|
|
1848
|
+
if (beforeBytes > MAX_FILE_BYTES || afterBytes > MAX_FILE_BYTES) {
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
const before = (status === "A" ? "" : await git(root, ["show", `${base}:${path}`]) ?? "").replace(/\r\n/g, "\n");
|
|
1852
|
+
const after = (status === "D" ? "" : await readWorkingFile(readFile, root, path)).replace(/\r\n/g, "\n");
|
|
1853
|
+
if (before === after) {
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
const size = Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8");
|
|
1857
|
+
if (totalBytes + size > MAX_TOTAL_BYTES || looksBinary(before) || looksBinary(after)) {
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
totalBytes += size;
|
|
1861
|
+
files.push({ path, before, after });
|
|
1862
|
+
}
|
|
1863
|
+
if (files.length === 0) {
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
const subject = (await git(root, ["log", "-1", "--format=%s", "HEAD"]))?.trim();
|
|
1867
|
+
const fileWord = files.length === 1 ? "file" : "files";
|
|
1868
|
+
const head = label?.trim() || subject || "Working-tree change";
|
|
1869
|
+
const against = fromTrunk ? "vs trunk" : "uncommitted (vs HEAD)";
|
|
1870
|
+
return {
|
|
1871
|
+
description: `${head} (${files.length} ${fileWord} changed ${against})`,
|
|
1872
|
+
files
|
|
1873
|
+
};
|
|
1874
|
+
} catch {
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
async function resolveBase(git, root) {
|
|
1879
|
+
const forced = process.env?.BITFAB_CODE_CHANGE_BASE;
|
|
1880
|
+
if (forced && await refExists(git, root, forced)) {
|
|
1881
|
+
const base = (await git(root, ["merge-base", "HEAD", forced]))?.trim() || (await git(root, ["rev-parse", "--verify", forced]))?.trim() || null;
|
|
1882
|
+
return base ? { base, fromTrunk: true } : null;
|
|
1883
|
+
}
|
|
1884
|
+
for (const candidate of TRUNK_CANDIDATES) {
|
|
1885
|
+
if (!await refExists(git, root, candidate)) {
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
const mb = (await git(root, ["merge-base", "HEAD", candidate]))?.trim();
|
|
1889
|
+
if (mb) {
|
|
1890
|
+
return { base: mb, fromTrunk: true };
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return await refExists(git, root, "HEAD") ? { base: "HEAD", fromTrunk: false } : null;
|
|
1894
|
+
}
|
|
1895
|
+
async function refExists(git, root, ref) {
|
|
1896
|
+
return await git(root, ["rev-parse", "--verify", `${ref}^{object}`]) !== null;
|
|
1897
|
+
}
|
|
1898
|
+
async function readWorkingFile(readFile, root, path) {
|
|
1899
|
+
try {
|
|
1900
|
+
const { join } = await import("path");
|
|
1901
|
+
return await readFile(join(root, path), "utf8");
|
|
1902
|
+
} catch {
|
|
1903
|
+
return "";
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
function parseNameStatusZ(raw) {
|
|
1907
|
+
const parts = raw.split(NUL).filter((p) => p.length > 0);
|
|
1908
|
+
const out = [];
|
|
1909
|
+
for (let i = 0; i + 1 < parts.length; i += 2) {
|
|
1910
|
+
out.push({ status: parts[i].charAt(0), path: parts[i + 1] });
|
|
1911
|
+
}
|
|
1912
|
+
return out;
|
|
1913
|
+
}
|
|
1914
|
+
function looksBinary(s) {
|
|
1915
|
+
return s.slice(0, 8e3).includes(NUL);
|
|
1916
|
+
}
|
|
1917
|
+
var MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, TRUNK_CANDIDATES, NUL;
|
|
1918
|
+
var init_codeChange = __esm({
|
|
1919
|
+
"src/codeChange.ts"() {
|
|
1920
|
+
"use strict";
|
|
1921
|
+
MAX_FILES = 60;
|
|
1922
|
+
MAX_FILE_BYTES = 5e5;
|
|
1923
|
+
MAX_TOTAL_BYTES = 2e6;
|
|
1924
|
+
TRUNK_CANDIDATES = [
|
|
1925
|
+
"origin/HEAD",
|
|
1926
|
+
"origin/main",
|
|
1927
|
+
"origin/master",
|
|
1928
|
+
"main",
|
|
1929
|
+
"master"
|
|
1930
|
+
];
|
|
1931
|
+
NUL = String.fromCharCode(0);
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
// src/replay.ts
|
|
1936
|
+
var replay_exports = {};
|
|
1937
|
+
__export(replay_exports, {
|
|
1938
|
+
BITFAB_PROGRESS_PREFIX: () => BITFAB_PROGRESS_PREFIX,
|
|
1939
|
+
replay: () => replay,
|
|
1940
|
+
reportReplayProgress: () => reportReplayProgress
|
|
1941
|
+
});
|
|
1942
|
+
function dbBranchEnabled(dbBranch) {
|
|
1943
|
+
return dbBranch !== void 0 && dbBranch !== false;
|
|
1944
|
+
}
|
|
1945
|
+
function resolveDbBranchSettings(dbBranch) {
|
|
1946
|
+
if (!dbBranch || dbBranch === true) {
|
|
1947
|
+
return void 0;
|
|
1948
|
+
}
|
|
1949
|
+
const { minCu, maxCu, warmupSql } = dbBranch;
|
|
1950
|
+
const settings = {
|
|
1951
|
+
...minCu === void 0 ? {} : { minCu },
|
|
1952
|
+
...maxCu === void 0 ? {} : { maxCu },
|
|
1953
|
+
...warmupSql === void 0 ? {} : { warmupSql }
|
|
1954
|
+
};
|
|
1955
|
+
return Object.keys(settings).length === 0 ? void 0 : settings;
|
|
1956
|
+
}
|
|
1957
|
+
function reportReplayProgress(progress) {
|
|
1958
|
+
const stderr = typeof process !== "undefined" ? process.stderr : void 0;
|
|
1959
|
+
if (!stderr) {
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
try {
|
|
1963
|
+
stderr.write(`${BITFAB_PROGRESS_PREFIX}${JSON.stringify(progress)}
|
|
1964
|
+
`);
|
|
1965
|
+
} catch {
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
function deserializeInputs(spanData) {
|
|
1969
|
+
const inputMeta = spanData.input_meta;
|
|
1970
|
+
const rawInput = spanData.input;
|
|
1971
|
+
if (inputMeta !== void 0 && inputMeta !== null) {
|
|
1972
|
+
const deserialized = deserializeValue({ json: rawInput, meta: inputMeta });
|
|
1973
|
+
if (Array.isArray(deserialized)) {
|
|
1974
|
+
return deserialized;
|
|
1975
|
+
}
|
|
1976
|
+
return deserialized !== void 0 && deserialized !== null ? [deserialized] : [];
|
|
1977
|
+
}
|
|
1978
|
+
if (Array.isArray(rawInput)) {
|
|
1979
|
+
return rawInput;
|
|
1980
|
+
}
|
|
1981
|
+
return rawInput !== void 0 && rawInput !== null ? [rawInput] : [];
|
|
1982
|
+
}
|
|
1983
|
+
function deserializeOutput(spanData) {
|
|
1984
|
+
const outputMeta = spanData.output_meta;
|
|
1985
|
+
const rawOutput = spanData.output;
|
|
1986
|
+
if (outputMeta !== void 0 && outputMeta !== null) {
|
|
1987
|
+
return deserializeValue({ json: rawOutput, meta: outputMeta });
|
|
1988
|
+
}
|
|
1989
|
+
return rawOutput;
|
|
1990
|
+
}
|
|
1991
|
+
function buildMockTree(rootNode) {
|
|
1992
|
+
const spans = /* @__PURE__ */ new Map();
|
|
1993
|
+
const counters = /* @__PURE__ */ new Map();
|
|
1994
|
+
function walk(node) {
|
|
1995
|
+
const key = node.traceFunctionKey;
|
|
1996
|
+
if (key) {
|
|
1997
|
+
const name = node.spanName || key;
|
|
1998
|
+
const counterKey = `${key}:${name}`;
|
|
1999
|
+
const index = counters.get(counterKey) ?? 0;
|
|
2000
|
+
counters.set(counterKey, index + 1);
|
|
2001
|
+
spans.set(`${counterKey}:${index}`, {
|
|
2002
|
+
sourceSpanId: node.sourceSpanId,
|
|
2003
|
+
externalSpanId: node.externalSpanId,
|
|
2004
|
+
output: node.output,
|
|
2005
|
+
outputMeta: node.outputMeta
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
for (const child of node.children) {
|
|
2009
|
+
walk(child);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
for (const child of rootNode.children) {
|
|
2013
|
+
walk(child);
|
|
2014
|
+
}
|
|
2015
|
+
return { spans };
|
|
2016
|
+
}
|
|
2017
|
+
async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, includeDbBranchLease, dbBranchSettings, adaptInputs) {
|
|
2018
|
+
let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
|
|
2019
|
+
let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
|
|
2020
|
+
let dbSnapshotRef = serverItem.dbSnapshotRef;
|
|
2021
|
+
let inputs = [];
|
|
2022
|
+
let originalOutput;
|
|
2023
|
+
let result;
|
|
2024
|
+
let error = null;
|
|
2025
|
+
const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
|
|
2026
|
+
const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
|
|
2027
|
+
try {
|
|
2028
|
+
if (includeDbBranchLease && !lease && !leaseError) {
|
|
2029
|
+
const resolved = await httpClient.resolveDbBranchLease(
|
|
2030
|
+
testRunId,
|
|
2031
|
+
originalTraceId,
|
|
2032
|
+
dbBranchSettings
|
|
2033
|
+
);
|
|
2034
|
+
lease = resolved.lease ?? void 0;
|
|
2035
|
+
leaseError = resolved.leaseError ?? void 0;
|
|
2036
|
+
dbSnapshotRef = resolved.dbSnapshotRef ?? dbSnapshotRef;
|
|
2037
|
+
}
|
|
2038
|
+
if (leaseError) {
|
|
2039
|
+
throw new BitfabError(
|
|
2040
|
+
`Replay requested a database branch for trace ${originalTraceId} but it could not be resolved (${leaseError.code}): ${leaseError.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.`
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
const span = await httpClient.getExternalSpan(originalSpanId);
|
|
2044
|
+
const spanData = span.rawData?.span_data ?? {};
|
|
2045
|
+
inputs = deserializeInputs(spanData);
|
|
2046
|
+
originalOutput = deserializeOutput(spanData);
|
|
2047
|
+
if (adaptInputs) {
|
|
2048
|
+
inputs = adaptInputs(inputs, {
|
|
2049
|
+
originalTraceId,
|
|
2050
|
+
originalSpanId,
|
|
2051
|
+
// Deprecated aliases for originalTraceId/originalSpanId.
|
|
2052
|
+
sourceTraceId: originalTraceId,
|
|
2053
|
+
sourceSpanId: originalSpanId
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
const hasOverrides = resolvedOverrides.length > 0;
|
|
2057
|
+
const needTree = mockStrategy === "all" || mockStrategy === "marked" || hasOverrides;
|
|
2058
|
+
const includeOutputs = mockStrategy === "all";
|
|
2059
|
+
let mockTree;
|
|
2060
|
+
if (needTree) {
|
|
2061
|
+
try {
|
|
2062
|
+
const treeResponse = await httpClient.getSpanTree(originalSpanId, {
|
|
2063
|
+
includeOutputs
|
|
2064
|
+
});
|
|
2065
|
+
if (treeResponse.root) {
|
|
2066
|
+
mockTree = buildMockTree(treeResponse.root);
|
|
2067
|
+
} else if (mockStrategy === "all" || hasOverrides) {
|
|
2068
|
+
throw new BitfabError(
|
|
2069
|
+
`Replay mock strategy "${mockStrategy}"${hasOverrides ? " with overrides" : ""} requires a span tree root for original span ${originalSpanId}.`
|
|
2070
|
+
);
|
|
2071
|
+
} else {
|
|
2072
|
+
mockTree = void 0;
|
|
2073
|
+
}
|
|
2074
|
+
} catch (e) {
|
|
2075
|
+
if (mockStrategy === "all" || hasOverrides) {
|
|
2076
|
+
throw e;
|
|
2077
|
+
}
|
|
2078
|
+
mockTree = void 0;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
const outputCache = /* @__PURE__ */ new Map();
|
|
2082
|
+
const fetchSpanOutput = mockTree && !includeOutputs ? (externalSpanId) => {
|
|
2083
|
+
let pending = outputCache.get(externalSpanId);
|
|
2084
|
+
if (!pending) {
|
|
2085
|
+
pending = httpClient.getExternalSpan(externalSpanId).then(
|
|
2086
|
+
(s) => deserializeOutput(
|
|
2087
|
+
s.rawData?.span_data ?? {}
|
|
2088
|
+
)
|
|
2089
|
+
);
|
|
2090
|
+
outputCache.set(externalSpanId, pending);
|
|
2091
|
+
}
|
|
2092
|
+
return pending;
|
|
2093
|
+
} : void 0;
|
|
2094
|
+
const maybePromise = runWithReplayContext(
|
|
2095
|
+
{
|
|
2096
|
+
testRunId,
|
|
2097
|
+
traceId: replayedTraceId,
|
|
2098
|
+
inputSourceSpanId: span.id,
|
|
2099
|
+
inputSourceTraceId: span.externalTraceId,
|
|
2100
|
+
sourceBitfabTraceId: originalTraceId,
|
|
2101
|
+
mockTree,
|
|
2102
|
+
callCounters: mockTree ? /* @__PURE__ */ new Map() : void 0,
|
|
2103
|
+
mockStrategy,
|
|
2104
|
+
mockOverrides: hasOverrides ? resolvedOverrides : void 0,
|
|
2105
|
+
fetchSpanOutput,
|
|
2106
|
+
dbBranchLease: lease
|
|
2107
|
+
},
|
|
2108
|
+
() => fn(...inputs)
|
|
2109
|
+
);
|
|
2110
|
+
result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
|
|
2111
|
+
} catch (e) {
|
|
2112
|
+
error = e instanceof Error ? e.message : String(e);
|
|
2113
|
+
} finally {
|
|
2114
|
+
if (lease) {
|
|
2115
|
+
try {
|
|
2116
|
+
await httpClient.releaseDbBranchLease(lease.neonBranchId);
|
|
2117
|
+
} catch (e) {
|
|
1083
2118
|
try {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
out[k] = sanitize(v, seen);
|
|
1087
|
-
}
|
|
1088
|
-
result = out;
|
|
1089
|
-
} catch {
|
|
1090
|
-
warnOnce(
|
|
1091
|
-
"payload:field-getter-threw",
|
|
1092
|
-
"a value with a throwing getter/proxy could not be serialized into a span payload; it was replaced with a placeholder. The span still ships with its other fields intact."
|
|
2119
|
+
console.warn(
|
|
2120
|
+
`Bitfab: failed to release DB branch ${lease.neonBranchId} (TTL janitor will catch it): ${e instanceof Error ? e.message : String(e)}`
|
|
1093
2121
|
);
|
|
1094
|
-
|
|
1095
|
-
result = `<unserializable: ${className}>`;
|
|
2122
|
+
} catch {
|
|
1096
2123
|
}
|
|
1097
2124
|
}
|
|
1098
|
-
seen.delete(obj);
|
|
1099
|
-
return result;
|
|
1100
|
-
};
|
|
1101
|
-
let sanitized;
|
|
1102
|
-
try {
|
|
1103
|
-
sanitized = sanitize(payload, /* @__PURE__ */ new WeakSet());
|
|
1104
|
-
} catch (error) {
|
|
1105
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1106
|
-
return {
|
|
1107
|
-
body: JSON.stringify({ error: `payload_serialize_failed: ${message}` }),
|
|
1108
|
-
dropped
|
|
1109
|
-
};
|
|
1110
2125
|
}
|
|
1111
|
-
if (dropped.length > 0 && typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
|
|
1112
|
-
const obj = sanitized;
|
|
1113
|
-
const existing = Array.isArray(obj.errors) ? obj.errors : [];
|
|
1114
|
-
obj.errors = [
|
|
1115
|
-
...existing,
|
|
1116
|
-
{
|
|
1117
|
-
source: "sdk",
|
|
1118
|
-
step: "json_serialize",
|
|
1119
|
-
error: `stubbed non-serializable value(s): ${[
|
|
1120
|
-
...new Set(dropped)
|
|
1121
|
-
].join(", ")}`
|
|
1122
|
-
}
|
|
1123
|
-
];
|
|
1124
|
-
}
|
|
1125
|
-
return { body: JSON.stringify(sanitized), dropped };
|
|
1126
2126
|
}
|
|
2127
|
+
return {
|
|
2128
|
+
// Written in by replay() from the complete-replay response once the server
|
|
2129
|
+
// has minted this replay trace's row. Null until then: the client-side
|
|
2130
|
+
// correlation id (replayedTraceId) is never surfaced as the item's traceId.
|
|
2131
|
+
traceId: null,
|
|
2132
|
+
originalTraceId,
|
|
2133
|
+
originalSpanId,
|
|
2134
|
+
// Deprecated aliases for originalTraceId/originalSpanId.
|
|
2135
|
+
sourceTraceId: originalTraceId,
|
|
2136
|
+
sourceSpanId: originalSpanId,
|
|
2137
|
+
input: inputs,
|
|
2138
|
+
result,
|
|
2139
|
+
originalOutput,
|
|
2140
|
+
error,
|
|
2141
|
+
durationMs: serverItem.durationMs ?? null,
|
|
2142
|
+
// Filled in by replay() from the complete-replay response once the
|
|
2143
|
+
// replay traces are persisted and their spans aggregated server-side.
|
|
2144
|
+
// Null here (and on older servers) means "replay tokens not known".
|
|
2145
|
+
tokens: null,
|
|
2146
|
+
model: serverItem.model ?? null,
|
|
2147
|
+
dbSnapshotRef: dbSnapshotRef ?? null
|
|
2148
|
+
};
|
|
1127
2149
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
if (
|
|
2150
|
+
async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds) {
|
|
2151
|
+
const deferredSettled = await httpClient.settleDeferredWork(
|
|
2152
|
+
REPLAY_PERSISTENCE_TIMEOUT_MS
|
|
2153
|
+
);
|
|
2154
|
+
if (!deferredSettled) {
|
|
2155
|
+
throw new BitfabError(
|
|
2156
|
+
`Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
|
|
2160
|
+
if (Object.keys(expectedSpanCounts).length === 0) {
|
|
1139
2161
|
return;
|
|
1140
2162
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
2163
|
+
const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
|
|
2164
|
+
const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
|
|
2165
|
+
let missing = Object.keys(expectedSpanCounts).length;
|
|
2166
|
+
while (true) {
|
|
2167
|
+
const status = await httpClient.getReplayStatus(
|
|
2168
|
+
testRunId,
|
|
2169
|
+
expectedSpanCounts
|
|
2170
|
+
);
|
|
2171
|
+
const ready = status.traceIds ?? {};
|
|
2172
|
+
missing = Object.keys(expectedSpanCounts).filter(
|
|
2173
|
+
(traceId) => ready[traceId] === void 0
|
|
2174
|
+
).length;
|
|
2175
|
+
if (missing === 0) {
|
|
2176
|
+
return;
|
|
1153
2177
|
}
|
|
2178
|
+
if (Date.now() >= deadline) {
|
|
2179
|
+
break;
|
|
2180
|
+
}
|
|
2181
|
+
await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
1154
2182
|
}
|
|
2183
|
+
const cause = flushed ? "" : " Delivery was also not confirmed before the flush deadline, so the spans likely never reached the server.";
|
|
2184
|
+
throw new BitfabError(
|
|
2185
|
+
`Replay traces were not fully persisted before the delivery deadline (testRunId ${testRunId}, missing ${missing} of ${Object.keys(expectedSpanCounts).length} trace(s)).${cause}`
|
|
2186
|
+
);
|
|
1155
2187
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
isFlushing = true;
|
|
1161
|
-
Promise.allSettled(
|
|
1162
|
-
Array.from(pendingTracePromises).map(
|
|
1163
|
-
(p) => p.catch(() => {
|
|
1164
|
-
})
|
|
1165
|
-
)
|
|
1166
|
-
).then(() => {
|
|
1167
|
-
isFlushing = false;
|
|
1168
|
-
}).catch(() => {
|
|
1169
|
-
isFlushing = false;
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
2188
|
+
function sleep(ms) {
|
|
2189
|
+
return new Promise((resolve) => {
|
|
2190
|
+
const timer = setTimeout(resolve, ms);
|
|
2191
|
+
unrefTimer(timer);
|
|
1172
2192
|
});
|
|
1173
2193
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
2194
|
+
async function mapWithConcurrency2(tasks, maxConcurrency, onSettled) {
|
|
2195
|
+
const results = new Array(tasks.length);
|
|
2196
|
+
let nextIndex = 0;
|
|
2197
|
+
async function worker() {
|
|
2198
|
+
while (nextIndex < tasks.length) {
|
|
2199
|
+
const index = nextIndex++;
|
|
2200
|
+
const result = await tasks[index]();
|
|
2201
|
+
results[index] = result;
|
|
2202
|
+
onSettled?.(result, index);
|
|
2203
|
+
}
|
|
1179
2204
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
2205
|
+
const workers = Array.from(
|
|
2206
|
+
{ length: Math.min(maxConcurrency, tasks.length) },
|
|
2207
|
+
() => worker()
|
|
2208
|
+
);
|
|
2209
|
+
await Promise.all(workers);
|
|
2210
|
+
return results;
|
|
2211
|
+
}
|
|
2212
|
+
async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, registeredOverrides = []) {
|
|
2213
|
+
if (options?.traceIds !== void 0) {
|
|
2214
|
+
if (options.traceIds.length === 0) {
|
|
2215
|
+
throw new BitfabError("traceIds must contain at least one trace ID.");
|
|
2216
|
+
}
|
|
2217
|
+
if (options.traceIds.length > 100) {
|
|
2218
|
+
throw new BitfabError(
|
|
2219
|
+
`traceIds supports at most 100 trace IDs per replay (got ${options.traceIds.length}).`
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
1186
2222
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
if (dropped.length > 0) {
|
|
1205
|
-
try {
|
|
1206
|
-
console.warn(
|
|
1207
|
-
`Bitfab: request body to ${endpoint} held ${dropped.length} non-serializable value(s) (${[...new Set(dropped)].join(", ")}); they were stubbed so the span still sends, but the trace may be incomplete or not replayable. Capture a JSON-safe projection of this input to make it replayable.`
|
|
1208
|
-
);
|
|
1209
|
-
} catch {
|
|
2223
|
+
if (options?.limit !== void 0 && options?.traceIds !== void 0) {
|
|
2224
|
+
try {
|
|
2225
|
+
console.warn(
|
|
2226
|
+
"Bitfab: limit is ignored when traceIds is passed: the explicit trace ID list already determines how many traces replay."
|
|
2227
|
+
);
|
|
2228
|
+
} catch {
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
await replayContextReady;
|
|
2232
|
+
let codeChangeDescription = options?.codeChangeDescription;
|
|
2233
|
+
let codeChangeFiles = options?.codeChangeFiles;
|
|
2234
|
+
if (codeChangeFiles === void 0) {
|
|
2235
|
+
const captured = await resolveAutoCodeChange(options?.name);
|
|
2236
|
+
if (captured) {
|
|
2237
|
+
codeChangeFiles = captured.files;
|
|
2238
|
+
if (codeChangeDescription === void 0) {
|
|
2239
|
+
codeChangeDescription = captured.description;
|
|
1210
2240
|
}
|
|
1211
2241
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
2242
|
+
}
|
|
2243
|
+
const {
|
|
2244
|
+
testRunId,
|
|
2245
|
+
testRunUrl,
|
|
2246
|
+
items: serverItems
|
|
2247
|
+
} = await httpClient.startReplay(
|
|
2248
|
+
traceFunctionKey,
|
|
2249
|
+
// limit is meaningless with explicit traceIds (the ID list determines
|
|
2250
|
+
// the count), so it's omitted from the request entirely.
|
|
2251
|
+
options?.traceIds ? void 0 : options?.limit ?? 5,
|
|
2252
|
+
options?.traceIds,
|
|
2253
|
+
options?.name,
|
|
2254
|
+
codeChangeDescription,
|
|
2255
|
+
codeChangeFiles,
|
|
2256
|
+
dbBranchEnabled(options?.dbBranch),
|
|
2257
|
+
// includeDbBranchLease
|
|
2258
|
+
options?.experimentGroupId,
|
|
2259
|
+
options?.datasetId,
|
|
2260
|
+
options?.graderIds,
|
|
2261
|
+
resolveDbBranchSettings(options?.dbBranch)
|
|
2262
|
+
);
|
|
2263
|
+
const mockStrategy = options?.mock ?? "marked";
|
|
2264
|
+
const maxConcurrency = options?.maxConcurrency ?? 10;
|
|
2265
|
+
const resolvedOverrides = [
|
|
2266
|
+
...normalizeMockOverrides(options?.mockOverride),
|
|
2267
|
+
...registeredOverrides
|
|
2268
|
+
];
|
|
2269
|
+
const replayedTraceIds = serverItems.map(() => randomUuid());
|
|
2270
|
+
const tasks = serverItems.map(
|
|
2271
|
+
(serverItem, index) => () => processItem(
|
|
2272
|
+
httpClient,
|
|
2273
|
+
serverItem,
|
|
2274
|
+
fn,
|
|
2275
|
+
testRunId,
|
|
2276
|
+
mockStrategy,
|
|
2277
|
+
resolvedOverrides,
|
|
2278
|
+
replayedTraceIds[index],
|
|
2279
|
+
dbBranchEnabled(options?.dbBranch),
|
|
2280
|
+
resolveDbBranchSettings(options?.dbBranch),
|
|
2281
|
+
options?.adaptInputs
|
|
2282
|
+
)
|
|
2283
|
+
);
|
|
2284
|
+
const total = tasks.length;
|
|
2285
|
+
let completed = 0;
|
|
2286
|
+
let succeeded = 0;
|
|
2287
|
+
let errored = 0;
|
|
2288
|
+
const resultItems = await mapWithConcurrency2(
|
|
2289
|
+
tasks,
|
|
2290
|
+
maxConcurrency,
|
|
2291
|
+
options?.onProgress ? (item) => {
|
|
2292
|
+
completed += 1;
|
|
2293
|
+
if (item.error === null) {
|
|
2294
|
+
succeeded += 1;
|
|
2295
|
+
} else {
|
|
2296
|
+
errored += 1;
|
|
1227
2297
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
2298
|
+
try {
|
|
2299
|
+
options?.onProgress?.({
|
|
2300
|
+
testRunId,
|
|
2301
|
+
completed,
|
|
2302
|
+
total,
|
|
2303
|
+
succeeded,
|
|
2304
|
+
errored,
|
|
2305
|
+
item: {
|
|
2306
|
+
// The server replay trace id isn't known until completeReplay
|
|
2307
|
+
// runs (below), so it can't be reported mid-run and we never
|
|
2308
|
+
// emit the client-side placeholder. originalTraceId (the
|
|
2309
|
+
// historical trace) is known now and is what a UI keys on to
|
|
2310
|
+
// identify what just settled.
|
|
2311
|
+
traceId: null,
|
|
2312
|
+
originalTraceId: item.originalTraceId ?? null,
|
|
2313
|
+
originalSpanId: item.originalSpanId ?? null,
|
|
2314
|
+
// Deprecated aliases for originalTraceId/originalSpanId.
|
|
2315
|
+
sourceTraceId: item.originalTraceId ?? null,
|
|
2316
|
+
sourceSpanId: item.originalSpanId ?? null,
|
|
2317
|
+
input: item.input,
|
|
2318
|
+
result: item.result,
|
|
2319
|
+
originalOutput: item.originalOutput,
|
|
2320
|
+
error: item.error,
|
|
2321
|
+
durationMs: item.durationMs,
|
|
2322
|
+
tokens: item.tokens,
|
|
2323
|
+
model: item.model,
|
|
2324
|
+
dbSnapshotRef: item.dbSnapshotRef
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
} catch {
|
|
2328
|
+
}
|
|
2329
|
+
} : void 0
|
|
2330
|
+
);
|
|
2331
|
+
await waitForReplayPersistence(httpClient, testRunId, replayedTraceIds);
|
|
2332
|
+
const completeResult = await httpClient.completeReplay(testRunId);
|
|
2333
|
+
const serverTraceIds = completeResult.traceIds;
|
|
2334
|
+
const replayTokens = completeResult.tokens;
|
|
2335
|
+
if (serverTraceIds !== void 0) {
|
|
2336
|
+
const missing = [];
|
|
2337
|
+
let completedCount = 0;
|
|
2338
|
+
for (let index = 0; index < resultItems.length; index += 1) {
|
|
2339
|
+
const item = resultItems[index];
|
|
2340
|
+
const localId = replayedTraceIds[index];
|
|
2341
|
+
const mapped = localId ? serverTraceIds[localId] : void 0;
|
|
2342
|
+
item.traceId = mapped ?? null;
|
|
2343
|
+
if (item.error === null) {
|
|
2344
|
+
completedCount += 1;
|
|
2345
|
+
if (mapped === void 0) {
|
|
2346
|
+
missing.push(localId ?? item.originalTraceId);
|
|
1235
2347
|
}
|
|
1236
|
-
throw new BitfabError(result.error);
|
|
1237
|
-
}
|
|
1238
|
-
return result;
|
|
1239
|
-
} catch (error) {
|
|
1240
|
-
if (error instanceof BitfabError) {
|
|
1241
|
-
throw error;
|
|
1242
2348
|
}
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1245
|
-
throw new BitfabError(`Request timed out after ${timeout}ms`);
|
|
1246
|
-
}
|
|
1247
|
-
throw new BitfabError(error.message);
|
|
2349
|
+
if (mapped !== void 0) {
|
|
2350
|
+
item.tokens = replayTokens?.[mapped] ?? null;
|
|
1248
2351
|
}
|
|
1249
|
-
throw new BitfabError("Unknown error occurred");
|
|
1250
|
-
} finally {
|
|
1251
|
-
clearTimeout(timeoutId);
|
|
1252
2352
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
async lookupFunction(name) {
|
|
1259
|
-
return this.request("/api/sdk/functions/lookup", { name });
|
|
1260
|
-
}
|
|
1261
|
-
async getTraceSpan(traceId, lookup) {
|
|
1262
|
-
const searchParams = new URLSearchParams();
|
|
1263
|
-
if (lookup.id !== void 0) {
|
|
1264
|
-
searchParams.set("id", lookup.id);
|
|
1265
|
-
} else {
|
|
1266
|
-
searchParams.set("name", lookup.name);
|
|
1267
|
-
searchParams.set("occurrence", String(lookup.occurrence ?? "last"));
|
|
1268
|
-
}
|
|
1269
|
-
const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}/span?${searchParams.toString()}`;
|
|
1270
|
-
const response = await this.get(endpoint);
|
|
1271
|
-
return response.span;
|
|
1272
|
-
}
|
|
1273
|
-
async get(endpoint) {
|
|
1274
|
-
const url = `${this.serviceUrl}${endpoint}`;
|
|
1275
|
-
const controller = new AbortController();
|
|
1276
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1277
|
-
try {
|
|
1278
|
-
const response = await fetch(url, {
|
|
1279
|
-
method: "GET",
|
|
1280
|
-
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1281
|
-
signal: controller.signal
|
|
1282
|
-
});
|
|
1283
|
-
if (!response.ok) {
|
|
1284
|
-
const errorText = await response.text();
|
|
1285
|
-
throw new BitfabError(
|
|
1286
|
-
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1287
|
-
);
|
|
1288
|
-
}
|
|
1289
|
-
return await response.json();
|
|
1290
|
-
} catch (error) {
|
|
1291
|
-
if (error instanceof BitfabError) {
|
|
1292
|
-
throw error;
|
|
1293
|
-
}
|
|
1294
|
-
if (error instanceof Error) {
|
|
1295
|
-
if (error.name === "AbortError") {
|
|
1296
|
-
throw new BitfabError(`Request timed out after ${this.timeout}ms`);
|
|
1297
|
-
}
|
|
1298
|
-
throw new BitfabError(error.message);
|
|
1299
|
-
}
|
|
1300
|
-
throw new BitfabError("Unknown error occurred");
|
|
1301
|
-
} finally {
|
|
1302
|
-
clearTimeout(timeoutId);
|
|
2353
|
+
if (completedCount > 0 && missing.length === completedCount) {
|
|
2354
|
+
const serverCount = completeResult.traceCount !== void 0 ? ` The server persisted ${completeResult.traceCount} trace(s) for this run.` : "";
|
|
2355
|
+
throw new BitfabError(
|
|
2356
|
+
`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.`
|
|
2357
|
+
);
|
|
1303
2358
|
}
|
|
1304
|
-
|
|
1305
|
-
/**
|
|
1306
|
-
* Send an internal trace (from BAML execution).
|
|
1307
|
-
* Fire-and-forget with awaitOnExit - doesn't block the caller.
|
|
1308
|
-
*/
|
|
1309
|
-
sendInternalTrace(functionId, payload) {
|
|
1310
|
-
void awaitOnExit(
|
|
1311
|
-
this.request(`/api/sdk/functions/${functionId}/traces`, {
|
|
1312
|
-
...payload,
|
|
1313
|
-
sdkVersion: __version__
|
|
1314
|
-
})
|
|
1315
|
-
).catch((error) => {
|
|
1316
|
-
try {
|
|
1317
|
-
console.error("Bitfab: Failed to create trace:", error);
|
|
1318
|
-
} catch {
|
|
1319
|
-
}
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
/**
|
|
1323
|
-
* Send an external span (from withSpan wrapper or OpenAI tracing).
|
|
1324
|
-
* Fire-and-forget with awaitOnExit - doesn't block the caller.
|
|
1325
|
-
* Returns the tracked promise so callers can optionally await it.
|
|
1326
|
-
*/
|
|
1327
|
-
sendExternalSpan(payload) {
|
|
1328
|
-
return awaitOnExit(
|
|
1329
|
-
this.request("/api/sdk/externalSpans", {
|
|
1330
|
-
...payload,
|
|
1331
|
-
sdkVersion: __version__
|
|
1332
|
-
})
|
|
1333
|
-
).catch((error) => {
|
|
1334
|
-
try {
|
|
1335
|
-
console.error("Bitfab: Failed to create external span:", error);
|
|
1336
|
-
} catch {
|
|
1337
|
-
}
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
/**
|
|
1341
|
-
* Send an external trace (from OpenAI tracing).
|
|
1342
|
-
* Fire-and-forget with awaitOnExit - doesn't block the caller.
|
|
1343
|
-
* Returns the tracked promise so callers can optionally await it
|
|
1344
|
-
* (the replay path does, so trace completions are persisted before
|
|
1345
|
-
* `completeReplay` builds the trace-ID mapping).
|
|
1346
|
-
*/
|
|
1347
|
-
sendExternalTrace(payload) {
|
|
1348
|
-
return awaitOnExit(
|
|
1349
|
-
this.request("/api/sdk/externalTraces", {
|
|
1350
|
-
...payload,
|
|
1351
|
-
sdkVersion: __version__
|
|
1352
|
-
})
|
|
1353
|
-
).catch((error) => {
|
|
1354
|
-
try {
|
|
1355
|
-
console.error("Bitfab: Failed to create external trace:", error);
|
|
1356
|
-
} catch {
|
|
1357
|
-
}
|
|
1358
|
-
});
|
|
1359
|
-
}
|
|
1360
|
-
/**
|
|
1361
|
-
* Partial update of an existing trace identified by its Bitfab trace ID.
|
|
1362
|
-
* Used by the detached `client.getTrace(id)` handle. Fire-and-forget;
|
|
1363
|
-
* returns a tracked promise that callers may optionally await.
|
|
1364
|
-
*/
|
|
1365
|
-
patchTrace(traceId, payload) {
|
|
1366
|
-
const endpoint = `/api/sdk/traces/${encodeURIComponent(traceId)}`;
|
|
1367
|
-
return awaitOnExit(
|
|
1368
|
-
this.request(endpoint, payload, { method: "PATCH" })
|
|
1369
|
-
).catch((error) => {
|
|
2359
|
+
if (missing.length > 0) {
|
|
1370
2360
|
try {
|
|
1371
|
-
console.error(
|
|
2361
|
+
console.error(
|
|
2362
|
+
`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.`
|
|
2363
|
+
);
|
|
1372
2364
|
} catch {
|
|
1373
2365
|
}
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Start a replay session by fetching historical traces.
|
|
1378
|
-
* Blocking call - creates a test run and returns lightweight item references.
|
|
1379
|
-
*/
|
|
1380
|
-
async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetId, graderIds, dbBranchSettings) {
|
|
1381
|
-
const payload = { traceFunctionKey };
|
|
1382
|
-
if (limit !== void 0) {
|
|
1383
|
-
payload.limit = limit;
|
|
1384
|
-
}
|
|
1385
|
-
if (traceIds) {
|
|
1386
|
-
payload.traceIds = traceIds;
|
|
1387
|
-
}
|
|
1388
|
-
if (name !== void 0) {
|
|
1389
|
-
payload.name = name;
|
|
1390
2366
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
if (graderIds !== void 0) {
|
|
1408
|
-
payload.graderIds = graderIds;
|
|
1409
|
-
}
|
|
1410
|
-
if (dbBranchSettings !== void 0) {
|
|
1411
|
-
payload.dbBranchSettings = dbBranchSettings;
|
|
1412
|
-
}
|
|
1413
|
-
const timeout = includeDbBranchLease ? REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS : 3e4;
|
|
1414
|
-
return this.request("/api/sdk/replay/start", payload, {
|
|
1415
|
-
timeout
|
|
2367
|
+
}
|
|
2368
|
+
const result = {
|
|
2369
|
+
items: resultItems,
|
|
2370
|
+
testRunId,
|
|
2371
|
+
testRunUrl: `${serviceUrl}${testRunUrl}`
|
|
2372
|
+
};
|
|
2373
|
+
await writeReplayResultFile(result);
|
|
2374
|
+
try {
|
|
2375
|
+
options?.onProgress?.({
|
|
2376
|
+
type: "complete",
|
|
2377
|
+
testRunId,
|
|
2378
|
+
completed: total,
|
|
2379
|
+
total,
|
|
2380
|
+
succeeded,
|
|
2381
|
+
errored,
|
|
2382
|
+
result
|
|
1416
2383
|
});
|
|
2384
|
+
} catch {
|
|
1417
2385
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const controller = new AbortController();
|
|
1425
|
-
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
1426
|
-
try {
|
|
1427
|
-
const response = await fetch(url, {
|
|
1428
|
-
method: "GET",
|
|
1429
|
-
headers: { Authorization: `Bearer ${this.resolveApiKey() ?? ""}` },
|
|
1430
|
-
signal: controller.signal
|
|
1431
|
-
});
|
|
1432
|
-
if (!response.ok) {
|
|
1433
|
-
const errorText = await response.text();
|
|
1434
|
-
throw new BitfabError(
|
|
1435
|
-
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1436
|
-
);
|
|
1437
|
-
}
|
|
1438
|
-
return await response.json();
|
|
1439
|
-
} catch (error) {
|
|
1440
|
-
if (error instanceof BitfabError) {
|
|
1441
|
-
throw error;
|
|
1442
|
-
}
|
|
1443
|
-
if (error instanceof Error) {
|
|
1444
|
-
if (error.name === "AbortError") {
|
|
1445
|
-
throw new BitfabError("Request timed out after 30000ms");
|
|
1446
|
-
}
|
|
1447
|
-
throw new BitfabError(error.message);
|
|
1448
|
-
}
|
|
1449
|
-
throw new BitfabError("Unknown error occurred");
|
|
1450
|
-
} finally {
|
|
1451
|
-
clearTimeout(timeoutId);
|
|
1452
|
-
}
|
|
2386
|
+
return result;
|
|
2387
|
+
}
|
|
2388
|
+
async function writeReplayResultFile(result) {
|
|
2389
|
+
const resultPath = typeof process !== "undefined" ? process.env?.BITFAB_REPLAY_RESULT_PATH : void 0;
|
|
2390
|
+
if (!resultPath) {
|
|
2391
|
+
return;
|
|
1453
2392
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
const query = options?.includeOutputs === false ? "?includeOutputs=false" : "";
|
|
1464
|
-
const url = `${this.serviceUrl}/api/sdk/replay/spanTree/${externalSpanId}${query}`;
|
|
1465
|
-
const controller = new AbortController();
|
|
1466
|
-
const timeoutId = setTimeout(() => controller.abort(), 3e4);
|
|
2393
|
+
try {
|
|
2394
|
+
const [{ dirname }, { mkdir, writeFile }] = await Promise.all([
|
|
2395
|
+
import("path"),
|
|
2396
|
+
import("fs/promises")
|
|
2397
|
+
]);
|
|
2398
|
+
await mkdir(dirname(resultPath), { recursive: true });
|
|
2399
|
+
await writeFile(resultPath, `${JSON.stringify(result, null, 2)}
|
|
2400
|
+
`);
|
|
2401
|
+
} catch (err) {
|
|
1467
2402
|
try {
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
});
|
|
1473
|
-
if (!response.ok) {
|
|
1474
|
-
const errorText = await response.text();
|
|
1475
|
-
throw new BitfabError(
|
|
1476
|
-
`HTTP ${response.status}: ${errorText.slice(0, 500)}`
|
|
1477
|
-
);
|
|
1478
|
-
}
|
|
1479
|
-
return await response.json();
|
|
1480
|
-
} catch (error) {
|
|
1481
|
-
if (error instanceof BitfabError) {
|
|
1482
|
-
throw error;
|
|
1483
|
-
}
|
|
1484
|
-
if (error instanceof Error) {
|
|
1485
|
-
if (error.name === "AbortError") {
|
|
1486
|
-
throw new BitfabError("Request timed out after 30000ms");
|
|
1487
|
-
}
|
|
1488
|
-
throw new BitfabError(error.message);
|
|
1489
|
-
}
|
|
1490
|
-
throw new BitfabError("Unknown error occurred");
|
|
1491
|
-
} finally {
|
|
1492
|
-
clearTimeout(timeoutId);
|
|
2403
|
+
console.warn(
|
|
2404
|
+
`Bitfab: failed to write replay result to BITFAB_REPLAY_RESULT_PATH (${resultPath}): ${err instanceof Error ? err.message : String(err)}`
|
|
2405
|
+
);
|
|
2406
|
+
} catch {
|
|
1493
2407
|
}
|
|
1494
2408
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
);
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
*/
|
|
1512
|
-
async resolveDbBranchLease(testRunId, traceId, dbBranchSettings) {
|
|
1513
|
-
return this.request(
|
|
1514
|
-
"/api/sdk/replay/resolveDbBranchLease",
|
|
1515
|
-
{ testRunId, traceId, dbBranchSettings },
|
|
1516
|
-
{ timeout: REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS }
|
|
1517
|
-
);
|
|
1518
|
-
}
|
|
1519
|
-
/** Release a previously-resolved DB branch by deleting its Neon branch. Idempotent server-side. */
|
|
1520
|
-
async releaseDbBranchLease(neonBranchId) {
|
|
1521
|
-
await this.request(
|
|
1522
|
-
"/api/sdk/replay/releaseDbBranchLease",
|
|
1523
|
-
{ neonBranchId },
|
|
1524
|
-
{ timeout: 3e4 }
|
|
1525
|
-
);
|
|
2409
|
+
}
|
|
2410
|
+
var REPLAY_PERSISTENCE_TIMEOUT_MS, BITFAB_PROGRESS_PREFIX;
|
|
2411
|
+
var init_replay = __esm({
|
|
2412
|
+
"src/replay.ts"() {
|
|
2413
|
+
"use strict";
|
|
2414
|
+
init_codeChange();
|
|
2415
|
+
init_errors();
|
|
2416
|
+
init_http();
|
|
2417
|
+
init_mockOverride();
|
|
2418
|
+
init_randomUuid();
|
|
2419
|
+
init_replayContext();
|
|
2420
|
+
init_serialize();
|
|
2421
|
+
init_transport();
|
|
2422
|
+
init_unrefTimer();
|
|
2423
|
+
REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
|
|
2424
|
+
BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
|
|
1526
2425
|
}
|
|
1527
|
-
};
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
// src/index.ts
|
|
2429
|
+
var index_exports = {};
|
|
2430
|
+
__export(index_exports, {
|
|
2431
|
+
BITFAB_PROGRESS_PREFIX: () => BITFAB_PROGRESS_PREFIX,
|
|
2432
|
+
Bitfab: () => Bitfab,
|
|
2433
|
+
BitfabClaudeAgentHandler: () => BitfabClaudeAgentHandler,
|
|
2434
|
+
BitfabError: () => BitfabError,
|
|
2435
|
+
BitfabFunction: () => BitfabFunction,
|
|
2436
|
+
BitfabLangChainCallbackHandler: () => BitfabLangGraphCallbackHandler,
|
|
2437
|
+
BitfabLangGraphCallbackHandler: () => BitfabLangGraphCallbackHandler,
|
|
2438
|
+
BitfabOpenAIAgentHandler: () => BitfabOpenAIAgentHandler,
|
|
2439
|
+
BitfabOpenAITracingProcessor: () => BitfabOpenAITracingProcessor,
|
|
2440
|
+
BitfabVercelAiHandler: () => BitfabVercelAiHandler,
|
|
2441
|
+
DEFAULT_SERVICE_URL: () => DEFAULT_SERVICE_URL,
|
|
2442
|
+
SUPPORTED_PROVIDERS: () => SUPPORTED_PROVIDERS,
|
|
2443
|
+
__version__: () => __version__,
|
|
2444
|
+
finalizers: () => finalizers,
|
|
2445
|
+
flushTraces: () => flushTraces,
|
|
2446
|
+
getCurrentReplayBranch: () => getCurrentReplayBranch,
|
|
2447
|
+
getCurrentSpan: () => getCurrentSpan,
|
|
2448
|
+
getCurrentTrace: () => getCurrentTrace,
|
|
2449
|
+
reportReplayProgress: () => reportReplayProgress
|
|
2450
|
+
});
|
|
2451
|
+
module.exports = __toCommonJS(index_exports);
|
|
2452
|
+
|
|
2453
|
+
// src/claudeAgentSdk.ts
|
|
2454
|
+
init_constants();
|
|
2455
|
+
init_http();
|
|
1528
2456
|
|
|
1529
2457
|
// src/processorPayload.ts
|
|
1530
2458
|
init_serialize();
|
|
@@ -1649,7 +2577,8 @@ var BitfabClaudeAgentHandler = class {
|
|
|
1649
2577
|
// its root. The prompt is not present anywhere in the message stream, so it
|
|
1650
2578
|
// must be handed in explicitly.
|
|
1651
2579
|
this.hasRootInput = false;
|
|
1652
|
-
this.
|
|
2580
|
+
this.ownsHttpClient = config._httpClient === void 0;
|
|
2581
|
+
this.httpClient = config._httpClient ?? new HttpClient({
|
|
1653
2582
|
apiKey: config.apiKey,
|
|
1654
2583
|
serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
|
|
1655
2584
|
timeout: config.timeout ?? 1e4
|
|
@@ -1662,6 +2591,14 @@ var BitfabClaudeAgentHandler = class {
|
|
|
1662
2591
|
this.subagentStartHook = this.subagentStartHook.bind(this);
|
|
1663
2592
|
this.subagentStopHook = this.subagentStopHook.bind(this);
|
|
1664
2593
|
}
|
|
2594
|
+
/**
|
|
2595
|
+
* Flush and release the span transport this handler started. A no-op when
|
|
2596
|
+
* the handler borrowed a `Bitfab` client's HTTP client: that client's
|
|
2597
|
+
* `close()` owns the worker's lifetime.
|
|
2598
|
+
*/
|
|
2599
|
+
async close(timeoutMs) {
|
|
2600
|
+
return this.ownsHttpClient ? this.httpClient.close(timeoutMs) : true;
|
|
2601
|
+
}
|
|
1665
2602
|
// ── trace lifecycle ──────────────────────────────────────────
|
|
1666
2603
|
ensureTrace() {
|
|
1667
2604
|
if (this.traceId !== null) {
|
|
@@ -2432,6 +3369,9 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
|
|
|
2432
3369
|
};
|
|
2433
3370
|
}
|
|
2434
3371
|
|
|
3372
|
+
// src/client.ts
|
|
3373
|
+
init_constants();
|
|
3374
|
+
|
|
2435
3375
|
// src/dbSnapshot.ts
|
|
2436
3376
|
init_errors();
|
|
2437
3377
|
var SUPPORTED_PROVIDERS = ["neon"];
|
|
@@ -2449,7 +3389,12 @@ function buildSnapshotRef(config, sdkWallClockBeforeFn) {
|
|
|
2449
3389
|
};
|
|
2450
3390
|
}
|
|
2451
3391
|
|
|
3392
|
+
// src/client.ts
|
|
3393
|
+
init_http();
|
|
3394
|
+
|
|
2452
3395
|
// src/langgraph.ts
|
|
3396
|
+
init_constants();
|
|
3397
|
+
init_http();
|
|
2453
3398
|
init_randomUuid();
|
|
2454
3399
|
init_serialize();
|
|
2455
3400
|
var LANGSMITH_HIDDEN_TAG = "langsmith:hidden";
|
|
@@ -2673,7 +3618,8 @@ var BitfabLangGraphCallbackHandler = class {
|
|
|
2673
3618
|
this.ignoreCustomEvent = true;
|
|
2674
3619
|
this.runToSpan = /* @__PURE__ */ new Map();
|
|
2675
3620
|
this.invocations = /* @__PURE__ */ new Map();
|
|
2676
|
-
this.
|
|
3621
|
+
this.ownsHttpClient = config._httpClient === void 0;
|
|
3622
|
+
this.httpClient = config._httpClient ?? new HttpClient({
|
|
2677
3623
|
apiKey: config.apiKey,
|
|
2678
3624
|
serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
|
|
2679
3625
|
timeout: config.timeout ?? 1e4
|
|
@@ -2681,6 +3627,14 @@ var BitfabLangGraphCallbackHandler = class {
|
|
|
2681
3627
|
this.traceFunctionKey = config.traceFunctionKey;
|
|
2682
3628
|
this.getActiveSpanContext = config.getActiveSpanContext ?? null;
|
|
2683
3629
|
}
|
|
3630
|
+
/**
|
|
3631
|
+
* Flush and release the span transport this handler started. A no-op when
|
|
3632
|
+
* the handler borrowed a `Bitfab` client's HTTP client: that client's
|
|
3633
|
+
* `close()` owns the worker's lifetime.
|
|
3634
|
+
*/
|
|
3635
|
+
async close(timeoutMs) {
|
|
3636
|
+
return this.ownsHttpClient ? this.httpClient.close(timeoutMs) : true;
|
|
3637
|
+
}
|
|
2684
3638
|
// ── lifecycle helpers ──────────────────────────────────────────
|
|
2685
3639
|
startSpan(runId, parentRunId, name, spanType, inputData, metadata, tags) {
|
|
2686
3640
|
const parentSpan = parentRunId ? this.runToSpan.get(parentRunId) : void 0;
|
|
@@ -3152,6 +4106,8 @@ init_replayContext();
|
|
|
3152
4106
|
init_serialize();
|
|
3153
4107
|
|
|
3154
4108
|
// src/tracing.ts
|
|
4109
|
+
init_constants();
|
|
4110
|
+
init_http();
|
|
3155
4111
|
init_randomUuid();
|
|
3156
4112
|
var BitfabOpenAITracingProcessor = class {
|
|
3157
4113
|
/**
|
|
@@ -3163,7 +4119,8 @@ var BitfabOpenAITracingProcessor = class {
|
|
|
3163
4119
|
this.activeTraces = {};
|
|
3164
4120
|
this.activeSpanMappings = {};
|
|
3165
4121
|
this.canonicalTraceIds = {};
|
|
3166
|
-
this.
|
|
4122
|
+
this.ownsHttpClient = config._httpClient === void 0;
|
|
4123
|
+
this.httpClient = config._httpClient ?? new HttpClient({
|
|
3167
4124
|
apiKey: config.apiKey,
|
|
3168
4125
|
serviceUrl: config.serviceUrl ?? DEFAULT_SERVICE_URL,
|
|
3169
4126
|
timeout: config.timeout ?? 1e4
|
|
@@ -3179,6 +4136,14 @@ var BitfabOpenAITracingProcessor = class {
|
|
|
3179
4136
|
this.canonicalTraceIds[sourceTraceId] = created;
|
|
3180
4137
|
return created;
|
|
3181
4138
|
}
|
|
4139
|
+
/**
|
|
4140
|
+
* Flush and release the span transport this processor started. A no-op when
|
|
4141
|
+
* the processor borrowed a `Bitfab` client's HTTP client: that client's
|
|
4142
|
+
* `close()` owns the worker's lifetime.
|
|
4143
|
+
*/
|
|
4144
|
+
async close(timeoutMs) {
|
|
4145
|
+
return this.ownsHttpClient ? this.httpClient.close(timeoutMs) : true;
|
|
4146
|
+
}
|
|
3182
4147
|
/**
|
|
3183
4148
|
* Called when a trace is started.
|
|
3184
4149
|
* If there's an active withSpan context, the trace ID is remapped to the
|
|
@@ -3233,14 +4198,16 @@ var BitfabOpenAITracingProcessor = class {
|
|
|
3233
4198
|
* Called when a trace is being flushed.
|
|
3234
4199
|
*/
|
|
3235
4200
|
async forceFlush() {
|
|
4201
|
+
await this.httpClient.waitForPendingRequests();
|
|
3236
4202
|
}
|
|
3237
4203
|
/**
|
|
3238
4204
|
* Called when the trace processor is shutting down.
|
|
3239
4205
|
*/
|
|
3240
|
-
async shutdown(
|
|
4206
|
+
async shutdown(timeout) {
|
|
3241
4207
|
this.activeTraces = {};
|
|
3242
4208
|
this.activeSpanMappings = {};
|
|
3243
4209
|
this.canonicalTraceIds = {};
|
|
4210
|
+
await this.close(timeout);
|
|
3244
4211
|
}
|
|
3245
4212
|
/**
|
|
3246
4213
|
* Send trace to Bitfab API (fire-and-forget).
|
|
@@ -3507,7 +4474,6 @@ var BitfabVercelAiHandler = class {
|
|
|
3507
4474
|
// src/client.ts
|
|
3508
4475
|
init_warnOnce();
|
|
3509
4476
|
var activeTraceStates = /* @__PURE__ */ new Map();
|
|
3510
|
-
var pendingSpanPromises = /* @__PURE__ */ new Map();
|
|
3511
4477
|
var asyncLocalStorage = null;
|
|
3512
4478
|
var SPAN_CONTEXT_STORAGE_SYMBOL = /* @__PURE__ */ Symbol.for("bitfab.spanContextStorage");
|
|
3513
4479
|
var initializeAsyncContext = () => {
|
|
@@ -3825,7 +4791,7 @@ function getCurrentTrace() {
|
|
|
3825
4791
|
}
|
|
3826
4792
|
};
|
|
3827
4793
|
}
|
|
3828
|
-
function
|
|
4794
|
+
function readEnv2(name) {
|
|
3829
4795
|
if (typeof process !== "undefined" && process.env) {
|
|
3830
4796
|
return process.env[name];
|
|
3831
4797
|
}
|
|
@@ -3863,6 +4829,23 @@ var Bitfab = class {
|
|
|
3863
4829
|
timeout: this.timeout
|
|
3864
4830
|
});
|
|
3865
4831
|
}
|
|
4832
|
+
/**
|
|
4833
|
+
* Flush and permanently close this client's tracing resources: its pending
|
|
4834
|
+
* requests and the single span-transport worker shared by its decorators and
|
|
4835
|
+
* framework handlers.
|
|
4836
|
+
*
|
|
4837
|
+
* Resolves `false` when delivery failed or the deadline expired. Long-lived
|
|
4838
|
+
* processes never need this (the transport batches in the background and the
|
|
4839
|
+
* exit hook drains it); scripts and tests that want a hard guarantee should
|
|
4840
|
+
* await it.
|
|
4841
|
+
*
|
|
4842
|
+
* Deliberately not a `Symbol.asyncDispose` method: the SDK targets runtimes
|
|
4843
|
+
* where that symbol may be absent, and a computed key on a missing symbol
|
|
4844
|
+
* throws at class-definition time, taking the whole SDK down on load.
|
|
4845
|
+
*/
|
|
4846
|
+
close(timeoutMs) {
|
|
4847
|
+
return this.httpClient.close(timeoutMs);
|
|
4848
|
+
}
|
|
3866
4849
|
/**
|
|
3867
4850
|
* Resolve the API key lazily, the first time a span actually needs it.
|
|
3868
4851
|
*
|
|
@@ -3883,7 +4866,7 @@ var Bitfab = class {
|
|
|
3883
4866
|
return this.resolvedApiKey;
|
|
3884
4867
|
}
|
|
3885
4868
|
const fromConfig = typeof this.apiKeyConfig === "function" ? this.apiKeyConfig() : this.apiKeyConfig;
|
|
3886
|
-
const candidate = fromConfig && fromConfig.trim() !== "" ? fromConfig :
|
|
4869
|
+
const candidate = fromConfig && fromConfig.trim() !== "" ? fromConfig : readEnv2("BITFAB_API_KEY");
|
|
3887
4870
|
const key = candidate && candidate.trim() !== "" ? candidate : void 0;
|
|
3888
4871
|
if (key) {
|
|
3889
4872
|
this.resolvedApiKey = key;
|
|
@@ -4012,7 +4995,8 @@ var Bitfab = class {
|
|
|
4012
4995
|
getActiveSpanContext: () => {
|
|
4013
4996
|
const stack = getSpanStack();
|
|
4014
4997
|
return stack[stack.length - 1] ?? null;
|
|
4015
|
-
}
|
|
4998
|
+
},
|
|
4999
|
+
_httpClient: this.httpClient
|
|
4016
5000
|
});
|
|
4017
5001
|
}
|
|
4018
5002
|
/**
|
|
@@ -4072,7 +5056,8 @@ var Bitfab = class {
|
|
|
4072
5056
|
getActiveSpanContext: () => {
|
|
4073
5057
|
const stack = getSpanStack();
|
|
4074
5058
|
return stack[stack.length - 1] ?? null;
|
|
4075
|
-
}
|
|
5059
|
+
},
|
|
5060
|
+
_httpClient: this.httpClient
|
|
4076
5061
|
});
|
|
4077
5062
|
}
|
|
4078
5063
|
/**
|
|
@@ -4124,7 +5109,8 @@ var Bitfab = class {
|
|
|
4124
5109
|
getActiveSpanContext: () => {
|
|
4125
5110
|
const stack = getSpanStack();
|
|
4126
5111
|
return stack[stack.length - 1] ?? null;
|
|
4127
|
-
}
|
|
5112
|
+
},
|
|
5113
|
+
_httpClient: this.httpClient
|
|
4128
5114
|
});
|
|
4129
5115
|
}
|
|
4130
5116
|
/**
|
|
@@ -4377,7 +5363,6 @@ var Bitfab = class {
|
|
|
4377
5363
|
},
|
|
4378
5364
|
dbSnapshotRef
|
|
4379
5365
|
});
|
|
4380
|
-
pendingSpanPromises.set(traceId, []);
|
|
4381
5366
|
registeredTraceId = traceId;
|
|
4382
5367
|
}
|
|
4383
5368
|
const functionName = fn.name !== "" ? fn.name : void 0;
|
|
@@ -4392,57 +5377,29 @@ var Bitfab = class {
|
|
|
4392
5377
|
startedAt,
|
|
4393
5378
|
spanType: options.type ?? "custom"
|
|
4394
5379
|
};
|
|
4395
|
-
const sendSpan = async (params
|
|
5380
|
+
const sendSpan = async (params) => {
|
|
4396
5381
|
const replayCtx = getReplayContext();
|
|
4397
|
-
const persistenceCollector = isRootSpan ? replayCtx?.pendingPersistence : void 0;
|
|
4398
|
-
let resolvePersistence;
|
|
4399
|
-
if (persistenceCollector && !spanOpts?.skipPersistenceRegistration) {
|
|
4400
|
-
persistenceCollector.push(
|
|
4401
|
-
new Promise((resolve) => {
|
|
4402
|
-
resolvePersistence = resolve;
|
|
4403
|
-
})
|
|
4404
|
-
);
|
|
4405
|
-
}
|
|
4406
5382
|
try {
|
|
4407
5383
|
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4408
5384
|
const traceDropped = activeTraceStates.get(traceId)?.dropped === true;
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
testRunId
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
inputSourceSpanId
|
|
4420
|
-
|
|
4421
|
-
});
|
|
4422
|
-
if (isRootSpan) {
|
|
4423
|
-
const pending = pendingSpanPromises.get(traceId) ?? [];
|
|
4424
|
-
pending.push(spanPromise);
|
|
4425
|
-
if (persistenceCollector) {
|
|
4426
|
-
await Promise.allSettled(pending);
|
|
4427
|
-
} else {
|
|
4428
|
-
let raceTimer;
|
|
4429
|
-
try {
|
|
4430
|
-
await Promise.race([
|
|
4431
|
-
Promise.allSettled(pending),
|
|
4432
|
-
new Promise((resolve) => {
|
|
4433
|
-
raceTimer = setTimeout(resolve, 5e3);
|
|
4434
|
-
unrefTimer(raceTimer);
|
|
4435
|
-
})
|
|
4436
|
-
]);
|
|
4437
|
-
} finally {
|
|
4438
|
-
if (raceTimer) {
|
|
4439
|
-
clearTimeout(raceTimer);
|
|
4440
|
-
}
|
|
5385
|
+
if (!traceDropped) {
|
|
5386
|
+
self.sendWrapperSpan({
|
|
5387
|
+
...baseSpanParams,
|
|
5388
|
+
...params,
|
|
5389
|
+
contexts: newContext.contexts,
|
|
5390
|
+
prompt: newContext.prompt,
|
|
5391
|
+
endedAt,
|
|
5392
|
+
...replayCtx?.testRunId && {
|
|
5393
|
+
testRunId: replayCtx.testRunId
|
|
5394
|
+
},
|
|
5395
|
+
...replayCtx?.inputSourceSpanId && {
|
|
5396
|
+
inputSourceSpanId: replayCtx.inputSourceSpanId
|
|
4441
5397
|
}
|
|
4442
|
-
}
|
|
4443
|
-
|
|
5398
|
+
});
|
|
5399
|
+
}
|
|
5400
|
+
if (isRootSpan) {
|
|
4444
5401
|
const traceState = activeTraceStates.get(traceId);
|
|
4445
|
-
|
|
5402
|
+
self.sendTraceCompletion({
|
|
4446
5403
|
traceFunctionKey,
|
|
4447
5404
|
traceId,
|
|
4448
5405
|
startedAt: traceState?.startedAt ?? startedAt,
|
|
@@ -4468,20 +5425,8 @@ var Bitfab = class {
|
|
|
4468
5425
|
}
|
|
4469
5426
|
});
|
|
4470
5427
|
activeTraceStates.delete(traceId);
|
|
4471
|
-
if (persistenceCollector) {
|
|
4472
|
-
await completionPromise;
|
|
4473
|
-
}
|
|
4474
|
-
} else {
|
|
4475
|
-
const pending = pendingSpanPromises.get(traceId);
|
|
4476
|
-
if (pending) {
|
|
4477
|
-
pending.push(spanPromise);
|
|
4478
|
-
} else {
|
|
4479
|
-
pendingSpanPromises.set(traceId, [spanPromise]);
|
|
4480
|
-
}
|
|
4481
5428
|
}
|
|
4482
5429
|
} catch {
|
|
4483
|
-
} finally {
|
|
4484
|
-
resolvePersistence?.();
|
|
4485
5430
|
}
|
|
4486
5431
|
};
|
|
4487
5432
|
const replayCtxForMock = getReplayContext();
|
|
@@ -4565,30 +5510,14 @@ var Bitfab = class {
|
|
|
4565
5510
|
}
|
|
4566
5511
|
const recordSpan = (result) => {
|
|
4567
5512
|
if (options.finalize) {
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
if (persistenceCollector) {
|
|
4572
|
-
persistenceCollector.push(
|
|
4573
|
-
new Promise((resolve) => {
|
|
4574
|
-
resolvePersistence = resolve;
|
|
4575
|
-
})
|
|
4576
|
-
);
|
|
4577
|
-
}
|
|
4578
|
-
void Promise.resolve().then(() => options.finalize(result)).then(
|
|
4579
|
-
(output) => sendSpan(
|
|
4580
|
-
{ result: output },
|
|
4581
|
-
{ skipPersistenceRegistration: true }
|
|
4582
|
-
)
|
|
4583
|
-
).catch(
|
|
4584
|
-
(error) => sendSpan(
|
|
4585
|
-
{
|
|
5513
|
+
void self.httpClient.trackDeferred(
|
|
5514
|
+
Promise.resolve().then(() => options.finalize(result)).then((output) => sendSpan({ result: output })).catch(
|
|
5515
|
+
(error) => sendSpan({
|
|
4586
5516
|
result: void 0,
|
|
4587
5517
|
error: error instanceof Error ? `finalize failed: ${error.message}` : `finalize failed: ${String(error)}`
|
|
4588
|
-
}
|
|
4589
|
-
{ skipPersistenceRegistration: true }
|
|
5518
|
+
})
|
|
4590
5519
|
)
|
|
4591
|
-
)
|
|
5520
|
+
);
|
|
4592
5521
|
} else {
|
|
4593
5522
|
void sendSpan({ result });
|
|
4594
5523
|
}
|
|
@@ -4616,7 +5545,6 @@ var Bitfab = class {
|
|
|
4616
5545
|
} catch (setupError) {
|
|
4617
5546
|
if (registeredTraceId) {
|
|
4618
5547
|
activeTraceStates.delete(registeredTraceId);
|
|
4619
|
-
pendingSpanPromises.delete(registeredTraceId);
|
|
4620
5548
|
}
|
|
4621
5549
|
if (getReplayContext()) {
|
|
4622
5550
|
throw setupError;
|
|
@@ -4739,7 +5667,7 @@ var Bitfab = class {
|
|
|
4739
5667
|
/**
|
|
4740
5668
|
* Send trace completion when a root span ends.
|
|
4741
5669
|
* Internal method to record trace completion with end time.
|
|
4742
|
-
*
|
|
5670
|
+
* Queued on the client's span transport; delivery is the transport's job.
|
|
4743
5671
|
*/
|
|
4744
5672
|
sendTraceCompletion(params) {
|
|
4745
5673
|
const rawTrace = {
|
|
@@ -4775,7 +5703,7 @@ var Bitfab = class {
|
|
|
4775
5703
|
accessed: params.dbSnapshotUsage.accessed
|
|
4776
5704
|
};
|
|
4777
5705
|
}
|
|
4778
|
-
|
|
5706
|
+
this.httpClient.sendExternalTrace({
|
|
4779
5707
|
id: params.traceId,
|
|
4780
5708
|
type: "sdk-function",
|
|
4781
5709
|
source: "typescript-sdk-function",
|
|
@@ -4790,7 +5718,7 @@ var Bitfab = class {
|
|
|
4790
5718
|
/**
|
|
4791
5719
|
* Send a wrapper span from function execution.
|
|
4792
5720
|
* Internal method to record spans when using withSpan.
|
|
4793
|
-
*
|
|
5721
|
+
* Queued on the client's span transport; delivery is the transport's job.
|
|
4794
5722
|
*/
|
|
4795
5723
|
sendWrapperSpan(params) {
|
|
4796
5724
|
const serializedInputs = serializeValue(params.inputs);
|
|
@@ -4831,7 +5759,7 @@ var Bitfab = class {
|
|
|
4831
5759
|
if (params.inputSourceSpanId) {
|
|
4832
5760
|
externalSpan.input_source_span_id = params.inputSourceSpanId;
|
|
4833
5761
|
}
|
|
4834
|
-
|
|
5762
|
+
this.httpClient.sendExternalSpan({
|
|
4835
5763
|
id: params.spanId,
|
|
4836
5764
|
traceId: params.traceId,
|
|
4837
5765
|
type: "sdk-function",
|
|
@@ -5020,6 +5948,9 @@ var BitfabFunction = class {
|
|
|
5020
5948
|
}
|
|
5021
5949
|
};
|
|
5022
5950
|
|
|
5951
|
+
// src/index.ts
|
|
5952
|
+
init_constants();
|
|
5953
|
+
|
|
5023
5954
|
// src/finalizers.ts
|
|
5024
5955
|
async function settle(value) {
|
|
5025
5956
|
try {
|
|
@@ -5069,6 +6000,7 @@ var finalizers = {
|
|
|
5069
6000
|
};
|
|
5070
6001
|
|
|
5071
6002
|
// src/index.ts
|
|
6003
|
+
init_http();
|
|
5072
6004
|
init_replay();
|
|
5073
6005
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5074
6006
|
0 && (module.exports = {
|