@reproapp/node-sdk 0.0.45 → 0.0.47

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,39 @@
1
+ process.env.REPRO_SDK_BACKGROUND_WORK_TIMEOUT_MS = '20';
2
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
3
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
4
+
5
+ const assert = require('node:assert/strict');
6
+ const { __reproTestHooks } = require('../dist');
7
+
8
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
+
10
+ async function main() {
11
+ const hooks = __reproTestHooks;
12
+ hooks.resetSdkBackgroundQueuesForTest();
13
+ const order = [];
14
+
15
+ hooks.scheduleSdkBackgroundWorkForTest(async () => {
16
+ order.push('first-start');
17
+ await sleep(80);
18
+ order.push('first-end');
19
+ }, { priority: 'high' });
20
+ hooks.scheduleSdkBackgroundWorkForTest(() => {
21
+ order.push('second');
22
+ }, { priority: 'high' });
23
+
24
+ await hooks.drainSdkBackgroundQueueForTest();
25
+
26
+ assert.deepEqual(
27
+ order,
28
+ ['first-start', 'first-end', 'second'],
29
+ 'a slow background capture must finish before the queue advances',
30
+ );
31
+
32
+ hooks.resetSdkBackgroundQueuesForTest();
33
+ console.log('sdk background timeout isolation OK');
34
+ }
35
+
36
+ main().catch((error) => {
37
+ console.error(error);
38
+ process.exit(1);
39
+ });
@@ -43,6 +43,10 @@ function main() {
43
43
  );
44
44
 
