@reproapp/node-sdk 0.0.47 → 0.0.48

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.
@@ -0,0 +1,264 @@
1
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '50';
2
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
3
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
4
+ process.env.REPRO_SDK_TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS = '2048';
5
+ process.env.REPRO_TRACE_BATCH_MAX_SERIALIZED_BYTES = '65536';
6
+ process.env.TRACE_IDLE_FLUSH_MS = '10';
7
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
8
+
9
+ const assert = require('node:assert/strict');
10
+ const { EventEmitter } = require('node:events');
11
+ const { initReproTracing, reproMiddleware, __reproTestHooks } = require('../dist');
12
+ const { flushIngestQueue } = require('../dist/ingest/client');
13
+ const { normalizeRuntimePrivacyPolicy } = require('../dist/privacy');
14
+
15
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+
17
+ function makeReqRes(sessionId, actionId) {
18
+ const req = new EventEmitter();
19
+ req.method = 'POST';
20
+ req.url = '/content-addressed-values';
21
+ req.originalUrl = req.url;
22
+ req.headers = {
23
+ 'x-bug-session-id': sessionId,
24
+ 'x-bug-action-id': actionId,
25
+ 'x-bug-request-start': String(Date.now()),
26
+ };
27
+ req.body = {};
28
+ req.params = {};
29
+ req.query = {};
30
+
31
+ const res = new EventEmitter();
32
+ res.statusCode = 204;
33
+ res.getHeader = () => undefined;
34
+ res.getHeaders = () => ({});
35
+ res.setHeader = () => {};
36
+ res.write = () => true;
37
+ res.end = function end() {
38
+ this.emit('finish');
39
+ return this;
40
+ };
41
+ return { req, res };
42
+ }
43
+
44
+ function makePayload(changed = false, internalSecret) {
45
+ const payload = {
46
+ eventType: 'clinical.shipment.observed',
47
+ shipmentNumber: changed ? 'SHIP-CONTENT-CHANGED' : 'SHIP-CONTENT-STABLE',
48
+ rows: Array.from({ length: 300 }, (_, index) => ({
49
+ index,
50
+ depot: `DEPOT-${String(index).padStart(4, '0')}`,
51
+ site: {
52
+ number: `SITE-${index % 20}`,
53
+ country: { code: 'US', region: 'FDA' },
54
+ },
55
+ status: index % 2 === 0 ? 'available' : 'released',
56
+ })),
57
+ };
58
+ if (internalSecret !== undefined) {
59
+ payload.internalSecret = internalSecret;
60
+ }
61
+ return payload;
62
+ }
63
+
64
+ function observePayload(payload) {
65
+ return payload.shipmentNumber;
66
+ }
67
+
68
+ async function runRequest(config, sessionId, repeatedPayload, changedPayload = makePayload(true)) {
69
+ const posts = [];
70
+ const previousFetch = global.fetch;
71
+ global.fetch = async (_url, init = {}) => {
72
+ posts.push(JSON.parse(String(init.body || '{}')));
73
+ return {
74
+ ok: true,
75
+ status: 202,
76
+ json: async () => ({ ok: true }),
77
+ text: async () => '{"ok":true}',
78
+ };
79
+ };
80
+
81
+ const middleware = reproMiddleware(config);
82
+ const { req, res } = makeReqRes(sessionId, `A_${sessionId}`);
83
+ try {
84
+ await new Promise((resolve, reject) => {
85
+ middleware(req, res, async (error) => {
86
+ if (error) {
87
+ reject(error);
88
+ return;
89
+ }
90
+ try {
91
+ for (let index = 0; index < 60; index += 1) {
92
+ const result = global.__repro_call(
93
+ observePayload,
94
+ null,
95
+ [repeatedPayload],
96
+ '/app/shipments.service.ts',
97
+ 70,
98
+ 'observePayload',
99
+ false,
100
+ );
101
+ assert.equal(result, repeatedPayload.shipmentNumber);
102
+ }
103
+ const changedResult = global.__repro_call(
104
+ observePayload,
105
+ null,
106
+ [changedPayload],
107
+ '/app/shipments.service.ts',
108
+ 75,
109
+ 'observePayload',
110
+ false,
111
+ );
112
+ assert.equal(changedResult, changedPayload.shipmentNumber);
113
+ res.end();
114
+ resolve();
115
+ } catch (traceError) {
116
+ reject(traceError);
117
+ }
118
+ });
119
+ });
120
+
121
+ const deadline = Date.now() + 5000;
122
+ while (Date.now() < deadline) {
123
+ const events = posts.flatMap((post) => post.events || []);
124
+ if (events.some((event) => event.event_type === 'backend_request') &&
125
+ events.some((event) => event.event_type === 'trace_batch')) {
126
+ break;
127
+ }
128
+ await sleep(20);
129
+ }
130
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
131
+ await flushIngestQueue();
132
+ return posts;
133
+ } finally {
134
+ global.fetch = previousFetch;
135
+ }
136
+ }
137
+
138
+ function inspectCapture(posts) {
139
+ const ingestEvents = posts.flatMap((post) => post.events || []);
140
+ const traceBatches = ingestEvents.filter((event) => event.event_type === 'trace_batch');
141
+ const traceEvents = traceBatches.flatMap((event) => event.payload?.trace || []);
142
+ const observedEvents = traceEvents.filter((event) => event.fn === 'observePayload');
143
+ const definitions = traceBatches.flatMap((event) => event.payload?.traceValues || []);
144
+ const refs = observedEvents
145
+ .map((event) => event.argsValueCapture?.valueId)
146
+ .filter((valueId) => typeof valueId === 'string');
147
+ return { ingestEvents, traceBatches, observedEvents, definitions, refs };
148
+ }
149
+
150
+ async function main() {
151
+ const config = {
152
+ tenantId: 'TENANT_content_addressed',
153
+ appId: 'APP_content_addressed',
154
+ appSecret: 'content-addressed-test-secret',
155
+ captureHeaders: false,
156
+ privacy: { environment: 'dev', captureRaw: true },
157
+ ingestBase: 'http://127.0.0.1:65535',
158
+ };
159
+
160
+ initReproTracing({ instrument: false, fullValueCapture: true, logFunctionCalls: false });
161
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
162
+
163
+ const repeatedPayload = makePayload(false);
164
+ const first = inspectCapture(await runRequest(config, 'S_content_addressed_one', repeatedPayload));
165
+
166
+ assert.equal(first.observedEvents.length, 122, 'each invocation must retain enter and exit evidence');
167
+ assert.equal(first.refs.length, 122, 'every large argument occurrence must retain a resolvable ref');
168
+ assert.equal(new Set(first.refs).size, 2, 'only stable and changed values should receive IDs');
169
+ assert.equal(first.definitions.length, 2, 'each distinct value must be defined once per request');
170
+ assert.equal(new Set(first.definitions.map((entry) => entry.id)).size, 2);
171
+
172
+ for (const ref of first.refs) {
173
+ assert(first.definitions.some((entry) => entry.id === ref), `unresolved captured value ref ${ref}`);
174
+ }
175
+ const stableDefinition = first.definitions.find((entry) =>
176
+ entry.value?.[0]?.shipmentNumber === 'SHIP-CONTENT-STABLE'
177
+ );
178
+ assert(stableDefinition, JSON.stringify(first.definitions));
179
+ assert.equal(stableDefinition.value[0].rows.length, 300);
180
+ assert.equal(stableDefinition.value[0].rows[299].depot, 'DEPOT-0299');
181
+
182
+ const serializedBytes = Buffer.byteLength(JSON.stringify(first.traceBatches));
183
+ assert(serializedBytes < 250000, `repeated values inflated trace transport to ${serializedBytes} bytes`);
184
+
185
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
186
+ const second = inspectCapture(await runRequest(config, 'S_content_addressed_two', repeatedPayload));
187
+ assert.equal(second.definitions.length, 2);
188
+ assert.notEqual(
189
+ first.definitions[0].id,
190
+ second.definitions[0].id,
191
+ 'content IDs must be session-scoped to prevent cross-session correlation',
192
+ );
193
+
194
+ const secretPolicy = normalizeRuntimePrivacyPolicy({
195
+ environment: 'dev',
196
+ groups: [
197
+ {
198
+ gid: 'content-addressed-secret-policy',
199
+ scope: 'FIELD',
200
+ selector: 'field:internalSecret',
201
+ statements: [
202
+ {
203
+ sid: 'drop-trace-argument-secret',
204
+ when: {
205
+ scope: 'FIELD',
206
+ selector: 'field:internalSecret',
207
+ target: 'trace.args[].internalSecret',
208
+ },
209
+ apply: { action: 'DROP' },
210
+ meta: { priority: 1 },
211
+ },
212
+ ],
213
+ },
214
+ ],
215
+ });
216
+ assert(secretPolicy);
217
+
218
+ const policyConfig = {
219
+ ...config,
220
+ appId: 'APP_content_addressed_policy',
221
+ privacy: { environment: 'dev' },
222
+ };
223
+ __reproTestHooks.setRuntimePrivacyPolicyForTest(policyConfig, secretPolicy);
224
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
225
+ const policyCapture = inspectCapture(await runRequest(
226
+ policyConfig,
227
+ 'S_content_addressed_policy',
228
+ makePayload(false, 'policy-secret-alpha'),
229
+ makePayload(false, 'policy-secret-beta'),
230
+ ));
231
+ assert.equal(policyCapture.refs.length, 122);
232
+ assert.equal(new Set(policyCapture.refs).size, 1);
233
+ assert.equal(policyCapture.definitions.length, 1);
234
+ const policySerialized = JSON.stringify(policyCapture.definitions);
235
+ assert(!policySerialized.includes('policy-secret-alpha'), policySerialized);
236
+ assert(!policySerialized.includes('policy-secret-beta'), policySerialized);
237
+ assert(policySerialized.includes('[dropped]'), policySerialized);
238
+
239
+ const rawConfig = {
240
+ ...config,
241
+ appId: 'APP_content_addressed_raw',
242
+ privacy: { environment: 'dev', captureRaw: true },
243
+ };
244
+ __reproTestHooks.setRuntimePrivacyPolicyForTest(rawConfig, secretPolicy);
245
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
246
+ const rawCapture = inspectCapture(await runRequest(
247
+ rawConfig,
248
+ 'S_content_addressed_raw',
249
+ makePayload(false, 'raw-secret-alpha'),
250
+ makePayload(false, 'raw-secret-beta'),
251
+ ));
252
+ assert.equal(new Set(rawCapture.refs).size, 2);
253
+ assert.equal(rawCapture.definitions.length, 2);
254
+ const rawSerialized = JSON.stringify(rawCapture.definitions);
255
+ assert(rawSerialized.includes('raw-secret-alpha'), rawSerialized);
256
+ assert(rawSerialized.includes('raw-secret-beta'), rawSerialized);
257
+
258
+ console.log('content-addressed trace value deduplication OK');
259
+ }
260
+
261
+ main().catch((error) => {
262
+ console.error(error);
263
+ process.exit(1);
264
+ });
@@ -0,0 +1,85 @@
1
+ class Logger {
2
+ constructor(context) {
3
+ this.context = context;
4
+ this.internalBuffer = Array.from({ length: 200 }, () => 'logger-runtime-state');
5
+ }
6
+
7
+ log() {}
8
+ }
9
+
10
+ class ConfigService {
11
+ constructor() {
12
+ this.cache = {
13
+ DATABASE_URL: 'mongodb://runtime-user:runtime-password@database/repro',
14
+ SENDGRID_API_KEY: 'sendgrid-runtime-secret',
15
+ };
16
+ this.internalConfig = {
17
+ modules: Array.from({ length: 800 }, (_, index) => ({
18
+ index,
19
+ secret: `config-secret-${index}`,
20
+ nested: { host: `database-${index}.internal`, port: 27017 },
21
+ })),
22
+ };
23
+ }
24
+
25
+ get(key) {
26
+ return this.cache[key];
27
+ }
28
+ }
29
+
30
+ class ApiConfigService {
31
+ constructor(configService) {
32
+ this.configService = configService;
33
+ }
34
+ }
35
+
36
+ class ShipmentGenerator {
37
+ constructor(configService) {
38
+ this.apiConfigService = new ApiConfigService(configService);
39
+ this.logger = new Logger('ShipmentGenerator');
40
+ this.triggerAction = 'ShipmentCreated';
41
+ }
42
+ }
43
+
44
+ class ShipmentDto {
45
+ constructor() {
46
+ this.shipmentNumber = 'SHIP-TRACE-1001';
47
+ this.site = {
48
+ number: 'SITE-0042',
49
+ address: {
50
+ country: {
51
+ code: 'US',
52
+ regulatory: { region: 'FDA', enabled: true },
53
+ },
54
+ },
55
+ };
56
+ this.kits = Array.from({ length: 160 }, (_, index) => ({
57
+ kitNumber: `KIT-${String(index).padStart(4, '0')}`,
58
+ status: index % 2 === 0 ? 'available' : 'released',
59
+ temperature: { minC: 2, maxC: 8 },
60
+ }));
61
+ }
62
+ }
63
+
64
+ function recordShipment(generator, shipment) {
65
+ return {
66
+ accepted: true,
67
+ originalGeneratorObserved: generator instanceof ShipmentGenerator,
68
+ originalShipmentObserved: shipment instanceof ShipmentDto,
69
+ runtimeConfigStillReadable:
70
+ generator.apiConfigService.configService.cache.DATABASE_URL.startsWith('mongodb://'),
71
+ shipmentNumber: shipment.shipmentNumber,
72
+ lastKit: shipment.kits[shipment.kits.length - 1].kitNumber,
73
+ };
74
+ }
75
+
76
+ async function handleSemanticTrace(_req, res) {
77
+ const configService = new ConfigService();
78
+ const generator = new ShipmentGenerator(configService);
79
+ const shipment = new ShipmentDto();
80
+ res.json(recordShipment(generator, shipment));
81
+ }
82
+
83
+ module.exports = {
84
+ handleSemanticTrace,
85
+ };
@@ -0,0 +1,19 @@
1
+ const express = require('express');
2
+ const { reproMiddleware } = require('../../dist');
3
+ const { handleSemanticTrace } = require('./semantic-serialization-trace-controller');
4
+
5
+ async function startServer(config) {
6
+ const app = express();
7
+ app.use(reproMiddleware(config));
8
+ app.get('/semantic-trace', handleSemanticTrace);
9
+
10
+ return new Promise((resolve, reject) => {
11
+ const server = app.listen(0, '127.0.0.1');
12
+ server.once('listening', () => resolve(server));
13
+ server.once('error', reject);
14
+ });
15
+ }
16
+
17
+ module.exports = {
18
+ startServer,
19
+ };
@@ -0,0 +1,152 @@
1
+ const assert = require('node:assert/strict');
2
+ const http = require('node:http');
3
+
4
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
5
+ process.env.REPRO_SDK_TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS = '2048';
6
+ process.env.TRACE_IDLE_FLUSH_MS = '10';
7
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
8
+
9
+ const originalFetch = global.fetch;
10
+ const capturedBodies = [];
11
+
12
+ global.fetch = async (url, init = {}) => {
13
+ assert.match(String(url), /\/v1\/ingest\/events$/);
14
+ capturedBodies.push(JSON.parse(String(init.body || '{}')));
15
+ return {
16
+ ok: true,
17
+ status: 202,
18
+ json: async () => ({ ok: true }),
19
+ text: async () => '{"ok":true}',
20
+ };
21
+ };
22
+
23
+ const { initRepro } = require('../dist');
24
+ const { flushIngestQueue } = require('../dist/ingest/client');
25
+
26
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27
+
28
+ async function waitFor(predicate, timeoutMs = 4000) {
29
+ const deadline = Date.now() + timeoutMs;
30
+ while (Date.now() < deadline) {
31
+ if (predicate()) return;
32
+ await sleep(20);
33
+ }
34
+ assert(predicate(), 'timed out waiting for semantic trace capture');
35
+ }
36
+
37
+ function eventsOf(type) {
38
+ return capturedBodies
39
+ .flatMap((body) => Array.isArray(body?.events) ? body.events : [])
40
+ .filter((event) => event?.event_type === type);
41
+ }
42
+
43
+ function get(url) {
44
+ return new Promise((resolve, reject) => {
45
+ const request = http.get(url, (response) => {
46
+ let body = '';
47
+ response.setEncoding('utf8');
48
+ response.on('data', (chunk) => { body += chunk; });
49
+ response.on('end', () => resolve({ statusCode: response.statusCode, body }));
50
+ });
51
+ request.on('error', reject);
52
+ });
53
+ }
54
+
55
+ async function main() {
56
+ const config = {
57
+ tenantId: 'TENANT_semantic_trace',
58
+ appId: 'APP_semantic_trace',
59
+ appSecret: 'secret',
60
+ appName: 'semantic-trace',
61
+ serviceName: 'semantic-trace',
62
+ ingestBase: 'http://127.0.0.1:65535',
63
+ tracing: {
64
+ disableFunctionTypes: ['constructor'],
65
+ fullValueCapture: true,
66
+ logFunctionCalls: false,
67
+ },
68
+ };
69
+
70
+ await initRepro(config);
71
+ const { startServer } = require('./fixtures/semantic-serialization-trace-server');
72
+ const server = await startServer(config);
73
+
74
+ try {
75
+ const port = server.address().port;
76
+ const response = await get(
77
+ `http://127.0.0.1:${port}/semantic-trace` +
78
+ `?__repro_sid=S_semantic_trace&__repro_aid=A_semantic_trace&__repro_start=${Date.now()}`,
79
+ );
80
+ assert.equal(response.statusCode, 200);
81
+ assert.deepEqual(JSON.parse(response.body), {
82
+ accepted: true,
83
+ originalGeneratorObserved: true,
84
+ originalShipmentObserved: true,
85
+ runtimeConfigStillReadable: true,
86
+ shipmentNumber: 'SHIP-TRACE-1001',
87
+ lastKit: 'KIT-0159',
88
+ });
89
+
90
+ await waitFor(() => eventsOf('backend_request').length === 1);
91
+ await waitFor(() => eventsOf('trace_batch').some((event) =>
92
+ event.payload?.trace?.some((traceEvent) => traceEvent?.fn === 'recordShipment')
93
+ ));
94
+ await flushIngestQueue();
95
+
96
+ const traceBatches = eventsOf('trace_batch');
97
+ const traceEvents = traceBatches.flatMap((event) => event.payload?.trace || []);
98
+ const enter = traceEvents.find((event) => event?.type === 'enter' && event?.fn === 'recordShipment');
99
+ assert(enter, JSON.stringify(traceEvents));
100
+
101
+ const traceValues = traceBatches.flatMap((event) => event.payload?.traceValues || []);
102
+ const argsValue = traceValues.find((entry) =>
103
+ entry?.id === enter.argsValueCapture?.valueId && entry?.target === 'trace.args'
104
+ );
105
+ const capturedArgs = argsValue?.value || enter.args;
106
+ assert(Array.isArray(capturedArgs), JSON.stringify({ enter, traceValues }));
107
+
108
+ assert.equal(capturedArgs[0].__class, 'ShipmentGenerator');
109
+ assert.equal(capturedArgs[0].triggerAction, 'ShipmentCreated');
110
+ assert.deepEqual(capturedArgs[0].logger, {
111
+ __kind: 'repro-semantic',
112
+ category: 'logger',
113
+ className: 'Logger',
114
+ context: 'ShipmentGenerator',
115
+ });
116
+ assert.deepEqual(capturedArgs[0].apiConfigService.configService, {
117
+ __kind: 'repro-semantic',
118
+ category: 'configuration-service',
119
+ className: 'ConfigService',
120
+ });
121
+
122
+ assert.equal(capturedArgs[1].shipmentNumber, 'SHIP-TRACE-1001');
123
+ assert.equal(capturedArgs[1].site.address.country.regulatory.region, 'FDA');
124
+ assert.equal(capturedArgs[1].kits.length, 160);
125
+ assert.equal(capturedArgs[1].kits[159].kitNumber, 'KIT-0159');
126
+
127
+ const allCapturedJson = JSON.stringify(capturedBodies);
128
+ assert(!allCapturedJson.includes('sendgrid-runtime-secret'));
129
+ assert(!allCapturedJson.includes('runtime-password'));
130
+ assert(!allCapturedJson.includes('config-secret-799'));
131
+ assert(!allCapturedJson.includes('logger-runtime-state'));
132
+
133
+ const argsBytes = Buffer.byteLength(JSON.stringify(capturedArgs));
134
+ assert(argsBytes < 30000, `semantic trace args unexpectedly large: ${argsBytes} bytes`);
135
+
136
+ console.log('semantic serialization real trace pipeline OK');
137
+ } finally {
138
+ await new Promise((resolve, reject) => {
139
+ server.close((error) => error ? reject(error) : resolve());
140
+ });
141
+ }
142
+ }
143
+
144
+ main()
145
+ .catch((error) => {
146
+ console.error(error);
147
+ process.exitCode = 1;
148
+ })
149
+ .finally(async () => {
150
+ await flushIngestQueue().catch(() => {});
151
+ global.fetch = originalFetch;
152
+ });