bitfab 0.36.5 → 0.36.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.cjs CHANGED
@@ -97,7 +97,7 @@ var __version__;
97
97
  var init_version_generated = __esm({
98
98
  "src/version.generated.ts"() {
99
99
  "use strict";
100
- __version__ = "0.36.5";
100
+ __version__ = "0.36.7";
101
101
  }
102
102
  });
103
103
 
@@ -131,33 +131,58 @@ function toArrayBuffer(view) {
131
131
  view.byteOffset + view.byteLength
132
132
  );
133
133
  }
134
+ function compressedRequest(body, rawBytes, compressed) {
135
+ if (compressed.byteLength >= rawBytes) {
136
+ return { body, rawBytes, wireBytes: rawBytes };
137
+ }
138
+ return {
139
+ body: compressed instanceof Uint8Array ? toArrayBuffer(compressed) : compressed,
140
+ contentEncoding: "gzip",
141
+ rawBytes,
142
+ wireBytes: compressed.byteLength
143
+ };
144
+ }
134
145
  async function gzipViaStream(bytes) {
135
146
  const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
136
147
  return await new Response(stream).arrayBuffer();
137
148
  }
138
149
  function encodeRequestBody(body) {
139
150
  if (readEnv(DISABLE_COMPRESSION_ENV)) {
140
- return { body };
151
+ const rawBytes = new TextEncoder().encode(body).byteLength;
152
+ return { body, rawBytes, wireBytes: rawBytes };
141
153
  }
142
154
  const bytes = new TextEncoder().encode(body);
143
155
  if (bytes.byteLength < MIN_COMPRESSED_BYTES) {
144
- return { body };
156
+ return {
157
+ body,
158
+ rawBytes: bytes.byteLength,
159
+ wireBytes: bytes.byteLength
160
+ };
145
161
  }
146
162
  if (gzipNode) {
147
163
  return gzipNode(bytes).then(
148
- (compressed) => ({
149
- body: toArrayBuffer(compressed),
150
- contentEncoding: "gzip"
151
- }),
152
- () => ({ body })
164
+ (compressed) => compressedRequest(body, bytes.byteLength, compressed),
165
+ () => ({
166
+ body,
167
+ rawBytes: bytes.byteLength,
168
+ wireBytes: bytes.byteLength
169
+ })
153
170
  );
154
171
  }
155
172
  if (typeof CompressionStream === "undefined") {
156
- return { body };
173
+ return {
174
+ body,
175
+ rawBytes: bytes.byteLength,
176
+ wireBytes: bytes.byteLength
177
+ };
157
178
  }
158
179
  return gzipViaStream(bytes).then(
159
- (compressed) => ({ body: compressed, contentEncoding: "gzip" }),
160
- () => ({ body })
180
+ (compressed) => compressedRequest(body, bytes.byteLength, compressed),
181
+ () => ({
182
+ body,
183
+ rawBytes: bytes.byteLength,
184
+ wireBytes: bytes.byteLength
185
+ })
161
186
  );
162
187
  }
163
188
  var DISABLE_COMPRESSION_ENV, MIN_COMPRESSED_BYTES, gzipNode, _nodeGzipReady;
@@ -263,15 +288,15 @@ function carrierBytesOf(encoded, body) {
263
288
  }
264
289
  return encoded.length + extra;
265
290
  }
