@reproapp/node-sdk 0.0.46 → 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.
- package/dist/index.d.ts +16 -3
- package/dist/index.js +327 -159
- package/dist/ingest/client.js +11 -0
- package/dist/privacy.d.ts +1 -0
- package/dist/privacy.js +306 -7
- package/dist/semantic-serialization.d.ts +9 -0
- package/dist/semantic-serialization.js +171 -0
- package/package.json +3 -3
- package/src/index.ts +513 -144
- package/src/ingest/client.ts +13 -0
- package/src/privacy.ts +409 -7
- package/src/semantic-serialization.ts +182 -0
- package/test/content-addressed-mongodb.testcontainers.test.js +348 -0
- package/test/content-addressed-trace-values.test.js +264 -0
- package/test/fixtures/semantic-serialization-trace-controller.js +85 -0
- package/test/fixtures/semantic-serialization-trace-server.js +19 -0
- package/test/ingest-high-priority-fairness.test.js +76 -0
- package/test/policy-capture-attestation.test.js +234 -0
- package/test/policy-session-completion.testcontainers.test.js +271 -0
- package/test/privacy-policy-index.test.js +194 -0
- package/test/sdk-background-timeout-isolation.test.js +39 -0
- package/test/semantic-serialization-trace.test.js +152 -0
- package/test/semantic-serialization.test.js +229 -0
- package/test/session-storage-budget.test.js +298 -0
- package/test/trace-batch-size.test.js +37 -0
|
@@ -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
|
+
});
|
|
@@ -43,6 +43,10 @@ function main() {
|
|
|
43
43
|
);
|
|
44
44
|
|
|
45
45
|
batches.forEach((batch, index) => {
|
|
46
|
+
assert(
|
|
47
|
+
batch.length <= 100,
|
|
48
|
+
`batch ${index} exceeded the 100-event transport limit: ${batch.length}`,
|
|
49
|
+
);
|
|
46
50
|
const size = __reproTestHooks.estimateTraceBatchSerializedBytesForTest(
|
|
47
51
|
batch,
|
|
48
52
|
'RID_test_trace_batch_size',
|
|
@@ -72,6 +76,39 @@ function main() {
|
|
|
72
76
|
assert.strictEqual(cappedEvents[500].fn, 'fn1000');
|
|
73
77
|
assert.strictEqual(cappedEvents[cappedEvents.length - 1].fn, 'fn1499');
|
|
74
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
|
+
|
|
75
112
|
// eslint-disable-next-line no-console
|
|
76
113
|
console.log('trace batch size chunking OK');
|
|
77
114
|
}
|