@reproapp/node-sdk 0.0.33 → 0.0.35

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 CHANGED
@@ -824,6 +824,19 @@ const getCtx = () => als.getStore() || {};
824
824
  function normalizeNativeMongoSuppressionOp(op) {
825
825
  return String(op ?? '').trim();
826
826
  }
827
+ const NATIVE_MONGO_WRITE_OPS = new Set([
828
+ 'insertOne',
829
+ 'insertMany',
830
+ 'updateOne',
831
+ 'updateMany',
832
+ 'replaceOne',
833
+ 'deleteOne',
834
+ 'deleteMany',
835
+ 'bulkWrite',
836
+ 'findOneAndUpdate',
837
+ 'findOneAndDelete',
838
+ 'findOneAndReplace',
839
+ ]);
827
840
  function pushNativeMongoSuppression(collection, ops) {
828
841
  const store = als.getStore();
829
842
  if (!store)
@@ -887,6 +900,8 @@ function isNativeMongoSuppressed(collection, op) {
887
900
  return false;
888
901
  const normalizedCollection = collection == null ? null : String(collection);
889
902
  const normalizedOp = normalizeNativeMongoSuppressionOp(op);
903
+ if (NATIVE_MONGO_WRITE_OPS.has(normalizedOp))
904
+ return false;
890
905
  const now = Date.now();
891
906
  const matchingTokens = [];
892
907
  for (let i = suppressions.length - 1; i >= 0; i -= 1) {
@@ -2988,9 +3003,12 @@ function limitRawInlinePrivacyValue(target, value) {
2988
3003
  return limited.skipped ? limited : null;
2989
3004
  }
2990
3005
  function materializeInlinePrivacyValue(target, value, cfg, req, trace, masking, privacy, db) {
2991
- if (value === undefined || shouldCapturePrivacyRaw(cfg)) {
3006
+ if (value === undefined) {
2992
3007
  return { value };
2993
3008
  }
3009
+ if (shouldCapturePrivacyRaw(cfg)) {
3010
+ return { value: limitMaterializedTraceInlineValue(value) };
3011
+ }
2994
3012
  try {
2995
3013
  const valueAfterPrivacy = applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy, db);
2996
3014
  return { value: valueAfterPrivacy };
@@ -3007,9 +3025,12 @@ function materializeInlinePrivacyValue(target, value, cfg, req, trace, masking,
3007
3025
  }
3008
3026
  }
3009
3027
  async function materializeInlinePrivacyValueAsync(target, value, cfg, req, trace, masking, privacy, db) {
3010
- if (value === undefined || shouldCapturePrivacyRaw(cfg)) {
3028
+ if (value === undefined) {
3011
3029
  return { value };
3012
3030
  }
3031
+ if (shouldCapturePrivacyRaw(cfg)) {
3032
+ return { value: limitMaterializedTraceInlineValue(value) };
3033
+ }
3013
3034
  try {
3014
3035
  const valueAfterPrivacy = await applyPrivacyThenMaskAsync(target, value, cfg, req, trace, masking, privacy, db);
3015
3036
  return { value: valueAfterPrivacy };
@@ -3899,7 +3920,7 @@ function estimateTraceBatchSerializedBytes(batch, requestRid, actionId, batchInd
3899
3920
  function chunkTraceEventsForTransport(events, requestRid, actionId) {
3900
3921
  if (!Array.isArray(events) || events.length === 0)
3901
3922
  return [];
3902
- const boundedEvents = events.slice(0, TRACE_MAX_EVENTS_PER_REQUEST);
3923
+ const boundedEvents = boundTraceEventsForTransport(events);
3903
3924
  const countBatches = chunkArray(boundedEvents, TRACE_BATCH_SIZE);
3904
3925
  const sizedBatches = [];
3905
3926
  let current = [];
@@ -3928,6 +3949,13 @@ function chunkTraceEventsForTransport(events, requestRid, actionId) {
3928
3949
  }
3929
3950
  return sizedBatches;
3930
3951
  }
3952
+ function boundTraceEventsForTransport(events) {
3953
+ if (events.length <= TRACE_MAX_EVENTS_PER_REQUEST)
3954
+ return events;
3955
+ const headCount = Math.floor(TRACE_MAX_EVENTS_PER_REQUEST / 2);
3956
+ const tailCount = TRACE_MAX_EVENTS_PER_REQUEST - headCount;
3957
+ return events.slice(0, headCount).concat(events.slice(events.length - tailCount));
3958
+ }
3931
3959
  function createCapturedValueEntry(params) {
3932
3960
  if (!__FULL_VALUE_CAPTURE_ENABLED)
3933
3961
  return undefined;
@@ -5188,7 +5216,7 @@ function emitDbQuery(cfg, sid, aid, payload) {
5188
5216
  await als.run(runCtx, async () => {
5189
5217
  await emitDbQueryAsync(cfg, sid, aid, payload).catch(() => undefined);
5190
5218
  });
5191
- });
5219
+ }, { priority: 'high' });
5192
5220
  }
5193
5221
  async function emitDbQueryAsync(cfg, sid, aid, payload) {
5194
5222
  if (!sid)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -1227,6 +1227,20 @@ function normalizeNativeMongoSuppressionOp(op: string | null | undefined): strin
1227
1227
  return String(op ?? '').trim();
1228
1228
  }
1229
1229
 
1230
+ const NATIVE_MONGO_WRITE_OPS = new Set([
1231
+ 'insertOne',
1232
+ 'insertMany',
1233
+ 'updateOne',
1234
+ 'updateMany',
1235
+ 'replaceOne',
1236
+ 'deleteOne',
1237
+ 'deleteMany',
1238
+ 'bulkWrite',
1239
+ 'findOneAndUpdate',
1240
+ 'findOneAndDelete',
1241
+ 'findOneAndReplace',
1242
+ ]);
1243
+
1230
1244
  function pushNativeMongoSuppression(collection: any, ops?: string[] | readonly string[]): NativeMongoSuppression | null {
1231
1245
  const store = als.getStore();
1232
1246
  if (!store) return null;
@@ -1284,6 +1298,7 @@ function isNativeMongoSuppressed(collection: any, op: string): boolean {
1284
1298
  if (!Array.isArray(suppressions) || suppressions.length === 0) return false;
1285
1299
  const normalizedCollection = collection == null ? null : String(collection);
1286
1300
  const normalizedOp = normalizeNativeMongoSuppressionOp(op);
1301
+ if (NATIVE_MONGO_WRITE_OPS.has(normalizedOp)) return false;
1287
1302
  const now = Date.now();
1288
1303
  const matchingTokens: NativeMongoSuppression[] = [];
1289
1304
  for (let i = suppressions.length - 1; i >= 0; i -= 1) {
@@ -3595,9 +3610,12 @@ function materializeInlinePrivacyValue(
3595
3610
  privacy: NormalizedRuntimePrivacyPolicy | null,
3596
3611
  db?: RuntimePrivacyDbContext | null,
3597
3612
  ): InlinePrivacyMaterializationResult {
3598
- if (value === undefined || shouldCapturePrivacyRaw(cfg)) {
3613
+ if (value === undefined) {
3599
3614
  return { value };
3600
3615
  }
3616
+ if (shouldCapturePrivacyRaw(cfg)) {
3617
+ return { value: limitMaterializedTraceInlineValue(value) };
3618
+ }
3601
3619
 
3602
3620
  try {
3603
3621
  const valueAfterPrivacy = applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy, db);
@@ -3624,9 +3642,12 @@ async function materializeInlinePrivacyValueAsync(
3624
3642
  privacy: NormalizedRuntimePrivacyPolicy | null,
3625
3643
  db?: RuntimePrivacyDbContext | null,
3626
3644
  ): Promise<InlinePrivacyMaterializationResult> {
3627
- if (value === undefined || shouldCapturePrivacyRaw(cfg)) {
3645
+ if (value === undefined) {
3628
3646
  return { value };
3629
3647
  }
3648
+ if (shouldCapturePrivacyRaw(cfg)) {
3649
+ return { value: limitMaterializedTraceInlineValue(value) };
3650
+ }
3630
3651
 
3631
3652
  try {
3632
3653
  const valueAfterPrivacy = await applyPrivacyThenMaskAsync(target, value, cfg, req, trace, masking, privacy, db);
@@ -4627,7 +4648,7 @@ function chunkTraceEventsForTransport(
4627
4648
  ): TraceEventRecord[][] {
4628
4649
  if (!Array.isArray(events) || events.length === 0) return [];
4629
4650
 
4630
- const boundedEvents = events.slice(0, TRACE_MAX_EVENTS_PER_REQUEST);
4651
+ const boundedEvents = boundTraceEventsForTransport(events);
4631
4652
  const countBatches = chunkArray(boundedEvents, TRACE_BATCH_SIZE);
4632
4653
  const sizedBatches: TraceEventRecord[][] = [];
4633
4654
  let current: TraceEventRecord[] = [];
@@ -4671,6 +4692,13 @@ function chunkTraceEventsForTransport(
4671
4692
  return sizedBatches;
4672
4693
  }
4673
4694
 
4695
+ function boundTraceEventsForTransport(events: TraceEventRecord[]): TraceEventRecord[] {
4696
+ if (events.length <= TRACE_MAX_EVENTS_PER_REQUEST) return events;
4697
+ const headCount = Math.floor(TRACE_MAX_EVENTS_PER_REQUEST / 2);
4698
+ const tailCount = TRACE_MAX_EVENTS_PER_REQUEST - headCount;
4699
+ return events.slice(0, headCount).concat(events.slice(events.length - tailCount));
4700
+ }
4701
+
4674
4702
  function createCapturedValueEntry(
4675
4703
  params: {
4676
4704
  target:
@@ -6145,7 +6173,7 @@ function emitDbQuery(cfg: any, sid?: string, aid?: string, payload?: any) {
6145
6173
  await als.run(runCtx, async () => {
6146
6174
  await emitDbQueryAsync(cfg, sid, aid, payload).catch(() => undefined);
6147
6175
  });
6148
- });
6176
+ }, { priority: 'high' });
6149
6177
  }
6150
6178
 
6151
6179
  async function emitDbQueryAsync(cfg: any, sid?: string, aid?: string, payload?: any) {
@@ -139,6 +139,13 @@ function dbChanges(events) {
139
139
  .flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
140
140
  }
141
141
 
142
+ function requestCaptureManifests(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
+
142
149
  async function testMongooseSuppressionDoesNotHideExplicitNativeInsertOne() {
143
150
  await withCapturedFetch(async (capturedBodies) => {
144
151
  __reproTestHooks.resetSdkBackgroundQueuesForTest();
@@ -185,9 +192,11 @@ async function testMongooseSuppressionDoesNotHideExplicitNativeInsertOne() {
185
192
  });
186
193
 
187
194
  await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
195
+ await waitFor(() => requestCaptureManifests(flattenEvents(capturedBodies)).length === 1);
188
196
  await drainSdk();
189
197
 
190
198
  const [change] = dbChanges(flattenEvents(capturedBodies));
199
+ const [manifest] = requestCaptureManifests(flattenEvents(capturedBodies));
191
200
  assert.strictEqual(change.type, 'mongo');
192
201
  assert.strictEqual(change.collection, 'subjects');
193
202
  assert.strictEqual(change.op, 'insertOne');
@@ -198,6 +207,7 @@ async function testMongooseSuppressionDoesNotHideExplicitNativeInsertOne() {
198
207
  assert.strictEqual(change.query.options.session.__kind, 'mongodb_client_session');
199
208
  assert.strictEqual(change.query.options.session.id, 'native-explicit-session-1');
200
209
  assert.strictEqual(change.after.subjectNumber, '10013038');
210
+ assert.strictEqual(manifest.expected.dbChange, 1);
201
211
  });
202
212
  }
203
213
 
@@ -379,6 +389,68 @@ async function testNativeInsertOneIsCaptured() {
379
389
  });
380
390
  }
381
391
 
392
+ async function testCaptureRawNativeInsertOneUsesJsonSafeTransportValues() {
393
+ await withCapturedFetch(async (capturedBodies) => {
394
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
395
+ const sessionId = 'S_mongo_insert_raw_complex';
396
+ const actionId = 'A_mongo_insert_raw_complex';
397
+ const cfg = {
398
+ ...makeConfig(sessionId, actionId),
399
+ privacy: { environment: 'dev', captureRaw: true },
400
+ };
401
+
402
+ class FakeCollection {
403
+ constructor() {
404
+ this.collectionName = 'subjects';
405
+ this.dbName = 'clinical';
406
+ }
407
+
408
+ async insertOne(doc, options) {
409
+ assert.strictEqual(options.session.id, 'native-raw-complex-session-1');
410
+ doc._id = doc._id ?? 'subject-raw-complex-1';
411
+ return { acknowledged: true, insertedId: doc._id };
412
+ }
413
+ }
414
+
415
+ __reproTestHooks.patchMongoCollectionPrototypeForTest(FakeCollection.prototype, cfg);
416
+ const collection = new FakeCollection();
417
+ const doc = {
418
+ subjectNumber: '10013039',
419
+ status: 'screened',
420
+ counter: BigInt(42),
421
+ };
422
+ doc.self = doc;
423
+
424
+ await runCapturedRequest(cfg, sessionId, actionId, async () => {
425
+ await collection.insertOne(doc, {
426
+ session: {
427
+ id: 'native-raw-complex-session-1',
428
+ hasEnded: false,
429
+ inTransaction: () => true,
430
+ },
431
+ });
432
+ });
433
+
434
+ await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
435
+ await waitFor(() => requestCaptureManifests(flattenEvents(capturedBodies)).length === 1);
436
+ await drainSdk();
437
+
438
+ const [change] = dbChanges(flattenEvents(capturedBodies));
439
+ const [manifest] = requestCaptureManifests(flattenEvents(capturedBodies));
440
+ assert.strictEqual(change.type, 'mongo');
441
+ assert.strictEqual(change.collection, 'subjects');
442
+ assert.strictEqual(change.op, 'insertOne');
443
+ assert.strictEqual(change.query.driver, 'mongodb');
444
+ assert.strictEqual(change.query.doc.subjectNumber, '10013039');
445
+ assert.strictEqual(change.query.doc.counter, '42');
446
+ assert.deepStrictEqual(change.query.doc.self, { __reproCircularRef: '$.doc' });
447
+ assert.strictEqual(change.after.subjectNumber, '10013039');
448
+ assert.strictEqual(change.after.counter, '42');
449
+ assert.deepStrictEqual(change.after.self, { __reproCircularRef: '$' });
450
+ assert.strictEqual(manifest.expected.dbChange, 1);
451
+ });
452
+ }
453
+
382
454
  async function testNativeUpdateOneUpsertIsCaptured() {
383
455
  await withCapturedFetch(async (capturedBodies) => {
384
456
  __reproTestHooks.resetSdkBackgroundQueuesForTest();
@@ -525,6 +597,7 @@ async function testNativeOperationFailureIsCapturedAndRethrown() {
525
597
  await testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne();
526
598
  await testMongoClientDbCollectionChainIsPatchedWithoutCollectionExport();
527
599
  await testNativeInsertOneIsCaptured();
600
+ await testCaptureRawNativeInsertOneUsesJsonSafeTransportValues();
528
601
  await testNativeUpdateOneUpsertIsCaptured();
529
602
  await testNativeFindCursorToArrayIsCaptured();
530
603
  await testNativeOperationFailureIsCapturedAndRethrown();
@@ -34,6 +34,13 @@ function dbChanges(events) {
34
34
  .flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
35
35
  }
36
36
 
37
+ function requestCaptureManifests(events) {
38
+ return events
39
+ .filter((event) => event.event_type === 'request_capture_manifest')
40
+ .map((event) => event.payload?.requestCaptureManifest ?? event.payload)
41
+ .filter(Boolean);
42
+ }
43
+
37
44
  function makeReqRes(sessionId, actionId) {
38
45
  const req = new EventEmitter();
39
46
  req.method = 'POST';
@@ -131,16 +138,29 @@ async function main() {
131
138
  await Subject.create({ subjectNumber: '10013038', status: 'screened' });
132
139
  });
133
140
 
134
- await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
141
+ await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length >= 2);
142
+ await waitFor(() => requestCaptureManifests(flattenEvents(capturedBodies)).length === 1);
135
143
  await __reproTestHooks.drainSdkBackgroundQueueForTest();
136
144
  await flushIngestQueue();
137
145
 
138
146
  const changes = dbChanges(flattenEvents(capturedBodies));
139
- assert.strictEqual(changes.length, 1);
140
- assert.strictEqual(changes[0].type, 'mongo');
141
- assert.strictEqual(changes[0].collection, 'subjects');
142
- assert.strictEqual(changes[0].op, 'insert');
143
- assert.strictEqual(changes[0].after.subjectNumber, '10013038');
147
+ const manifest = requestCaptureManifests(flattenEvents(capturedBodies))[0];
148
+ const mongooseWrite = changes.find((change) => (
149
+ change.type === 'mongo' &&
150
+ change.collection === 'subjects' &&
151
+ change.op === 'insert' &&
152
+ change.after?.subjectNumber === '10013038'
153
+ ));
154
+ const nativeWrite = changes.find((change) => (
155
+ change.type === 'mongo' &&
156
+ change.collection === 'subjects' &&
157
+ change.op === 'insertOne' &&
158
+ change.query?.driver === 'mongodb' &&
159
+ change.after?.subjectNumber === '10013038'
160
+ ));
161
+ assert.ok(mongooseWrite, 'expected mongoose-level insert evidence');
162
+ assert.ok(nativeWrite, 'expected native mongodb insertOne evidence');
163
+ assert.ok(manifest.expected.dbChange >= 2);
144
164
 
145
165
  console.log('mongodb mongoose native suppression testcontainers OK');
146
166
  } finally {
@@ -66,6 +66,11 @@ function main() {
66
66
  cappedEventCount <= 1000,
67
67
  `expected trace transport to cap events per request, got ${cappedEventCount}`,
68
68
  );
69
+ const cappedEvents = cappedBatches.flat();
70
+ assert.strictEqual(cappedEvents[0].fn, 'fn0');
71
+ assert.strictEqual(cappedEvents[499].fn, 'fn499');
72
+ assert.strictEqual(cappedEvents[500].fn, 'fn1000');
73
+ assert.strictEqual(cappedEvents[cappedEvents.length - 1].fn, 'fn1499');
69
74
 
70
75
  // eslint-disable-next-line no-console
71
76
  console.log('trace batch size chunking OK');