@reproapp/node-sdk 0.0.3 → 0.0.4

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.
Files changed (45) hide show
  1. package/README.md +36 -2
  2. package/dist/index.d.ts +140 -15
  3. package/dist/index.js +4927 -927
  4. package/dist/ingest/client.d.ts +10 -0
  5. package/dist/ingest/client.js +158 -0
  6. package/dist/ingest/mapper.d.ts +2 -0
  7. package/dist/ingest/mapper.js +92 -0
  8. package/dist/ingest/types.d.ts +40 -0
  9. package/dist/ingest/types.js +2 -0
  10. package/dist/ingest/worker.js +19 -0
  11. package/dist/integrations/sendgrid.d.ts +2 -4
  12. package/dist/integrations/sendgrid.js +4 -14
  13. package/dist/privacy-fallback.d.ts +1 -0
  14. package/dist/privacy-fallback.js +27 -0
  15. package/dist/privacy-redaction.d.ts +3 -0
  16. package/dist/privacy-redaction.js +38 -0
  17. package/dist/privacy.d.ts +108 -0
  18. package/dist/privacy.js +2868 -0
  19. package/dist/trace-materializer-worker.d.ts +1 -0
  20. package/dist/trace-materializer-worker.js +33 -0
  21. package/docs/tracing.md +1 -0
  22. package/package.json +8 -2
  23. package/src/index.ts +5583 -954
  24. package/src/ingest/client.ts +194 -0
  25. package/src/ingest/mapper.ts +104 -0
  26. package/src/ingest/types.ts +42 -0
  27. package/src/integrations/sendgrid.ts +6 -19
  28. package/src/privacy-fallback.ts +25 -0
  29. package/src/privacy-redaction.ts +37 -0
  30. package/src/privacy.ts +3593 -0
  31. package/src/trace-materializer-worker.ts +39 -0
  32. package/test/circular-capture.test.js +111 -0
  33. package/test/disable-subtree.test.js +154 -0
  34. package/test/integration-unawaited.js +183 -0
  35. package/test/kafka-runtime-privacy-policy.test.js +285 -0
  36. package/test/privacy-runtime-policy.test.js +2043 -0
  37. package/test/promise-map.test.js +72 -0
  38. package/test/unawaited.test.js +163 -0
  39. package/test/wrap-plugin-arrow-args.test.js +80 -0
  40. package/tracer/cjs-hook.js +0 -1
  41. package/tracer/wrap-plugin.js +96 -10
  42. package/dist/redaction.d.ts +0 -44
  43. package/dist/redaction.js +0 -167
  44. package/dist/server.js +0 -26
  45. /package/dist/{server.d.ts → ingest/worker.d.ts} +0 -0