45
45
  batches.forEach((batch, index) => {
46
+ assert(
47
+ batch.length <= 100,
48
+ `batch ${index} exceeded the 100-event transport limit: ${batch.length}`,
49
+ );
46
50
  const size = __reproTestHooks.estimateTraceBatchSerializedBytesForTest(
47
51
  batch,
48
52
  'RID_test_trace_batch_size',
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+
3
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
4
+ process.env.TRACE_IDLE_FLUSH_MS = '25';
5
+ process.env.SESSION_DRAIN_TIMEOUT_MS = '0';
6
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
7
+
8
+ const assert = require('node:assert');
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const { EventEmitter } = require('node:events');
12
+ const {
13
+ initReproTracing,
14
+ reproMiddleware,
15
+ } = require('../dist');
16
+
17
+ const excludeTenantModule =
18
+ /src[\\/]modules[\\/]core[\\/]database[\\/]modules[\\/]tenant[\\/]tenant-database\.module\.ts$/;
19
+
20
+ function writeFixture(root) {
21
+ const distRoot = path.join(root, 'dist');
22
+ const tenantDistDir = path.join(
23
+ distRoot,
24
+ 'modules/core/database/modules/tenant',
25
+ );
26
+ const tenantSourceFile = path.join(
27
+ root,
28
+ 'src/modules/core/database/modules/tenant/tenant-database.module.ts',
29
+ );
30
+ const tenantDistFile = path.join(tenantDistDir, 'tenant-database.module.js');
31
+ const tenantMapFile = `${tenantDistFile}.map`;
32
+ const callerFile = path.join(distRoot, 'caller.js');
33
+
34
+ fs.mkdirSync(tenantDistDir, { recursive: true });
35
+ fs.writeFileSync(
36
+ tenantDistFile,
37
+ [
38
+ "'use strict';",
39
+ 'function connectTenantDatabase() {',
40
+ " return 'connected';",
41
+ '}',
42
+ 'module.exports = { connectTenantDatabase };',
43
+ '//# sourceMappingURL=tenant-database.module.js.map',
44
+ '',
45
+ ].join('\n'),
46
+ );
47
+ fs.writeFileSync(
48
+ tenantMapFile,
49
+ JSON.stringify({
50
+ version: 3,
51
+ file: path.basename(tenantDistFile),
52
+ sourceRoot: '',
53
+ sources: [tenantSourceFile],
54
+ names: [],
55
+ mappings: '',
56
+ }),
57
+ );
58
+ fs.writeFileSync(
59
+ callerFile,
60
+ [
61
+ "'use strict';",
62
+ "const tenant = require('./modules/core/database/modules/tenant/tenant-database.module');",
63
+ 'function invokeTenantDatabase() {',
64
+ ' return tenant.connectTenantDatabase();',
65
+ '}',
66
+ 'module.exports = { invokeTenantDatabase, tenant };',
67
+ '',
68
+ ].join('\n'),
69
+ );
70
+
71
+ return { callerFile, tenantSourceFile };
72
+ }
73
+
74
+ function makeReqRes() {
75
+ const req = new EventEmitter();
76
+ req.method = 'GET';
77
+ req.url = '/source-exclude';
78
+ req.headers = {
79
+ 'x-bug-session-id': 'S_trace_source_exclude',
80
+ 'x-bug-action-id': 'A_trace_source_exclude',
81
+ 'x-bug-request-start': String(Date.now()),
82
+ };
83
+ req.params = {};
84
+ req.query = {};
85
+
86
+ const res = new EventEmitter();
87
+ res.statusCode = 200;
88
+ res.getHeader = () => undefined;
89
+ res.setHeader = () => {};
90
+ res.json = function json(body) {
91
+ this.body = body;
92
+ this.emit('finish');
93
+ return body;
94
+ };
95
+ res.send = res.json;
96
+ res.write = () => true;
97
+ res.end = () => {
98
+ res.emit('finish');
99
+ return true;
100
+ };
101
+
102
+ return { req, res };
103
+ }
104
+
105
+ function traceEventsFromPosts(posts) {
106
+ const traces = [];
107
+ const append = (value) => {
108
+ if (Array.isArray(value)) {
109
+ traces.push(...value);
110
+ return;
111
+ }
112
+ if (typeof value !== 'string') return;
113
+ try {
114
+ const parsed = JSON.parse(value);
115
+ if (Array.isArray(parsed)) traces.push(...parsed);
116
+ } catch {}
117
+ };
118
+
119
+ for (const post of posts) {
120
+ let payload;
121
+ try {
122
+ payload = JSON.parse(String(post.body || ''));
123
+ } catch {
124
+ continue;
125
+ }
126
+ for (const entry of Array.isArray(payload.entries) ? payload.entries : []) {
127
+ append(entry?.trace);
128
+ append(entry?.request?.trace);
129
+ }
130
+ for (const event of Array.isArray(payload.events) ? payload.events : []) {
131
+ append(event?.payload?.trace);
132
+ append(event?.payload?.request?.trace);
133
+ }
134
+ }
135
+
136
+ return traces;
137
+ }
138
+
139
+ async function main() {
140
+ const fixtureRoot = fs.mkdtempSync(path.join(__dirname, '.tmp-source-exclude-'));
141
+ const posts = [];
142
+ const originalFetch = global.fetch;
143
+
144
+ try {
145
+ const fixture = writeFixture(fixtureRoot);
146
+ const includeFixture = new RegExp(
147
+ `^${fixtureRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
148
+ );
149
+
150
+ global.fetch = async (url, options = {}) => {
151
+ posts.push({ url: String(url), body: options.body });
152
+ return {
153
+ ok: true,
154
+ status: 200,
155
+ json: async () => ({ ok: true }),
156
+ text: async () => '{"ok":true}',
157
+ };
158
+ };
159
+
160
+ initReproTracing({
161
+ include: [includeFixture],
162
+ exclude: [excludeTenantModule],
163
+ logFunctionCalls: false,
164
+ });
165
+
166
+ const caller = require(fixture.callerFile);
167
+ assert.strictEqual(
168
+ caller.tenant.connectTenantDatabase.__repro_instrumented,
169
+ undefined,
170
+ 'source-mapped excluded module must not be instrumented',
171
+ );
172
+ assert.strictEqual(
173
+ caller.invokeTenantDatabase.__repro_instrumented,
174
+ true,
175
+ 'non-excluded caller must remain instrumented',
176
+ );
177
+
178
+ const middleware = reproMiddleware({
179
+ appId: 'APP_trace_source_exclude',
180
+ tenantId: 'TENANT_trace_source_exclude',
181
+ appSecret: 'secret',
182
+ captureHeaders: false,
183
+ });
184
+ const { req, res } = makeReqRes();
185
+ await new Promise((resolve, reject) => {
186
+ middleware(req, res, () => {
187
+ try {
188
+ assert.strictEqual(caller.invokeTenantDatabase(), 'connected');
189
+ res.json({ ok: true });
190
+ resolve();
191
+ } catch (error) {
192
+ reject(error);
193
+ }
194
+ });
195
+ });
196
+ await new Promise((resolve) => setTimeout(resolve, 500));
197
+
198
+ const traces = traceEventsFromPosts(posts);
199
+ assert(
200
+ traces.some((event) => event?.fn === 'invokeTenantDatabase'),
201
+ `expected caller trace, received ${JSON.stringify(traces)}`,
202
+ );
203
+ assert(
204
+ !traces.some(
205
+ (event) =>
206
+ event?.fn === 'connectTenantDatabase' ||
207
+ event?.sourceFile === fixture.tenantSourceFile,
208
+ ),
209
+ `excluded source leaked into trace payload: ${JSON.stringify(traces)}`,
210
+ );
211
+
212
+ console.log('source-mapped tracing exclusion OK');
213
+ } finally {
214
+ global.fetch = originalFetch;
215
+ fs.rmSync(fixtureRoot, { recursive: true, force: true });
216
+ }
217
+ }
218
+
219
+ main().catch((error) => {
220
+ console.error(error);
221
+ process.exitCode = 1;
222
+ });
@@ -31,16 +31,19 @@ function tagExports(value, filename, seen = new WeakSet(), depth = 0, instrument
31
31
 
32
32
  if (typeof value === 'function') {
33
33
  try {
34
- if (!value[SYM_SRC_FILE]) {
34
+ const existingSourceFile = value[SYM_SRC_FILE];
35
+ const belongsToCurrentModule =
36
+ !existingSourceFile || toPosix(existingSourceFile) === toPosix(filename);
37
+ if (!existingSourceFile) {
35
38
  Object.defineProperty(value, SYM_SRC_FILE, { value: filename, configurable: true });
36
39
  }
37
- if (value[SYM_IS_APP] !== isApp) {
40
+ if (belongsToCurrentModule && value[SYM_IS_APP] !== isApp) {
38
41
  Object.defineProperty(value, SYM_IS_APP, { value: isApp, configurable: true });
39
42
  }
40
- if (instrumented && value.__repro_instrumented !== true) {
43
+ if (instrumented && belongsToCurrentModule && value.__repro_instrumented !== true) {
41
44
  Object.defineProperty(value, '__repro_instrumented', { value: true, configurable: true });
42
45
  }
43
- if (instrumented && value[SYM_BODY_TRACED] !== true) {
46
+ if (instrumented && belongsToCurrentModule && value[SYM_BODY_TRACED] !== true) {
44
47
  Object.defineProperty(value, SYM_BODY_TRACED, { value: true, configurable: true });
45
48
  }
46
49
  } catch {}
@@ -82,10 +85,24 @@ function installCJS({ include, exclude, parserPlugins } = {}) {
82
85
  : [ new RegExp('^' + escapeRx(CWD + '/')) ];
83
86
  const exc = Array.isArray(exclude) ? exclude : [];
84
87
 
85
- const shouldHandle = (f) => {
86
- const s = String(f || '').replace(/\\/g, '/');
87
- if (exc.some(rx => rx.test(s))) return false;
88
- return inc.some(rx => rx.test(s));
88
+ const matchesAny = (patterns, file) => {
89
+ const normalized = toPosix(file);
90
+ return patterns.some((rx) => {
91
+ try {
92
+ rx.lastIndex = 0;
93
+ const matched = rx.test(normalized);
94
+ rx.lastIndex = 0;
95
+ return matched;
96
+ } catch {
97
+ return false;
98
+ }
99
+ });
100
+ };
101
+
102
+ const shouldHandle = (physicalFile, sourceFile) => {
103
+ const candidates = [physicalFile, sourceFile].filter(Boolean);
104
+ if (candidates.some(file => matchesAny(exc, file))) return false;
105
+ return candidates.some(file => matchesAny(inc, file));
89
106
  };
90
107
 
91
108
  // ---- Global hook: intercept *all* compiles, regardless of how .ts was compiled ----
@@ -100,8 +117,7 @@ function installCJS({ include, exclude, parserPlugins } = {}) {
100
117
  if (transformedFiles.has(filename)) {
101
118
  // Already transformed once; avoid double-transforming code that may already include __repro_call wrappers.
102
119
  out = code;
103
- } else if (shouldHandle(filename) && isAppFile(filename)) {
104
- try { process.stderr.write(`[trace-debug] compile: ${filename}\n`); } catch {}
120
+ } else if (isAppFile(filename)) {
105
121
  const sourceInfo = getSourceInfo(code, filename);
106
122
  if (sourceInfo?.metaFilename) {
107
123
  metaFilename = sourceInfo.metaFilename;
@@ -109,35 +125,38 @@ function installCJS({ include, exclude, parserPlugins } = {}) {
109
125
  if (typeof sourceInfo?.mapOriginalPosition === 'function') {
110
126
  mapOriginalPosition = sourceInfo.mapOriginalPosition;
111
127
  }
112
- // Transform the already-compiled JS (keeps Nest’s decorator metadata intact)
113
- const res = babel.transformSync(code, {
114
- filename,
115
- sourceType: 'unambiguous',
116
- retainLines: true,
117
- sourceMaps: 'inline',
118
- parserOpts: {
128
+ if (shouldHandle(filename, metaFilename)) {
129
+ try { process.stderr.write(`[trace-debug] compile: ${filename}\n`); } catch {}
130
+ // Transform the already-compiled JS (keeps Nest’s decorator metadata intact).
131
+ const res = babel.transformSync(code, {
132
+ filename,
119
133
  sourceType: 'unambiguous',
120
- // We’re parsing JS here; TS was compiled by ts-node/Nest.
121
- plugins: parserPlugins || [
122
- 'jsx', 'classProperties', 'classPrivateProperties',
123
- 'classPrivateMethods', 'dynamicImport', 'topLevelAwait',
124
- 'optionalChaining', 'nullishCoalescingOperator',
134
+ retainLines: true,
135
+ sourceMaps: 'inline',
136
+ parserOpts: {
137
+ sourceType: 'unambiguous',
138
+ // We’re parsing JS here; TS was compiled by ts-node/Nest.
139
+ plugins: parserPlugins || [
140
+ 'jsx', 'classProperties', 'classPrivateProperties',
141
+ 'classPrivateMethods', 'dynamicImport', 'topLevelAwait',
142
+ 'optionalChaining', 'nullishCoalescingOperator',
143
+ ],
144
+ },
145
+ // only the wrap plugin; do NOT run TS transform here
146
+ plugins: [
147
+ [ makeWrap(metaFilename, {
148
+ mode: 'all',
149
+ wrapGettersSetters: false,
150
+ skipAnonymous: false,
151
+ mapOriginalPosition,
152
+ }) ],
125
153
  ],
126
- },
127
- // only the wrap plugin; do NOT run TS transform here
128
- plugins: [
129
- [ makeWrap(metaFilename, {
130
- mode: 'all',
131
- wrapGettersSetters: false,
132
- skipAnonymous: false,
133
- mapOriginalPosition,
134
- }) ],
135
- ],
136
- compact: false,
137
- comments: true,
138
- });
139
- out = res?.code || code;
140
- wasInstrumented = !!res?.code;
154
+ compact: false,
155
+ comments: true,
156
+ });
157
+ out = res?.code || code;
158
+ wasInstrumented = !!res?.code;
159
+ }
141
160
  transformedFiles.add(filename);
142
161
  }
143
162
  } catch (err) {