bitfab 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
@@ -97,7 +97,7 @@ var __version__;
97
97
  var init_version_generated = __esm({
98
98
  "src/version.generated.ts"() {
99
99
  "use strict";
100
- __version__ = "0.36.7";
100
+ __version__ = "0.36.9";
101
101
  }
102
102
  });
103
103
 
@@ -223,10 +223,11 @@ var init_errors = __esm({
223
223
  "src/errors.ts"() {
224
224
  "use strict";
225
225
  BitfabError = class extends Error {
226
- constructor(message, url, status) {
226
+ constructor(message, url, status, retryAfterMs) {
227
227
  super(message);
228
228
  this.url = url;
229
229
  this.status = status;
230
+ this.retryAfterMs = retryAfterMs;
230
231
  this.name = "BitfabError";
231
232
  }
232
233
  };
@@ -542,6 +543,23 @@ var init_serializePayload = __esm({
542
543
  }
543
544
  });
544
545
 
546
+ // src/transportTypes.ts
547
+ var DeliveryError;
548
+ var init_transportTypes = __esm({
549
+ "src/transportTypes.ts"() {
550
+ "use strict";
551
+ DeliveryError = class extends Error {
552
+ constructor(message, options = {}) {
553
+ super(message);
554
+ this.name = "DeliveryError";
555
+ this.retryable = options.retryable ?? false;
556
+ this.oversized = options.oversized ?? false;
557
+ this.retryAfterMs = options.retryAfterMs;
558
+ }
559
+ };
560
+ }
561
+ });
562
+
545
563
  // src/unrefTimer.ts
546
564
  function unrefTimer(timer) {
547
565
  const handle = timer;
@@ -581,59 +599,6 @@ function logError(message, error) {
581
599
  } catch {
582
600
  }
583
601
  }
584
- function recordTraceSubmission(operation, payload) {
585
- const sourceTraceId = resolveSourceTraceId(payload);
586
- if (sourceTraceId === void 0) {
587
- return;
588
- }
589
- if (operation === "external_span") {
590
- const rawSpan = asRecord2(payload.rawSpan);
591
- if (typeof rawSpan?.id !== "string") {
592
- submissionCounter += 1;
593
- }
594
- const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
595
- const existing = traceSubmissionSpanIds.get(sourceTraceId);
596
- if (existing) {
597
- existing.add(sourceSpanId);
598
- } else {
599
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
600
- }
601
- return;
602
- }
603
- if (payload.completed !== true) {
604
- return;
605
- }
606
- if (typeof payload.testRunId === "string") {
607
- replayTraceSubmissions.add(sourceTraceId);
608
- if (!traceSubmissionSpanIds.has(sourceTraceId)) {
609
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
610
- }
611
- } else {
612
- traceSubmissionSpanIds.delete(sourceTraceId);
613
- }
614
- }
615
- function takeReplaySpanCounts(traceIds) {
616
- const counts = {};
617
- for (const traceId of traceIds) {
618
- if (!replayTraceSubmissions.has(traceId)) {
619
- continue;
620
- }
621
- counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
622
- traceSubmissionSpanIds.delete(traceId);
623
- replayTraceSubmissions.delete(traceId);
624
- }
625
- return counts;
626
- }
627
- function asRecord2(value) {
628
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
629
- }
630
- function resolveSourceTraceId(payload) {
631
- if (typeof payload.sourceTraceId === "string") {
632
- return payload.sourceTraceId;
633
- }
634
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
635
- return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
636
- }
637
602
  function otlpValue(value) {
638
603
  if (typeof value === "boolean") {
639
604
  return { boolValue: value };
@@ -691,7 +656,11 @@ function spanToOtlp(span) {
691
656
  }
692
657
  function encodeSpan(span) {
693
658
  const json = JSON.stringify(spanToOtlp(span));
694
- return { json, size: byteLength(json) };
659
+ return {
660
+ json,
661
+ size: byteLength(json),
662
+ ref: carrierRefs.get(span)
663
+ };
695
664
  }
696
665
  function trimEncodedSpan(span) {
697
666
  try {
@@ -774,48 +743,27 @@ async function mapWithConcurrency(items, limit, task) {
774
743
  await Promise.all(workers);
775
744
  return results;
776
745
  }
777
- function responseStatus(error) {
778
- return error instanceof BitfabError ? error.status : void 0;
779
- }
780
746
  function isRetryable(error) {
781
- const status = responseStatus(error);
782
- if (status === void 0) {
783
- return true;
784
- }
785
- return RETRYABLE_STATUSES.has(status) || status >= 500;
747
+ return error instanceof DeliveryError && error.retryable;
786
748
  }
787
- function endSpan(span, endTime) {
788
- span.end(endTime);
749
+ function isOversized(error) {
750
+ return error instanceof DeliveryError && error.oversized;
789
751
  }
790
- function spanName(operation, payload) {
791
- if (operation === "external_span") {
792
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
793
- if (typeof spanData?.name === "string") {
794
- return spanData.name;
795
- }
752
+ function retryWaitMillis(error, attempt, remainingMillis) {
753
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
754
+ const affordable = remainingMillis / 2;
755
+ if (requested !== void 0) {
756
+ return requested < affordable ? requested : null;
796
757
  }
797
- if (typeof payload.traceFunctionKey === "string") {
798
- return payload.traceFunctionKey;
799
- }
800
- return `bitfab.${operation}`;
801
- }
802
- function payloadTimestamp(payload, field) {
803
- const rawSpan = asRecord2(payload.rawSpan);
804
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
805
- const raw = rawSpan?.[field] ?? rawTrace?.[field];
806
- if (typeof raw !== "string") {
807
- return void 0;
808
- }
809
- const parsed = Date.parse(raw);
810
- return Number.isNaN(parsed) ? void 0 : parsed;
758
+ const backoff = Math.min(
759
+ RETRY_BASE_DELAY_MILLIS * 2 ** attempt,
760
+ RETRY_BACKOFF_CEILING_MILLIS
761
+ );
762
+ const jittered = backoff / 2 + Math.random() * (backoff / 2);
763
+ return jittered < affordable ? jittered : null;
811
764
  }
812
- function hasError(payload) {
813
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
814
- if (spanData?.error != null) {
815
- return true;
816
- }
817
- const errors = payload.errors;
818
- return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
765
+ function endSpan(span, endTime) {
766
+ span.end(endTime);
819
767
  }
820
768
  function createOtelTransport(options) {
821
769
  return new OtelBatchTransport({
@@ -854,7 +802,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
854
802
  (transport, remaining) => transport.shutdown(remaining)
855
803
  );
856
804
  }
857
- 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;
805
+ 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;
858
806
  var init_otel = __esm({
859
807
  "src/otel.ts"() {
860
808
  "use strict";
@@ -868,11 +816,11 @@ var init_otel = __esm({
868
816
  init_payloadBudget();
869
817
  init_readEnv();
870
818
  init_serializePayload();
819
+ init_transportTypes();
871
820
  init_unrefTimer();
872
821
  init_warnOnce();
873
822
  OPERATION_ATTRIBUTE = "bitfab.operation";
874
823
  PAYLOAD_ATTRIBUTE = "bitfab.payload";
875
- OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
876
824
  MAX_EXPORT_REQUEST_BYTES = 3e6;
877
825
  MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
878
826
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
@@ -884,25 +832,23 @@ var init_otel = __esm({
884
832
  MAX_EXPORT_CONCURRENCY = 64;
885
833
  SCHEDULE_DELAY_MILLIS = 5e3;
886
834
  EXPORT_TIMEOUT_MILLIS = 3e4;
887
- RETRY_DELAY_MILLIS = 100;
835
+ RETRY_BASE_DELAY_MILLIS = 100;
836
+ RETRY_BACKOFF_CEILING_MILLIS = 5e3;
888
837
  MAX_SEND_ATTEMPTS = 3;
889
838
  DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
890
- RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
891
839
  liveTransports = /* @__PURE__ */ new Set();
892
- traceSubmissionSpanIds = /* @__PURE__ */ new Map();
893
- replayTraceSubmissions = /* @__PURE__ */ new Set();
894
- submissionCounter = 0;
840
+ carrierRefs = /* @__PURE__ */ new WeakMap();
895
841
  SPAN_SEPARATOR_BYTES = 1;
896
- OtlpPayloadTooLargeError = class extends Error {
897
- };
898
- OtlpPartialSuccessError = class extends Error {
899
- };
900
842
  BitfabSpanExporter = class {
901
- constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
843
+ constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency, onDelivered, exportTimeoutMillis = EXPORT_TIMEOUT_MILLIS) {
902
844
  this.directSender = directSender;
903
845
  this.maxRequestBytes = maxRequestBytes;
904
846
  this.maxRequestBatchSize = maxRequestBatchSize;
905
847
  this.exportConcurrency = exportConcurrency;
848
+ this.onDelivered = onDelivered;
849
+ this.exportTimeoutMillis = exportTimeoutMillis;
850
+ /** Epoch ms until which the server has asked this exporter to stay away. */
851
+ this.throttledUntil = 0;
906
852
  }
907
853
  export(spans, resultCallback) {
908
854
  void this.exportAsync(spans).then(
@@ -968,6 +914,7 @@ var init_otel = __esm({
968
914
  );
969
915
  if (prepared.wireBytes <= this.maxRequestBytes) {
970
916
  await this.sendWithRetries(prepared);
917
+ this.reportDelivered(batch.spans);
971
918
  return true;
972
919
  }
973
920
  }
@@ -995,15 +942,12 @@ var init_otel = __esm({
995
942
  alreadyTrimmed = true;
996
943
  }
997
944
  } catch (error) {
998
- if (error instanceof OtlpPayloadTooLargeError) {
945
+ if (isOversized(error)) {
999
946
  logError(
1000
947
  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"
1001
948
  );
1002
949
  return false;
1003
950
  }
1004
- if (error instanceof OtlpPartialSuccessError) {
1005
- return false;
1006
- }
1007
951
  logError("failed to export an OpenTelemetry span batch", error);
1008
952
  return false;
1009
953
  }
@@ -1021,37 +965,79 @@ var init_otel = __esm({
1021
965
  * the server does not yet understand. The fix is a client-supplied
1022
966
  * idempotency key that ingestion dedupes on.
1023
967
  */
968
+ /**
969
+ * Remember a throttle the server asked for, so the requests fanned out
970
+ * alongside this one respect it too. Delaying only the request that was
971
+ * refused leaves the other seven in the window hitting a server that just
972
+ * asked for room.
973
+ */
974
+ recordThrottle(error) {
975
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
976
+ if (requested !== void 0) {
977
+ this.throttledUntil = Math.max(
978
+ this.throttledUntil,
979
+ Date.now() + requested
980
+ );
981
+ }
982
+ }
983
+ /**
984
+ * Waits out an active throttle, or reports the batch undeliverable when the
985
+ * throttle outlasts what we are willing to hold it for. Either way nothing is
986
+ * sent while the server has asked us to stay away.
987
+ */
988
+ async awaitThrottle(deadline) {
989
+ const remaining = this.throttledUntil - Date.now();
990
+ if (remaining <= 0) {
991
+ return;
992
+ }
993
+ if (remaining >= (deadline - Date.now()) / 2) {
994
+ throw new DeliveryError(
995
+ `OTLP ingestion is throttled for another ${remaining}ms, longer than the export budget`
996
+ );
997
+ }
998
+ await delay(remaining);
999
+ }
1024
1000
  async sendWithRetries(request) {
1001
+ const deadline = Date.now() + this.exportTimeoutMillis;
1025
1002
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
1026
1003
  try {
1027
- const response = await this.directSender(
1028
- OTLP_TRACES_ENDPOINT,
1029
- request,
1030
- EXPORT_TIMEOUT_MILLIS
1031
- );
1032
- const partialSuccess = asRecord2(response?.partialSuccess);
1033
- const rejected = partialSuccess?.rejectedSpans;
1034
- if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1035
- logError(
1036
- `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1037
- );
1038
- throw new OtlpPartialSuccessError();
1039
- }
1004
+ await this.awaitThrottle(deadline);
1005
+ await this.directSender(request, Math.max(0, deadline - Date.now()));
1040
1006
  return;
1041
1007
  } catch (error) {
1042
- if (error instanceof OtlpPartialSuccessError) {
1008
+ if (isOversized(error)) {
1043
1009
  throw error;
1044
1010
  }
1045
- if (responseStatus(error) === 413) {
1046
- throw new OtlpPayloadTooLargeError();
1047
- }
1011
+ this.recordThrottle(error);
1048
1012
  if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
1049
1013
  throw error;
1050
1014
  }
1051
- await delay(RETRY_DELAY_MILLIS);
1015
+ const wait = retryWaitMillis(error, attempt, deadline - Date.now());
1016
+ if (wait === null) {
1017
+ throw error;
1018
+ }
1019
+ await delay(wait);
1052
1020
  }
1053
1021
  }
1054
1022
  }
1023
+ /**
1024
+ * Announce the carriers a request delivered. Wrapped because a listener that
1025
+ * throws must never turn a delivered batch into a failed export.
1026
+ */
1027
+ reportDelivered(spans) {
1028
+ if (this.onDelivered === void 0) {
1029
+ return;
1030
+ }
1031
+ const refs = spans.map((span) => span.ref).filter((ref) => ref !== void 0);
1032
+ if (refs.length === 0) {
1033
+ return;
1034
+ }
1035
+ try {
1036
+ this.onDelivered(refs);
1037
+ } catch (error) {
1038
+ logError("a delivery listener threw", error);
1039
+ }
1040
+ }
1055
1041
  async shutdown() {
1056
1042
  }
1057
1043
  async forceFlush() {
@@ -1109,7 +1095,9 @@ var init_otel = __esm({
1109
1095
  options.directSender,
1110
1096
  maxRequestBytes,
1111
1097
  maxRequestBatchSize,
1112
- options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
1098
+ options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY,
1099
+ options.onDelivered,
1100
+ options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
1113
1101
  )
1114
1102
  );
1115
1103
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
@@ -1133,8 +1121,7 @@ var init_otel = __esm({
1133
1121
  this.tracer = this.provider.getTracer("bitfab", __version__);
1134
1122
  liveTransports.add(this);
1135
1123
  }
1136
- submit(operation, payload) {
1137
- recordTraceSubmission(operation, payload);
1124
+ submit(operation, payload, meta = {}) {
1138
1125
  if (this.closed) {
1139
1126
  warnOnce(
1140
1127
  "otel-submit-after-shutdown",
@@ -1155,17 +1142,20 @@ var init_otel = __esm({
1155
1142
  ].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
1156
1143
  );
1157
1144
  }
1158
- const span = this.tracer.startSpan(spanName(operation, payload), {
1145
+ const span = this.tracer.startSpan(meta.name ?? `bitfab.${operation}`, {
1159
1146
  attributes: {
1160
1147
  [OPERATION_ATTRIBUTE]: operation,
1161
1148
  [PAYLOAD_ATTRIBUTE]: body
1162
1149
  },
1163
- startTime: payloadTimestamp(payload, "started_at")
1150
+ startTime: meta.startTime
1164
1151
  });
1165
- if (hasError(payload)) {
1152
+ if (meta.ref !== void 0) {
1153
+ carrierRefs.set(span, meta.ref);
1154
+ }
1155
+ if (meta.errored === true) {
1166
1156
  span.setStatus({ code: import_api.SpanStatusCode.ERROR });
1167
1157
  }
1168
- endSpan(span, payloadTimestamp(payload, "ended_at"));
1158
+ endSpan(span, meta.endTime);
1169
1159
  } catch (error) {
1170
1160
  logError("failed to queue an OpenTelemetry span", error);
1171
1161
  }
@@ -1215,9 +1205,6 @@ function flushTraceTransports(timeoutMs) {
1215
1205
  function shutdownTraceTransports(timeoutMs) {
1216
1206
  return shutdownOtelTransports(timeoutMs);
1217
1207
  }
1218
- function takeReplaySpanCounts2(traceIds) {
1219
- return takeReplaySpanCounts(traceIds);
1220
- }
1221
1208
  var init_transport = __esm({
1222
1209
  "src/transport.ts"() {
1223
1210
  "use strict";
@@ -1266,7 +1253,96 @@ async function waitForPromises(promises, timeoutMs) {
1266
1253
  }
1267
1254
  }
1268
1255
  }
1269
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, HttpClient;
1256
+ function readHeader(response, name) {
1257
+ try {
1258
+ return response.headers?.get(name) ?? null;
1259
+ } catch {
1260
+ return null;
1261
+ }
1262
+ }
1263
+ function parseRetryAfterMs(header) {
1264
+ const value = header?.trim();
1265
+ if (!value) {
1266
+ return void 0;
1267
+ }
1268
+ const seconds = Number(value);
1269
+ if (Number.isFinite(seconds)) {
1270
+ return seconds >= 0 ? seconds * 1e3 : void 0;
1271
+ }
1272
+ const at = Date.parse(value);
1273
+ if (Number.isNaN(at)) {
1274
+ return void 0;
1275
+ }
1276
+ return Math.max(0, at - Date.now());
1277
+ }
1278
+ function carrierMeta(operation, payload, ref) {
1279
+ return {
1280
+ ref,
1281
+ name: carrierName(operation, payload),
1282
+ startTime: payloadTimestamp(payload, "started_at"),
1283
+ endTime: payloadTimestamp(payload, "ended_at"),
1284
+ errored: payloadHasError(payload)
1285
+ };
1286
+ }
1287
+ function carrierName(operation, payload) {
1288
+ if (operation === "external_span") {
1289
+ const spanData = asPayloadRecord(
1290
+ asPayloadRecord(payload.rawSpan)?.span_data
1291
+ );
1292
+ if (typeof spanData?.name === "string") {
1293
+ return spanData.name;
1294
+ }
1295
+ }
1296
+ if (typeof payload.traceFunctionKey === "string") {
1297
+ return payload.traceFunctionKey;
1298
+ }
1299
+ return `bitfab.${operation}`;
1300
+ }
1301
+ function payloadTimestamp(payload, field) {
1302
+ const rawSpan = asPayloadRecord(payload.rawSpan);
1303
+ const rawTrace = asPayloadRecord(payload.externalTrace) ?? asPayloadRecord(payload.rawTrace);
1304
+ const raw = rawSpan?.[field] ?? rawTrace?.[field];
1305
+ if (typeof raw !== "string") {
1306
+ return void 0;
1307
+ }
1308
+ const parsed = Date.parse(raw);
1309
+ return Number.isNaN(parsed) ? void 0 : parsed;
1310
+ }
1311
+ function payloadHasError(payload) {
1312
+ const spanData = asPayloadRecord(asPayloadRecord(payload.rawSpan)?.span_data);
1313
+ if (spanData?.error != null) {
1314
+ return true;
1315
+ }
1316
+ const errors = payload.errors;
1317
+ return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
1318
+ }
1319
+ function asPayloadRecord(value) {
1320
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1321
+ }
1322
+ function carrierRef(payload) {
1323
+ const traceId = sourceTraceIdOf(payload);
1324
+ if (traceId === void 0) {
1325
+ return void 0;
1326
+ }
1327
+ const rawSpan = payload.rawSpan;
1328
+ if (rawSpan === void 0) {
1329
+ return { traceId };
1330
+ }
1331
+ const spanId = rawSpan?.id;
1332
+ return {
1333
+ traceId,
1334
+ spanId: typeof spanId === "string" ? spanId : `submission-${++carrierSeq}`
1335
+ };
1336
+ }
1337
+ function sourceTraceIdOf(payload) {
1338
+ if (typeof payload.sourceTraceId === "string") {
1339
+ return payload.sourceTraceId;
1340
+ }
1341
+ const rawTrace = payload.externalTrace ?? payload.rawTrace;
1342
+ const id = rawTrace?.id;
1343
+ return typeof id === "string" ? id : void 0;
1344
+ }
1345
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, carrierSeq, HttpClient;
1270
1346
  var init_http = __esm({
1271
1347
  "src/http.ts"() {
1272
1348
  "use strict";
@@ -1276,9 +1352,12 @@ var init_http = __esm({
1276
1352
  init_replayContext();
1277
1353
  init_serializePayload();
1278
1354
  init_transport();
1355
+ init_transportTypes();
1279
1356
  init_unrefTimer();
1280
1357
  init_warnOnce();
1281
1358
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
1359
+ OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
1360
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1282
1361
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
1283
1362
  DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
1284
1363
  pendingTracePromises = /* @__PURE__ */ new Set();
@@ -1298,8 +1377,12 @@ var init_http = __esm({
1298
1377
  });
1299
1378
  });
1300
1379
  }
1380
+ carrierSeq = 0;
1301
1381
  HttpClient = class {
1302
1382
  constructor(config) {
1383
+ // Only traces a caller asked about are tracked, so ordinary tracing stores
1384
+ // nothing here.
1385
+ this.traceDeliveries = /* @__PURE__ */ new Map();
1303
1386
  // Deferred span work owned by THIS client. The module-global set backs the
1304
1387
  // process-wide `flushTraces()` and the exit hook, but per-client lifecycle
1305
1388
  // must not wait on another client's slow finalize: a false `close()` failure
@@ -1335,13 +1418,131 @@ var init_http = __esm({
1335
1418
  }
1336
1419
  if (!this.traceTransport) {
1337
1420
  this.traceTransport = createTraceTransport({
1338
- directSender: (endpoint, request, timeoutMs) => this.sendPrepared(endpoint, request, {
1339
- timeout: timeoutMs
1340
- })
1421
+ directSender: (request, timeoutMs) => this.deliverCarriers(request, timeoutMs),
1422
+ onDelivered: (refs) => this.recordDeliveredCarriers(refs)
1341
1423
  });
1342
1424
  }
1343
1425
  return this.traceTransport;
1344
1426
  }
1427
+ /**
1428
+ * Post one encoded batch and decide what the server's answer means, so the
1429
+ * transport never reads a response. Rejections and permanent statuses come
1430
+ * back as a non-retryable {@link DeliveryError}; anything the server might
1431
+ * still accept on a second try comes back retryable.
1432
+ */
1433
+ async deliverCarriers(request, timeoutMs) {
1434
+ let response;
1435
+ try {
1436
+ response = await this.sendPrepared(
1437
+ OTLP_TRACES_ENDPOINT,
1438
+ request,
1439
+ { timeout: timeoutMs }
1440
+ );
1441
+ } catch (error) {
1442
+ const status = error instanceof BitfabError ? error.status : void 0;
1443
+ if (status === void 0) {
1444
+ throw new DeliveryError(`OTLP ingestion failed: ${String(error)}`, {
1445
+ retryable: true
1446
+ });
1447
+ }
1448
+ throw new DeliveryError(`OTLP ingestion failed with HTTP ${status}`, {
1449
+ retryable: RETRYABLE_STATUSES.has(status),
1450
+ oversized: status === 413,
1451
+ ...error instanceof BitfabError && error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
1452
+ });
1453
+ }
1454
+ const partialSuccess = asPayloadRecord(response?.partialSuccess);
1455
+ const rejected = partialSuccess?.rejectedSpans;
1456
+ if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1457
+ throw new DeliveryError(
1458
+ `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1459
+ );
1460
+ }
1461
+ }
1462
+ /**
1463
+ * Start tracking delivery for `traceIds`. Nothing is recorded for a trace
1464
+ * that was never tracked, so ordinary tracing costs no bookkeeping at all.
1465
+ */
1466
+ trackTraceDeliveries(traceIds) {
1467
+ for (const traceId of traceIds) {
1468
+ if (!this.traceDeliveries.has(traceId)) {
1469
+ this.traceDeliveries.set(traceId, {
1470
+ submittedSpanIds: /* @__PURE__ */ new Set(),
1471
+ ackedSpanIds: /* @__PURE__ */ new Set(),
1472
+ closed: false,
1473
+ closingAcked: false
1474
+ });
1475
+ }
1476
+ }
1477
+ }
1478
+ /** Whether any tracked trace has had its closing carrier submitted. */
1479
+ hasClosedDeliveries(traceIds) {
1480
+ return traceIds.some((traceId) => this.traceDeliveries.get(traceId)?.closed);
1481
+ }
1482
+ /**
1483
+ * Report what each tracked trace submitted and whether the server confirmed
1484
+ * it, and stop tracking them. Every id passed is freed, so a caller cannot
1485
+ * leak a record for a trace that never closed.
1486
+ *
1487
+ * `delivered` is only meaningful once a flush has settled: acks land before
1488
+ * an export resolves, so a flush that reported success has already collected
1489
+ * every ack it is going to collect.
1490
+ */
1491
+ takeTraceDeliveries(traceIds) {
1492
+ const reports = {};
1493
+ for (const traceId of traceIds) {
1494
+ const delivery = this.traceDeliveries.get(traceId);
1495
+ if (delivery === void 0) {
1496
+ continue;
1497
+ }
1498
+ this.traceDeliveries.delete(traceId);
1499
+ reports[traceId] = {
1500
+ spanCount: delivery.submittedSpanIds.size,
1501
+ closed: delivery.closed,
1502
+ delivered: delivery.closingAcked && [...delivery.submittedSpanIds].every(
1503
+ (spanId) => delivery.ackedSpanIds.has(spanId)
1504
+ )
1505
+ };
1506
+ }
1507
+ return reports;
1508
+ }
1509
+ /** Build a carrier's meta and record what it adds to its trace's expected set. */
1510
+ recordedMeta(operation, payload, ref) {
1511
+ this.recordSubmittedCarrier(ref);
1512
+ return carrierMeta(operation, payload, ref);
1513
+ }
1514
+ recordSubmittedCarrier(ref) {
1515
+ if (ref === void 0) {
1516
+ return;
1517
+ }
1518
+ const delivery = this.traceDeliveries.get(ref.traceId);
1519
+ if (delivery === void 0) {
1520
+ return;
1521
+ }
1522
+ if (ref.spanId === void 0) {
1523
+ delivery.closed = true;
1524
+ } else {
1525
+ delivery.submittedSpanIds.add(ref.spanId);
1526
+ }
1527
+ }
1528
+ /**
1529
+ * Ingestion commits every carrier in a request before it answers, so a
1530
+ * delivered ref is proof its row exists: the same fact the replay status
1531
+ * endpoint would report, already in hand.
1532
+ */
1533
+ recordDeliveredCarriers(refs) {
1534
+ for (const ref of refs) {
1535
+ const delivery = this.traceDeliveries.get(ref.traceId);
1536
+ if (delivery === void 0) {
1537
+ continue;
1538
+ }
1539
+ if (ref.spanId === void 0) {
1540
+ delivery.closingAcked = true;
1541
+ } else {
1542
+ delivery.ackedSpanIds.add(ref.spanId);
1543
+ }
1544
+ }
1545
+ }
1345
1546
  /**
1346
1547
  * Track deferred span work so this client's own lifecycle waits for it, and
1347
1548
  * so the process-wide flush and exit hook do too.
@@ -1451,7 +1652,8 @@ var init_http = __esm({
1451
1652
  throw new BitfabError(
1452
1653
  `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1453
1654
  void 0,
1454
- response.status
1655
+ response.status,
1656
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1455
1657
  );
1456
1658
  }
1457
1659
  const result = await response.json();
@@ -1537,11 +1739,12 @@ var init_http = __esm({
1537
1739
  * the OTLP carrier has no path to carry it.
1538
1740
  */
1539
1741
  sendInternalTrace(functionId, payload) {
1540
- this.getTraceTransport()?.submit("internal_trace", {
1541
- ...payload,
1542
- functionId,
1543
- sdkVersion: __version__
1544
- });
1742
+ const body = { ...payload, functionId, sdkVersion: __version__ };
1743
+ this.getTraceTransport()?.submit(
1744
+ "internal_trace",
1745
+ body,
1746
+ carrierMeta("internal_trace", body, void 0)
1747
+ );
1545
1748
  }
1546
1749
  /**
1547
1750
  * Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
@@ -1550,10 +1753,11 @@ var init_http = __esm({
1550
1753
  * promise.
1551
1754
  */
1552
1755
  sendExternalSpan(payload) {
1553
- this.getTraceTransport()?.submit("external_span", {
1554
- ...payload,
1555
- sdkVersion: __version__
1556
- });
1756
+ this.getTraceTransport()?.submit(
1757
+ "external_span",
1758
+ { ...payload, sdkVersion: __version__ },
1759
+ this.recordedMeta("external_span", payload, carrierRef(payload))
1760
+ );
1557
1761
  }
1558
1762
  /**
1559
1763
  * Queue an external trace completion (from OpenAI tracing) onto this
@@ -1562,10 +1766,15 @@ var init_http = __esm({
1562
1766
  * server-authoritative barrier in `replay.ts`, not by awaiting this call.
1563
1767
  */
1564
1768
  sendExternalTrace(payload) {
1565
- this.getTraceTransport()?.submit("external_trace", {
1566
- ...payload,
1567
- sdkVersion: __version__
1568
- });
1769
+ this.getTraceTransport()?.submit(
1770
+ "external_trace",
1771
+ { ...payload, sdkVersion: __version__ },
1772
+ this.recordedMeta(
1773
+ "external_trace",
1774
+ payload,
1775
+ payload.completed === true ? carrierRef(payload) : void 0
1776
+ )
1777
+ );
1569
1778
  }
1570
1779
  /**
1571
1780
  * Partial update of an existing trace identified by its Bitfab trace ID.
@@ -2161,7 +2370,9 @@ __export(replay_exports, {
2161
2370
  ReplayError: () => ReplayError,
2162
2371
  replay: () => replay,
2163
2372
  reportReplayProgress: () => reportReplayProgress,
2164
- serializeReplayResult: () => serializeReplayResult
2373
+ serializeReplayResult: () => serializeReplayResult,
2374
+ sleepForReplayPersistence: () => sleepForReplayPersistence,
2375
+ waitForReplayPersistence: () => waitForReplayPersistence
2165
2376
  });
2166
2377
  function dbBranchEnabled(dbBranch) {
2167
2378
  return dbBranch !== void 0 && dbBranch !== false;
@@ -2449,15 +2660,29 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2449
2660
  REPLAY_PERSISTENCE_TIMEOUT_MS
2450
2661
  );
2451
2662
  if (!deferredSettled) {
2663
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2452
2664
  throw new BitfabError(
2453
2665
  `Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
2454
2666
  );
2455
2667
  }
2456
- const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
2457
- if (Object.keys(expectedSpanCounts).length === 0) {
2668
+ if (!httpClient.hasClosedDeliveries(replayedTraceIds)) {
2669
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2458
2670
  return;
2459
2671
  }
2460
2672
  const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2673
+ const deliveries = httpClient.takeTraceDeliveries(replayedTraceIds);
2674
+ const expectedSpanCounts = {};
2675
+ let allDelivered = true;
2676
+ for (const [traceId, delivery] of Object.entries(deliveries)) {
2677
+ if (!delivery.closed) {
2678
+ continue;
2679
+ }
2680
+ expectedSpanCounts[traceId] = delivery.spanCount;
2681
+ allDelivered = allDelivered && delivery.delivered;
2682
+ }
2683
+ if (allDelivered) {
2684
+ return;
2685
+ }
2461
2686
  const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
2462
2687
  let missing = Object.keys(expectedSpanCounts).length;
2463
2688
  while (true) {
@@ -2475,17 +2700,18 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2475
2700
  if (Date.now() >= deadline) {
2476
2701
  break;
2477
2702
  }
2478
- await sleep(Math.min(100, Math.max(0, deadline - Date.now())));
2703
+ await sleepForReplayPersistence(
2704
+ Math.min(100, Math.max(0, deadline - Date.now()))
2705
+ );
2479
2706
  }
2480
2707
  const cause = flushed ? "" : " Delivery was also not confirmed before the flush deadline, so the spans likely never reached the server.";
2481
2708
  throw new BitfabError(
2482
2709
  `Replay traces were not fully persisted before the delivery deadline (testRunId ${testRunId}, missing ${missing} of ${Object.keys(expectedSpanCounts).length} trace(s)).${cause}`
2483
2710
  );
2484
2711
  }
2485
- function sleep(ms) {
2712
+ function sleepForReplayPersistence(ms) {
2486
2713
  return new Promise((resolve) => {
2487
- const timer = setTimeout(resolve, ms);
2488
- unrefTimer(timer);
2714
+ setTimeout(resolve, ms);
2489
2715
  });
2490
2716
  }
2491
2717
  async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted) {
@@ -2566,6 +2792,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2566
2792
  ...registeredOverrides
2567
2793
  ];
2568
2794
  const replayedTraceIds = serverItems.map(() => randomUuid());
2795
+ httpClient.trackTraceDeliveries(replayedTraceIds);
2569
2796
  const tasks = serverItems.map(
2570
2797
  (serverItem, index) => () => processItem(
2571
2798
  httpClient,
@@ -2763,8 +2990,6 @@ var init_replay = __esm({
2763
2990
  init_randomUuid();
2764
2991
  init_replayContext();
2765
2992
  init_serialize();
2766
- init_transport();
2767
- init_unrefTimer();
2768
2993
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
2769
2994
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";
2770
2995
  ReplayError = class extends BitfabError {