@bitfab/sdk 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
@@ -42,7 +42,7 @@ var __version__;
42
42
  var init_version_generated = __esm({
43
43
  "src/version.generated.ts"() {
44
44
  "use strict";
45
- __version__ = "0.34.1";
45
+ __version__ = "0.36.0";
46
46
  }
47
47
  });
48
48
 
@@ -56,6 +56,87 @@ var init_constants = __esm({
56
56
  }
57
57
  });
58
58
 
59
+ // src/readEnv.ts
60
+ function readEnv(name) {
61
+ if (typeof process !== "undefined" && process.env) {
62
+ return process.env[name];
63
+ }
64
+ return void 0;
65
+ }
66
+ var init_readEnv = __esm({
67
+ "src/readEnv.ts"() {
68
+ "use strict";
69
+ }
70
+ });
71
+
72
+ // src/compress.ts
73
+ function toArrayBuffer(view) {
74
+ return view.buffer.slice(
75
+ view.byteOffset,
76
+ view.byteOffset + view.byteLength
77
+ );
78
+ }
79
+ async function gzipViaStream(bytes) {
80
+ const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
81
+ return await new Response(stream).arrayBuffer();
82
+ }
83
+ function encodeRequestBody(body) {
84
+ if (readEnv(DISABLE_COMPRESSION_ENV)) {
85
+ return { body };
86
+ }
87
+ const bytes = new TextEncoder().encode(body);
88
+ if (bytes.byteLength < MIN_COMPRESSED_BYTES) {
89
+ return { body };
90
+ }
91
+ if (gzipNode) {
92
+ return gzipNode(bytes).then(
93
+ (compressed) => ({
94
+ body: toArrayBuffer(compressed),
95
+ contentEncoding: "gzip"
96
+ }),
97
+ () => ({ body })
98
+ );
99
+ }
100
+ if (typeof CompressionStream === "undefined") {
101
+ return { body };
102
+ }
103
+ return gzipViaStream(bytes).then(
104
+ (compressed) => ({ body: compressed, contentEncoding: "gzip" }),
105
+ () => ({ body })
106
+ );
107
+ }
108
+ var DISABLE_COMPRESSION_ENV, MIN_COMPRESSED_BYTES, gzipNode, _nodeGzipReady;
109
+ var init_compress = __esm({
110
+ "src/compress.ts"() {
111
+ "use strict";
112
+ init_readEnv();
113
+ DISABLE_COMPRESSION_ENV = "BITFAB_DISABLE_COMPRESSION";
114
+ MIN_COMPRESSED_BYTES = 8192;
115
+ _nodeGzipReady = (typeof process !== "undefined" && process.versions?.node ? (
116
+ // The join trick hides "node:zlib" from static analysis so bundlers that
117
+ // ban Node.js built-ins don't fail at build time. webpackIgnore tells
118
+ // webpack/turbopack to emit a native import() so Node.js can resolve the
119
+ // module at runtime. Same pattern as `asyncStorage.ts`.
120
+ import(
121
+ /* webpackIgnore: true */
122
+ ["node", "zlib"].join(":")
123
+ ).then(({ gzip }) => {
124
+ gzipNode = (data) => new Promise((resolve, reject) => {
125
+ gzip(data, (error, result) => {
126
+ if (error) {
127
+ reject(error);
128
+ } else {
129
+ resolve(result);
130
+ }
131
+ });
132
+ });
133
+ }).catch(() => {
134
+ })
135
+ ) : Promise.resolve()).then(() => {
136
+ });
137
+ }
138
+ });
139
+
59
140
  // src/errors.ts
60
141
  var BitfabError;
