@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.
- package/dist/index.d.ts +16 -10
- package/dist/index.js +462 -439
- package/package.json +2 -2
- package/src/index.ts +539 -488
- package/test/circular-capture.test.js +1 -1
- package/test/disable-subtree.test.js +1 -1
- package/test/kafka-runtime-privacy-policy.test.js +451 -18
- package/test/runtime-privacy-materialization.test.js +76 -0
|
@@ -94,7 +94,7 @@ async function main() {
|
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
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');
|
|
@@ -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(
|
|
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
|
-
|
|
199
|
-
const
|
|
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:
|
|
206
|
-
webhookSecret: '
|
|
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(
|
|
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].
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
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]
|
|
271
|
-
|
|
272
|
-
assert
|
|
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
|
|
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
|
+
});
|