@reproapp/node-sdk 0.0.4 → 0.0.6

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.
@@ -94,7 +94,7 @@ async function main() {
94
94
  }
95
95
  });
96
96
  });
97
- await new Promise((resolve) => setTimeout(resolve, 120));
97
+ await new Promise((resolve) => setTimeout(resolve, 500));
98
98
 
99
99
  const exitEvent = collectTraceEvents(posts).find((event) => event?.fn === 'circularFn' && event?.type === 'exit');
100
100
  assert(exitEvent, 'expected circularFn exit trace');
@@ -121,7 +121,7 @@ async function runScenario(disableRules) {
121
121
  });
122
122
 
123
123
  // Allow flush timers to send payloads.
124
- await new Promise(r => setTimeout(r, 120));
124
+ await new Promise(r => setTimeout(r, 500));
125
125
 
126
126
  return collectTraceFnsFromPosts(posts);
127
127
  }
@@ -1,7 +1,10 @@
1
1
  const assert = require('assert');
2
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '50';
3
+
4
+ const { EventEmitter } = require('events');
2
5
  const { normalizeRuntimePrivacyPolicy } = require('../dist/privacy');
3
6
  const { flushIngestQueue } = require('../dist/ingest/client');
4
- const { initReproTracing, __reproTestHooks } = require('../dist');
7
+ const { initReproTracing, reproMiddleware, __reproTestHooks } = require('../dist');
5
8
 
6
9
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
10
 
