@reproapp/node-sdk 0.0.19 → 0.0.21
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
|
@@ -4175,13 +4175,16 @@ function reproMiddleware(cfg) {
|
|
|
4175
4175
|
privacy: activePrivacy,
|
|
4176
4176
|
},
|
|
4177
4177
|
}, requestValueEntries);
|
|
4178
|
+
const responseDurationMs = finishedAt !== null && Number.isFinite(finishedAt)
|
|
4179
|
+
? Math.max(0, finishedAt - t0)
|
|
4180
|
+
: Date.now() - t0;
|
|
4178
4181
|
const requestPayload = {
|
|
4179
4182
|
rid,
|
|
4180
4183
|
method: req.method,
|
|
4181
4184
|
url,
|
|
4182
4185
|
path,
|
|
4183
4186
|
status: res.statusCode,
|
|
4184
|
-
durMs:
|
|
4187
|
+
durMs: responseDurationMs,
|
|
4185
4188
|
headers: maskedHeaders,
|
|
4186
4189
|
key,
|
|
4187
4190
|
respBody: responseBody,
|
|
@@ -4217,6 +4220,12 @@ function reproMiddleware(cfg) {
|
|
|
4217
4220
|
return;
|
|
4218
4221
|
requestCaptureScheduled = true;
|
|
4219
4222
|
try {
|
|
4223
|
+
if (capturedBody === undefined && chunks.length) {
|
|
4224
|
+
const buf = Buffer.isBuffer(chunks[0])
|
|
4225
|
+
? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
|
|
4226
|
+
: Buffer.from(chunks.map(String).join(''));
|
|
4227
|
+
capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
|
|
4228
|
+
}
|
|
4220
4229
|
const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
|
|
4221
4230
|
const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(chosenEndpoint, hasTraceEvents);
|
|
4222
4231
|
post(cfg, sid, {
|
|
@@ -4333,13 +4342,7 @@ function reproMiddleware(cfg) {
|
|
|
4333
4342
|
finishedAt = Date.now();
|
|
4334
4343
|
finishedAtAligned = alignTimestamp(finishedAt);
|
|
4335
4344
|
}
|
|
4336
|
-
|
|
4337
|
-
const buf = Buffer.isBuffer(chunks[0])
|
|
4338
|
-
? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
|
|
4339
|
-
: Buffer.from(chunks.map(String).join(''));
|
|
4340
|
-
capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
|
|
4341
|
-
}
|
|
4342
|
-
void emitRequestCaptureAsync();
|
|
4345
|
+
scheduleSdkBackgroundWork(emitRequestCaptureAsync, { priority: 'high' });
|
|
4343
4346
|
if (!flushPayload) {
|
|
4344
4347
|
flushPayload = async () => {
|
|
4345
4348
|
let traceBatches = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"description": "Repro Nest SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"build": "tsc -p tsconfig.json",
|
|
13
13
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
15
|
-
"test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/ingest-queue-retention.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js",
|
|
16
|
-
"test:containers": "npm run build && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js"
|
|
15
|
+
"test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-capture-background.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/ingest-queue-retention.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/function-arg-identity.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js",
|
|
16
|
+
"test:containers": "npm run build && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js && node test/function-arg-identity.testcontainers.test.js"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"express": "^5.1.0",
|
package/src/index.ts
CHANGED
|
@@ -5099,13 +5099,17 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5099
5099
|
},
|
|
5100
5100
|
}, requestValueEntries);
|
|
5101
5101
|
|
|
5102
|
+
const responseDurationMs =
|
|
5103
|
+
finishedAt !== null && Number.isFinite(finishedAt)
|
|
5104
|
+
? Math.max(0, finishedAt - t0)
|
|
5105
|
+
: Date.now() - t0;
|
|
5102
5106
|
const requestPayload: Record<string, any> = {
|
|
5103
5107
|
rid,
|
|
5104
5108
|
method: req.method,
|
|
5105
5109
|
url,
|
|
5106
5110
|
path,
|
|
5107
5111
|
status: res.statusCode,
|
|
5108
|
-
durMs:
|
|
5112
|
+
durMs: responseDurationMs,
|
|
5109
5113
|
headers: maskedHeaders,
|
|
5110
5114
|
key,
|
|
5111
5115
|
respBody: responseBody,
|
|
@@ -5134,6 +5138,13 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5134
5138
|
if (requestCaptureScheduled) return;
|
|
5135
5139
|
requestCaptureScheduled = true;
|
|
5136
5140
|
try {
|
|
5141
|
+
if (capturedBody === undefined && chunks.length) {
|
|
5142
|
+
const buf = Buffer.isBuffer(chunks[0])
|
|
5143
|
+
? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
|
|
5144
|
+
: Buffer.from(chunks.map(String).join(''));
|
|
5145
|
+
capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5137
5148
|
const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
|
|
5138
5149
|
const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(
|
|
5139
5150
|
chosenEndpoint,
|
|
@@ -5259,14 +5270,8 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5259
5270
|
finishedAt = Date.now();
|
|
5260
5271
|
finishedAtAligned = alignTimestamp(finishedAt);
|
|
5261
5272
|
}
|
|
5262
|
-
if (capturedBody === undefined && chunks.length) {
|
|
5263
|
-
const buf = Buffer.isBuffer(chunks[0])
|
|
5264
|
-
? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
|
|
5265
|
-
: Buffer.from(chunks.map(String).join(''));
|
|
5266
|
-
capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
|
|
5267
|
-
}
|
|
5268
5273
|
|
|
5269
|
-
|
|
5274
|
+
scheduleSdkBackgroundWork(emitRequestCaptureAsync, { priority: 'high' });
|
|
5270
5275
|
|
|
5271
5276
|
if (!flushPayload) {
|
|
5272
5277
|
flushPayload = async () => {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const vm = require('vm');
|
|
3
|
+
const babel = require('@babel/core');
|
|
4
|
+
const makeWrapPlugin = require('../tracer/wrap-plugin');
|
|
5
|
+
const { trace, setFunctionLogsEnabled } = require('../tracer/runtime');
|
|
6
|
+
|
|
7
|
+
setFunctionLogsEnabled(false);
|
|
8
|
+
|
|
9
|
+
function transform(source, filename) {
|
|
10
|
+
return babel.transformSync(source, {
|
|
11
|
+
filename,
|
|
12
|
+
plugins: [makeWrapPlugin(filename, {})],
|
|
13
|
+
parserOpts: { sourceType: 'script', plugins: ['typescript'] },
|
|
14
|
+
generatorOpts: { retainLines: true },
|
|
15
|
+
}).code;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadInstrumentedModule(source, filename, extraContext = {}) {
|
|
19
|
+
const code = transform(source, filename);
|
|
20
|
+
const module = { exports: {} };
|
|
21
|
+
const context = {
|
|
22
|
+
module,
|
|
23
|
+
exports: module.exports,
|
|
24
|
+
require,
|
|
25
|
+
console,
|
|
26
|
+
__trace: global.__trace,
|
|
27
|
+
__repro_call: global.__repro_call,
|
|
28
|
+
Promise,
|
|
29
|
+
Buffer,
|
|
30
|
+
setTimeout,
|
|
31
|
+
clearTimeout,
|
|
32
|
+
setImmediate,
|
|
33
|
+
clearImmediate,
|
|
34
|
+
...extraContext,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
vm.runInNewContext(code, context, { filename });
|
|
38
|
+
return { exports: module.exports, code };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
const filename = `${process.cwd()}/node-sdk/test/fixtures/ioc-container.fixture.js`;
|
|
43
|
+
const external = {
|
|
44
|
+
readDepNames(comp) {
|
|
45
|
+
return comp.depList.map((dep) => dep.name);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const { exports: mod } = loadInstrumentedModule(`
|
|
49
|
+
class Leaf {}
|
|
50
|
+
Leaf.depList = [];
|
|
51
|
+
|
|
52
|
+
class Root {}
|
|
53
|
+
Root.depList = [Leaf];
|
|
54
|
+
|
|
55
|
+
function recurseDeps(comp) {
|
|
56
|
+
const names = [comp.name];
|
|
57
|
+
for (const dep of comp.depList) {
|
|
58
|
+
names.push(...recurseDeps(dep));
|
|
59
|
+
}
|
|
60
|
+
return names;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getDeps(comp) {
|
|
64
|
+
return recurseDeps(comp);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runAppToApp() {
|
|
68
|
+
return getDeps(Root);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runThroughExternal() {
|
|
72
|
+
return external.readDepNames(Root);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { runAppToApp, runThroughExternal, Root, Leaf };
|
|
76
|
+
`, filename, { external });
|
|
77
|
+
|
|
78
|
+
assert.deepStrictEqual(
|
|
79
|
+
Array.from(trace.withTrace('function-arg-app-to-app', () => mod.runAppToApp())),
|
|
80
|
+
['Root', 'Leaf'],
|
|
81
|
+
);
|
|
82
|
+
assert.deepStrictEqual(
|
|
83
|
+
Array.from(trace.withTrace('function-arg-external', () => mod.runThroughExternal())),
|
|
84
|
+
['Leaf'],
|
|
85
|
+
);
|
|
86
|
+
assert.strictEqual(mod.Root.depList[0], mod.Leaf);
|
|
87
|
+
|
|
88
|
+
console.log('function argument identity OK');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main().catch((error) => {
|
|
92
|
+
console.error(error);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const vm = require('vm');
|
|
3
|
+
const babel = require('@babel/core');
|
|
4
|
+
const mongoose = require('mongoose');
|
|
5
|
+
const { GenericContainer, Wait } = require('testcontainers');
|
|
6
|
+
|
|
7
|
+
const makeWrapPlugin = require('../tracer/wrap-plugin');
|
|
8
|
+
const { trace, setFunctionLogsEnabled } = require('../tracer/runtime');
|
|
9
|
+
|
|
10
|
+
setFunctionLogsEnabled(false);
|
|
11
|
+
|
|
12
|
+
function transform(source, filename) {
|
|
13
|
+
return babel.transformSync(source, {
|
|
14
|
+
filename,
|
|
15
|
+
plugins: [makeWrapPlugin(filename, {})],
|
|
16
|
+
parserOpts: { sourceType: 'script', plugins: ['typescript'] },
|
|
17
|
+
generatorOpts: { retainLines: true },
|
|
18
|
+
}).code;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadInstrumentedModule(source, filename, extraContext = {}) {
|
|
22
|
+
const code = transform(source, filename);
|
|
23
|
+
const module = { exports: {} };
|
|
24
|
+
const context = {
|
|
25
|
+
module,
|
|
26
|
+
exports: module.exports,
|
|
27
|
+
require,
|
|
28
|
+
console,
|
|
29
|
+
__trace: global.__trace,
|
|
30
|
+
__repro_call: global.__repro_call,
|
|
31
|
+
Promise,
|
|
32
|
+
Buffer,
|
|
33
|
+
setTimeout,
|
|
34
|
+
clearTimeout,
|
|
35
|
+
setImmediate,
|
|
36
|
+
clearImmediate,
|
|
37
|
+
...extraContext,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
vm.runInNewContext(code, context, { filename });
|
|
41
|
+
return { exports: module.exports, code };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function startMongo() {
|
|
45
|
+
return new GenericContainer('mongo:7')
|
|
46
|
+
.withExposedPorts(27017)
|
|
47
|
+
.withTmpFs({ '/data/db': 'rw,noexec,nosuid,size=1g' })
|
|
48
|
+
.withCommand(['mongod', '--bind_ip_all', '--setParameter', 'diagnosticDataCollectionEnabled=false'])
|
|
49
|
+
.withWaitStrategy(Wait.forListeningPorts())
|
|
50
|
+
.start();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
let container;
|
|
55
|
+
let connection;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
container = await startMongo();
|
|
59
|
+
const uri = `mongodb://${container.getHost()}:${container.getMappedPort(27017)}/repro_node_sdk_identity_test`;
|
|
60
|
+
connection = await mongoose.createConnection(uri).asPromise();
|
|
61
|
+
const SeenDependency = connection.model('SeenDependency', new mongoose.Schema({
|
|
62
|
+
root: { type: String, required: true },
|
|
63
|
+
dep: { type: String, required: true },
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
const external = {
|
|
67
|
+
async persistDepNames(comp) {
|
|
68
|
+
const docs = comp.depList.map((dep) => ({
|
|
69
|
+
root: comp.name,
|
|
70
|
+
dep: dep.name,
|
|
71
|
+
}));
|
|
72
|
+
await SeenDependency.insertMany(docs);
|
|
73
|
+
return docs.length;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const filename = `${process.cwd()}/node-sdk/test/fixtures/ioc-container-container.fixture.js`;
|
|
78
|
+
const { exports: mod } = loadInstrumentedModule(`
|
|
79
|
+
class Leaf {}
|
|
80
|
+
Leaf.depList = [];
|
|
81
|
+
|
|
82
|
+
class Root {}
|
|
83
|
+
Root.depList = [Leaf];
|
|
84
|
+
|
|
85
|
+
async function runThroughExternal() {
|
|
86
|
+
return external.persistDepNames(Root);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { runThroughExternal };
|
|
90
|
+
`, filename, { external });
|
|
91
|
+
|
|
92
|
+
const inserted = await trace.withTrace('function-arg-container', () => mod.runThroughExternal());
|
|
93
|
+
|
|
94
|
+
assert.strictEqual(inserted, 1);
|
|
95
|
+
assert.deepStrictEqual(
|
|
96
|
+
await SeenDependency.find({}, { _id: 0, root: 1, dep: 1 }).lean(),
|
|
97
|
+
[{ root: 'Root', dep: 'Leaf' }],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
console.log('function argument identity testcontainers OK');
|
|
101
|
+
} finally {
|
|
102
|
+
if (connection) {
|
|
103
|
+
await connection.close().catch(() => {});
|
|
104
|
+
}
|
|
105
|
+
if (container) {
|
|
106
|
+
await container.stop().catch(() => {});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
main().catch((error) => {
|
|
112
|
+
console.error(error);
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '500';
|
|
2
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
|
|
3
|
+
process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
|
|
4
|
+
process.env.TRACE_IDLE_FLUSH_MS = '10';
|
|
5
|
+
process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
|
|
6
|
+
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
const { EventEmitter } = require('events');
|
|
9
|
+
const { reproMiddleware } = require('../dist');
|
|
10
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
11
|
+
|
|
12
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
|
|
14
|
+
async function waitFor(predicate, timeoutMs = 1500) {
|
|
15
|
+
const deadline = Date.now() + timeoutMs;
|
|
16
|
+
while (Date.now() < deadline) {
|
|
17
|
+
if (predicate()) return;
|
|
18
|
+
await sleep(20);
|
|
19
|
+
}
|
|
20
|
+
assert(predicate(), 'timed out waiting for expected condition');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeReqRes(sessionId, actionId) {
|
|
24
|
+
const req = new EventEmitter();
|
|
25
|
+
req.method = 'GET';
|
|
26
|
+
req.url = sessionId ? '/captured' : '/held-open';
|
|
27
|
+
req.headers = sessionId && actionId
|
|
28
|
+
? {
|
|
29
|
+
'x-bug-session-id': sessionId,
|
|
30
|
+
'x-bug-action-id': actionId,
|
|
31
|
+
'x-bug-request-start': String(Date.now()),
|
|
32
|
+
}
|
|
33
|
+
: {};
|
|
34
|
+
req.body = undefined;
|
|
35
|
+
req.params = {};
|
|
36
|
+
req.query = {};
|
|
37
|
+
|
|
38
|
+
const res = new EventEmitter();
|
|
39
|
+
res.statusCode = 200;
|
|
40
|
+
res.getHeader = () => 'application/json';
|
|
41
|
+
res.setHeader = () => {};
|
|
42
|
+
res.json = function (body) { this.body = body; this.emit('finish'); return body; };
|
|
43
|
+
res.send = function (body) { this.body = body; this.emit('finish'); return body; };
|
|
44
|
+
res.write = () => true;
|
|
45
|
+
res.end = () => { res.emit('finish'); return true; };
|
|
46
|
+
|
|
47
|
+
return { req, res };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function flattenEvents(posts) {
|
|
51
|
+
return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function countEvents(posts, eventType) {
|
|
55
|
+
return flattenEvents(posts).filter((event) => event?.event_type === eventType).length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function invokeMiddleware(middleware, req, res, handler) {
|
|
59
|
+
await new Promise((resolve, reject) => {
|
|
60
|
+
void middleware(req, res, async (err) => {
|
|
61
|
+
if (err) {
|
|
62
|
+
reject(err);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await handler();
|
|
67
|
+
resolve();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
reject(error);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
const capturedBodies = [];
|
|
77
|
+
const originalFetch = global.fetch;
|
|
78
|
+
global.fetch = async (_url, init = {}) => {
|
|
79
|
+
capturedBodies.push(JSON.parse(String(init.body || '{}')));
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
status: 202,
|
|
83
|
+
json: async () => ({ ok: true }),
|
|
84
|
+
text: async () => '{"ok":true}',
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const cfg = {
|
|
90
|
+
tenantId: 'TENANT_test',
|
|
91
|
+
appId: 'APP_test',
|
|
92
|
+
appSecret: 'secret',
|
|
93
|
+
captureHeaders: false,
|
|
94
|
+
privacy: { environment: 'dev' },
|
|
95
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
96
|
+
};
|
|
97
|
+
const middleware = reproMiddleware(cfg);
|
|
98
|
+
|
|
99
|
+
const held = makeReqRes(null, null);
|
|
100
|
+
await invokeMiddleware(middleware, held.req, held.res, async () => {
|
|
101
|
+
// Keep this request open to simulate another client endpoint still in flight.
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const tagged = makeReqRes('S_request_capture_background', 'A_request_capture_background');
|
|
105
|
+
await invokeMiddleware(middleware, tagged.req, tagged.res, async () => {
|
|
106
|
+
tagged.res.json({ ok: true, rows: Array.from({ length: 50 }, (_, i) => ({ i, value: `row-${i}` })) });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await sleep(100);
|
|
110
|
+
assert.equal(
|
|
111
|
+
countEvents(capturedBodies, 'backend_request'),
|
|
112
|
+
0,
|
|
113
|
+
`request capture should defer while another client request is active: ${JSON.stringify(capturedBodies)}`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
held.res.emit('finish');
|
|
117
|
+
await waitFor(() => countEvents(capturedBodies, 'backend_request') === 1);
|
|
118
|
+
await flushIngestQueue();
|
|
119
|
+
|
|
120
|
+
assert.equal(countEvents(capturedBodies, 'backend_request'), 1, JSON.stringify(capturedBodies));
|
|
121
|
+
console.log('request capture background OK');
|
|
122
|
+
} finally {
|
|
123
|
+
global.fetch = originalFetch;
|
|
124
|
+
await flushIngestQueue();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main().catch((error) => {
|
|
129
|
+
console.error(error);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
package/tracer/runtime.js
CHANGED
|
@@ -312,6 +312,59 @@ const SYM_SKIP_WRAP= Symbol.for('__repro_skip_wrap'); // guard to avoid wrapping
|
|
|
312
312
|
const SYM_BODY_TRACED = Symbol.for('__repro_body_traced'); // set on functions whose bodies already emit trace enter/exit
|
|
313
313
|
const SYM_UNAWAITED = Symbol.for('__repro_unawaited');
|
|
314
314
|
const SYM_PROMISE_STORE = Symbol.for('__repro_promise_store');
|
|
315
|
+
const REPRO_FUNCTION_SYMBOLS = new Set([
|
|
316
|
+
SYM_SRC_FILE,
|
|
317
|
+
SYM_IS_APP,
|
|
318
|
+
SYM_SKIP_WRAP,
|
|
319
|
+
SYM_BODY_TRACED,
|
|
320
|
+
SYM_UNAWAITED,
|
|
321
|
+
SYM_PROMISE_STORE,
|
|
322
|
+
]);
|
|
323
|
+
const FUNCTION_IDENTITY_BUILTINS = new Set([
|
|
324
|
+
'length',
|
|
325
|
+
'name',
|
|
326
|
+
'prototype',
|
|
327
|
+
'arguments',
|
|
328
|
+
'caller',
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
function isClassLikeFunction(fn) {
|
|
332
|
+
if (!fn || typeof fn !== 'function') return false;
|
|
333
|
+
try {
|
|
334
|
+
const src = Function.prototype.toString.call(fn);
|
|
335
|
+
if (/^class\s/.test(src)) return true;
|
|
336
|
+
} catch {}
|
|
337
|
+
try {
|
|
338
|
+
const proto = fn.prototype;
|
|
339
|
+
if (proto && typeof proto === 'object') {
|
|
340
|
+
const props = Object.getOwnPropertyNames(proto);
|
|
341
|
+
if (props.some((name) => name !== 'constructor')) return true;
|
|
342
|
+
}
|
|
343
|
+
} catch {}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function hasCustomFunctionMetadata(fn) {
|
|
348
|
+
if (!fn || typeof fn !== 'function') return false;
|
|
349
|
+
try {
|
|
350
|
+
for (const name of Object.getOwnPropertyNames(fn)) {
|
|
351
|
+
if (FUNCTION_IDENTITY_BUILTINS.has(name)) continue;
|
|
352
|
+
if (name.startsWith('__repro')) continue;
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
} catch {}
|
|
356
|
+
try {
|
|
357
|
+
for (const sym of Object.getOwnPropertySymbols(fn)) {
|
|
358
|
+
if (REPRO_FUNCTION_SYMBOLS.has(sym)) continue;
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
} catch {}
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function shouldPreserveFunctionArgumentIdentity(fn) {
|
|
366
|
+
return isClassLikeFunction(fn) || hasCustomFunctionMetadata(fn);
|
|
367
|
+
}
|
|
315
368
|
|
|
316
369
|
function emit(ev){
|
|
317
370
|
if (EMITTING) return;
|
|
@@ -562,6 +615,7 @@ if (!global.__repro_call) {
|
|
|
562
615
|
const wrapArgWithStore = (arg, baseStoreSnapshot) => {
|
|
563
616
|
if (typeof arg !== 'function') return arg;
|
|
564
617
|
if (arg[SYM_SKIP_WRAP] || arg.__repro_arg_wrapper) return arg;
|
|
618
|
+
if (shouldPreserveFunctionArgumentIdentity(arg)) return arg;
|
|
565
619
|
const hasContext = baseStoreSnapshot
|
|
566
620
|
&& (baseStoreSnapshot.traceId != null
|
|
567
621
|
|| (Array.isArray(baseStoreSnapshot.__repro_span_stack) && baseStoreSnapshot.__repro_span_stack.length));
|
|
@@ -601,6 +655,13 @@ if (!global.__repro_call) {
|
|
|
601
655
|
return false;
|
|
602
656
|
};
|
|
603
657
|
|
|
658
|
+
const shouldWrapFunctionArgsForCallee = (targetFn, targetThis) => {
|
|
659
|
+
if (isAsyncLocalContextSetter(targetFn, targetThis)) return false;
|
|
660
|
+
if (isApp || instrumented || bodyTraced) return false;
|
|
661
|
+
if (targetFn && targetFn[SYM_IS_APP] === true) return false;
|
|
662
|
+
return true;
|
|
663
|
+
};
|
|
664
|
+
|
|
604
665
|
const runWithCallStore = (fnToRun) => {
|
|
605
666
|
const callStore = makeCallStore();
|
|
606
667
|
const parentSpanId = callStore.__repro_span_stack.length
|
|
@@ -641,7 +702,7 @@ if (!global.__repro_call) {
|
|
|
641
702
|
// After entering, snapshot a clean store that captures the parent + this call’s span,
|
|
642
703
|
// so callbacks invoked during the callee run are isolated per invocation.
|
|
643
704
|
const snapshotForArgs = cloneStore(als.getStore());
|
|
644
|
-
const shouldWrap =
|
|
705
|
+
const shouldWrap = shouldWrapFunctionArgsForCallee(fn, thisArg);
|
|
645
706
|
const argsForCall = shouldWrap ? wrapArgsWithStore(args, snapshotForArgs) : args;
|
|
646
707
|
try {
|
|
647
708
|
const out = invokeTarget(thisArg, argsForCall);
|
|
@@ -743,7 +804,7 @@ if (!global.__repro_call) {
|
|
|
743
804
|
? (forkAlsStoreForUnawaited(callStoreForArgs || ctx) || cloneStore(callStoreForArgs || ctx))
|
|
744
805
|
: null;
|
|
745
806
|
try {
|
|
746
|
-
const shouldWrap =
|
|
807
|
+
const shouldWrap = shouldWrapFunctionArgsForCallee(fn, thisArg);
|
|
747
808
|
const argsForCall = shouldWrap ? wrapArgsWithStore(args, callStoreForArgs || ctx) : args;
|
|
748
809
|
const normalizedCallFile = callFile ? String(callFile).trim() : '';
|
|
749
810
|
const callsiteFile = normalizedCallFile ? normalizedCallFile : null;
|