bitfab 0.34.1 → 0.36.0

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.34.1";
54
+ __version__ = "0.36.0";
55
55
  }
56
56
  });
57
57
 
@@ -65,6 +65,87 @@ var init_constants = __esm({
65
65
  }
66
66
  });
67
67
 
68
+ // src/readEnv.ts
69
+ function readEnv(name) {
70
+ if (typeof process !== "undefined" && process.env) {
71
+ return process.env[name];
72
+ }
73
+ return void 0;
74
+ }
75
+ var init_readEnv = __esm({
76
+ "src/readEnv.ts"() {
77
+ "use strict";
78
+ }
79
+ });
80
+
81
+ // src/compress.ts
82
+ function toArrayBuffer(view) {
83
+ return view.buffer.slice(
84
+ view.byteOffset,
85
+ view.byteOffset + view.byteLength
86
+ );
87
+ }
88
+ async function gzipViaStream(bytes) {
89
+ const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
90
+ return await new Response(stream).arrayBuffer();
91
+ }
92
+ function encodeRequestBody(body) {
93
+ if (readEnv(DISABLE_COMPRESSION_ENV)) {
94
+ return { body };
95
+ }
96
+ const bytes = new TextEncoder().encode(body);
97
+ if (bytes.byteLength < MIN_COMPRESSED_BYTES) {
98
+ return { body };
99
+ }
100
+ if (gzipNode) {
101
+ return gzipNode(bytes).then(
102
+ (compressed) => ({
103
+ body: toArrayBuffer(compressed),
104
+ contentEncoding: "gzip"
105
+ }),
106
+ () => ({ body })
107
+ );
108
+ }
109
+ if (typeof CompressionStream === "undefined") {
110
+ return { body };
111
+ }
112
+ return gzipViaStream(bytes).then(
113
+ (compressed) => ({ body: compressed, contentEncoding: "gzip" }),
114
+ () => ({ body })
115
+ );
116
+ }
117
+ var DISABLE_COMPRESSION_ENV, MIN_COMPRESSED_BYTES, gzipNode, _nodeGzipReady;
118
+ var init_compress = __esm({
119
+ "src/compress.ts"() {
120
+ "use strict";
121
+ init_readEnv();
122
+ DISABLE_COMPRESSION_ENV = "BITFAB_DISABLE_COMPRESSION";
123
+ MIN_COMPRESSED_BYTES = 8192;
124
+ _nodeGzipReady = (typeof process !== "undefined" && process.versions?.node ? (
125
+ // The join trick hides "node:zlib" from static analysis so bundlers that
126
+ // ban Node.js built-ins don't fail at build time. webpackIgnore tells
127
+ // webpack/turbopack to emit a native import() so Node.js can resolve the
128
+ // module at runtime. Same pattern as `asyncStorage.ts`.
129
+ import(
130
+ /* webpackIgnore: true */
131
+ ["node", "zlib"].join(":")
132
+ ).then(({ gzip }) => {
133
+ gzipNode = (data) => new Promise((resolve, reject) => {
134
+ gzip(data, (error, result) => {
135
+ if (error) {
136
+ reject(error);
137
+ } else {
138
+ resolve(result);
139
+ }
140
+ });
141
+ });
142
+ }).catch(() => {
143
+ })
144
+ ) : Promise.resolve()).then(() => {
145
+ });
146
+ }
147
+ });
148
+
68
149
  // src/errors.ts
69
150
  var BitfabError;
