bitfab 0.36.6 → 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
@@ -51,7 +51,7 @@ var __version__;
51
51
  var init_version_generated = __esm({
52
52
  "src/version.generated.ts"() {
53
53
  "use strict";
54
- __version__ = "0.36.6";
54
+ __version__ = "0.36.8";
55
55
  }
56
56
  });
57
57
 
@@ -85,33 +85,58 @@ function toArrayBuffer(view) {
85
85
  view.byteOffset + view.byteLength
86
86
  );
87
87
  }
88
+ function compressedRequest(body, rawBytes, compressed) {
89
+ if (compressed.byteLength >= rawBytes) {
90
+ return { body, rawBytes, wireBytes: rawBytes };
91
+ }
92
+ return {
93
+ body: compressed instanceof Uint8Array ? toArrayBuffer(compressed) : compressed,
94
+ contentEncoding: "gzip",
95
+ rawBytes,
96
+ wireBytes: compressed.byteLength
97
+ };
98
+ }
88
99
  async function gzipViaStream(bytes) {
89
100
  const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
90
101
  return await new Response(stream).arrayBuffer();
91
102
  }
92
103
  function encodeRequestBody(body) {
93
104
  if (readEnv(DISABLE_COMPRESSION_ENV)) {
94
- return { body };
105
+ const rawBytes = new TextEncoder().encode(body).byteLength;
106
+ return { body, rawBytes, wireBytes: rawBytes };
95
107
  }
96
108
  const bytes = new TextEncoder().encode(body);
97
109
  if (bytes.byteLength < MIN_COMPRESSED_BYTES) {
98
- return { body };
110
+ return {
111
+ body,
112
+ rawBytes: bytes.byteLength,
113
+ wireBytes: bytes.byteLength
114
+ };
99
115
  }
100
116
  if (gzipNode) {
101
117
  return gzipNode(bytes).then(
102
- (compressed) => ({
103
- body: toArrayBuffer(compressed),
104
- contentEncoding: "gzip"
105
- }),
106
- () => ({ body })
118
+ (compressed) => compressedRequest(body, bytes.byteLength, compressed),
119
+ () => ({
120
+ body,
121
+ rawBytes: bytes.byteLength,
122
+ wireBytes: bytes.byteLength
123
+ })
107
124
  );
108
125
  }
109
126
  if (typeof CompressionStream === "undefined") {
110
- return { body };
127
+ return {
128
+ body,
129
+ rawBytes: bytes.byteLength,
130
+ wireBytes: bytes.byteLength
131
+ };
111
132
  }
112
133
  return gzipViaStream(bytes).then(
113
- (compressed) => ({ body: compressed, contentEncoding: "gzip" }),
114
- () => ({ body })
134
+ (compressed) => compressedRequest(body, bytes.byteLength, compressed),
135
+ () => ({
136
+ body,
137
+ rawBytes: bytes.byteLength,
138
+ wireBytes: bytes.byteLength
139
+ })
115
140
  );
116
141
  }
117
142
  var DISABLE_COMPRESSION_ENV, MIN_COMPRESSED_BYTES, gzipNode, _nodeGzipReady;
