@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.
package/dist/node.cjs CHANGED
@@ -88,7 +88,7 @@ var __version__;
88
88
  var init_version_generated = __esm({
89
89
  "src/version.generated.ts"() {
90
90
  "use strict";
91
- __version__ = "0.36.7";
91
+ __version__ = "0.36.9";
92
92
  }
93
93
  });
94
94
 
@@ -214,10 +214,11 @@ var init_errors = __esm({
214
214
  "src/errors.ts"() {
215
215
  "use strict";
216
216
  BitfabError = class extends Error {
217
- constructor(message, url, status) {
217
+ constructor(message, url, status, retryAfterMs) {
218
218
  super(message);
219
219
  this.url = url;
220
220
  this.status = status;
221
+ this.retryAfterMs = retryAfterMs;
221
222
  this.name = "BitfabError";
222
223
  }
223
224
  };
@@ -533,6 +534,23 @@ var init_serializePayload = __esm({
533
534
  }
534
535
  });
535
536
 
537
+ // src/transportTypes.ts
538
+ var DeliveryError;
539
+ var init_transportTypes = __esm({
540
+ "src/transportTypes.ts"() {
541
+ "use strict";
542
+ DeliveryError = class extends Error {
543
+ constructor(message, options = {}) {
544
+ super(message);
545
+ this.name = "DeliveryError";
546
+ this.retryable = options.retryable ?? false;
547
+ this.oversized = options.oversized ?? false;
548
+ this.retryAfterMs = options.retryAfterMs;
549
+ }
550
+ };
551
+ }
552
+ });
553
+
536
554
  // src/unrefTimer.ts
537
555
  function unrefTimer(timer) {
538
556
  const handle = timer;
@@ -572,59 +590,6 @@ function logError(message, error) {
572
590
  } catch {
573
591
  }
574
592
  }
575
- function recordTraceSubmission(operation, payload) {
576
- const sourceTraceId = resolveSourceTraceId(payload);
577
- if (sourceTraceId === void 0) {
578
- return;
579
- }
580
- if (operation === "external_span") {
581
- const rawSpan = asRecord2(payload.rawSpan);
582
- if (typeof rawSpan?.id !== "string") {
583
- submissionCounter += 1;
584
- }
585
- const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
586
- const existing = traceSubmissionSpanIds.get(sourceTraceId);
587
- if (existing) {
588
- existing.add(sourceSpanId);
589
- } else {
590
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
591
- }
592
- return;
593
- }
594
- if (payload.completed !== true) {
595
- return;
596
- }
597
- if (typeof payload.testRunId === "string") {
598
- replayTraceSubmissions.add(sourceTraceId);
599
- if (!traceSubmissionSpanIds.has(sourceTraceId)) {
600
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
601
- }
602
- } else {
603
- traceSubmissionSpanIds.delete(sourceTraceId);
604
- }
605
- }
606
- function takeReplaySpanCounts(traceIds) {
607
- const counts = {};
608
- for (const traceId of traceIds) {
609
- if (!replayTraceSubmissions.has(traceId)) {
610
- continue;
611
- }
612
- counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
613
- traceSubmissionSpanIds.delete(traceId);
614
- replayTraceSubmissions.delete(traceId);
615
- }
616
- return counts;
617
- }
618
- function asRecord2(value) {
619
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
620
- }
621
- function resolveSourceTraceId(payload) {
622
- if (typeof payload.sourceTraceId === "string") {
623
- return payload.sourceTraceId;
624
- }
625
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
626
- return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
627
- }
628
593
  function otlpValue(value) {
629
594
  if (typeof value === "boolean") {
630
595
  return { boolValue: value };
@@ -682,7 +647,11 @@ function spanToOtlp(span) {
682
647
  }
683
648
  function encodeSpan(span) {
684
649
  const json = JSON.stringify(spanToOtlp(span));
685
- return { json, size: byteLength(json) };
650
+ return {
651
+ json,
652
+ size: byteLength(json),
653
+ ref: carrierRefs.get(span)
654
+ };
686
655
  }
687
656
  function trimEncodedSpan(span) {
688
657
  try {
@@ -765,48 +734,27 @@ async function mapWithConcurrency(items, limit, task) {
765
734
  await Promise.all(workers);
766
735
  return results;
767
736
  }
768
- function responseStatus(error) {
769
- return error instanceof BitfabError ? error.status : void 0;
770
- }
771
737
  function isRetryable(error) {
772
- const status = responseStatus(error);
773
- if (status === void 0) {
774
- return true;
775
- }
776
- return RETRYABLE_STATUSES.has(status) || status >= 500;
738
+ return error instanceof DeliveryError && error.retryable;
777
739
  }
778
- function endSpan(span, endTime) {
779
- span.end(endTime);
740
+ function isOversized(error) {
741
+ return error instanceof DeliveryError && error.oversized;
780
742
  }
781
- function spanName(operation, payload) {
782
- if (operation === "external_span") {
783
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
784
- if (typeof spanData?.name === "string") {
785
- return spanData.name;
786
- }
743
+ function retryWaitMillis(error, attempt, remainingMillis) {
744
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
745
+ const affordable = remainingMillis / 2;
746
+ if (requested !== void 0) {
747
+ return requested < affordable ? requested : null;
787
748
  }
788
- if (typeof payload.traceFunctionKey === "string") {
789
- return payload.traceFunctionKey;
790
- }
791
- return `bitfab.${operation}`;
792
- }
793
- function payloadTimestamp(payload, field) {
794
- const rawSpan = asRecord2(payload.rawSpan);
795
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
796
- const raw = rawSpan?.[field] ?? rawTrace?.[field];
797
- if (typeof raw !== "string") {
798
- return void 0;
799
- }
800
- const parsed = Date.parse(raw);
801
- return Number.isNaN(parsed) ? void 0 : parsed;
749
+ const backoff = Math.min(
750
+ RETRY_BASE_DELAY_MILLIS * 2 ** attempt,
751
+ RETRY_BACKOFF_CEILING_MILLIS
752
+ );
753
+ const jittered = backoff / 2 + Math.random() * (backoff / 2);
754
+ return jittered < affordable ? jittered : null;
802
755
  }
803
- function hasError(payload) {
804
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
805
- if (spanData?.error != null) {
806
- return true;
807
- }
808
- const errors = payload.errors;
809
- return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
756
+ function endSpan(span, endTime) {
757
+ span.end(endTime);
810
758
  }
811
759
  function createOtelTransport(options) {
812
760
  return new OtelBatchTransport({
@@ -845,7 +793,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
845
793
  (transport, remaining) => transport.shutdown(remaining)
846
794
  );
847
795
  }
848
- var import_api, import_core, import_resources, import_sdk_trace_base, OPERATION_ATTRIBUTE, PAYLOAD_ATTRIBUTE, OTLP_TRACES_ENDPOINT, MAX_EXPORT_REQUEST_BYTES, MAX_DECOMPRESSED_REQUEST_BYTES, MAX_REQUEST_BYTES_ENV, EXPORT_CONCURRENCY_ENV, MAX_QUEUE_SIZE, DIRECT_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, SPAN_SEPARATOR_BYTES, OtlpPayloadTooLargeError, OtlpPartialSuccessError, BitfabSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
796
+ var import_api, import_core, import_resources, import_sdk_trace_base, OPERATION_ATTRIBUTE, PAYLOAD_ATTRIBUTE, MAX_EXPORT_REQUEST_BYTES, MAX_DECOMPRESSED_REQUEST_BYTES, MAX_REQUEST_BYTES_ENV, EXPORT_CONCURRENCY_ENV, MAX_QUEUE_SIZE, DIRECT_MAX_EXPORT_BATCH_SIZE, DIRECT_MAX_REQUEST_BATCH_SIZE, DEFAULT_EXPORT_CONCURRENCY, MAX_EXPORT_CONCURRENCY, SCHEDULE_DELAY_MILLIS, EXPORT_TIMEOUT_MILLIS, RETRY_BASE_DELAY_MILLIS, RETRY_BACKOFF_CEILING_MILLIS, MAX_SEND_ATTEMPTS, DEFAULT_LIFECYCLE_TIMEOUT_MS, liveTransports, carrierRefs, SPAN_SEPARATOR_BYTES, BitfabSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
849
797
  var init_otel = __esm({
850
798
  "src/otel.ts"() {
851
799
  "use strict";
@@ -859,11 +807,11 @@ var init_otel = __esm({
859
807
  init_payloadBudget();
860
808
  init_readEnv();
861
809
  init_serializePayload();
810
+ init_transportTypes();
862
811
  init_unrefTimer();
863
812
  init_warnOnce();
864
813
  OPERATION_ATTRIBUTE = "bitfab.operation";
865
814
  PAYLOAD_ATTRIBUTE = "bitfab.payload";
866
- OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
867
815
  MAX_EXPORT_REQUEST_BYTES = 3e6;
868
816
  MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
869
817
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
@@ -875,25 +823,23 @@ var init_otel = __esm({
875
823
  MAX_EXPORT_CONCURRENCY = 64;
876
824
  SCHEDULE_DELAY_MILLIS = 5e3;
877
825
  EXPORT_TIMEOUT_MILLIS = 3e4;
878
- RETRY_DELAY_MILLIS = 100;
826
+ RETRY_BASE_DELAY_MILLIS = 100;
827
+ RETRY_BACKOFF_CEILING_MILLIS = 5e3;
879
828
  MAX_SEND_ATTEMPTS = 3;
880
829
  DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
881
- RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
882
830
  liveTransports = /* @__PURE__ */ new Set();
883
- traceSubmissionSpanIds = /* @__PURE__ */ new Map();
884
- replayTraceSubmissions = /* @__PURE__ */ new Set();
885
- submissionCounter = 0;
831
+ carrierRefs = /* @__PURE__ */ new WeakMap();
886
832
  SPAN_SEPARATOR_BYTES = 1;
887
- OtlpPayloadTooLargeError = class extends Error {
888
- };
889
- OtlpPartialSuccessError = class extends Error {
890
- };
891
833
  BitfabSpanExporter = class {
892
- constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
834
+ constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency, onDelivered, exportTimeoutMillis = EXPORT_TIMEOUT_MILLIS) {
893
835
  this.directSender = directSender;
894
836
  this.maxRequestBytes = maxRequestBytes;
895
837
  this.maxRequestBatchSize = maxRequestBatchSize;
896
838
  this.exportConcurrency = exportConcurrency;
839
+ this.onDelivered = onDelivered;
840
+ this.exportTimeoutMillis = exportTimeoutMillis;
841
+ /** Epoch ms until which the server has asked this exporter to stay away. */
842
+ this.throttledUntil = 0;
897
843
  }
898
844
  export(spans, resultCallback) {
899
845
  void this.exportAsync(spans).then(
@@ -959,6 +905,7 @@ var init_otel = __esm({
959
905
  );
960
906
  if (prepared.wireBytes <= this.maxRequestBytes) {
961
907
  await this.sendWithRetries(prepared);
908
+ this.reportDelivered(batch.spans);
962
909
  return true;
963
910
  }
964
911
  }
@@ -986,15 +933,12 @@ var init_otel = __esm({
986
933
  alreadyTrimmed = true;
987
934
  }
988
935
  } catch (error) {
989
- if (error instanceof OtlpPayloadTooLargeError) {
936
+ if (isOversized(error)) {
990
937
  logError(
991
938
  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"
992
939
  );
993
940
  return false;
994
941
  }
995
- if (error instanceof OtlpPartialSuccessError) {
996
- return false;
997
- }
998
942
  logError("failed to export an OpenTelemetry span batch", error);
999
943
  return false;
1000
944
  }
@@ -1012,37 +956,79 @@ var init_otel = __esm({
1012
956
  * the server does not yet understand. The fix is a client-supplied
1013
957
  * idempotency key that ingestion dedupes on.
1014
958
  */
959
+ /**
960
+ * Remember a throttle the server asked for, so the requests fanned out
961
+ * alongside this one respect it too. Delaying only the request that was
962
+ * refused leaves the other seven in the window hitting a server that just
963
+ * asked for room.
964
+ */
965
+ recordThrottle(error) {
966
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
967
+ if (requested !== void 0) {
968
+ this.throttledUntil = Math.max(
969
+ this.throttledUntil,
970
+ Date.now() + requested
971
+ );
972
+ }
973
+ }
974
+ /**
975
+ * Waits out an active throttle, or reports the batch undeliverable when the
976
+ * throttle outlasts what we are willing to hold it for. Either way nothing is
977
+ * sent while the server has asked us to stay away.
978
+ */
979
+ async awaitThrottle(deadline) {
980
+ const remaining = this.throttledUntil - Date.now();
981
+ if (remaining <= 0) {
982
+ return;
983
+ }
984
+ if (remaining >= (deadline - Date.now()) / 2) {
985
+ throw new DeliveryError(
986
+ `OTLP ingestion is throttled for another ${remaining}ms, longer than the export budget`
987
+ );
988
+ }
989
+ await delay(remaining);
990
+ }
1015
991
  async sendWithRetries(request) {
992
+ const deadline = Date.now() + this.exportTimeoutMillis;
1016
993
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
1017
994
  try {
1018
- const response = await this.directSender(
1019
- OTLP_TRACES_ENDPOINT,
1020
- request,
1021
- EXPORT_TIMEOUT_MILLIS
1022
- );
1023
- const partialSuccess = asRecord2(response?.partialSuccess);
1024
- const rejected = partialSuccess?.rejectedSpans;
1025
- if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1026
- logError(
1027
- `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1028
- );
1029
- throw new OtlpPartialSuccessError();
1030
- }
995
+ await this.awaitThrottle(deadline);
996
+ await this.directSender(request, Math.max(0, deadline - Date.now()));
1031
997
  return;
1032
998
  } catch (error) {
1033
- if (error instanceof OtlpPartialSuccessError) {
999
+ if (isOversized(error)) {
1034
1000
  throw error;
1035
1001
  }
1036
- if (responseStatus(error) === 413) {
1037
- throw new OtlpPayloadTooLargeError();
1038
- }
1002
+ this.recordThrottle(error);
1039
1003
  if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
1040
1004
  throw error;
1041
1005
  }
1042
- await delay(RETRY_DELAY_MILLIS);
1006
+ const wait = retryWaitMillis(error, attempt, deadline - Date.now());
1007
+ if (wait === null) {
1008
+ throw error;
1009
+ }
1010
+ await delay(wait);
1043
1011
  }
1044
1012
  }
1045
1013
  }
1014
+ /**
1015
+ * Announce the carriers a request delivered. Wrapped because a listener that
1016
+ * throws must never turn a delivered batch into a failed export.
1017
+ */
1018
+ reportDelivered(spans) {
1019
+ if (this.onDelivered === void 0) {
1020
+ return;
1021
+ }
1022
+ const refs = spans.map((span) => span.ref).filter((ref) => ref !== void 0);
1023
+ if (refs.length === 0) {
1024
+ return;
1025
+ }
1026
+ try {
1027
+ this.onDelivered(refs);
1028
+ } catch (error) {
1029
+ logError("a delivery listener threw", error);
1030
+ }
1031
+ }
1046
1032
  async shutdown() {
1047
1033
  }
1048
1034
  async forceFlush() {
@@ -1100,7 +1086,9 @@ var init_otel = __esm({
1100
1086
  options.directSender,
1101
1087
  maxRequestBytes,
1102
1088
  maxRequestBatchSize,
1103
- options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
1089
+ options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY,
1090
+ options.onDelivered,
1091
+ options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
1104
1092
  )
1105
1093
  );
1106
1094
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
@@ -1124,8 +1112,7 @@ var init_otel = __esm({
1124
1112
  this.tracer = this.provider.getTracer("bitfab", __version__);
1125
1113
  liveTransports.add(this);
1126
1114
  }
1127
- submit(operation, payload) {
1128
- recordTraceSubmission(operation, payload);
1115
+ submit(operation, payload, meta = {}) {
1129
1116
  if (this.closed) {
1130
1117
  warnOnce(
1131
1118
  "otel-submit-after-shutdown",
@@ -1146,17 +1133,20 @@ var init_otel = __esm({
1146
1133
  ].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
1147
1134
  );
1148
1135
  }
1149
- const span = this.tracer.startSpan(spanName(operation, payload), {
1136
+ const span = this.tracer.startSpan(meta.name ?? `bitfab.${operation}`, {
1150
1137
  attributes: {
1151
1138
  [OPERATION_ATTRIBUTE]: operation,
1152
1139
  [PAYLOAD_ATTRIBUTE]: body
1153
1140
  },
1154
- startTime: payloadTimestamp(payload, "started_at")
1141
+ startTime: meta.startTime
1155
1142
  });
1156
- if (hasError(payload)) {
1143
+ if (meta.ref !== void 0) {
1144
+ carrierRefs.set(span, meta.ref);
1145
+ }
1146
+ if (meta.errored === true) {
1157
1147
  span.setStatus({ code: import_api.SpanStatusCode.ERROR });
1158
1148
  }
1159
- endSpan(span, payloadTimestamp(payload, "ended_at"));
1149
+ endSpan(span, meta.endTime);
1160
1150
  } catch (error) {
1161
1151
  logError("failed to queue an OpenTelemetry span", error);
1162
1152
  }
@@ -1206,9 +1196,6 @@ function flushTraceTransports(timeoutMs) {
1206
1196
  function shutdownTraceTransports(timeoutMs) {
1207
1197
  return shutdownOtelTransports(timeoutMs);
1208
1198
  }
1209
- function takeReplaySpanCounts2(traceIds) {
1210
- return takeReplaySpanCounts(traceIds);
1211
- }
1212
1199
  var init_transport = __esm({
1213
1200
  "src/transport.ts"() {
1214
1201
  "use strict";
@@ -1257,7 +1244,96 @@ async function waitForPromises(promises, timeoutMs) {
1257
1244
  }
1258
1245
  }
1259
1246
  }
1260
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, HttpClient;
1247
+ function readHeader(response, name) {
1248
+ try {
1249
+ return response.headers?.get(name) ?? null;
1250
+ } catch {
1251
+ return null;
1252
+ }
1253
+ }
1254
+ function parseRetryAfterMs(header) {
1255
+ const value = header?.trim();
1256
+ if (!value) {
1257
+ return void 0;
1258
+ }
1259
+ const seconds = Number(value);
1260
+ if (Number.isFinite(seconds)) {
1261
+ return seconds >= 0 ? seconds * 1e3 : void 0;
1262
+ }
1263
+ const at = Date.parse(value);
1264
+ if (Number.isNaN(at)) {
1265
+ return void 0;
1266
+ }
1267
+ return Math.max(0, at - Date.now());
1268
+ }
1269
+ function carrierMeta(operation, payload, ref) {
1270
+ return {
1271
+ ref,
1272
+ name: carrierName(operation, payload),
1273
+ startTime: payloadTimestamp(payload, "started_at"),
1274
+ endTime: payloadTimestamp(payload, "ended_at"),
1275
+ errored: payloadHasError(payload)
1276
+ };
1277
+ }
1278
+ function carrierName(operation, payload) {
1279
+ if (operation === "external_span") {
1280
+ const spanData = asPayloadRecord(
1281
+ asPayloadRecord(payload.rawSpan)?.span_data
1282
+ );
1283
+ if (typeof spanData?.name === "string") {
1284
+ return spanData.name;
1285
+ }
1286
+ }
1287
+ if (typeof payload.traceFunctionKey === "string") {
1288
+ return payload.traceFunctionKey;
1289
+ }
1290
+ return `bitfab.${operation}`;
1291
+ }
1292
+ function payloadTimestamp(payload, field) {
1293
+ const rawSpan = asPayloadRecord(payload.rawSpan);
1294
+ const rawTrace = asPayloadRecord(payload.externalTrace) ?? asPayloadRecord(payload.rawTrace);
1295
+ const raw = rawSpan?.[field] ?? rawTrace?.[field];
1296
+ if (typeof raw !== "string") {
1297
+ return void 0;
1298
+ }
1299
+ const parsed = Date.parse(raw);
1300
+ return Number.isNaN(parsed) ? void 0 : parsed;
1301
+ }
1302
+ function payloadHasError(payload) {
1303
+ const spanData = asPayloadRecord(asPayloadRecord(payload.rawSpan)?.span_data);
1304
+ if (spanData?.error != null) {
1305
+ return true;
1306
+ }
1307
+ const errors = payload.errors;
1308
+ return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
1309
+ }
1310
+ function asPayloadRecord(value) {
1311
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1312
+ }
1313
+ function carrierRef(payload) {
1314
+ const traceId = sourceTraceIdOf(payload);
1315
+ if (traceId === void 0) {
1316
+ return void 0;
1317
+ }
1318
+ const rawSpan = payload.rawSpan;
1319
+ if (rawSpan === void 0) {
1320
+ return { traceId };
1321
+ }
1322
+ const spanId = rawSpan?.id;
1323
+ return {
1324
+ traceId,
1325
+ spanId: typeof spanId === "string" ? spanId : `submission-${++carrierSeq}`
1326
+ };
1327
+ }
1328
+ function sourceTraceIdOf(payload) {
1329
+ if (typeof payload.sourceTraceId === "string") {
1330
+ return payload.sourceTraceId;
1331
+ }
1332
+ const rawTrace = payload.externalTrace ?? payload.rawTrace;
1333
+ const id = rawTrace?.id;
1334
+ return typeof id === "string" ? id : void 0;
1335
+ }
1336
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, carrierSeq, HttpClient;
1261
1337
  var init_http = __esm({
1262
1338
  "src/http.ts"() {
1263
1339
  "use strict";
@@ -1267,9 +1343,12 @@ var init_http = __esm({
1267
1343
  init_replayContext();
1268
1344
  init_serializePayload();
1269
1345
  init_transport();
1346
+ init_transportTypes();
1270
1347
  init_unrefTimer();
1271
1348
  init_warnOnce();
1272
1349
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
1350
+ OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
1351
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1273
1352
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
1274
1353
  DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
1275
1354
  pendingTracePromises = /* @__PURE__ */ new Set();
@@ -1289,8 +1368,12 @@ var init_http = __esm({
1289
1368
  });
1290
1369
  });
1291
1370
  }
1371
+ carrierSeq = 0;
1292
1372
  HttpClient = class {
1293
1373
  constructor(config) {
1374
+ // Only traces a caller asked about are tracked, so ordinary tracing stores
1375
+ // nothing here.
1376
+ this.traceDeliveries = /* @__PURE__ */ new Map();
1294
1377
  // Deferred span work owned by THIS client. The module-global set backs the
1295
1378
  // process-wide `flushTraces()` and the exit hook, but per-client lifecycle
1296
1379
  // must not wait on another client's slow finalize: a false `close()` failure
@@ -1326,13 +1409,131 @@ var init_http = __esm({
1326
1409
  }
1327
1410
  if (!this.traceTransport) {
1328
1411
  this.traceTransport = createTraceTransport({
1329
- directSender: (endpoint, request, timeoutMs) => this.sendPrepared(endpoint, request, {
1330
- timeout: timeoutMs
1331
- })
1412
+ directSender: (request, timeoutMs) => this.deliverCarriers(request, timeoutMs),
1413
+ onDelivered: (refs) => this.recordDeliveredCarriers(refs)
1332
1414
  });
1333
1415
  }
1334
1416
  return this.traceTransport;
1335
1417
  }
1418
+ /**
1419
+ * Post one encoded batch and decide what the server's answer means, so the
1420
+ * transport never reads a response. Rejections and permanent statuses come
1421
+ * back as a non-retryable {@link DeliveryError}; anything the server might
1422
+ * still accept on a second try comes back retryable.
1423
+ */
1424
+ async deliverCarriers(request, timeoutMs) {
1425
+ let response;
1426
+ try {
1427
+ response = await this.sendPrepared(
1428
+ OTLP_TRACES_ENDPOINT,
1429
+ request,
1430
+ { timeout: timeoutMs }
1431
+ );
1432
+ } catch (error) {
1433
+ const status = error instanceof BitfabError ? error.status : void 0;
1434
+ if (status === void 0) {
1435
+ throw new DeliveryError(`OTLP ingestion failed: ${String(error)}`, {
1436
+ retryable: true
1437
+ });
1438
+ }
1439
+ throw new DeliveryError(`OTLP ingestion failed with HTTP ${status}`, {
1440
+ retryable: RETRYABLE_STATUSES.has(status),
1441
+ oversized: status === 413,
1442
+ ...error instanceof BitfabError && error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
1443
+ });
1444
+ }
1445
+ const partialSuccess = asPayloadRecord(response?.partialSuccess);
1446
+ const rejected = partialSuccess?.rejectedSpans;
1447
+ if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1448
+ throw new DeliveryError(
1449
+ `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1450
+ );
1451
+ }
1452
+ }
1453
+ /**
1454
+ * Start tracking delivery for `traceIds`. Nothing is recorded for a trace
1455
+ * that was never tracked, so ordinary tracing costs no bookkeeping at all.
1456
+ */
1457
+ trackTraceDeliveries(traceIds) {
1458
+ for (const traceId of traceIds) {
1459
+ if (!this.traceDeliveries.has(traceId)) {
1460
+ this.traceDeliveries.set(traceId, {
1461
+ submittedSpanIds: /* @__PURE__ */ new Set(),
1462
+ ackedSpanIds: /* @__PURE__ */ new Set(),
1463
+ closed: false,
1464
+ closingAcked: false
1465
+ });
1466
+ }
1467
+ }
1468
+ }
1469
+ /** Whether any tracked trace has had its closing carrier submitted. */
1470
+ hasClosedDeliveries(traceIds) {
1471
+ return traceIds.some((traceId) => this.traceDeliveries.get(traceId)?.closed);
1472
+ }
1473
+ /**
1474
+ * Report what each tracked trace submitted and whether the server confirmed
1475
+ * it, and stop tracking them. Every id passed is freed, so a caller cannot
1476
+ * leak a record for a trace that never closed.
1477
+ *
1478
+ * `delivered` is only meaningful once a flush has settled: acks land before
1479
+ * an export resolves, so a flush that reported success has already collected
1480
+ * every ack it is going to collect.
1481
+ */
1482
+ takeTraceDeliveries(traceIds) {
1483
+ const reports = {};
1484
+ for (const traceId of traceIds) {
1485
+ const delivery = this.traceDeliveries.get(traceId);
1486
+ if (delivery === void 0) {
1487
+ continue;
1488
+ }
1489
+ this.traceDeliveries.delete(traceId);
1490
+ reports[traceId] = {
1491
+ spanCount: delivery.submittedSpanIds.size,
1492
+ closed: delivery.closed,
1493
+ delivered: delivery.closingAcked && [...delivery.submittedSpanIds].every(
1494
+ (spanId) => delivery.ackedSpanIds.has(spanId)
1495
+ )
1496
+ };
1497
+ }
1498
+ return reports;
1499
+ }
1500
+ /** Build a carrier's meta and record what it adds to its trace's expected set. */
1501
+ recordedMeta(operation, payload, ref) {
1502
+ this.recordSubmittedCarrier(ref);
1503
+ return carrierMeta(operation, payload, ref);
1504
+ }
1505
+ recordSubmittedCarrier(ref) {
1506
+ if (ref === void 0) {
1507
+ return;
1508
+ }
1509
+ const delivery = this.traceDeliveries.get(ref.traceId);
1510
+ if (delivery === void 0) {
1511
+ return;
1512
+ }
1513
+ if (ref.spanId === void 0) {
1514
+ delivery.closed = true;
1515
+ } else {
1516
+ delivery.submittedSpanIds.add(ref.spanId);
1517
+ }
1518
+ }
1519
+ /**
1520
+ * Ingestion commits every carrier in a request before it answers, so a
1521
+ * delivered ref is proof its row exists: the same fact the replay status
1522
+ * endpoint would report, already in hand.
1523
+ */
1524
+ recordDeliveredCarriers(refs) {
1525
+ for (const ref of refs) {
1526
+ const delivery = this.traceDeliveries.get(ref.traceId);
1527
+ if (delivery === void 0) {
1528
+ continue;
1529
+ }
1530
+ if (ref.spanId === void 0) {
1531
+ delivery.closingAcked = true;
1532
+ } else {
1533
+ delivery.ackedSpanIds.add(ref.spanId);
1534
+ }
1535
+ }
1536
+ }
1336
1537
  /**
1337
1538
  * Track deferred span work so this client's own lifecycle waits for it, and
1338
1539
  * so the process-wide flush and exit hook do too.
@@ -1442,7 +1643,8 @@ var init_http = __esm({
1442
1643
  throw new BitfabError(
1443
1644
  `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1444
1645
  void 0,
1445
- response.status
1646
+ response.status,
1647
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1446
1648
  );
1447
1649
  }
1448
1650
  const result = await response.json();
@@ -1528,11 +1730,12 @@ var init_http = __esm({
1528
1730
  * the OTLP carrier has no path to carry it.
1529
1731
  */
1530
1732
  sendInternalTrace(functionId, payload) {
1531
- this.getTraceTransport()?.submit("internal_trace", {
1532
- ...payload,
1533
- functionId,
1534
- sdkVersion: __version__
1535
- });
1733
+ const body = { ...payload, functionId, sdkVersion: __version__ };
1734
+ this.getTraceTransport()?.submit(
1735
+ "internal_trace",
1736
+ body,
1737
+ carrierMeta("internal_trace", body, void 0)
1738
+ );
1536
1739
  }
1537
1740
  /**
1538
1741
  * Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
@@ -1541,10 +1744,11 @@ var init_http = __esm({
1541
1744
  * promise.
1542
1745
  */
1543
1746
  sendExternalSpan(payload) {
1544
- this.getTraceTransport()?.submit("external_span", {
1545
- ...payload,
1546
- sdkVersion: __version__
1547
- });
1747
+ this.getTraceTransport()?.submit(
1748
+ "external_span",
1749
+ { ...payload, sdkVersion: __version__ },
1750
+ this.recordedMeta("external_span", payload, carrierRef(payload))
1751
+ );
1548
1752
  }
1549
1753
  /**
1550
1754
  * Queue an external trace completion (from OpenAI tracing) onto this
@@ -1553,10 +1757,15 @@ var init_http = __esm({
1553
1757
  * server-authoritative barrier in `replay.ts`, not by awaiting this call.
1554
1758
  */
1555
1759
  sendExternalTrace(payload) {
1556
- this.getTraceTransport()?.submit("external_trace", {
1557
- ...payload,
1558
- sdkVersion: __version__
1559
- });
1760
+ this.getTraceTransport()?.submit(
1761
+ "external_trace",
1762
+ { ...payload, sdkVersion: __version__ },
1763
+ this.recordedMeta(
1764
+ "external_trace",
1765
+ payload,
1766
+ payload.completed === true ? carrierRef(payload) : void 0
1767
+ )
1768
+ );
1560
1769
  }
1561
1770
  /**
1562
1771
  * Partial update of an existing trace identified by its Bitfab trace ID.
@@ -2152,7 +2361,9 @@ __export(replay_exports, {
2152
2361
  ReplayError: () => ReplayError,
2153
2362
  replay: () => replay,
2154
2363
  reportReplayProgress: () => reportReplayProgress,
2155
- serializeReplayResult: () => serializeReplayResult
2364
+ serializeReplayResult: () => serializeReplayResult,
2365
+ sleepForReplayPersistence: () => sleepForReplayPersistence,
2366
+ waitForReplayPersistence: () => waitForReplayPersistence
2156
2367
  });
2157
2368
  function dbBranchEnabled(dbBranch) {
2158
2369
  return dbBranch !== void 0 && dbBranch !== false;
@@ -2440,15 +2651,29 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2440
2651
  REPLAY_PERSISTENCE_TIMEOUT_MS
2441
2652
  );
2442
2653
  if (!deferredSettled) {
2654
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2443
2655
  throw new BitfabError(
2444
2656
  `Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
2445
2657
  );
2446
2658
  }
2447
- const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
2448
- if (Object.keys(expectedSpanCounts).length === 0) {
2659
+ if (!httpClient.hasClosedDeliveries(replayedTraceIds)) {
2660
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2449
2661
  return;
2450
2662
  }
2451
2663
  const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2664
+ const deliveries = httpClient.takeTraceDeliveries(replayedTraceIds);
2665
+ const expectedSpanCounts = {};
2666
+ let allDelivered = true;
2667
+ for (const [traceId, delivery] of Object.entries(deliveries)) {
2668
+ if (!delivery.closed) {
2669
+ continue;
2670
+ }
2671
+ expectedSpanCounts[traceId] = delivery.spanCount;
2672
+ allDelivered = allDelivered && delivery.delivered;
2673
+ }
2674
+ if (allDelivered) {
2675
+ return;
2676
+ }
2452
2677
  const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
2453
2678
  let missing = Object.keys(expectedSpanCounts).length;
2454
2679
  while (true) {
@@ -2466,17 +2691,18 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2466
2691
  if (Date.now() >= deadline) {
2467
2692
  break;
2468
2693
  }
2469
- await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
2694
+ await sleepForReplayPersistence(
2695
+ Math.min(100, Math.max(0, deadline - Date.now()))
2696
+ );
2470
2697
  }
2471
2698
  const cause = flushed ? "" : " Delivery was also not confirmed before the flush deadline, so the spans likely never reached the server.";
2472
2699
  throw new BitfabError(
2473
2700
  `Replay traces were not fully persisted before the delivery deadline (testRunId ${testRunId}, missing ${missing} of ${Object.keys(expectedSpanCounts).length} trace(s)).${cause}`
2474
2701
  );
2475
2702
  }
2476
- function sleep(ms) {
2703
+ function sleepForReplayPersistence(ms) {
2477
2704
  return new Promise((resolve) => {
2478
- const timer = setTimeout(resolve, ms);
2479
- unrefTimer(timer);
2705
+ setTimeout(resolve, ms);
2480
2706
  });
2481
2707
  }
2482
2708
  async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted) {
@@ -2557,6 +2783,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2557
2783
  ...registeredOverrides
2558
2784
  ];
2559
2785
  const replayedTraceIds = serverItems.map(() => randomUuid());
2786
+ httpClient.trackTraceDeliveries(replayedTraceIds);
2560
2787
  const tasks = serverItems.map(
2561
2788
  (serverItem, index) => () => processItem(
2562
2789
  httpClient,
@@ -2754,8 +2981,6 @@ var init_replay = __esm({
2754
2981
  init_randomUuid();
2755
2982
  init_replayContext();
2756
2983
  init_serialize();
2757
- init_transport();
2758
- init_unrefTimer();
2759
2984
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
2760
2985
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
2761
2986
  ReplayError = class extends BitfabError {