70
151
  var init_errors = __esm({
@@ -153,6 +234,131 @@ var init_replayContext = __esm({
153
234
  }
154
235
  });
155
236
 
237
+ // src/payloadBudget.ts
238
+ function byteLength(value) {
239
+ return textEncoder ? textEncoder.encode(value).length : value.length;
240
+ }
241
+ function carrierByteLength(body) {
242
+ return carrierBytesOf(textEncoder ? textEncoder.encode(body) : null, body);
243
+ }
244
+ function carrierBytesOf(encoded, body) {
245
+ if (!encoded) {
246
+ return body.length + 2;
247
+ }
248
+ let extra = 2;
249
+ for (let i = 0; i < encoded.length; i++) {
250
+ const byte = encoded[i];
251
+ if (byte === 34 || byte === 92) {
252
+ extra += 1;
253
+ } else if (byte < 32) {
254
+ extra += byte === 8 || byte === 9 || byte === 10 || byte === 12 || byte === 13 ? 1 : 5;
255
+ }
256
+ }
257
+ return encoded.length + extra;
258
+ }
259
+ function fitsCarrierBudget(body) {
260
+ const units = body.length;
261
+ if (units * MAX_BYTES_PER_UNIT + 2 <= MAX_SPAN_CARRIER_BYTES) {
262
+ return true;
263
+ }
264
+ if (units + 2 > MAX_SPAN_CARRIER_BYTES) {
265
+ return false;
266
+ }
267
+ return carrierByteLength(body) <= MAX_SPAN_CARRIER_BYTES;
268
+ }
269
+ function asRecord(value) {
270
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
271
+ }
272
+ function cloneTrimmable(payload) {
273
+ const copy = { ...payload };
274
+ const containers = [];
275
+ const spanData = asRecord(copy.span_data);
276
+ if (spanData) {
277
+ const clone = { ...spanData };
278
+ copy.span_data = clone;
279
+ containers.push(clone);
280
+ }
281
+ const rawSpan = asRecord(copy.rawSpan);
282
+ const rawSpanData = rawSpan && asRecord(rawSpan.span_data);
283
+ if (rawSpan && rawSpanData) {
284
+ const clone = { ...rawSpanData };
285
+ copy.rawSpan = { ...rawSpan, span_data: clone };
286
+ containers.push(clone);
287
+ }
288
+ if (containers.length === 0) {
289
+ containers.push(copy);
290
+ }
291
+ return { copy, containers };
292
+ }
293
+ function collectCandidates(containers) {
294
+ const candidates = [];
295
+ for (const container of containers) {
296
+ for (const [key, value] of Object.entries(container)) {
297
+ if (STRUCTURAL_SPAN_KEYS.has(key) || value == null) {
298
+ continue;
299
+ }
300
+ let size;
301
+ try {
302
+ size = byteLength(JSON.stringify(value) ?? "");
303
+ } catch {
304
+ continue;
305
+ }
306
+ candidates.push({ container, key, size });
307
+ }
308
+ }
309
+ return candidates.sort((a, b) => b.size - a.size);
310
+ }
311
+ function trimPayloadToBudget(payload, encode) {
312
+ const { copy, containers } = cloneTrimmable(payload);
313
+ const candidates = collectCandidates(containers);
314
+ if (candidates.length === 0) {
315
+ return void 0;
316
+ }
317
+ const trimmed = [];
318
+ for (const candidate of candidates) {
319
+ candidate.container[candidate.key] = `<unserializable: too_large_${candidate.size}_bytes>`;
320
+ trimmed.push(candidate.key);
321
+ let body;
322
+ try {
323
+ body = encode(copy);
324
+ } catch {
325
+ return void 0;
326
+ }
327
+ if (fitsCarrierBudget(body)) {
328
+ return { value: copy, trimmed };
329
+ }
330
+ }
331
+ return void 0;
332
+ }
333
+ function markPayloadTrimmed(value, trimmed) {
334
+ const existing = Array.isArray(value.errors) ? value.errors : [];
335
+ value.errors = [
336
+ ...existing,
337
+ {
338
+ source: "sdk",
339
+ step: "payload_budget",
340
+ error: `trimmed oversized field(s) to fit the ${MAX_SPAN_CARRIER_BYTES}-byte span carrier budget: ${[
341
+ ...new Set(trimmed)
342
+ ].join(", ")}`
343
+ }
344
+ ];
345
+ }
346
+ var MAX_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
347
+ var init_payloadBudget = __esm({
348
+ "src/payloadBudget.ts"() {
349
+ "use strict";
350
+ MAX_SPAN_CARRIER_BYTES = 28e5;
351
+ textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
352
+ MAX_BYTES_PER_UNIT = 3;
353
+ STRUCTURAL_SPAN_KEYS = /* @__PURE__ */ new Set([
354
+ "name",
355
+ "type",
356
+ "function_name",
357
+ "error_source"
358
+ ]);
359
+ }
360
+ });
361
+
156
362
  // src/warnOnce.ts
157
363
  function warnOnce(key, message) {
158
364
  if (warned.has(key)) {
@@ -174,8 +380,37 @@ var init_warnOnce = __esm({
174
380
 
175
381
  // src/serializePayload.ts
176
382
  function serializePayloadBody(payload) {
383
+ const encoded = encodePayloadBody(payload);
384
+ if (fitsCarrierBudget(encoded.body)) {
385
+ return { body: encoded.body, dropped: encoded.dropped };
386
+ }
387
+ return applyPayloadBudget(encoded);
388
+ }
389
+ function applyPayloadBudget(encoded) {
390
+ const result = encoded.value ? trimPayloadToBudget(
391
+ encoded.value,
392
+ (value) => encodePayloadBody(value).body
393
+ ) : void 0;
394
+ if (!result) {
395
+ return { body: encoded.body, dropped: encoded.dropped };
396
+ }
397
+ warnOnce(
398
+ "payload:over-budget",
399
+ `a span payload exceeded the ${MAX_SPAN_CARRIER_BYTES}-byte carrier budget; its largest field(s) (${[
400
+ ...new Set(result.trimmed)
401
+ ].join(
402
+ ", "
403
+ )}) were replaced with placeholders so the span still ships. The span is incomplete and may not be replayable.`
404
+ );
405
+ markPayloadTrimmed(result.value, result.trimmed);
406
+ return {
407
+ body: encodePayloadBody(result.value).body,
408
+ dropped: encoded.dropped
409
+ };
410
+ }
411
+ function encodePayloadBody(payload) {
177
412
  try {
178
- return { body: JSON.stringify(payload), dropped: [] };
413
+ return { body: JSON.stringify(payload), dropped: [], value: payload };
179
414
  } catch {
180
415
  const dropped = [];
181
416
  const sanitize = (value, seen) => {
@@ -240,12 +475,11 @@ function serializePayloadBody(payload) {
240
475
  sanitized = sanitize(payload, /* @__PURE__ */ new WeakSet());
241
476
  } catch (error) {
242
477
  const message = error instanceof Error ? error.message : String(error);
243
- return {
244
- body: JSON.stringify({ error: `payload_serialize_failed: ${message}` }),
245
- dropped
246
- };
478
+ const marker = { error: `payload_serialize_failed: ${message}` };
479
+ return { body: JSON.stringify(marker), dropped, value: marker };
247
480
  }
248
- if (dropped.length > 0 && typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
481
+ const isRecord = typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized);
482
+ if (dropped.length > 0 && isRecord) {
249
483
  const obj = sanitized;
250
484
  const existing = Array.isArray(obj.errors) ? obj.errors : [];
251
485
  obj.errors = [
@@ -259,29 +493,21 @@ function serializePayloadBody(payload) {
259
493
  }
260
494
  ];
261
495
  }
262
- return { body: JSON.stringify(sanitized), dropped };
496
+ return {
497
+ body: JSON.stringify(sanitized),
498
+ dropped,
499
+ value: isRecord ? sanitized : void 0
500
+ };
263
501
  }
264
502
  }
265
503
  var init_serializePayload = __esm({
266
504
  "src/serializePayload.ts"() {
267
505
  "use strict";
506
+ init_payloadBudget();
268
507
  init_warnOnce();
269
508
  }
270
509
  });
271
510
 
272
- // src/readEnv.ts
273
- function readEnv(name) {
274
- if (typeof process !== "undefined" && process.env) {
275
- return process.env[name];
276
- }
277
- return void 0;
278
- }
279
- var init_readEnv = __esm({
280
- "src/readEnv.ts"() {
281
- "use strict";
282
- }
283
- });
284
-
285
511
  // src/unrefTimer.ts
286
512
  function unrefTimer(timer) {
287
513
  const handle = timer;
@@ -327,7 +553,7 @@ function recordTraceSubmission(operation, payload) {
327
553
  return;
328
554
  }
329
555
  if (operation === "external_span") {
330
- const rawSpan = asRecord(payload.rawSpan);
556
+ const rawSpan = asRecord2(payload.rawSpan);
331
557
  if (typeof rawSpan?.id !== "string") {
332
558
  submissionCounter += 1;
333
559
  }
@@ -364,14 +590,14 @@ function takeReplaySpanCounts(traceIds) {
364
590
  }
365
591
  return counts;
366
592
  }
367
- function asRecord(value) {
593
+ function asRecord2(value) {
368
594
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
369
595
  }
370
596
  function resolveSourceTraceId(payload) {
371
597
  if (typeof payload.sourceTraceId === "string") {
372
598
  return payload.sourceTraceId;
373
599
  }
374
- const rawTrace = asRecord(payload.externalTrace) ?? asRecord(payload.rawTrace);
600
+ const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
375
601
  return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
376
602
  }
377
603
  function otlpValue(value) {
@@ -429,32 +655,27 @@ function spanToOtlp(span) {
429
655
  }
430
656
  return result;
431
657
  }
432
- function buildOtlpRequest(first, spans) {
658
+ function encodeSpan(span) {
659
+ const json = JSON.stringify(spanToOtlp(span));
660
+ return { json, size: byteLength(json) };
661
+ }
662
+ function requestEnvelope(first) {
433
663
  const scope = first.instrumentationScope;
434
- return {
435
- resourceSpans: [
436
- {
437
- resource: {
438
- attributes: otlpAttributes(
439
- first.resource.attributes
440
- )
441
- },
442
- scopeSpans: [
443
- {
444
- scope: { name: scope.name, version: scope.version ?? "" },
445
- spans
446
- }
447
- ]
448
- }
449
- ]
450
- };
664
+ const resource = JSON.stringify({
665
+ attributes: otlpAttributes(
666
+ first.resource.attributes
667
+ )
668
+ });
669
+ const scopeJson = JSON.stringify({
670
+ name: scope.name,
671
+ version: scope.version ?? ""
672
+ });
673
+ const head = `{"resourceSpans":[{"resource":${resource},"scopeSpans":[{"scope":${scopeJson},"spans":[`;
674
+ const tail = "]}]}]}";
675
+ return { head, tail, size: byteLength(head) + byteLength(tail) };
451
676
  }
452
- function encodedSize(value) {
453
- const json = JSON.stringify(value);
454
- if (typeof TextEncoder !== "undefined") {
455
- return new TextEncoder().encode(json).length;
456
- }
457
- return json.length;
677
+ function encodeRequest(envelope, spans) {
678
+ return envelope.head + spans.map((span) => span.json).join(",") + envelope.tail;
458
679
  }
459
680
  function delay(ms) {
460
681
  return new Promise((resolve) => {
@@ -504,16 +725,12 @@ function isRetryable(error) {
504
725
  }
505
726
  return RETRYABLE_STATUSES.has(status) || status >= 500;
506
727
  }
507
- function normalizeCollectorEndpoint(endpoint) {
508
- const trimmed = endpoint.replace(/\/+$/, "");
509
- return trimmed.endsWith("/v1/traces") ? trimmed : `${trimmed}/v1/traces`;
510
- }
511
728
  function endSpan(span, endTime) {
512
729
  span.end(endTime);
513
730
  }
514
731
  function spanName(operation, payload) {
515
732
  if (operation === "external_span") {
516
- const spanData = asRecord(asRecord(payload.rawSpan)?.span_data);
733
+ const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
517
734
  if (typeof spanData?.name === "string") {
518
735
  return spanData.name;
519
736
  }
@@ -524,8 +741,8 @@ function spanName(operation, payload) {
524
741
  return `bitfab.${operation}`;
525
742
  }
526
743
  function payloadTimestamp(payload, field) {
527
- const rawSpan = asRecord(payload.rawSpan);
528
- const rawTrace = asRecord(payload.externalTrace) ?? asRecord(payload.rawTrace);
744
+ const rawSpan = asRecord2(payload.rawSpan);
745
+ const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
529
746
  const raw = rawSpan?.[field] ?? rawTrace?.[field];
530
747
  if (typeof raw !== "string") {
531
748
  return void 0;
@@ -534,7 +751,7 @@ function payloadTimestamp(payload, field) {
534
751
  return Number.isNaN(parsed) ? void 0 : parsed;
535
752
  }
536
753
  function hasError(payload) {
537
- const spanData = asRecord(asRecord(payload.rawSpan)?.span_data);
754
+ const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
538
755
  if (spanData?.error != null) {
539
756
  return true;
540
757
  }
@@ -544,7 +761,6 @@ function hasError(payload) {
544
761
  function createOtelTransport(options) {
545
762
  return new OtelBatchTransport({
546
763
  ...options,
547
- collectorEndpoint: readEnv(COLLECTOR_ENDPOINT_ENV) || void 0,
548
764
  exportConcurrency: readBoundedIntEnv(
549
765
  EXPORT_CONCURRENCY_ENV,
550
766
  MAX_EXPORT_CONCURRENCY,
@@ -579,7 +795,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
579
795
  (transport, remaining) => transport.shutdown(remaining)
580
796
  );
581
797
  }
582
- 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, COLLECTOR_ENDPOINT_ENV, MAX_QUEUE_SIZE, DIRECT_MAX_EXPORT_BATCH_SIZE, COLLECTOR_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, OtlpPayloadTooLargeError, OtlpPartialSuccessError, BitfabSpanExporter, CollectorSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
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;
583
799
  var init_otel = __esm({
584
800
  "src/otel.ts"() {
585
801
  "use strict";
@@ -589,6 +805,7 @@ var init_otel = __esm({
589
805
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
590
806
  init_constants();
591
807
  init_errors();
808
+ init_payloadBudget();
592
809
  init_readEnv();
593
810
  init_serializePayload();
594
811
  init_unrefTimer();
@@ -599,10 +816,8 @@ var init_otel = __esm({
599
816
  MAX_EXPORT_REQUEST_BYTES = 3e6;
600
817
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
601
818
  EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
602
- COLLECTOR_ENDPOINT_ENV = "BITFAB_OTEL_EXPORTER_ENDPOINT";
603
819
  MAX_QUEUE_SIZE = 8192;
604
820
  DIRECT_MAX_EXPORT_BATCH_SIZE = 512;
605
- COLLECTOR_MAX_EXPORT_BATCH_SIZE = 32;
606
821
  DIRECT_MAX_REQUEST_BATCH_SIZE = 8;
607
822
  DEFAULT_EXPORT_CONCURRENCY = 32;
608
823
  MAX_EXPORT_CONCURRENCY = 64;
@@ -616,6 +831,7 @@ var init_otel = __esm({
616
831
  traceSubmissionSpanIds = /* @__PURE__ */ new Map();
617
832
  replayTraceSubmissions = /* @__PURE__ */ new Set();
618
833
  submissionCounter = 0;
834
+ SPAN_SEPARATOR_BYTES = 1;
619
835
  OtlpPayloadTooLargeError = class extends Error {
620
836
  };
621
837
  OtlpPartialSuccessError = class extends Error {
@@ -644,57 +860,55 @@ var init_otel = __esm({
644
860
  return true;
645
861
  }
646
862
  let encoded;
863
+ let envelope;
647
864
  try {
648
- encoded = spans.map(spanToOtlp);
865
+ encoded = spans.map(encodeSpan);
866
+ envelope = requestEnvelope(spans[0]);
649
867
  } catch (error) {
650
868
  logError("failed to encode an OpenTelemetry span batch", error);
651
869
  return false;
652
870
  }
653
- const first = spans[0];
654
- const batches = this.buildRequestBatches(first, encoded);
871
+ const batches = this.buildRequestBatches(envelope, encoded);
655
872
  const results = await mapWithConcurrency(
656
873
  batches,
657
874
  this.exportConcurrency,
658
- (batch) => this.send(first, batch)
875
+ (batch) => this.send(envelope, batch)
659
876
  );
660
877
  return results.every(Boolean);
661
878
  }
662
- buildRequestBatches(first, spans) {
879
+ buildRequestBatches(envelope, spans) {
663
880
  const batches = [];
664
881
  let current = [];
882
+ let size = envelope.size;
665
883
  for (const span of spans) {
666
- if (current.length >= this.maxRequestBatchSize) {
667
- batches.push(current);
884
+ const addition = span.size + (current.length > 0 ? SPAN_SEPARATOR_BYTES : 0);
885
+ if (current.length > 0 && (current.length >= this.maxRequestBatchSize || size + addition > this.maxRequestBytes)) {
886
+ batches.push({ spans: current, size });
668
887
  current = [];
888
+ size = envelope.size;
669
889
  }
670
- const candidate = [...current, span];
671
- if (current.length > 0 && encodedSize(buildOtlpRequest(first, candidate)) > this.maxRequestBytes) {
672
- batches.push(current);
673
- current = [span];
674
- } else {
675
- current = candidate;
676
- }
890
+ current.push(span);
891
+ size += span.size + (current.length > 1 ? SPAN_SEPARATOR_BYTES : 0);
677
892
  }
678
893
  if (current.length > 0) {
679
- batches.push(current);
894
+ batches.push({ spans: current, size });
680
895
  }
681
896
  return batches;
682
897
  }
683
- async send(first, spans) {
684
- const payload = buildOtlpRequest(first, spans);
685
- if (encodedSize(payload) > this.maxRequestBytes) {
898
+ async send(envelope, batch) {
899
+ if (batch.size > this.maxRequestBytes) {
686
900
  logError(
687
901
  "a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
688
902
  );
689
903
  return false;
690
904
  }
691
905
  try {
692
- await this.sendWithRetries(payload);
906
+ await this.sendWithRetries(encodeRequest(envelope, batch.spans));
693
907
  return true;
694
908
  } catch (error) {
695
909
  if (error instanceof OtlpPayloadTooLargeError) {
696
910
  logError(
697
- 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"
911
+ 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"
698
912
  );
699
913
  return false;
700
914
  }
@@ -718,15 +932,15 @@ var init_otel = __esm({
718
932
  * the server does not yet understand. The fix is a client-supplied
719
933
  * idempotency key that ingestion dedupes on.
720
934
  */
721
- async sendWithRetries(payload) {
935
+ async sendWithRetries(body) {
722
936
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
723
937
  try {
724
938
  const response = await this.directSender(
725
939
  OTLP_TRACES_ENDPOINT,
726
- payload,
940
+ body,
727
941
  EXPORT_TIMEOUT_MILLIS
728
942
  );
729
- const partialSuccess = asRecord(response?.partialSuccess);
943
+ const partialSuccess = asRecord2(response?.partialSuccess);
730
944
  const rejected = partialSuccess?.rejectedSpans;
731
945
  if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
732
946
  logError(
@@ -754,115 +968,6 @@ var init_otel = __esm({
754
968
  async forceFlush() {
755
969
  }
756
970
  };
757
- CollectorSpanExporter = class {
758
- constructor(endpoint, apiKey, maxRequestBytes) {
759
- this.endpoint = endpoint;
760
- this.apiKey = apiKey;
761
- this.maxRequestBytes = maxRequestBytes;
762
- }
763
- /**
764
- * Loaded through a dynamic import rather than a top-level one so bundlers
765
- * code-split it: Collector delivery is opt-in, and a consumer who never sets
766
- * an endpoint should not pay for the exporter in their initial bundle. It is
767
- * a hard dependency, so this cannot fail for want of the package.
768
- */
769
- loadExporterModule() {
770
- if (!this.pendingModule) {
771
- this.pendingModule = import("@opentelemetry/exporter-trace-otlp-proto");
772
- }
773
- return this.pendingModule;
774
- }
775
- export(spans, resultCallback) {
776
- void this.exportAsync(spans).then(
777
- (succeeded) => {
778
- resultCallback({
779
- code: succeeded ? import_core.ExportResultCode.SUCCESS : import_core.ExportResultCode.FAILED
780
- });
781
- },
782
- (error) => {
783
- resultCallback({ code: import_core.ExportResultCode.FAILED, error });
784
- }
785
- );
786
- }
787
- async exportAsync(spans) {
788
- if (spans.length === 0) {
789
- return true;
790
- }
791
- let delegate;
792
- try {
793
- delegate = await this.resolveDelegate();
794
- } catch (error) {
795
- logError("failed to build the OTLP Collector exporter", error);
796
- return false;
797
- }
798
- const results = await Promise.all(
799
- this.partition(spans).map(
800
- (batch) => new Promise((resolve) => {
801
- try {
802
- delegate.export(batch, (result) => {
803
- resolve(result.code === import_core.ExportResultCode.SUCCESS);
804
- });
805
- } catch (error) {
806
- logError("Collector export threw", error);
807
- resolve(false);
808
- }
809
- })
810
- )
811
- );
812
- return results.every(Boolean);
813
- }
814
- /**
815
- * Partition by the encoded JSON size of each carrier rather than its encoded
816
- * protobuf size. Protobuf is strictly smaller than the equivalent JSON for
817
- * these payloads, so the JSON figure is a conservative bound that keeps every
818
- * request under the target without pulling `@opentelemetry/otlp-transformer`
819
- * into the dependency set purely to measure bytes.
820
- */
821
- partition(spans) {
822
- const batches = [];
823
- let current = [];
824
- let currentSize = 0;
825
- for (const span of spans) {
826
- const size = encodedSize(spanToOtlp(span));
827
- if (current.length > 0 && currentSize + size > this.maxRequestBytes) {
828
- batches.push(current);
829
- current = [];
830
- currentSize = 0;
831
- }
832
- current.push(span);
833
- currentSize += size;
834
- }
835
- if (current.length > 0) {
836
- batches.push(current);
837
- }
838
- return batches;
839
- }
840
- async resolveDelegate() {
841
- const apiKey = this.apiKey() ?? "";
842
- if (this.delegate && this.delegateApiKey === apiKey) {
843
- return this.delegate;
844
- }
845
- const { OTLPTraceExporter } = await this.loadExporterModule();
846
- const previous = this.delegate;
847
- this.delegate = new OTLPTraceExporter({
848
- url: this.endpoint,
849
- headers: { Authorization: `Bearer ${apiKey}` },
850
- timeoutMillis: EXPORT_TIMEOUT_MILLIS
851
- });
852
- this.delegateApiKey = apiKey;
853
- if (previous) {
854
- void previous.shutdown().catch(() => {
855
- });
856
- }
857
- return this.delegate;
858
- }
859
- async shutdown() {
860
- await this.delegate?.shutdown();
861
- }
862
- async forceFlush() {
863
- await this.delegate?.forceFlush?.();
864
- }
865
- };
866
971
  DeliveryTrackingExporter = class {
867
972
  constructor(exporter) {
868
973
  this.exporter = exporter;
@@ -905,27 +1010,22 @@ var init_otel = __esm({
905
1010
  OtelBatchTransport = class {
906
1011
  constructor(options) {
907
1012
  this.closed = false;
908
- const collectorEndpoint = options.collectorEndpoint;
909
1013
  const maxRequestBytes = options.maxRequestBytes ?? MAX_EXPORT_REQUEST_BYTES;
910
1014
  const maxRequestBatchSize = options.maxRequestBatchSize ?? DIRECT_MAX_REQUEST_BATCH_SIZE;
911
1015
  if (maxRequestBatchSize <= 0) {
912
1016
  throw new BitfabError("maxRequestBatchSize must be a positive integer");
913
1017
  }
914
1018
  this.deliveryTracker = new DeliveryTrackingExporter(
915
- collectorEndpoint === void 0 ? new BitfabSpanExporter(
1019
+ new BitfabSpanExporter(
916
1020
  options.directSender,
917
1021
  maxRequestBytes,
918
1022
  maxRequestBatchSize,
919
1023
  options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
920
- ) : new CollectorSpanExporter(
921
- normalizeCollectorEndpoint(collectorEndpoint),
922
- options.apiKey,
923
- maxRequestBytes
924
1024
  )
925
1025
  );
926
1026
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
927
1027
  maxQueueSize: options.maxQueueSize ?? MAX_QUEUE_SIZE,
928
- maxExportBatchSize: options.maxExportBatchSize ?? (collectorEndpoint === void 0 ? DIRECT_MAX_EXPORT_BATCH_SIZE : COLLECTOR_MAX_EXPORT_BATCH_SIZE),
1028
+ maxExportBatchSize: options.maxExportBatchSize ?? DIRECT_MAX_EXPORT_BATCH_SIZE,
929
1029
  scheduledDelayMillis: SCHEDULE_DELAY_MILLIS,
930
1030
  exportTimeoutMillis: options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
931
1031
  });
@@ -1078,6 +1178,7 @@ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCL
1078
1178
  var init_http = __esm({
1079
1179
  "src/http.ts"() {
1080
1180
  "use strict";
1181
+ init_compress();
1081
1182
  init_constants();
1082
1183
  init_errors();
1083
1184
  init_replayContext();
@@ -1142,8 +1243,7 @@ var init_http = __esm({
1142
1243
  }
1143
1244
  if (!this.traceTransport) {
1144
1245
  this.traceTransport = createTraceTransport({
1145
- apiKey: () => this.resolveApiKey(),
1146
- directSender: (endpoint, payload, timeoutMs) => this.request(endpoint, payload, {
1246
+ directSender: (endpoint, body, timeoutMs) => this.sendEncoded(endpoint, body, {
1147
1247
  timeout: timeoutMs
1148
1248
  })
1149
1249
  });
@@ -1213,11 +1313,6 @@ var init_http = __esm({
1213
1313
  * @throws {BitfabError} If the request fails
1214
1314
  */
1215
1315
  async request(endpoint, payload, options) {
1216
- const url = `${this.serviceUrl}${endpoint}`;
1217
- const timeout = options?.timeout ?? this.timeout;
1218
- const method = options?.method ?? "POST";
1219
- const controller = new AbortController();
1220
- const timeoutId = setTimeout(() => controller.abort(), timeout);
1221
1316
  const { body, dropped } = serializePayloadBody(payload);
1222
1317
  if (dropped.length > 0) {
1223
1318
  try {
@@ -1227,14 +1322,33 @@ var init_http = __esm({
1227
1322
  } catch {
1228
1323
  }
1229
1324
  }
1325
+ return this.sendEncoded(endpoint, body, options);
1326
+ }
1327
+ /**
1328
+ * POST an already-encoded body. The span transport encodes its own batches,
1329
+ * so routing them back through {@link HttpClient.request} would encode the
1330
+ * same data twice.
1331
+ */
1332
+ async sendEncoded(endpoint, body, options) {
1333
+ const url = `${this.serviceUrl}${endpoint}`;
1334
+ const timeout = options?.timeout ?? this.timeout;
1335
+ const method = options?.method ?? "POST";
1336
+ const controller = new AbortController();
1337
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1338
+ const prepared = encodeRequestBody(body);
1339
+ const encoded = prepared instanceof Promise ? await prepared : prepared;
1340
+ const headers = {
1341
+ "Content-Type": "application/json",
1342
+ Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
1343
+ };
1344
+ if (encoded.contentEncoding) {
1345
+ headers["Content-Encoding"] = encoded.contentEncoding;
1346
+ }
1230
1347
  try {
1231
1348
  const response = await fetch(url, {
1232
1349
  method,
1233
- headers: {
1234
- "Content-Type": "application/json",
1235
- Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
1236
- },
1237
- body,
1350
+ headers,
1351
+ body: encoded.body,
1238
1352
  signal: controller.signal
1239
1353
  });
1240
1354
  if (!response.ok) {
@@ -1683,9 +1797,10 @@ var init_serialize = __esm({
1683
1797
  "src/serialize.ts"() {
1684
1798
  "use strict";
1685
1799
  import_superjson = __toESM(require("superjson"), 1);
1800
+ init_payloadBudget();
1686
1801
  init_warnOnce();
1687
- MAX_SERIALIZED_BYTES = 512e3;
1688
- MAX_FRAMEWORK_SERIALIZED_BYTES = 2e6;
1802
+ MAX_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1803
+ MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1689
1804
  MAX_SAFE_DEPTH = 6;
1690
1805
  }
1691
1806
  });