266
- function fitsCarrierBudget(body) {
291
+ function fitsCarrierBudget(body, maxBytes = MAX_SPAN_CARRIER_BYTES) {
267
292
  const units = body.length;
268
- if (units * MAX_BYTES_PER_UNIT + 2 <= MAX_SPAN_CARRIER_BYTES) {
293
+ if (units * MAX_BYTES_PER_UNIT + 2 <= maxBytes) {
269
294
  return true;
270
295
  }
271
- if (units + 2 > MAX_SPAN_CARRIER_BYTES) {
296
+ if (units + 2 > maxBytes) {
272
297
  return false;
273
298
  }
274
- return carrierByteLength(body) <= MAX_SPAN_CARRIER_BYTES;
299
+ return carrierByteLength(body) <= maxBytes;
275
300
  }
276
301
  function asRecord(value) {
277
302
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
@@ -315,7 +340,7 @@ function collectCandidates(containers) {
315
340
  }
316
341
  return candidates.sort((a, b) => b.size - a.size);
317
342
  }
318
- function trimPayloadToBudget(payload, encode) {
343
+ function trimPayloadToBudget(payload, encode, maxBytes = MAX_SPAN_CARRIER_BYTES) {
319
344
  const { copy, containers } = cloneTrimmable(payload);
320
345
  const candidates = collectCandidates(containers);
321
346
  if (candidates.length === 0) {
@@ -331,30 +356,31 @@ function trimPayloadToBudget(payload, encode) {
331
356
  } catch {
332
357
  return void 0;
333
358
  }
334
- if (fitsCarrierBudget(body)) {
359
+ if (fitsCarrierBudget(body, maxBytes)) {
335
360
  return { value: copy, trimmed };
336
361
  }
337
362
  }
338
363
  return void 0;
339
364
  }
340
- function markPayloadTrimmed(value, trimmed) {
365
+ function markPayloadTrimmed(value, trimmed, maxBytes = MAX_SPAN_CARRIER_BYTES) {
341
366
  const existing = Array.isArray(value.errors) ? value.errors : [];
342
367
  value.errors = [
343
368
  ...existing,
344
369
  {
345
370
  source: "sdk",
346
371
  step: "payload_budget",
347
- error: `trimmed oversized field(s) to fit the ${MAX_SPAN_CARRIER_BYTES}-byte span carrier budget: ${[
372
+ error: `trimmed oversized field(s) to fit the ${maxBytes}-byte span carrier budget: ${[
348
373
  ...new Set(trimmed)
349
374
  ].join(", ")}`
350
375
  }
351
376
  ];
352
377
  }
353
- var MAX_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
378
+ var MAX_SPAN_CARRIER_BYTES, MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES, textEncoder, MAX_BYTES_PER_UNIT, STRUCTURAL_SPAN_KEYS;
354
379
  var init_payloadBudget = __esm({
355
380
  "src/payloadBudget.ts"() {
356
381
  "use strict";
357
382
  MAX_SPAN_CARRIER_BYTES = 28e5;
383
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES = 78e5;
358
384
  textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
359
385
  MAX_BYTES_PER_UNIT = 3;
360
386
  STRUCTURAL_SPAN_KEYS = /* @__PURE__ */ new Set([
@@ -386,30 +412,31 @@ var init_warnOnce = __esm({
386
412
  });
387
413
 
388
414
  // src/serializePayload.ts
389
- function serializePayloadBody(payload) {
415
+ function serializePayloadBody(payload, maxCarrierBytes = MAX_SPAN_CARRIER_BYTES) {
390
416
  const encoded = encodePayloadBody(payload);
391
- if (fitsCarrierBudget(encoded.body)) {
417
+ if (fitsCarrierBudget(encoded.body, maxCarrierBytes)) {
392
418
  return { body: encoded.body, dropped: encoded.dropped };
393
419
  }
394
- return applyPayloadBudget(encoded);
420
+ return applyPayloadBudget(encoded, maxCarrierBytes);
395
421
  }
396
- function applyPayloadBudget(encoded) {
422
+ function applyPayloadBudget(encoded, maxCarrierBytes) {
397
423
  const result = encoded.value ? trimPayloadToBudget(
398
424
  encoded.value,
399
- (value) => encodePayloadBody(value).body
425
+ (value) => encodePayloadBody(value).body,
426
+ maxCarrierBytes
400
427
  ) : void 0;
401
428
  if (!result) {
402
429
  return { body: encoded.body, dropped: encoded.dropped };
403
430
  }
404
431
  warnOnce(
405
432
  "payload:over-budget",
406
- `a span payload exceeded the ${MAX_SPAN_CARRIER_BYTES}-byte carrier budget; its largest field(s) (${[
433
+ `a span payload exceeded the ${maxCarrierBytes}-byte carrier budget; its largest field(s) (${[
407
434
  ...new Set(result.trimmed)
408
435
  ].join(
409
436
  ", "
410
437
  )}) were replaced with placeholders so the span still ships. The span is incomplete and may not be replayable.`
411
438
  );
412
- markPayloadTrimmed(result.value, result.trimmed);
439
+ markPayloadTrimmed(result.value, result.trimmed, maxCarrierBytes);
413
440
  return {
414
441
  body: encodePayloadBody(result.value).body,
415
442
  dropped: encoded.dropped
@@ -666,6 +693,31 @@ function encodeSpan(span) {
666
693
  const json = JSON.stringify(spanToOtlp(span));
667
694
  return { json, size: byteLength(json) };
668
695
  }
696
+ function trimEncodedSpan(span) {
697
+ try {
698
+ const carrier = JSON.parse(span.json);
699
+ const attribute = carrier.attributes?.find(
700
+ (entry) => entry.key === PAYLOAD_ATTRIBUTE
701
+ );
702
+ const payloadBody = attribute?.value?.stringValue;
703
+ if (!attribute?.value || payloadBody === void 0) {
704
+ return void 0;
705
+ }
706
+ const payload = JSON.parse(payloadBody);
707
+ attribute.value.stringValue = serializePayloadBody(
708
+ payload,
709
+ MAX_SPAN_CARRIER_BYTES
710
+ ).body;
711
+ const json = JSON.stringify(carrier);
712
+ return { json, size: byteLength(json) };
713
+ } catch {
714
+ return void 0;
715
+ }
716
+ }
717
+ async function prepareRequest(body) {
718
+ const prepared = encodeRequestBody(body);
719
+ return prepared instanceof Promise ? await prepared : prepared;
720
+ }
669
721
  function requestEnvelope(first) {
670
722
  const scope = first.instrumentationScope;
671
723
  const resource = JSON.stringify({
@@ -802,7 +854,7 @@ function shutdownOtelTransports(timeoutMs = DEFAULT_LIFECYCLE_TIMEOUT_MS) {
802
854
  (transport, remaining) => transport.shutdown(remaining)
803
855
  );
804
856
  }
805
- 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;
857
+ var import_api, import_core, import_resources, import_sdk_trace_base, OPERATION_ATTRIBUTE, PAYLOAD_ATTRIBUTE, OTLP_TRACES_ENDPOINT, MAX_EXPORT_REQUEST_BYTES, MAX_DECOMPRESSED_REQUEST_BYTES, MAX_REQUEST_BYTES_ENV, EXPORT_CONCURRENCY_ENV, MAX_QUEUE_SIZE, DIRECT_MAX_EXPORT_BATCH_SIZE, DIRECT_MAX_REQUEST_BATCH_SIZE, DEFAULT_EXPORT_CONCURRENCY, MAX_EXPORT_CONCURRENCY, SCHEDULE_DELAY_MILLIS, EXPORT_TIMEOUT_MILLIS, RETRY_DELAY_MILLIS, MAX_SEND_ATTEMPTS, DEFAULT_LIFECYCLE_TIMEOUT_MS, RETRYABLE_STATUSES, liveTransports, traceSubmissionSpanIds, replayTraceSubmissions, submissionCounter, SPAN_SEPARATOR_BYTES, OtlpPayloadTooLargeError, OtlpPartialSuccessError, BitfabSpanExporter, DeliveryTrackingExporter, OtelBatchTransport;
806
858
  var init_otel = __esm({
807
859
  "src/otel.ts"() {
808
860
  "use strict";
@@ -810,6 +862,7 @@ var init_otel = __esm({
810
862
  import_core = require("@opentelemetry/core");
811
863
  import_resources = require("@opentelemetry/resources");
812
864
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
865
+ init_compress();
813
866
  init_constants();
814
867
  init_errors();
815
868
  init_payloadBudget();
@@ -821,6 +874,7 @@ var init_otel = __esm({
821
874
  PAYLOAD_ATTRIBUTE = "bitfab.payload";
822
875
  OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
823
876
  MAX_EXPORT_REQUEST_BYTES = 3e6;
877
+ MAX_DECOMPRESSED_REQUEST_BYTES = 8e6;
824
878
  MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES";
825
879
  EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY";
826
880
  MAX_QUEUE_SIZE = 8192;
@@ -903,15 +957,43 @@ var init_otel = __esm({
903
957
  return batches;
904
958
  }
905
959
  async send(envelope, batch) {
906
- if (batch.size > this.maxRequestBytes) {
907
- logError(
908
- "a single OpenTelemetry span exceeded the configured request-size target and could not be exported"
909
- );
910
- return false;
911
- }
912
960
  try {
913
- await this.sendWithRetries(encodeRequest(envelope, batch.spans));
914
- return true;
961
+ let requestSpans = batch.spans;
962
+ let requestRawBytes = batch.size;
963
+ let alreadyTrimmed = false;
964
+ while (true) {
965
+ if (requestRawBytes <= MAX_DECOMPRESSED_REQUEST_BYTES) {
966
+ const prepared = await prepareRequest(
967
+ encodeRequest(envelope, requestSpans)
968
+ );
969
+ if (prepared.wireBytes <= this.maxRequestBytes) {
970
+ await this.sendWithRetries(prepared);
971
+ return true;
972
+ }
973
+ }
974
+ if (batch.spans.length !== 1) {
975
+ logError(
976
+ "an OpenTelemetry span batch exceeded the configured request-size target and could not be exported"
977
+ );
978
+ return false;
979
+ }
980
+ if (alreadyTrimmed) {
981
+ logError(
982
+ "a single OpenTelemetry span exceeded the configured request-size target after trimming"
983
+ );
984
+ return false;
985
+ }
986
+ const trimmed = trimEncodedSpan(batch.spans[0]);
987
+ if (!trimmed) {
988
+ logError(
989
+ "a single OpenTelemetry span exceeded the configured request-size target and could not be trimmed"
990
+ );
991
+ return false;
992
+ }
993
+ requestSpans = [trimmed];
994
+ requestRawBytes = envelope.size + trimmed.size;
995
+ alreadyTrimmed = true;
996
+ }
915
997
  } catch (error) {
916
998
  if (error instanceof OtlpPayloadTooLargeError) {
917
999
  logError(
@@ -939,12 +1021,12 @@ var init_otel = __esm({
939
1021
  * the server does not yet understand. The fix is a client-supplied
940
1022
  * idempotency key that ingestion dedupes on.
941
1023
  */
942
- async sendWithRetries(body) {
1024
+ async sendWithRetries(request) {
943
1025
  for (let attempt = 0; attempt < MAX_SEND_ATTEMPTS; attempt += 1) {
944
1026
  try {
945
1027
  const response = await this.directSender(
946
1028
  OTLP_TRACES_ENDPOINT,
947
- body,
1029
+ request,
948
1030
  EXPORT_TIMEOUT_MILLIS
949
1031
  );
950
1032
  const partialSuccess = asRecord2(response?.partialSuccess);
@@ -1061,7 +1143,10 @@ var init_otel = __esm({
1061
1143
  return;
1062
1144
  }
1063
1145
  try {
1064
- const { body, dropped } = serializePayloadBody(payload);
1146
+ const { body, dropped } = serializePayloadBody(
1147
+ payload,
1148
+ MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES
1149
+ );
1065
1150
  if (dropped.length > 0) {
1066
1151
  warnOnce(
1067
1152
  "otel-carrier-payload-stubbed",
@@ -1250,7 +1335,7 @@ var init_http = __esm({
1250
1335
  }
1251
1336
  if (!this.traceTransport) {
1252
1337
  this.traceTransport = createTraceTransport({
1253
- directSender: (endpoint, body, timeoutMs) => this.sendEncoded(endpoint, body, {
1338
+ directSender: (endpoint, request, timeoutMs) => this.sendPrepared(endpoint, request, {
1254
1339
  timeout: timeoutMs
1255
1340
  })
1256
1341
  });
@@ -1337,13 +1422,16 @@ var init_http = __esm({
1337
1422
  * same data twice.
1338
1423
  */
1339
1424
  async sendEncoded(endpoint, body, options) {
1425
+ const prepared = encodeRequestBody(body);
1426
+ const encoded = prepared instanceof Promise ? await prepared : prepared;
1427
+ return this.sendPrepared(endpoint, encoded, options);
1428
+ }
1429
+ async sendPrepared(endpoint, encoded, options) {
1340
1430
  const url = `${this.serviceUrl}${endpoint}`;
1341
1431
  const timeout = options?.timeout ?? this.timeout;
1342
1432
  const method = options?.method ?? "POST";
1343
1433
  const controller = new AbortController();
1344
1434
  const timeoutId = setTimeout(() => controller.abort(), timeout);
1345
- const prepared = encodeRequestBody(body);
1346
- const encoded = prepared instanceof Promise ? await prepared : prepared;
1347
1435
  const headers = {
1348
1436
  "Content-Type": "application/json",
1349
1437
  Authorization: `Bearer ${this.resolveApiKey() ?? ""}`
@@ -1817,8 +1905,8 @@ var init_serialize = __esm({
1817
1905
  import_superjson = __toESM(require("superjson"), 1);
1818
1906
  init_payloadBudget();
1819
1907
  init_warnOnce();
1820
- MAX_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1821
- MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_SPAN_CARRIER_BYTES;
1908
+ MAX_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
1909
+ MAX_FRAMEWORK_SERIALIZED_BYTES = MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES;
1822
1910
  MAX_SAFE_DEPTH = 6;
1823
1911
  }
1824
1912
  });
@@ -2400,12 +2488,13 @@ function sleep(ms) {
2400
2488
  unrefTimer(timer);
2401
2489
  });
2402
2490
  }
2403
- async function mapWithConcurrency2(tasks, maxConcurrency, onSettled) {
2491
+ async function mapWithConcurrency2(tasks, maxConcurrency, onSettled, onStarted) {
2404
2492
  const results = new Array(tasks.length);
2405
2493
  let nextIndex = 0;
2406
2494
  async function worker() {
2407
2495
  while (nextIndex < tasks.length) {
2408
2496
  const index = nextIndex++;
2497
+ onStarted?.(index);
2409
2498
  const result = await tasks[index]();
2410
2499
  results[index] = result;
2411
2500
  onSettled?.(result, index);
@@ -2492,13 +2581,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2492
2581
  )
2493
2582
  );
2494
2583
  const total = tasks.length;
2584
+ const onItemFinish = options?.onItemFinish ?? options?.onProgress;
2495
2585
  let completed = 0;
2586
+ let started = 0;
2496
2587
  let succeeded = 0;
2497
2588
  let errored = 0;
2498
2589
  const resultItems = await mapWithConcurrency2(
2499
2590
  tasks,
2500
2591
  maxConcurrency,
2501
- options?.onProgress ? (item) => {
2592
+ (item) => {
2502
2593
  completed += 1;
2503
2594
  if (item.error === null) {
2504
2595
  succeeded += 1;
@@ -2506,18 +2597,17 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2506
2597
  errored += 1;
2507
2598
  }
2508
2599
  try {
2509
- options?.onProgress?.({
2600
+ onItemFinish?.({
2510
2601
  testRunId,
2511
2602
  completed,
2512
2603
  total,
2513
2604
  succeeded,
2514
2605
  errored,
2515
2606
  item: {
2516
- // The server replay trace id isn't known until completeReplay
2517
- // runs (below), so it can't be reported mid-run and we never
2518
- // emit the client-side placeholder. originalTraceId (the
2519
- // historical trace) is known now and is what a UI keys on to
2520
- // identify what just settled.
2607
+ // The server replay trace id isn't known until completeReplay runs
2608
+ // (below), so it can't be reported mid-run and we never emit the
2609
+ // client-side placeholder. originalTraceId (the historical trace)
2610
+ // is known now and is what a UI keys on to identify what settled.
2521
2611
  traceId: null,
2522
2612
  originalTraceId: item.originalTraceId ?? null,
2523
2613
  originalSpanId: item.originalSpanId ?? null,
@@ -2538,6 +2628,30 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2538
2628
  });
2539
2629
  } catch {
2540
2630
  }
2631
+ },
2632
+ options?.onItemStart ? (index) => {
2633
+ started += 1;
2634
+ const serverItem = serverItems[index];
2635
+ const originalTraceId = serverItem.originalTraceId ?? serverItem.sourceTraceId;
2636
+ const originalSpanId = serverItem.originalSpanId ?? serverItem.sourceSpanId;
2637
+ try {
2638
+ options.onItemStart?.({
2639
+ type: "started",
2640
+ testRunId,
2641
+ started,
2642
+ completed,
2643
+ total,
2644
+ succeeded,
2645
+ errored,
2646
+ item: {
2647
+ originalTraceId,
2648
+ originalSpanId,
2649
+ sourceTraceId: originalTraceId,
2650
+ sourceSpanId: originalSpanId
2651
+ }
2652
+ });
2653
+ } catch {
2654
+ }
2541
2655
  } : void 0
2542
2656
  );
2543
2657
  await preserveReplayFailure(
@@ -2600,17 +2714,19 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2600
2714
  testRunUrl: fullTestRunUrl
2601
2715
  };
2602
2716
  await writeReplayResultFile(result);
2603
- try {
2604
- options?.onProgress?.({
2605
- type: "complete",
2606
- testRunId,
2607
- completed: total,
2608
- total,
2609
- succeeded,
2610
- errored,
2611
- result
2612
- });
2613
- } catch {
2717
+ if (!options?.onItemFinish) {
2718
+ try {
2719
+ options?.onProgress?.({
2720
+ type: "complete",
2721
+ testRunId,
2722
+ completed: total,
2723
+ total,
2724
+ succeeded,
2725
+ errored,
2726
+ result
2727
+ });
2728
+ } catch {
2729
+ }
2614
2730
  }
2615
2731
  return result;
2616
2732
  }