@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,229 @@
1
+ const assert = require('node:assert/strict');
2
+ const { __reproTestHooks } = require('../dist');
3
+
4
+ const {
5
+ projectSemanticInfrastructureValueForTest: project,
6
+ sanitizeTraceValueForPrivacyForTest: sanitize,
7
+ } = __reproTestHooks;
8
+
9
+ class Logger {
10
+ constructor(context) {
11
+ this.context = context;
12
+ this.internalBuffer = Array.from({ length: 100 }, () => 'log-internal-state');
13
+ }
14
+
15
+ log() {}
16
+ }
17
+
18
+ class ConfigService {
19
+ constructor() {
20
+ this.cache = {
21
+ SENDGRID_API_KEY: 'secret-sendgrid-value-that-must-not-be-captured',
22
+ DATABASE_URL: 'mongodb://user:password@database/repro',
23
+ };
24
+ this.internalConfig = {
25
+ infrastructure: Array.from({ length: 500 }, (_, index) => ({
26
+ index,
27
+ secret: `internal-secret-${index}`,
28
+ nested: { socket: { host: 'database.internal', port: 27017 } },
29
+ })),
30
+ };
31
+ }
32
+
33
+ get(key) {
34
+ return this.cache[key];
35
+ }
36
+ }
37
+
38
+ class ApiConfigService {
39
+ constructor(configService) {
40
+ this.configService = configService;
41
+ }
42
+
43
+ getDatabaseConfig() {
44
+ return this.configService.get('DATABASE_URL');
45
+ }
46
+ }
47
+
48
+ class GlobalUserRepository {
49
+ constructor(apiConfigService) {
50
+ this.apiConfigService = apiConfigService;
51
+ }
52
+ }
53
+
54
+ class UserService {
55
+ constructor(globalUserRepository) {
56
+ this.globalUserRepository = globalUserRepository;
57
+ this.logger = new Logger('UserService');
58
+ }
59
+ }
60
+
61
+ class StudyConfigService {
62
+ constructor(userService) {
63
+ this.userService = userService;
64
+ }
65
+ }
66
+
67
+ class BlindingMailerService {
68
+ constructor() {
69
+ this.logger = new Logger('BlindingMailerService');
70
+ this.fromEmail = 'automated@example.test';
71
+ }
72
+ }
73
+
74
+ class SiteStatusChangedGenerator {
75
+ constructor(configService) {
76
+ this.blindingMailerService = new BlindingMailerService();
77
+ this.logger = new Logger('SiteStatusChangedGenerator');
78
+ this.studyConfigService = new StudyConfigService(
79
+ new UserService(new GlobalUserRepository(new ApiConfigService(configService))),
80
+ );
81
+ this.triggerAction = 'SiteStatusChanged';
82
+ }
83
+ }
84
+
85
+ class ClinicalShipmentDto {
86
+ constructor() {
87
+ this.shipmentNumber = 'SHIP-10023';
88
+ this.site = {
89
+ number: 'SITE-0042',
90
+ address: {
91
+ country: {
92
+ code: 'US',
93
+ regulatory: { region: 'FDA', enabled: true },
94
+ },
95
+ },
96
+ };
97
+ this.kits = Array.from({ length: 40 }, (_, index) => ({
98
+ kitNumber: `KIT-${String(index).padStart(4, '0')}`,
99
+ status: index % 2 ? 'released' : 'available',
100
+ temperature: { minC: 2, maxC: 8 },
101
+ }));
102
+ }
103
+ }
104
+
105
+ class PricingService {
106
+ constructor() {
107
+ this.price = 125;
108
+ this.currency = 'USD';
109
+ this.breakdown = { base: 100, tax: 25 };
110
+ }
111
+ }
112
+
113
+ class DeliveryClient {
114
+ send() {}
115
+ connect() {}
116
+ disconnect() {}
117
+ }
118
+
119
+ class RuntimeConfigService {
120
+ constructor() {
121
+ Object.defineProperty(this, 'dangerousGetter', {
122
+ enumerable: true,
123
+ get() {
124
+ throw new Error('semantic projection invoked an application getter');
125
+ },
126
+ });
127
+ }
128
+
129
+ get() {
130
+ return 'value';
131
+ }
132
+ }
133
+
134
+ class RedisClient {
135
+ get() {}
136
+ set() {}
137
+ pipeline() {}
138
+ }
139
+
140
+ class KafkaProducer {
141
+ connect() {}
142
+ disconnect() {}
143
+ send() {}
144
+ }
145
+
146
+ class ChannelModel {
147
+ publish() {}
148
+ sendToQueue() {}
149
+ ack() {}
150
+ }
151
+
152
+ class Pool {
153
+ query() {}
154
+ connect() {}
155
+ end() {}
156
+ }
157
+
158
+ class DataSource {
159
+ createQueryRunner() {}
160
+ }
161
+
162
+ function main() {
163
+ const configService = new ConfigService();
164
+ const generator = new SiteStatusChangedGenerator(configService);
165
+ const businessDto = new ClinicalShipmentDto();
166
+
167
+ const rawJson = JSON.stringify([generator, businessDto]);
168
+ const captured = sanitize([generator, businessDto]);
169
+ const capturedJson = JSON.stringify(captured);
170
+
171
+ assert.equal(captured[0].__class, 'SiteStatusChangedGenerator');
172
+ assert.equal(captured[0].triggerAction, 'SiteStatusChanged');
173
+ assert.equal(captured[0].blindingMailerService.fromEmail, 'automated@example.test');
174
+ assert.deepEqual(captured[0].logger, {
175
+ __kind: 'repro-semantic',
176
+ category: 'logger',
177
+ className: 'Logger',
178
+ context: 'SiteStatusChangedGenerator',
179
+ });
180
+ assert.deepEqual(
181
+ captured[0]
182
+ .studyConfigService
183
+ .userService
184
+ .globalUserRepository
185
+ .apiConfigService
186
+ .configService,
187
+ {
188
+ __kind: 'repro-semantic',
189
+ category: 'configuration-service',
190
+ className: 'ConfigService',
191
+ },
192
+ );
193
+ assert(!capturedJson.includes('secret-sendgrid-value-that-must-not-be-captured'));
194
+ assert(!capturedJson.includes('internal-secret-499'));
195
+
196
+ assert.deepEqual(
197
+ { ...captured[1], __class: undefined },
198
+ { ...JSON.parse(JSON.stringify(businessDto)), __class: undefined },
199
+ 'business DTOs must remain byte-for-byte equivalent after JSON serialization',
200
+ );
201
+
202
+ assert(
203
+ Buffer.byteLength(capturedJson) < Buffer.byteLength(rawJson) * 0.15,
204
+ `semantic capture did not reduce infrastructure graph enough: raw=${Buffer.byteLength(rawJson)} captured=${Buffer.byteLength(capturedJson)}`,
205
+ );
206
+
207
+ assert.equal(project(configService).value.category, 'configuration-service');
208
+ assert.equal(project(new Logger('test')).value.category, 'logger');
209
+ assert.equal(project(new RedisClient()).value.category, 'redis-client');
210
+ assert.equal(project(new KafkaProducer()).value.category, 'kafka-producer');
211
+ assert.equal(project(new ChannelModel()).value.category, 'amqp-channel');
212
+ assert.equal(project(new Pool()).value.category, 'postgres-pool');
213
+ assert.equal(project(new DataSource()).value.category, 'typeorm-infrastructure');
214
+ assert.equal(project(new DeliveryClient()), null, 'generic clients must not be mistaken for Kafka');
215
+
216
+ assert.equal(project(new PricingService()), null, 'data-only application classes must remain complete');
217
+ assert.deepEqual(
218
+ sanitize(new PricingService()),
219
+ { ...JSON.parse(JSON.stringify(new PricingService())), __class: 'PricingService' },
220
+ 'data-only application classes must not be semantically projected',
221
+ );
222
+
223
+ assert.doesNotThrow(() => project(new RuntimeConfigService()));
224
+ assert.equal(project(new RuntimeConfigService()).value.category, 'configuration-service');
225
+
226
+ console.log('semantic serialization adversarial coverage OK');
227
+ }
228
+
229
+ main();
@@ -0,0 +1,298 @@
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
+
14
+ const CALL_COUNT = 120;
15
+ const UNIQUE_PAYLOAD_COUNT = 12;
16
+
17
+ const byteLength = (value) => Buffer.byteLength(JSON.stringify(value), 'utf8');
18
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
19
+
20
+ class ConfigService {
21
+ constructor() {
22
+ this.cache = {
23
+ DATABASE_URL: 'mongodb://runtime-user:runtime-password@database/repro',
24
+ SENDGRID_API_KEY: 'SG.runtime-secret',
25
+ };
26
+ this.internalConfig = Array.from({ length: 1500 }, (_, index) => ({
27
+ index,
28
+ tenant: `TENANT-${index}`,
29
+ connection: {
30
+ host: `mongo-${index % 3}.internal`,
31
+ password: `runtime-password-${index}`,
32
+ },
33
+ }));
34
+ }
35
+
36
+ get(key) {
37
+ return this.cache[key];
38
+ }
39
+ }
40
+
41
+ class Logger {
42
+ constructor(context) {
43
+ this.context = context;
44
+ this.buffers = Array.from({ length: 500 }, (_, index) => ({
45
+ index,
46
+ line: `runtime-log-buffer-${index}`,
47
+ }));
48
+ }
49
+
50
+ log() {}
51
+ }
52
+
53
+ class ShipmentWorkflowService {
54
+ constructor() {
55
+ this.configService = new ConfigService();
56
+ this.logger = new Logger('ShipmentWorkflowService');
57
+ this.workflow = 'clinical-supply-release';
58
+ }
59
+ }
60
+
61
+ function makeBusinessPayload(variant) {
62
+ return {
63
+ shipmentNumber: `SHIP-STORAGE-${String(variant).padStart(4, '0')}`,
64
+ protocol: {
65
+ id: 'PROTOCOL-1001',
66
+ version: 7,
67
+ countries: ['US', 'CA', 'DE', 'FR'],
68
+ },
69
+ kits: Array.from({ length: 180 }, (_, index) => ({
70
+ kitNumber: `KIT-${variant}-${String(index).padStart(5, '0')}`,
71
+ depot: `DEPOT-${index % 8}`,
72
+ site: {
73
+ number: `SITE-${index % 35}`,
74
+ country: { code: index % 2 ? 'US' : 'CA', region: 'GxP' },
75
+ },
76
+ status: index % 3 === 0 ? 'released' : 'available',
77
+ temperature: { minimumC: 2, maximumC: 8 },
78
+ })),
79
+ audit: {
80
+ actor: `USER-${variant}`,
81
+ correlationId: `CORRELATION-${variant}`,
82
+ reason: 'release-after-quality-review',
83
+ },
84
+ };
85
+ }
86
+
87
+ function inspectShipment(_service, payload) {
88
+ return payload;
89
+ }
90
+
91
+ function makeReqRes() {
92
+ const req = new EventEmitter();
93
+ req.method = 'POST';
94
+ req.url = '/storage-budget';
95
+ req.originalUrl = req.url;
96
+ req.headers = {
97
+ 'x-bug-session-id': 'S_storage_budget',
98
+ 'x-bug-action-id': 'A_storage_budget',
99
+ 'x-bug-request-start': String(Date.now()),
100
+ };
101
+ req.body = {};
102
+ req.params = {};
103
+ req.query = {};
104
+
105
+ const res = new EventEmitter();
106
+ res.statusCode = 204;
107
+ res.getHeader = () => undefined;
108
+ res.getHeaders = () => ({});
109
+ res.setHeader = () => {};
110
+ res.write = () => true;
111
+ res.end = function end() {
112
+ this.emit('finish');
113
+ return this;
114
+ };
115
+ return { req, res };
116
+ }
117
+
118
+ function captureRef(event) {
119
+ if (event.type === 'enter') return event.argsValueCapture;
120
+ if (event.type === 'exit') return event.returnValueCapture;
121
+ return null;
122
+ }
123
+
124
+ async function main() {
125
+ const posts = [];
126
+ const originalFetch = global.fetch;
127
+ global.fetch = async (_url, init = {}) => {
128
+ posts.push(JSON.parse(String(init.body || '{}')));
129
+ return {
130
+ ok: true,
131
+ status: 202,
132
+ json: async () => ({ ok: true }),
133
+ text: async () => '{"ok":true}',
134
+ };
135
+ };
136
+
137
+ const config = {
138
+ tenantId: 'TENANT_storage_budget',
139
+ appId: 'APP_storage_budget',
140
+ appSecret: 'storage-budget-secret',
141
+ captureHeaders: false,
142
+ privacy: { environment: 'dev', captureRaw: true },
143
+ ingestBase: 'http://127.0.0.1:65535',
144
+ };
145
+ const service = new ShipmentWorkflowService();
146
+ const payloads = Array.from(
147
+ { length: UNIQUE_PAYLOAD_COUNT },
148
+ (_, index) => makeBusinessPayload(index),
149
+ );
150
+
151
+ try {
152
+ initReproTracing({ instrument: false, fullValueCapture: true, logFunctionCalls: false });
153
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
154
+ const middleware = reproMiddleware(config);
155
+ const { req, res } = makeReqRes();
156
+
157
+ await new Promise((resolve, reject) => {
158
+ middleware(req, res, (error) => {
159
+ if (error) {
160
+ reject(error);
161
+ return;
162
+ }
163
+ try {
164
+ for (let index = 0; index < CALL_COUNT; index += 1) {
165
+ const payload = payloads[index % payloads.length];
166
+ const result = global.__repro_call(
167
+ inspectShipment,
168
+ null,
169
+ [service, payload],
170
+ '/app/src/shipment-workflow.service.ts',
171
+ 145,
172
+ 'inspectShipment',
173
+ false,
174
+ );
175
+ assert.strictEqual(result, payload, 'tracing changed return-value identity');
176
+ }
177
+ res.end();
178
+ resolve();
179
+ } catch (traceError) {
180
+ reject(traceError);
181
+ }
182
+ });
183
+ });
184
+
185
+ const deadline = Date.now() + 8000;
186
+ while (Date.now() < deadline) {
187
+ const events = posts.flatMap((post) => post.events || []);
188
+ const trace = events
189
+ .filter((event) => event.event_type === 'trace_batch')
190
+ .flatMap((event) => event.payload?.trace || []);
191
+ if (trace.filter((event) => event.fn === 'inspectShipment').length === CALL_COUNT * 2) {
192
+ break;
193
+ }
194
+ await sleep(20);
195
+ }
196
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
197
+ await flushIngestQueue();
198
+
199
+ const ingestEvents = posts.flatMap((post) => post.events || []);
200
+ const traceBatches = ingestEvents.filter((event) => event.event_type === 'trace_batch');
201
+ const traceEvents = traceBatches
202
+ .flatMap((event) => event.payload?.trace || [])
203
+ .filter((event) => event.fn === 'inspectShipment');
204
+ const definitions = traceBatches.flatMap((event) => event.payload?.traceValues || []);
205
+ const definitionsById = new Map(definitions.map((definition) => [definition.id, definition]));
206
+
207
+ assert.equal(traceEvents.length, CALL_COUNT * 2, 'trace occurrence evidence was dropped');
208
+ assert.equal(
209
+ definitionsById.size,
210
+ UNIQUE_PAYLOAD_COUNT * 2,
211
+ 'expected one args and one return definition per unique business payload',
212
+ );
213
+
214
+ const resolvedOccurrences = [];
215
+ for (const event of traceEvents) {
216
+ const ref = captureRef(event);
217
+ assert.equal(ref?.status, 'stored', `missing value ref for ${event.type}`);
218
+ const definition = definitionsById.get(ref.valueId);
219
+ assert(definition, `unresolved captured value ${ref.valueId}`);
220
+ resolvedOccurrences.push(definition.value);
221
+
222
+ const expectedVariant = Math.floor(resolvedOccurrences.length > 0
223
+ ? (resolvedOccurrences.length - 1) / 2
224
+ : 0) % UNIQUE_PAYLOAD_COUNT;
225
+ if (event.type === 'enter') {
226
+ assert.deepEqual(definition.value[1], payloads[expectedVariant]);
227
+ assert.deepEqual(definition.value[0].configService, {
228
+ __kind: 'repro-semantic',
229
+ category: 'configuration-service',
230
+ className: 'ConfigService',
231
+ });
232
+ assert.deepEqual(definition.value[0].logger, {
233
+ __kind: 'repro-semantic',
234
+ category: 'logger',
235
+ className: 'Logger',
236
+ context: 'ShipmentWorkflowService',
237
+ });
238
+ } else {
239
+ assert.deepEqual(definition.value, payloads[expectedVariant]);
240
+ }
241
+ }
242
+
243
+ const rawRuntimeBytes = byteLength(service);
244
+ const projectedRuntime = definitions.find((definition) => definition.target === 'trace.args')
245
+ ?.value?.[0];
246
+ assert(projectedRuntime, 'missing projected service argument');
247
+ const projectedRuntimeBytes = byteLength(projectedRuntime);
248
+ assert(
249
+ projectedRuntimeBytes < rawRuntimeBytes * 0.02,
250
+ `runtime projection budget exceeded: ${projectedRuntimeBytes}/${rawRuntimeBytes}`,
251
+ );
252
+
253
+ const legacyOccurrenceValueBytes = traceEvents.reduce((total, event) => {
254
+ const definition = definitionsById.get(captureRef(event).valueId);
255
+ return total + byteLength(definition.value);
256
+ }, 0);
257
+ const optimizedDefinitionBytes = Array.from(definitionsById.values())
258
+ .reduce((total, definition) => total + byteLength(definition.value), 0);
259
+ const optimizedRefBytes = traceEvents.reduce(
260
+ (total, event) => total + byteLength(captureRef(event)),
261
+ 0,
262
+ );
263
+ const optimizedValueBytes = optimizedDefinitionBytes + optimizedRefBytes;
264
+ const reduction = 1 - optimizedValueBytes / legacyOccurrenceValueBytes;
265
+
266
+ assert(
267
+ reduction > 0.85,
268
+ `content-addressed value storage reduced only ${(reduction * 100).toFixed(1)}%`,
269
+ );
270
+ const serializedCapture = JSON.stringify(posts);
271
+ assert(!serializedCapture.includes('runtime-password-1499'));
272
+ assert(!serializedCapture.includes('SG.runtime-secret'));
273
+ assert(serializedCapture.includes('KIT-11-00179'));
274
+
275
+ console.log('session storage budget OK', JSON.stringify({
276
+ calls: CALL_COUNT,
277
+ valueOccurrences: traceEvents.length,
278
+ uniqueDefinitions: definitionsById.size,
279
+ legacyOccurrenceValueBytes,
280
+ optimizedValueBytes,
281
+ contentAddressedReductionPercent: Number((reduction * 100).toFixed(2)),
282
+ rawRuntimeBytes,
283
+ projectedRuntimeBytes,
284
+ runtimeProjectionReductionPercent: Number(
285
+ ((1 - projectedRuntimeBytes / rawRuntimeBytes) * 100).toFixed(2),
286
+ ),
287
+ }));
288
+ } finally {
289
+ await __reproTestHooks.drainSdkBackgroundQueueForTest().catch(() => {});
290
+ await flushIngestQueue().catch(() => {});
291
+ global.fetch = originalFetch;
292
+ }
293
+ }
294
+
295
+ main().catch((error) => {
296
+ console.error(error);
297
+ process.exit(1);
298
+ });
@@ -76,6 +76,39 @@ function main() {
76
76
  assert.strictEqual(cappedEvents[500].fn, 'fn1000');
77
77
  assert.strictEqual(cappedEvents[cappedEvents.length - 1].fn, 'fn1499');
78
78
 
79
+ const distinctLargeValueEvents = Array.from({ length: 24 }, (_, index) => ({
80
+ ...makeEvent(index, { __kind: 'omitted', reason: 'inline-size-limit' }),
81
+ argsValueCapture: {
82
+ status: 'stored',
83
+ valueId: `rv1_${String(index).padStart(40, '0')}`,
84
+ },
85
+ __reproTraceValueEntries: [
86
+ {
87
+ id: `rv1_${String(index).padStart(40, '0')}`,
88
+ target: 'trace.args',
89
+ value: [{ index, exact: `VALUE-${index}`, body: 'z'.repeat(300 * 1024) }],
90
+ truncatedForStorage: false,
91
+ },
92
+ ],
93
+ }));
94
+ const distinctLargeValueBatches = __reproTestHooks.chunkTraceEventsForTransportForTest(
95
+ distinctLargeValueEvents,
96
+ 'RID_distinct_large_values',
97
+ 'A_distinct_large_values',
98
+ );
99
+ assert.equal(
100
+ distinctLargeValueBatches.flat().length,
101
+ distinctLargeValueEvents.length,
102
+ 'byte-based splitting must not apply the old ten-batch evidence cap',
103
+ );
104
+ assert(
105
+ distinctLargeValueBatches.length > 10,
106
+ `fixture did not exercise more than ten batches: ${distinctLargeValueBatches.length}`,
107
+ );
108
+ for (const [index, event] of distinctLargeValueBatches.flat().entries()) {
109
+ assert.equal(event.__reproTraceValueEntries[0].value[0].exact, `VALUE-${index}`);
110
+ }
111
+
79
112
  // eslint-disable-next-line no-console
80
113
  console.log('trace batch size chunking OK');
81
114
  }