@@ -152,10 +177,11 @@ var init_errors = __esm({
152
177
  "src/errors.ts"() {
153
178
  "use strict";
154
179
  BitfabError = class extends Error {
155
- constructor(message, url, status) {
180
+ constructor(message, url, status, retryAfterMs) {
156
181
  super(message);
157
182
  this.url = url;
158
183
  this.status = status;
184
+ this.retryAfterMs = retryAfterMs;
159
185
  this.name = "BitfabError";
160
186
  }
161
187
  };
@@ -256,15 +282,15 @@ function carrierBytesOf(encoded, body) {
256
282
  }
257
283
  return encoded.length + extra;
258
284
  }
259
- function fitsCarrierBudget(body) {
285
+ function fitsCarrierBudget(body, maxBytes = MAX_SPAN_CARRIER_BYTES) {
260
286
  const units = body.length;
261
- if (units * MAX_BYTES_PER_UNIT + 2 <= MAX_SPAN_CARRIER_BYTES) {
287
+ if (units * MAX_BYTES_PER_UNIT + 2 <= maxBytes) {
262
288
  return true;
263
289
  }
264
- if (units + 2 > MAX_SPAN_CARRIER_BYTES) {
290
+ if (units + 2 > maxBytes) {
265
291
  return false;
266
292
  }
267
- return carrierByteLength(body) <= MAX_SPAN_CARRIER_BYTES;
293
+ return carrierByteLength(body) <= maxBytes;
268
294
  }
269
295
  function asRecord(value) {
270
296
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
@@ -308,7 +334,7 @@ function collectCandidates(containers) {
308
334
  }
309
335
  return candidates.sort((a, b) => b.size - a.size);
310
336
  }
311
- function trimPayloadToBudget(payload, encode) {
337
+ function trimPayloadToBudget(payload, encode, maxBytes = MAX_SPAN_CARRIER_BYTES) {
312
338
  const { copy, containers } = cloneTrimmable(payload);
313
339
  const candidates = collectCandidates(containers);
314
340
  if (candidates.length === 0) {
@@ -324,30 +350,31 @@ function trimPayloadToBudget(payload, encode) {
324
350
  } catch {
325
351
  return void 0;
326
352
  }
327
- if (fitsCarrierBudget(body)) {
353
+ if (fitsCarrierBudget(body, maxBytes)) {
328
354
  return { value: copy, trimmed };
329
355
  }
330
356
  }
331
357
  return void 0;
332
358
  }
333
- function markPayloadTrimmed(value, trimmed) {
359
+ function markPayloadTrimmed(value, trimmed, maxBytes = MAX_SPAN_CARRIER_BYTES) {
334
360
  const existing = Array.isArray(value.errors) ? value.errors : [];
335
361
  value.errors = [
336
362
  ...existing,
337
363
  {
338
364
  source: "sdk",
339
365
  step: "payload_budget",
340
- error: `trimmed oversized field(s) to fit the ${MAX_SPAN_CARRIER_BYTES}-byte span carrier budget: ${[
366
+ error: `trimmed oversized field(s) to fit the ${maxBytes}-byte span carrier budget: ${[
341
367
  ...new Set(trimmed)
342
368
  ].join(", ")}`
343
369
  }
344
370
  ];
345
371
  }
346
- var MAX_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
372
+ var MAX_SPAN_CARRIER_BYTES, MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
347
373
  var init_payloadBudget = __esm({
348
374
  "src/payloadBudget.ts"() {
349
375
  "use strict";
350
376
  MAX_SPAN_CARRIER_BYTES = 28e5;
377
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES = 78e5;
351
378
  textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
352
379
  MAX_BYTES_PER_UNIT = 3;
353
380
  STRUCTURAL_SPAN_KEYS = /* @__PURE__ */ new Set([
@@ -379,30 +406,31 @@ var init_warnOnce = __esm({
379
406
  });
380
407
 
381
408
  // src/serializePayload.ts
382
- function serializePayloadBody(payload) {
409
+ function serializePayloadBody(payload, maxCarrierBytes = MAX_SPAN_CARRIER_BYTES) {
383
410
  const encoded = encodePayloadBody(payload);
384
- if (fitsCarrierBudget(encoded.body)) {
411
+ if (fitsCarrierBudget(encoded.body, maxCarrierBytes)) {
385
412
  return { body: encoded.body, dropped: encoded.dropped };
386
413
  }
387
- return applyPayloadBudget(encoded);
414
+ return applyPayloadBudget(encoded, maxCarrierBytes);
388
415
  }
389
- function applyPayloadBudget(encoded) {
416
+ function applyPayloadBudget(encoded, maxCarrierBytes) {
390
417
  const result = encoded.value ? trimPayloadToBudget(
391
418
  encoded.value,
392
- (value) => encodePayloadBody(value).body
419
+ (value) => encodePayloadBody(value).body,
420
+ maxCarrierBytes
393
421
  ) : void 0;
394
422
  if (!result) {
395
423
  return { body: encoded.body, dropped: encoded.dropped };
396
424
  }
397
425
  warnOnce(
398
426
  "payload:over-budget",
399
- `a span payload exceeded the ${MAX_SPAN_CARRIER_BYTES}-byte carrier budget; its largest field(s) (${[
427
+ `a span payload exceeded the ${maxCarrierBytes}-byte carrier budget; its largest field(s) (${[
400
428
  ...new Set(result.trimmed)
401
429
  ].join(
402
430
  ", "
403
431
  )}) were replaced with placeholders so the span still ships. The span is incomplete and may not be replayable.`
404
432
  );
405
- markPayloadTrimmed(result.value, result.trimmed);
433
+ markPayloadTrimmed(result.value, result.trimmed, maxCarrierBytes);
406
434
  return {
407
435
  body: encodePayloadBody(result.value).body,
408
436
  dropped: encoded.dropped
@@ -508,6 +536,23 @@ var init_serializePayload = __esm({
508
536
  }
509
537
  });
510
538
 
539
+ // src/transportTypes.ts
540
+ var DeliveryError;
541
+ var init_transportTypes = __esm({
542
+ "src/transportTypes.ts"() {
543
+ "use strict";
544
+ DeliveryError = class extends Error {
545
+ constructor(message, options = {}) {
546
+ super(message);
547
+ this.name = "DeliveryError";
548
+ this.retryable = options.retryable ?? false;
549
+ this.oversized = options.oversized ?? false;
550
+ this.retryAfterMs = options.retryAfterMs;
551
+ }
552
+ };
553
+ }
554
+ });
555
+
511
556
  // src/unrefTimer.ts
512
557
  function unrefTimer(timer) {
513
558
  const handle = timer;
@@ -547,59 +592,6 @@ function logError(message, error) {
547
592
  } catch {
548
593
  }
549
594
  }
550
- function recordTraceSubmission(operation, payload) {
551
- const sourceTraceId = resolveSourceTraceId(payload);
552
- if (sourceTraceId === void 0) {
553
- return;
554
- }
555
- if (operation === "external_span") {
556
- const rawSpan = asRecord2(payload.rawSpan);
557
- if (typeof rawSpan?.id !== "string") {
558
- submissionCounter += 1;
559
- }
560
- const sourceSpanId = typeof rawSpan?.id === "string" ? rawSpan.id : `submission-${submissionCounter}`;
561
- const existing = traceSubmissionSpanIds.get(sourceTraceId);
562
- if (existing) {
563
- existing.add(sourceSpanId);
564
- } else {
565
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set([sourceSpanId]));
566
- }
567
- return;
568
- }
569
- if (payload.completed !== true) {
570
- return;
571
- }
572
- if (typeof payload.testRunId === "string") {
573
- replayTraceSubmissions.add(sourceTraceId);
574
- if (!traceSubmissionSpanIds.has(sourceTraceId)) {
575
- traceSubmissionSpanIds.set(sourceTraceId, /* @__PURE__ */ new Set());
576
- }
577
- } else {
578
- traceSubmissionSpanIds.delete(sourceTraceId);
579
- }
580
- }
581
- function takeReplaySpanCounts(traceIds) {
582
- const counts = {};
583
- for (const traceId of traceIds) {
584
- if (!replayTraceSubmissions.has(traceId)) {
585
- continue;
586
- }
587
- counts[traceId] = traceSubmissionSpanIds.get(traceId)?.size ?? 0;
588
- traceSubmissionSpanIds.delete(traceId);
589
- replayTraceSubmissions.delete(traceId);
590
- }
591
- return counts;
592
- }
593
- function asRecord2(value) {
594
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
595
- }
596
- function resolveSourceTraceId(payload) {
597
- if (typeof payload.sourceTraceId === "string") {
598
- return payload.sourceTraceId;
599
- }
600
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
601
- return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
602
- }
603
595
  function otlpValue(value) {
604
596
  if (typeof value === "boolean") {
605
597
  return { boolValue: value };
@@ -657,7 +649,36 @@ function spanToOtlp(span) {
657
649
  }
658
650
  function encodeSpan(span) {
659
651
  const json = JSON.stringify(spanToOtlp(span));
660
- return { json, size: byteLength(json) };
652
+ return {
653
+ json,
654
+ size: byteLength(json),
655
+ ref: carrierRefs.get(span)
656
+ };
657
+ }
658
+ function trimEncodedSpan(span) {
659
+ try {
660
+ const carrier = JSON.parse(span.json);
661
+ const attribute = carrier.attributes?.find(
662
+ (entry) => entry.key === PAYLOAD_ATTRIBUTE
663
+ );
664
+ const payloadBody = attribute?.value?.stringValue;
665
+ if (!attribute?.value || payloadBody === void 0) {
666
+ return void 0;
667
+ }
668
+ const payload = JSON.parse(payloadBody);
669
+ attribute.value.stringValue = serializePayloadBody(
670
+ payload,
671
+ MAX_SPAN_CARRIER_BYTES
672
+ ).body;
673
+ const json = JSON.stringify(carrier);
674
+ return { json, size: byteLength(json) };
675
+ } catch {
676
+ return void 0;
677
+ }
678
+ }
679
+ async function prepareRequest(body) {
680
+ const prepared = encodeRequestBody(body);
681
+ return prepared instanceof Promise ? await prepared : prepared;
661
682
  }
662
683
  function requestEnvelope(first) {
663
684
  const scope = first.instrumentationScope;
@@ -715,48 +736,27 @@ async function mapWithConcurrency(items, limit, task) {
715
736
  await Promise.all(workers);
716
737
  return results;
717
738
  }
718
- function responseStatus(error) {
719
- return error instanceof BitfabError ? error.status : void 0;
720
- }
721
739
  function isRetryable(error) {
722
- const status = responseStatus(error);
723
- if (status === void 0) {
724
- return true;
725
- }
726
- return RETRYABLE_STATUSES.has(status) || status >= 500;
740
+ return error instanceof DeliveryError && error.retryable;
727
741
  }
728
- function endSpan(span, endTime) {
729
- span.end(endTime);
742
+ function isOversized(error) {
743
+ return error instanceof DeliveryError && error.oversized;
730
744
  }
731
- function spanName(operation, payload) {
732
- if (operation === "external_span") {
733
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
734
- if (typeof spanData?.name === "string") {
735
- return spanData.name;
736
- }
745
+ function retryWaitMillis(error, attempt, remainingMillis) {
746
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
747
+ const affordable = remainingMillis / 2;
748
+ if (requested !== void 0) {
749
+ return requested < affordable ? requested : null;
737
750
  }
738
- if (typeof payload.traceFunctionKey === "string") {
739
- return payload.traceFunctionKey;
740
- }
741
- return `bitfab.${operation}`;
742
- }
743
- function payloadTimestamp(payload, field) {
744
- const rawSpan = asRecord2(payload.rawSpan);
745
- const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
746
- const raw = rawSpan?.[field] ?? rawTrace?.[field];
747
- if (typeof raw !== "string") {
748
- return void 0;
749
- }
750
- const parsed = Date.parse(raw);
751
- return Number.isNaN(parsed) ? void 0 : parsed;
751
+ const backoff = Math.min(
752
+ RETRY_BASE_DELAY_MILLIS * 2 ** attempt,
753
+ RETRY_BACKOFF_CEILING_MILLIS
754
+ );
755
+ const jittered = backoff / 2 + Math.random() * (backoff / 2);
756
+ return jittered < affordable ? jittered : null;
752
757
  }
753
- function hasError(payload) {
754
- const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
755
- if (spanData?.error != null) {
756
- return true;
757
- }
758
- const errors = payload.errors;
759
- return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
758
+ function endSpan(span, endTime) {
759
+ span.end(endTime);
760
760
  }
761
761
  function createOtelTransport(options) {
762
762
  return new OtelBatchTransport({
@@ -795,7 +795,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
795
795
  (transport, remaining) => transport.shutdown(remaining)
796
796
  );
797
797
  }
798
- var import_api, import_core, import_resources, import_sdk_trace_base, OPERATION_ATTRIBUTE, PAYLOAD_ATTRIBUTE, OTLP_TRACES_ENDPOINT, MAX_EXPORT_REQUEST_BYTES, MAX_REQUEST_BYTES_ENV, EXPORT_CONCURRENCY_ENV, 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;
798
+ 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;
799
799
  var init_otel = __esm({
800
800
  "src/otel.ts"() {
801
801
  "use strict";
@@ -803,17 +803,19 @@ var init_otel = __esm({
803
803
  import_core = require("@opentelemetry/core");
804
804
  import_resources = require("@opentelemetry/resources");
805
805
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
806
+ init_compress();
806
807
  init_constants();
807
808
  init_errors();
808
809
  init_payloadBudget();
809
810
  init_readEnv();
810
811
  init_serializePayload();
812
+ init_transportTypes();
811
813
  init_unrefTimer();
812
814
  init_warnOnce();
813
815
  OPERATION_ATTRIBUTE = "bitfab.operation";
814
816
  PAYLOAD_ATTRIBUTE = "bitfab.payload";
815
- OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
816
817
  MAX_EXPORT_REQUEST_BYTES = 3e6;
818
+ MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
817
819
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
818
820
  EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
819
821
  MAX_QUEUE_SIZE = 8192;
@@ -823,25 +825,23 @@ var init_otel = __esm({
823
825
  MAX_EXPORT_CONCURRENCY = 64;
824
826
  SCHEDULE_DELAY_MILLIS = 5e3;
825
827
  EXPORT_TIMEOUT_MILLIS = 3e4;
826
- RETRY_DELAY_MILLIS = 100;
828
+ RETRY_BASE_DELAY_MILLIS = 100;
829
+ RETRY_BACKOFF_CEILING_MILLIS = 5e3;
827
830
  MAX_SEND_ATTEMPTS = 3;
828
831
  DEFAULT_LIFECYCLE_TIMEOUT_MS = 3e4;
829
- RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
830
832
  liveTransports = /* @__PURE__ */ new Set();
831
- traceSubmissionSpanIds = /* @__PURE__ */ new Map();
832
- replayTraceSubmissions = /* @__PURE__ */ new Set();
833
- submissionCounter = 0;
833
+ carrierRefs = /* @__PURE__ */ new WeakMap();
834
834
  SPAN_SEPARATOR_BYTES = 1;
835
- OtlpPayloadTooLargeError = class extends Error {
836
- };
837
- OtlpPartialSuccessError = class extends Error {
838
- };
839
835
  BitfabSpanExporter = class {
840
- constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency) {
836
+ constructor(directSender, maxRequestBytes, maxRequestBatchSize, exportConcurrency, onDelivered, exportTimeoutMillis = EXPORT_TIMEOUT_MILLIS) {
841
837
  this.directSender = directSender;
842
838
  this.maxRequestBytes = maxRequestBytes;
843
839
  this.maxRequestBatchSize = maxRequestBatchSize;
844
840
  this.exportConcurrency = exportConcurrency;
841
+ this.onDelivered = onDelivered;
842
+ this.exportTimeoutMillis = exportTimeoutMillis;
843
+ /** Epoch ms until which the server has asked this exporter to stay away. */
844
+ this.throttledUntil = 0;
845
845
  }
846
846
  export(spans, resultCallback) {
847
847
  void this.exportAsync(spans).then(
@@ -896,25 +896,51 @@ var init_otel = __esm({
896
896
  return batches;
897
897
  }
898
898
  async send(envelope, batch) {
899
- if (batch.size > this.maxRequestBytes) {
900
- logError(
901
- "a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
902
- );
903
- return false;
904
- }
905
899
  try {
906
- await this.sendWithRetries(encodeRequest(envelope, batch.spans));
907
- return true;
900
+ let requestSpans = batch.spans;
901
+ let requestRawBytes = batch.size;
902
+ let alreadyTrimmed = false;
903
+ while (true) {
904
+ if (requestRawBytes <= MAX_DECOMPRESSED_REQUEST_BYTES) {
905
+ const prepared = await prepareRequest(
906
+ encodeRequest(envelope, requestSpans)
907
+ );
908
+ if (prepared.wireBytes <= this.maxRequestBytes) {
909
+ await this.sendWithRetries(prepared);
910
+ this.reportDelivered(batch.spans);
911
+ return true;
912
+ }
913
+ }
914
+ if (batch.spans.length !== 1) {
915
+ logError(
916
+ "an OpenTelemetry span batch exceeded the configured request-size target and could not be exported"
917
+ );
918
+ return false;
919
+ }
920
+ if (alreadyTrimmed) {
921
+ logError(
922
+ "a single OpenTelemetry span exceeded the configured request-size target after trimming"
923
+ );
924
+ return false;
925
+ }
926
+ const trimmed = trimEncodedSpan(batch.spans[0]);
927
+ if (!trimmed) {
928
+ logError(
929
+ "a single OpenTelemetry span exceeded the configured request-size target and could not be trimmed"
930
+ );
931
+ return false;
932
+ }
933
+ requestSpans = [trimmed];
934
+ requestRawBytes = envelope.size + trimmed.size;
935
+ alreadyTrimmed = true;
936
+ }
908
937
  } catch (error) {
909
- if (error instanceof OtlpPayloadTooLargeError) {
938
+ if (isOversized(error)) {
910
939
  logError(
911
940
  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"
912
941
  );
913
942
  return false;
914
943
  }
915
- if (error instanceof OtlpPartialSuccessError) {
916
- return false;
917
- }
918
944
  logError("failed to export an OpenTelemetry span batch", error);
919
945
  return false;
920
946
  }
@@ -932,37 +958,79 @@ var init_otel = __esm({
932
958
  * the server does not yet understand. The fix is a client-supplied
933
959
  * idempotency key that ingestion dedupes on.
934
960
  */
935
- async sendWithRetries(body) {
961
+ /**
962
+ * Remember a throttle the server asked for, so the requests fanned out
963
+ * alongside this one respect it too. Delaying only the request that was
964
+ * refused leaves the other seven in the window hitting a server that just
965
+ * asked for room.
966
+ */
967
+ recordThrottle(error) {
968
+ const requested = error instanceof DeliveryError ? error.retryAfterMs : void 0;
969
+ if (requested !== void 0) {
970
+ this.throttledUntil = Math.max(
971
+ this.throttledUntil,
972
+ Date.now() + requested
973
+ );
974
+ }
975
+ }
976
+ /**
977
+ * Waits out an active throttle, or reports the batch undeliverable when the
978
+ * throttle outlasts what we are willing to hold it for. Either way nothing is
979
+ * sent while the server has asked us to stay away.
980
+ */
981
+ async awaitThrottle(deadline) {
982
+ const remaining = this.throttledUntil - Date.now();
983
+ if (remaining <= 0) {
984
+ return;
985
+ }
986
+ if (remaining >= (deadline - Date.now()) / 2) {
987
+ throw new DeliveryError(
988
+ `OTLP ingestion is throttled for another ${remaining}ms, longer than the export budget`
989
+ );
990
+ }
991
+ await delay(remaining);
992
+ }
993
+ async sendWithRetries(request) {
994
+ const deadline = Date.now() + this.exportTimeoutMillis;
936
995
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
937
996
  try {
938
- const response = await this.directSender(
939
- OTLP_TRACES_ENDPOINT,
940
- body,
941
- EXPORT_TIMEOUT_MILLIS
942
- );
943
- const partialSuccess = asRecord2(response?.partialSuccess);
944
- const rejected = partialSuccess?.rejectedSpans;
945
- if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
946
- logError(
947
- `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
948
- );
949
- throw new OtlpPartialSuccessError();
950
- }
997
+ await this.awaitThrottle(deadline);
998
+ await this.directSender(request, Math.max(0, deadline - Date.now()));
951
999
  return;
952
1000
  } catch (error) {
953
- if (error instanceof OtlpPartialSuccessError) {
1001
+ if (isOversized(error)) {
954
1002
  throw error;
955
1003
  }
956
- if (responseStatus(error) === 413) {
957
- throw new OtlpPayloadTooLargeError();
958
- }
1004
+ this.recordThrottle(error);
959
1005
  if (attempt === MAX_SEND_ATTEMPTS - 1 || !isRetryable(error)) {
960
1006
  throw error;
961
1007
  }
962
- await delay(RETRY_DELAY_MILLIS);
1008
+ const wait = retryWaitMillis(error, attempt, deadline - Date.now());
1009
+ if (wait === null) {
1010
+ throw error;
1011
+ }
1012
+ await delay(wait);
963
1013
  }
964
1014
  }
965
1015
  }
1016
+ /**
1017
+ * Announce the carriers a request delivered. Wrapped because a listener that
1018
+ * throws must never turn a delivered batch into a failed export.
1019
+ */
1020
+ reportDelivered(spans) {
1021
+ if (this.onDelivered === void 0) {
1022
+ return;
1023
+ }
1024
+ const refs = spans.map((span) => span.ref).filter((ref) => ref !== void 0);
1025
+ if (refs.length === 0) {
1026
+ return;
1027
+ }
1028
+ try {
1029
+ this.onDelivered(refs);
1030
+ } catch (error) {
1031
+ logError("a delivery listener threw", error);
1032
+ }
1033
+ }
966
1034
  async shutdown() {
967
1035
  }
968
1036
  async forceFlush() {
@@ -1020,7 +1088,9 @@ var init_otel = __esm({
1020
1088
  options.directSender,
1021
1089
  maxRequestBytes,
1022
1090
  maxRequestBatchSize,
1023
- options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
1091
+ options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY,
1092
+ options.onDelivered,
1093
+ options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
1024
1094
  )
1025
1095
  );
1026
1096
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
@@ -1044,8 +1114,7 @@ var init_otel = __esm({
1044
1114
  this.tracer = this.provider.getTracer("bitfab", __version__);
1045
1115
  liveTransports.add(this);
1046
1116
  }
1047
- submit(operation, payload) {
1048
- recordTraceSubmission(operation, payload);
1117
+ submit(operation, payload, meta = {}) {
1049
1118
  if (this.closed) {
1050
1119
  warnOnce(
1051
1120
  "otel-submit-after-shutdown",
@@ -1054,7 +1123,10 @@ var init_otel = __esm({
1054
1123
  return;
1055
1124
  }
1056
1125
  try {
1057
- const { body, dropped } = serializePayloadBody(payload);
1126
+ const { body, dropped } = serializePayloadBody(
1127
+ payload,
1128
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES
1129
+ );
1058
1130
  if (dropped.length > 0) {
1059
1131
  warnOnce(
1060
1132
  "otel-carrier-payload-stubbed",
@@ -1063,17 +1135,20 @@ var init_otel = __esm({
1063
1135
  ].join(", ")}); they were stubbed so the span still ships, but the trace may be incomplete or not replayable.`
1064
1136
  );
1065
1137
  }
1066
- const span = this.tracer.startSpan(spanName(operation, payload), {
1138
+ const span = this.tracer.startSpan(meta.name ?? `bitfab.${operation}`, {
1067
1139
  attributes: {
1068
1140
  [OPERATION_ATTRIBUTE]: operation,
1069
1141
  [PAYLOAD_ATTRIBUTE]: body
1070
1142
  },
1071
- startTime: payloadTimestamp(payload, "started_at")
1143
+ startTime: meta.startTime
1072
1144
  });
1073
- if (hasError(payload)) {
1145
+ if (meta.ref !== void 0) {
1146
+ carrierRefs.set(span, meta.ref);
1147
+ }
1148
+ if (meta.errored === true) {
1074
1149
  span.setStatus({ code: import_api.SpanStatusCode.ERROR });
1075
1150
  }
1076
- endSpan(span, payloadTimestamp(payload, "ended_at"));
1151
+ endSpan(span, meta.endTime);
1077
1152
  } catch (error) {
1078
1153
  logError("failed to queue an OpenTelemetry span", error);
1079
1154
  }
@@ -1123,9 +1198,6 @@ function flushTraceTransports(timeoutMs) {
1123
1198
  function shutdownTraceTransports(timeoutMs) {
1124
1199
  return shutdownOtelTransports(timeoutMs);
1125
1200
  }
1126
- function takeReplaySpanCounts2(traceIds) {
1127
- return takeReplaySpanCounts(traceIds);
1128
- }
1129
1201
  var init_transport = __esm({
1130
1202
  "src/transport.ts"() {
1131
1203
  "use strict";
@@ -1174,7 +1246,96 @@ async function waitForPromises(promises, timeoutMs) {
1174
1246
  }
1175
1247
  }
1176
1248
  }
1177
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, HttpClient;
1249
+ function readHeader(response, name) {
1250
+ try {
1251
+ return response.headers?.get(name) ?? null;
1252
+ } catch {
1253
+ return null;
1254
+ }
1255
+ }
1256
+ function parseRetryAfterMs(header) {
1257
+ const value = header?.trim();
1258
+ if (!value) {
1259
+ return void 0;
1260
+ }
1261
+ const seconds = Number(value);
1262
+ if (Number.isFinite(seconds)) {
1263
+ return seconds >= 0 ? seconds * 1e3 : void 0;
1264
+ }
1265
+ const at = Date.parse(value);
1266
+ if (Number.isNaN(at)) {
1267
+ return void 0;
1268
+ }
1269
+ return Math.max(0, at - Date.now());
1270
+ }
1271
+ function carrierMeta(operation, payload, ref) {
1272
+ return {
1273
+ ref,
1274
+ name: carrierName(operation, payload),
1275
+ startTime: payloadTimestamp(payload, "started_at"),
1276
+ endTime: payloadTimestamp(payload, "ended_at"),
1277
+ errored: payloadHasError(payload)
1278
+ };
1279
+ }
1280
+ function carrierName(operation, payload) {
1281
+ if (operation === "external_span") {
1282
+ const spanData = asPayloadRecord(
1283
+ asPayloadRecord(payload.rawSpan)?.span_data
1284
+ );
1285
+ if (typeof spanData?.name === "string") {
1286
+ return spanData.name;
1287
+ }
1288
+ }
1289
+ if (typeof payload.traceFunctionKey === "string") {
1290
+ return payload.traceFunctionKey;
1291
+ }
1292
+ return `bitfab.${operation}`;
1293
+ }
1294
+ function payloadTimestamp(payload, field) {
1295
+ const rawSpan = asPayloadRecord(payload.rawSpan);
1296
+ const rawTrace = asPayloadRecord(payload.externalTrace) ?? asPayloadRecord(payload.rawTrace);
1297
+ const raw = rawSpan?.[field] ?? rawTrace?.[field];
1298
+ if (typeof raw !== "string") {
1299
+ return void 0;
1300
+ }
1301
+ const parsed = Date.parse(raw);
1302
+ return Number.isNaN(parsed) ? void 0 : parsed;
1303
+ }
1304
+ function payloadHasError(payload) {
1305
+ const spanData = asPayloadRecord(asPayloadRecord(payload.rawSpan)?.span_data);
1306
+ if (spanData?.error != null) {
1307
+ return true;
1308
+ }
1309
+ const errors = payload.errors;
1310
+ return Array.isArray(errors) ? errors.length > 0 : Boolean(errors);
1311
+ }
1312
+ function asPayloadRecord(value) {
1313
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1314
+ }
1315
+ function carrierRef(payload) {
1316
+ const traceId = sourceTraceIdOf(payload);
1317
+ if (traceId === void 0) {
1318
+ return void 0;
1319
+ }
1320
+ const rawSpan = payload.rawSpan;
1321
+ if (rawSpan === void 0) {
1322
+ return { traceId };
1323
+ }
1324
+ const spanId = rawSpan?.id;
1325
+ return {
1326
+ traceId,
1327
+ spanId: typeof spanId === "string" ? spanId : `submission-${++carrierSeq}`
1328
+ };
1329
+ }
1330
+ function sourceTraceIdOf(payload) {
1331
+ if (typeof payload.sourceTraceId === "string") {
1332
+ return payload.sourceTraceId;
1333
+ }
1334
+ const rawTrace = payload.externalTrace ?? payload.rawTrace;
1335
+ const id = rawTrace?.id;
1336
+ return typeof id === "string" ? id : void 0;
1337
+ }
1338
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, carrierSeq, HttpClient;
1178
1339
  var init_http = __esm({
1179
1340
  "src/http.ts"() {
1180
1341
  "use strict";
@@ -1184,9 +1345,12 @@ var init_http = __esm({
1184
1345
  init_replayContext();
1185
1346
  init_serializePayload();
1186
1347
  init_transport();
1348
+ init_transportTypes();
1187
1349
  init_unrefTimer();
1188
1350
  init_warnOnce();
1189
1351
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
1352
+ OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
1353
+ RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
1190
1354
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
1191
1355
  DEFAULT_LIFECYCLE_TIMEOUT_MS2 = 3e4;
1192
1356
  pendingTracePromises = /* @__PURE__ */ new Set();
@@ -1206,8 +1370,12 @@ var init_http = __esm({
1206
1370
  });
1207
1371
  });
1208
1372
  }
1373
+ carrierSeq = 0;
1209
1374
  HttpClient = class {
1210
1375
  constructor(config) {
1376
+ // Only traces a caller asked about are tracked, so ordinary tracing stores
1377
+ // nothing here.
1378
+ this.traceDeliveries = /* @__PURE__ */ new Map();
1211
1379
  // Deferred span work owned by THIS client. The module-global set backs the
1212
1380
  // process-wide `flushTraces()` and the exit hook, but per-client lifecycle
1213
1381
  // must not wait on another client's slow finalize: a false `close()` failure
@@ -1243,13 +1411,131 @@ var init_http = __esm({
1243
1411
  }
1244
1412
  if (!this.traceTransport) {
1245
1413
  this.traceTransport = createTraceTransport({
1246
- directSender: (endpoint, body, timeoutMs) => this.sendEncoded(endpoint, body, {
1247
- timeout: timeoutMs
1248
- })
1414
+ directSender: (request, timeoutMs) => this.deliverCarriers(request, timeoutMs),
1415
+ onDelivered: (refs) => this.recordDeliveredCarriers(refs)
1249
1416
  });
1250
1417
  }
1251
1418
  return this.traceTransport;
1252
1419
  }
1420
+ /**
1421
+ * Post one encoded batch and decide what the server's answer means, so the
1422
+ * transport never reads a response. Rejections and permanent statuses come
1423
+ * back as a non-retryable {@link DeliveryError}; anything the server might
1424
+ * still accept on a second try comes back retryable.
1425
+ */
1426
+ async deliverCarriers(request, timeoutMs) {
1427
+ let response;
1428
+ try {
1429
+ response = await this.sendPrepared(
1430
+ OTLP_TRACES_ENDPOINT,
1431
+ request,
1432
+ { timeout: timeoutMs }
1433
+ );
1434
+ } catch (error) {
1435
+ const status = error instanceof BitfabError ? error.status : void 0;
1436
+ if (status === void 0) {
1437
+ throw new DeliveryError(`OTLP ingestion failed: ${String(error)}`, {
1438
+ retryable: true
1439
+ });
1440
+ }
1441
+ throw new DeliveryError(`OTLP ingestion failed with HTTP ${status}`, {
1442
+ retryable: RETRYABLE_STATUSES.has(status),
1443
+ oversized: status === 413,
1444
+ ...error instanceof BitfabError && error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {}
1445
+ });
1446
+ }
1447
+ const partialSuccess = asPayloadRecord(response?.partialSuccess);
1448
+ const rejected = partialSuccess?.rejectedSpans;
1449
+ if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
1450
+ throw new DeliveryError(
1451
+ `OTLP ingestion rejected ${rejected} span(s): ${partialSuccess?.errorMessage ?? "no reason provided"}`
1452
+ );
1453
+ }
1454
+ }
1455
+ /**
1456
+ * Start tracking delivery for `traceIds`. Nothing is recorded for a trace
1457
+ * that was never tracked, so ordinary tracing costs no bookkeeping at all.
1458
+ */
1459
+ trackTraceDeliveries(traceIds) {
1460
+ for (const traceId of traceIds) {
1461
+ if (!this.traceDeliveries.has(traceId)) {
1462
+ this.traceDeliveries.set(traceId, {
1463
+ submittedSpanIds: /* @__PURE__ */ new Set(),
1464
+ ackedSpanIds: /* @__PURE__ */ new Set(),
1465
+ closed: false,
1466
+ closingAcked: false
1467
+ });
1468
+ }
1469
+ }
1470
+ }
1471
+ /** Whether any tracked trace has had its closing carrier submitted. */
1472
+ hasClosedDeliveries(traceIds) {
1473
+ return traceIds.some((traceId) => this.traceDeliveries.get(traceId)?.closed);
1474
+ }
1475
+ /**
1476
+ * Report what each tracked trace submitted and whether the server confirmed
1477
+ * it, and stop tracking them. Every id passed is freed, so a caller cannot
1478
+ * leak a record for a trace that never closed.
1479
+ *
1480
+ * `delivered` is only meaningful once a flush has settled: acks land before
1481
+ * an export resolves, so a flush that reported success has already collected
1482
+ * every ack it is going to collect.
1483
+ */
1484
+ takeTraceDeliveries(traceIds) {
1485
+ const reports = {};
1486
+ for (const traceId of traceIds) {
1487
+ const delivery = this.traceDeliveries.get(traceId);
1488
+ if (delivery === void 0) {
1489
+ continue;
1490
+ }
1491
+ this.traceDeliveries.delete(traceId);
1492
+ reports[traceId] = {
1493
+ spanCount: delivery.submittedSpanIds.size,
1494
+ closed: delivery.closed,
1495
+ delivered: delivery.closingAcked && [...delivery.submittedSpanIds].every(
1496
+ (spanId) => delivery.ackedSpanIds.has(spanId)
1497
+ )
1498
+ };
1499
+ }
1500
+ return reports;
1501
+ }
1502
+ /** Build a carrier's meta and record what it adds to its trace's expected set. */
1503
+ recordedMeta(operation, payload, ref) {
1504
+ this.recordSubmittedCarrier(ref);
1505
+ return carrierMeta(operation, payload, ref);
1506
+ }
1507
+ recordSubmittedCarrier(ref) {
1508
+ if (ref === void 0) {
1509
+ return;
1510
+ }
1511
+ const delivery = this.traceDeliveries.get(ref.traceId);
1512
+ if (delivery === void 0) {
1513
+ return;
1514
+ }
1515
+ if (ref.spanId === void 0) {
1516
+ delivery.closed = true;
1517
+ } else {
1518
+ delivery.submittedSpanIds.add(ref.spanId);
1519
+ }
1520
+ }
1521
+ /**
1522
+ * Ingestion commits every carrier in a request before it answers, so a
1523
+ * delivered ref is proof its row exists: the same fact the replay status
1524
+ * endpoint would report, already in hand.
1525
+ */
1526
+ recordDeliveredCarriers(refs) {
1527
+ for (const ref of refs) {
1528
+ const delivery = this.traceDeliveries.get(ref.traceId);
1529
+ if (delivery === void 0) {
1530
+ continue;
1531
+ }
1532
+ if (ref.spanId === void 0) {
1533
+ delivery.closingAcked = true;
1534
+ } else {
1535
+ delivery.ackedSpanIds.add(ref.spanId);
1536
+ }
1537
+ }
1538
+ }
1253
1539
  /**
1254
1540
  * Track deferred span work so this client's own lifecycle waits for it, and
1255
1541
  * so the process-wide flush and exit hook do too.
@@ -1330,13 +1616,16 @@ var init_http = __esm({
1330
1616
  * same data twice.
1331
1617
  */
1332
1618
  async sendEncoded(endpoint, body, options) {
1619
+ const prepared = encodeRequestBody(body);
1620
+ const encoded = prepared instanceof Promise ? await prepared : prepared;
1621
+ return this.sendPrepared(endpoint, encoded, options);
1622
+ }
1623
+ async sendPrepared(endpoint, encoded, options) {
1333
1624
  const url = `${this.serviceUrl}${endpoint}`;
1334
1625
  const timeout = options?.timeout ?? this.timeout;
1335
1626
  const method = options?.method ?? "POST";
1336
1627
  const controller = new AbortController();
1337
1628
  const timeoutId = setTimeout(() => controller.abort(), timeout);
1338
- const prepared = encodeRequestBody(body);
1339
- const encoded = prepared instanceof Promise ? await prepared : prepared;
1340
1629
  const headers = {
1341
1630
  "Content-Type": "application/json",
1342
1631
  Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
@@ -1356,7 +1645,8 @@ var init_http = __esm({
1356
1645
  throw new BitfabError(
1357
1646
  `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1358
1647
  void 0,
1359
- response.status
1648
+ response.status,
1649
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1360
1650
  );
1361
1651
  }
1362
1652
  const result = await response.json();
@@ -1442,11 +1732,12 @@ var init_http = __esm({
1442
1732
  * the OTLP carrier has no path to carry it.
1443
1733
  */
1444
1734
  sendInternalTrace(functionId, payload) {
1445
- this.getTraceTransport()?.submit("internal_trace", {
1446
- ...payload,
1447
- functionId,
1448
- sdkVersion: __version__
1449
- });
1735
+ const body = { ...payload, functionId, sdkVersion: __version__ };
1736
+ this.getTraceTransport()?.submit(
1737
+ "internal_trace",
1738
+ body,
1739
+ carrierMeta("internal_trace", body, void 0)
1740
+ );
1450
1741
  }
1451
1742
  /**
1452
1743
  * Queue an external span (from withSpan wrapper or OpenAI tracing) onto this
@@ -1455,10 +1746,11 @@ var init_http = __esm({
1455
1746
  * promise.
1456
1747
  */
1457
1748
  sendExternalSpan(payload) {
1458
- this.getTraceTransport()?.submit("external_span", {
1459
- ...payload,
1460
- sdkVersion: __version__
1461
- });
1749
+ this.getTraceTransport()?.submit(
1750
+ "external_span",
1751
+ { ...payload, sdkVersion: __version__ },
1752
+ this.recordedMeta("external_span", payload, carrierRef(payload))
1753
+ );
1462
1754
  }
1463
1755
  /**
1464
1756
  * Queue an external trace completion (from OpenAI tracing) onto this
@@ -1467,10 +1759,15 @@ var init_http = __esm({
1467
1759
  * server-authoritative barrier in `replay.ts`, not by awaiting this call.
1468
1760
  */
1469
1761
  sendExternalTrace(payload) {
1470
- this.getTraceTransport()?.submit("external_trace", {
1471
- ...payload,
1472
- sdkVersion: __version__
1473
- });
1762
+ this.getTraceTransport()?.submit(
1763
+ "external_trace",
1764
+ { ...payload, sdkVersion: __version__ },
1765
+ this.recordedMeta(
1766
+ "external_trace",
1767
+ payload,
1768
+ payload.completed === true ? carrierRef(payload) : void 0
1769
+ )
1770
+ );
1474
1771
  }
1475
1772
  /**
1476
1773
  * Partial update of an existing trace identified by its Bitfab trace ID.
@@ -1810,8 +2107,8 @@ var init_serialize = __esm({
1810
2107
  import_superjson = __toESM(require("superjson"), 1);
1811
2108
  init_payloadBudget();
1812
2109
  init_warnOnce();
1813
- MAX_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1814
- MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
2110
+ MAX_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
2111
+ MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
1815
2112
  MAX_SAFE_DEPTH = 6;
1816
2113
  }
1817
2114
  });
@@ -2066,7 +2363,8 @@ __export(replay_exports, {
2066
2363
  ReplayError: () => ReplayError,
2067
2364
  replay: () => replay,
2068
2365
  reportReplayProgress: () => reportReplayProgress,
2069
- serializeReplayResult: () => serializeReplayResult
2366
+ serializeReplayResult: () => serializeReplayResult,
2367
+ waitForReplayPersistence: () => waitForReplayPersistence
2070
2368
  });
2071
2369
  function dbBranchEnabled(dbBranch) {
2072
2370
  return dbBranch !== void 0 && dbBranch !== false;
@@ -2354,15 +2652,29 @@ async function waitForReplayPersistence(httpClient, testRunId, replayedTraceIds)
2354
2652
  REPLAY_PERSISTENCE_TIMEOUT_MS
2355
2653
  );
2356
2654
  if (!deferredSettled) {
2655
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2357
2656
  throw new BitfabError(
2358
2657
  `Replay could not settle deferred span work before the deadline, so the expected span counts are incomplete (testRunId ${testRunId}).`
2359
2658
  );
2360
2659
  }
2361
- const expectedSpanCounts = takeReplaySpanCounts2(replayedTraceIds);
2362
- if (Object.keys(expectedSpanCounts).length === 0) {
2660
+ if (!httpClient.hasClosedDeliveries(replayedTraceIds)) {
2661
+ httpClient.takeTraceDeliveries(replayedTraceIds);
2363
2662
  return;
2364
2663
  }
2365
2664
  const flushed = await flushTraces(REPLAY_PERSISTENCE_TIMEOUT_MS);
2665
+ const deliveries = httpClient.takeTraceDeliveries(replayedTraceIds);
2666
+ const expectedSpanCounts = {};
2667
+ let allDelivered = true;
2668
+ for (const [traceId, delivery] of Object.entries(deliveries)) {
2669
+ if (!delivery.closed) {
2670
+ continue;
2671
+ }
2672
+ expectedSpanCounts[traceId] = delivery.spanCount;
2673
+ allDelivered = allDelivered && delivery.delivered;
2674
+ }
2675
+ if (allDelivered) {
2676
+ return;
2677
+ }
2366
2678
  const deadline = Date.now() + REPLAY_PERSISTENCE_TIMEOUT_MS;
2367
2679
  let missing = Object.keys(expectedSpanCounts).length;
2368
2680
  while (true) {
@@ -2471,6 +2783,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2471
2783
  ...registeredOverrides
2472
2784
  ];
2473
2785
  const replayedTraceIds = serverItems.map(() => randomUuid());
2786
+ httpClient.trackTraceDeliveries(replayedTraceIds);
2474
2787
  const tasks = serverItems.map(
2475
2788
  (serverItem, index) => () => processItem(
2476
2789
  httpClient,
@@ -2668,7 +2981,6 @@ var init_replay = __esm({
2668
2981
  init_randomUuid();
2669
2982
  init_replayContext();
2670
2983
  init_serialize();
2671
- init_transport();
2672
2984
  init_unrefTimer();
2673
2985
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
2674
2986
  BITFAB_PROGRESS_PREFIX = "@@bitfab:progress ";