@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.
- package/dist/index.d.ts +11 -2
- package/dist/index.js +269 -126
- package/dist/ingest/client.js +11 -0
- package/dist/privacy.d.ts +1 -0
- package/dist/privacy.js +306 -7
- package/package.json +3 -3
- package/src/index.ts +396 -111
- package/src/ingest/client.ts +13 -0
- package/src/privacy.ts +409 -7
- package/test/ingest-high-priority-fairness.test.js +76 -0
- package/test/policy-capture-attestation.test.js +234 -0
- package/test/policy-session-completion.testcontainers.test.js +271 -0
- package/test/privacy-policy-index.test.js +194 -0
- package/test/sdk-background-timeout-isolation.test.js +39 -0
- package/test/trace-batch-size.test.js +4 -0
- package/test/trace-source-exclude.test.js +222 -0
- package/tracer/cjs-hook.js +56 -37
|
@@ -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
|
+
});
|
package/tracer/cjs-hook.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
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 (
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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) {
|