@reproapp/node-sdk 0.0.58 → 0.0.60

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,8 +18,9 @@
18
18
  "build": "tsc -p tsconfig.json",
19
19
  "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
20
20
  "prepublishOnly": "npm run build",
21
- "test": "npm run build && node test/semantic-serialization.test.js && node test/semantic-serialization-trace.test.js && node test/content-addressed-trace-values.test.js && node test/trace-observation-snapshots.test.js && node test/session-storage-budget.test.js && node test/trace-source-exclude.test.js && 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/init-auto-http-server.test.js && node test/backend-live-mode.test.js && node test/client-performance-regression.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/sdk-background-timeout-isolation.test.js && node test/runtime-service-registration.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-high-priority-fairness.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-batching.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-service-scope.test.js && node test/privacy-policy-index.test.js && node test/policy-capture-attestation.test.js && node test/object-id-shapes.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js && node test/postgresql-integration.test.js",
22
- "test:containers": "npm run build && node test/trace-worker-policy-reference.test.js && node test/trace-throughput.testcontainers.test.js && node test/twelve-element-capture.testcontainers.test.js && node test/background-continuous-traffic.testcontainers.test.js && 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 && node test/mongodb-mongoose-native-suppression.testcontainers.test.js && node test/content-addressed-mongodb.testcontainers.test.js && node test/policy-session-completion.testcontainers.test.js && node test/postgresql-integrations.testcontainers.test.js"
21
+ "test": "npm run build && node test/optional-chain-semantics.test.js && node test/semantic-serialization.test.js && node test/semantic-serialization-trace.test.js && node test/content-addressed-trace-values.test.js && node test/trace-observation-snapshots.test.js && node test/session-storage-budget.test.js && node test/trace-source-exclude.test.js && 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/init-auto-http-server.test.js && node test/backend-live-mode.test.js && node test/client-performance-regression.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/sdk-background-timeout-isolation.test.js && node test/runtime-service-registration.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-high-priority-fairness.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-batching.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-service-scope.test.js && node test/privacy-policy-index.test.js && node test/policy-capture-attestation.test.js && node test/object-id-shapes.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js && node test/postgresql-integration.test.js",
22
+ "test:containers": "npm run build && node test/trace-worker-policy-reference.test.js && node test/trace-throughput.testcontainers.test.js && node test/twelve-element-capture.testcontainers.test.js && node test/background-continuous-traffic.testcontainers.test.js && 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 && node test/mongodb-mongoose-native-suppression.testcontainers.test.js && node test/content-addressed-mongodb.testcontainers.test.js && node test/policy-session-completion.testcontainers.test.js && node test/postgresql-integrations.testcontainers.test.js",
23
+ "test:optional-chains": "npm run build && node test/optional-chain-semantics.test.js && node test/optional-chain.testcontainers.test.js && node test/optional-chain-http.testcontainers.test.js"
23
24
  },
