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