@@ -162,7 +165,13 @@ async function testKafkaConsumerTraceUsesConsumeTraceId() {
162
165
  offset: '42',
163
166
  timestamp: '1778965503962',
164
167
  key: Buffer.from('case-1'),
165
- value: Buffer.from('hello'),
168
+ value: Buffer.from(JSON.stringify({
169
+ eventId: 'evt-kafka-consumed',
170
+ payload: {
171
+ marker: 'FULL_KAFKA_CONSUMED_VALUE_MARKER',
172
+ repeated: 'x'.repeat(12 * 1024),
173
+ },
174
+ })),
166
175
  headers: {
167
176
  'x-bug-session-id': Buffer.from('S_kafka_trace'),
168
177
  'x-bug-action-id': Buffer.from('A_kafka_trace'),
@@ -183,6 +192,13 @@ async function testKafkaConsumerTraceUsesConsumeTraceId() {
183
192
  assert(consumeRequest, 'expected Kafka consume request event');
184
193
  assert.strictEqual(consumeRequest.payload.request.body.parentTraceId, 'parent-http-trace');
185
194
  assert.strictEqual(consumeRequest.payload.request.body.parentRequestRid, 'parent-request-rid');
195
+ assert.strictEqual(consumeRequest.payload.request.body.messageKey, 'case-1');
196
+ assert.strictEqual(consumeRequest.payload.request.body.value.eventId, 'evt-kafka-consumed');
197
+ assert.strictEqual(consumeRequest.payload.request.body.value.payload.marker, 'FULL_KAFKA_CONSUMED_VALUE_MARKER');
198
+ assert.strictEqual(consumeRequest.payload.request.body.value.payload.repeated, 'x'.repeat(12 * 1024));
199
+ assert(consumeRequest.payload.request.body.rawValue.includes('FULL_KAFKA_CONSUMED_VALUE_MARKER'));
200
+ assert.strictEqual(consumeRequest.payload.request.body.message.value.payload.repeated, 'x'.repeat(12 * 1024));
201
+ assertNoLargeJsonPreviewArtifacts(consumeRequest.payload.request.body);
186
202
 
187
203
  const traceFns = events
188
204
  .filter((event) => event.event_type === 'trace_batch')
@@ -195,17 +211,246 @@ async function testKafkaConsumerTraceUsesConsumeTraceId() {
195
211
  }
196
212
  }
197
213
 
198
- async function testOversizedKafkaTraceArgsKeepBoundedPreview() {
199
- const secret = 'sk_live_privacy_large_preview_secret';
214
+ function makeOpenTaggedReqRes(sessionId, actionId) {
215
+ const req = new EventEmitter();
216
+ req.method = 'GET';
217
+ req.url = '/long-running';
218
+ req.headers = {
219
+ 'x-bug-session-id': sessionId,
220
+ 'x-bug-action-id': actionId,
221
+ 'x-bug-request-start': String(Date.now()),
222
+ };
223
+ req.body = {};
224
+ req.params = {};
225
+ req.query = {};
226
+
227
+ const res = new EventEmitter();
228
+ res.statusCode = 200;
229
+ res.getHeader = () => undefined;
230
+ res.setHeader = () => {};
231
+ res.json = function (body) { this.body = body; this.emit('finish'); return body; };
232
+ res.send = function (body) { this.body = body; this.emit('finish'); return body; };
233
+ res.write = () => true;
234
+ res.end = () => { res.emit('finish'); return true; };
235
+
236
+ return { req, res };
237
+ }
238
+
239
+ async function testKafkaTraceFlushIsNotBlockedForeverByActiveHttpRequest() {
240
+ const tracer = initReproTracing({ instrument: false, functionLogs: false });
241
+ const capturedBodies = [];
242
+ const originalFetch = global.fetch;
243
+ global.fetch = async (_url, init) => {
244
+ capturedBodies.push(JSON.parse(String(init?.body ?? '{}')));
245
+ return { ok: true };
246
+ };
247
+
248
+ const cfg = {
249
+ tenantId: 'TENANT_test',
250
+ appId: 'APP_test',
251
+ appSecret: 'secret',
252
+ captureHeaders: false,
253
+ privacy: { environment: 'dev' },
254
+ ingestBase: 'http://127.0.0.1:65535',
255
+ };
256
+ const { req, res } = makeOpenTaggedReqRes('S_stuck_http', 'A_stuck_http');
257
+
258
+ try {
259
+ await new Promise((resolve, reject) => {
260
+ void reproMiddleware(cfg)(req, res, (err) => {
261
+ if (err) reject(err);
262
+ else resolve();
263
+ });
264
+ });
265
+
266
+ const wrapped = __reproTestHooks.wrapKafkaEachMessageHandlerForTest(
267
+ async () => {
268
+ tracer.tracer.enter('backgroundConsumerWork', { file: '/app/consumer.ts', line: 30 }, { args: ['ok'] });
269
+ tracer.tracer.exit(
270
+ { fn: 'backgroundConsumerWork', file: '/app/consumer.ts', line: 30 },
271
+ { returnValue: 'done' },
272
+ );
273
+ },
274
+ cfg,
275
+ { __repro_group_id: 'group-a' },
276
+ );
277
+
278
+ await wrapped({
279
+ topic: 'privacy-lab.events',
280
+ partition: 0,
281
+ message: {
282
+ offset: '43',
283
+ timestamp: '1778965503963',
284
+ key: Buffer.from('case-2'),
285
+ value: Buffer.from('hello'),
286
+ headers: {
287
+ 'x-bug-session-id': Buffer.from('S_kafka_background_flush'),
288
+ 'x-bug-action-id': Buffer.from('A_kafka_background_flush'),
289
+ },
290
+ },
291
+ });
292
+
293
+ await waitFor(() => capturedBodies.some((body) => {
294
+ const events = Array.isArray(body.events) ? body.events : [];
295
+ return events.some((event) => {
296
+ if (event.event_type !== 'trace_batch') return false;
297
+ const trace = Array.isArray(event.payload?.trace) ? event.payload.trace : [];
298
+ return trace.some((traceEvent) => traceEvent.fn === 'backgroundConsumerWork');
299
+ });
300
+ }));
301
+ } finally {
302
+ res.emit('close');
303
+ await sleep(100);
304
+ await flushIngestQueue();
305
+ global.fetch = originalFetch;
306
+ }
307
+ }
308
+
309
+ async function testLargeHttpResponseBodyIsCapturedWithoutDroppingRequest() {
310
+ const marker = 'OVERSIZED_HTTP_RESPONSE_MARKER';
311
+ const capturedBodies = [];
312
+ const originalFetch = global.fetch;
313
+ global.fetch = async (_url, init) => {
314
+ capturedBodies.push(JSON.parse(String(init?.body ?? '{}')));
315
+ return { ok: true };
316
+ };
317
+
318
+ const cfg = {
319
+ tenantId: 'TENANT_test',
320
+ appId: 'APP_test',
321
+ appSecret: 'secret',
322
+ captureHeaders: false,
323
+ privacy: { environment: 'dev' },
324
+ ingestBase: 'http://127.0.0.1:65535',
325
+ };
326
+ const { req, res } = makeOpenTaggedReqRes('S_large_response', 'A_large_response');
327
+ req.url = '/large-response';
328
+
329
+ try {
330
+ await new Promise((resolve, reject) => {
331
+ void reproMiddleware(cfg)(req, res, (err) => {
332
+ if (err) reject(err);
333
+ else resolve();
334
+ });
335
+ });
336
+ res.json({
337
+ ok: true,
338
+ payload: `${marker}:${'x'.repeat(12 * 1024)}`,
339
+ });
340
+
341
+ await waitFor(() => capturedBodies.some((body) => {
342
+ const text = JSON.stringify(body);
343
+ return text.includes('"event_type":"backend_request"') &&
344
+ text.includes(marker);
345
+ }));
346
+ const text = JSON.stringify(capturedBodies);
347
+ assert(text.includes(marker), text);
348
+ assert(!text.includes('respBodyMaterialization'), text);
349
+ assert(!text.includes('inline_value_omitted_too_large'), text);
350
+ assertNoLargeJsonPreviewArtifacts(capturedBodies);
351
+ } finally {
352
+ await flushIngestQueue();
353
+ global.fetch = originalFetch;
354
+ }
355
+ }
356
+
357
+ async function testKafkaProducerCapturesFullPublishedMessageValue() {
358
+ const capturedBodies = [];
359
+ const originalFetch = global.fetch;
360
+ global.fetch = async (_url, init) => {
361
+ capturedBodies.push(JSON.parse(String(init?.body ?? '{}')));
362
+ return { ok: true };
363
+ };
364
+
365
+ try {
366
+ const cfg = {
367
+ tenantId: 'TENANT_test',
368
+ appId: 'APP_test',
369
+ appSecret: 'secret',
370
+ captureHeaders: false,
371
+ privacy: { environment: 'dev' },
372
+ ingestBase: 'http://127.0.0.1:65535',
373
+ resolveContext: () => ({ sid: 'S_kafka_publish', aid: 'A_kafka_publish' }),
374
+ };
375
+ const producer = {
376
+ async send(payload) {
377
+ return [{ topicName: payload.topic, partition: 0, baseOffset: '12', errorCode: 0 }];
378
+ },
379
+ };
380
+ __reproTestHooks.patchKafkaProducerInstanceForTest(producer, cfg);
381
+
382
+ await producer.send({
383
+ topic: 'privacy-lab.events',
384
+ messages: [
385
+ {
386
+ key: Buffer.from('case-published'),
387
+ value: Buffer.from(JSON.stringify({
388
+ eventId: 'evt-kafka-published',
389
+ payload: {
390
+ marker: 'FULL_KAFKA_PUBLISHED_VALUE_MARKER',
391
+ repeated: 'y'.repeat(12 * 1024),
392
+ },
393
+ })),
394
+ },
395
+ ],
396
+ });
397
+
398
+ await waitFor(() => capturedBodies.some((body) => {
399
+ const text = JSON.stringify(body);
400
+ return text.includes('"event_type":"backend_request"') &&
401
+ text.includes('FULL_KAFKA_PUBLISHED_VALUE_MARKER');
402
+ }));
403
+ await flushIngestQueue();
404
+
405
+ const events = capturedBodies.flatMap((body) => Array.isArray(body.events) ? body.events : []);
406
+ const publishRequest = events.find((event) => event.event_type === 'backend_request');
407
+ assert(publishRequest, 'expected Kafka publish request event');
408
+ const message = publishRequest.payload.request.body.messages[0];
409
+ assert.strictEqual(message.key, 'case-published');
410
+ assert.strictEqual(message.value.eventId, 'evt-kafka-published');
411
+ assert.strictEqual(message.value.payload.marker, 'FULL_KAFKA_PUBLISHED_VALUE_MARKER');
412
+ assert.strictEqual(message.value.payload.repeated, 'y'.repeat(12 * 1024));
413
+ assert(message.rawValue.includes('FULL_KAFKA_PUBLISHED_VALUE_MARKER'));
414
+ assertNoLargeJsonPreviewArtifacts(publishRequest.payload.request.body);
415
+ } finally {
416
+ await flushIngestQueue();
417
+ global.fetch = originalFetch;
418
+ }
419
+ }
420
+
421
+ function assertNoLargeJsonPreviewArtifacts(value) {
422
+ const text = JSON.stringify(value);
423
+ assert(!text.includes('__type":"json-string'), text);
424
+ assert(!text.includes('__truncatedKeys'), text);
425
+ assert(!text.includes('__truncatedItems'), text);
426
+ assert(!text.includes('__truncatedEntries'), text);
427
+ assert(!text.includes('"__truncated"'), text);
428
+ assert(!text.includes('more chars'), text);
429
+ assert(!text.includes('more items'), text);
430
+ assert(!text.includes('Materialization'), text);
431
+ assert(!text.includes('ValueCapture'), text);
432
+ }
433
+
434
+ function assertJsonBuiltinPayloadOmitted(event) {
435
+ assert.strictEqual(event.args, undefined, JSON.stringify(event));
436
+ assert.strictEqual(event.returnValue, undefined, JSON.stringify(event));
437
+ assert.strictEqual(event.argsMaterialization, undefined, JSON.stringify(event));
438
+ assert.strictEqual(event.returnValueMaterialization, undefined, JSON.stringify(event));
439
+ assert.strictEqual(event.argsValueCapture, undefined, JSON.stringify(event));
440
+ assert.strictEqual(event.returnValueCapture, undefined, JSON.stringify(event));
441
+ }
442
+
443
+ async function testJsonBuiltinTracePayloadsAreOmitted() {
200
444
  const largeJson = JSON.stringify({
201
445
  eventId: 'evt-large',
202
446
  eventType: 'privacy-lab.case.observed',
447
+ wideObject: Object.fromEntries(Array.from({ length: 30 }, (_, index) => [`k${index}`, `v${index}`])),
203
448
  providerResponse: {
204
449
  credentials: {
205
- apiKey: secret,
206
- webhookSecret: 'whsec_large_preview_secret',
450
+ apiKey: 'not-a-real-key-for-test',
451
+ webhookSecret: 'not-a-real-webhook-secret-for-test',
207
452
  },
208
- rawNarrative: `large narrative ${'x'.repeat(9000)}`,
453
+ rawNarrative: `large narrative ${'x'.repeat(2500)}`,
209
454
  },
210
455
  structuredSnapshot: {
211
456
  subject: {
@@ -242,12 +487,29 @@ async function testOversizedKafkaTraceArgsKeepBoundedPreview() {
242
487
  );
243
488
 
244
489
  assert.strictEqual(parseSink.length, 1);
245
- assert(parseSink[0].args, 'expected oversized JSON.parse args to keep a bounded preview');
246
- assert.strictEqual(parseSink[0].args[0].__type, 'json-string');
247
- assert(parseSink[0].args[0].parsed.providerResponse, JSON.stringify(parseSink[0].args[0]));
248
- assert.strictEqual(parseSink[0].argsMaterialization.reason, 'inline_value_too_large');
249
- assert.strictEqual(parseSink[0].argsMaterialization.preview, 'bounded');
250
- assert(!JSON.stringify(parseSink[0]).includes(secret), JSON.stringify(parseSink[0]));
490
+ assert.strictEqual(parseSink[0].fn, 'JSON.parse');
491
+ assertJsonBuiltinPayloadOmitted(parseSink[0]);
492
+ assertNoLargeJsonPreviewArtifacts(parseSink[0]);
493
+
494
+ const stringifySink = [];
495
+ await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
496
+ {
497
+ type: 'exit',
498
+ fn: 'JSON.stringify',
499
+ file: '/app/privacy-lab-drills.service.ts',
500
+ line: 79,
501
+ args: [envelope],
502
+ returnValue: largeJson,
503
+ },
504
+ stringifySink,
505
+ cfg,
506
+ maskReq,
507
+ );
508
+
509
+ assert.strictEqual(stringifySink.length, 1);
510
+ assert.strictEqual(stringifySink[0].fn, 'JSON.stringify');
511
+ assertJsonBuiltinPayloadOmitted(stringifySink[0]);
512
+ assertNoLargeJsonPreviewArtifacts(stringifySink[0]);
251
513
 
252
514
  const consumedSink = [];
253
515
  await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
@@ -264,18 +526,189 @@ async function testOversizedKafkaTraceArgsKeepBoundedPreview() {
264
526
  );
265
527
 
266
528
  assert.strictEqual(consumedSink.length, 1);
267
- assert(consumedSink[0].args, 'expected oversized recordConsumedEvent args to keep a bounded preview');
529
+ assert(consumedSink[0].args, 'expected recordConsumedEvent args to be captured normally');
530
+ assert.strictEqual(consumedSink[0].argsMaterialization, undefined);
268
531
  assert.strictEqual(consumedSink[0].args[1], 'privacy-lab-case');
532
+ assert.strictEqual(consumedSink[0].args[0].wideObject.k29, 'v29');
269
533
  assert(consumedSink[0].args[0].providerResponse, JSON.stringify(consumedSink[0].args[0]));
270
- assert.strictEqual(consumedSink[0].args[2].__type, 'json-string');
271
- assert.strictEqual(consumedSink[0].argsMaterialization.reason, 'inline_value_too_large');
272
- assert(!JSON.stringify(consumedSink[0]).includes(secret), JSON.stringify(consumedSink[0]));
534
+ assert.strictEqual(typeof consumedSink[0].args[2], 'string');
535
+ const parsedConsumedRawMessage = JSON.parse(consumedSink[0].args[2]);
536
+ assert.strictEqual(parsedConsumedRawMessage.wideObject.k29, 'v29');
537
+ assert.strictEqual(parsedConsumedRawMessage.providerResponse.credentials.apiKey, '[dropped]');
538
+ assert.strictEqual(parsedConsumedRawMessage.providerResponse.credentials.webhookSecret, '[dropped]');
539
+ assert.strictEqual(parsedConsumedRawMessage.providerResponse.rawNarrative, envelope.providerResponse.rawNarrative);
540
+ assertNoLargeJsonPreviewArtifacts(consumedSink[0]);
541
+ }
542
+
543
+ async function testLargeStringTraceValuesAreCapturedInlineWithoutTruncation() {
544
+ const hugeMarker = 'HUGE_INLINE_VALUE_MARKER';
545
+ const hugeJson = JSON.stringify({
546
+ eventId: 'evt-huge',
547
+ eventType: 'privacy-lab.case.observed',
548
+ providerResponse: {
549
+ credentials: {
550
+ apiKey: 'not-a-real-key-for-huge-test',
551
+ webhookSecret: 'not-a-real-webhook-secret-for-huge-test',
552
+ },
553
+ rawNarrative: `${hugeMarker}:${'x'.repeat(140 * 1024)}`,
554
+ },
555
+ });
556
+ const cfg = {
557
+ tenantId: 'TENANT_test',
558
+ appId: 'APP_test',
559
+ appSecret: 'secret',
560
+ captureHeaders: false,
561
+ privacy: { environment: 'dev' },
562
+ };
563
+ const maskReq = {
564
+ method: 'KAFKA_CONSUME',
565
+ path: 'kafka://privacy-lab.events',
566
+ key: 'KAFKA_CONSUME privacy-lab.events',
567
+ };
568
+
569
+ const sink = [];
570
+ await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
571
+ {
572
+ type: 'enter',
573
+ fn: 'parseGatewayPayload',
574
+ file: '/app/privacy-lab-drills.service.ts',
575
+ line: 77,
576
+ args: [hugeJson],
577
+ },
578
+ sink,
579
+ cfg,
580
+ maskReq,
581
+ );
582
+
583
+ assert.strictEqual(sink.length, 1);
584
+ assert.strictEqual(sink[0].fn, 'parseGatewayPayload');
585
+ assert.strictEqual(typeof sink[0].args[0], 'string');
586
+ 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]);
591
+ }
592
+
593
+ async function testLargeStructuredTraceValuesAreCapturedInlineWithoutTruncation() {
594
+ const hugeMarker = 'HUGE_STRUCTURED_TRACE_VALUE_MARKER';
595
+ const hugeEnvelope = {
596
+ eventId: 'evt-huge-structured',
597
+ eventType: 'privacy-lab.case.observed',
598
+ providerResponse: {
599
+ credentials: {
600
+ apiKey: 'not-a-real-key-for-structured-test',
601
+ webhookSecret: 'not-a-real-webhook-secret-for-structured-test',
602
+ },
603
+ rawNarrative: `${hugeMarker}:${'x'.repeat(40 * 1024)}`,
604
+ },
605
+ structuredSnapshot: {
606
+ subject: {
607
+ email: 'avery.debugson@example.com',
608
+ },
609
+ },
610
+ };
611
+ const cfg = {
612
+ tenantId: 'TENANT_test',
613
+ appId: 'APP_test',
614
+ appSecret: 'secret',
615
+ captureHeaders: false,
616
+ privacy: { environment: 'dev' },
617
+ };
618
+ const maskReq = {
619
+ method: 'KAFKA_CONSUME',
620
+ path: 'kafka://privacy-lab.events',
621
+ key: 'KAFKA_CONSUME privacy-lab.events',
622
+ };
623
+
624
+ const sink = [];
625
+ await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
626
+ {
627
+ type: 'enter',
628
+ fn: 'recordConsumedEvent',
629
+ file: '/app/privacy-lab-drills.service.ts',
630
+ line: 78,
631
+ args: [hugeEnvelope, 'privacy-lab-case', JSON.stringify(hugeEnvelope)],
632
+ },
633
+ sink,
634
+ cfg,
635
+ maskReq,
636
+ );
637
+
638
+ assert.strictEqual(sink.length, 1);
639
+ 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)}`);
642
+ assert.strictEqual(typeof sink[0].args[2], 'string');
643
+ const parsedRawArg = JSON.parse(sink[0].args[2]);
644
+ assert.strictEqual(parsedRawArg.providerResponse.rawNarrative, `${hugeMarker}:${'x'.repeat(40 * 1024)}`);
645
+ assert.strictEqual(parsedRawArg.providerResponse.credentials.apiKey, '[dropped]');
646
+ 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]);
650
+ }
651
+
652
+ async function testLargeTraceReturnValueIsCapturedInlineWithoutTruncation() {
653
+ const hugeMarker = 'HUGE_STRUCTURED_TRACE_RETURN_MARKER';
654
+ const hugeResponse = {
655
+ id: 'drill-1',
656
+ structuredSnapshot: {
657
+ credentials: {
658
+ apiKey: 'not-a-real-key-for-return-test',
659
+ bearerToken: 'Bearer not-a-real-token-for-return-test',
660
+ webhookSecret: 'not-a-real-webhook-secret-for-return-test',
661
+ connectionString: 'postgres://user:password@example.test:5432/app',
662
+ },
663
+ payload: `${hugeMarker}:${'x'.repeat(40 * 1024)}`,
664
+ },
665
+ };
666
+ const cfg = {
667
+ tenantId: 'TENANT_test',
668
+ appId: 'APP_test',
669
+ appSecret: 'secret',
670
+ captureHeaders: false,
671
+ privacy: { environment: 'dev' },
672
+ };
673
+ const maskReq = {
674
+ method: 'POST',
675
+ path: '/api/privacy-lab-drills/run',
676
+ key: 'POST /api/privacy-lab-drills/run',
677
+ };
678
+
679
+ const sink = [];
680
+ await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
681
+ {
682
+ type: 'exit',
683
+ fn: 'toDrillResponse',
684
+ file: '/app/privacy-lab-drills.service.ts',
685
+ line: 210,
686
+ returnValue: hugeResponse,
687
+ },
688
+ sink,
689
+ cfg,
690
+ maskReq,
691
+ );
692
+
693
+ assert.strictEqual(sink.length, 1);
694
+ 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]);
273
700
  }
274
701
 
275
702
  async function main() {
276
703
  await testKafkaIntegrationConfigUsesStartupLoadedPrivacyPolicy();
277
704
  await testKafkaConsumerTraceUsesConsumeTraceId();
278
- await testOversizedKafkaTraceArgsKeepBoundedPreview();
705
+ await testKafkaProducerCapturesFullPublishedMessageValue();
706
+ await testKafkaTraceFlushIsNotBlockedForeverByActiveHttpRequest();
707
+ await testLargeHttpResponseBodyIsCapturedWithoutDroppingRequest();
708
+ await testJsonBuiltinTracePayloadsAreOmitted();
709
+ await testLargeStringTraceValuesAreCapturedInlineWithoutTruncation();
710
+ await testLargeStructuredTraceValuesAreCapturedInlineWithoutTruncation();
711
+ await testLargeTraceReturnValueIsCapturedInlineWithoutTruncation();
279
712
  console.log('kafka runtime privacy policy wiring OK');
280
713
  }
281
714
 
@@ -0,0 +1,76 @@
1
+ const assert = require('node:assert');
2
+ const { __reproTestHooks } = require('../dist/index.js');
3
+
4
+ async function withTimeout(promise, ms) {
5
+ let timeout;
6
+ try {
7
+ return await Promise.race([
8
+ promise,
9
+ new Promise((_, reject) => {
10
+ timeout = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
11
+ }),
12
+ ]);
13
+ } finally {
14
+ if (timeout) clearTimeout(timeout);
15
+ }
16
+ }
17
+
18
+ async function main() {
19
+ const mongooseLikeDoc = {
20
+ $__: {},
21
+ toObject() {
22
+ return {
23
+ _id: '6a0e53c8aba8240c69b27d7b',
24
+ caseId: 'privacy-lab-debug-root',
25
+ payload: {
26
+ rawMessage: '{"eventType":"privacy-lab.case.observed"}',
27
+ nested: {
28
+ reviewerEmail: 'privacy.operator@example.com',
29
+ },
30
+ },
31
+ };
32
+ },
33
+ };
34
+ mongooseLikeDoc.$__.owner = mongooseLikeDoc;
35
+ mongooseLikeDoc.$__.cache = {
36
+ veryLargeInternalValue: 'x'.repeat(10000),
37
+ };
38
+
39
+ const materialized = await withTimeout(
40
+ __reproTestHooks.materializeInlinePrivacyValueAsyncForTest(
41
+ 'db.query',
42
+ {
43
+ op: 'insertOne',
44
+ doc: mongooseLikeDoc,
45
+ },
46
+ {
47
+ appId: 'APP_test',
48
+ appSecret: 'secret',
49
+ tenantId: 'TENANT_test',
50
+ },
51
+ {
52
+ method: 'POST',
53
+ path: '/api/privacy-lab-drills/run',
54
+ key: 'POST /api/privacy-lab-drills/run',
55
+ },
56
+ null,
57
+ null,
58
+ null,
59
+ ),
60
+ 1000,
61
+ );
62
+
63
+ assert.strictEqual(materialized.skipped, undefined);
64
+ assert.strictEqual(materialized.value.doc.$__, undefined);
65
+ assert.strictEqual(materialized.value.doc.caseId, 'privacy-lab-debug-root');
66
+ assert.strictEqual(materialized.value.doc.payload.rawMessage, '{"eventType":"privacy-lab.case.observed"}');
67
+ assert.match(materialized.value.doc.payload.nested.reviewerEmail, /^email_tok_[a-f0-9]+$/);
68
+ assert.deepStrictEqual(Object.keys(materialized.value.doc).sort(), ['_id', 'caseId', 'payload']);
69
+
70
+ console.log('runtime privacy materialization OK');
71
+ }
72
+
73
+ main().catch((error) => {
74
+ console.error(error);
75
+ process.exit(1);
76
+ });