@reproapp/node-sdk 0.0.19 → 0.0.20

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.19",
3
+ "version": "0.0.20",
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-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",
@@ -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
+ });
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 = !isAsyncLocalContextSetter(fn, thisArg);
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 = !isAsyncLocalContextSetter(fn, thisArg);
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;