@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.
- package/dist/index.d.ts +6 -1
- package/dist/index.js +95 -39
- package/dist/semantic-serialization.d.ts +9 -0
- package/dist/semantic-serialization.js +171 -0
- package/package.json +3 -3
- package/src/index.ts +161 -39
- 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/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 +33 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export type SemanticProjection = {
|
|
2
|
+
matched: true;
|
|
3
|
+
value: Record<string, unknown>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
type DataDescriptor = PropertyDescriptor & { value: unknown };
|
|
7
|
+
|
|
8
|
+
function constructorName(value: unknown): string {
|
|
9
|
+
if (!value || typeof value !== 'object') return '';
|
|
10
|
+
try {
|
|
11
|
+
const prototype = Object.getPrototypeOf(value);
|
|
12
|
+
const constructor = prototype
|
|
13
|
+
? Object.getOwnPropertyDescriptor(prototype, 'constructor')?.value
|
|
14
|
+
: undefined;
|
|
15
|
+
const name = constructor?.name;
|
|
16
|
+
return typeof name === 'string' ? name : '';
|
|
17
|
+
} catch {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ownDataDescriptors(value: object): Record<string, DataDescriptor> {
|
|
23
|
+
const out: Record<string, DataDescriptor> = {};
|
|
24
|
+
let descriptors: Record<string, PropertyDescriptor>;
|
|
25
|
+
try {
|
|
26
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
27
|
+
} catch {
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
31
|
+
if (Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
32
|
+
out[key] = descriptor as DataDescriptor;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function method(value: unknown, key: string): boolean {
|
|
39
|
+
try {
|
|
40
|
+
let current: object | null = value as object;
|
|
41
|
+
while (current) {
|
|
42
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
43
|
+
if (descriptor) {
|
|
44
|
+
return Object.prototype.hasOwnProperty.call(descriptor, 'value') &&
|
|
45
|
+
typeof descriptor.value === 'function';
|
|
46
|
+
}
|
|
47
|
+
current = Object.getPrototypeOf(current);
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function primitiveStateValue(value: unknown): string | number | boolean | null | undefined {
|
|
56
|
+
if (value === null) return null;
|
|
57
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === 'bigint') return value.toString();
|
|
61
|
+
if (value instanceof Date) return value.toISOString();
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function semanticValue(
|
|
66
|
+
category: string,
|
|
67
|
+
className: string,
|
|
68
|
+
details: Record<string, unknown> = {},
|
|
69
|
+
): SemanticProjection {
|
|
70
|
+
return {
|
|
71
|
+
matched: true,
|
|
72
|
+
value: {
|
|
73
|
+
__kind: 'repro-semantic',
|
|
74
|
+
category,
|
|
75
|
+
...(className ? { className } : {}),
|
|
76
|
+
...details,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function projectConfigurationService(value: object, className: string): SemanticProjection | null {
|
|
82
|
+
if (!/(^|Api|App|Runtime|Environment)ConfigService$/i.test(className)) return null;
|
|
83
|
+
if (!method(value, 'get') && !method(value, 'getOrThrow')) return null;
|
|
84
|
+
return semanticValue('configuration-service', className);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function projectLogger(value: object, className: string): SemanticProjection | null {
|
|
88
|
+
if (!/(Logger|LoggerService)$/i.test(className)) return null;
|
|
89
|
+
const descriptors = ownDataDescriptors(value);
|
|
90
|
+
const context = primitiveStateValue(descriptors.context?.value);
|
|
91
|
+
return semanticValue('logger', className, context !== undefined ? { context } : {});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function projectNestInfrastructure(value: object, className: string): SemanticProjection | null {
|
|
95
|
+
if (!/(ModuleRef|ModulesContainer|InstanceWrapper|NestContainer|Injector|DiscoveryService|ExecutionContextHost|ArgumentsHost|HttpAdapterHost)$/i.test(className)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return semanticValue('nestjs-infrastructure', className);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function projectRedis(value: object, className: string): SemanticProjection | null {
|
|
102
|
+
const looksLikeRedis = /Redis|Commander|Cluster/i.test(className) && (
|
|
103
|
+
method(value, 'get') || method(value, 'set') || method(value, 'pipeline') || method(value, 'multi')
|
|
104
|
+
);
|
|
105
|
+
if (!looksLikeRedis) return null;
|
|
106
|
+
const descriptors = ownDataDescriptors(value);
|
|
107
|
+
const status = primitiveStateValue(descriptors.status?.value);
|
|
108
|
+
return semanticValue('redis-client', className, status !== undefined ? { status } : {});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function projectKafka(value: object, className: string): SemanticProjection | null {
|
|
112
|
+
if (!/Kafka/i.test(className)) return null;
|
|
113
|
+
if (method(value, 'send') && method(value, 'connect') && method(value, 'disconnect')) {
|
|
114
|
+
return semanticValue('kafka-producer', className || 'KafkaProducer');
|
|
115
|
+
}
|
|
116
|
+
if (method(value, 'subscribe') && method(value, 'run') && method(value, 'connect')) {
|
|
117
|
+
return semanticValue('kafka-consumer', className || 'KafkaConsumer');
|
|
118
|
+
}
|
|
119
|
+
if (method(value, 'listTopics') && method(value, 'connect') && method(value, 'disconnect')) {
|
|
120
|
+
return semanticValue('kafka-admin', className || 'KafkaAdmin');
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function projectAmqp(value: object, className: string): SemanticProjection | null {
|
|
126
|
+
if (!/(Amqp|Channel|ChannelModel|ConfirmChannel)/i.test(className)) return null;
|
|
127
|
+
if (method(value, 'publish') && method(value, 'sendToQueue') && method(value, 'ack')) {
|
|
128
|
+
return semanticValue('amqp-channel', className || 'AmqpChannel');
|
|
129
|
+
}
|
|
130
|
+
if (method(value, 'createChannel') && method(value, 'close')) {
|
|
131
|
+
return semanticValue('amqp-connection', className || 'AmqpConnection');
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function projectPostgres(value: object, className: string): SemanticProjection | null {
|
|
137
|
+
if (/^(Pool|BoundPool)$/i.test(className) && method(value, 'query') && method(value, 'connect')) {
|
|
138
|
+
return semanticValue('postgres-pool', className);
|
|
139
|
+
}
|
|
140
|
+
if (/^(Client|PoolClient)$/i.test(className) && method(value, 'query')) {
|
|
141
|
+
return semanticValue('postgres-client', className);
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function projectTypeOrm(value: object, className: string): SemanticProjection | null {
|
|
147
|
+
if (/^DataSource$/i.test(className) && method(value, 'createQueryRunner')) {
|
|
148
|
+
return semanticValue('typeorm-infrastructure', className);
|
|
149
|
+
}
|
|
150
|
+
if (/^EntityManager$/i.test(className) && method(value, 'getRepository') && method(value, 'query')) {
|
|
151
|
+
return semanticValue('typeorm-infrastructure', className);
|
|
152
|
+
}
|
|
153
|
+
if (/^QueryRunner$/i.test(className) && method(value, 'query') && method(value, 'release')) {
|
|
154
|
+
return semanticValue('typeorm-infrastructure', className);
|
|
155
|
+
}
|
|
156
|
+
if (/^Repository$/i.test(className) && method(value, 'find') && method(value, 'save')) {
|
|
157
|
+
return semanticValue('typeorm-infrastructure', className);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Projects known runtime machinery into stable, non-recursive descriptions.
|
|
164
|
+
* Unknown application values deliberately return null and remain fully captured.
|
|
165
|
+
*/
|
|
166
|
+
export function projectSemanticInfrastructureValue(value: unknown): SemanticProjection | null {
|
|
167
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
168
|
+
|
|
169
|
+
const className = constructorName(value);
|
|
170
|
+
if (!className || className === 'Object') return null;
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
projectConfigurationService(value, className) ||
|
|
174
|
+
projectLogger(value, className) ||
|
|
175
|
+
projectNestInfrastructure(value, className) ||
|
|
176
|
+
projectRedis(value, className) ||
|
|
177
|
+
projectKafka(value, className) ||
|
|
178
|
+
projectAmqp(value, className) ||
|
|
179
|
+
projectPostgres(value, className) ||
|
|
180
|
+
projectTypeOrm(value, className)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
process.env.REPRO_INGEST_REQUEST_TIMEOUT_MS = '100';
|
|
2
|
+
process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '50';
|
|
3
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
|
|
4
|
+
process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
|
|
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 assert = require('node:assert/strict');
|
|
10
|
+
const { EventEmitter } = require('node:events');
|
|
11
|
+
const mongoose = require('mongoose');
|
|
12
|
+
const { GenericContainer, Wait } = require('testcontainers');
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
initReproTracing,
|
|
16
|
+
reproMiddleware,
|
|
17
|
+
reproMongoosePlugin,
|
|
18
|
+
__reproTestHooks,
|
|
19
|
+
} = require('../dist');
|
|
20
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
21
|
+
|
|
22
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
+
const byteLength = (value) => Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
24
|
+
|
|
25
|
+
class Logger {
|
|
26
|
+
constructor(context) {
|
|
27
|
+
this.context = context;
|
|
28
|
+
this.buffers = Array.from({ length: 200 }, () => 'mongo-test-logger-buffer');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
log() {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class ConfigService {
|
|
35
|
+
constructor() {
|
|
36
|
+
this.cache = {
|
|
37
|
+
DATABASE_URL: 'mongodb://captured-config-secret@database/repro',
|
|
38
|
+
};
|
|
39
|
+
this.internalConfig = Array.from({ length: 1000 }, (_, index) => ({
|
|
40
|
+
index,
|
|
41
|
+
credential: `mongo-config-credential-${index}`,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get(key) {
|
|
46
|
+
return this.cache[key];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class SubjectRepository {
|
|
51
|
+
constructor(model, configService) {
|
|
52
|
+
this.model = model;
|
|
53
|
+
this.configService = configService;
|
|
54
|
+
this.logger = new Logger('SubjectRepository');
|
|
55
|
+
this.collectionName = 'subjects';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function loadSubjectRepeatedly(repository, query) {
|
|
60
|
+
const docs = [];
|
|
61
|
+
for (let index = 0; index < 6; index += 1) {
|
|
62
|
+
docs.push(await repository.model.findOne(query).lean().exec());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let duplicateCode = null;
|
|
66
|
+
try {
|
|
67
|
+
await repository.model.create({
|
|
68
|
+
...docs[0],
|
|
69
|
+
_id: undefined,
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
duplicateCode = error?.code ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
repositoryIdentityPreserved: repository instanceof SubjectRepository,
|
|
77
|
+
duplicateCode,
|
|
78
|
+
docs,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeSubject() {
|
|
83
|
+
return {
|
|
84
|
+
subjectNumber: 'SUBJECT-CONTENT-1001',
|
|
85
|
+
status: 'screened',
|
|
86
|
+
visits: Array.from({ length: 80 }, (_, visitIndex) => ({
|
|
87
|
+
visitNumber: `VISIT-${String(visitIndex).padStart(3, '0')}`,
|
|
88
|
+
assessments: Array.from({ length: 5 }, (_, assessmentIndex) => ({
|
|
89
|
+
code: `ASSESSMENT-${visitIndex}-${assessmentIndex}`,
|
|
90
|
+
result: visitIndex * 10 + assessmentIndex,
|
|
91
|
+
referenceRange: { min: 0, max: 1000, unit: 'mg' },
|
|
92
|
+
flags: assessmentIndex === 4 ? ['review'] : [],
|
|
93
|
+
})),
|
|
94
|
+
})),
|
|
95
|
+
metadata: {
|
|
96
|
+
source: 'clinical-import',
|
|
97
|
+
audit: {
|
|
98
|
+
importedBy: 'SYSTEM',
|
|
99
|
+
importBatch: 'BATCH-CONTENT-1',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function makeReqRes(sessionId, actionId) {
|
|
106
|
+
const req = new EventEmitter();
|
|
107
|
+
req.method = 'POST';
|
|
108
|
+
req.url = '/subjects/repeated-read';
|
|
109
|
+
req.originalUrl = req.url;
|
|
110
|
+
req.headers = {
|
|
111
|
+
'x-bug-session-id': sessionId,
|
|
112
|
+
'x-bug-action-id': actionId,
|
|
113
|
+
'x-bug-request-start': String(Date.now()),
|
|
114
|
+
};
|
|
115
|
+
req.body = {};
|
|
116
|
+
req.params = {};
|
|
117
|
+
req.query = {};
|
|
118
|
+
|
|
119
|
+
const res = new EventEmitter();
|
|
120
|
+
res.statusCode = 200;
|
|
121
|
+
res.getHeader = () => undefined;
|
|
122
|
+
res.getHeaders = () => ({});
|
|
123
|
+
res.setHeader = () => {};
|
|
124
|
+
res.write = () => true;
|
|
125
|
+
res.end = function end() {
|
|
126
|
+
this.emit('finish');
|
|
127
|
+
return this;
|
|
128
|
+
};
|
|
129
|
+
return { req, res };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function flattenEvents(posts) {
|
|
133
|
+
return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function dbChanges(events) {
|
|
137
|
+
return events
|
|
138
|
+
.filter((event) => event.event_type === 'db_change')
|
|
139
|
+
.flatMap((event) => event.payload?.db || []);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function manifests(events) {
|
|
143
|
+
return events
|
|
144
|
+
.filter((event) => event.event_type === 'request_capture_manifest')
|
|
145
|
+
.map((event) => event.payload?.requestCaptureManifest ?? event.payload)
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function startMongo() {
|
|
150
|
+
if (process.env.REPRO_MONGO_TEST_URI) {
|
|
151
|
+
return { uri: process.env.REPRO_MONGO_TEST_URI, stop: async () => {} };
|
|
152
|
+
}
|
|
153
|
+
const container = await new GenericContainer('mongo:7')
|
|
154
|
+
.withExposedPorts(27017)
|
|
155
|
+
.withTmpFs({ '/data/db': 'rw,noexec,nosuid,size=1g' })
|
|
156
|
+
.withCommand(['mongod', '--bind_ip_all', '--setParameter', 'diagnosticDataCollectionEnabled=false'])
|
|
157
|
+
.withWaitStrategy(Wait.forListeningPorts())
|
|
158
|
+
.start();
|
|
159
|
+
return {
|
|
160
|
+
uri: `mongodb://${container.getHost()}:${container.getMappedPort(27017)}/repro_content_addressed`,
|
|
161
|
+
stop: () => container.stop(),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function main() {
|
|
166
|
+
const posts = [];
|
|
167
|
+
const originalFetch = global.fetch;
|
|
168
|
+
global.fetch = async (_url, init = {}) => {
|
|
169
|
+
posts.push(JSON.parse(String(init.body || '{}')));
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
status: 202,
|
|
173
|
+
json: async () => ({ ok: true }),
|
|
174
|
+
text: async () => '{"ok":true}',
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
let mongo;
|
|
179
|
+
let connection;
|
|
180
|
+
try {
|
|
181
|
+
mongo = await startMongo();
|
|
182
|
+
connection = await mongoose.createConnection(mongo.uri).asPromise();
|
|
183
|
+
|
|
184
|
+
const config = {
|
|
185
|
+
tenantId: 'TENANT_content_addressed_mongo',
|
|
186
|
+
appId: 'APP_content_addressed_mongo',
|
|
187
|
+
appSecret: 'content-addressed-mongo-secret',
|
|
188
|
+
captureHeaders: false,
|
|
189
|
+
privacy: { environment: 'dev', captureRaw: true },
|
|
190
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
191
|
+
};
|
|
192
|
+
const assessmentSchema = new mongoose.Schema({
|
|
193
|
+
code: String,
|
|
194
|
+
result: Number,
|
|
195
|
+
referenceRange: { min: Number, max: Number, unit: String },
|
|
196
|
+
flags: [String],
|
|
197
|
+
}, { _id: false });
|
|
198
|
+
const visitSchema = new mongoose.Schema({
|
|
199
|
+
visitNumber: String,
|
|
200
|
+
assessments: [assessmentSchema],
|
|
201
|
+
}, { _id: false });
|
|
202
|
+
const subjectSchema = new mongoose.Schema({
|
|
203
|
+
subjectNumber: { type: String, unique: true },
|
|
204
|
+
status: String,
|
|
205
|
+
visits: [visitSchema],
|
|
206
|
+
metadata: mongoose.Schema.Types.Mixed,
|
|
207
|
+
});
|
|
208
|
+
reproMongoosePlugin(config)(subjectSchema);
|
|
209
|
+
const Subject = connection.model('ContentSubject', subjectSchema, 'content_subjects');
|
|
210
|
+
await Subject.init();
|
|
211
|
+
await Subject.create(makeSubject());
|
|
212
|
+
|
|
213
|
+
initReproTracing({ instrument: false, fullValueCapture: true, logFunctionCalls: false });
|
|
214
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
215
|
+
const middleware = reproMiddleware(config);
|
|
216
|
+
const { req, res } = makeReqRes('S_content_addressed_mongo', 'A_content_addressed_mongo');
|
|
217
|
+
const repository = new SubjectRepository(Subject, new ConfigService());
|
|
218
|
+
let operationResult;
|
|
219
|
+
|
|
220
|
+
await new Promise((resolve, reject) => {
|
|
221
|
+
middleware(req, res, async (error) => {
|
|
222
|
+
if (error) {
|
|
223
|
+
reject(error);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
operationResult = await global.__repro_call(
|
|
228
|
+
loadSubjectRepeatedly,
|
|
229
|
+
null,
|
|
230
|
+
[repository, { subjectNumber: 'SUBJECT-CONTENT-1001' }],
|
|
231
|
+
'/app/src/subjects.repository.ts',
|
|
232
|
+
120,
|
|
233
|
+
'loadSubjectRepeatedly',
|
|
234
|
+
false,
|
|
235
|
+
);
|
|
236
|
+
res.end();
|
|
237
|
+
resolve();
|
|
238
|
+
} catch (operationError) {
|
|
239
|
+
reject(operationError);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
assert.equal(operationResult.repositoryIdentityPreserved, true);
|
|
245
|
+
assert.equal(operationResult.duplicateCode, 11000);
|
|
246
|
+
assert.equal(operationResult.docs.length, 6);
|
|
247
|
+
assert.equal(operationResult.docs[0].visits.length, 80);
|
|
248
|
+
assert.equal(operationResult.docs[5].visits[79].assessments[4].code, 'ASSESSMENT-79-4');
|
|
249
|
+
|
|
250
|
+
const deadline = Date.now() + 8000;
|
|
251
|
+
while (Date.now() < deadline) {
|
|
252
|
+
const events = flattenEvents(posts);
|
|
253
|
+
if (dbChanges(events).length >= 7 && manifests(events).length === 1 &&
|
|
254
|
+
events.some((event) => event.event_type === 'trace_batch')) {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
await sleep(25);
|
|
258
|
+
}
|
|
259
|
+
await __reproTestHooks.drainSdkBackgroundQueueForTest();
|
|
260
|
+
await flushIngestQueue();
|
|
261
|
+
|
|
262
|
+
const events = flattenEvents(posts);
|
|
263
|
+
const changes = dbChanges(events);
|
|
264
|
+
const manifest = manifests(events)[0];
|
|
265
|
+
assert(manifest, 'expected request capture manifest');
|
|
266
|
+
assert.equal(manifest.expected.dbChange, changes.length);
|
|
267
|
+
|
|
268
|
+
const readChanges = changes.filter((change) =>
|
|
269
|
+
change.collection === 'content_subjects' &&
|
|
270
|
+
change.op === 'findOne' &&
|
|
271
|
+
change.resultMetaValueCapture?.status === 'stored'
|
|
272
|
+
);
|
|
273
|
+
assert(readChanges.length >= 6, JSON.stringify(changes));
|
|
274
|
+
const resultValueIds = new Set(
|
|
275
|
+
readChanges.map((change) => change.resultMetaValueCapture.valueId),
|
|
276
|
+
);
|
|
277
|
+
assert.equal(resultValueIds.size, 1, 'identical query results must share one content ID');
|
|
278
|
+
|
|
279
|
+
const dbValueDefinitions = events
|
|
280
|
+
.filter((event) => event.event_type === 'db_change')
|
|
281
|
+
.flatMap((event) => event.payload?.dbValues || []);
|
|
282
|
+
const resultDefinitions = dbValueDefinitions.filter((entry) =>
|
|
283
|
+
resultValueIds.has(entry.id) && entry.target === 'db.resultMeta'
|
|
284
|
+
);
|
|
285
|
+
assert.equal(resultDefinitions.length, 1, 'repeated DB result must be transported once');
|
|
286
|
+
const fullResult = resultDefinitions[0].value;
|
|
287
|
+
assert.equal(fullResult.subjectNumber, 'SUBJECT-CONTENT-1001');
|
|
288
|
+
assert.equal(fullResult.visits.length, 80);
|
|
289
|
+
assert.equal(fullResult.visits[79].assessments[4].code, 'ASSESSMENT-79-4');
|
|
290
|
+
const legacyRepeatedResultBytes = readChanges.length * byteLength(fullResult);
|
|
291
|
+
const optimizedResultBytes = byteLength(fullResult) + readChanges.reduce(
|
|
292
|
+
(total, change) => total + byteLength(change.resultMetaValueCapture),
|
|
293
|
+
0,
|
|
294
|
+
);
|
|
295
|
+
const resultReduction = 1 - optimizedResultBytes / legacyRepeatedResultBytes;
|
|
296
|
+
assert(
|
|
297
|
+
resultReduction > 0.75,
|
|
298
|
+
`repeated DB result storage reduced only ${(resultReduction * 100).toFixed(1)}%`,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const failedWrite = changes.find((change) =>
|
|
302
|
+
change.collection === 'content_subjects' && change.error?.code === 11000
|
|
303
|
+
);
|
|
304
|
+
assert(failedWrite, 'duplicate-key failure must remain captured');
|
|
305
|
+
|
|
306
|
+
const traceBatches = events.filter((event) => event.event_type === 'trace_batch');
|
|
307
|
+
const traceEvents = traceBatches.flatMap((event) => event.payload?.trace || []);
|
|
308
|
+
const enter = traceEvents.find((event) =>
|
|
309
|
+
event.fn === 'loadSubjectRepeatedly' && event.type === 'enter'
|
|
310
|
+
);
|
|
311
|
+
assert(enter, JSON.stringify(traceEvents));
|
|
312
|
+
const traceDefinitions = traceBatches.flatMap((event) => event.payload?.traceValues || []);
|
|
313
|
+
const argsDefinition = traceDefinitions.find((entry) =>
|
|
314
|
+
entry.id === enter.argsValueCapture?.valueId
|
|
315
|
+
);
|
|
316
|
+
assert(argsDefinition, JSON.stringify(traceDefinitions));
|
|
317
|
+
assert.deepEqual(argsDefinition.value[0].configService, {
|
|
318
|
+
__kind: 'repro-semantic',
|
|
319
|
+
category: 'configuration-service',
|
|
320
|
+
className: 'ConfigService',
|
|
321
|
+
});
|
|
322
|
+
assert.equal(argsDefinition.value[1].subjectNumber, 'SUBJECT-CONTENT-1001');
|
|
323
|
+
|
|
324
|
+
const capturedJson = JSON.stringify(posts);
|
|
325
|
+
assert(!capturedJson.includes('captured-config-secret'));
|
|
326
|
+
assert(!capturedJson.includes('mongo-config-credential-999'));
|
|
327
|
+
assert(!capturedJson.includes('mongo-test-logger-buffer'));
|
|
328
|
+
|
|
329
|
+
console.log('content-addressed MongoDB testcontainers coverage OK', JSON.stringify({
|
|
330
|
+
repeatedResultOccurrences: readChanges.length,
|
|
331
|
+
uniqueResultDefinitions: resultDefinitions.length,
|
|
332
|
+
legacyRepeatedResultBytes,
|
|
333
|
+
optimizedResultBytes,
|
|
334
|
+
resultReductionPercent: Number((resultReduction * 100).toFixed(2)),
|
|
335
|
+
}));
|
|
336
|
+
} finally {
|
|
337
|
+
await __reproTestHooks.drainSdkBackgroundQueueForTest().catch(() => {});
|
|
338
|
+
await flushIngestQueue().catch(() => {});
|
|
339
|
+
global.fetch = originalFetch;
|
|
340
|
+
if (connection) await connection.close().catch(() => {});
|
|
341
|
+
if (mongo) await mongo.stop().catch(() => {});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
main().catch((error) => {
|
|
346
|
+
console.error(error);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
});
|