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