@reproapp/node-sdk 0.0.9 → 0.0.12

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
@@ -67,9 +67,12 @@ type TraceEventRecord = {
67
67
  spanId?: string | number | null;
68
68
  parentSpanId?: string | number | null;
69
69
  args?: any;
70
+ argsValueCapture?: TraceValueCaptureRef;
70
71
  returnValue?: any;
72
+ returnValueCapture?: TraceValueCaptureRef;
71
73
  threw?: boolean;
72
74
  error?: any;
75
+ errorValueCapture?: TraceValueCaptureRef;
73
76
  unawaited?: boolean;
74
77
  };
75
78
  type PendingTraceEventRecord = TraceEventRecord & {
@@ -82,9 +85,14 @@ type PendingTraceEventRecord = TraceEventRecord & {
82
85
  __reproSourceFile?: string | null;
83
86
  __reproTraceValueEntries?: TraceValueBatchEntry[];
84
87
  };
88
+ type TraceValueCaptureRef = {
89
+ status: 'stored' | 'truncated-for-storage' | 'skipped-non-meaningful';
90
+ valueId?: string;
91
+ projectedKind?: string;
92
+ };
85
93
  type TraceValueBatchEntry = {
86
94
  id: string;
87
- target: 'db.pk' | 'db.before' | 'db.after' | 'db.query' | 'db.resultMeta' | 'db.error' | 'request.headers' | 'request.body' | 'request.params' | 'request.query' | 'response.body';
95
+ target: 'db.pk' | 'db.before' | 'db.after' | 'db.query' | 'db.resultMeta' | 'db.error' | 'request.headers' | 'request.body' | 'request.params' | 'request.query' | 'response.body' | 'trace.args' | 'trace.returnValue' | 'trace.error';
88
96
  value: any;
89
97
  truncatedForStorage?: boolean;
90
98
  projectedKind?: string;
@@ -254,6 +262,8 @@ export type ReproTracingInitOptions = TracerInitOpts & {
254
262
  export declare function initReproTracing(opts?: ReproTracingInitOptions): TracerApi | null;
255
263
  /** Optional helper if users want to check it. */
256
264
  export declare function isReproTracingEnabled(): boolean;
265
+ declare function sanitizeTraceValueForPrivacy(value: any): any;
266
+ declare function sanitizeMaterializedTraceValue(value: any): any;
257
267
  export declare function __materializePendingTraceEventsForWorker(payload: TraceMaterializationWorkerPayload): Promise<PendingTraceEventRecord[]>;
258
268
  type NormalizedMaskRule = {
259
269
  when?: ReproMaskWhen;
@@ -283,6 +293,7 @@ type InlinePrivacyMaterializationResult = {
283
293
  };
284
294
  };
285
295
  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>;
296
+ declare function getEventTraceValueEntries(event: TraceEventRecord | PendingTraceEventRecord): TraceValueBatchEntry[] | undefined;
286
297
  declare function estimateTraceBatchSerializedBytes(batch: TraceEventRecord[], requestRid: string, actionId: string | null | undefined, batchIndex: number): number;
287
298
  declare function chunkTraceEventsForTransport(events: TraceEventRecord[], requestRid: string, actionId: string | null | undefined): TraceEventRecord[][];
288
299
  export type ReproMiddlewareConfig = IngestClientConfig & {
@@ -344,6 +355,12 @@ export declare function initRepro(cfg: ReproInitConfig): Promise<void>;
344
355
  /** @internal Test hooks for runtime privacy policy wiring. */
345
356
  export declare const __reproTestHooks: {
346
357
  shareRuntimePrivacyStateForTest: typeof shareRuntimePrivacyState;
358
+ setFullValueCaptureEnabledForTest(enabled: boolean): void;
359
+ scheduleSdkBackgroundWorkForTest(work: () => Promise<void> | void, options?: {
360
+ priority?: 'high' | 'normal';
361
+ }): void;
362
+ drainSdkBackgroundQueueForTest(): Promise<void>;
363
+ resetSdkBackgroundQueuesForTest(): void;
347
364
  getRuntimePrivacyPolicyForTest(cfg: ReproMiddlewareConfig): NormalizedRuntimePrivacyPolicy | null;
348
365
  setRuntimePrivacyPolicyForTest(cfg: ReproMiddlewareConfig, policy: NormalizedRuntimePrivacyPolicy | null): void;
349
366
  recordKafkaTraceEventAsyncForTest: typeof recordKafkaTraceEventAsync;
@@ -352,5 +369,8 @@ export declare const __reproTestHooks: {
352
369
  wrapKafkaEachMessageHandlerForTest: typeof wrapKafkaEachMessageHandler;
353
370
  chunkTraceEventsForTransportForTest: typeof chunkTraceEventsForTransport;
354
371
  estimateTraceBatchSerializedBytesForTest: typeof estimateTraceBatchSerializedBytes;
372
+ sanitizeTraceValueForPrivacyForTest: typeof sanitizeTraceValueForPrivacy;
373
+ sanitizeMaterializedTraceValueForTest: typeof sanitizeMaterializedTraceValue;
374
+ getEventTraceValueEntriesForTest: typeof getEventTraceValueEntries;
355
375
  };
356
376
  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 {
@@ -2032,14 +2038,26 @@ function sanitizeTraceValue(value, depth = 0, seen = new WeakMap(), options = {}
2032
2038
  return circularReference(existingPath);
2033
2039
  seen.set(value, valuePath);
2034
2040
  if (!options.disableTruncation && depth >= TRACE_VALUE_MAX_DEPTH) {
2035
- const shallow = safeJson(value);
2036
- if (shallow !== undefined) {
2037
- return shallow;
2041
+ if (Array.isArray(value)) {
2042
+ return {
2043
+ __type: 'Array',
2044
+ length: value.length,
2045
+ __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
2046
+ };
2038
2047
  }
2039
2048
  const ctor = value?.constructor?.name;
2040
- return ctor && ctor !== 'Object'
2041
- ? { __class: ctor, __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}` }
2042
- : { __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;
2043
2061
  }
2044
2062
  if (Array.isArray(value)) {
2045
2063
  const sourceItems = options.disableTruncation ? value : value.slice(0, TRACE_VALUE_MAX_ITEMS);
@@ -2105,9 +2123,62 @@ function sanitizeTraceArgs(values) {
2105
2123
  function sanitizeTraceValueForPrivacy(value) {
2106
2124
  return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2107
2125
  }
2126
+ function sanitizeInlinePrivacyValue(value) {
2127
+ return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2128
+ }
2108
2129
  function sanitizeMaterializedTraceValue(value) {
2109
2130
  return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2110
2131
  }
2132
+ function compactMaterializedTracePreview(value, depth = 0) {
2133
+ if (value === undefined || value === null)
2134
+ return value;
2135
+ if (depth >= TRACE_VALUE_MAX_DEPTH) {
2136
+ return makeOmittedValue(`depth>${TRACE_VALUE_MAX_DEPTH}`);
2137
+ }
2138
+ const type = typeof value;
2139
+ if (type === 'string') {
2140
+ if (value.length <= TRACE_VALUE_MAX_STRING)
2141
+ return value;
2142
+ return value.slice(0, TRACE_VALUE_MAX_STRING);
2143
+ }
2144
+ if (type === 'number' || type === 'boolean')
2145
+ return value;
2146
+ if (type === 'bigint')
2147
+ return value.toString();
2148
+ if (type === 'symbol')
2149
+ return value.toString();
2150
+ if (type === 'function')
2151
+ return `[Function${value.name ? ` ${value.name}` : ''}]`;
2152
+ if (type !== 'object')
2153
+ return String(value);
2154
+ if (Array.isArray(value)) {
2155
+ return value
2156
+ .slice(0, TRACE_VALUE_MAX_ITEMS)
2157
+ .map((item) => compactMaterializedTracePreview(item, depth + 1));
2158
+ }
2159
+ const out = {};
2160
+ const keys = Object.keys(value).slice(0, TRACE_VALUE_MAX_KEYS);
2161
+ for (const key of keys) {
2162
+ try {
2163
+ out[key] = compactMaterializedTracePreview(value[key], depth + 1);
2164
+ }
2165
+ catch (err) {
2166
+ out[key] = `[Cannot serialize: ${err?.message || 'unknown error'}]`;
2167
+ }
2168
+ }
2169
+ return out;
2170
+ }
2171
+ function limitMaterializedTraceInlineValue(value) {
2172
+ const inlineValue = sanitizeMaterializedTraceValue(value);
2173
+ if (!traceInlineValueExceedsLimit(inlineValue)) {
2174
+ return inlineValue;
2175
+ }
2176
+ const compactPreview = compactMaterializedTracePreview(inlineValue);
2177
+ if (!traceInlineValueExceedsLimit(compactPreview)) {
2178
+ return compactPreview;
2179
+ }
2180
+ return makeOmittedValue('inline-size-limit');
2181
+ }
2111
2182
  function sanitizeTraceArgsForPrivacy(values) {
2112
2183
  if (!Array.isArray(values))
2113
2184
  return [sanitizeTraceValueForPrivacy(values)];
@@ -2121,6 +2192,14 @@ function normalizeRawTraceArgs(values) {
2121
2192
  function hasMeaningfulRawTraceError(error) {
2122
2193
  return error !== undefined && error !== null;
2123
2194
  }
2195
+ function attachTraceValueEntry(event, entry) {
2196
+ const current = event[TRACE_FULL_VALUE_ENTRY_SYMBOL];
2197
+ if (Array.isArray(current)) {
2198
+ current.push(entry);
2199
+ return;
2200
+ }
2201
+ event[TRACE_FULL_VALUE_ENTRY_SYMBOL] = [entry];
2202
+ }
2124
2203
  async function materializePendingTraceEvents(events, params) {
2125
2204
  for (const evt of events) {
2126
2205
  const pending = evt.__reproPending;
@@ -2130,14 +2209,71 @@ async function materializePendingTraceEvents(events, params) {
2130
2209
  const candidate = pending.candidate ?? traceEventCandidateFromRecord(evt);
2131
2210
  const omitJsonBuiltinValues = shouldOmitJsonBuiltinTraceValues(candidate);
2132
2211
  const activePrivacy = params.resolvePrivacy();
2212
+ const captureContext = {
2213
+ runtimeConfig: params.cfg,
2214
+ captureHeaders: params.cfg.captureHeaders,
2215
+ maskReq: params.maskReq,
2216
+ trace: candidate,
2217
+ masking: params.masking,
2218
+ privacy: activePrivacy,
2219
+ };
2220
+ const traceValueMetadata = {
2221
+ fn: evt.fn,
2222
+ file: evt.file,
2223
+ line: evt.line ?? null,
2224
+ spanId: evt.spanId ?? null,
2225
+ parentSpanId: evt.parentSpanId ?? null,
2226
+ };
2133
2227
  if (!omitJsonBuiltinValues && pending.argsRaw !== undefined) {
2134
- evt.args = sanitizeMaterializedTraceValue(await materializeTracePrivacyValueAsync('trace.args', sanitizeTraceArgsForPrivacy(pending.argsRaw), params.cfg, params.maskReq, candidate, params.masking, activePrivacy));
2228
+ const argsMaterialized = await materializeTracePrivacyValueAsync('trace.args', sanitizeTraceArgsForPrivacy(pending.argsRaw), params.cfg, params.maskReq, candidate, params.masking, activePrivacy);
2229
+ evt.args = limitMaterializedTraceInlineValue(argsMaterialized);
2230
+ const argsValueCapture = await maybeCaptureTraceValueAsync({
2231
+ target: 'trace.args',
2232
+ rawValue: pending.argsRaw,
2233
+ previewValue: evt.args,
2234
+ capture: captureContext,
2235
+ metadata: traceValueMetadata,
2236
+ });
2237
+ if (argsValueCapture?.entry) {
2238
+ attachTraceValueEntry(evt, argsValueCapture.entry);
2239
+ }
2240
+ if (argsValueCapture?.captureRef) {
2241
+ evt.argsValueCapture = argsValueCapture.captureRef;
2242
+ }
2135
2243
  }
2136
2244
  if (!omitJsonBuiltinValues && pending.returnValueRaw !== undefined) {
2137
- evt.returnValue = sanitizeMaterializedTraceValue(await materializeTracePrivacyValueAsync('trace.returnValue', sanitizeTraceValueForPrivacy(pending.returnValueRaw), params.cfg, params.maskReq, candidate, params.masking, activePrivacy));
2245
+ const returnValueMaterialized = await materializeTracePrivacyValueAsync('trace.returnValue', sanitizeTraceValueForPrivacy(pending.returnValueRaw), params.cfg, params.maskReq, candidate, params.masking, activePrivacy);
2246
+ evt.returnValue = limitMaterializedTraceInlineValue(returnValueMaterialized);
2247
+ const returnValueCapture = await maybeCaptureTraceValueAsync({
2248
+ target: 'trace.returnValue',
2249
+ rawValue: pending.returnValueRaw,
2250
+ previewValue: evt.returnValue,
2251
+ capture: captureContext,
2252
+ metadata: traceValueMetadata,
2253
+ });
2254
+ if (returnValueCapture?.entry) {
2255
+ attachTraceValueEntry(evt, returnValueCapture.entry);
2256
+ }
2257
+ if (returnValueCapture?.captureRef) {
2258
+ evt.returnValueCapture = returnValueCapture.captureRef;
2259
+ }
2138
2260
  }
2139
2261
  if (pending.errorRaw !== undefined) {
2140
- evt.error = sanitizeMaterializedTraceValue(await materializeTracePrivacyValueAsync('trace.error', sanitizeTraceValueForPrivacy(pending.errorRaw), params.cfg, params.maskReq, candidate, params.masking, activePrivacy));
2262
+ const errorMaterialized = await materializeTracePrivacyValueAsync('trace.error', sanitizeTraceValueForPrivacy(pending.errorRaw), params.cfg, params.maskReq, candidate, params.masking, activePrivacy);
2263
+ evt.error = limitMaterializedTraceInlineValue(errorMaterialized);
2264
+ const errorValueCapture = await maybeCaptureTraceValueAsync({
2265
+ target: 'trace.error',
2266
+ rawValue: pending.errorRaw,
2267
+ previewValue: evt.error,
2268
+ capture: captureContext,
2269
+ metadata: traceValueMetadata,
2270
+ });
2271
+ if (errorValueCapture?.entry) {
2272
+ attachTraceValueEntry(evt, errorValueCapture.entry);
2273
+ }
2274
+ if (errorValueCapture?.captureRef) {
2275
+ evt.errorValueCapture = errorValueCapture.captureRef;
2276
+ }
2141
2277
  }
2142
2278
  }
2143
2279
  return events;
@@ -3689,6 +3825,12 @@ async function maybeCaptureRequestValueAsync(params, sink) {
3689
3825
  }
3690
3826
  return result.captureRef;
3691
3827
  }
3828
+ function maybeCaptureTraceValue(params) {
3829
+ return createCapturedValueEntry(params);
3830
+ }
3831
+ async function maybeCaptureTraceValueAsync(params) {
3832
+ return await createCapturedValueEntryAsync(params);
3833
+ }
3692
3834
  function maybeCaptureDbValue(params, sink) {
3693
3835
  const result = createCapturedValueEntry(params);
3694
3836
  if (!result)
@@ -3860,7 +4002,7 @@ function reproMiddleware(cfg) {
3860
4002
  }
3861
4003
  catch { }
3862
4004
  if (flushPayload) {
3863
- const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload);
4005
+ const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload, { priority: 'high' });
3864
4006
  if (sessionDrainWait) {
3865
4007
  sessionDrainWait.then(scheduleFlushPayload).catch(scheduleFlushPayload);
3866
4008
  }
@@ -5176,7 +5318,7 @@ function cloneKafkaTelemetryValue(value) {
5176
5318
  }
5177
5319
  }
5178
5320
  async function materializeKafkaRequestBody(cfg, maskReq, body) {
5179
- return materializeInlinePrivacyValueAsync('request.body', sanitizeTraceValueForPrivacy(body), cfg, maskReq, null, normalizeMaskingConfig(cfg.masking), getRuntimePrivacyState(cfg).policy ?? null);
5321
+ return materializeInlinePrivacyValueAsync('request.body', sanitizeInlinePrivacyValue(body), cfg, maskReq, null, normalizeMaskingConfig(cfg.masking), getRuntimePrivacyState(cfg).policy ?? null);
5180
5322
  }
5181
5323
  function summarizeKafkaError(error) {
5182
5324
  const message = typeof error?.message === 'string'
@@ -5267,14 +5409,71 @@ function recordKafkaTraceEvent(raw, sink, cfg, maskReq) {
5267
5409
  };
5268
5410
  const privacy = getRuntimePrivacyState(cfg).policy ?? null;
5269
5411
  const masking = normalizeMaskingConfig(cfg.masking);
5412
+ const captureContext = {
5413
+ runtimeConfig: cfg,
5414
+ captureHeaders: cfg.captureHeaders,
5415
+ maskReq,
5416
+ trace: candidate,
5417
+ masking,
5418
+ privacy,
5419
+ };
5420
+ const traceValueMetadata = {
5421
+ fn: evt.fn,
5422
+ file: evt.file,
5423
+ line: evt.line ?? null,
5424
+ spanId: evt.spanId ?? null,
5425
+ parentSpanId: evt.parentSpanId ?? null,
5426
+ };
5270
5427
  if (!omitJsonBuiltinValues && raw.args !== undefined) {
5271
- evt.args = sanitizeMaterializedTraceValue(materializeTracePrivacyValue('trace.args', sanitizeTraceArgsForPrivacy(raw.args), cfg, maskReq, candidate, masking, privacy));
5428
+ const argsMaterialized = materializeTracePrivacyValue('trace.args', sanitizeTraceArgsForPrivacy(raw.args), cfg, maskReq, candidate, masking, privacy);
5429
+ evt.args = limitMaterializedTraceInlineValue(argsMaterialized);
5430
+ const argsValueCapture = maybeCaptureTraceValue({
5431
+ target: 'trace.args',
5432
+ rawValue: normalizeRawTraceArgs(raw.args),
5433
+ previewValue: evt.args,
5434
+ capture: captureContext,
5435
+ metadata: traceValueMetadata,
5436
+ });
5437
+ if (argsValueCapture?.entry) {
5438
+ attachTraceValueEntry(evt, argsValueCapture.entry);
5439
+ }
5440
+ if (argsValueCapture?.captureRef) {
5441
+ evt.argsValueCapture = argsValueCapture.captureRef;
5442
+ }
5272
5443
  }
5273
5444
  if (!omitJsonBuiltinValues && raw.returnValue !== undefined) {
5274
- evt.returnValue = sanitizeMaterializedTraceValue(materializeTracePrivacyValue('trace.returnValue', sanitizeTraceValueForPrivacy(raw.returnValue), cfg, maskReq, candidate, masking, privacy));
5445
+ const returnValueMaterialized = materializeTracePrivacyValue('trace.returnValue', sanitizeTraceValueForPrivacy(raw.returnValue), cfg, maskReq, candidate, masking, privacy);
5446
+ evt.returnValue = limitMaterializedTraceInlineValue(returnValueMaterialized);
5447
+ const returnValueCapture = maybeCaptureTraceValue({
5448
+ target: 'trace.returnValue',
5449
+ rawValue: raw.returnValue,
5450
+ previewValue: evt.returnValue,
5451
+ capture: captureContext,
5452
+ metadata: traceValueMetadata,
5453
+ });
5454
+ if (returnValueCapture?.entry) {
5455
+ attachTraceValueEntry(evt, returnValueCapture.entry);
5456
+ }
5457
+ if (returnValueCapture?.captureRef) {
5458
+ evt.returnValueCapture = returnValueCapture.captureRef;
5459
+ }
5275
5460
  }
5276
5461
  if (hasMeaningfulRawTraceError(raw.error)) {
5277
- evt.error = sanitizeMaterializedTraceValue(materializeTracePrivacyValue('trace.error', sanitizeTraceValueForPrivacy(raw.error), cfg, maskReq, candidate, masking, privacy));
5462
+ const errorMaterialized = materializeTracePrivacyValue('trace.error', sanitizeTraceValueForPrivacy(raw.error), cfg, maskReq, candidate, masking, privacy);
5463
+ evt.error = limitMaterializedTraceInlineValue(errorMaterialized);
5464
+ const errorValueCapture = maybeCaptureTraceValue({
5465
+ target: 'trace.error',
5466
+ rawValue: raw.error,
5467
+ previewValue: evt.error,
5468
+ capture: captureContext,
5469
+ metadata: traceValueMetadata,
5470
+ });
5471
+ if (errorValueCapture?.entry) {
5472
+ attachTraceValueEntry(evt, errorValueCapture.entry);
5473
+ }
5474
+ if (errorValueCapture?.captureRef) {
5475
+ evt.errorValueCapture = errorValueCapture.captureRef;
5476
+ }
5278
5477
  }
5279
5478
  if (raw.threw !== undefined)
5280
5479
  evt.threw = raw.threw === true;
@@ -5367,6 +5566,7 @@ function buildKafkaTraceEntries(actionId, requestRid, events) {
5367
5566
  return batches.map((batch, index) => ({
5368
5567
  actionId: actionId ?? null,
5369
5568
  trace: batch,
5569
+ traceValues: collectBatchTraceValueEntries(batch, index),
5370
5570
  traceBatch: {
5371
5571
  rid: requestRid,
5372
5572
  index,
@@ -5404,7 +5604,7 @@ function scheduleKafkaTraceFlush(cfg, kafkaCtx, actionId, requestRid, rawTraceEv
5404
5604
  post(cfg, sid, { entries });
5405
5605
  }
5406
5606
  });
5407
- });
5607
+ }, { priority: 'high' });
5408
5608
  }
5409
5609
  function patchKafkaProducerInstance(producer, cfg) {
5410
5610
  if (!producer || producer.__repro_kafka_producer_patched)
@@ -6667,6 +6867,31 @@ exports.initRepro = initRepro;
6667
6867
  /** @internal Test hooks for runtime privacy policy wiring. */
6668
6868
  exports.__reproTestHooks = {
6669
6869
  shareRuntimePrivacyStateForTest: shareRuntimePrivacyState,
6870
+ setFullValueCaptureEnabledForTest(enabled) {
6871
+ __FULL_VALUE_CAPTURE_ENABLED = enabled === true;
6872
+ },
6873
+ scheduleSdkBackgroundWorkForTest(work, options) {
6874
+ scheduleSdkBackgroundWork(work, options);
6875
+ },
6876
+ async drainSdkBackgroundQueueForTest() {
6877
+ await drainSdkBackgroundQueue();
6878
+ },
6879
+ resetSdkBackgroundQueuesForTest() {
6880
+ sdkBackgroundHighQueue.length = 0;
6881
+ sdkBackgroundQueue.length = 0;
6882
+ if (sdkBackgroundTimer) {
6883
+ try {
6884
+ clearTimeout(sdkBackgroundTimer);
6885
+ }
6886
+ catch { }
6887
+ sdkBackgroundTimer = null;
6888
+ }
6889
+ sdkBackgroundDraining = false;
6890
+ activeClientRequestCount = 0;
6891
+ oldestActiveClientRequestAt = null;
6892
+ lastClientActivityAt = 0;
6893
+ sdkBackgroundQuietUntil = 0;
6894
+ },
6670
6895
  getRuntimePrivacyPolicyForTest(cfg) {
6671
6896
  return getRuntimePrivacyState(cfg).policy;
6672
6897
  },
@@ -6679,4 +6904,7 @@ exports.__reproTestHooks = {
6679
6904
  wrapKafkaEachMessageHandlerForTest: wrapKafkaEachMessageHandler,
6680
6905
  chunkTraceEventsForTransportForTest: chunkTraceEventsForTransport,
6681
6906
  estimateTraceBatchSerializedBytesForTest: estimateTraceBatchSerializedBytes,
6907
+ sanitizeTraceValueForPrivacyForTest: sanitizeTraceValueForPrivacy,
6908
+ sanitizeMaterializedTraceValueForTest: sanitizeMaterializedTraceValue,
6909
+ getEventTraceValueEntriesForTest: getEventTraceValueEntries,
6682
6910
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.9",
3
+ "version": "0.0.12",
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 test/trace-batch-size.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
@@ -351,9 +351,12 @@ type TraceEventRecord = {
351
351
  spanId?: string | number | null;
352
352
  parentSpanId?: string | number | null;
353
353
  args?: any;
354
+ argsValueCapture?: TraceValueCaptureRef;
354
355
  returnValue?: any;
356
+ returnValueCapture?: TraceValueCaptureRef;
355
357
  threw?: boolean;
356
358
  error?: any;
359
+ errorValueCapture?: TraceValueCaptureRef;
357
360
  unawaited?: boolean;
358
361
  };
359
362
 
@@ -387,7 +390,10 @@ type TraceValueBatchEntry = {
387
390
  | 'request.body'
388
391
  | 'request.params'
389
392
  | 'request.query'
390
- | 'response.body';
393
+ | 'response.body'
394
+ | 'trace.args'
395
+ | 'trace.returnValue'
396
+ | 'trace.error';
391
397
  value: any;
392
398
  truncatedForStorage?: boolean;
393
399
  projectedKind?: string;
@@ -1231,6 +1237,7 @@ let activeClientRequestCount = 0;
1231
1237
  let oldestActiveClientRequestAt: number | null = null;
1232
1238
  let lastClientActivityAt = 0;
1233
1239
  let sdkBackgroundQuietUntil = 0;
1240
+ const sdkBackgroundHighQueue: Array<() => Promise<void> | void> = [];
1234
1241
  const sdkBackgroundQueue: Array<() => Promise<void> | void> = [];
1235
1242
  let sdkBackgroundTimer: NodeJS.Timeout | null = null;
1236
1243
  let sdkBackgroundDraining = false;
@@ -1499,8 +1506,15 @@ function endClientRequest(): void {
1499
1506
  }
1500
1507
  }
1501
1508
 
1502
- function scheduleSdkBackgroundWork(work: () => Promise<void> | void): void {
1503
- sdkBackgroundQueue.push(work);
1509
+ function scheduleSdkBackgroundWork(
1510
+ work: () => Promise<void> | void,
1511
+ options: { priority?: 'high' | 'normal' } = {},
1512
+ ): void {
1513
+ if (options.priority === 'high') {
1514
+ sdkBackgroundHighQueue.push(work);
1515
+ } else {
1516
+ sdkBackgroundQueue.push(work);
1517
+ }
1504
1518
  scheduleSdkBackgroundDrain();
1505
1519
  }
1506
1520
 
@@ -1549,12 +1563,12 @@ async function drainSdkBackgroundQueue(): Promise<void> {
1549
1563
 
1550
1564
  sdkBackgroundDraining = true;
1551
1565
  try {
1552
- while (sdkBackgroundQueue.length > 0) {
1566
+ while (sdkBackgroundHighQueue.length > 0 || sdkBackgroundQueue.length > 0) {
1553
1567
  if (shouldDeferSdkBackgroundWork()) {
1554
1568
  scheduleSdkBackgroundDrain();
1555
1569
  return;
1556
1570
  }
1557
- const work = sdkBackgroundQueue.shift();
1571
+ const work = sdkBackgroundHighQueue.shift() ?? sdkBackgroundQueue.shift();
1558
1572
  if (!work) continue;
1559
1573
  try {
1560
1574
  await drainQueuedIngestBeforeBackgroundWork();
@@ -1565,7 +1579,7 @@ async function drainSdkBackgroundQueue(): Promise<void> {
1565
1579
  }
1566
1580
  } finally {
1567
1581
  sdkBackgroundDraining = false;
1568
- if (sdkBackgroundQueue.length > 0) {
1582
+ if (sdkBackgroundHighQueue.length > 0 || sdkBackgroundQueue.length > 0) {
1569
1583
  scheduleSdkBackgroundDrain();
1570
1584
  } else {
1571
1585
  kickIngestQueueDrain();
@@ -2429,14 +2443,26 @@ function sanitizeTraceValue(
2429
2443
  seen.set(value, valuePath);
2430
2444
 
2431
2445
  if (!options.disableTruncation && depth >= TRACE_VALUE_MAX_DEPTH) {
2432
- const shallow = safeJson(value);
2433
- if (shallow !== undefined) {
2434
- return shallow;
2446
+ if (Array.isArray(value)) {
2447
+ return {
2448
+ __type: 'Array',
2449
+ length: value.length,
2450
+ __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
2451
+ };
2435
2452
  }
2436
2453
  const ctor = value?.constructor?.name;
2437
- return ctor && ctor !== 'Object'
2438
- ? { __class: ctor, __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}` }
2439
- : { __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}` };
2454
+ const keys = Object.keys(value);
2455
+ const summary: Record<string, any> = {
2456
+ __truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
2457
+ __keys: keys.slice(0, TRACE_VALUE_MAX_KEYS),
2458
+ };
2459
+ if (keys.length > TRACE_VALUE_MAX_KEYS) {
2460
+ summary.__truncatedKeys = keys.length - TRACE_VALUE_MAX_KEYS;
2461
+ }
2462
+ if (ctor && ctor !== 'Object') {
2463
+ summary.__class = ctor;
2464
+ }
2465
+ return summary;
2440
2466
  }
2441
2467
 
2442
2468
  if (Array.isArray(value)) {
@@ -2505,10 +2531,61 @@ function sanitizeTraceValueForPrivacy(value: any): any {
2505
2531
  return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2506
2532
  }
2507
2533
 
2534
+ function sanitizeInlinePrivacyValue(value: any): any {
2535
+ return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2536
+ }
2537
+
2508
2538
  function sanitizeMaterializedTraceValue(value: any): any {
2509
2539
  return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2510
2540
  }
2511
2541
 
2542
+ function compactMaterializedTracePreview(value: any, depth = 0): any {
2543
+ if (value === undefined || value === null) return value;
2544
+ if (depth >= TRACE_VALUE_MAX_DEPTH) {
2545
+ return makeOmittedValue(`depth>${TRACE_VALUE_MAX_DEPTH}`);
2546
+ }
2547
+
2548
+ const type = typeof value;
2549
+ if (type === 'string') {
2550
+ if (value.length <= TRACE_VALUE_MAX_STRING) return value;
2551
+ return value.slice(0, TRACE_VALUE_MAX_STRING);
2552
+ }
2553
+ if (type === 'number' || type === 'boolean') return value;
2554
+ if (type === 'bigint') return value.toString();
2555
+ if (type === 'symbol') return value.toString();
2556
+ if (type === 'function') return `[Function${value.name ? ` ${value.name}` : ''}]`;
2557
+ if (type !== 'object') return String(value);
2558
+
2559
+ if (Array.isArray(value)) {
2560
+ return value
2561
+ .slice(0, TRACE_VALUE_MAX_ITEMS)
2562
+ .map((item) => compactMaterializedTracePreview(item, depth + 1));
2563
+ }
2564
+
2565
+ const out: Record<string, any> = {};
2566
+ const keys = Object.keys(value).slice(0, TRACE_VALUE_MAX_KEYS);
2567
+ for (const key of keys) {
2568
+ try {
2569
+ out[key] = compactMaterializedTracePreview((value as Record<string, any>)[key], depth + 1);
2570
+ } catch (err) {
2571
+ out[key] = `[Cannot serialize: ${(err as Error)?.message || 'unknown error'}]`;
2572
+ }
2573
+ }
2574
+ return out;
2575
+ }
2576
+
2577
+ function limitMaterializedTraceInlineValue(value: any): any {
2578
+ const inlineValue = sanitizeMaterializedTraceValue(value);
2579
+ if (!traceInlineValueExceedsLimit(inlineValue)) {
2580
+ return inlineValue;
2581
+ }
2582
+ const compactPreview = compactMaterializedTracePreview(inlineValue);
2583
+ if (!traceInlineValueExceedsLimit(compactPreview)) {
2584
+ return compactPreview;
2585
+ }
2586
+ return makeOmittedValue('inline-size-limit');
2587
+ }
2588
+
2512
2589
  function sanitizeTraceArgsForPrivacy(values: any): any {
2513
2590
  if (!Array.isArray(values)) return [sanitizeTraceValueForPrivacy(values)];
2514
2591
  return values.map(v => sanitizeTraceValueForPrivacy(v));
@@ -2523,6 +2600,18 @@ function hasMeaningfulRawTraceError(error: any): boolean {
2523
2600
  return error !== undefined && error !== null;
2524
2601
  }
2525
2602
 
2603
+ function attachTraceValueEntry(
2604
+ event: TraceEventRecord | PendingTraceEventRecord,
2605
+ entry: TraceValueBatchEntry,
2606
+ ): void {
2607
+ const current = (event as any)[TRACE_FULL_VALUE_ENTRY_SYMBOL] as TraceValueBatchEntry[] | undefined;
2608
+ if (Array.isArray(current)) {
2609
+ current.push(entry);
2610
+ return;
2611
+ }
2612
+ (event as any)[TRACE_FULL_VALUE_ENTRY_SYMBOL] = [entry];
2613
+ }
2614
+
2526
2615
  async function materializePendingTraceEvents(
2527
2616
  events: PendingTraceEventRecord[],
2528
2617
  params: {
@@ -2540,9 +2629,24 @@ async function materializePendingTraceEvents(
2540
2629
  const candidate = pending.candidate ?? traceEventCandidateFromRecord(evt);
2541
2630
  const omitJsonBuiltinValues = shouldOmitJsonBuiltinTraceValues(candidate);
2542
2631
  const activePrivacy = params.resolvePrivacy();
2632
+ const captureContext: FullValueCaptureContext = {
2633
+ runtimeConfig: params.cfg,
2634
+ captureHeaders: params.cfg.captureHeaders,
2635
+ maskReq: params.maskReq,
2636
+ trace: candidate,
2637
+ masking: params.masking,
2638
+ privacy: activePrivacy,
2639
+ };
2640
+ const traceValueMetadata = {
2641
+ fn: evt.fn,
2642
+ file: evt.file,
2643
+ line: evt.line ?? null,
2644
+ spanId: evt.spanId ?? null,
2645
+ parentSpanId: evt.parentSpanId ?? null,
2646
+ };
2543
2647
 
2544
2648
  if (!omitJsonBuiltinValues && pending.argsRaw !== undefined) {
2545
- evt.args = sanitizeMaterializedTraceValue(await materializeTracePrivacyValueAsync(
2649
+ const argsMaterialized = await materializeTracePrivacyValueAsync(
2546
2650
  'trace.args',
2547
2651
  sanitizeTraceArgsForPrivacy(pending.argsRaw),
2548
2652
  params.cfg,
@@ -2550,10 +2654,24 @@ async function materializePendingTraceEvents(
2550
2654
  candidate,
2551
2655
  params.masking,
2552
2656
  activePrivacy,
2553
- ));
2657
+ );
2658
+ evt.args = limitMaterializedTraceInlineValue(argsMaterialized);
2659
+ const argsValueCapture = await maybeCaptureTraceValueAsync({
2660
+ target: 'trace.args',
2661
+ rawValue: pending.argsRaw,
2662
+ previewValue: evt.args,
2663
+ capture: captureContext,
2664
+ metadata: traceValueMetadata,
2665
+ });
2666
+ if (argsValueCapture?.entry) {
2667
+ attachTraceValueEntry(evt, argsValueCapture.entry);
2668
+ }
2669
+ if (argsValueCapture?.captureRef) {
2670
+ evt.argsValueCapture = argsValueCapture.captureRef;
2671
+ }
2554
2672
  }
2555
2673
  if (!omitJsonBuiltinValues && pending.returnValueRaw !== undefined) {
2556
- evt.returnValue = sanitizeMaterializedTraceValue(await materializeTracePrivacyValueAsync(
2674
+ const returnValueMaterialized = await materializeTracePrivacyValueAsync(
2557
2675
  'trace.returnValue',
2558
2676
  sanitizeTraceValueForPrivacy(pending.returnValueRaw),
2559
2677
  params.cfg,
@@ -2561,10 +2679,24 @@ async function materializePendingTraceEvents(
2561
2679
  candidate,
2562
2680
  params.masking,
2563
2681
  activePrivacy,
2564
- ));
2682
+ );
2683
+ evt.returnValue = limitMaterializedTraceInlineValue(returnValueMaterialized);
2684
+ const returnValueCapture = await maybeCaptureTraceValueAsync({
2685
+ target: 'trace.returnValue',
2686
+ rawValue: pending.returnValueRaw,
2687
+ previewValue: evt.returnValue,
2688
+ capture: captureContext,
2689
+ metadata: traceValueMetadata,
2690
+ });
2691
+ if (returnValueCapture?.entry) {
2692
+ attachTraceValueEntry(evt, returnValueCapture.entry);
2693
+ }
2694
+ if (returnValueCapture?.captureRef) {
2695
+ evt.returnValueCapture = returnValueCapture.captureRef;
2696
+ }
2565
2697
  }
2566
2698
  if (pending.errorRaw !== undefined) {
2567
- evt.error = sanitizeMaterializedTraceValue(await materializeTracePrivacyValueAsync(
2699
+ const errorMaterialized = await materializeTracePrivacyValueAsync(
2568
2700
  'trace.error',
2569
2701
  sanitizeTraceValueForPrivacy(pending.errorRaw),
2570
2702
  params.cfg,
@@ -2572,7 +2704,21 @@ async function materializePendingTraceEvents(
2572
2704
  candidate,
2573
2705
  params.masking,
2574
2706
  activePrivacy,
2575
- ));
2707
+ );
2708
+ evt.error = limitMaterializedTraceInlineValue(errorMaterialized);
2709
+ const errorValueCapture = await maybeCaptureTraceValueAsync({
2710
+ target: 'trace.error',
2711
+ rawValue: pending.errorRaw,
2712
+ previewValue: evt.error,
2713
+ capture: captureContext,
2714
+ metadata: traceValueMetadata,
2715
+ });
2716
+ if (errorValueCapture?.entry) {
2717
+ attachTraceValueEntry(evt, errorValueCapture.entry);
2718
+ }
2719
+ if (errorValueCapture?.captureRef) {
2720
+ evt.errorValueCapture = errorValueCapture.captureRef;
2721
+ }
2576
2722
  }
2577
2723
  }
2578
2724
  return events;
@@ -4301,7 +4447,10 @@ function createCapturedValueEntry(
4301
4447
  | 'request.body'
4302
4448
  | 'request.params'
4303
4449
  | 'request.query'
4304
- | 'response.body';
4450
+ | 'response.body'
4451
+ | 'trace.args'
4452
+ | 'trace.returnValue'
4453
+ | 'trace.error';
4305
4454
  rawValue: any;
4306
4455
  previewValue: any;
4307
4456
  capture: FullValueCaptureContext;
@@ -4380,7 +4529,10 @@ async function createCapturedValueEntryAsync(
4380
4529
  | 'request.body'
4381
4530
  | 'request.params'
4382
4531
  | 'request.query'
4383
- | 'response.body';
4532
+ | 'response.body'
4533
+ | 'trace.args'
4534
+ | 'trace.returnValue'
4535
+ | 'trace.error';
4384
4536
  rawValue: any;
4385
4537
  previewValue: any;
4386
4538
  capture: FullValueCaptureContext;
@@ -4480,6 +4632,30 @@ async function maybeCaptureRequestValueAsync(
4480
4632
  return result.captureRef;
4481
4633
  }
4482
4634
 
4635
+ function maybeCaptureTraceValue(
4636
+ params: {
4637
+ target: 'trace.args' | 'trace.returnValue' | 'trace.error';
4638
+ rawValue: any;
4639
+ previewValue: any;
4640
+ capture: FullValueCaptureContext;
4641
+ metadata?: Partial<TraceValueBatchEntry>;
4642
+ },
4643
+ ): { captureRef: TraceValueCaptureRef; entry?: TraceValueBatchEntry } | undefined {
4644
+ return createCapturedValueEntry(params);
4645
+ }
4646
+
4647
+ async function maybeCaptureTraceValueAsync(
4648
+ params: {
4649
+ target: 'trace.args' | 'trace.returnValue' | 'trace.error';
4650
+ rawValue: any;
4651
+ previewValue: any;
4652
+ capture: FullValueCaptureContext;
4653
+ metadata?: Partial<TraceValueBatchEntry>;
4654
+ },
4655
+ ): Promise<{ captureRef: TraceValueCaptureRef; entry?: TraceValueBatchEntry } | undefined> {
4656
+ return await createCapturedValueEntryAsync(params);
4657
+ }
4658
+
4483
4659
  function maybeCaptureDbValue(
4484
4660
  params: {
4485
4661
  target: 'db.pk' | 'db.before' | 'db.after' | 'db.query' | 'db.resultMeta' | 'db.error';
@@ -4674,11 +4850,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4674
4850
  return;
4675
4851
  }
4676
4852
  }
4677
- flushed = true;
4678
- clearTimers();
4853
+ flushed = true;
4854
+ clearTimers();
4679
4855
  try { unsubscribe && unsubscribe(); } catch {}
4680
4856
  if (flushPayload) {
4681
- const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload!);
4857
+ const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload!, { priority: 'high' });
4682
4858
  if (sessionDrainWait) {
4683
4859
  sessionDrainWait.then(scheduleFlushPayload).catch(scheduleFlushPayload);
4684
4860
  } else {
@@ -6083,7 +6259,7 @@ async function materializeKafkaRequestBody(
6083
6259
  ): Promise<InlinePrivacyMaterializationResult> {
6084
6260
  return materializeInlinePrivacyValueAsync(
6085
6261
  'request.body',
6086
- sanitizeTraceValueForPrivacy(body),
6262
+ sanitizeInlinePrivacyValue(body),
6087
6263
  cfg,
6088
6264
  maskReq,
6089
6265
  null,
@@ -6198,8 +6374,23 @@ function recordKafkaTraceEvent(
6198
6374
 
6199
6375
  const privacy = getRuntimePrivacyState(cfg).policy ?? null;
6200
6376
  const masking = normalizeMaskingConfig(cfg.masking);
6377
+ const captureContext: FullValueCaptureContext = {
6378
+ runtimeConfig: cfg,
6379
+ captureHeaders: cfg.captureHeaders,
6380
+ maskReq,
6381
+ trace: candidate,
6382
+ masking,
6383
+ privacy,
6384
+ };
6385
+ const traceValueMetadata = {
6386
+ fn: evt.fn,
6387
+ file: evt.file,
6388
+ line: evt.line ?? null,
6389
+ spanId: evt.spanId ?? null,
6390
+ parentSpanId: evt.parentSpanId ?? null,
6391
+ };
6201
6392
  if (!omitJsonBuiltinValues && raw.args !== undefined) {
6202
- evt.args = sanitizeMaterializedTraceValue(materializeTracePrivacyValue(
6393
+ const argsMaterialized = materializeTracePrivacyValue(
6203
6394
  'trace.args',
6204
6395
  sanitizeTraceArgsForPrivacy(raw.args),
6205
6396
  cfg,
@@ -6207,10 +6398,24 @@ function recordKafkaTraceEvent(
6207
6398
  candidate,
6208
6399
  masking,
6209
6400
  privacy,
6210
- ));
6401
+ );
6402
+ evt.args = limitMaterializedTraceInlineValue(argsMaterialized);
6403
+ const argsValueCapture = maybeCaptureTraceValue({
6404
+ target: 'trace.args',
6405
+ rawValue: normalizeRawTraceArgs(raw.args),
6406
+ previewValue: evt.args,
6407
+ capture: captureContext,
6408
+ metadata: traceValueMetadata,
6409
+ });
6410
+ if (argsValueCapture?.entry) {
6411
+ attachTraceValueEntry(evt, argsValueCapture.entry);
6412
+ }
6413
+ if (argsValueCapture?.captureRef) {
6414
+ evt.argsValueCapture = argsValueCapture.captureRef;
6415
+ }
6211
6416
  }
6212
6417
  if (!omitJsonBuiltinValues && raw.returnValue !== undefined) {
6213
- evt.returnValue = sanitizeMaterializedTraceValue(materializeTracePrivacyValue(
6418
+ const returnValueMaterialized = materializeTracePrivacyValue(
6214
6419
  'trace.returnValue',
6215
6420
  sanitizeTraceValueForPrivacy(raw.returnValue),
6216
6421
  cfg,
@@ -6218,10 +6423,24 @@ function recordKafkaTraceEvent(
6218
6423
  candidate,
6219
6424
  masking,
6220
6425
  privacy,
6221
- ));
6426
+ );
6427
+ evt.returnValue = limitMaterializedTraceInlineValue(returnValueMaterialized);
6428
+ const returnValueCapture = maybeCaptureTraceValue({
6429
+ target: 'trace.returnValue',
6430
+ rawValue: raw.returnValue,
6431
+ previewValue: evt.returnValue,
6432
+ capture: captureContext,
6433
+ metadata: traceValueMetadata,
6434
+ });
6435
+ if (returnValueCapture?.entry) {
6436
+ attachTraceValueEntry(evt, returnValueCapture.entry);
6437
+ }
6438
+ if (returnValueCapture?.captureRef) {
6439
+ evt.returnValueCapture = returnValueCapture.captureRef;
6440
+ }
6222
6441
  }
6223
6442
  if (hasMeaningfulRawTraceError(raw.error)) {
6224
- evt.error = sanitizeMaterializedTraceValue(materializeTracePrivacyValue(
6443
+ const errorMaterialized = materializeTracePrivacyValue(
6225
6444
  'trace.error',
6226
6445
  sanitizeTraceValueForPrivacy(raw.error),
6227
6446
  cfg,
@@ -6229,7 +6448,21 @@ function recordKafkaTraceEvent(
6229
6448
  candidate,
6230
6449
  masking,
6231
6450
  privacy,
6232
- ));
6451
+ );
6452
+ evt.error = limitMaterializedTraceInlineValue(errorMaterialized);
6453
+ const errorValueCapture = maybeCaptureTraceValue({
6454
+ target: 'trace.error',
6455
+ rawValue: raw.error,
6456
+ previewValue: evt.error,
6457
+ capture: captureContext,
6458
+ metadata: traceValueMetadata,
6459
+ });
6460
+ if (errorValueCapture?.entry) {
6461
+ attachTraceValueEntry(evt, errorValueCapture.entry);
6462
+ }
6463
+ if (errorValueCapture?.captureRef) {
6464
+ evt.errorValueCapture = errorValueCapture.captureRef;
6465
+ }
6233
6466
  }
6234
6467
  if (raw.threw !== undefined) evt.threw = raw.threw === true;
6235
6468
  if (raw.unawaited !== undefined) evt.unawaited = raw.unawaited === true;
@@ -6338,6 +6571,7 @@ function buildKafkaTraceEntries(
6338
6571
  return batches.map((batch, index) => ({
6339
6572
  actionId: actionId ?? null,
6340
6573
  trace: batch,
6574
+ traceValues: collectBatchTraceValueEntries(batch, index),
6341
6575
  traceBatch: {
6342
6576
  rid: requestRid,
6343
6577
  index,
@@ -6381,7 +6615,7 @@ function scheduleKafkaTraceFlush(
6381
6615
  post(cfg, sid, { entries });
6382
6616
  }
6383
6617
  });
6384
- });
6618
+ }, { priority: 'high' });
6385
6619
  }
6386
6620
 
6387
6621
  function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
@@ -7686,6 +7920,31 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
7686
7920
  /** @internal Test hooks for runtime privacy policy wiring. */
7687
7921
  export const __reproTestHooks = {
7688
7922
  shareRuntimePrivacyStateForTest: shareRuntimePrivacyState,
7923
+ setFullValueCaptureEnabledForTest(enabled: boolean): void {
7924
+ __FULL_VALUE_CAPTURE_ENABLED = enabled === true;
7925
+ },
7926
+ scheduleSdkBackgroundWorkForTest(
7927
+ work: () => Promise<void> | void,
7928
+ options?: { priority?: 'high' | 'normal' },
7929
+ ): void {
7930
+ scheduleSdkBackgroundWork(work, options);
7931
+ },
7932
+ async drainSdkBackgroundQueueForTest(): Promise<void> {
7933
+ await drainSdkBackgroundQueue();
7934
+ },
7935
+ resetSdkBackgroundQueuesForTest(): void {
7936
+ sdkBackgroundHighQueue.length = 0;
7937
+ sdkBackgroundQueue.length = 0;
7938
+ if (sdkBackgroundTimer) {
7939
+ try { clearTimeout(sdkBackgroundTimer); } catch {}
7940
+ sdkBackgroundTimer = null;
7941
+ }
7942
+ sdkBackgroundDraining = false;
7943
+ activeClientRequestCount = 0;
7944
+ oldestActiveClientRequestAt = null;
7945
+ lastClientActivityAt = 0;
7946
+ sdkBackgroundQuietUntil = 0;
7947
+ },
7689
7948
  getRuntimePrivacyPolicyForTest(cfg: ReproMiddlewareConfig): NormalizedRuntimePrivacyPolicy | null {
7690
7949
  return getRuntimePrivacyState(cfg).policy;
7691
7950
  },
@@ -7701,4 +7960,7 @@ export const __reproTestHooks = {
7701
7960
  wrapKafkaEachMessageHandlerForTest: wrapKafkaEachMessageHandler,
7702
7961
  chunkTraceEventsForTransportForTest: chunkTraceEventsForTransport,
7703
7962
  estimateTraceBatchSerializedBytesForTest: estimateTraceBatchSerializedBytes,
7963
+ sanitizeTraceValueForPrivacyForTest: sanitizeTraceValueForPrivacy,
7964
+ sanitizeMaterializedTraceValueForTest: sanitizeMaterializedTraceValue,
7965
+ getEventTraceValueEntriesForTest: getEventTraceValueEntries,
7704
7966
  };
@@ -419,7 +419,19 @@ async function testKafkaProducerCapturesFullPublishedMessageValue() {
419
419
  }
420
420
 
421
421
  function assertNoLargeJsonPreviewArtifacts(value) {
422
- const text = JSON.stringify(value);
422
+ const text = JSON.stringify(value, (key, currentValue) => {
423
+ if (
424
+ key === 'argsValueCapture' ||
425
+ key === 'returnValueCapture' ||
426
+ key === 'errorValueCapture' ||
427
+ key === 'argsMaterialization' ||
428
+ key === 'returnValueMaterialization' ||
429
+ key === 'errorMaterialization'
430
+ ) {
431
+ return undefined;
432
+ }
433
+ return currentValue;
434
+ });
423
435
  assert(!text.includes('__type":"json-string'), text);
424
436
  assert(!text.includes('__truncatedKeys'), text);
425
437
  assert(!text.includes('__truncatedItems'), text);
@@ -540,7 +552,7 @@ async function testJsonBuiltinTracePayloadsAreOmitted() {
540
552
  assertNoLargeJsonPreviewArtifacts(consumedSink[0]);
541
553
  }
542
554
 
543
- async function testLargeStringTraceValuesAreCapturedInlineWithoutTruncation() {
555
+ async function testLargeStringTraceValuesStayIngestibleAndCaptureFullValueOutOfBand() {
544
556
  const hugeMarker = 'HUGE_INLINE_VALUE_MARKER';
545
557
  const hugeJson = JSON.stringify({
546
558
  eventId: 'evt-huge',
@@ -567,6 +579,7 @@ async function testLargeStringTraceValuesAreCapturedInlineWithoutTruncation() {
567
579
  };
568
580
 
569
581
  const sink = [];
582
+ __reproTestHooks.setFullValueCaptureEnabledForTest(true);
570
583
  await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
571
584
  {
572
585
  type: 'enter',
@@ -579,18 +592,24 @@ async function testLargeStringTraceValuesAreCapturedInlineWithoutTruncation() {
579
592
  cfg,
580
593
  maskReq,
581
594
  );
595
+ __reproTestHooks.setFullValueCaptureEnabledForTest(false);
582
596
 
583
597
  assert.strictEqual(sink.length, 1);
584
598
  assert.strictEqual(sink[0].fn, 'parseGatewayPayload');
585
599
  assert.strictEqual(typeof sink[0].args[0], 'string');
586
600
  assert(sink[0].args[0].includes(hugeMarker), JSON.stringify(sink[0]));
587
- assert(sink[0].args[0].includes('x'.repeat(140 * 1024)), JSON.stringify(sink[0]));
588
- assert.strictEqual(sink[0].argsMaterialization, undefined, JSON.stringify(sink[0]));
589
- assert.strictEqual(sink[0].argsValueCapture, undefined, JSON.stringify(sink[0]));
590
- assertNoLargeJsonPreviewArtifacts(sink[0]);
601
+ assert(sink[0].args[0].length < 4096, JSON.stringify(sink[0]));
602
+ assert.strictEqual(sink[0].argsValueCapture?.status, 'stored', JSON.stringify(sink[0]));
603
+ const entries = __reproTestHooks.getEventTraceValueEntriesForTest(sink[0]);
604
+ assert(Array.isArray(entries) && entries.length === 1, JSON.stringify(entries));
605
+ assert.strictEqual(entries[0].target, 'trace.args');
606
+ assert.strictEqual(typeof entries[0].value?.[0], 'string');
607
+ assert(entries[0].value[0].includes(hugeMarker), JSON.stringify(entries[0]));
608
+ assert(entries[0].value[0].includes('x'.repeat(140 * 1024)), JSON.stringify(entries[0]));
609
+ assertNoLargeJsonPreviewArtifacts(sink[0].args);
591
610
  }
592
611
 
593
- async function testLargeStructuredTraceValuesAreCapturedInlineWithoutTruncation() {
612
+ async function testLargeStructuredTraceValuesStayIngestibleAndCaptureFullValueOutOfBand() {
594
613
  const hugeMarker = 'HUGE_STRUCTURED_TRACE_VALUE_MARKER';
595
614
  const hugeEnvelope = {
596
615
  eventId: 'evt-huge-structured',
@@ -622,6 +641,7 @@ async function testLargeStructuredTraceValuesAreCapturedInlineWithoutTruncation(
622
641
  };
623
642
 
624
643
  const sink = [];
644
+ __reproTestHooks.setFullValueCaptureEnabledForTest(true);
625
645
  await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
626
646
  {
627
647
  type: 'enter',
@@ -634,22 +654,33 @@ async function testLargeStructuredTraceValuesAreCapturedInlineWithoutTruncation(
634
654
  cfg,
635
655
  maskReq,
636
656
  );
657
+ __reproTestHooks.setFullValueCaptureEnabledForTest(false);
637
658
 
638
659
  assert.strictEqual(sink.length, 1);
639
660
  assert.strictEqual(sink[0].fn, 'recordConsumedEvent');
640
- assert(sink[0].args, 'expected args to be stored inline');
641
- assert.strictEqual(sink[0].args[0].providerResponse.rawNarrative, `${hugeMarker}:${'x'.repeat(40 * 1024)}`);
661
+ assert(sink[0].args, 'expected args preview to be stored inline');
662
+ assert(JSON.stringify(sink[0].args).length < 9000, JSON.stringify(sink[0]).slice(0, 12000));
663
+ assert.strictEqual(
664
+ sink[0].args[0]?.providerResponse?.rawNarrative?.__kind,
665
+ 'omitted',
666
+ JSON.stringify(sink[0]),
667
+ );
642
668
  assert.strictEqual(typeof sink[0].args[2], 'string');
643
- const parsedRawArg = JSON.parse(sink[0].args[2]);
669
+ assert(sink[0].args[2].includes('providerResponse'), JSON.stringify(sink[0]));
670
+ assert.strictEqual(sink[0].argsValueCapture?.status, 'stored', JSON.stringify(sink[0]));
671
+ const entries = __reproTestHooks.getEventTraceValueEntriesForTest(sink[0]);
672
+ assert(Array.isArray(entries) && entries.length === 1, JSON.stringify(entries));
673
+ assert.strictEqual(entries[0].target, 'trace.args');
674
+ assert.strictEqual(entries[0].value?.[0]?.providerResponse?.rawNarrative, `${hugeMarker}:${'x'.repeat(40 * 1024)}`);
675
+ assert.strictEqual(typeof entries[0].value?.[2], 'string');
676
+ const parsedRawArg = JSON.parse(entries[0].value[2]);
644
677
  assert.strictEqual(parsedRawArg.providerResponse.rawNarrative, `${hugeMarker}:${'x'.repeat(40 * 1024)}`);
645
678
  assert.strictEqual(parsedRawArg.providerResponse.credentials.apiKey, '[dropped]');
646
679
  assert.strictEqual(parsedRawArg.providerResponse.credentials.webhookSecret, '[dropped]');
647
- assert.strictEqual(sink[0].argsMaterialization, undefined, JSON.stringify(sink[0]));
648
- assert.strictEqual(sink[0].argsValueCapture, undefined, JSON.stringify(sink[0]));
649
- assertNoLargeJsonPreviewArtifacts(sink[0]);
680
+ assertNoLargeJsonPreviewArtifacts(sink[0].args);
650
681
  }
651
682
 
652
- async function testLargeTraceReturnValueIsCapturedInlineWithoutTruncation() {
683
+ async function testLargeTraceReturnValueStaysIngestibleAndCapturesFullValueOutOfBand() {
653
684
  const hugeMarker = 'HUGE_STRUCTURED_TRACE_RETURN_MARKER';
654
685
  const hugeResponse = {
655
686
  id: 'drill-1',
@@ -677,6 +708,7 @@ async function testLargeTraceReturnValueIsCapturedInlineWithoutTruncation() {
677
708
  };
678
709
 
679
710
  const sink = [];
711
+ __reproTestHooks.setFullValueCaptureEnabledForTest(true);
680
712
  await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
681
713
  {
682
714
  type: 'exit',
@@ -689,14 +721,21 @@ async function testLargeTraceReturnValueIsCapturedInlineWithoutTruncation() {
689
721
  cfg,
690
722
  maskReq,
691
723
  );
724
+ __reproTestHooks.setFullValueCaptureEnabledForTest(false);
692
725
 
693
726
  assert.strictEqual(sink.length, 1);
694
727
  assert.strictEqual(sink[0].fn, 'toDrillResponse');
695
- assert(sink[0].returnValue, 'expected return value to be stored inline');
696
- assert.strictEqual(sink[0].returnValue.structuredSnapshot.payload, `${hugeMarker}:${'x'.repeat(40 * 1024)}`);
697
- assert.strictEqual(sink[0].returnValueMaterialization, undefined, JSON.stringify(sink[0]));
698
- assert.strictEqual(sink[0].returnValueCapture, undefined, JSON.stringify(sink[0]));
699
- assertNoLargeJsonPreviewArtifacts(sink[0]);
728
+ assert(sink[0].returnValue, 'expected return value preview to be stored inline');
729
+ assert(JSON.stringify(sink[0].returnValue).length < 9000, JSON.stringify(sink[0]).slice(0, 12000));
730
+ assert.strictEqual(typeof sink[0].returnValue?.structuredSnapshot?.payload, 'string');
731
+ assert(sink[0].returnValue.structuredSnapshot.payload.includes(hugeMarker), JSON.stringify(sink[0]));
732
+ assert(sink[0].returnValue.structuredSnapshot.payload.length < 4096, JSON.stringify(sink[0]));
733
+ assert.strictEqual(sink[0].returnValueCapture?.status, 'stored', JSON.stringify(sink[0]));
734
+ const entries = __reproTestHooks.getEventTraceValueEntriesForTest(sink[0]);
735
+ assert(Array.isArray(entries) && entries.length === 1, JSON.stringify(entries));
736
+ assert.strictEqual(entries[0].target, 'trace.returnValue');
737
+ assert.strictEqual(entries[0].value?.structuredSnapshot?.payload, `${hugeMarker}:${'x'.repeat(40 * 1024)}`);
738
+ assertNoLargeJsonPreviewArtifacts(sink[0].returnValue);
700
739
  }
701
740
 
702
741
  async function main() {
@@ -706,9 +745,9 @@ async function main() {
706
745
  await testKafkaTraceFlushIsNotBlockedForeverByActiveHttpRequest();
707
746
  await testLargeHttpResponseBodyIsCapturedWithoutDroppingRequest();
708
747
  await testJsonBuiltinTracePayloadsAreOmitted();
709
- await testLargeStringTraceValuesAreCapturedInlineWithoutTruncation();
710
- await testLargeStructuredTraceValuesAreCapturedInlineWithoutTruncation();
711
- await testLargeTraceReturnValueIsCapturedInlineWithoutTruncation();
748
+ await testLargeStringTraceValuesStayIngestibleAndCaptureFullValueOutOfBand();
749
+ await testLargeStructuredTraceValuesStayIngestibleAndCaptureFullValueOutOfBand();
750
+ await testLargeTraceReturnValueStaysIngestibleAndCapturesFullValueOutOfBand();
712
751
  console.log('kafka runtime privacy policy wiring OK');
713
752
  }
714
753
 
@@ -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
+ });