@bitfab/sdk 0.36.7 → 0.36.9

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.
@@ -199,10 +199,11 @@ function looksBinary(s) {
199
199
 
200
200
  // src/errors.ts
201
201
  var BitfabError = class extends Error {
202
- constructor(message, url, status) {
202
+ constructor(message, url, status, retryAfterMs) {
203
203
  super(message);
204
204
  this.url = url;
205
205
  this.status = status;
206
+ this.retryAfterMs = retryAfterMs;
206
207
  this.name = "BitfabError";
207
208
  }
208
209
  };
@@ -303,7 +304,7 @@ function encodeRequestBody(body) {
303
304
  }
304
305
 
305
306
  // src/version.generated.ts
306
- var __version__ = "0.36.7";
307
+ var __version__ = "0.36.9";
307
308
 
308
309
  // src/constants.ts
309
310
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
@@ -643,6 +644,17 @@ import {
643
644
  BatchSpanProcessor
644
645
  } from "@opentelemetry/sdk-trace-base";
645
646
 
647
+ // src/transportTypes.ts
648
+ var DeliveryError = class extends Error {
649
+ constructor(message, options = {}) {
650
+ super(message);
651
+ this.name = "DeliveryError";
652
+ this.retryable = options.retryable ?? false;
653
+ this.oversized = options.oversized ?? false;
654
+ this.retryAfterMs = options.retryAfterMs;
655
+ }
656
+ };
657
+
646
658
  // src/unrefTimer.ts
647
659
  function unrefTimer(timer) {
648
660
  const handle = timer;
@@ -654,7 +666,6 @@ function unrefTimer(timer) {
654
666
  // src/otel.ts
655
667
  var OPERATION_ATTRIBUTE = "bitfab.operation";
656
668
  var PAYLOAD_ATTRIBUTE = "bitfab.payload";
657
- var OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
658
669
  var MAX_EXPORT_REQUEST_BYTES = 3e6;
659
670
  var MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
660
671
  var MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
@@ -666,14 +677,12 @@ var DEFAULT_EXPORT_CONCURRENCY = 32;
666
677
  var MAX_EXPORT_CONCURRENCY = 64;
667
678
  var SCHEDULE_DELAY_MILLIS = 5e3;
668
679
  var EXPORT_TIMEOUT_MILLIS = 3e4;
669
- var RETRY_DELAY_MILLIS = 100;
680
+ var RETRY_BASE_DELAY_MILLIS = 100;
681
+ var RETRY_BACKOFF_CEILING_MILLIS = 5e3;
670
682
  var MAX_SEND_ATTEMPTS = 3;
671
683
  var DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
672
- var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
673
684
  var liveTransports = /* @__PURE__ */ new Set();
674
- var traceSubmissionSpanIds = /* @__PURE__ */ new Map();
675
- var replayTraceSubmissions = /* @__PURE__ */ new Set();
676
- var submissionCounter = 0;
685
+ var carrierRefs = /* @__PURE__ */ new WeakMap();
677
686
  function readBoundedIntEnv(name, max, fallback, warnKey) {
678
687
  const raw = readEnv(name);
679
688
  if (raw === void 0) {
@@ -699,59 +708,6 @@ function logError(message, error) {
699
708
  } catch {
700
709
  }
701
710
  }
702
- function recordTraceSubmission(operation, payload) {
703
- const sourceTraceId = resolveSourceTraceId(payload);
704
- if (sourceTraceId === void 0) {
705
- return;
706
- }
707
- if (operation === "external_span") {
708
- const rawSpan = asRecord2(payload.rawSpan);
709
- if (typeof rawSpan?.id !== "string") {
710
- submissionCounter += 1;
711
- }
712
- const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
713
- const existing = traceSubmissionSpanIds.get(sourceTraceId);
714
- if (existing) {
715
- existing.add(sourceSpanId);
716
- } else {
717
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
718
- }
719
- return;
720
- }
721
- if (payload.completed !== true) {
722
- return;
723
- }
724
- if (typeof payload.testRunId === "string") {
725
- replayTraceSubmissions.add(sourceTraceId);
726
- if (!traceSubmissionSpanIds.has(sourceTraceId)) {
727
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
728
- }
729
- } else {
730
- traceSubmissionSpanIds.delete(sourceTraceId);
731
- }
732
- }
733
- function takeReplaySpanCounts(traceIds) {
734
- const counts = {};
735
- for (const traceId of traceIds) {
736
- if (!replayTraceSubmissions.has(traceId)) {
737
- continue;
738
- }
739
- counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
740
- traceSubmissionSpanIds.delete(traceId);
741
- replayTraceSubmissions.delete(traceId);
742
- }
743
- return counts;
744
- }
745
- function asRecord2(value) {
746
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
747
- }
748
- function resolveSourceTraceId(payload) {
749
- if (typeof payload.sourceTraceId === "string") {
750
- return payload.sourceTraceId;
751
- }
752
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
753
- return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
754
- }
755
711
  function otlpValue(value) {
756
712
  if (typeof value === "boolean") {
757
713
  return { boolValue: value };
@@ -810,7 +766,11 @@ function spanToOtlp(span) {
810
766
  var SPAN_SEPARATOR_BYTES = 1;
811
767
  function encodeSpan(span) {
812
768
  const json = JSON.stringify(spanToOtlp(span));
813
- return { json, size: byteLength(json) };
769
+ return {
770
+ json,
771
+ size: byteLength(json),
772
+ ref: carrierRefs.get(span)
773
+ };
814
774
  }
815
775
  function trimEncodedSpan(span) {
816
776
  try {
@@ -893,26 +853,35 @@ async function mapWithConcurrency(items, limit, task) {
893
853
  await Promise.all(workers);
894
854
  return results;
895
855
  }
896
- var OtlpPayloadTooLargeError = class extends Error {
897
- };
898
- var OtlpPartialSuccessError = class extends Error {
899
- };
900
- function responseStatus(error) {
901
- return error instanceof BitfabError ? error.status : void 0;
902
- }
903
856
  function isRetryable(error) {
904
- const status = responseStatus(error);
905
- if (status === void 0) {
906
- return true;
857
+ return error instanceof DeliveryError && error.retryable;
858
+ }
859
+ function isOversized(error) {
860
+ return error instanceof DeliveryError && error.oversized;
861
+ }
862
+ function retryWaitMillis(error, attempt, remainingMillis) {
863
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
864
+ const affordable = remainingMillis / 2;
865
+ if (requested !== void 0) {
866
+ return requested < affordable ? requested : null;
907
867
  }
908
- return RETRYABLE_STATUSES.has(status) || status >= 500;
868
+ const backoff = Math.min(
869
+ RETRY_BASE_DELAY_MILLIS * 2 ** attempt,
870
+ RETRY_BACKOFF_CEILING_MILLIS
871
+ );
872
+ const jittered = backoff / 2 + Math.random() * (backoff / 2);
873
+ return jittered < affordable ? jittered : null;
909
874
  }
910
875
  var BitfabSpanExporter = class {
911
- constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
876
+ constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency, onDelivered, exportTimeoutMillis = EXPORT_TIMEOUT_MILLIS) {
912
877
  this.directSender = directSender;
913
878
  this.maxRequestBytes = maxRequestBytes;
914
879
  this.maxRequestBatchSize = maxRequestBatchSize;
915
880
  this.exportConcurrency = exportConcurrency;
881
+ this.onDelivered = onDelivered;
882
+ this.exportTimeoutMillis = exportTimeoutMillis;
883
+ /** Epoch ms until which the server has asked this exporter to stay away. */
884
+ this.throttledUntil = 0;
916
885
  }
917
886
  export(spans, resultCallback) {
918
887
  void this.exportAsync(spans).then(
@@ -978,6 +947,7 @@ var BitfabSpanExporter = class {
978
947
  );
979
948
  if (prepared.wireBytes <= this.maxRequestBytes) {
980
949
  await this.sendWithRetries(prepared);
950
+ this.reportDelivered(batch.spans);
981
951
  return true;
982
952
  }
983
953
  }
@@ -1005,15 +975,12 @@ var BitfabSpanExporter = class {
1005
975
  alreadyTrimmed = true;
1006
976
  }
1007
977
  } catch (error) {
1008
- if (error instanceof OtlpPayloadTooLargeError) {
978
+ if (isOversized(error)) {
1009
979
  logError(
1010
980
  batch.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"
1011
981
  );
1012
982
  return false;
1013
983
  }
1014
- if (error instanceof OtlpPartialSuccessError) {
1015
- return false;
1016
- }
1017
984
  logError("failed to export an OpenTelemetry span batch", error);
1018
985
  return false;
1019
986
  }
@@ -1031,37 +998,79 @@ var BitfabSpanExporter = class {
1031
998
  * the server does not yet understand. The fix is a client-supplied
1032
999
  * idempotency key that ingestion dedupes on.
1033
1000
  */
1001
+ /**
1002
+ * Remember a throttle the server asked for, so the requests fanned out
1003
+ * alongside this one respect it too. Delaying only the request that was
1004
+ * refused leaves the other seven in the window hitting a server that just
1005
+ * asked for room.
1006
+ */
1007
+ recordThrottle(error) {
1008
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
1009
+ if (requested !== void 0) {
1010
+ this.throttledUntil = Math.max(
1011
+ this.throttledUntil,
1012
+ Date.now() + requested
1013
+ );
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Waits out an active throttle, or reports the batch undeliverable when the
1018
+ * throttle outlasts what we are willing to hold it for. Either way nothing is
1019
+ * sent while the server has asked us to stay away.
1020
+ */
1021
+ async awaitThrottle(deadline) {
1022
+ const remaining = this.throttledUntil - Date.now();
1023
+ if (remaining <= 0) {
1024
+ return;
1025
+ }
1026
+ if (remaining >= (deadline - Date.now()) / 2) {
1027
+ throw new DeliveryError(
1028
+ `OTLP ingestion is throttled for another ${remaining}ms, longer than the export budget`
1029
+ );
1030
+ }
1031
+ await delay(remaining);
1032
+ }
1034
1033
  async sendWithRetries(request) {
1034
+ const deadline = Date.now() + this.exportTimeoutMillis;
1035
1035
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
1036
1036
  try {
1037
- const response = await this.directSender(
1038
- OTLP_TRACES_ENDPOINT,
1039
- request,
1040
- EXPORT_TIMEOUT_MILLIS
1041
- );
1042
- const partialSuccess = asRecord2(response?.partialSuccess);
1043
- const rejected = partialSuccess?.rejectedSpans;
1044
- if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1045
- logError(
1046
- `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1047
- );
1048
- throw new OtlpPartialSuccessError();
1049
- }
1037
+ await this.awaitThrottle(deadline);
1038
+ await this.directSender(request, Math.max(0, deadline - Date.now()));
1050
1039
  return;
1051
1040
  } catch (error) {
1052
- if (error instanceof OtlpPartialSuccessError) {
1041
+ if (isOversized(error)) {
1053
1042
  throw error;
1054
1043
  }
1055
- if (responseStatus(error) === 413) {
1056
- throw new OtlpPayloadTooLargeError();
1057
- }
1044
+ this.recordThrottle(error);
1058
1045
  if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
1059
1046
  throw error;
1060
1047
  }
1061
- await delay(RETRY_DELAY_MILLIS);
1048
+ const wait = retryWaitMillis(error, attempt, deadline - Date.now());
1049
+ if (wait === null) {
1050
+ throw error;
1051
+ }
1052
+ await delay(wait);
1062
1053
  }
1063
1054
  }
1064
1055
  }
1056
+ /**
1057
+ * Announce the carriers a request delivered. Wrapped because a listener that
1058
+ * throws must never turn a delivered batch into a failed export.
1059
+ */
1060
+ reportDelivered(spans) {
1061
+ if (this.onDelivered === void 0) {
1062
+ return;
1063
+ }
1064
+ const refs = spans.map((span) => span.ref).filter((ref) => ref !== void 0);
1065
+ if (refs.length === 0) {
1066
+ return;
1067
+ }
1068
+ try {
1069
+ this.onDelivered(refs);
1070
+ } catch (error) {
1071
+ logError("a delivery listener threw", error);
1072
+ }
1073
+ }
1065
1074
  async shutdown() {
1066
1075
  }
1067
1076
  async forceFlush() {
@@ -1119,7 +1128,9 @@ var OtelBatchTransport = class {
1119
1128
  options.directSender,
1120
1129
  maxRequestBytes,
1121
1130
  maxRequestBatchSize,
1122
- options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
1131
+ options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY,
1132
+ options.onDelivered,
1133
+ options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
1123
1134
  )
1124
1135
  );
1125
1136
  this.processor = new BatchSpanProcessor(this.deliveryTracker, {
@@ -1143,8 +1154,7 @@ var OtelBatchTransport = class {
1143
1154
  this.tracer = this.provider.getTracer("bitfab", __version__);
1144
1155
  liveTransports.add(this);
1145
1156
  }
1146
- submit(operation, payload) {
1147
- recordTraceSubmission(operation, payload);
1157
+ submit(operation, payload, meta = {}) {
1148
1158
  if (this.closed) {
1149
1159
  warnOnce(
1150
1160
  "otel-submit-after-shutdown",
@@ -1165,17 +1175,20 @@ var OtelBatchTransport = class {
1165
1175
  ].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
1166
1176
  );
1167
1177
  }
1168
- const span = this.tracer.startSpan(spanName(operation, payload), {
1178
+ const span = this.tracer.startSpan(meta.name ?? `bitfab.${operation}`, {
1169
1179
  attributes: {
1170
1180
  [OPERATION_ATTRIBUTE]: operation,
1171
1181
  [PAYLOAD_ATTRIBUTE]: body
1172
1182
  },
1173
- startTime: payloadTimestamp(payload, "started_at")
1183
+ startTime: meta.startTime
1174
1184
  });
1175
- if (hasError(payload)) {
1185
+ if (meta.ref !== void 0) {
1186
+ carrierRefs.set(span, meta.ref);
1187
+ }
1188
+ if (meta.errored === true) {
1176
1189
  span.setStatus({ code: SpanStatusCode.ERROR });
1177
1190
  }
1178
- endSpan(span, payloadTimestamp(payload, "ended_at"));
1191
+ endSpan(span, meta.endTime);
1179
1192
  } catch (error) {
1180
1193
  logError("failed to queue an OpenTelemetry span", error);
1181
1194
  }
@@ -1215,36 +1228,6 @@ var OtelBatchTransport = class {
1215
1228
  function endSpan(span, endTime) {
1216
1229
  span.end(endTime);
1217
1230
  }
1218
- function spanName(operation, payload) {
1219
- if (operation === "external_span") {
1220
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
1221
- if (typeof spanData?.name === "string") {
1222
- return spanData.name;
1223
- }
1224
- }
1225
- if (typeof payload.traceFunctionKey === "string") {
1226
- return payload.traceFunctionKey;
1227
- }
1228
- return `bitfab.${operation}`;
1229
- }
1230
- function payloadTimestamp(payload, field) {
1231
- const rawSpan = asRecord2(payload.rawSpan);
1232
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
1233
- const raw = rawSpan?.[field] ?? rawTrace?.[field];
1234
- if (typeof raw !== "string") {
1235
- return void 0;
1236
- }
1237
- const parsed = Date.parse(raw);
1238
- return Number.isNaN(parsed) ? void 0 : parsed;
1239
- }
1240
- function hasError(payload) {
1241
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
1242
- if (spanData?.error != null) {
1243
- return true;
1244
- }
1245
- const errors = payload.errors;
1246
- return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
1247
- }
1248
1231
  function createOtelTransport(options) {
1249
1232
  return new OtelBatchTransport({
1250
1233
  ...options,
@@ -1293,12 +1276,11 @@ function flushTraceTransports(timeoutMs) {
1293
1276
  function shutdownTraceTransports(timeoutMs) {
1294
1277
  return shutdownOtelTransports(timeoutMs);
1295
1278
  }
1296
- function takeReplaySpanCounts2(traceIds) {
1297
- return takeReplaySpanCounts(traceIds);
1298
- }
1299
1279
 
1300
1280
  // src/http.ts
1301
1281
  var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
1282
+ var OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
1283
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1302
1284
  var EXIT_FLUSH_TIMEOUT_MS = 5e3;
1303
1285
  var DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
1304
1286
  var pendingTracePromises = /* @__PURE__ */ new Set();
@@ -1358,8 +1340,101 @@ if (typeof process !== "undefined" && process.versions != null && process.versio
1358
1340
  });
1359
1341
  });
1360
1342
  }
1343
+ function readHeader(response, name) {
1344
+ try {
1345
+ return response.headers?.get(name) ?? null;
1346
+ } catch {
1347
+ return null;
1348
+ }
1349
+ }
1350
+ function parseRetryAfterMs(header) {
1351
+ const value = header?.trim();
1352
+ if (!value) {
1353
+ return void 0;
1354
+ }
1355
+ const seconds = Number(value);
1356
+ if (Number.isFinite(seconds)) {
1357
+ return seconds >= 0 ? seconds * 1e3 : void 0;
1358
+ }
1359
+ const at = Date.parse(value);
1360
+ if (Number.isNaN(at)) {
1361
+ return void 0;
1362
+ }
1363
+ return Math.max(0, at - Date.now());
1364
+ }
1365
+ function carrierMeta(operation, payload, ref) {
1366
+ return {
1367
+ ref,
1368
+ name: carrierName(operation, payload),
1369
+ startTime: payloadTimestamp(payload, "started_at"),
1370
+ endTime: payloadTimestamp(payload, "ended_at"),
1371
+ errored: payloadHasError(payload)
1372
+ };
1373
+ }
1374
+ function carrierName(operation, payload) {
1375
+ if (operation === "external_span") {
1376
+ const spanData = asPayloadRecord(
1377
+ asPayloadRecord(payload.rawSpan)?.span_data
1378
+ );
1379
+ if (typeof spanData?.name === "string") {
1380
+ return spanData.name;
1381
+ }
1382
+ }
1383
+ if (typeof payload.traceFunctionKey === "string") {
1384
+ return payload.traceFunctionKey;
1385
+ }
1386
+ return `bitfab.${operation}`;
1387
+ }
1388
+ function payloadTimestamp(payload, field) {
1389
+ const rawSpan = asPayloadRecord(payload.rawSpan);
1390
+ const rawTrace = asPayloadRecord(payload.externalTrace) ?? asPayloadRecord(payload.rawTrace);
1391
+ const raw = rawSpan?.[field] ?? rawTrace?.[field];
1392
+ if (typeof raw !== "string") {
1393
+ return void 0;
1394
+ }
1395
+ const parsed = Date.parse(raw);
1396
+ return Number.isNaN(parsed) ? void 0 : parsed;
1397
+ }
1398
+ function payloadHasError(payload) {
1399
+ const spanData = asPayloadRecord(asPayloadRecord(payload.rawSpan)?.span_data);
1400
+ if (spanData?.error != null) {
1401
+ return true;
1402
+ }
1403
+ const errors = payload.errors;
1404
+ return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
1405
+ }
1406
+ function asPayloadRecord(value) {
1407
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1408
+ }
1409
+ function carrierRef(payload) {
1410
+ const traceId = sourceTraceIdOf(payload);
1411
+ if (traceId === void 0) {
1412
+ return void 0;
1413
+ }
1414
+ const rawSpan = payload.rawSpan;
1415
+ if (rawSpan === void 0) {
1416
+ return { traceId };
1417
+ }
1418
+ const spanId = rawSpan?.id;
1419
+ return {
1420
+ traceId,
1421
+ spanId: typeof spanId === "string" ? spanId : `submission-${++carrierSeq}`
1422
+ };
1423
+ }
1424
+ function sourceTraceIdOf(payload) {
1425
+ if (typeof payload.sourceTraceId === "string") {
1426
+ return payload.sourceTraceId;
1427
+ }
1428
+ const rawTrace = payload.externalTrace ?? payload.rawTrace;
1429
+ const id = rawTrace?.id;
1430
+ return typeof id === "string" ? id : void 0;
1431
+ }
1432
+ var carrierSeq = 0;
1361
1433
  var HttpClient = class {
1362
1434
  constructor(config) {
1435
+ // Only traces a caller asked about are tracked, so ordinary tracing stores
1436
+ // nothing here.
1437
+ this.traceDeliveries = /* @__PURE__ */ new Map();
1363
1438
  // Deferred span work owned by THIS client. The module-global set backs the
1364
1439
  // process-wide `flushTraces()` and the exit hook, but per-client lifecycle
1365
1440
  // must not wait on another client's slow finalize: a false `close()` failure
@@ -1395,13 +1470,131 @@ var HttpClient = class {
1395
1470
  }
1396
1471
  if (!this.traceTransport) {
1397
1472
  this.traceTransport = createTraceTransport({
1398
- directSender: (endpoint, request, timeoutMs) => this.sendPrepared(endpoint, request, {
1399
- timeout: timeoutMs
1400
- })
1473
+ directSender: (request, timeoutMs) => this.deliverCarriers(request, timeoutMs),
1474
+ onDelivered: (refs) => this.recordDeliveredCarriers(refs)
1401
1475
  });
1402
1476
  }
1403
1477
  return this.traceTransport;
1404
1478
  }
1479
+ /**
1480
+ * Post one encoded batch and decide what the server's answer means, so the
1481
+ * transport never reads a response. Rejections and permanent statuses come
1482
+ * back as a non-retryable {@link DeliveryError}; anything the server might
1483
+ * still accept on a second try comes back retryable.
1484
+ */
1485
+ async deliverCarriers(request, timeoutMs) {
1486
+ let response;
1487
+ try {
1488
+ response = await this.sendPrepared(
1489
+ OTLP_TRACES_ENDPOINT,
1490
+ request,
1491
+ { timeout: timeoutMs }
1492
+ );
1493
+ } catch (error) {
1494
+ const status = error instanceof BitfabError ? error.status : void 0;
1495
+ if (status === void 0) {
1496
+ throw new DeliveryError(`OTLP ingestion failed: ${String(error)}`, {
1497
+ retryable: true
1498
+ });
1499
+ }
1500
+ throw new DeliveryError(`OTLP ingestion failed with HTTP ${status}`, {
1501
+ retryable: RETRYABLE_STATUSES.has(status),
1502
+ oversized: status === 413,
1503
+ ...error instanceof BitfabError && error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
1504
+ });
1505
+ }
1506
+ const partialSuccess = asPayloadRecord(response?.partialSuccess);
1507
+ const rejected = partialSuccess?.rejectedSpans;
1508
+ if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1509
+ throw new DeliveryError(
1510
+ `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1511
+ );
1512
+ }
1513
+ }
1514
+ /**
1515
+ * Start tracking delivery for `traceIds`. Nothing is recorded for a trace
1516
+ * that was never tracked, so ordinary tracing costs no bookkeeping at all.
1517
+ */
1518
+ trackTraceDeliveries(traceIds) {
1519
+ for (const traceId of traceIds) {
1520
+ if (!this.traceDeliveries.has(traceId)) {
1521
+ this.traceDeliveries.set(traceId, {
1522
+ submittedSpanIds: /* @__PURE__ */ new Set(),
1523
+ ackedSpanIds: /* @__PURE__ */ new Set(),
1524
+ closed: false,
1525
+ closingAcked: false
1526
+ });
1527
+ }
1528
+ }
1529
+ }
1530
+ /** Whether any tracked trace has had its closing carrier submitted. */
1531
+ hasClosedDeliveries(traceIds) {
1532
+ return traceIds.some((traceId) => this.traceDeliveries.get(traceId)?.closed);
1533
+ }
1534
+ /**
1535
+ * Report what each tracked trace submitted and whether the server confirmed
1536
+ * it, and stop tracking them. Every id passed is freed, so a caller cannot
1537
+ * leak a record for a trace that never closed.
1538
+ *
1539
+ * `delivered` is only meaningful once a flush has settled: acks land before
1540
+ * an export resolves, so a flush that reported success has already collected
1541
+ * every ack it is going to collect.
1542
+ */
1543
+ takeTraceDeliveries(traceIds) {
1544
+ const reports = {};
1545
+ for (const traceId of traceIds) {
1546
+ const delivery = this.traceDeliveries.get(traceId);
1547
+ if (delivery === void 0) {
1548
+ continue;
1549
+ }
1550
+ this.traceDeliveries.delete(traceId);
1551
+ reports[traceId] = {
1552
+ spanCount: delivery.submittedSpanIds.size,
1553
+ closed: delivery.closed,
1554
+ delivered: delivery.closingAcked && [...delivery.submittedSpanIds].every(
1555
+ (spanId) => delivery.ackedSpanIds.has(spanId)
1556
+ )
1557
+ };
1558
+ }
1559
+ return reports;
1560
+ }
1561
+ /** Build a carrier's meta and record what it adds to its trace's expected set. */
1562
+ recordedMeta(operation, payload, ref) {
1563
+ this.recordSubmittedCarrier(ref);
1564
+ return carrierMeta(operation, payload, ref);
1565
+ }
1566
+ recordSubmittedCarrier(ref) {
1567
+ if (ref === void 0) {
1568
+ return;
1569
+ }
1570
+ const delivery = this.traceDeliveries.get(ref.traceId);
1571
+ if (delivery === void 0) {
1572
+ return;
1573
+ }
1574
+ if (ref.spanId === void 0) {
1575
+ delivery.closed = true;
1576
+ } else {
1577
+ delivery.submittedSpanIds.add(ref.spanId);
1578
+ }
1579
+ }
1580
+ /**
1581
+ * Ingestion commits every carrier in a request before it answers, so a
1582
+ * delivered ref is proof its row exists: the same fact the replay status
1583
+ * endpoint would report, already in hand.
1584
+ */
1585
+ recordDeliveredCarriers(refs) {
1586
+ for (const ref of refs) {
1587
+ const delivery = this.traceDeliveries.get(ref.traceId);
1588
+ if (delivery === void 0) {
1589
+ continue;
1590
+ }
1591
+ if (ref.spanId === void 0) {
1592
+ delivery.closingAcked = true;
1593
+ } else {
1594
+ delivery.ackedSpanIds.add(ref.spanId);
1595
+ }
1596
+ }
1597
+ }
1405
1598
  /**
1406
1599
  * Track deferred span work so this client's own lifecycle waits for it, and
1407
1600
  * so the process-wide flush and exit hook do too.
@@ -1511,7 +1704,8 @@ var HttpClient = class {
1511
1704
  throw new BitfabError(
1512
1705
  `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1513
1706
  void 0,
1514
- response.status
1707
+ response.status,
1708
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1515
1709
  );
1516
1710
  }
1517
1711
  const result = await response.json();
@@ -1597,11 +1791,12 @@ var HttpClient = class {
1597
1791
  * the OTLP carrier has no path to carry it.
1598
1792
  */
1599
1793
  sendInternalTrace(functionId, payload) {
1600
- this.getTraceTransport()?.submit("internal_trace", {
1601
- ...payload,
1602
- functionId,
1603
- sdkVersion: __version__
1604
- });
1794
+ const body = { ...payload, functionId, sdkVersion: __version__ };
1795
+ this.getTraceTransport()?.submit(
1796
+ "internal_trace",
1797
+ body,
1798
+ carrierMeta("internal_trace", body, void 0)
1799
+ );
1605
1800
  }
1606
1801
  /**
1607
1802
  * Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
@@ -1610,10 +1805,11 @@ var HttpClient = class {
1610
1805
  * promise.
1611
1806
  */
1612
1807
  sendExternalSpan(payload) {
1613
- this.getTraceTransport()?.submit("external_span", {
1614
- ...payload,
1615
- sdkVersion: __version__
1616
- });
1808
+ this.getTraceTransport()?.submit(
1809
+ "external_span",
1810
+ { ...payload, sdkVersion: __version__ },
1811
+ this.recordedMeta("external_span", payload, carrierRef(payload))
1812
+ );
1617
1813
  }
1618
1814
  /**
1619
1815
  * Queue an external trace completion (from OpenAI tracing) onto this
@@ -1622,10 +1818,15 @@ var HttpClient = class {
1622
1818
  * server-authoritative barrier in `replay.ts`, not by awaiting this call.
1623
1819
  */
1624
1820
  sendExternalTrace(payload) {
1625
- this.getTraceTransport()?.submit("external_trace", {
1626
- ...payload,
1627
- sdkVersion: __version__
1628
- });
1821
+ this.getTraceTransport()?.submit(
1822
+ "external_trace",
1823
+ { ...payload, sdkVersion: __version__ },
1824
+ this.recordedMeta(
1825
+ "external_trace",
1826
+ payload,
1827
+ payload.completed === true ? carrierRef(payload) : void 0
1828
+ )
1829
+ );
1629
1830
  }
1630
1831
  /**
1631
1832
  * Partial update of an existing trace identified by its Bitfab trace ID.
@@ -2303,15 +2504,29 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2303
2504
  REPLAY_PERSISTENCE_TIMEOUT_MS
2304
2505
  );
2305
2506
  if (!deferredSettled) {
2507
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2306
2508
  throw new BitfabError(
2307
2509
  `Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
2308
2510
  );
2309
2511
  }
2310
- const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
2311
- if (Object.keys(expectedSpanCounts).length === 0) {
2512
+ if (!httpClient.hasClosedDeliveries(replayedTraceIds)) {
2513
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2312
2514
  return;
2313
2515
  }
2314
2516
  const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2517
+ const deliveries = httpClient.takeTraceDeliveries(replayedTraceIds);
2518
+ const expectedSpanCounts = {};
2519
+ let allDelivered = true;
2520
+ for (const [traceId, delivery] of Object.entries(deliveries)) {
2521
+ if (!delivery.closed) {
2522
+ continue;
2523
+ }
2524
+ expectedSpanCounts[traceId] = delivery.spanCount;
2525
+ allDelivered = allDelivered && delivery.delivered;
2526
+ }
2527
+ if (allDelivered) {
2528
+ return;
2529
+ }
2315
2530
  const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
2316
2531
  let missing = Object.keys(expectedSpanCounts).length;
2317
2532
  while (true) {
@@ -2329,17 +2544,18 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2329
2544
  if (Date.now() >= deadline) {
2330
2545
  break;
2331
2546
  }
2332
- await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
2547
+ await sleepForReplayPersistence(
2548
+ Math.min(100, Math.max(0, deadline - Date.now()))
2549
+ );
2333
2550
  }
2334
2551
  const cause = flushed ? "" : " Delivery was also not confirmed before the flush deadline, so the spans likely never reached the server.";
2335
2552
  throw new BitfabError(
2336
2553
  `Replay traces were not fully persisted before the delivery deadline (testRunId ${testRunId}, missing ${missing} of ${Object.keys(expectedSpanCounts).length} trace(s)).${cause}`
2337
2554
  );
2338
2555
  }
2339
- function sleep(ms) {
2556
+ function sleepForReplayPersistence(ms) {
2340
2557
  return new Promise((resolve) => {
2341
- const timer = setTimeout(resolve, ms);
2342
- unrefTimer(timer);
2558
+ setTimeout(resolve, ms);
2343
2559
  });
2344
2560
  }
2345
2561
  async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted) {
@@ -2420,6 +2636,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2420
2636
  ...registeredOverrides
2421
2637
  ];
2422
2638
  const replayedTraceIds = serverItems.map(() => randomUuid());
2639
+ httpClient.trackTraceDeliveries(replayedTraceIds);
2423
2640
  const tasks = serverItems.map(
2424
2641
  (serverItem, index) => () => processItem(
2425
2642
  httpClient,
@@ -2634,6 +2851,8 @@ export {
2634
2851
  ReplayError,
2635
2852
  DbBranchReplayError,
2636
2853
  serializeReplayResult,
2854
+ waitForReplayPersistence,
2855
+ sleepForReplayPersistence,
2637
2856
  replay
2638
2857
  };
2639
- //# sourceMappingURL=chunk-CFECTUDU.js.map
2858
+ //# sourceMappingURL=chunk-MBKY42QC.js.map