@reproapp/node-sdk 0.0.36 → 0.0.38
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 +45 -6
- package/package.json +1 -1
- package/src/index.ts +55 -6
- package/test/client-performance-regression.test.js +21 -3
- package/test/mongodb-integration.test.js +164 -0
package/dist/index.js
CHANGED
|
@@ -330,7 +330,7 @@ let disabledFunctionTraceRules = computeDisabledFunctionTraceRules();
|
|
|
330
330
|
let disabledFunctionTypePatterns = [];
|
|
331
331
|
let disabledTraceFilePatterns = [];
|
|
332
332
|
let __TRACE_LOG_PREF = null;
|
|
333
|
-
let __FULL_VALUE_CAPTURE_ENABLED =
|
|
333
|
+
let __FULL_VALUE_CAPTURE_ENABLED = true;
|
|
334
334
|
function setInterceptorTracingEnabled(enabled) {
|
|
335
335
|
const next = !!enabled;
|
|
336
336
|
if (interceptorTracingEnabled === next)
|
|
@@ -581,9 +581,9 @@ function defaultTracerInitOpts() {
|
|
|
581
581
|
/** Call this from the client app to enable tracing. Safe to call multiple times. */
|
|
582
582
|
function initReproTracing(opts) {
|
|
583
583
|
const options = opts ?? {};
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
584
|
+
__FULL_VALUE_CAPTURE_ENABLED = hasOwn(options, 'fullValueCapture')
|
|
585
|
+
? options.fullValueCapture === true
|
|
586
|
+
: true;
|
|
587
587
|
if (hasOwn(options, 'traceInterceptors')) {
|
|
588
588
|
setInterceptorTracingEnabled(!!options.traceInterceptors);
|
|
589
589
|
}
|
|
@@ -4092,6 +4092,45 @@ async function maybeCaptureDbValueAsync(params, sink) {
|
|
|
4092
4092
|
}
|
|
4093
4093
|
return result.captureRef;
|
|
4094
4094
|
}
|
|
4095
|
+
function pushUniqueCaptureCandidate(candidates, value) {
|
|
4096
|
+
if (value === undefined)
|
|
4097
|
+
return;
|
|
4098
|
+
if (candidates.some((candidate) => candidate === value))
|
|
4099
|
+
return;
|
|
4100
|
+
candidates.push(value);
|
|
4101
|
+
}
|
|
4102
|
+
function buildResultMetaFullValueCandidates(resultMetaRaw, resultMetaSource) {
|
|
4103
|
+
const candidates = [];
|
|
4104
|
+
pushUniqueCaptureCandidate(candidates, resultMetaRaw);
|
|
4105
|
+
if (resultMetaSource &&
|
|
4106
|
+
typeof resultMetaSource === 'object' &&
|
|
4107
|
+
Object.prototype.hasOwnProperty.call(resultMetaSource, 'result')) {
|
|
4108
|
+
pushUniqueCaptureCandidate(candidates, resultMetaSource.result);
|
|
4109
|
+
}
|
|
4110
|
+
pushUniqueCaptureCandidate(candidates, resultMetaSource);
|
|
4111
|
+
return candidates;
|
|
4112
|
+
}
|
|
4113
|
+
async function maybeCaptureDbValueFromCandidatesAsync(params, sink) {
|
|
4114
|
+
let skippedRef;
|
|
4115
|
+
for (const rawValue of params.rawValues) {
|
|
4116
|
+
const result = await createCapturedValueEntryAsync({
|
|
4117
|
+
target: params.target,
|
|
4118
|
+
rawValue,
|
|
4119
|
+
previewValue: params.previewValue,
|
|
4120
|
+
capture: params.capture,
|
|
4121
|
+
});
|
|
4122
|
+
if (!result)
|
|
4123
|
+
continue;
|
|
4124
|
+
if (result.entry) {
|
|
4125
|
+
sink.push(result.entry);
|
|
4126
|
+
return result.captureRef;
|
|
4127
|
+
}
|
|
4128
|
+
if (!skippedRef) {
|
|
4129
|
+
skippedRef = result.captureRef;
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
return skippedRef;
|
|
4133
|
+
}
|
|
4095
4134
|
function reproMiddleware(cfg) {
|
|
4096
4135
|
const masking = normalizeMaskingConfig(cfg.masking);
|
|
4097
4136
|
patchMongoDb(cfg);
|
|
@@ -5285,9 +5324,9 @@ async function emitDbQueryAsync(cfg, sid, aid, payload) {
|
|
|
5285
5324
|
previewValue: queryPreview,
|
|
5286
5325
|
capture,
|
|
5287
5326
|
}, dbValues);
|
|
5288
|
-
const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await
|
|
5327
|
+
const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await maybeCaptureDbValueFromCandidatesAsync({
|
|
5289
5328
|
target: 'db.resultMeta',
|
|
5290
|
-
|
|
5329
|
+
rawValues: buildResultMetaFullValueCandidates(payload.resultMetaRaw, resultMetaSource),
|
|
5291
5330
|
previewValue: resultMetaPreview,
|
|
5292
5331
|
capture,
|
|
5293
5332
|
}, dbValues);
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -604,7 +604,7 @@ let disabledFunctionTraceRules: DisableFunctionTraceConfig[] = computeDisabledFu
|
|
|
604
604
|
let disabledFunctionTypePatterns: Array<string | number | RegExp> = [];
|
|
605
605
|
let disabledTraceFilePatterns: Array<string | number | RegExp> = [];
|
|
606
606
|
let __TRACE_LOG_PREF: boolean | null = null;
|
|
607
|
-
let __FULL_VALUE_CAPTURE_ENABLED =
|
|
607
|
+
let __FULL_VALUE_CAPTURE_ENABLED = true;
|
|
608
608
|
|
|
609
609
|
function setInterceptorTracingEnabled(enabled: boolean) {
|
|
610
610
|
const next = !!enabled;
|
|
@@ -929,9 +929,9 @@ function defaultTracerInitOpts(): TracerInitOpts {
|
|
|
929
929
|
export function initReproTracing(opts?: ReproTracingInitOptions) {
|
|
930
930
|
const options = opts ?? {};
|
|
931
931
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
932
|
+
__FULL_VALUE_CAPTURE_ENABLED = hasOwn(options, 'fullValueCapture')
|
|
933
|
+
? options.fullValueCapture === true
|
|
934
|
+
: true;
|
|
935
935
|
|
|
936
936
|
if (hasOwn(options, 'traceInterceptors')) {
|
|
937
937
|
setInterceptorTracingEnabled(!!options.traceInterceptors);
|
|
@@ -4955,6 +4955,55 @@ async function maybeCaptureDbValueAsync(
|
|
|
4955
4955
|
return result.captureRef;
|
|
4956
4956
|
}
|
|
4957
4957
|
|
|
4958
|
+
function pushUniqueCaptureCandidate(candidates: any[], value: any): void {
|
|
4959
|
+
if (value === undefined) return;
|
|
4960
|
+
if (candidates.some((candidate) => candidate === value)) return;
|
|
4961
|
+
candidates.push(value);
|
|
4962
|
+
}
|
|
4963
|
+
|
|
4964
|
+
function buildResultMetaFullValueCandidates(resultMetaRaw: any, resultMetaSource: any): any[] {
|
|
4965
|
+
const candidates: any[] = [];
|
|
4966
|
+
pushUniqueCaptureCandidate(candidates, resultMetaRaw);
|
|
4967
|
+
if (
|
|
4968
|
+
resultMetaSource &&
|
|
4969
|
+
typeof resultMetaSource === 'object' &&
|
|
4970
|
+
Object.prototype.hasOwnProperty.call(resultMetaSource, 'result')
|
|
4971
|
+
) {
|
|
4972
|
+
pushUniqueCaptureCandidate(candidates, resultMetaSource.result);
|
|
4973
|
+
}
|
|
4974
|
+
pushUniqueCaptureCandidate(candidates, resultMetaSource);
|
|
4975
|
+
return candidates;
|
|
4976
|
+
}
|
|
4977
|
+
|
|
4978
|
+
async function maybeCaptureDbValueFromCandidatesAsync(
|
|
4979
|
+
params: {
|
|
4980
|
+
target: 'db.pk' | 'db.before' | 'db.after' | 'db.query' | 'db.resultMeta' | 'db.error';
|
|
4981
|
+
rawValues: any[];
|
|
4982
|
+
previewValue: any;
|
|
4983
|
+
capture: FullValueCaptureContext;
|
|
4984
|
+
},
|
|
4985
|
+
sink: TraceValueBatchEntry[],
|
|
4986
|
+
): Promise<TraceValueCaptureRef | undefined> {
|
|
4987
|
+
let skippedRef: TraceValueCaptureRef | undefined;
|
|
4988
|
+
for (const rawValue of params.rawValues) {
|
|
4989
|
+
const result = await createCapturedValueEntryAsync({
|
|
4990
|
+
target: params.target,
|
|
4991
|
+
rawValue,
|
|
4992
|
+
previewValue: params.previewValue,
|
|
4993
|
+
capture: params.capture,
|
|
4994
|
+
});
|
|
4995
|
+
if (!result) continue;
|
|
4996
|
+
if (result.entry) {
|
|
4997
|
+
sink.push(result.entry);
|
|
4998
|
+
return result.captureRef;
|
|
4999
|
+
}
|
|
5000
|
+
if (!skippedRef) {
|
|
5001
|
+
skippedRef = result.captureRef;
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
return skippedRef;
|
|
5005
|
+
}
|
|
5006
|
+
|
|
4958
5007
|
// ===================================================================
|
|
4959
5008
|
// reproMiddleware — unchanged behavior + passive per-request trace
|
|
4960
5009
|
// ===================================================================
|
|
@@ -6241,9 +6290,9 @@ async function emitDbQueryAsync(cfg: any, sid?: string, aid?: string, payload?:
|
|
|
6241
6290
|
previewValue: queryPreview,
|
|
6242
6291
|
capture,
|
|
6243
6292
|
}, dbValues);
|
|
6244
|
-
const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await
|
|
6293
|
+
const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await maybeCaptureDbValueFromCandidatesAsync({
|
|
6245
6294
|
target: 'db.resultMeta',
|
|
6246
|
-
|
|
6295
|
+
rawValues: buildResultMetaFullValueCandidates(payload.resultMetaRaw, resultMetaSource),
|
|
6247
6296
|
previewValue: resultMetaPreview,
|
|
6248
6297
|
capture,
|
|
6249
6298
|
}, dbValues);
|
|
@@ -170,10 +170,28 @@ async function main() {
|
|
|
170
170
|
event?.returnValue,
|
|
171
171
|
);
|
|
172
172
|
assert(largeReturnExit, JSON.stringify(traceEvents));
|
|
173
|
-
assert
|
|
174
|
-
largeReturnExit.returnValue,
|
|
175
|
-
|
|
173
|
+
assert(
|
|
174
|
+
Buffer.byteLength(JSON.stringify(largeReturnExit.returnValue), 'utf8') <= 2048,
|
|
175
|
+
JSON.stringify(largeReturnExit.returnValue),
|
|
176
176
|
);
|
|
177
|
+
assert.equal(
|
|
178
|
+
largeReturnExit.returnValueCapture?.status,
|
|
179
|
+
'stored',
|
|
180
|
+
JSON.stringify(largeReturnExit),
|
|
181
|
+
);
|
|
182
|
+
assert(
|
|
183
|
+
typeof largeReturnExit.returnValueCapture?.valueId === 'string' &&
|
|
184
|
+
largeReturnExit.returnValueCapture.valueId.length > 0,
|
|
185
|
+
JSON.stringify(largeReturnExit),
|
|
186
|
+
);
|
|
187
|
+
const traceValues = eventsOf(capturedBodies, 'trace_batch')
|
|
188
|
+
.flatMap((event) => Array.isArray(event?.payload?.traceValues) ? event.payload.traceValues : []);
|
|
189
|
+
const capturedReturnValue = traceValues.find((entry) =>
|
|
190
|
+
entry?.id === largeReturnExit.returnValueCapture.valueId &&
|
|
191
|
+
entry?.target === 'trace.returnValue'
|
|
192
|
+
);
|
|
193
|
+
assert(capturedReturnValue, JSON.stringify(traceValues));
|
|
194
|
+
assert.equal(capturedReturnValue.value?.rows?.length, 400, JSON.stringify(capturedReturnValue));
|
|
177
195
|
|
|
178
196
|
console.log('client performance regression guards OK');
|
|
179
197
|
} finally {
|
|
@@ -133,12 +133,24 @@ function execPre(hooks, name, context) {
|
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
function execPost(hooks, name, context, args = []) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
hooks.execPost(name, context, args, {}, (error) => (error ? reject(error) : resolve()));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
136
142
|
function dbChanges(events) {
|
|
137
143
|
return events
|
|
138
144
|
.filter((event) => event.event_type === 'db_change')
|
|
139
145
|
.flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
function dbValueEntries(events) {
|
|
149
|
+
return events
|
|
150
|
+
.filter((event) => event.event_type === 'db_change')
|
|
151
|
+
.flatMap((event) => Array.isArray(event.payload?.dbValues) ? event.payload.dbValues : []);
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
function requestCaptureManifests(events) {
|
|
143
155
|
return events
|
|
144
156
|
.filter((event) => event.event_type === 'request_capture_manifest')
|
|
@@ -146,6 +158,81 @@ function requestCaptureManifests(events) {
|
|
|
146
158
|
.filter(Boolean);
|
|
147
159
|
}
|
|
148
160
|
|
|
161
|
+
function makeNotificationModuleDocument() {
|
|
162
|
+
const notificationPreferences = Array.from({ length: 24 }, (_, index) => ({
|
|
163
|
+
trigger: 'SubjectScreened',
|
|
164
|
+
enabled: true,
|
|
165
|
+
channels: [
|
|
166
|
+
{
|
|
167
|
+
type: 'email',
|
|
168
|
+
template: {
|
|
169
|
+
subject: `Participant Screening ${index}`,
|
|
170
|
+
html: `<p>Screening notification ${index}</p>`,
|
|
171
|
+
},
|
|
172
|
+
recipients: {
|
|
173
|
+
to: ['noreply@korioclinical.com'],
|
|
174
|
+
cc: [
|
|
175
|
+
`pobrien+dev-conf-csm${index}@korioclinical.com`,
|
|
176
|
+
`mnebot+site-monitor${index}@korioclinical.com`,
|
|
177
|
+
],
|
|
178
|
+
bcc: [`audit-${index}@korioclinical.com`],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
}));
|
|
183
|
+
const notificationTrigger = Array.from({ length: 24 }, (_, index) => ({
|
|
184
|
+
active: true,
|
|
185
|
+
event: 'SubjectScreened',
|
|
186
|
+
visitType: 'Screening',
|
|
187
|
+
subjectType: 'Participant',
|
|
188
|
+
rules: [
|
|
189
|
+
{
|
|
190
|
+
condition: {
|
|
191
|
+
path: 'subject.subjectStatusOption',
|
|
192
|
+
operator: 'equals',
|
|
193
|
+
value: 'Screened',
|
|
194
|
+
},
|
|
195
|
+
action: {
|
|
196
|
+
module: 'NotificationGeneratorService',
|
|
197
|
+
method: 'SubjectScreened',
|
|
198
|
+
order: index,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
__t: 'NotificationModule',
|
|
206
|
+
_id: '69e77c77ab541cfac917b37a',
|
|
207
|
+
active: true,
|
|
208
|
+
environment: 'dev-configure-pulmovant',
|
|
209
|
+
notificationPreferences,
|
|
210
|
+
notification_trigger: notificationTrigger,
|
|
211
|
+
protocol: {
|
|
212
|
+
id: '69e77c77ab541cfac917b36b',
|
|
213
|
+
name: 'RVT-2301-301',
|
|
214
|
+
},
|
|
215
|
+
schema_version: 1,
|
|
216
|
+
sponsor_name: 'Pulmovant',
|
|
217
|
+
study_name: 'RVT-2301-301',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
class NotificationModuleHostDocument {
|
|
222
|
+
constructor(value) {
|
|
223
|
+
this.$__ = {};
|
|
224
|
+
this.value = value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
toObject() {
|
|
228
|
+
return JSON.parse(JSON.stringify(this.value));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
toJSON() {
|
|
232
|
+
return this.toObject();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
149
236
|
async function testMongooseSuppressionDoesNotHideExplicitNativeInsertOne() {
|
|
150
237
|
await withCapturedFetch(async (capturedBodies) => {
|
|
151
238
|
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
@@ -477,6 +564,82 @@ async function testClientScreenNativeMongoInsertOneShapeIsCaptured() {
|
|
|
477
564
|
});
|
|
478
565
|
}
|
|
479
566
|
|
|
567
|
+
async function testLargeMongooseFindOneResultStoresFullResultMetaSidecar() {
|
|
568
|
+
await withCapturedFetch(async (capturedBodies) => {
|
|
569
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
570
|
+
const sessionId = 'S_mongoose_large_result_meta';
|
|
571
|
+
const actionId = 'A_mongoose_large_result_meta';
|
|
572
|
+
const cfg = {
|
|
573
|
+
...makeConfig(sessionId, actionId),
|
|
574
|
+
privacy: { environment: 'dev', captureRaw: true },
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const schema = new mongoose.Schema({}, { strict: false });
|
|
578
|
+
reproMongoosePlugin(cfg)(schema);
|
|
579
|
+
|
|
580
|
+
const notificationModule = makeNotificationModuleDocument();
|
|
581
|
+
const rawResult = new NotificationModuleHostDocument(notificationModule);
|
|
582
|
+
const queryContext = {
|
|
583
|
+
model: {
|
|
584
|
+
collection: {
|
|
585
|
+
name: 'configStudy',
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
getFilter: () => ({
|
|
589
|
+
__t: 'NotificationModule',
|
|
590
|
+
active: true,
|
|
591
|
+
protocol: {
|
|
592
|
+
id: '69e77c77ab541cfac917b36b',
|
|
593
|
+
},
|
|
594
|
+
}),
|
|
595
|
+
getUpdate: () => undefined,
|
|
596
|
+
projection: () => undefined,
|
|
597
|
+
getOptions: () => ({ lean: false }),
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
await runCapturedRequest(cfg, sessionId, actionId, async () => {
|
|
601
|
+
await execPre(schema.s.hooks, 'findOne', queryContext);
|
|
602
|
+
await execPost(schema.s.hooks, 'findOne', queryContext, [rawResult]);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
|
|
606
|
+
await waitFor(() => dbValueEntries(flattenEvents(capturedBodies)).some((entry) => entry.target === 'db.resultMeta'));
|
|
607
|
+
await drainSdk();
|
|
608
|
+
|
|
609
|
+
const [change] = dbChanges(flattenEvents(capturedBodies));
|
|
610
|
+
const values = dbValueEntries(flattenEvents(capturedBodies));
|
|
611
|
+
const resultMetaValue = values.find((entry) => entry.id === change.resultMetaValueCapture?.valueId);
|
|
612
|
+
|
|
613
|
+
assert.strictEqual(change.type, 'mongo');
|
|
614
|
+
assert.strictEqual(change.collection, 'configStudy');
|
|
615
|
+
assert.strictEqual(change.op, 'findOne');
|
|
616
|
+
assert.strictEqual(change.query.filter.__t, 'NotificationModule');
|
|
617
|
+
assert.strictEqual(change.resultMeta.docsCount, 1);
|
|
618
|
+
assert.deepStrictEqual(change.resultMeta.result.notificationPreferences[0], {
|
|
619
|
+
__kind: 'omitted',
|
|
620
|
+
reason: 'depth>3',
|
|
621
|
+
});
|
|
622
|
+
assert.deepStrictEqual(change.resultMeta.result.notification_trigger[0], {
|
|
623
|
+
__kind: 'omitted',
|
|
624
|
+
reason: 'depth>3',
|
|
625
|
+
});
|
|
626
|
+
assert.strictEqual(change.resultMetaValueCapture.status, 'stored');
|
|
627
|
+
assert.ok(change.resultMetaValueCapture.valueId, 'expected resultMeta value id');
|
|
628
|
+
assert.ok(resultMetaValue, 'expected matching db.resultMeta sidecar');
|
|
629
|
+
assert.strictEqual(resultMetaValue.target, 'db.resultMeta');
|
|
630
|
+
assert.strictEqual(resultMetaValue.value.__t, 'NotificationModule');
|
|
631
|
+
assert.strictEqual(resultMetaValue.value.notificationPreferences.length, 24);
|
|
632
|
+
assert.strictEqual(
|
|
633
|
+
resultMetaValue.value.notificationPreferences[0].channels[0].recipients.cc[0],
|
|
634
|
+
'pobrien+dev-conf-csm0@korioclinical.com',
|
|
635
|
+
);
|
|
636
|
+
assert.strictEqual(
|
|
637
|
+
resultMetaValue.value.notification_trigger[0].rules[0].action.method,
|
|
638
|
+
'SubjectScreened',
|
|
639
|
+
);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
480
643
|
async function testNativeInsertOneIsCaptured() {
|
|
481
644
|
await withCapturedFetch(async (capturedBodies) => {
|
|
482
645
|
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
@@ -742,6 +905,7 @@ async function testNativeOperationFailureIsCapturedAndRethrown() {
|
|
|
742
905
|
await testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne();
|
|
743
906
|
await testMongoClientDbCollectionChainIsPatchedWithoutCollectionExport();
|
|
744
907
|
await testClientScreenNativeMongoInsertOneShapeIsCaptured();
|
|
908
|
+
await testLargeMongooseFindOneResultStoresFullResultMetaSidecar();
|
|
745
909
|
await testNativeInsertOneIsCaptured();
|
|
746
910
|
await testCaptureRawNativeInsertOneUsesJsonSafeTransportValues();
|
|
747
911
|
await testNativeUpdateOneUpsertIsCaptured();
|