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