@reproapp/node-sdk 0.0.30 → 0.0.32

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.
@@ -0,0 +1,207 @@
1
+ process.env.REPRO_INGEST_REQUEST_TIMEOUT_MS = '50';
2
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '25';
3
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
4
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
5
+ process.env.TRACE_IDLE_FLUSH_MS = '10';
6
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
7
+
8
+ const assert = require('assert');
9
+ const { EventEmitter } = require('events');
10
+ const { pathToFileURL } = require('url');
11
+ const { join } = require('path');
12
+ const { mkdtempSync, mkdirSync, rmSync, writeFileSync } = require('fs');
13
+ const { tmpdir } = require('os');
14
+ const { initRepro, reproMiddleware, __reproTestHooks } = require('../dist');
15
+ const { flushIngestQueue } = require('../dist/ingest/client');
16
+
17
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
18
+
19
+ async function waitFor(predicate, timeoutMs = 3000) {
20
+ const deadline = Date.now() + timeoutMs;
21
+ while (Date.now() < deadline) {
22
+ if (predicate()) return;
23
+ await sleep(20);
24
+ }
25
+ assert(predicate(), 'timed out waiting for expected ingest payload');
26
+ }
27
+
28
+ function flattenEvents(posts) {
29
+ return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
30
+ }
31
+
32
+ function dbChanges(events) {
33
+ return events
34
+ .filter((event) => event.event_type === 'db_change')
35
+ .flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
36
+ }
37
+
38
+ function makeReqRes() {
39
+ const req = new EventEmitter();
40
+ req.method = 'POST';
41
+ req.url = '/native-mongodb-esm';
42
+ req.originalUrl = '/native-mongodb-esm';
43
+ req.headers = {
44
+ 'x-bug-session-id': 'S_mongo_esm',
45
+ 'x-bug-action-id': 'A_mongo_esm',
46
+ };
47
+ req.query = {};
48
+ req.params = {};
49
+ req.body = undefined;
50
+
51
+ const res = new EventEmitter();
52
+ res.statusCode = 200;
53
+ res.getHeader = () => undefined;
54
+ res.getHeaders = () => ({});
55
+ res.write = () => true;
56
+ res.end = function end() {
57
+ this.emit('finish');
58
+ return this;
59
+ };
60
+
61
+ return { req, res };
62
+ }
63
+
64
+ function createEsmMongoFixture() {
65
+ const root = mkdtempSync(join(tmpdir(), 'repro-node-sdk-esm-mongodb-'));
66
+ const packageDir = join(root, 'node_modules', 'mongodb');
67
+ mkdirSync(packageDir, { recursive: true });
68
+ writeFileSync(join(packageDir, 'package.json'), JSON.stringify({
69
+ name: 'mongodb',
70
+ version: '99.0.0-repro-fixture',
71
+ type: 'module',
72
+ exports: './index.mjs',
73
+ }, null, 2));
74
+ writeFileSync(join(packageDir, 'index.mjs'), `
75
+ export class Collection {
76
+ constructor(dbName, collectionName) {
77
+ this.dbName = dbName;
78
+ this.collectionName = collectionName;
79
+ }
80
+
81
+ async insertOne(doc, options) {
82
+ if (options?.session?.id !== 'esm-native-session-1') {
83
+ throw new Error(\`unexpected session \${options?.session?.id}\`);
84
+ }
85
+ doc._id = doc._id ?? 'subject-esm-1';
86
+ return { acknowledged: true, insertedId: doc._id };
87
+ }
88
+ }
89
+
90
+ export class Db {
91
+ constructor(dbName) {
92
+ this.databaseName = dbName;
93
+ }
94
+
95
+ collection(collectionName) {
96
+ return new Collection(this.databaseName, collectionName);
97
+ }
98
+ }
99
+
100
+ export class MongoClient {
101
+ constructor(uri) {
102
+ this.uri = uri;
103
+ }
104
+
105
+ db(dbName) {
106
+ return new Db(dbName);
107
+ }
108
+ }
109
+ `);
110
+ const appFile = join(root, 'app.mjs');
111
+ writeFileSync(appFile, `
112
+ import { MongoClient } from 'mongodb';
113
+
114
+ const dbClient = new MongoClient('mongodb://127.0.0.1:27017/clinical');
115
+
116
+ export async function insertSubject() {
117
+ const subjectsCollection = dbClient.db('clinical').collection('subjects');
118
+ return subjectsCollection.insertOne(
119
+ { subjectNumber: '10013038', status: 'screened' },
120
+ {
121
+ session: {
122
+ id: 'esm-native-session-1',
123
+ hasEnded: false,
124
+ inTransaction: () => true,
125
+ },
126
+ },
127
+ );
128
+ }
129
+ `);
130
+ return { root, appFile };
131
+ }
132
+
133
+ async function main() {
134
+ const capturedBodies = [];
135
+ const originalFetch = global.fetch;
136
+ let fixtureRoot = null;
137
+ global.fetch = async (_url, init) => {
138
+ capturedBodies.push(JSON.parse(String(init?.body ?? '{}')));
139
+ return {
140
+ ok: true,
141
+ status: 202,
142
+ json: async () => ({ ok: true }),
143
+ text: async () => '{"ok":true}',
144
+ };
145
+ };
146
+
147
+ try {
148
+ const cfg = {
149
+ tenantId: 'TENANT_test',
150
+ appId: 'APP_test',
151
+ appSecret: 'secret',
152
+ captureHeaders: false,
153
+ privacy: { environment: 'dev', captureRaw: true },
154
+ ingestBase: 'http://127.0.0.1:65535',
155
+ };
156
+ await initRepro(cfg);
157
+
158
+ const fixture = createEsmMongoFixture();
159
+ fixtureRoot = fixture.root;
160
+ const fixtureUrl = pathToFileURL(fixture.appFile).href;
161
+ const { insertSubject } = await import(`${fixtureUrl}?run=${Date.now()}`);
162
+
163
+ const middleware = reproMiddleware(cfg);
164
+ const { req, res } = makeReqRes();
165
+ await new Promise((resolve, reject) => {
166
+ middleware(req, res, (err) => {
167
+ if (err) {
168
+ reject(err);
169
+ return;
170
+ }
171
+ Promise.resolve(insertSubject()).then(resolve, reject);
172
+ });
173
+ });
174
+ res.end();
175
+
176
+ await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
177
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
178
+ await flushIngestQueue();
179
+
180
+ const [change] = dbChanges(flattenEvents(capturedBodies));
181
+ assert.strictEqual(change.type, 'mongo');
182
+ assert.strictEqual(change.collection, 'subjects');
183
+ assert.strictEqual(change.op, 'insertOne');
184
+ assert.strictEqual(change.pk._id, 'subject-esm-1');
185
+ assert.strictEqual(change.query.driver, 'mongodb');
186
+ assert.strictEqual(change.query.db, 'clinical');
187
+ assert.strictEqual(change.query.doc.subjectNumber, '10013038');
188
+ assert.strictEqual(change.query.options.session.__kind, 'mongodb_client_session');
189
+ assert.strictEqual(change.query.options.session.id, 'esm-native-session-1');
190
+ assert.strictEqual(change.after.subjectNumber, '10013038');
191
+ assert.strictEqual(change.resultMeta.insertedId, 'subject-esm-1');
192
+
193
+ console.log('mongodb esm import hook OK');
194
+ } finally {
195
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
196
+ await flushIngestQueue();
197
+ if (fixtureRoot) {
198
+ try { rmSync(fixtureRoot, { recursive: true, force: true }); } catch {}
199
+ }
200
+ global.fetch = originalFetch;
201
+ }
202
+ }
203
+
204
+ main().catch((error) => {
205
+ console.error(error);
206
+ process.exit(1);
207
+ });
@@ -0,0 +1,72 @@
1
+ const assert = require('assert');
2
+ const mongoose = require('mongoose');
3
+ const { reproMongoosePlugin, __reproTestHooks } = require('../dist');
4
+
5
+ function execPre(hooks, name, context) {
6
+ return new Promise((resolve, reject) => {
7
+ hooks.execPre(name, context, [], (error) => (error ? reject(error) : resolve()));
8
+ });
9
+ }
10
+
11
+ function execPost(hooks, name, context, args = []) {
12
+ return new Promise((resolve, reject) => {
13
+ hooks.execPost(name, context, args, {}, (error) => (error ? reject(error) : resolve()));
14
+ });
15
+ }
16
+
17
+ async function testPostSaveEarlyReturnReleasesSuppression() {
18
+ const schema = new mongoose.Schema({ name: String });
19
+ reproMongoosePlugin({
20
+ tenantId: 'TENANT_test',
21
+ appId: 'APP_test',
22
+ appSecret: 'secret',
23
+ privacy: { environment: 'dev', captureRaw: true },
24
+ ingestBase: 'http://127.0.0.1:65535',
25
+ })(schema);
26
+ const Model = mongoose.model(`ReproSuppressionCleanup${Date.now()}`, schema, 'subjects');
27
+ const doc = new Model({ name: 'subject' });
28
+
29
+ await __reproTestHooks.runWithRequestContextForTest({ sid: 'S_cleanup', aid: 'A_cleanup' }, async () => {
30
+ await execPre(schema.s.hooks, 'save', doc);
31
+ assert.equal(__reproTestHooks.getNativeMongoSuppressionCountForTest(), 1);
32
+
33
+ // No rid/span is present, so the post hook exits through shouldCaptureDbSpan(false).
34
+ await execPost(schema.s.hooks, 'save', doc, [doc]);
35
+ assert.equal(__reproTestHooks.getNativeMongoSuppressionCountForTest(), 0);
36
+ });
37
+ }
38
+
39
+ async function testPostSaveOutsideOriginalAlsReleasesOwnerStoreSuppression() {
40
+ const schema = new mongoose.Schema({ name: String });
41
+ reproMongoosePlugin({
42
+ tenantId: 'TENANT_test',
43
+ appId: 'APP_test',
44
+ appSecret: 'secret',
45
+ privacy: { environment: 'dev', captureRaw: true },
46
+ ingestBase: 'http://127.0.0.1:65535',
47
+ })(schema);
48
+ const Model = mongoose.model(`ReproSuppressionOwnerStore${Date.now()}`, schema, 'subjects');
49
+ const doc = new Model({ name: 'subject' });
50
+
51
+ let token;
52
+ await __reproTestHooks.runWithRequestContextForTest({ sid: 'S_owner', aid: 'A_owner', rid: 'R_owner' }, async () => {
53
+ await execPre(schema.s.hooks, 'save', doc);
54
+ assert.equal(__reproTestHooks.getNativeMongoSuppressionCountForTest(), 1);
55
+ token = doc.__repro_native_mongo_suppression;
56
+ assert.equal(token.ownerStore.nativeMongoSuppressions.length, 1);
57
+ });
58
+
59
+ await execPost(schema.s.hooks, 'save', doc, [doc]);
60
+ assert.equal(token.ownerStore, undefined);
61
+ }
62
+
63
+ async function main() {
64
+ await testPostSaveEarlyReturnReleasesSuppression();
65
+ await testPostSaveOutsideOriginalAlsReleasesOwnerStoreSuppression();
66
+ console.log('mongodb suppression cleanup OK');
67
+ }
68
+
69
+ main().catch((error) => {
70
+ console.error(error);
71
+ process.exit(1);
72
+ });