61
142
  var init_errors = __esm({
@@ -144,6 +225,131 @@ var init_replayContext = __esm({
144
225
  }
145
226
  });
146
227
 
228
+ // src/payloadBudget.ts
229
+ function byteLength(value) {
230
+ return textEncoder ? textEncoder.encode(value).length : value.length;
231
+ }
232
+ function carrierByteLength(body) {
233
+ return carrierBytesOf(textEncoder ? textEncoder.encode(body) : null, body);
234
+ }
235
+ function carrierBytesOf(encoded, body) {
236
+ if (!encoded) {
237
+ return body.length + 2;
238
+ }
239
+ let extra = 2;
240
+ for (let i = 0; i < encoded.length; i++) {
241
+ const byte = encoded[i];
242
+ if (byte === 34 || byte === 92) {
243
+ extra += 1;
244
+ } else if (byte < 32) {
245
+ extra += byte === 8 || byte === 9 || byte === 10 || byte === 12 || byte === 13 ? 1 : 5;
246
+ }
247
+ }
248
+ return encoded.length + extra;
249
+ }
250
+ function fitsCarrierBudget(body) {
251
+ const units = body.length;
252
+ if (units * MAX_BYTES_PER_UNIT + 2 <= MAX_SPAN_CARRIER_BYTES) {
253
+ return true;
254
+ }
255
+ if (units + 2 > MAX_SPAN_CARRIER_BYTES) {
256
+ return false;
257
+ }
258
+ return carrierByteLength(body) <= MAX_SPAN_CARRIER_BYTES;
259
+ }
260
+ function asRecord(value) {
261
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
262
+ }
263
+ function cloneTrimmable(payload) {
264
+ const copy = { ...payload };
265
+ const containers = [];
266
+ const spanData = asRecord(copy.span_data);
267
+ if (spanData) {
268
+ const clone = { ...spanData };
269
+ copy.span_data = clone;
270
+ containers.push(clone);
271
+ }
272
+ const rawSpan = asRecord(copy.rawSpan);
273
+ const rawSpanData = rawSpan && asRecord(rawSpan.span_data);
274
+ if (rawSpan && rawSpanData) {
275
+ const clone = { ...rawSpanData };
276
+ copy.rawSpan = { ...rawSpan, span_data: clone };
277
+ containers.push(clone);
278
+ }
279
+ if (containers.length === 0) {
280
+ containers.push(copy);
281
+ }
282
+ return { copy, containers };
283
+ }
284
+ function collectCandidates(containers) {
285
+ const candidates = [];
286
+ for (const container of containers) {
287
+ for (const [key, value] of Object.entries(container)) {
288
+ if (STRUCTURAL_SPAN_KEYS.has(key) || value == null) {
289
+ continue;
290
+ }
291
+ let size;
292
+ try {
293
+ size = byteLength(JSON.stringify(value) ?? "");
294
+ } catch {
295
+ continue;
296
+ }
297
+ candidates.push({ container, key, size });
298
+ }
299
+ }
300
+ return candidates.sort((a, b) => b.size - a.size);
301
+ }
302
+ function trimPayloadToBudget(payload, encode) {
303
+ const { copy, containers } = cloneTrimmable(payload);
304
+ const candidates = collectCandidates(containers);
305
+ if (candidates.length === 0) {
306
+ return void 0;
307
+ }
308
+ const trimmed = [];
309
+ for (const candidate of candidates) {
310
+ candidate.container[candidate.key] = `<unserializable: too_large_${candidate.size}_bytes>`;
311
+ trimmed.push(candidate.key);
312
+ let body;
313
+ try {
314
+ body = encode(copy);
315
+ } catch {
316
+ return void 0;
317
+ }
318
+ if (fitsCarrierBudget(body)) {
319
+ return { value: copy, trimmed };
320
+ }
321
+ }
322
+ return void 0;
323
+ }
324
+ function markPayloadTrimmed(value, trimmed) {
325
+ const existing = Array.isArray(value.errors) ? value.errors : [];
326
+ value.errors = [
327
+ ...existing,
328
+ {
329
+ source: "sdk",
330
+ step: "payload_budget",
331
+ error: `trimmed oversized field(s) to fit the ${MAX_SPAN_CARRIER_BYTES}-byte span carrier budget: ${[
332
+ ...new Set(trimmed)
333
+ ].join(", ")}`
334
+ }
335
+ ];
336
+ }
337
+ var MAX_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
338
+ var init_payloadBudget = __esm({
339
+ "src/payloadBudget.ts"() {
340
+ "use strict";
341
+ MAX_SPAN_CARRIER_BYTES = 28e5;
342
+ textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
343
+ MAX_BYTES_PER_UNIT = 3;
344
+ STRUCTURAL_SPAN_KEYS = /* @__PURE__ */ new Set([
345
+ "name",
346
+ "type",
347
+ "function_name",
348
+ "error_source"
349
+ ]);
350
+ }
351
+ });
352
+
147
353
  // src/warnOnce.ts
148
354
  function warnOnce(key, message) {
149
355
  if (warned.has(key)) {
@@ -165,8 +371,37 @@ var init_warnOnce = __esm({
165
371
 
166
372
  // src/serializePayload.ts
167
373
  function serializePayloadBody(payload) {
374
+ const encoded = encodePayloadBody(payload);
375
+ if (fitsCarrierBudget(encoded.body)) {
376
+ return { body: encoded.body, dropped: encoded.dropped };
377
+ }
378
+ return applyPayloadBudget(encoded);
379
+ }
380
+ function applyPayloadBudget(encoded) {
381
+ const result = encoded.value ? trimPayloadToBudget(
382
+ encoded.value,
383
+ (value) => encodePayloadBody(value).body
384
+ ) : void 0;
385
+ if (!result) {
386
+ return { body: encoded.body, dropped: encoded.dropped };
387
+ }
388
+ warnOnce(
389
+ "payload:over-budget",
390
+ `a span payload exceeded the ${MAX_SPAN_CARRIER_BYTES}-byte carrier budget; its largest field(s) (${[
391
+ ...new Set(result.trimmed)
392
+ ].join(
393
+ ", "
394
+ )}) were replaced with placeholders so the span still ships. The span is incomplete and may not be replayable.`
395
+ );
396
+ markPayloadTrimmed(result.value, result.trimmed);
397
+ return {
398
+ body: encodePayloadBody(result.value).body,
399
+ dropped: encoded.dropped
400
+ };
401
+ }
402
+ function encodePayloadBody(payload) {
168
403
  try {
169
- return { body: JSON.stringify(payload), dropped: [] };
404
+ return { body: JSON.stringify(payload), dropped: [], value: payload };
170
405
  } catch {
171
406
  const dropped = [];
172
407
  const sanitize = (value, seen) => {
@@ -231,12 +466,11 @@ function serializePayloadBody(payload) {
231
466
  sanitized = sanitize(payload, /* @__PURE__ */ new WeakSet());
232
467
  } catch (error) {
233
468
  const message = error instanceof Error ? error.message : String(error);
234
- return {
235
- body: JSON.stringify({ error: `payload_serialize_failed: ${message}` }),
236
- dropped
237
- };
469
+ const marker = { error: `payload_serialize_failed: ${message}` };
470
+ return { body: JSON.stringify(marker), dropped, value: marker };
238
471
  }
239
- if (dropped.length > 0 && typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
472
+ const isRecord = typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized);
473
+ if (dropped.length > 0 && isRecord) {
240
474
  const obj = sanitized;
241
475
  const existing = Array.isArray(obj.errors) ? obj.errors : [];
242
476
  obj.errors = [
@@ -250,29 +484,21 @@ function serializePayloadBody(payload) {
250
484
  }
251
485
  ];
252
486
  }
253
- return { body: JSON.stringify(sanitized), dropped };
487
+ return {
488
+ body: JSON.stringify(sanitized),
489
+ dropped,
490
+ value: isRecord ? sanitized : void 0
491
+ };
254
492
  }
255
493
  }
256
494
  var init_serializePayload = __esm({
257
495
  "src/serializePayload.ts"() {
258
496
  "use strict";
497
+ init_payloadBudget();
259
498
  init_warnOnce();
260
499
  }
261
500
  });
262
501
 
263
- // src/readEnv.ts
264
- function readEnv(name) {
265
- if (typeof process !== "undefined" && process.env) {
266
- return process.env[name];
267
- }
268
- return void 0;
269
- }
270
- var init_readEnv = __esm({
271
- "src/readEnv.ts"() {
272
- "use strict";
273
- }
274
- });
275
-
276
502
  // src/unrefTimer.ts
277
503
  function unrefTimer(timer) {
278
504
  const handle = timer;
@@ -318,7 +544,7 @@ function recordTraceSubmission(operation, payload) {
318
544
  return;
319
545
  }
320
546
  if (operation === "external_span") {
321
- const rawSpan = asRecord(payload.rawSpan);
547
+ const rawSpan = asRecord2(payload.rawSpan);
322
548
  if (typeof rawSpan?.id !== "string") {
323
549
  submissionCounter += 1;
324
550
  }
@@ -355,14 +581,14 @@ function takeReplaySpanCounts(traceIds) {
355
581
  }
356
582
  return counts;
357
583
  }
358
- function asRecord(value) {
584
+ function asRecord2(value) {
359
585
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
360
586
  }
361
587
  function resolveSourceTraceId(payload) {
362
588
  if (typeof payload.sourceTraceId === "string") {
363
589
  return payload.sourceTraceId;
364
590
  }
365
- const rawTrace = asRecord(payload.externalTrace) ?? asRecord(payload.rawTrace);
591
+ const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
366
592
  return typeof rawTrace?.id === "string" ? rawTrace.id : void 0;
367
593
  }
368
594
  function otlpValue(value) {
@@ -420,32 +646,27 @@ function spanToOtlp(span) {
420
646
  }
421
647
  return result;
422
648
  }
423
- function buildOtlpRequest(first, spans) {
649
+ function encodeSpan(span) {
650
+ const json = JSON.stringify(spanToOtlp(span));
651
+ return { json, size: byteLength(json) };
652
+ }
653
+ function requestEnvelope(first) {
424
654
  const scope = first.instrumentationScope;
425
- return {
426
- resourceSpans: [
427
- {
428
- resource: {
429
- attributes: otlpAttributes(
430
- first.resource.attributes
431
- )
432
- },
433
- scopeSpans: [
434
- {
435
- scope: { name: scope.name, version: scope.version ?? "" },
436
- spans
437
- }
438
- ]
439
- }
440
- ]
441
- };
655
+ const resource = JSON.stringify({
656
+ attributes: otlpAttributes(
657
+ first.resource.attributes
658
+ )
659
+ });
660
+ const scopeJson = JSON.stringify({
661
+ name: scope.name,
662
+ version: scope.version ?? ""
663
+ });
664
+ const head = `{"resourceSpans":[{"resource":${resource},"scopeSpans":[{"scope":${scopeJson},"spans":[`;
665
+ const tail = "]}]}]}";
666
+ return { head, tail, size: byteLength(head) + byteLength(tail) };
442
667
  }
443
- function encodedSize(value) {
444
- const json = JSON.stringify(value);
445
- if (typeof TextEncoder !== "undefined") {
446
- return new TextEncoder().encode(json).length;
447
- }
448
- return json.length;
668
+ function encodeRequest(envelope, spans) {
669
+ return envelope.head + spans.map((span) => span.json).join(",") + envelope.tail;
449
670
  }
450
671
  function delay(ms) {
451
672
  return new Promise((resolve) => {
@@ -495,16 +716,12 @@ function isRetryable(error) {
495
716
  }
496
717
  return RETRYABLE_STATUSES.has(status) || status >= 500;
497
718
  }
498
- function normalizeCollectorEndpoint(endpoint) {
499
- const trimmed = endpoint.replace(/\/+$/, "");
500
- return trimmed.endsWith("/v1/traces") ? trimmed : `${trimmed}/v1/traces`;
501
- }
502
719
  function endSpan(span, endTime) {
503
720
  span.end(endTime);
504
721
  }
505
722
  function spanName(operation, payload) {
506
723
  if (operation === "external_span") {
507
- const spanData = asRecord(asRecord(payload.rawSpan)?.span_data);
724
+ const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
508
725
  if (typeof spanData?.name === "string") {
509
726
  return spanData.name;
510
727
  }
@@ -515,8 +732,8 @@ function spanName(operation, payload) {
515
732
  return `bitfab.${operation}`;
516
733
  }
517
734
  function payloadTimestamp(payload, field) {
518
- const rawSpan = asRecord(payload.rawSpan);
519
- const rawTrace = asRecord(payload.externalTrace) ?? asRecord(payload.rawTrace);
735
+ const rawSpan = asRecord2(payload.rawSpan);
736
+ const rawTrace = asRecord2(payload.externalTrace) ?? asRecord2(payload.rawTrace);
520
737
  const raw = rawSpan?.[field] ?? rawTrace?.[field];
521
738
  if (typeof raw !== "string") {
522
739
  return void 0;
@@ -525,7 +742,7 @@ function payloadTimestamp(payload, field) {
525
742
  return Number.isNaN(parsed) ? void 0 : parsed;
526
743
  }
527
744
  function hasError(payload) {
528
- const spanData = asRecord(asRecord(payload.rawSpan)?.span_data);
745
+ const spanData = asRecord2(asRecord2(payload.rawSpan)?.span_data);
529
746
  if (spanData?.error != null) {
530
747
  return true;
531
748
  }
@@ -535,7 +752,6 @@ function hasError(payload) {
535
752
  function createOtelTransport(options) {
536
753
  return new OtelBatchTransport({
537
754
  ...options,
538
- collectorEndpoint: readEnv(COLLECTOR_ENDPOINT_ENV) || void 0,
539
755
  exportConcurrency: readBoundedIntEnv(
540
756
  EXPORT_CONCURRENCY_ENV,
541
757
  MAX_EXPORT_CONCURRENCY,
@@ -570,7 +786,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
570
786
  (transport, remaining) => transport.shutdown(remaining)
571
787
  );
572
788
  }
573
- 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;
789
+ 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;
574
790
  var init_otel = __esm({
575
791
  "src/otel.ts"() {
576
792
  "use strict";
@@ -580,6 +796,7 @@ var init_otel = __esm({
580
796
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
581
797
  init_constants();
582
798
  init_errors();
799
+ init_payloadBudget();
583
800
  init_readEnv();
584
801
  init_serializePayload();
585
802
  init_unrefTimer();
@@ -590,10 +807,8 @@ var init_otel = __esm({
590
807
  MAX_EXPORT_REQUEST_BYTES = 3e6;
591
808
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
592
809
  EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
593
- COLLECTOR_ENDPOINT_ENV = "BITFAB_OTEL_EXPORTER_ENDPOINT";
594
810
  MAX_QUEUE_SIZE = 8192;
595
811
  DIRECT_MAX_EXPORT_BATCH_SIZE = 512;
596
- COLLECTOR_MAX_EXPORT_BATCH_SIZE = 32;
597
812
  DIRECT_MAX_REQUEST_BATCH_SIZE = 8;
598
813
  DEFAULT_EXPORT_CONCURRENCY = 32;
599
814
  MAX_EXPORT_CONCURRENCY = 64;
@@ -607,6 +822,7 @@ var init_otel = __esm({
607
822
  traceSubmissionSpanIds = /* @__PURE__ */ new Map();
608
823
  replayTraceSubmissions = /* @__PURE__ */ new Set();
609
824
  submissionCounter = 0;
825
+ SPAN_SEPARATOR_BYTES = 1;
610
826
  OtlpPayloadTooLargeError = class extends Error {
611
827
  };
612
828
  OtlpPartialSuccessError = class extends Error {
@@ -635,57 +851,55 @@ var init_otel = __esm({
635
851
  return true;
636
852
  }
637
853
  let encoded;
854
+ let envelope;
638
855
  try {
639
- encoded = spans.map(spanToOtlp);
856
+ encoded = spans.map(encodeSpan);
857
+ envelope = requestEnvelope(spans[0]);
640
858
  } catch (error) {
641
859
  logError("failed to encode an OpenTelemetry span batch", error);
642
860
  return false;
643
861
  }
644
- const first = spans[0];
645
- const batches = this.buildRequestBatches(first, encoded);
862
+ const batches = this.buildRequestBatches(envelope, encoded);
646
863
  const results = await mapWithConcurrency(
647
864
  batches,
648
865
  this.exportConcurrency,
649
- (batch) => this.send(first, batch)
866
+ (batch) => this.send(envelope, batch)
650
867
  );
651
868
  return results.every(Boolean);
652
869
  }
653
- buildRequestBatches(first, spans) {
870
+ buildRequestBatches(envelope, spans) {
654
871
  const batches = [];
655
872
  let current = [];
873
+ let size = envelope.size;
656
874
  for (const span of spans) {
657
- if (current.length >= this.maxRequestBatchSize) {
658
- batches.push(current);
875
+ const addition = span.size + (current.length > 0 ? SPAN_SEPARATOR_BYTES : 0);
876
+ if (current.length > 0 && (current.length >= this.maxRequestBatchSize || size + addition > this.maxRequestBytes)) {
877
+ batches.push({ spans: current, size });
659
878
  current = [];
879
+ size = envelope.size;
660
880
  }
661
- const candidate = [...current, span];
662
- if (current.length > 0 && encodedSize(buildOtlpRequest(first, candidate)) > this.maxRequestBytes) {
663
- batches.push(current);
664
- current = [span];
665
- } else {
666
- current = candidate;
667
- }
881
+ current.push(span);
882
+ size += span.size + (current.length > 1 ? SPAN_SEPARATOR_BYTES : 0);
668
883
  }
669
884
  if (current.length > 0) {
670
- batches.push(current);
885
+ batches.push({ spans: current, size });
671
886
  }
672
887
  return batches;
673
888
  }
674
- async send(first, spans) {
675
- const payload = buildOtlpRequest(first, spans);
676
- if (encodedSize(payload) > this.maxRequestBytes) {
889
+ async send(envelope, batch) {
890
+ if (batch.size > this.maxRequestBytes) {
677
891
  logError(
678
892
  "a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
679
893
  );
680
894
  return false;
681
895
  }
682
896
  try {
683
- await this.sendWithRetries(payload);
897
+ await this.sendWithRetries(encodeRequest(envelope, batch.spans));
684
898
  return true;
685
899
  } catch (error) {
686
900
  if (error instanceof OtlpPayloadTooLargeError) {
687
901
  logError(
688
- 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"
902
+ 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"
689
903
  );
690
904
  return false;
691
905
  }
@@ -709,15 +923,15 @@ var init_otel = __esm({
709
923
  * the server does not yet understand. The fix is a client-supplied
710
924
  * idempotency key that ingestion dedupes on.
711
925
  */
712
- async sendWithRetries(payload) {
926
+ async sendWithRetries(body) {
713
927
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
714
928
  try {
715
929
  const response = await this.directSender(
716
930
  OTLP_TRACES_ENDPOINT,
717
- payload,
931
+ body,
718
932
  EXPORT_TIMEOUT_MILLIS
719
933
  );
720
- const partialSuccess = asRecord(response?.partialSuccess);
934
+ const partialSuccess = asRecord2(response?.partialSuccess);
721
935
  const rejected = partialSuccess?.rejectedSpans;
722
936
  if (rejected !== void 0 && rejected !== "0" && rejected !== 0) {
723
937
  logError(
@@ -745,115 +959,6 @@ var init_otel = __esm({
745
959
  async forceFlush() {
746
960
  }
747
961
  };
748
- CollectorSpanExporter = class {
749
- constructor(endpoint, apiKey, maxRequestBytes) {
750
- this.endpoint = endpoint;
751
- this.apiKey = apiKey;
752
- this.maxRequestBytes = maxRequestBytes;
753
- }
754
- /**
755
- * Loaded through a dynamic import rather than a top-level one so bundlers
756
- * code-split it: Collector delivery is opt-in, and a consumer who never sets
757
- * an endpoint should not pay for the exporter in their initial bundle. It is
758
- * a hard dependency, so this cannot fail for want of the package.
759
- */
760
- loadExporterModule() {
761
- if (!this.pendingModule) {
762
- this.pendingModule = import("@opentelemetry/exporter-trace-otlp-proto");
763
- }
764
- return this.pendingModule;
765
- }
766
- export(spans, resultCallback) {
767
- void this.exportAsync(spans).then(
768
- (succeeded) => {
769
- resultCallback({
770
- code: succeeded ? import_core.ExportResultCode.SUCCESS : import_core.ExportResultCode.FAILED
771
- });
772
- },
773
- (error) => {
774
- resultCallback({ code: import_core.ExportResultCode.FAILED, error });
775
- }
776
- );
777
- }
778
- async exportAsync(spans) {
779
- if (spans.length === 0) {
780
- return true;
781
- }
782
- let delegate;
783
- try {
784
- delegate = await this.resolveDelegate();
785
- } catch (error) {
786
- logError("failed to build the OTLP Collector exporter", error);
787
- return false;
788
- }
789
- const results = await Promise.all(
790
- this.partition(spans).map(
791
- (batch) => new Promise((resolve) => {
792
- try {
793
- delegate.export(batch, (result) => {
794
- resolve(result.code === import_core.ExportResultCode.SUCCESS);
795
- });
796
- } catch (error) {
797
- logError("Collector export threw", error);
798
- resolve(false);
799
- }
800
- })
801
- )
802
- );
803
- return results.every(Boolean);
804
- }
805
- /**
806
- * Partition by the encoded JSON size of each carrier rather than its encoded
807
- * protobuf size. Protobuf is strictly smaller than the equivalent JSON for
808
- * these payloads, so the JSON figure is a conservative bound that keeps every
809
- * request under the target without pulling `@opentelemetry/otlp-transformer`
810
- * into the dependency set purely to measure bytes.
811
- */
812
- partition(spans) {
813
- const batches = [];
814
- let current = [];
815
- let currentSize = 0;
816
- for (const span of spans) {
817
- const size = encodedSize(spanToOtlp(span));
818
- if (current.length > 0 && currentSize + size > this.maxRequestBytes) {
819
- batches.push(current);
820
- current = [];
821
- currentSize = 0;
822
- }
823
- current.push(span);
824
- currentSize += size;
825
- }
826
- if (current.length > 0) {
827
- batches.push(current);
828
- }
829
- return batches;
830
- }
831
- async resolveDelegate() {
832
- const apiKey = this.apiKey() ?? "";
833
- if (this.delegate && this.delegateApiKey === apiKey) {
834
- return this.delegate;
835
- }
836
- const { OTLPTraceExporter } = await this.loadExporterModule();
837
- const previous = this.delegate;
838
- this.delegate = new OTLPTraceExporter({
839
- url: this.endpoint,
840
- headers: { Authorization: `Bearer ${apiKey}` },
841
- timeoutMillis: EXPORT_TIMEOUT_MILLIS
842
- });
843
- this.delegateApiKey = apiKey;
844
- if (previous) {
845
- void previous.shutdown().catch(() => {
846
- });
847
- }
848
- return this.delegate;
849
- }
850
- async shutdown() {
851
- await this.delegate?.shutdown();
852
- }
853
- async forceFlush() {
854
- await this.delegate?.forceFlush?.();
855
- }
856
- };
857
962
  DeliveryTrackingExporter = class {
858
963
  constructor(exporter) {
859
964
  this.exporter = exporter;
@@ -896,27 +1001,22 @@ var init_otel = __esm({
896
1001
  OtelBatchTransport = class {
897
1002
  constructor(options) {
898
1003
  this.closed = false;
899
- const collectorEndpoint = options.collectorEndpoint;
900
1004
  const maxRequestBytes = options.maxRequestBytes ?? MAX_EXPORT_REQUEST_BYTES;
901
1005
  const maxRequestBatchSize = options.maxRequestBatchSize ?? DIRECT_MAX_REQUEST_BATCH_SIZE;
902
1006
  if (maxRequestBatchSize <= 0) {
903
1007
  throw new BitfabError("maxRequestBatchSize must be a positive integer");
904
1008
  }
905
1009
  this.deliveryTracker = new DeliveryTrackingExporter(
906
- collectorEndpoint === void 0 ? new BitfabSpanExporter(
1010
+ new BitfabSpanExporter(
907
1011
  options.directSender,
908
1012
  maxRequestBytes,
909
1013
  maxRequestBatchSize,
910
1014
  options.exportConcurrency ?? DEFAULT_EXPORT_CONCURRENCY
911
- ) : new CollectorSpanExporter(
912
- normalizeCollectorEndpoint(collectorEndpoint),
913
- options.apiKey,
914
- maxRequestBytes
915
1015
  )
916
1016
  );
917
1017
  this.processor = new import_sdk_trace_base.BatchSpanProcessor(this.deliveryTracker, {
918
1018
  maxQueueSize: options.maxQueueSize ?? MAX_QUEUE_SIZE,
919
- maxExportBatchSize: options.maxExportBatchSize ?? (collectorEndpoint === void 0 ? DIRECT_MAX_EXPORT_BATCH_SIZE : COLLECTOR_MAX_EXPORT_BATCH_SIZE),
1019
+ maxExportBatchSize: options.maxExportBatchSize ?? DIRECT_MAX_EXPORT_BATCH_SIZE,
920
1020
  scheduledDelayMillis: SCHEDULE_DELAY_MILLIS,
921
1021
  exportTimeoutMillis: options.exportTimeoutMillis ?? EXPORT_TIMEOUT_MILLIS
922
1022
  });
@@ -1069,6 +1169,7 @@ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCL
1069
1169
  var init_http = __esm({
1070
1170
  "src/http.ts"() {
1071
1171
  "use strict";
1172
+ init_compress();
1072
1173
  init_constants();
1073
1174
  init_errors();
1074
1175
  init_replayContext();
@@ -1133,8 +1234,7 @@ var init_http = __esm({
1133
1234
  }
1134
1235
  if (!this.traceTransport) {
1135
1236
  this.traceTransport = createTraceTransport({
1136
- apiKey: () => this.resolveApiKey(),
1137
- directSender: (endpoint, payload, timeoutMs) => this.request(endpoint, payload, {
1237
+ directSender: (endpoint, body, timeoutMs) => this.sendEncoded(endpoint, body, {
1138
1238
  timeout: timeoutMs
1139
1239
  })
1140
1240
  });
@@ -1204,11 +1304,6 @@ var init_http = __esm({
1204
1304
  * @throws {BitfabError} If the request fails
1205
1305
  */
1206
1306
  async request(endpoint, payload, options) {
1207
- const url = `${this.serviceUrl}${endpoint}`;
1208
- const timeout = options?.timeout ?? this.timeout;
1209
- const method = options?.method ?? "POST";
1210
- const controller = new AbortController();
1211
- const timeoutId = setTimeout(() => controller.abort(), timeout);
1212
1307
  const { body, dropped } = serializePayloadBody(payload);
1213
1308
  if (dropped.length > 0) {
1214
1309
  try {
@@ -1218,14 +1313,33 @@ var init_http = __esm({
1218
1313
  } catch {
1219
1314
  }
1220
1315
  }
1316
+ return this.sendEncoded(endpoint, body, options);
1317
+ }
1318
+ /**
1319
+ * POST an already-encoded body. The span transport encodes its own batches,
1320
+ * so routing them back through {@link HttpClient.request} would encode the
1321
+ * same data twice.
1322
+ */
1323
+ async sendEncoded(endpoint, body, options) {
1324
+ const url = `${this.serviceUrl}${endpoint}`;
1325
+ const timeout = options?.timeout ?? this.timeout;
1326
+ const method = options?.method ?? "POST";
1327
+ const controller = new AbortController();
1328
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1329
+ const prepared = encodeRequestBody(body);
1330
+ const encoded = prepared instanceof Promise ? await prepared : prepared;
1331
+ const headers = {
1332
+ "Content-Type": "application/json",
1333
+ Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
1334
+ };
1335
+ if (encoded.contentEncoding) {
1336
+ headers["Content-Encoding"] = encoded.contentEncoding;
1337
+ }
1221
1338
  try {
1222
1339
  const response = await fetch(url, {
1223
1340
  method,
1224
- headers: {
1225
- "Content-Type": "application/json",
1226
- Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
1227
- },
1228
- body,
1341
+ headers,
1342
+ body: encoded.body,
1229
1343
  signal: controller.signal
1230
1344
  });
1231
1345
  if (!response.ok) {
@@ -1674,9 +1788,10 @@ var init_serialize = __esm({
1674
1788
  "src/serialize.ts"() {
1675
1789
  "use strict";
1676
1790
  import_superjson = __toESM(require("superjson"), 1);
1791
+ init_payloadBudget();
1677
1792
  init_warnOnce();
1678
- MAX_SERIALIZED_BYTES = 512e3;
1679
- MAX_FRAMEWORK_SERIALIZED_BYTES = 2e6;
1793
+ MAX_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1794
+ MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1680
1795
  MAX_SAFE_DEPTH = 6;
1681
1796
  }
1682
1797
  });