24
25
  "peerDependencies": {
25
26
  "express": "^5.1.0",
@@ -45,6 +46,7 @@
45
46
  "@babel/core": "^7.28.4",
46
47
  "@babel/generator": "^7.28.3",
47
48
  "@babel/parser": "^7.28.4",
49
+ "@babel/plugin-transform-optional-chaining": "^7.29.7",
48
50
  "@babel/plugin-transform-typescript": "^7.28.0",
49
51
  "@babel/traverse": "^7.28.4",
50
52
  "@babel/types": "^7.28.4",
@@ -8,6 +8,7 @@ const capturedBodies = [];
8
8
 
9
9
  global.fetch = async (url, init = {}) => {
10
10
  const target = String(url || '');
11
+ if (target.endsWith('/v1/sdk/runtime-services')) return { ok: true };
11
12
  if (!target.includes('/v1/ingest/events')) {
12
13
  throw new Error(`unexpected fetch target: ${target}`);
13
14
  }
@@ -56,6 +57,9 @@ async function main() {
56
57
  appSecret: 'secret',
57
58
  appName: 'express-trace-http-args',
58
59
  serviceName: 'express-trace-http-args',
60
+ // This fixture tests HTTP arguments, not hosted policy initialization.
61
+ privacy: { enabled: false },
62
+ apiBase: 'http://127.0.0.1:65535',
59
63
  ingestBase: 'http://127.0.0.1:65535',
60
64
  tracing: {
61
65
  disableFunctionTypes: ['constructor'],
@@ -0,0 +1,52 @@
1
+ const http = require('node:http');
2
+ const assert = require('node:assert/strict');
3
+ const { MongoClient } = require('mongodb');
4
+ const { initRepro, __reproTestHooks } = require('../../dist');
5
+ const { flushIngestQueue } = require('../../dist/ingest/client');
6
+
7
+ async function main() {
8
+ const [mode, uri, collector] = process.argv.slice(2);
9
+ if (mode === 'traced') await initRepro({ tenantId: 'TENANT_test', appId: 'APP_test', appSecret: 'test-only',
10
+ apiBase: collector, ingestBase: collector, privacy: { captureRaw: true },
11
+ tracing: { include: [/optional-chain-business\.cjs$/], logFunctionCalls: false } });
12
+ const { processReturn } = require('./optional-chain-business.cjs');
13
+ const client = await new MongoClient(uri).connect();
14
+ const db = client.db('optional_chain');
15
+ const server = http.createServer(async (req, res) => {
16
+ try {
17
+ const value = await processReturn(db.collection('preferences'), db.collection('writes'), req.url.slice(1), mode);
18
+ res.end(JSON.stringify({ value }));
19
+ } catch (error) { res.statusCode = 500; res.end(JSON.stringify({ error: error.name })); }
20
+ });
21
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
22
+ try {
23
+ const responses = [];
24
+ for (const id of ['missing', 'empty', 'valid', 'invalid']) {
25
+ responses.push(await new Promise((resolve, reject) => {
26
+ http.get({ hostname: '127.0.0.1', port: server.address().port, path: `/${id}`,
27
+ headers: { 'x-bug-session-id': `S_optional_${mode}`, 'x-bug-action-id': `A_${id}` } }, res => {
28
+ const chunks = [];
29
+ res.on('data', chunk => chunks.push(chunk));
30
+ res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(Buffer.concat(chunks)) }));
31
+ }).on('error', reject);
32
+ }));
33
+ }
34
+ assert.deepEqual(responses, [
35
+ { status: 200, body: { value: null } }, { status: 200, body: { value: null } },
36
+ { status: 200, body: { value: 1 } }, { status: 500, body: { error: 'TypeError' } },
37
+ ]);
38
+ if (mode === 'traced') {
39
+ for (let i = 0; i < 20; i++) {
40
+ await new Promise(resolve => setTimeout(resolve, 25));
41
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
42
+ await flushIngestQueue();
43
+ }
44
+ }
45
+ assert.equal(await db.collection('writes').countDocuments({ mode }), 1, 'No duplicate/short-circuited writes');
46
+ console.log(`${mode}: HTTP responses and MongoDB side effects passed`);
47
+ } finally {
48
+ server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
49
+ await client.close();
50
+ }
51
+ }
52
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,6 @@
1
+ exports.processReturn = async function processReturn(collection, writes, id, mode) {
2
+ const productReturnsModule = await collection.findOne({ _id: id });
3
+ const preference = productReturnsModule?.productReturnsPreferences.find(entry => entry.id === 1);
4
+ if (preference) await writes.insertOne({ id, mode, preference: preference.id });
5
+ return preference?.id ?? null;
6
+ };
@@ -0,0 +1,27 @@
1
+ import assert from 'node:assert/strict';
2
+ import runtime from '../../tracer/runtime.js';
3
+
4
+ runtime.setFunctionLogsEnabled(false);
5
+ const events = [];
6
+ const unsubscribe = runtime.trace.on(event => events.push(event));
7
+ function selectPreferences(productReturnsModule) {
8
+ return productReturnsModule?.productReturnsPreferences.find(entry => entry.id === 1)?.id;
9
+ }
10
+ await runtime.trace.withTrace('optional-esm', async () => {
11
+ assert.equal(selectPreferences(null), undefined);
12
+ assert.equal(selectPreferences(undefined), undefined);
13
+ assert.equal(selectPreferences({ productReturnsPreferences: [{ id: 1 }] }), 1);
14
+ assert.throws(() => selectPreferences({}), TypeError);
15
+ let calls = 0;
16
+ const increment = () => ++calls;
17
+ const empty = null;
18
+ assert.equal(empty?.nested.method(increment()).value, undefined);
19
+ assert.equal(calls, 0);
20
+ const object = { value: 7, method() { return this; } };
21
+ assert.equal((object?.method)().value, 7);
22
+ assert.equal(object?.method?.().value, 7);
23
+ });
24
+ unsubscribe();
25
+ assert.ok(events.some(event => event.type === 'enter' && event.fn === 'selectPreferences'));
26
+ assert.ok(events.some(event => event.type === 'exit' && event.fn === 'selectPreferences' && event.returnValue === 1));
27
+ console.log('native ESM optional chain and trace evidence passed');
@@ -0,0 +1,55 @@
1
+ const assert = require('node:assert/strict');
2
+ const http = require('node:http');
3
+ const path = require('node:path');
4
+ const { promisify } = require('node:util');
5
+ const run = promisify(require('node:child_process').execFile);
6
+ const { GenericContainer, Wait } = require('testcontainers');
7
+ const { MongoClient } = require('mongodb');
8
+
9
+ async function main() {
10
+ let container, mongo, server;
11
+ const captured = [];
12
+ try {
13
+ container = await new GenericContainer('mongo:7').withExposedPorts(27017)
14
+ .withWaitStrategy(Wait.forListeningPorts()).start();
15
+ const uri = `mongodb://${container.getHost()}:${container.getMappedPort(27017)}`;
16
+ mongo = await new MongoClient(uri).connect();
17
+ await mongo.db('optional_chain').collection('preferences').insertMany([
18
+ { _id: 'empty', productReturnsPreferences: [] },
19
+ { _id: 'valid', productReturnsPreferences: [{ id: 1 }] }, { _id: 'invalid' },
20
+ ]);
21
+ server = http.createServer(async (req, res) => {
22
+ const chunks = [];
23
+ for await (const chunk of req) chunks.push(chunk);
24
+ const body = JSON.parse(Buffer.concat(chunks).toString() || '{}');
25
+ captured.push(...(body.events || []));
26
+ res.writeHead(202, { 'content-type': 'application/json' });
27
+ res.end(JSON.stringify({ accepted: body.events?.length ?? 0 }));
28
+ });
29
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
30
+ const collector = `http://127.0.0.1:${server.address().port}`;
31
+ for (const mode of ['baseline', 'traced']) {
32
+ const result = await run(process.execPath, [path.join(__dirname, 'fixtures/optional-chain-app.cjs'), mode, uri, collector], {
33
+ timeout: 30000, env: { ...process.env, REPRO_INGEST_BASE: collector, REPRO_API_BASE: collector,
34
+ REPRO_API_BASE_URL: collector, REPRO_PRIVACY_API_BASE: collector, TRACE_IDLE_FLUSH_MS: '10',
35
+ TRACE_LINGER_AFTER_FINISH_MS: '0', REPRO_SDK_BACKGROUND_IDLE_DELAY_MS: '0', REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS: '0' },
36
+ });
37
+ console.log(result.stdout.trim());
38
+ }
39
+ const requests = captured.filter(event => event.event_type === 'backend_request');
40
+ assert.equal(requests.length, 4, 'All four requests must remain captured');
41
+ assert.ok(captured.some(event => event.event_type === 'db_change'), 'Database evidence must remain captured');
42
+ const traces = captured.filter(event => event.event_type === 'trace_batch').flatMap(event => event.payload.trace || []);
43
+ assert.ok(traces.some(event => event.fn === 'processReturn' && event.type === 'enter'), 'Arguments/call evidence still present');
44
+ assert.ok(traces.some(event => event.fn === 'processReturn' && event.type === 'exit' && event.returnValue === 1), 'Return-value evidence still present');
45
+ assert.ok(traces.some(event => event.fn === 'processReturn' && event.type === 'exit' && event.threw), 'Real application errors remain captured');
46
+ const manifests = captured.filter(event => event.event_type === 'request_capture_manifest');
47
+ assert.equal(manifests.length, 4);
48
+ assert.ok(manifests.every(event => !event.payload.requestCaptureManifest.captureFailures?.length));
49
+ console.log('HTTP -> traced optional chains -> MongoDB -> collector: outcomes, writes, traces and manifests verified');
50
+ } finally {
51
+ if (server) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); }
52
+ await mongo?.close(); await container?.stop();
53
+ }
54
+ }
55
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,145 @@
1
+ const assert = require('node:assert/strict');
2
+ const vm = require('node:vm');
3
+ const babel = require('@babel/core');
4
+ const ts = require('typescript');
5
+ const Module = require('node:module');
6
+ const path = require('node:path');
7
+ const { installCJS } = require('../tracer/cjs-hook');
8
+ const makeWrap = require('../tracer/wrap-plugin');
9
+ const { trace, setFunctionLogsEnabled } = require('../tracer/runtime');
10
+ setFunctionLogsEnabled(false);
11
+
12
+ const cases = [
13
+ 'root?.productReturnsPreferences.find(x => x.id === 1)',
14
+ 'root?.productReturnsPreferences.find(x => x.id === 1).id',
15
+ 'root?.productReturnsPreferences.find(x => x.id === 2)?.id',
16
+ 'root?.nested.method(arg())',
17
+ 'root?.nested.method?.(arg())',
18
+ 'root?.missing.method(arg())',
19
+ 'root?.missing?.method(arg())',
20
+ 'root?.method(arg()).value',
21
+ 'root?.method(arg())?.value',
22
+ 'root?.method(arg()).next(arg()).value',
23
+ 'root?.[key()]?.(arg())',
24
+ 'root?.nested[key()](arg())',
25
+ 'root?.nested[key()]?.(arg())',
26
+ 'root?.getter(arg())',
27
+ 'root?.getter?.(arg())',
28
+ 'root?.absent?.(arg())',
29
+ 'root?.notCallable?.(arg())',
30
+ '(root?.method)(arg())',
31
+ '(root?.method)?.(arg())',
32
+ '(root?.nested.method)(arg())',
33
+ '(root?.nested.method)?.(arg())',
34
+ '(root?.nested).method(arg())',
35
+ 'root?.method(...spread)',
36
+ 'root?.method(arg(), root?.nested.method(arg()))',
37
+ 'root?.method(await asyncArg())',
38
+ 'root?.nested.method(await asyncArg())?.value',
39
+ 'delete root?.method(arg()).value',
40
+ 'delete root?.nested.value',
41
+ 'root?.method(arg()) ?? "fallback"',
42
+ 'root?.method(arg()) && arg()',
43
+ 'root?.method(arg()) ? arg() : "false"',
44
+ '(root?.method(arg()), arg())',
45
+ 'root?.returnsNull().value',
46
+ 'root?.returnsNull()?.value',
47
+ 'root?.returnsNull().method(arg())',
48
+ 'root?.returnsNull()?.method(arg())',
49
+ 'fn?.(arg()).value',
50
+ 'fn?.(arg())?.value',
51
+ 'fn?.(arg()).next(arg()).value',
52
+ '(fn?.(arg())).value',
53
+ 'root?.nested.method.call(root, arg())',
54
+ 'root?.method.bind(root)(arg())',
55
+ 'root?.throwing?.(arg())',
56
+ 'root?.method(root?.getter(arg()))',
57
+ 'root?.method(root?.absent?.(arg()))',
58
+ 'root?.[await asyncKey()](await asyncArg())',
59
+ 'root?.method(...(root?.productReturnsPreferences ?? []))',
60
+ 'root?.method(arg()).next?.(arg()).value',
61
+ 'root?.method?.(arg()).next?.(arg()).value',
62
+ 'root?.nested.method?.(root?.method(arg()))',
63
+ 'new (root?.Ctor)(arg())',
64
+ 'eval?.("typeof effects")',
65
+ '(function(undefined) { return root?.absent?.(); })("shadowed")',
66
+ '(function(x = root?.nested.method(arg())) { return x; })()',
67
+ '(function*() { return root?.nested.method(yield "pause"); })().next()',
68
+ '(new Derived()).read(root)',
69
+ ];
70
+
71
+ function source(expression) {
72
+ return `module.exports = async function run(kind) {
73
+ const effects = [];
74
+ function arg() { effects.push('arg'); return 7; }
75
+ async function asyncArg() { effects.push('await'); return 7; }
76
+ async function asyncKey() { effects.push('await-key'); return 'method'; }
77
+ function key() { effects.push('key'); return 'method'; }
78
+ const result = { value: 11, next(value) { effects.push('next:' + value); return this; } };
79
+ const object = {
80
+ value: 3, productReturnsPreferences: [{ id: 1 }], notCallable: 4,
81
+ method(value) { effects.push('this:' + this.value + ':' + value); return result; },
82
+ nested: { value: 5, method(value) { effects.push('nested:' + this.value + ':' + value); return result; } },
83
+ get getter() { effects.push('getter'); return this.method; },
84
+ get throwing() { effects.push('throwing'); throw new RangeError('expected'); },
85
+ Ctor: class { constructor(value) { this.value = value; } },
86
+ returnsNull() { effects.push('null'); return null; }
87
+ };
88
+ class Base { read(value) { effects.push('super'); return value; } }
89
+ class Derived extends Base {
90
+ #get(value) { effects.push('private'); return value; }
91
+ read(value) { return super.read?.(value)?.method(this.#get?.(7)); }
92
+ }
93
+ const root = kind === 'null' ? null : kind === 'undefined' ? undefined : object;
94
+ const fn = kind === 'null' ? null : kind === 'undefined' ? undefined : value => { effects.push('fn:' + value); return result; };
95
+ const spread = { *[Symbol.iterator]() { effects.push('spread'); yield 7; } };
96
+ try {
97
+ const value = ${expression};
98
+ return JSON.stringify({ value, effects });
99
+ } catch (error) { return JSON.stringify({ error: error.name, effects }); }
100
+ };`;
101
+ }
102
+
103
+ function load(code, filename) {
104
+ const module = { exports: {} };
105
+ vm.runInNewContext(code, { module, exports: module.exports,
106
+ __trace: global.__trace, __repro_call: global.__repro_call }, { filename });
107
+ return module.exports;
108
+ }
109
+
110
+ async function main() {
111
+ installCJS({ include: [/optional-chain-virtual-/] });
112
+ let count = 0;
113
+ for (const expression of cases) {
114
+ const filename = `/test/optional-chain-${count}.js`;
115
+ const code = source(expression);
116
+ const transformed = babel.transformSync(code, { filename, configFile: false, babelrc: false,
117
+ plugins: [makeWrap(filename)] }).code;
118
+ const original = load(code, filename);
119
+ const variants = [['babel', load(transformed, filename), original]];
120
+ for (const target of [ts.ScriptTarget.ES2019, ts.ScriptTarget.ES2022]) {
121
+ const typescript = code.replace('run(kind)', 'run(kind: string)').replace('const effects = []', 'const effects: string[] = []');
122
+ const compiled = ts.transpileModule(typescript, { compilerOptions: { target, module: ts.ModuleKind.CommonJS } }).outputText;
123
+ const moduleFilename = path.join(__dirname, `fixtures/optional-chain-virtual-${count}-${target}.cjs`);
124
+ const mod = new Module(moduleFilename, module);
125
+ mod.filename = moduleFilename;
126
+ mod.paths = module.paths;
127
+ mod._compile(compiled, moduleFilename);
128
+ assert.equal(mod.exports.__repro_instrumented, true, 'CJS hook must actually instrument, not silently fall back');
129
+ // Compare to the code handed to the hook, including the TS compiler's
130
+ // own downleveling semantics, not to a different compilation target.
131
+ variants.push([`CJS/TS-${target}`, mod.exports, load(compiled, moduleFilename)]);
132
+ }
133
+ for (const [variant, instrumented, baseline] of variants) {
134
+ for (const kind of ['null', 'undefined', 'present']) {
135
+ const expected = await baseline(kind);
136
+ assert.equal(await instrumented(kind), expected, `${expression} (${variant}, ${kind}, idle)`);
137
+ assert.equal(await trace.withTrace(`optional-${count}`, () => instrumented(kind)), expected,
138
+ `${expression} (${variant}, ${kind}, recording)`);
139
+ count += 2;
140
+ }
141
+ }
142
+ }
143
+ console.log(`optional chaining: ${count} native/instrumented semantic comparisons passed`);
144
+ }
145
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,24 @@
1
+ const assert = require('node:assert/strict');
2
+ const path = require('node:path');
3
+ const { GenericContainer, Wait } = require('testcontainers');
4
+
5
+ async function main() {
6
+ for (const image of ['node:18-bookworm-slim', 'node:20-alpine', 'node:22.22.0-bookworm-slim']) {
7
+ const container = await new GenericContainer(image)
8
+ .withBindMounts([{ source: path.resolve(__dirname, '..'), target: '/sdk', mode: 'ro' }])
9
+ .withWorkingDir('/sdk')
10
+ .withCommand(['node', '-e', 'console.log("ready"); setInterval(() => {}, 60000)'])
11
+ .withWaitStrategy(Wait.forLogMessage('ready')).start();
12
+ try {
13
+ for (const command of [
14
+ ['node', 'test/optional-chain-semantics.test.js'],
15
+ ['node', '--require', './tracer/runtime.js', '--experimental-loader', './tracer/esm-loader.mjs', 'test/fixtures/optional-chain-esm.mjs'],
16
+ ]) {
17
+ const result = await container.exec(command);
18
+ assert.equal(result.exitCode, 0, `${image}: ${result.output}`);
19
+ console.log(`${image}: ${result.output.trim()}`);
20
+ }
21
+ } finally { await container.stop(); }
22
+ }
23
+ }
24
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -10,6 +10,7 @@ const originalFetch = global.fetch;
10
10
  const capturedBodies = [];
