@bitfab/sdk 0.36.7 → 0.36.8

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/index.cjs CHANGED
@@ -42,7 +42,7 @@ var __version__;
42
42
  var init_version_generated = __esm({
43
43
  "src/version.generated.ts"() {
44
44
  "use strict";
45
- __version__ = "0.36.7";
45
+ __version__ = "0.36.8";
46
46
  }
47
47
  });
48
48
 
@@ -168,10 +168,11 @@ var init_errors = __esm({
168
168
  "src/errors.ts"() {
169
169
  "use strict";
170
170
  BitfabError = class extends Error {
171
- constructor(message, url, status) {
171
+ constructor(message, url, status, retryAfterMs) {
172
172
  super(message);
173
173
  this.url = url;
174
174
  this.status = status;
175
+ this.retryAfterMs = retryAfterMs;
175
176
  this.name = "BitfabError";
176
177
  }
177
178
  };
@@ -526,6 +527,23 @@ var init_serializePayload = __esm({
526
527
  }
527
528
  });
528
529
 
530
+ // src/transportTypes.ts
531
+ var DeliveryError;
532
+ var init_transportTypes = __esm({
533
+ "src/transportTypes.ts"() {
534
+ "use strict";
535
+ DeliveryError = class extends Error {
536
+ constructor(message, options = {}) {
537
+ super(message);
538
+ this.name = "DeliveryError";
539
+ this.retryable = options.retryable ?? false;
540
+ this.oversized = options.oversized ?? false;
541
+ this.retryAfterMs = options.retryAfterMs;
542
+ }
543
+ };
544
+ }
545
+ });
546
+
529
547
  // src/unrefTimer.ts
530
548
  function unrefTimer(timer) {
531
549
  const handle = timer;
@@ -565,59 +583,6 @@ function logError(message, error) {
565
583
  } catch {
566
584
  }
567
585
  }
568
- function recordTraceSubmission(operation, payload) {
569
- const sourceTraceId = resolveSourceTraceId(payload);
570
- if (sourceTraceId === void 0) {
571
- return;
572
- }
573
- if (operation === "external_span") {
574
- const rawSpan = asRecord2(payload.rawSpan);
575
- if (typeof rawSpan?.id !== "string") {
576
- submissionCounter += 1;
577
- }
578
- const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
579
- const existing = traceSubmissionSpanIds.get(sourceTraceId);
580
- if (existing) {
581
- existing.add(sourceSpanId);
582
- } else {
583
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
584
- }
585
- return;
586
- }
587
- if (payload.completed !== true) {
588
- return;
589
- }
590
- if (typeof payload.testRunId === "string") {
591
- replayTraceSubmissions.add(sourceTraceId);
592
- if (!traceSubmissionSpanIds.has(sourceTraceId)) {
593
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
594
- }
595
- } else {
596
- traceSubmissionSpanIds.delete(sourceTraceId);
597
- }
598
- }
599
- function takeReplaySpanCounts(traceIds) {
600
- const counts = {};
601
- for (const traceId of traceIds) {
602
- if (!replayTraceSubmissions.has(traceId)) {
603
- continue;
604
- }
605
- counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
606
- traceSubmissionSpanIds.delete(traceId);
607
- replayTraceSubmissions.delete(traceId);
608
- }
609
- return counts;
610
- }
611
- function asRecord2(value) {
612
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
613
- }
614
- function resolveSourceTraceId(payload) {
615
- if (typeof payload.sourceTraceId === "string") {
616
- return payload.sourceTraceId;
617
- }
618
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
619
- return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
620
- }
621
586
  function otlpValue(value) {
622
587
  if (typeof value === "boolean") {
623
588
  return { boolValue: value };
@@ -675,7 +640,11 @@ function spanToOtlp(span) {
675
640
  }
676
641
  function encodeSpan(span) {
677
642
  const json = JSON.stringify(spanToOtlp(span));
678
- return { json, size: byteLength(json) };
643
+ return {
644
+ json,
645
+ size: byteLength(json),
646
+ ref: carrierRefs.get(span)
647
+ };
679
648
  }
680
649
  function trimEncodedSpan(span) {
681
650
  try {
@@ -758,48 +727,27 @@ async function mapWithConcurrency(items, limit, task) {
758
727
  await Promise.all(workers);
759
728
  return results;
760
729
  }
761
- function responseStatus(error) {
762
- return error instanceof BitfabError ? error.status : void 0;
763
- }
764
730
  function isRetryable(error) {
765
- const status = responseStatus(error);
766
- if (status === void 0) {
767
- return true;
768
- }
769
- return RETRYABLE_STATUSES.has(status) || status >= 500;
770
- }
771
- function endSpan(span, endTime) {
772
- span.end(endTime);
731
+ return error instanceof DeliveryError && error.retryable;
773
732
  }
774
- function spanName(operation, payload) {
775
- if (operation === "external_span") {
776
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
777
- if (typeof spanData?.name === "string") {
778
- return spanData.name;
779
- }
780
- }
781
- if (typeof payload.traceFunctionKey === "string") {
782
- return payload.traceFunctionKey;
783
- }
784
- return `bitfab.${operation}`;
733
+ function isOversized(error) {
734
+ return error instanceof DeliveryError && error.oversized;
785
735
  }
786
- function payloadTimestamp(payload, field) {
787
- const rawSpan = asRecord2(payload.rawSpan);
788
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
789
- const raw = rawSpan?.[field] ?? rawTrace?.[field];
790
- if (typeof raw !== "string") {
791
- return void 0;
736
+ function retryWaitMillis(error, attempt, remainingMillis) {
737
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
738
+ const affordable = remainingMillis / 2;
739
+ if (requested !== void 0) {
740
+ return requested < affordable ? requested : null;
792
741
  }
793
- const parsed = Date.parse(raw);
794
- return Number.isNaN(parsed) ? void 0 : parsed;
742
+ const backoff = Math.min(
743
+ RETRY_BASE_DELAY_MILLIS * 2 ** attempt,
744
+ RETRY_BACKOFF_CEILING_MILLIS
745
+ );
746
+ const jittered = backoff / 2 + Math.random() * (backoff / 2);
747
+ return jittered < affordable ? jittered : null;
795
748
  }
796
- function hasError(payload) {
797
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
798
- if (spanData?.error != null) {
799
- return true;
800
- }
801
- const errors = payload.errors;
802
- return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
749
+ function endSpan(span, endTime) {
750
+ span.end(endTime);
803
751
  }
804
752
  function createOtelTransport(options) {
805
753
  return new OtelBatchTransport({
@@ -838,7 +786,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
838
786
  (transport, remaining) => transport.shutdown(remaining)
839
787
  );
840
788
  }
841
- 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;
789
+ 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;
842
790
  var init_otel = __esm({
843
791
  "src/otel.ts"() {
844
792
  "use strict";
@@ -852,11 +800,11 @@ var init_otel = __esm({
852
800
  init_payloadBudget();
853
801
  init_readEnv();
854
802
  init_serializePayload();
803
+ init_transportTypes();
855
804
  init_unrefTimer();
856
805
  init_warnOnce();
857
806
  OPERATION_ATTRIBUTE = "bitfab.operation";
858
807
  PAYLOAD_ATTRIBUTE = "bitfab.payload";
859
- OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
860
808
  MAX_EXPORT_REQUEST_BYTES = 3e6;
861
809
  MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
862
810
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
@@ -868,25 +816,23 @@ var init_otel = __esm({
868
816
  MAX_EXPORT_CONCURRENCY = 64;
869
817
  SCHEDULE_DELAY_MILLIS = 5e3;
870
818
  EXPORT_TIMEOUT_MILLIS = 3e4;
871
- RETRY_DELAY_MILLIS = 100;
819
+ RETRY_BASE_DELAY_MILLIS = 100;
820
+ RETRY_BACKOFF_CEILING_MILLIS = 5e3;
872
821
  MAX_SEND_ATTEMPTS = 3;
873
822
  DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
874
- RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
875
823
  liveTransports = /* @__PURE__ */ new Set();
876
- traceSubmissionSpanIds = /* @__PURE__ */ new Map();
877
- replayTraceSubmissions = /* @__PURE__ */ new Set();
878
- submissionCounter = 0;
824
+ carrierRefs = /* @__PURE__ */ new WeakMap();
879
825
  SPAN_SEPARATOR_BYTES = 1;
880
- OtlpPayloadTooLargeError = class extends Error {
881
- };
882
- OtlpPartialSuccessError = class extends Error {
883
- };
884
826
  BitfabSpanExporter = class {
885
- constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
827
+ constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency, onDelivered, exportTimeoutMillis = EXPORT_TIMEOUT_MILLIS) {
886
828
  this.directSender = directSender;
887
829
  this.maxRequestBytes = maxRequestBytes;
888
830
  this.maxRequestBatchSize = maxRequestBatchSize;
889
831
  this.exportConcurrency = exportConcurrency;
832
+ this.onDelivered = onDelivered;
833
+ this.exportTimeoutMillis = exportTimeoutMillis;
834
+ /** Epoch ms until which the server has asked this exporter to stay away. */
835
+ this.throttledUntil = 0;
890
836
  }
891
837
  export(spans, resultCallback) {
892
838
  void this.exportAsync(spans).then(
@@ -952,6 +898,7 @@ var init_otel = __esm({
952
898
  );
953
899
  if (prepared.wireBytes <= this.maxRequestBytes) {
954
900
  await this.sendWithRetries(prepared);
901
+ this.reportDelivered(batch.spans);
955
902
  return true;
956
903
  }
957
904
  }
@@ -979,15 +926,12 @@ var init_otel = __esm({
979
926
  alreadyTrimmed = true;
980
927
  }
981
928
  } catch (error) {
982
- if (error instanceof OtlpPayloadTooLargeError) {
929
+ if (isOversized(error)) {
983
930
  logError(
984
931
  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"
985
932
  );
986
933
  return false;
987
934
  }
988
- if (error instanceof OtlpPartialSuccessError) {
989
- return false;
990
- }
991
935
  logError("failed to export an OpenTelemetry span batch", error);
992
936
  return false;
993
937
  }
@@ -1005,37 +949,79 @@ var init_otel = __esm({
1005
949
  * the server does not yet understand. The fix is a client-supplied
1006
950
  * idempotency key that ingestion dedupes on.
1007
951
  */
952
+ /**
953
+ * Remember a throttle the server asked for, so the requests fanned out
954
+ * alongside this one respect it too. Delaying only the request that was
955
+ * refused leaves the other seven in the window hitting a server that just
956
+ * asked for room.
957
+ */
958
+ recordThrottle(error) {
959
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
960
+ if (requested !== void 0) {
961
+ this.throttledUntil = Math.max(
962
+ this.throttledUntil,
963
+ Date.now() + requested
964
+ );
965
+ }
966
+ }
967
+ /**
968
+ * Waits out an active throttle, or reports the batch undeliverable when the
969
+ * throttle outlasts what we are willing to hold it for. Either way nothing is
970
+ * sent while the server has asked us to stay away.
971
+ */
972
+ async awaitThrottle(deadline) {
973
+ const remaining = this.throttledUntil - Date.now();
974
+ if (remaining <= 0) {
975
+ return;
976
+ }
977
+ if (remaining >= (deadline - Date.now()) / 2) {
978
+ throw new DeliveryError(
979
+ `OTLP ingestion is throttled for another ${remaining}ms, longer than the export budget`
980
+ );
981
+ }
982
+ await delay(remaining);
983
+ }
1008
984
  async sendWithRetries(request) {
985
+ const deadline = Date.now() + this.exportTimeoutMillis;
1009
986
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
1010
987
  try {
1011
- const response = await this.directSender(
1012
- OTLP_TRACES_ENDPOINT,
1013
- request,
1014
- EXPORT_TIMEOUT_MILLIS
1015
- );
1016
- const partialSuccess = asRecord2(response?.partialSuccess);
1017
- const rejected = partialSuccess?.rejectedSpans;
1018
- if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1019
- logError(
1020
- `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1021
- );
1022
- throw new OtlpPartialSuccessError();
1023
- }
988
+ await this.awaitThrottle(deadline);
989
+ await this.directSender(request, Math.max(0, deadline - Date.now()));
1024
990
  return;
1025
991
  } catch (error) {
1026
- if (error instanceof OtlpPartialSuccessError) {
992
+ if (isOversized(error)) {
1027
993
  throw error;
1028
994
  }
1029
- if (responseStatus(error) === 413) {
1030
- throw new OtlpPayloadTooLargeError();
1031
- }
995
+ this.recordThrottle(error);
1032
996
  if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
1033
997
  throw error;
1034
998
  }
1035
- await delay(RETRY_DELAY_MILLIS);
999
+ const wait = retryWaitMillis(error, attempt, deadline - Date.now());
1000
+ if (wait === null) {
1001
+ throw error;
1002
+ }
1003
+ await delay(wait);
1036
1004
  }
1037
1005
  }
1038
1006
  }
1007
+ /**
1008
+ * Announce the carriers a request delivered. Wrapped because a listener that
1009
+ * throws must never turn a delivered batch into a failed export.
1010
+ */
1011
+ reportDelivered(spans) {
1012
+ if (this.onDelivered === void 0) {
1013
+ return;
1014
+ }
1015
+ const refs = spans.map((span) => span.ref).filter((ref) => ref !== void 0);
1016
+ if (refs.length === 0) {
1017
+ return;
1018
+ }
1019
+ try {
1020
+ this.onDelivered(refs);
1021
+ } catch (error) {
1022
+ logError("a delivery listener threw", error);
1023
+ }
1024
+ }
1039
1025
  async shutdown() {
1040
1026
  }
1041
1027
  async forceFlush() {
@@ -1093,7 +1079,9 @@ var init_otel = __esm({
1093
1079
  options.directSender,
1094
1080
  maxRequestBytes,
1095
1081
  maxRequestBatchSize,
1096
- options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
1082
+ options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY,
1083
+ options.onDelivered,
1084
+ options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
1097
1085
  )
1098
1086
  );
1099
1087
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
@@ -1117,8 +1105,7 @@ var init_otel = __esm({
1117
1105
  this.tracer = this.provider.getTracer("bitfab", __version__);
1118
1106
  liveTransports.add(this);
1119
1107
  }
1120
- submit(operation, payload) {
1121
- recordTraceSubmission(operation, payload);
1108
+ submit(operation, payload, meta = {}) {
1122
1109
  if (this.closed) {
1123
1110
  warnOnce(
1124
1111
  "otel-submit-after-shutdown",
@@ -1139,17 +1126,20 @@ var init_otel = __esm({
1139
1126
  ].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
1140
1127
  );
1141
1128
  }
1142
- const span = this.tracer.startSpan(spanName(operation, payload), {
1129
+ const span = this.tracer.startSpan(meta.name ?? `bitfab.${operation}`, {
1143
1130
  attributes: {
1144
1131
  [OPERATION_ATTRIBUTE]: operation,
1145
1132
  [PAYLOAD_ATTRIBUTE]: body
1146
1133
  },
1147
- startTime: payloadTimestamp(payload, "started_at")
1134
+ startTime: meta.startTime
1148
1135
  });
1149
- if (hasError(payload)) {
1136
+ if (meta.ref !== void 0) {
1137
+ carrierRefs.set(span, meta.ref);
1138
+ }
1139
+ if (meta.errored === true) {
1150
1140
  span.setStatus({ code: import_api.SpanStatusCode.ERROR });
1151
1141
  }
1152
- endSpan(span, payloadTimestamp(payload, "ended_at"));
1142
+ endSpan(span, meta.endTime);
1153
1143
  } catch (error) {
1154
1144
  logError("failed to queue an OpenTelemetry span", error);
1155
1145
  }
@@ -1199,9 +1189,6 @@ function flushTraceTransports(timeoutMs) {
1199
1189
  function shutdownTraceTransports(timeoutMs) {
1200
1190
  return shutdownOtelTransports(timeoutMs);
1201
1191
  }
1202
- function takeReplaySpanCounts2(traceIds) {
1203
- return takeReplaySpanCounts(traceIds);
1204
- }
1205
1192
  var init_transport = __esm({
1206
1193
  "src/transport.ts"() {
1207
1194
  "use strict";
@@ -1250,7 +1237,96 @@ async function waitForPromises(promises, timeoutMs) {
1250
1237
  }
1251
1238
  }
1252
1239
  }
1253
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, HttpClient;
1240
+ function readHeader(response, name) {
1241
+ try {
1242
+ return response.headers?.get(name) ?? null;
1243
+ } catch {
1244
+ return null;
1245
+ }
1246
+ }
1247
+ function parseRetryAfterMs(header) {
1248
+ const value = header?.trim();
1249
+ if (!value) {
1250
+ return void 0;
1251
+ }
1252
+ const seconds = Number(value);
1253
+ if (Number.isFinite(seconds)) {
1254
+ return seconds >= 0 ? seconds * 1e3 : void 0;
1255
+ }
1256
+ const at = Date.parse(value);
1257
+ if (Number.isNaN(at)) {
1258
+ return void 0;
1259
+ }
1260
+ return Math.max(0, at - Date.now());
1261
+ }
1262
+ function carrierMeta(operation, payload, ref) {
1263
+ return {
1264
+ ref,
1265
+ name: carrierName(operation, payload),
1266
+ startTime: payloadTimestamp(payload, "started_at"),
1267
+ endTime: payloadTimestamp(payload, "ended_at"),
1268
+ errored: payloadHasError(payload)
1269
+ };
1270
+ }
1271
+ function carrierName(operation, payload) {
1272
+ if (operation === "external_span") {
1273
+ const spanData = asPayloadRecord(
1274
+ asPayloadRecord(payload.rawSpan)?.span_data
1275
+ );
1276
+ if (typeof spanData?.name === "string") {
1277
+ return spanData.name;
1278
+ }
1279
+ }
1280
+ if (typeof payload.traceFunctionKey === "string") {
1281
+ return payload.traceFunctionKey;
1282
+ }
1283
+ return `bitfab.${operation}`;
1284
+ }
1285
+ function payloadTimestamp(payload, field) {
1286
+ const rawSpan = asPayloadRecord(payload.rawSpan);
1287
+ const rawTrace = asPayloadRecord(payload.externalTrace) ?? asPayloadRecord(payload.rawTrace);
1288
+ const raw = rawSpan?.[field] ?? rawTrace?.[field];
1289
+ if (typeof raw !== "string") {
1290
+ return void 0;
1291
+ }
1292
+ const parsed = Date.parse(raw);
1293
+ return Number.isNaN(parsed) ? void 0 : parsed;
1294
+ }
1295
+ function payloadHasError(payload) {
1296
+ const spanData = asPayloadRecord(asPayloadRecord(payload.rawSpan)?.span_data);
1297
+ if (spanData?.error != null) {
1298
+ return true;
1299
+ }
1300
+ const errors = payload.errors;
1301
+ return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
1302
+ }
1303
+ function asPayloadRecord(value) {
1304
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1305
+ }
1306
+ function carrierRef(payload) {
1307
+ const traceId = sourceTraceIdOf(payload);
1308
+ if (traceId === void 0) {
1309
+ return void 0;
1310
+ }
1311
+ const rawSpan = payload.rawSpan;
1312
+ if (rawSpan === void 0) {
1313
+ return { traceId };
1314
+ }
1315
+ const spanId = rawSpan?.id;
1316
+ return {
1317
+ traceId,
1318
+ spanId: typeof spanId === "string" ? spanId : `submission-${++carrierSeq}`
1319
+ };
1320
+ }
1321
+ function sourceTraceIdOf(payload) {
1322
+ if (typeof payload.sourceTraceId === "string") {
1323
+ return payload.sourceTraceId;
1324
+ }
1325
+ const rawTrace = payload.externalTrace ?? payload.rawTrace;
1326
+ const id = rawTrace?.id;
1327
+ return typeof id === "string" ? id : void 0;
1328
+ }
1329
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, carrierSeq, HttpClient;
1254
1330
  var init_http = __esm({
1255
1331
  "src/http.ts"() {
1256
1332
  "use strict";
@@ -1260,9 +1336,12 @@ var init_http = __esm({
1260
1336
  init_replayContext();
1261
1337
  init_serializePayload();
1262
1338
  init_transport();
1339
+ init_transportTypes();
1263
1340
  init_unrefTimer();
1264
1341
  init_warnOnce();
1265
1342
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
1343
+ OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
1344
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1266
1345
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
1267
1346
  DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
1268
1347
  pendingTracePromises = /* @__PURE__ */ new Set();
@@ -1282,8 +1361,12 @@ var init_http = __esm({
1282
1361
  });
1283
1362
  });
1284
1363
  }
1364
+ carrierSeq = 0;
1285
1365
  HttpClient = class {
1286
1366
  constructor(config) {
1367
+ // Only traces a caller asked about are tracked, so ordinary tracing stores
1368
+ // nothing here.
1369
+ this.traceDeliveries = /* @__PURE__ */ new Map();
1287
1370
  // Deferred span work owned by THIS client. The module-global set backs the
1288
1371
  // process-wide `flushTraces()` and the exit hook, but per-client lifecycle
1289
1372
  // must not wait on another client's slow finalize: a false `close()` failure
@@ -1319,13 +1402,131 @@ var init_http = __esm({
1319
1402
  }
1320
1403
  if (!this.traceTransport) {
1321
1404
  this.traceTransport = createTraceTransport({
1322
- directSender: (endpoint, request, timeoutMs) => this.sendPrepared(endpoint, request, {
1323
- timeout: timeoutMs
1324
- })
1405
+ directSender: (request, timeoutMs) => this.deliverCarriers(request, timeoutMs),
1406
+ onDelivered: (refs) => this.recordDeliveredCarriers(refs)
1325
1407
  });
1326
1408
  }
1327
1409
  return this.traceTransport;
1328
1410
  }
1411
+ /**
1412
+ * Post one encoded batch and decide what the server's answer means, so the
1413
+ * transport never reads a response. Rejections and permanent statuses come
1414
+ * back as a non-retryable {@link DeliveryError}; anything the server might
1415
+ * still accept on a second try comes back retryable.
1416
+ */
1417
+ async deliverCarriers(request, timeoutMs) {
1418
+ let response;
1419
+ try {
1420
+ response = await this.sendPrepared(
1421
+ OTLP_TRACES_ENDPOINT,
1422
+ request,
1423
+ { timeout: timeoutMs }
1424
+ );
1425
+ } catch (error) {
1426
+ const status = error instanceof BitfabError ? error.status : void 0;
1427
+ if (status === void 0) {
1428
+ throw new DeliveryError(`OTLP ingestion failed: ${String(error)}`, {
1429
+ retryable: true
1430
+ });
1431
+ }
1432
+ throw new DeliveryError(`OTLP ingestion failed with HTTP ${status}`, {
1433
+ retryable: RETRYABLE_STATUSES.has(status),
1434
+ oversized: status === 413,
1435
+ ...error instanceof BitfabError && error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
1436
+ });
1437
+ }
1438
+ const partialSuccess = asPayloadRecord(response?.partialSuccess);
1439
+ const rejected = partialSuccess?.rejectedSpans;
1440
+ if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1441
+ throw new DeliveryError(
1442
+ `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1443
+ );
1444
+ }
1445
+ }
1446
+ /**
1447
+ * Start tracking delivery for `traceIds`. Nothing is recorded for a trace
1448
+ * that was never tracked, so ordinary tracing costs no bookkeeping at all.
1449
+ */
1450
+ trackTraceDeliveries(traceIds) {
1451
+ for (const traceId of traceIds) {
1452
+ if (!this.traceDeliveries.has(traceId)) {
1453
+ this.traceDeliveries.set(traceId, {
1454
+ submittedSpanIds: /* @__PURE__ */ new Set(),
1455
+ ackedSpanIds: /* @__PURE__ */ new Set(),
1456
+ closed: false,
1457
+ closingAcked: false
1458
+ });
1459
+ }
1460
+ }
1461
+ }
1462
+ /** Whether any tracked trace has had its closing carrier submitted. */
1463
+ hasClosedDeliveries(traceIds) {
1464
+ return traceIds.some((traceId) => this.traceDeliveries.get(traceId)?.closed);
1465
+ }
1466
+ /**
1467
+ * Report what each tracked trace submitted and whether the server confirmed
1468
+ * it, and stop tracking them. Every id passed is freed, so a caller cannot
1469
+ * leak a record for a trace that never closed.
1470
+ *
1471
+ * `delivered` is only meaningful once a flush has settled: acks land before
1472
+ * an export resolves, so a flush that reported success has already collected
1473
+ * every ack it is going to collect.
1474
+ */
1475
+ takeTraceDeliveries(traceIds) {
1476
+ const reports = {};
1477
+ for (const traceId of traceIds) {
1478
+ const delivery = this.traceDeliveries.get(traceId);
1479
+ if (delivery === void 0) {
1480
+ continue;
1481
+ }
1482
+ this.traceDeliveries.delete(traceId);
1483
+ reports[traceId] = {
1484
+ spanCount: delivery.submittedSpanIds.size,
1485
+ closed: delivery.closed,
1486
+ delivered: delivery.closingAcked && [...delivery.submittedSpanIds].every(
1487
+ (spanId) => delivery.ackedSpanIds.has(spanId)
1488
+ )
1489
+ };
1490
+ }
1491
+ return reports;
1492
+ }
1493
+ /** Build a carrier's meta and record what it adds to its trace's expected set. */
1494
+ recordedMeta(operation, payload, ref) {
1495
+ this.recordSubmittedCarrier(ref);
1496
+ return carrierMeta(operation, payload, ref);
1497
+ }
1498
+ recordSubmittedCarrier(ref) {
1499
+ if (ref === void 0) {
1500
+ return;
1501
+ }
1502
+ const delivery = this.traceDeliveries.get(ref.traceId);
1503
+ if (delivery === void 0) {
1504
+ return;
1505
+ }
1506
+ if (ref.spanId === void 0) {
1507
+ delivery.closed = true;
1508
+ } else {
1509
+ delivery.submittedSpanIds.add(ref.spanId);
1510
+ }
1511
+ }
1512
+ /**
1513
+ * Ingestion commits every carrier in a request before it answers, so a
1514
+ * delivered ref is proof its row exists: the same fact the replay status
1515
+ * endpoint would report, already in hand.
1516
+ */
1517
+ recordDeliveredCarriers(refs) {
1518
+ for (const ref of refs) {
1519
+ const delivery = this.traceDeliveries.get(ref.traceId);
1520
+ if (delivery === void 0) {
1521
+ continue;
1522
+ }
1523
+ if (ref.spanId === void 0) {
1524
+ delivery.closingAcked = true;
1525
+ } else {
1526
+ delivery.ackedSpanIds.add(ref.spanId);
1527
+ }
1528
+ }
1529
+ }
1329
1530
  /**
1330
1531
  * Track deferred span work so this client's own lifecycle waits for it, and
1331
1532
  * so the process-wide flush and exit hook do too.
@@ -1435,7 +1636,8 @@ var init_http = __esm({
1435
1636
  throw new BitfabError(
1436
1637
  `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1437
1638
  void 0,
1438
- response.status
1639
+ response.status,
1640
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1439
1641
  );
1440
1642
  }
1441
1643
  const result = await response.json();
@@ -1521,11 +1723,12 @@ var init_http = __esm({
1521
1723
  * the OTLP carrier has no path to carry it.
1522
1724
  */
1523
1725
  sendInternalTrace(functionId, payload) {
1524
- this.getTraceTransport()?.submit("internal_trace", {
1525
- ...payload,
1526
- functionId,
1527
- sdkVersion: __version__
1528
- });
1726
+ const body = { ...payload, functionId, sdkVersion: __version__ };
1727
+ this.getTraceTransport()?.submit(
1728
+ "internal_trace",
1729
+ body,
1730
+ carrierMeta("internal_trace", body, void 0)
1731
+ );
1529
1732
  }
1530
1733
  /**
1531
1734
  * Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
@@ -1534,10 +1737,11 @@ var init_http = __esm({
1534
1737
  * promise.
1535
1738
  */
1536
1739
  sendExternalSpan(payload) {
1537
- this.getTraceTransport()?.submit("external_span", {
1538
- ...payload,
1539
- sdkVersion: __version__
1540
- });
1740
+ this.getTraceTransport()?.submit(
1741
+ "external_span",
1742
+ { ...payload, sdkVersion: __version__ },
1743
+ this.recordedMeta("external_span", payload, carrierRef(payload))
1744
+ );
1541
1745
  }
1542
1746
  /**
1543
1747
  * Queue an external trace completion (from OpenAI tracing) onto this
@@ -1546,10 +1750,15 @@ var init_http = __esm({
1546
1750
  * server-authoritative barrier in `replay.ts`, not by awaiting this call.
1547
1751
  */
1548
1752
  sendExternalTrace(payload) {
1549
- this.getTraceTransport()?.submit("external_trace", {
1550
- ...payload,
1551
- sdkVersion: __version__
1552
- });
1753
+ this.getTraceTransport()?.submit(
1754
+ "external_trace",
1755
+ { ...payload, sdkVersion: __version__ },
1756
+ this.recordedMeta(
1757
+ "external_trace",
1758
+ payload,
1759
+ payload.completed === true ? carrierRef(payload) : void 0
1760
+ )
1761
+ );
1553
1762
  }
1554
1763
  /**
1555
1764
  * Partial update of an existing trace identified by its Bitfab trace ID.
@@ -2145,7 +2354,8 @@ __export(replay_exports, {
2145
2354
  ReplayError: () => ReplayError,
2146
2355
  replay: () => replay,
2147
2356
  reportReplayProgress: () => reportReplayProgress,
2148
- serializeReplayResult: () => serializeReplayResult
2357
+ serializeReplayResult: () => serializeReplayResult,
2358
+ waitForReplayPersistence: () => waitForReplayPersistence
2149
2359
  });
2150
2360
  function dbBranchEnabled(dbBranch) {
2151
2361
  return dbBranch !== void 0 && dbBranch !== false;
@@ -2433,15 +2643,29 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2433
2643
  REPLAY_PERSISTENCE_TIMEOUT_MS
2434
2644
  );
2435
2645
  if (!deferredSettled) {
2646
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2436
2647
  throw new BitfabError(
2437
2648
  `Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
2438
2649
  );
2439
2650
  }
2440
- const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
2441
- if (Object.keys(expectedSpanCounts).length === 0) {
2651
+ if (!httpClient.hasClosedDeliveries(replayedTraceIds)) {
2652
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2442
2653
  return;
2443
2654
  }
2444
2655
  const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2656
+ const deliveries = httpClient.takeTraceDeliveries(replayedTraceIds);
2657
+ const expectedSpanCounts = {};
2658
+ let allDelivered = true;
2659
+ for (const [traceId, delivery] of Object.entries(deliveries)) {
2660
+ if (!delivery.closed) {
2661
+ continue;
2662
+ }
2663
+ expectedSpanCounts[traceId] = delivery.spanCount;
2664
+ allDelivered = allDelivered && delivery.delivered;
2665
+ }
2666
+ if (allDelivered) {
2667
+ return;
2668
+ }
2445
2669
  const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
2446
2670
  let missing = Object.keys(expectedSpanCounts).length;
2447
2671
  while (true) {
@@ -2550,6 +2774,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2550
2774
  ...registeredOverrides
2551
2775
  ];
2552
2776
  const replayedTraceIds = serverItems.map(() => randomUuid());
2777
+ httpClient.trackTraceDeliveries(replayedTraceIds);
2553
2778
  const tasks = serverItems.map(
2554
2779
  (serverItem, index) => () => processItem(
2555
2780
  httpClient,
@@ -2747,7 +2972,6 @@ var init_replay = __esm({
2747
2972
  init_randomUuid();
2748
2973
  init_replayContext();
2749
2974
  init_serialize();
2750
- init_transport();
2751
2975
  init_unrefTimer();
2752
2976
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
2753
2977
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";