@@ -0,0 +1,285 @@
1
+ const assert = require('assert');
2
+ const { normalizeRuntimePrivacyPolicy } = require('../dist/privacy');
3
+ const { flushIngestQueue } = require('../dist/ingest/client');
4
+ const { initReproTracing, __reproTestHooks } = require('../dist');
5
+
6
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7
+
8
+ async function waitFor(predicate, timeoutMs = 3000) {
9
+ const deadline = Date.now() + timeoutMs;
10
+ while (Date.now() < deadline) {
11
+ if (predicate()) return;
12
+ await sleep(25);
13
+ }
14
+ assert(predicate(), 'timed out waiting for expected ingest payload');
15
+ }
16
+
17
+ async function testKafkaIntegrationConfigUsesStartupLoadedPrivacyPolicy() {
18
+ const mainConfig = {
19
+ tenantId: 'TENANT_test',
20
+ appId: 'APP_test',
21
+ appSecret: 'secret',
22
+ captureHeaders: false,
23
+ privacy: { environment: 'dev' },
24
+ };
25
+ const kafkaConfig = {
26
+ ...mainConfig,
27
+ resolveContext: () => ({ sid: 'S_test', aid: 'A_test' }),
28
+ };
29
+
30
+ __reproTestHooks.shareRuntimePrivacyStateForTest(mainConfig, kafkaConfig);
31
+
32
+ const policy = normalizeRuntimePrivacyPolicy({
33
+ environment: 'dev',
34
+ rawTextHints: [
35
+ {
36
+ name: 'Raw message opaque detector',
37
+ scope: 'RAW_TEXT_ONLY',
38
+ action: 'TOKENIZE',
39
+ regex: '\\bZXQ[0-9A-Z]{8,32}(?:-[A-Z0-9]{2,32})*\\b',
40
+ tokenLabel: 'rawMessage',
41
+ },
42
+ {
43
+ name: 'Unbound same-regex detector',
44
+ scope: 'RAW_TEXT_ONLY',
45
+ action: 'TOKENIZE',
46
+ regex: '\\bZXQ[0-9A-Z]{8,32}(?:-[A-Z0-9]{2,32})*\\b',
47
+ tokenLabel: 'wrongLabel',
48
+ },
49
+ ],
50
+ groups: [
51
+ {
52
+ gid: 'raw-message',
53
+ scope: 'FIELD',
54
+ selector: 'rawMessage',
55
+ statements: [
56
+ {
57
+ sid: 'raw-message',
58
+ when: {
59
+ scope: 'FIELD',
60
+ selector: 'rawMessage',
61
+ target: 'trace.args[2]',
62
+ },
63
+ apply: {
64
+ action: 'SUMMARIZE',
65
+ rawTextMode: 'REGEX_ASSISTED_EXACT',
66
+ },
67
+ meta: {
68
+ priority: 25,
69
+ review: {
70
+ rawTextHintNames: ['Raw message opaque detector'],
71
+ },
72
+ },
73
+ },
74
+ {
75
+ sid: 'function-param-fallback',
76
+ when: {
77
+ scope: 'FUNCTION',
78
+ selector: 'fn:PrivacyLabDrillsService.recordConsumedEvent',
79
+ target: 'trace.args[2]',
80
+ },
81
+ apply: { action: 'SUMMARIZE' },
82
+ meta: { priority: 113 },
83
+ },
84
+ ],
85
+ },
86
+ ],
87
+ });
88
+
89
+ __reproTestHooks.setRuntimePrivacyPolicyForTest(mainConfig, policy);
90
+ assert.strictEqual(
91
+ __reproTestHooks.getRuntimePrivacyPolicyForTest(kafkaConfig),
92
+ policy,
93
+ 'Kafka integration config should share the startup-loaded runtime privacy state',
94
+ );
95
+
96
+ const opaque = 'ZXQ44B02BD9032D78A19KLM-TRACE-44B02B';
97
+ const sink = [];
98
+ await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
99
+ {
100
+ type: 'enter',
101
+ fn: 'PrivacyLabDrillsService.recordConsumedEvent',
102
+ file: '/app/privacy-lab-drills.service.ts',
103
+ line: 76,
104
+ args: [
105
+ { eventId: 'evt-1' },
106
+ 'privacy-lab-case',
107
+ `raw kafka message has ${opaque}`,
108
+ ],
109
+ },
110
+ sink,
111
+ kafkaConfig,
112
+ {
113
+ method: 'KAFKA_CONSUME',
114
+ path: 'kafka://privacy-lab.events',
115
+ key: 'KAFKA_CONSUME privacy-lab.events',
116
+ },
117
+ );
118
+
119
+ assert.strictEqual(sink.length, 1);
120
+ const text = JSON.stringify(sink[0]);
121
+ assert(!text.includes(opaque), text);
122
+ assert.match(sink[0].args[2], /^raw kafka message has rawMessage_tok_[a-f0-9]{12}$/);
123
+ assert(!text.includes('wrongLabel_tok_'), text);
124
+ }
125
+
126
+ async function testKafkaConsumerTraceUsesConsumeTraceId() {
127
+ const tracer = initReproTracing({ instrument: false, functionLogs: false });
128
+ const capturedBodies = [];
129
+ const originalFetch = global.fetch;
130
+ global.fetch = async (_url, init) => {
131
+ capturedBodies.push(JSON.parse(String(init?.body ?? '{}')));
132
+ return { ok: true };
133
+ };
134
+
135
+ try {
136
+ const cfg = {
137
+ tenantId: 'TENANT_test',
138
+ appId: 'APP_test',
139
+ appSecret: 'secret',
140
+ captureHeaders: false,
141
+ privacy: { environment: 'dev' },
142
+ ingestBase: 'http://127.0.0.1:65535',
143
+ };
144
+ const wrapped = __reproTestHooks.wrapKafkaEachMessageHandlerForTest(
145
+ async () => {
146
+ tracer.tracer.enter('consumerWork', { file: '/app/consumer.ts', line: 10 }, { args: ['ok'] });
147
+ tracer.tracer.exit({ fn: 'consumerWork', file: '/app/consumer.ts', line: 10 }, { returnValue: 'done' });
148
+
149
+ tracer.withTrace('parent-http-trace', () => {
150
+ tracer.tracer.enter('parentWork', { file: '/app/controller.ts', line: 20 }, { args: ['wrong'] });
151
+ tracer.tracer.exit({ fn: 'parentWork', file: '/app/controller.ts', line: 20 }, { returnValue: 'wrong' });
152
+ });
153
+ },
154
+ cfg,
155
+ { __repro_group_id: 'group-a' },
156
+ );
157
+
158
+ await wrapped({
159
+ topic: 'privacy-lab.events',
160
+ partition: 0,
161
+ message: {
162
+ offset: '42',
163
+ timestamp: '1778965503962',
164
+ key: Buffer.from('case-1'),
165
+ value: Buffer.from('hello'),
166
+ headers: {
167
+ 'x-bug-session-id': Buffer.from('S_kafka_trace'),
168
+ 'x-bug-action-id': Buffer.from('A_kafka_trace'),
169
+ 'x-repro-trace-id': Buffer.from('parent-http-trace'),
170
+ 'x-repro-request-rid': Buffer.from('parent-request-rid'),
171
+ 'x-repro-span-id': Buffer.from('217'),
172
+ },
173
+ },
174
+ });
175
+
176
+ await waitFor(() => capturedBodies.some((body) => {
177
+ return Array.isArray(body.events) && body.events.some((event) => event.event_type === 'trace_batch');
178
+ }));
179
+ await flushIngestQueue();
180
+
181
+ const events = capturedBodies.flatMap((body) => Array.isArray(body.events) ? body.events : []);
182
+ const consumeRequest = events.find((event) => event.event_type === 'backend_request');
183
+ assert(consumeRequest, 'expected Kafka consume request event');
184
+ assert.strictEqual(consumeRequest.payload.request.body.parentTraceId, 'parent-http-trace');
185
+ assert.strictEqual(consumeRequest.payload.request.body.parentRequestRid, 'parent-request-rid');
186
+
187
+ const traceFns = events
188
+ .filter((event) => event.event_type === 'trace_batch')
189
+ .flatMap((event) => Array.isArray(event.payload.trace) ? event.payload.trace : [])
190
+ .map((traceEvent) => traceEvent.fn);
191
+ assert(traceFns.includes('consumerWork'), traceFns.join(','));
192
+ assert(!traceFns.includes('parentWork'), traceFns.join(','));
193
+ } finally {
194
+ global.fetch = originalFetch;
195
+ }
196
+ }
197
+
198
+ async function testOversizedKafkaTraceArgsKeepBoundedPreview() {
199
+ const secret = 'sk_live_privacy_large_preview_secret';
200
+ const largeJson = JSON.stringify({
201
+ eventId: 'evt-large',
202
+ eventType: 'privacy-lab.case.observed',
203
+ providerResponse: {
204
+ credentials: {
205
+ apiKey: secret,
206
+ webhookSecret: 'whsec_large_preview_secret',
207
+ },
208
+ rawNarrative: `large narrative ${'x'.repeat(9000)}`,
209
+ },
210
+ structuredSnapshot: {
211
+ subject: {
212
+ email: 'avery.debugson@example.com',
213
+ },
214
+ },
215
+ });
216
+ const envelope = JSON.parse(largeJson);
217
+ const cfg = {
218
+ tenantId: 'TENANT_test',
219
+ appId: 'APP_test',
220
+ appSecret: 'secret',
221
+ captureHeaders: false,
222
+ privacy: { environment: 'dev' },
223
+ };
224
+ const maskReq = {
225
+ method: 'KAFKA_CONSUME',
226
+ path: 'kafka://privacy-lab.events',
227
+ key: 'KAFKA_CONSUME privacy-lab.events',
228
+ };
229
+
230
+ const parseSink = [];
231
+ await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
232
+ {
233
+ type: 'enter',
234
+ fn: 'JSON.parse',
235
+ file: '/app/privacy-lab-drills.service.ts',
236
+ line: 77,
237
+ args: [largeJson],
238
+ },
239
+ parseSink,
240
+ cfg,
241
+ maskReq,
242
+ );
243
+
244
+ 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]));
251
+
252
+ const consumedSink = [];
253
+ await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
254
+ {
255
+ type: 'enter',
256
+ fn: 'recordConsumedEvent',
257
+ file: '/app/privacy-lab-drills.service.ts',
258
+ line: 78,
259
+ args: [envelope, 'privacy-lab-case', largeJson],
260
+ },
261
+ consumedSink,
262
+ cfg,
263
+ maskReq,
264
+ );
265
+
266
+ assert.strictEqual(consumedSink.length, 1);
267
+ assert(consumedSink[0].args, 'expected oversized recordConsumedEvent args to keep a bounded preview');
268
+ assert.strictEqual(consumedSink[0].args[1], 'privacy-lab-case');
269
+ 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]));
273
+ }
274
+
275
+ async function main() {
276
+ await testKafkaIntegrationConfigUsesStartupLoadedPrivacyPolicy();
277
+ await testKafkaConsumerTraceUsesConsumeTraceId();
278
+ await testOversizedKafkaTraceArgsKeepBoundedPreview();
279
+ console.log('kafka runtime privacy policy wiring OK');
280
+ }
281
+
282
+ main().catch((err) => {
283
+ console.error(err);
284
+ process.exitCode = 1;
285
+ });