11
11
 
12
12
  global.fetch = async (url, init = {}) => {
13
+ if (String(url).endsWith('/v1/sdk/runtime-services')) return { ok: true };
13
14
  assert.match(String(url), /\/v1\/ingest\/events$/);
14
15
  capturedBodies.push(JSON.parse(String(init.body || '{}')));
15
16
  return {
@@ -59,6 +60,9 @@ async function main() {
59
60
  appSecret: 'secret',
60
61
  appName: 'semantic-trace',
61
62
  serviceName: 'semantic-trace',
63
+ // Exercise the built-in fallback without depending on a hosted policy.
64
+ privacy: { enabled: false },
65
+ apiBase: 'http://127.0.0.1:65535',
62
66
  ingestBase: 'http://127.0.0.1:65535',
63
67
  tracing: {
64
68
  disableFunctionTypes: ['constructor'],
@@ -126,7 +126,6 @@ function installCJS({ include, exclude, parserPlugins } = {}) {
126
126
  mapOriginalPosition = sourceInfo.mapOriginalPosition;
127
127
  }
128
128
  if (shouldHandle(filename, metaFilename)) {
129
- try { process.stderr.write(`[trace-debug] compile: ${filename}\n`); } catch {}
130
129
  // Transform the already-compiled JS (keeps Nest’s decorator metadata intact).
131
130
  const res = babel.transformSync(code, {
132
131
  filename,
@@ -161,7 +160,6 @@ function installCJS({ include, exclude, parserPlugins } = {}) {
161
160
  }
162
161
  } catch (err) {
163
162
  out = code; // never break the app if transform fails
164
- try { process.stderr.write(`[trace-debug] compile error: ${filename} :: ${err?.message || err}\n`); } catch {}
165
163
  }
166
164
 
167
165
  const ret = origCompile.call(this, out, filename);
@@ -1,4 +1,5 @@
1
1
  // wrap-plugin.js
2
+ const transformOptionalChaining = require('@babel/plugin-transform-optional-chaining').default;
2
3
  module.exports = function makeWrapPlugin(filenameForMeta, opts = {}) {
3
4
  return ({ types: t }) => {
4
5
  const {
@@ -453,10 +454,13 @@ module.exports = function makeWrapPlugin(filenameForMeta, opts = {}) {
453
454
  if (n.__repro_call_wrapped) return;
454
455
  if (n.__repro_internal) return;
455
456
 
456
- // Skip our helper, super(), import(), optional calls for now
457
+ // Preserve special call semantics and avoid instrumenting our helpers.
457
458
  if (t.isIdentifier(n.callee, { name: '__repro_call' })) return;
458
459
  if (t.isIdentifier(n.callee, { name: '__awaiter' })) return;
459
460
  if (t.isIdentifier(n.callee, { name: '__generator' })) return;
461
+ // Routing direct eval through a helper turns it into indirect eval
462
+ // and changes its lexical scope (including TS-downleveled chains).
463
+ if (t.isIdentifier(n.callee, { name: 'eval' })) return;
460
464
  if (t.isSuper(n.callee)) return;
461
465
  if (isSuperMemberCall(n.callee)) return;
462
466
  if (t.isImport(n.callee)) return;
@@ -558,127 +562,12 @@ module.exports = function makeWrapPlugin(filenameForMeta, opts = {}) {
558
562
  path.node.__repro_call_wrapped = true;
559
563
  }
560
564
 
561
- function wrapOptionalCall(path, state) {
562
- const { node: n } = path;
563
- if (n.__repro_call_wrapped) return;
564
- if (n.__repro_internal) return;
565
-
566
- if (t.isIdentifier(n.callee, { name: '__repro_call' })) return;
567
- if (t.isIdentifier(n.callee, { name: '__awaiter' })) return;
568
- if (t.isIdentifier(n.callee, { name: '__generator' })) return;
569
- if (t.isSuper(n.callee)) return;
570
- if (isSuperMemberCall(n.callee)) return;
571
- if (t.isImport(n.callee)) return;
572
-
573
- const loc = n.loc?.start || null;
574
- const mapped = loc && typeof mapOriginalPosition === 'function'
575
- ? mapOriginalPosition(loc.line ?? null, loc.column ?? 0)
576
- : null;
577
-
578
- const file = mapped?.file || filenameForMeta || state.file.opts.filename || '';
579
- const line = mapped?.line ?? loc?.line ?? 0;
580
-
581
- const fileLit = t.stringLiteral(file ?? '');
582
- const lineLit = t.numericLiteral(line ?? 0);
583
-
584
- const unawaited = !isAwaitedCall(path);
585
- const label = describeExpression(n.callee) || '';
586
- const makeArgsArray = () => t.arrayExpression(n.arguments.map(arg => t.cloneNode(arg, true)));
587
-
588
- const isOptMember = (t.isOptionalMemberExpression && t.isOptionalMemberExpression(n.callee)) || false;
589
- if (t.isMemberExpression(n.callee) || isOptMember) {
590
- const callee = n.callee;
591
- const objOrig = callee.object;
592
- const prop = callee.property;
593
- const computed = callee.computed === true;
594
-
595
- const needsObjGuard = Boolean(callee.optional);
596
- const needsCalleeGuard = n.optional === true;
597
-
598
- const objId = path.scope.generateUidIdentifierBasedOnNode(objOrig, 'obj');
599
- const fnId = path.scope.generateUidIdentifier('fn');
600
-
601
- const member = t.memberExpression(objId, prop, computed);
602
- const fnInit = needsObjGuard
603
- ? t.conditionalExpression(
604
- t.binaryExpression('==', objId, t.nullLiteral()),
605
- t.identifier('undefined'),
606
- member
607
- )
608
- : member;
609
-
610
- const tests = [];
611
- if (needsObjGuard) tests.push(t.binaryExpression('==', objId, t.nullLiteral()));
612
- if (needsCalleeGuard) tests.push(t.binaryExpression('==', fnId, t.nullLiteral()));
613
-
614
- let guardTest = null;
615
- if (tests.length === 1) guardTest = tests[0];
616
- else if (tests.length === 2) guardTest = t.logicalExpression('||', tests[0], tests[1]);
617
-
618
- const reproCall = t.callExpression(
619
- t.identifier('__repro_call'),
620
- [
621
- fnId,
622
- objId,
623
- makeArgsArray(),
624
- fileLit,
625
- lineLit,
626
- t.stringLiteral(label || ''),
627
- t.booleanLiteral(unawaited)
628
- ]
629
- );
630
-
631
- const guardedCall = guardTest
632
- ? t.conditionalExpression(guardTest, t.identifier('undefined'), reproCall)
633
- : reproCall;
634
-
635
- const seq = t.sequenceExpression([
636
- t.assignmentExpression('=', objId, objOrig),
637
- t.assignmentExpression('=', fnId, fnInit),
638
- guardedCall
639
- ]);
640
-
641
- path.scope.push({ id: objId });
642
- path.scope.push({ id: fnId });
643
- path.replaceWith(seq);
644
- path.node.__repro_call_wrapped = true;
645
- return;
646
- }
647
-
648
- const fnOrig = n.callee;
649
- const fnId = path.scope.generateUidIdentifier('fn');
650
- const needsCalleeGuard = n.optional === true;
651
- const guardTest = needsCalleeGuard ? t.binaryExpression('==', fnId, t.nullLiteral()) : null;
652
-
653
- const reproCall = t.callExpression(
654
- t.identifier('__repro_call'),
655
- [
656
- fnId,
657
- t.nullLiteral(),
658
- makeArgsArray(),
659
- fileLit,
660
- lineLit,
661
- t.stringLiteral(label || ''),
662
- t.booleanLiteral(unawaited)
663
- ]
664
- );
665
-
666
- const guardedCall = guardTest
667
- ? t.conditionalExpression(guardTest, t.identifier('undefined'), reproCall)
668
- : reproCall;
669
-
670
- const seq = t.sequenceExpression([
671
- t.assignmentExpression('=', fnId, fnOrig),
672
- guardedCall
673
- ]);
674
-
675
- path.scope.push({ id: fnId });
676
- path.replaceWith(seq);
677
- path.node.__repro_call_wrapped = true;
678
- }
679
565
 
680
566
  return {
681
567
  name: 'omnitrace-wrap-functions-and-calls',
568
+ // Lower complete chains before wrapping their calls; guarding only
569
+ // the immediate receiver loses short-circuiting across chain segments.
570
+ inherits: transformOptionalChaining,
682
571
  visitor: {
683
572
  // function body enter/exit
684
573
  FunctionDeclaration: wrap,
@@ -692,11 +581,6 @@ module.exports = function makeWrapPlugin(filenameForMeta, opts = {}) {
692
581
  CallExpression: {
693
582
  exit(path, state) { wrapCall(path, state); }
694
583
  },
695
- OptionalCallExpression: {
696
- exit(path, state) { wrapOptionalCall(path, state); }
697
- },
698
- // (If you also want to wrap OptionalCallExpression in older Babel ASTs,
699
- // add the same handler here)
700
584
  }
701
585
  };
702
586
  };