@reproapp/node-sdk 0.0.8 → 0.0.11

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.d.ts CHANGED
@@ -254,6 +254,8 @@ export type ReproTracingInitOptions = TracerInitOpts & {
254
254
  export declare function initReproTracing(opts?: ReproTracingInitOptions): TracerApi | null;
255
255
  /** Optional helper if users want to check it. */
256
256
  export declare function isReproTracingEnabled(): boolean;
257
+ declare function sanitizeTraceValueForPrivacy(value: any): any;
258
+ declare function sanitizeMaterializedTraceValue(value: any): any;
257
259
  export declare function __materializePendingTraceEventsForWorker(payload: TraceMaterializationWorkerPayload): Promise<PendingTraceEventRecord[]>;
258
260
  type NormalizedMaskRule = {
259
261
  when?: ReproMaskWhen;
@@ -283,6 +285,8 @@ type InlinePrivacyMaterializationResult = {
283
285
  };
284
286
  };
285
287
  declare function materializeInlinePrivacyValueAsync(target: RuntimePrivacySurface, value: any, cfg: ReproMiddlewareConfig, req: MaskRequestContext, trace: TraceEventForFilter | null, masking: NormalizedMaskingConfig | null, privacy: NormalizedRuntimePrivacyPolicy | null, db?: RuntimePrivacyDbContext | null): Promise<InlinePrivacyMaterializationResult>;
288
+ declare function estimateTraceBatchSerializedBytes(batch: TraceEventRecord[], requestRid: string, actionId: string | null | undefined, batchIndex: number): number;
289
+ declare function chunkTraceEventsForTransport(events: TraceEventRecord[], requestRid: string, actionId: string | null | undefined): TraceEventRecord[][];
286
290
  export type ReproMiddlewareConfig = IngestClientConfig & {
287
291
  /** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
288
292
  captureHeaders?: boolean | HeaderCaptureOptions;
@@ -342,11 +346,21 @@ export declare function initRepro(cfg: ReproInitConfig): Promise<void>;
342
346
  /** @internal Test hooks for runtime privacy policy wiring. */
343
347
  export declare const __reproTestHooks: {
344
348
  shareRuntimePrivacyStateForTest: typeof shareRuntimePrivacyState;
349
+ setFullValueCaptureEnabledForTest(enabled: boolean): void;
350
+ scheduleSdkBackgroundWorkForTest(work: () => Promise<void> | void, options?: {
351
+ priority?: 'high' | 'normal';
352
+ }): void;
353
+ drainSdkBackgroundQueueForTest(): Promise<void>;
354
+ resetSdkBackgroundQueuesForTest(): void;
345
355
  getRuntimePrivacyPolicyForTest(cfg: ReproMiddlewareConfig): NormalizedRuntimePrivacyPolicy | null;
346
356
  setRuntimePrivacyPolicyForTest(cfg: ReproMiddlewareConfig, policy: NormalizedRuntimePrivacyPolicy | null): void;
347
357
  recordKafkaTraceEventAsyncForTest: typeof recordKafkaTraceEventAsync;
348
358
  materializeInlinePrivacyValueAsyncForTest: typeof materializeInlinePrivacyValueAsync;
349
359
  patchKafkaProducerInstanceForTest: typeof patchKafkaProducerInstance;
350
360
  wrapKafkaEachMessageHandlerForTest: typeof wrapKafkaEachMessageHandler;
361
+ chunkTraceEventsForTransportForTest: typeof chunkTraceEventsForTransport;
362
+ estimateTraceBatchSerializedBytesForTest: typeof estimateTraceBatchSerializedBytes;
363
+ sanitizeTraceValueForPrivacyForTest: typeof sanitizeTraceValueForPrivacy;
364
+ sanitizeMaterializedTraceValueForTest: typeof sanitizeMaterializedTraceValue;
351
365
  };
352
366
  export {};
package/dist/index.js CHANGED
@@ -863,6 +863,7 @@ let activeClientRequestCount = 0;
863
863
  let oldestActiveClientRequestAt = null;
864
864
  let lastClientActivityAt = 0;
865
865
  let sdkBackgroundQuietUntil = 0;
866
+ const sdkBackgroundHighQueue = [];
866
867
  const sdkBackgroundQueue = [];
867
868
  let sdkBackgroundTimer = null;
868
869
  let sdkBackgroundDraining = false;
@@ -1155,8 +1156,13 @@ function endClientRequest() {
1155
1156
  scheduleSdkBackgroundDrain();
1156
1157
  }
1157
1158
  }
1158
- function scheduleSdkBackgroundWork(work) {
1159
- sdkBackgroundQueue.push(work);
1159
+ function scheduleSdkBackgroundWork(work, options = {}) {
1160
+ if (options.priority === 'high') {
1161
+ sdkBackgroundHighQueue.push(work);
1162
+ }
1163
+ else {
1164
+ sdkBackgroundQueue.push(work);
1165
+ }
1160
1166
  scheduleSdkBackgroundDrain();
1161
1167
  }
1162
1168
  function scheduleSdkBackgroundDrain(delayMs = 0) {
@@ -1209,12 +1215,12 @@ async function drainSdkBackgroundQueue() {
1209
1215
  }
1210
1216
  sdkBackgroundDraining = true;
1211
1217
  try {
1212
- while (sdkBackgroundQueue.length > 0) {
1218
+ while (sdkBackgroundHighQueue.length > 0 || sdkBackgroundQueue.length > 0) {
1213
1219
  if (shouldDeferSdkBackgroundWork()) {
1214
1220
  scheduleSdkBackgroundDrain();
1215
1221
  return;
1216
1222
  }
1217
- const work = sdkBackgroundQueue.shift();
1223
+ const work = sdkBackgroundHighQueue.shift() ?? sdkBackgroundQueue.shift();
1218
1224
  if (!work)
1219
1225
  continue;
1220
1226
  try {
@@ -1228,7 +1234,7 @@ async function drainSdkBackgroundQueue() {
1228
1234
  }
1229
1235
  finally {
1230
1236
  sdkBackgroundDraining = false;
1231
- if (sdkBackgroundQueue.length > 0) {
1237
+ if (sdkBackgroundHighQueue.length > 0 || sdkBackgroundQueue.length > 0) {
1232
1238
  scheduleSdkBackgroundDrain();
1233
1239
  }
1234
1240
  else {
@@ -1657,6 +1663,12 @@ const TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS = (() => {
1657
1663
  })();
1658
1664
  const TRACE_VALUE_SIZE_EXCEEDED = Symbol('trace-value-size-exceeded');
1659
1665
  const TRACE_BATCH_SIZE = 100;
1666
+ const TRACE_BATCH_MAX_SERIALIZED_BYTES = (() => {
1667
+ const env = Number(process.env.REPRO_TRACE_BATCH_MAX_SERIALIZED_BYTES);
1668
+ if (Number.isFinite(env) && env > 0)
1669
+ return Math.trunc(env);
1670
+ return 512 * 1024;
1671
+ })();
1660
1672
  const TRACE_FLUSH_DELAY_MS = 20;
1661
1673
  // Choose how to order trace events in payloads.
1662
1674
  // - "chronological" (default): preserve event arrival order (no reshuffle).
@@ -2026,14 +2038,26 @@ function sanitizeTraceValue(value, depth = 0, seen = new WeakMap(), options = {}
2026
2038
  return circularReference(existingPath);
2027
2039
  seen.set(value, valuePath);
2028
2040
  if (!options.disableTruncation && depth >= TRACE_VALUE_MAX_DEPTH) {
2029
- const shallow = safeJson(value);
2030
- if (shallow !== undefined) {
2031
- return shallow;
2041
+ if (Array.isArray(value)) {
2042
+ return {
2043
+ __type: 'Array',
2044
+ length: value.length,
2045
+ __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
2046
+ };
2032
2047
  }
2033
2048
  const ctor = value?.constructor?.name;
2034
- return ctor && ctor !== 'Object'
2035
- ? { __class: ctor, __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}` }
2036
- : { __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}` };
2049
+ const keys = Object.keys(value);
2050
+ const summary = {
2051
+ __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
2052
+ __keys: keys.slice(0, TRACE_VALUE_MAX_KEYS),
2053
+ };
2054
+ if (keys.length > TRACE_VALUE_MAX_KEYS) {
2055
+ summary.__truncatedKeys = keys.length - TRACE_VALUE_MAX_KEYS;
2056
+ }
2057
+ if (ctor && ctor !== 'Object') {
2058
+ summary.__class = ctor;
2059
+ }
2060
+ return summary;
2037
2061
  }
2038
2062
  if (Array.isArray(value)) {
2039
2063
  const sourceItems = options.disableTruncation ? value : value.slice(0, TRACE_VALUE_MAX_ITEMS);
@@ -2099,6 +2123,9 @@ function sanitizeTraceArgs(values) {
2099
2123
  function sanitizeTraceValueForPrivacy(value) {
2100
2124
  return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2101
2125
  }
2126
+ function sanitizeInlinePrivacyValue(value) {
2127
+ return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2128
+ }
2102
2129
  function sanitizeMaterializedTraceValue(value) {
2103
2130
  return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2104
2131
  }
@@ -3526,6 +3553,51 @@ function collectBatchTraceValueEntries(batch, batchIndex) {
3526
3553
  });
3527
3554
  return collected;
3528
3555
  }
3556
+ function serializedByteLength(value) {
3557
+ try {
3558
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
3559
+ }
3560
+ catch {
3561
+ return Number.MAX_SAFE_INTEGER;
3562
+ }
3563
+ }
3564
+ function estimateTraceBatchSerializedBytes(batch, requestRid, actionId, batchIndex) {
3565
+ const traceValues = collectBatchTraceValueEntries(batch, batchIndex);
3566
+ return serializedByteLength({
3567
+ actionId: actionId ?? null,
3568
+ trace: batch,
3569
+ traceValues: traceValues.length ? traceValues : undefined,
3570
+ traceBatch: {
3571
+ rid: requestRid,
3572
+ index: batchIndex,
3573
+ total: 0,
3574
+ },
3575
+ t: 0,
3576
+ });
3577
+ }
3578
+ function chunkTraceEventsForTransport(events, requestRid, actionId) {
3579
+ if (!Array.isArray(events) || events.length === 0)
3580
+ return [];
3581
+ const countBatches = chunkArray(events, TRACE_BATCH_SIZE);
3582
+ const sizedBatches = [];
3583
+ let current = [];
3584
+ for (const event of countBatches.flat()) {
3585
+ const candidate = current.concat(event);
3586
+ const batchIndex = sizedBatches.length;
3587
+ const estimatedBytes = estimateTraceBatchSerializedBytes(candidate, requestRid, actionId, batchIndex);
3588
+ if (current.length > 0 &&
3589
+ estimatedBytes > TRACE_BATCH_MAX_SERIALIZED_BYTES) {
3590
+ sizedBatches.push(current);
3591
+ current = [event];
3592
+ continue;
3593
+ }
3594
+ current = candidate;
3595
+ }
3596
+ if (current.length > 0) {
3597
+ sizedBatches.push(current);
3598
+ }
3599
+ return sizedBatches;
3600
+ }
3529
3601
  function createCapturedValueEntry(params) {
3530
3602
  if (!__FULL_VALUE_CAPTURE_ENABLED)
3531
3603
  return undefined;
@@ -3809,7 +3881,7 @@ function reproMiddleware(cfg) {
3809
3881
  }
3810
3882
  catch { }
3811
3883
  if (flushPayload) {
3812
- const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload);
3884
+ const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload, { priority: 'high' });
3813
3885
  if (sessionDrainWait) {
3814
3886
  sessionDrainWait.then(scheduleFlushPayload).catch(scheduleFlushPayload);
3815
3887
  }
@@ -4109,7 +4181,7 @@ function reproMiddleware(cfg) {
4109
4181
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
4110
4182
  ? reorderTraceEvents(baseEvents)
4111
4183
  : sortTraceEventsChronologically(baseEvents);
4112
- const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
4184
+ const traceBatches = chunkTraceEventsForTransport(orderedEvents, rid, aid);
4113
4185
  if (traceBatches.length) {
4114
4186
  for (let i = 0; i < traceBatches.length; i++) {
4115
4187
  const batch = traceBatches[i];
@@ -5125,7 +5197,7 @@ function cloneKafkaTelemetryValue(value) {
5125
5197
  }
5126
5198
  }
5127
5199
  async function materializeKafkaRequestBody(cfg, maskReq, body) {
5128
- return materializeInlinePrivacyValueAsync('request.body', sanitizeTraceValueForPrivacy(body), cfg, maskReq, null, normalizeMaskingConfig(cfg.masking), getRuntimePrivacyState(cfg).policy ?? null);
5200
+ return materializeInlinePrivacyValueAsync('request.body', sanitizeInlinePrivacyValue(body), cfg, maskReq, null, normalizeMaskingConfig(cfg.masking), getRuntimePrivacyState(cfg).policy ?? null);
5129
5201
  }
5130
5202
  function summarizeKafkaError(error) {
5131
5203
  const message = typeof error?.message === 'string'
@@ -5312,7 +5384,7 @@ function buildKafkaTraceEntries(actionId, requestRid, events) {
5312
5384
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
5313
5385
  ? reorderTraceEvents(baseEvents)
5314
5386
  : sortTraceEventsChronologically(baseEvents);
5315
- const batches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
5387
+ const batches = chunkTraceEventsForTransport(orderedEvents, requestRid, actionId);
5316
5388
  return batches.map((batch, index) => ({
5317
5389
  actionId: actionId ?? null,
5318
5390
  trace: batch,
@@ -5353,7 +5425,7 @@ function scheduleKafkaTraceFlush(cfg, kafkaCtx, actionId, requestRid, rawTraceEv
5353
5425
  post(cfg, sid, { entries });
5354
5426
  }
5355
5427
  });
5356
- });
5428
+ }, { priority: 'high' });
5357
5429
  }
5358
5430
  function patchKafkaProducerInstance(producer, cfg) {
5359
5431
  if (!producer || producer.__repro_kafka_producer_patched)
@@ -6616,6 +6688,31 @@ exports.initRepro = initRepro;
6616
6688
  /** @internal Test hooks for runtime privacy policy wiring. */
6617
6689
  exports.__reproTestHooks = {
6618
6690
  shareRuntimePrivacyStateForTest: shareRuntimePrivacyState,
6691
+ setFullValueCaptureEnabledForTest(enabled) {
6692
+ __FULL_VALUE_CAPTURE_ENABLED = enabled === true;
6693
+ },
6694
+ scheduleSdkBackgroundWorkForTest(work, options) {
6695
+ scheduleSdkBackgroundWork(work, options);
6696
+ },
6697
+ async drainSdkBackgroundQueueForTest() {
6698
+ await drainSdkBackgroundQueue();
6699
+ },
6700
+ resetSdkBackgroundQueuesForTest() {
6701
+ sdkBackgroundHighQueue.length = 0;
6702
+ sdkBackgroundQueue.length = 0;
6703
+ if (sdkBackgroundTimer) {
6704
+ try {
6705
+ clearTimeout(sdkBackgroundTimer);
6706
+ }
6707
+ catch { }
6708
+ sdkBackgroundTimer = null;
6709
+ }
6710
+ sdkBackgroundDraining = false;
6711
+ activeClientRequestCount = 0;
6712
+ oldestActiveClientRequestAt = null;
6713
+ lastClientActivityAt = 0;
6714
+ sdkBackgroundQuietUntil = 0;
6715
+ },
6619
6716
  getRuntimePrivacyPolicyForTest(cfg) {
6620
6717
  return getRuntimePrivacyState(cfg).policy;
6621
6718
  },
@@ -6626,4 +6723,8 @@ exports.__reproTestHooks = {
6626
6723
  materializeInlinePrivacyValueAsyncForTest: materializeInlinePrivacyValueAsync,
6627
6724
  patchKafkaProducerInstanceForTest: patchKafkaProducerInstance,
6628
6725
  wrapKafkaEachMessageHandlerForTest: wrapKafkaEachMessageHandler,
6726
+ chunkTraceEventsForTransportForTest: chunkTraceEventsForTransport,
6727
+ estimateTraceBatchSerializedBytesForTest: estimateTraceBatchSerializedBytes,
6728
+ sanitizeTraceValueForPrivacyForTest: sanitizeTraceValueForPrivacy,
6729
+ sanitizeMaterializedTraceValueForTest: sanitizeMaterializedTraceValue,
6629
6730
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.8",
3
+ "version": "0.0.11",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,7 @@
12
12
  "build": "tsc -p tsconfig.json",
13
13
  "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
14
14
  "prepublishOnly": "npm run build",
15
- "test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/express-trace-http-args.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js"
15
+ "test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js"
16
16
  },
17
17
  "peerDependencies": {
18
18
  "express": "^5.1.0",
package/src/index.ts CHANGED
@@ -1231,6 +1231,7 @@ let activeClientRequestCount = 0;
1231
1231
  let oldestActiveClientRequestAt: number | null = null;
1232
1232
  let lastClientActivityAt = 0;
1233
1233
  let sdkBackgroundQuietUntil = 0;
1234
+ const sdkBackgroundHighQueue: Array<() => Promise<void> | void> = [];
1234
1235
  const sdkBackgroundQueue: Array<() => Promise<void> | void> = [];
1235
1236
  let sdkBackgroundTimer: NodeJS.Timeout | null = null;
1236
1237
  let sdkBackgroundDraining = false;
@@ -1499,8 +1500,15 @@ function endClientRequest(): void {
1499
1500
  }
1500
1501
  }
1501
1502
 
1502
- function scheduleSdkBackgroundWork(work: () => Promise<void> | void): void {
1503
- sdkBackgroundQueue.push(work);
1503
+ function scheduleSdkBackgroundWork(
1504
+ work: () => Promise<void> | void,
1505
+ options: { priority?: 'high' | 'normal' } = {},
1506
+ ): void {
1507
+ if (options.priority === 'high') {
1508
+ sdkBackgroundHighQueue.push(work);
1509
+ } else {
1510
+ sdkBackgroundQueue.push(work);
1511
+ }
1504
1512
  scheduleSdkBackgroundDrain();
1505
1513
  }
1506
1514
 
@@ -1549,12 +1557,12 @@ async function drainSdkBackgroundQueue(): Promise<void> {
1549
1557
 
1550
1558
  sdkBackgroundDraining = true;
1551
1559
  try {
1552
- while (sdkBackgroundQueue.length > 0) {
1560
+ while (sdkBackgroundHighQueue.length > 0 || sdkBackgroundQueue.length > 0) {
1553
1561
  if (shouldDeferSdkBackgroundWork()) {
1554
1562
  scheduleSdkBackgroundDrain();
1555
1563
  return;
1556
1564
  }
1557
- const work = sdkBackgroundQueue.shift();
1565
+ const work = sdkBackgroundHighQueue.shift() ?? sdkBackgroundQueue.shift();
1558
1566
  if (!work) continue;
1559
1567
  try {
1560
1568
  await drainQueuedIngestBeforeBackgroundWork();
@@ -1565,7 +1573,7 @@ async function drainSdkBackgroundQueue(): Promise<void> {
1565
1573
  }
1566
1574
  } finally {
1567
1575
  sdkBackgroundDraining = false;
1568
- if (sdkBackgroundQueue.length > 0) {
1576
+ if (sdkBackgroundHighQueue.length > 0 || sdkBackgroundQueue.length > 0) {
1569
1577
  scheduleSdkBackgroundDrain();
1570
1578
  } else {
1571
1579
  kickIngestQueueDrain();
@@ -2028,6 +2036,11 @@ const TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS = (() => {
2028
2036
  })();
2029
2037
  const TRACE_VALUE_SIZE_EXCEEDED = Symbol('trace-value-size-exceeded');
2030
2038
  const TRACE_BATCH_SIZE = 100;
2039
+ const TRACE_BATCH_MAX_SERIALIZED_BYTES = (() => {
2040
+ const env = Number(process.env.REPRO_TRACE_BATCH_MAX_SERIALIZED_BYTES);
2041
+ if (Number.isFinite(env) && env > 0) return Math.trunc(env);
2042
+ return 512 * 1024;
2043
+ })();
2031
2044
  const TRACE_FLUSH_DELAY_MS = 20;
2032
2045
  // Choose how to order trace events in payloads.
2033
2046
  // - "chronological" (default): preserve event arrival order (no reshuffle).
@@ -2424,14 +2437,26 @@ function sanitizeTraceValue(
2424
2437
  seen.set(value, valuePath);
2425
2438
 
2426
2439
  if (!options.disableTruncation && depth >= TRACE_VALUE_MAX_DEPTH) {
2427
- const shallow = safeJson(value);
2428
- if (shallow !== undefined) {
2429
- return shallow;
2440
+ if (Array.isArray(value)) {
2441
+ return {
2442
+ __type: 'Array',
2443
+ length: value.length,
2444
+ __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
2445
+ };
2430
2446
  }
2431
2447
  const ctor = value?.constructor?.name;
2432
- return ctor && ctor !== 'Object'
2433
- ? { __class: ctor, __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}` }
2434
- : { __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}` };
2448
+ const keys = Object.keys(value);
2449
+ const summary: Record<string, any> = {
2450
+ __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
2451
+ __keys: keys.slice(0, TRACE_VALUE_MAX_KEYS),
2452
+ };
2453
+ if (keys.length > TRACE_VALUE_MAX_KEYS) {
2454
+ summary.__truncatedKeys = keys.length - TRACE_VALUE_MAX_KEYS;
2455
+ }
2456
+ if (ctor && ctor !== 'Object') {
2457
+ summary.__class = ctor;
2458
+ }
2459
+ return summary;
2435
2460
  }
2436
2461
 
2437
2462
  if (Array.isArray(value)) {
@@ -2500,6 +2525,10 @@ function sanitizeTraceValueForPrivacy(value: any): any {
2500
2525
  return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2501
2526
  }
2502
2527
 
2528
+ function sanitizeInlinePrivacyValue(value: any): any {
2529
+ return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2530
+ }
2531
+
2503
2532
  function sanitizeMaterializedTraceValue(value: any): any {
2504
2533
  return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2505
2534
  }
@@ -4215,6 +4244,74 @@ function collectBatchTraceValueEntries(batch: TraceEventRecord[], batchIndex: nu
4215
4244
  return collected;
4216
4245
  }
4217
4246
 
4247
+ function serializedByteLength(value: any): number {
4248
+ try {
4249
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
4250
+ } catch {
4251
+ return Number.MAX_SAFE_INTEGER;
4252
+ }
4253
+ }
4254
+
4255
+ function estimateTraceBatchSerializedBytes(
4256
+ batch: TraceEventRecord[],
4257
+ requestRid: string,
4258
+ actionId: string | null | undefined,
4259
+ batchIndex: number,
4260
+ ): number {
4261
+ const traceValues = collectBatchTraceValueEntries(batch, batchIndex);
4262
+ return serializedByteLength({
4263
+ actionId: actionId ?? null,
4264
+ trace: batch,
4265
+ traceValues: traceValues.length ? traceValues : undefined,
4266
+ traceBatch: {
4267
+ rid: requestRid,
4268
+ index: batchIndex,
4269
+ total: 0,
4270
+ },
4271
+ t: 0,
4272
+ });
4273
+ }
4274
+
4275
+ function chunkTraceEventsForTransport(
4276
+ events: TraceEventRecord[],
4277
+ requestRid: string,
4278
+ actionId: string | null | undefined,
4279
+ ): TraceEventRecord[][] {
4280
+ if (!Array.isArray(events) || events.length === 0) return [];
4281
+
4282
+ const countBatches = chunkArray(events, TRACE_BATCH_SIZE);
4283
+ const sizedBatches: TraceEventRecord[][] = [];
4284
+ let current: TraceEventRecord[] = [];
4285
+
4286
+ for (const event of countBatches.flat()) {
4287
+ const candidate = current.concat(event);
4288
+ const batchIndex = sizedBatches.length;
4289
+ const estimatedBytes = estimateTraceBatchSerializedBytes(
4290
+ candidate,
4291
+ requestRid,
4292
+ actionId,
4293
+ batchIndex,
4294
+ );
4295
+
4296
+ if (
4297
+ current.length > 0 &&
4298
+ estimatedBytes > TRACE_BATCH_MAX_SERIALIZED_BYTES
4299
+ ) {
4300
+ sizedBatches.push(current);
4301
+ current = [event];
4302
+ continue;
4303
+ }
4304
+
4305
+ current = candidate;
4306
+ }
4307
+
4308
+ if (current.length > 0) {
4309
+ sizedBatches.push(current);
4310
+ }
4311
+
4312
+ return sizedBatches;
4313
+ }
4314
+
4218
4315
  function createCapturedValueEntry(
4219
4316
  params: {
4220
4317
  target:
@@ -4601,11 +4698,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4601
4698
  return;
4602
4699
  }
4603
4700
  }
4604
- flushed = true;
4605
- clearTimers();
4701
+ flushed = true;
4702
+ clearTimers();
4606
4703
  try { unsubscribe && unsubscribe(); } catch {}
4607
4704
  if (flushPayload) {
4608
- const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload!);
4705
+ const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload!, { priority: 'high' });
4609
4706
  if (sessionDrainWait) {
4610
4707
  sessionDrainWait.then(scheduleFlushPayload).catch(scheduleFlushPayload);
4611
4708
  } else {
@@ -4953,7 +5050,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4953
5050
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
4954
5051
  ? reorderTraceEvents(baseEvents)
4955
5052
  : sortTraceEventsChronologically(baseEvents);
4956
- const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
5053
+ const traceBatches = chunkTraceEventsForTransport(
5054
+ orderedEvents,
5055
+ rid,
5056
+ aid,
5057
+ );
4957
5058
 
4958
5059
  if (traceBatches.length) {
4959
5060
  for (let i = 0; i < traceBatches.length; i++) {
@@ -6006,7 +6107,7 @@ async function materializeKafkaRequestBody(
6006
6107
  ): Promise<InlinePrivacyMaterializationResult> {
6007
6108
  return materializeInlinePrivacyValueAsync(
6008
6109
  'request.body',
6009
- sanitizeTraceValueForPrivacy(body),
6110
+ sanitizeInlinePrivacyValue(body),
6010
6111
  cfg,
6011
6112
  maskReq,
6012
6113
  null,
@@ -6253,7 +6354,11 @@ function buildKafkaTraceEntries(
6253
6354
  TRACE_ORDER_MODE === 'tree'
6254
6355
  ? reorderTraceEvents(baseEvents)
6255
6356
  : sortTraceEventsChronologically(baseEvents);
6256
- const batches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
6357
+ const batches = chunkTraceEventsForTransport(
6358
+ orderedEvents,
6359
+ requestRid,
6360
+ actionId,
6361
+ );
6257
6362
  return batches.map((batch, index) => ({
6258
6363
  actionId: actionId ?? null,
6259
6364
  trace: batch,
@@ -6300,7 +6405,7 @@ function scheduleKafkaTraceFlush(
6300
6405
  post(cfg, sid, { entries });
6301
6406
  }
6302
6407
  });
6303
- });
6408
+ }, { priority: 'high' });
6304
6409
  }
6305
6410
 
6306
6411
  function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
@@ -7605,6 +7710,31 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
7605
7710
  /** @internal Test hooks for runtime privacy policy wiring. */
7606
7711
  export const __reproTestHooks = {
7607
7712
  shareRuntimePrivacyStateForTest: shareRuntimePrivacyState,
7713
+ setFullValueCaptureEnabledForTest(enabled: boolean): void {
7714
+ __FULL_VALUE_CAPTURE_ENABLED = enabled === true;
7715
+ },
7716
+ scheduleSdkBackgroundWorkForTest(
7717
+ work: () => Promise<void> | void,
7718
+ options?: { priority?: 'high' | 'normal' },
7719
+ ): void {
7720
+ scheduleSdkBackgroundWork(work, options);
7721
+ },
7722
+ async drainSdkBackgroundQueueForTest(): Promise<void> {
7723
+ await drainSdkBackgroundQueue();
7724
+ },
7725
+ resetSdkBackgroundQueuesForTest(): void {
7726
+ sdkBackgroundHighQueue.length = 0;
7727
+ sdkBackgroundQueue.length = 0;
7728
+ if (sdkBackgroundTimer) {
7729
+ try { clearTimeout(sdkBackgroundTimer); } catch {}
7730
+ sdkBackgroundTimer = null;
7731
+ }
7732
+ sdkBackgroundDraining = false;
7733
+ activeClientRequestCount = 0;
7734
+ oldestActiveClientRequestAt = null;
7735
+ lastClientActivityAt = 0;
7736
+ sdkBackgroundQuietUntil = 0;
7737
+ },
7608
7738
  getRuntimePrivacyPolicyForTest(cfg: ReproMiddlewareConfig): NormalizedRuntimePrivacyPolicy | null {
7609
7739
  return getRuntimePrivacyState(cfg).policy;
7610
7740
  },
@@ -7618,4 +7748,8 @@ export const __reproTestHooks = {
7618
7748
  materializeInlinePrivacyValueAsyncForTest: materializeInlinePrivacyValueAsync,
7619
7749
  patchKafkaProducerInstanceForTest: patchKafkaProducerInstance,
7620
7750
  wrapKafkaEachMessageHandlerForTest: wrapKafkaEachMessageHandler,
7751
+ chunkTraceEventsForTransportForTest: chunkTraceEventsForTransport,
7752
+ estimateTraceBatchSerializedBytesForTest: estimateTraceBatchSerializedBytes,
7753
+ sanitizeTraceValueForPrivacyForTest: sanitizeTraceValueForPrivacy,
7754
+ sanitizeMaterializedTraceValueForTest: sanitizeMaterializedTraceValue,
7621
7755
  };
@@ -567,6 +567,7 @@ async function testLargeStringTraceValuesAreCapturedInlineWithoutTruncation() {
567
567
  };
568
568
 
569
569
  const sink = [];
570
+ __reproTestHooks.setFullValueCaptureEnabledForTest(true);
570
571
  await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
571
572
  {
572
573
  type: 'enter',
@@ -579,6 +580,7 @@ async function testLargeStringTraceValuesAreCapturedInlineWithoutTruncation() {
579
580
  cfg,
580
581
  maskReq,
581
582
  );
583
+ __reproTestHooks.setFullValueCaptureEnabledForTest(false);
582
584
 
583
585
  assert.strictEqual(sink.length, 1);
584
586
  assert.strictEqual(sink[0].fn, 'parseGatewayPayload');
@@ -622,6 +624,7 @@ async function testLargeStructuredTraceValuesAreCapturedInlineWithoutTruncation(
622
624
  };
623
625
 
624
626
  const sink = [];
627
+ __reproTestHooks.setFullValueCaptureEnabledForTest(true);
625
628
  await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
626
629
  {
627
630
  type: 'enter',
@@ -634,6 +637,7 @@ async function testLargeStructuredTraceValuesAreCapturedInlineWithoutTruncation(
634
637
  cfg,
635
638
  maskReq,
636
639
  );
640
+ __reproTestHooks.setFullValueCaptureEnabledForTest(false);
637
641
 
638
642
  assert.strictEqual(sink.length, 1);
639
643
  assert.strictEqual(sink[0].fn, 'recordConsumedEvent');
@@ -677,6 +681,7 @@ async function testLargeTraceReturnValueIsCapturedInlineWithoutTruncation() {
677
681
  };
678
682
 
679
683
  const sink = [];
684
+ __reproTestHooks.setFullValueCaptureEnabledForTest(true);
680
685
  await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
681
686
  {
682
687
  type: 'exit',
@@ -689,6 +694,7 @@ async function testLargeTraceReturnValueIsCapturedInlineWithoutTruncation() {
689
694
  cfg,
690
695
  maskReq,
691
696
  );
697
+ __reproTestHooks.setFullValueCaptureEnabledForTest(false);
692
698
 
693
699
  assert.strictEqual(sink.length, 1);
694
700
  assert.strictEqual(sink[0].fn, 'toDrillResponse');
@@ -0,0 +1,35 @@
1
+ const assert = require('node:assert/strict');
2
+
3
+ const sdk = require('../dist/index.js');
4
+
5
+ async function main() {
6
+ const hooks = sdk.__reproTestHooks;
7
+ hooks.resetSdkBackgroundQueuesForTest();
8
+
9
+ const order = [];
10
+
11
+ hooks.scheduleSdkBackgroundWorkForTest(() => {
12
+ order.push('normal-1');
13
+ });
14
+ hooks.scheduleSdkBackgroundWorkForTest(() => {
15
+ order.push('normal-2');
16
+ });
17
+ hooks.scheduleSdkBackgroundWorkForTest(() => {
18
+ order.push('high-1');
19
+ }, { priority: 'high' });
20
+ hooks.scheduleSdkBackgroundWorkForTest(() => {
21
+ order.push('high-2');
22
+ }, { priority: 'high' });
23
+
24
+ await hooks.drainSdkBackgroundQueueForTest();
25
+
26
+ assert.deepStrictEqual(order, ['high-1', 'high-2', 'normal-1', 'normal-2']);
27
+
28
+ hooks.resetSdkBackgroundQueuesForTest();
29
+ console.log('sdk background priority OK');
30
+ }
31
+
32
+ main().catch((error) => {
33
+ console.error(error);
34
+ process.exit(1);
35
+ });
@@ -0,0 +1,58 @@
1
+ const assert = require('assert');
2
+ const { __reproTestHooks } = require('../dist');
3
+
4
+ function makeEvent(index, payload) {
5
+ return {
6
+ t: index,
7
+ type: 'enter',
8
+ fn: `fn${index}`,
9
+ file: '/app/src/controllers/subjects/index.ts',
10
+ line: index + 1,
11
+ depth: 1,
12
+ spanId: index + 1,
13
+ parentSpanId: null,
14
+ args: [payload],
15
+ };
16
+ }
17
+
18
+ function main() {
19
+ const payload = {
20
+ __kind: 'http-request',
21
+ method: 'POST',
22
+ url: '/api/v1/subject_visits/697001aaac70cc3d60f21273/subjects/createNewSubject?tenantId=tgtherapeutics',
23
+ headers: {
24
+ x_http_user: 'x'.repeat(12000),
25
+ authorization: '[dropped]',
26
+ },
27
+ body: {
28
+ value: 'y'.repeat(12000),
29
+ },
30
+ };
31
+
32
+ const events = Array.from({ length: 120 }, (_, index) => makeEvent(index, payload));
33
+ const batches = __reproTestHooks.chunkTraceEventsForTransportForTest(
34
+ events,
35
+ 'RID_test_trace_batch_size',
36
+ 'A_test_trace_batch_size',
37
+ );
38
+
39
+ assert(batches.length > 1, `expected more than one batch, got ${batches.length}`);
40
+
41
+ batches.forEach((batch, index) => {
42
+ const size = __reproTestHooks.estimateTraceBatchSerializedBytesForTest(
43
+ batch,
44
+ 'RID_test_trace_batch_size',
45
+ 'A_test_trace_batch_size',
46
+ index,
47
+ );
48
+ assert(
49
+ size <= 512 * 1024,
50
+ `batch ${index} too large: ${size}`,
51
+ );
52
+ });
53
+
54
+ // eslint-disable-next-line no-console
55
+ console.log('trace batch size chunking OK');
56
+ }
57
+
58
+ main();