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