@reproapp/node-sdk 0.0.52 → 0.0.53
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.js +6 -4
- package/dist/privacy.js +5 -4
- package/package.json +4 -4
- package/src/index.ts +5 -4
- package/src/privacy.ts +4 -4
- package/test/object-id-shapes.test.js +36 -0
- package/test/twelve-element-capture.testcontainers.test.js +116 -0
package/dist/index.js
CHANGED
|
@@ -2340,12 +2340,15 @@ function coerceMongoId(value) {
|
|
|
2340
2340
|
if (typeof value !== 'object') {
|
|
2341
2341
|
return null;
|
|
2342
2342
|
}
|
|
2343
|
+
// Argument lists and query-result arrays are business data, not BSON ids.
|
|
2344
|
+
if (Array.isArray(value))
|
|
2345
|
+
return null;
|
|
2343
2346
|
const bsonType = typeof value?._bsontype === 'string' ? String(value._bsontype).toLowerCase() : '';
|
|
2344
2347
|
const ctorName = typeof value?.constructor?.name === 'string' ? String(value.constructor.name).toLowerCase() : '';
|
|
2345
2348
|
const looksLikeObjectId = bsonType.includes('objectid') ||
|
|
2346
2349
|
ctorName === 'objectid' ||
|
|
2347
2350
|
typeof value.toHexString === 'function';
|
|
2348
|
-
const fromBufferShape = coerceMongoIdFromBufferShape(value,
|
|
2351
|
+
const fromBufferShape = looksLikeObjectId ? coerceMongoIdFromBufferShape(value, true) : null;
|
|
2349
2352
|
if (fromBufferShape) {
|
|
2350
2353
|
return fromBufferShape;
|
|
2351
2354
|
}
|
|
@@ -2422,12 +2425,11 @@ function coerceMongoIdFromBufferShape(value, includeIdCandidate = false) {
|
|
|
2422
2425
|
if (!Array.isArray(bytes) || bytes.length !== 12) {
|
|
2423
2426
|
return null;
|
|
2424
2427
|
}
|
|
2425
|
-
|
|
2426
|
-
if (normalized.some((byte) => !Number.isInteger(byte) || byte < 0 || byte > 255)) {
|
|
2428
|
+
if (bytes.some((byte) => typeof byte !== 'number' || !Number.isInteger(byte) || byte < 0 || byte > 255)) {
|
|
2427
2429
|
return null;
|
|
2428
2430
|
}
|
|
2429
2431
|
try {
|
|
2430
|
-
return Buffer.from(
|
|
2432
|
+
return Buffer.from(bytes).toString('hex');
|
|
2431
2433
|
}
|
|
2432
2434
|
catch {
|
|
2433
2435
|
return null;
|
package/dist/privacy.js
CHANGED
|
@@ -2891,12 +2891,14 @@ function coercePrivacyObjectId(value) {
|
|
|
2891
2891
|
if (typeof value !== 'object') {
|
|
2892
2892
|
return null;
|
|
2893
2893
|
}
|
|
2894
|
+
if (Array.isArray(value))
|
|
2895
|
+
return null;
|
|
2894
2896
|
const bsonType = typeof value?._bsontype === 'string' ? String(value._bsontype).toLowerCase() : '';
|
|
2895
2897
|
const ctorName = typeof value?.constructor?.name === 'string' ? String(value.constructor.name).toLowerCase() : '';
|
|
2896
2898
|
const looksLikeObjectId = bsonType.includes('objectid') ||
|
|
2897
2899
|
ctorName === 'objectid' ||
|
|
2898
2900
|
typeof value.toHexString === 'function';
|
|
2899
|
-
const fromBufferShape = coercePrivacyObjectIdFromBufferShape(value,
|
|
2901
|
+
const fromBufferShape = looksLikeObjectId ? coercePrivacyObjectIdFromBufferShape(value, true) : null;
|
|
2900
2902
|
if (fromBufferShape) {
|
|
2901
2903
|
return fromBufferShape;
|
|
2902
2904
|
}
|
|
@@ -2970,12 +2972,11 @@ function coercePrivacyObjectIdFromBufferShape(value, includeIdCandidate = false)
|
|
|
2970
2972
|
if (!Array.isArray(bytes) || bytes.length !== 12) {
|
|
2971
2973
|
return null;
|
|
2972
2974
|
}
|
|
2973
|
-
|
|
2974
|
-
if (normalized.some((byte) => !Number.isInteger(byte) || byte < 0 || byte > 255)) {
|
|
2975
|
+
if (bytes.some((byte) => typeof byte !== 'number' || !Number.isInteger(byte) || byte < 0 || byte > 255)) {
|
|
2975
2976
|
return null;
|
|
2976
2977
|
}
|
|
2977
2978
|
try {
|
|
2978
|
-
return Buffer.from(
|
|
2979
|
+
return Buffer.from(bytes).toString('hex');
|
|
2979
2980
|
}
|
|
2980
2981
|
catch {
|
|
2981
2982
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.53",
|
|
4
4
|
"description": "Repro Nest SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test:latency": "npm run build && node test/privacy-fetch-timeout.test.js && node test/capture-latency.testcontainers.test.js",
|
|
13
|
-
"test:live-capture": "npm run build && node test/live-capture-limits.test.js && node test/ingest-throttle-retention.test.js && node test/live-capture-limits.testcontainers.test.js",
|
|
13
|
+
"test:live-capture": "npm run build && node test/twelve-element-capture.testcontainers.test.js && node test/live-capture-limits.test.js && node test/ingest-throttle-retention.test.js && node test/live-capture-limits.testcontainers.test.js",
|
|
14
14
|
"test:live-brokers": "npm run build && node test/live-broker-limits.testcontainers.test.js",
|
|
15
15
|
"build": "tsc -p tsconfig.json",
|
|
16
16
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
17
17
|
"prepublishOnly": "npm run build",
|
|
18
|
-
"test": "npm run build && node test/semantic-serialization.test.js && node test/semantic-serialization-trace.test.js && node test/content-addressed-trace-values.test.js && node test/trace-observation-snapshots.test.js && node test/session-storage-budget.test.js && node test/trace-source-exclude.test.js && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-capture-background.test.js && node test/init-auto-http-server.test.js && node test/backend-live-mode.test.js && node test/client-performance-regression.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/sdk-background-timeout-isolation.test.js && node test/runtime-service-registration.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-high-priority-fairness.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-batching.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/function-arg-identity.test.js && node test/privacy-service-scope.test.js && node test/privacy-policy-index.test.js && node test/policy-capture-attestation.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js && node test/postgresql-integration.test.js",
|
|
19
|
-
"test:containers": "npm run build && node test/background-continuous-traffic.testcontainers.test.js && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js && node test/function-arg-identity.testcontainers.test.js && node test/mongodb-mongoose-native-suppression.testcontainers.test.js && node test/content-addressed-mongodb.testcontainers.test.js && node test/policy-session-completion.testcontainers.test.js && node test/postgresql-integrations.testcontainers.test.js"
|
|
18
|
+
"test": "npm run build && node test/semantic-serialization.test.js && node test/semantic-serialization-trace.test.js && node test/content-addressed-trace-values.test.js && node test/trace-observation-snapshots.test.js && node test/session-storage-budget.test.js && node test/trace-source-exclude.test.js && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-capture-background.test.js && node test/init-auto-http-server.test.js && node test/backend-live-mode.test.js && node test/client-performance-regression.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/sdk-background-timeout-isolation.test.js && node test/runtime-service-registration.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-high-priority-fairness.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-batching.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/function-arg-identity.test.js && node test/privacy-service-scope.test.js && node test/privacy-policy-index.test.js && node test/policy-capture-attestation.test.js && node test/object-id-shapes.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js && node test/postgresql-integration.test.js",
|
|
19
|
+
"test:containers": "npm run build && node test/twelve-element-capture.testcontainers.test.js && node test/background-continuous-traffic.testcontainers.test.js && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js && node test/function-arg-identity.testcontainers.test.js && node test/mongodb-mongoose-native-suppression.testcontainers.test.js && node test/content-addressed-mongodb.testcontainers.test.js && node test/policy-session-completion.testcontainers.test.js && node test/postgresql-integrations.testcontainers.test.js"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"express": "^5.1.0",
|
package/src/index.ts
CHANGED
|
@@ -2906,6 +2906,8 @@ function coerceMongoId(value: any): string | null {
|
|
|
2906
2906
|
if (typeof value !== 'object') {
|
|
2907
2907
|
return null;
|
|
2908
2908
|
}
|
|
2909
|
+
// Argument lists and query-result arrays are business data, not BSON ids.
|
|
2910
|
+
if (Array.isArray(value)) return null;
|
|
2909
2911
|
|
|
2910
2912
|
const bsonType = typeof (value as any)?._bsontype === 'string' ? String((value as any)._bsontype).toLowerCase() : '';
|
|
2911
2913
|
const ctorName = typeof value?.constructor?.name === 'string' ? String(value.constructor.name).toLowerCase() : '';
|
|
@@ -2914,7 +2916,7 @@ function coerceMongoId(value: any): string | null {
|
|
|
2914
2916
|
ctorName === 'objectid' ||
|
|
2915
2917
|
typeof (value as any).toHexString === 'function';
|
|
2916
2918
|
|
|
2917
|
-
const fromBufferShape = coerceMongoIdFromBufferShape(value,
|
|
2919
|
+
const fromBufferShape = looksLikeObjectId ? coerceMongoIdFromBufferShape(value, true) : null;
|
|
2918
2920
|
if (fromBufferShape) {
|
|
2919
2921
|
return fromBufferShape;
|
|
2920
2922
|
}
|
|
@@ -2994,13 +2996,12 @@ function coerceMongoIdFromBufferShape(value: any, includeIdCandidate: boolean =
|
|
|
2994
2996
|
return null;
|
|
2995
2997
|
}
|
|
2996
2998
|
|
|
2997
|
-
|
|
2998
|
-
if (normalized.some((byte) => !Number.isInteger(byte) || byte < 0 || byte > 255)) {
|
|
2999
|
+
if (bytes.some((byte) => typeof byte !== 'number' || !Number.isInteger(byte) || byte < 0 || byte > 255)) {
|
|
2999
3000
|
return null;
|
|
3000
3001
|
}
|
|
3001
3002
|
|
|
3002
3003
|
try {
|
|
3003
|
-
return Buffer.from(
|
|
3004
|
+
return Buffer.from(bytes).toString('hex');
|
|
3004
3005
|
} catch {
|
|
3005
3006
|
return null;
|
|
3006
3007
|
}
|
package/src/privacy.ts
CHANGED
|
@@ -3685,6 +3685,7 @@ function coercePrivacyObjectId(value: any): string | null {
|
|
|
3685
3685
|
if (typeof value !== 'object') {
|
|
3686
3686
|
return null;
|
|
3687
3687
|
}
|
|
3688
|
+
if (Array.isArray(value)) return null;
|
|
3688
3689
|
|
|
3689
3690
|
const bsonType = typeof (value as any)?._bsontype === 'string' ? String((value as any)._bsontype).toLowerCase() : '';
|
|
3690
3691
|
const ctorName = typeof value?.constructor?.name === 'string' ? String(value.constructor.name).toLowerCase() : '';
|
|
@@ -3693,7 +3694,7 @@ function coercePrivacyObjectId(value: any): string | null {
|
|
|
3693
3694
|
ctorName === 'objectid' ||
|
|
3694
3695
|
typeof value.toHexString === 'function';
|
|
3695
3696
|
|
|
3696
|
-
const fromBufferShape = coercePrivacyObjectIdFromBufferShape(value,
|
|
3697
|
+
const fromBufferShape = looksLikeObjectId ? coercePrivacyObjectIdFromBufferShape(value, true) : null;
|
|
3697
3698
|
if (fromBufferShape) {
|
|
3698
3699
|
return fromBufferShape;
|
|
3699
3700
|
}
|
|
@@ -3774,13 +3775,12 @@ function coercePrivacyObjectIdFromBufferShape(value: any, includeIdCandidate: bo
|
|
|
3774
3775
|
return null;
|
|
3775
3776
|
}
|
|
3776
3777
|
|
|
3777
|
-
|
|
3778
|
-
if (normalized.some((byte) => !Number.isInteger(byte) || byte < 0 || byte > 255)) {
|
|
3778
|
+
if (bytes.some((byte) => typeof byte !== 'number' || !Number.isInteger(byte) || byte < 0 || byte > 255)) {
|
|
3779
3779
|
return null;
|
|
3780
3780
|
}
|
|
3781
3781
|
|
|
3782
3782
|
try {
|
|
3783
|
-
return Buffer.from(
|
|
3783
|
+
return Buffer.from(bytes).toString('hex');
|
|
3784
3784
|
} catch {
|
|
3785
3785
|
return null;
|
|
3786
3786
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const { ObjectId } = require('mongodb');
|
|
3
|
+
const { __reproTestHooks } = require('../dist');
|
|
4
|
+
const { applyRuntimePrivacyPolicy, applyRuntimePrivacyPolicyAsync } = require('../dist/privacy');
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const serializers = {
|
|
8
|
+
trace: value => __reproTestHooks.sanitizeTraceValueForPrivacyForTest(value),
|
|
9
|
+
policy: value => applyRuntimePrivacyPolicy(null, 'trace.args', value, { appId: 'APP_test' }),
|
|
10
|
+
asyncPolicy: value => applyRuntimePrivacyPolicyAsync(null, 'db.resultMeta', value, { appId: 'APP_test' }),
|
|
11
|
+
};
|
|
12
|
+
const bytes = Array.from({ length: 12 }, (_, i) => i);
|
|
13
|
+
const hex = '000102030405060708090a0b';
|
|
14
|
+
for (const [name, serialize] of Object.entries(serializers)) {
|
|
15
|
+
const rows = bytes.map(ordinal => Object.assign(Object.create(null), { ordinal, status: 'active' }));
|
|
16
|
+
const numericKeys = Object.fromEntries(bytes.map(i => [i, i]));
|
|
17
|
+
for (const input of [bytes, rows, numericKeys, { buffer: bytes }, { type: 'Buffer', data: bytes }]) {
|
|
18
|
+
const result = await serialize(input);
|
|
19
|
+
assert.deepEqual(JSON.parse(JSON.stringify(result)), JSON.parse(JSON.stringify(input)),
|
|
20
|
+
`${name}: ordinary data must not be collapsed to a BSON id`);
|
|
21
|
+
}
|
|
22
|
+
for (const input of [new ObjectId(hex), { $oid: hex }, { _bsontype: 'ObjectId', buffer: bytes }]) {
|
|
23
|
+
assert.equal(await serialize(input), hex, `${name}: genuine ObjectId representation must remain supported`);
|
|
24
|
+
}
|
|
25
|
+
let coercions = 0;
|
|
26
|
+
const invalidByte = { valueOf() { coercions += 1; throw new Error('must not coerce application objects'); } };
|
|
27
|
+
for (const invalid of [invalidByte, Object.create(null), '1', -1, 256, 1.5, NaN]) {
|
|
28
|
+
const input = { _bsontype: 'ObjectId', buffer: [...bytes.slice(0, 11), invalid] };
|
|
29
|
+
assert.equal(await serialize(input), '[mongo-id]', `${name}: invalid bytes must not become a fabricated ObjectId`);
|
|
30
|
+
}
|
|
31
|
+
assert.equal(coercions, 0);
|
|
32
|
+
}
|
|
33
|
+
console.log('ObjectId discrimination passed for trace, synchronous and asynchronous privacy serialization');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
main().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
|
|
2
|
+
process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
|
|
3
|
+
process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '30';
|
|
4
|
+
process.env.TRACE_IDLE_FLUSH_MS = '10';
|
|
5
|
+
process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
const http = require('node:http');
|
|
8
|
+
const express = require('express');
|
|
9
|
+
const mongoose = require('mongoose');
|
|
10
|
+
const { GenericContainer, Wait } = require('testcontainers');
|
|
11
|
+
const { initReproTracing, reproMiddleware, reproMongoosePlugin } = require('../dist');
|
|
12
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
13
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
let container, connection, server, sink;
|
|
17
|
+
const posts = [];
|
|
18
|
+
try {
|
|
19
|
+
sink = http.createServer(async (req, res) => {
|
|
20
|
+
const parts = []; for await (const part of req) parts.push(part);
|
|
21
|
+
posts.push(JSON.parse(Buffer.concat(parts).toString()));
|
|
22
|
+
res.writeHead(202); res.end('{}');
|
|
23
|
+
});
|
|
24
|
+
await new Promise(resolve => sink.listen(0, '127.0.0.1', resolve));
|
|
25
|
+
container = await new GenericContainer('mongo:7').withExposedPorts(27017)
|
|
26
|
+
.withWaitStrategy(Wait.forListeningPorts()).start();
|
|
27
|
+
connection = await mongoose.createConnection(`mongodb://${container.getHost()}:${container.getMappedPort(27017)}/twelve_elements`).asPromise();
|
|
28
|
+
initReproTracing({ instrument: false, logFunctionCalls: false, fullValueCapture: true });
|
|
29
|
+
const app = express();
|
|
30
|
+
const policy = { environment: 'dev', statements: [{ sid: 'drop-secret',
|
|
31
|
+
when: { scope: 'FIELD', selector: 'field:internalSecret', target: '**.internalSecret' },
|
|
32
|
+
apply: { action: 'DROP' }, meta: { priority: 1 } }] };
|
|
33
|
+
for (const privacy of ['policy', 'raw']) {
|
|
34
|
+
const config = { tenantId: 'TENANT_twelve', appId: 'APP_twelve', appSecret: 'test',
|
|
35
|
+
serviceName: `depot-${privacy}`, captureHeaders: false,
|
|
36
|
+
privacy: { environment: 'dev', captureRaw: privacy === 'raw', ...(privacy === 'policy' ? { policy } : {}) },
|
|
37
|
+
ingestBase: `http://127.0.0.1:${sink.address().port}` };
|
|
38
|
+
const schema = new mongoose.Schema({ ordinal: Number, location: String, internalSecret: String });
|
|
39
|
+
schema.plugin(reproMongoosePlugin(config));
|
|
40
|
+
const Depot = connection.model(`Depot_${privacy}`, schema);
|
|
41
|
+
await Depot.insertMany(Array.from({ length: 13 }, (_, ordinal) => ({ ordinal, location: `Depot ${ordinal}`, internalSecret: 'private-fixture-secret' })));
|
|
42
|
+
app.use(`/${privacy}`, reproMiddleware(config));
|
|
43
|
+
app.get(`/${privacy}/:count`, async (req, res, next) => {
|
|
44
|
+
try {
|
|
45
|
+
const rows = await Depot.find({}).sort({ ordinal: 1 }).limit(Number(req.params.count)).lean().exec();
|
|
46
|
+
function projectDepots(input) { return input; }
|
|
47
|
+
const observed = global.__repro_call(projectDepots, null, [rows], __filename, 1, 'projectDepots', false);
|
|
48
|
+
function numericArguments(...values) { return values; }
|
|
49
|
+
const numbers = global.__repro_call(numericArguments, null, Array.from({ length: 12 }, (_, i) => i), __filename, 2, 'numericArguments', false);
|
|
50
|
+
res.json({ rows: observed, numbers });
|
|
51
|
+
} catch (error) { next(error); }
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
server = await new Promise(resolve => { const listener = app.listen(0, '127.0.0.1', () => resolve(listener)); });
|
|
55
|
+
const scenarios = [];
|
|
56
|
+
for (const mode of ['live', 'staging']) for (const privacy of ['policy', 'raw']) for (const count of [12, 11, 13]) {
|
|
57
|
+
const sid = `S_${mode}_${privacy}_${count}`;
|
|
58
|
+
const response = await fetch(`http://127.0.0.1:${server.address().port}/${privacy}/${count}`, { headers: {
|
|
59
|
+
'x-bug-session-id': sid, 'x-bug-action-id': `A_${sid}`, 'x-repro-capture-mode': mode,
|
|
60
|
+
} });
|
|
61
|
+
assert.equal(response.status, 200);
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
assert.equal(data.rows.length, count);
|
|
64
|
+
assert.equal(data.rows[0].internalSecret, 'private-fixture-secret', 'SDK must not mutate business results');
|
|
65
|
+
assert.deepEqual(data.numbers, Array.from({ length: 12 }, (_, i) => i));
|
|
66
|
+
scenarios.push({ sid, mode, privacy, count });
|
|
67
|
+
}
|
|
68
|
+
let events;
|
|
69
|
+
const deadline = Date.now() + 30000;
|
|
70
|
+
do {
|
|
71
|
+
await flushIngestQueue();
|
|
72
|
+
events = posts.flatMap(post => post.events || []);
|
|
73
|
+
if (events.filter(e => e.event_type === 'request_capture_manifest').length >= scenarios.length) break;
|
|
74
|
+
await sleep(50);
|
|
75
|
+
} while (Date.now() < deadline);
|
|
76
|
+
for (const { sid, privacy, count } of scenarios) {
|
|
77
|
+
const scoped = events.filter(e => e.session_id === sid);
|
|
78
|
+
const manifest = scoped.find(e => e.event_type === 'request_capture_manifest')?.payload.requestCaptureManifest;
|
|
79
|
+
assert(manifest, `missing manifest ${sid}`);
|
|
80
|
+
assert.equal(manifest.captureFailures, undefined, `${sid}: ${JSON.stringify(manifest.captureFailures)}`);
|
|
81
|
+
if (privacy === 'policy') assert.equal(manifest.privacy.status, 'applied');
|
|
82
|
+
const values = scoped.flatMap(e => [...(e.payload.traceValues || []), ...(e.payload.requestValues || []), ...(e.payload.dbValues || [])]);
|
|
83
|
+
const resolve = (record, ref, field) => values.find(v => v.id === record?.[ref]?.valueId)?.value ?? record?.[field];
|
|
84
|
+
const trace = scoped.flatMap(e => e.payload.trace || []);
|
|
85
|
+
const args = resolve(trace.find(e => e.fn === 'projectDepots' && e.type === 'enter'), 'argsValueCapture', 'args');
|
|
86
|
+
const result = resolve(trace.find(e => e.fn === 'projectDepots' && e.type === 'exit'), 'returnValueCapture', 'returnValue');
|
|
87
|
+
for (const rows of [args?.[0], result]) {
|
|
88
|
+
assert.equal(rows?.length, count, `missing business array ${sid}`);
|
|
89
|
+
assert.equal(rows.at(-1).ordinal, count - 1);
|
|
90
|
+
assert.equal(rows[0].internalSecret, privacy === 'policy' ? '[dropped]' : 'private-fixture-secret');
|
|
91
|
+
}
|
|
92
|
+
const numbers = resolve(trace.find(e => e.fn === 'numericArguments' && e.type === 'enter'), 'argsValueCapture', 'args');
|
|
93
|
+
assert.deepEqual(numbers, Array.from({ length: 12 }, (_, i) => i), 'numeric argument list must not become an ObjectId');
|
|
94
|
+
const request = scoped.find(e => e.event_type === 'backend_request')?.payload.request;
|
|
95
|
+
const body = resolve(request, 'respBodyValueCapture', 'respBody');
|
|
96
|
+
assert.equal(body.rows.length, count);
|
|
97
|
+
const dbRead = scoped.flatMap(e => e.payload.db || []).find(e => e.op === 'find');
|
|
98
|
+
assert(dbRead, `missing Mongo read ${sid}`);
|
|
99
|
+
const dbResult = resolve(dbRead, 'resultMetaValueCapture', 'resultMeta');
|
|
100
|
+
const rows = Array.isArray(dbResult) ? dbResult : dbResult?.result;
|
|
101
|
+
assert.equal(rows?.length, count, `missing Mongo result ${sid}: ${JSON.stringify({ dbRead, dbResult })}`);
|
|
102
|
+
assert.equal(rows[0].internalSecret, privacy === 'policy' ? '[dropped]' : 'private-fixture-secret');
|
|
103
|
+
assert.equal(scoped.filter(e => e.event_type === 'trace_batch').length, manifest.expected.traceBatch);
|
|
104
|
+
assert.equal(scoped.flatMap(e => e.payload.db || []).length, manifest.expected.dbChange);
|
|
105
|
+
if (privacy === 'policy') assert(!JSON.stringify(scoped).includes('private-fixture-secret'), 'secret leaked into captured evidence');
|
|
106
|
+
}
|
|
107
|
+
console.log('12 live/staging policy/raw scenarios: business arrays, numeric arguments, Mongo results and manifests passed');
|
|
108
|
+
} finally {
|
|
109
|
+
if (server) await new Promise(resolve => server.close(resolve));
|
|
110
|
+
if (connection) await connection.close();
|
|
111
|
+
await flushIngestQueue();
|
|
112
|
+
if (sink) await new Promise(resolve => sink.close(resolve));
|
|
113
|
+
if (container) await container.stop();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
main().catch(error => { console.error(error); process.exitCode = 1; });
|