@reproapp/node-sdk 0.0.17 → 0.0.19

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.17",
3
+ "version": "0.0.19",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +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/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/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.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/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"
16
17
  },
17
18
  "peerDependencies": {
18
19
  "express": "^5.1.0",
@@ -0,0 +1,68 @@
1
+ const assert = require('assert');
2
+
3
+ const { trace, SYM_BODY_TRACED } = require('../tracer/runtime');
4
+
5
+ function assertSyncThrowsOnceWithNoActiveTrace() {
6
+ let calls = 0;
7
+ function throwsOnce() {
8
+ calls += 1;
9
+ throw new Error('app failure');
10
+ }
11
+
12
+ assert.throws(
13
+ () => global.__repro_call(throwsOnce, null, [], __filename, 10, 'throwsOnce', false),
14
+ /app failure/,
15
+ );
16
+ assert.strictEqual(calls, 1);
17
+ }
18
+
19
+ function assertSyncThrowsOnceWithActiveTrace() {
20
+ let calls = 0;
21
+ function throwsOnce() {
22
+ calls += 1;
23
+ throw new Error('traced app failure');
24
+ }
25
+
26
+ assert.throws(
27
+ () => trace.withTrace('runtime-no-double-invoke', () => (
28
+ global.__repro_call(throwsOnce, null, [], __filename, 25, 'throwsOnce', false)
29
+ )),
30
+ /traced app failure/,
31
+ );
32
+ assert.strictEqual(calls, 1);
33
+ }
34
+
35
+ function assertBodyTracedThrowsOnceWithActiveTrace() {
36
+ let calls = 0;
37
+ function bodyTracedThrowsOnce() {
38
+ calls += 1;
39
+ throw new Error('body traced app failure');
40
+ }
41
+ Object.defineProperty(bodyTracedThrowsOnce, SYM_BODY_TRACED, {
42
+ value: true,
43
+ configurable: true,
44
+ });
45
+
46
+ assert.throws(
47
+ () => trace.withTrace('runtime-no-double-invoke-body', () => (
48
+ global.__repro_call(bodyTracedThrowsOnce, null, [], __filename, 45, 'bodyTracedThrowsOnce', false)
49
+ )),
50
+ /body traced app failure/,
51
+ );
52
+ assert.strictEqual(calls, 1);
53
+ }
54
+
55
+ async function main() {
56
+ assertSyncThrowsOnceWithNoActiveTrace();
57
+ assertSyncThrowsOnceWithActiveTrace();
58
+ assertBodyTracedThrowsOnceWithActiveTrace();
59
+
60
+ // eslint-disable-next-line no-console
61
+ console.log('runtime no double invoke OK');
62
+ }
63
+
64
+ main().catch((error) => {
65
+ // eslint-disable-next-line no-console
66
+ console.error(error);
67
+ process.exitCode = 1;
68
+ });
@@ -0,0 +1,70 @@
1
+ const assert = require('assert');
2
+ const mongoose = require('mongoose');
3
+ const { GenericContainer, Wait } = require('testcontainers');
4
+
5
+ const { trace } = require('../tracer/runtime');
6
+
7
+ async function startMongo() {
8
+ return new GenericContainer('mongo:7')
9
+ .withExposedPorts(27017)
10
+ .withTmpFs({ '/data/db': 'rw,noexec,nosuid,size=1g' })
11
+ .withCommand(['mongod', '--bind_ip_all', '--setParameter', 'diagnosticDataCollectionEnabled=false'])
12
+ .withWaitStrategy(Wait.forListeningPorts())
13
+ .start();
14
+ }
15
+
16
+ async function main() {
17
+ let container;
18
+ let connection;
19
+
20
+ try {
21
+ container = await startMongo();
22
+ const uri = `mongodb://${container.getHost()}:${container.getMappedPort(27017)}/repro_node_sdk_test`;
23
+ connection = await mongoose.createConnection(uri).asPromise();
24
+
25
+ const SideEffect = connection.model('SideEffect', new mongoose.Schema({
26
+ name: { type: String, required: true },
27
+ }));
28
+
29
+ let calls = 0;
30
+ async function writeThenThrow(name) {
31
+ calls += 1;
32
+ await SideEffect.create({ name });
33
+ throw new Error('intentional app failure');
34
+ }
35
+
36
+ await assert.rejects(
37
+ () => trace.withTrace('runtime-no-double-invoke-container', () => (
38
+ global.__repro_call(
39
+ writeThenThrow,
40
+ null,
41
+ ['single-write'],
42
+ __filename,
43
+ 35,
44
+ 'writeThenThrow',
45
+ false,
46
+ )
47
+ )),
48
+ /intentional app failure/,
49
+ );
50
+
51
+ assert.strictEqual(calls, 1);
52
+ assert.strictEqual(await SideEffect.countDocuments({ name: 'single-write' }), 1);
53
+
54
+ // eslint-disable-next-line no-console
55
+ console.log('runtime no double invoke testcontainers OK');
56
+ } finally {
57
+ if (connection) {
58
+ await connection.close().catch(() => {});
59
+ }
60
+ if (container) {
61
+ await container.stop().catch(() => {});
62
+ }
63
+ }
64
+ }
65
+
66
+ main().catch((error) => {
67
+ // eslint-disable-next-line no-console
68
+ console.error(error);
69
+ process.exitCode = 1;
70
+ });
@@ -0,0 +1,80 @@
1
+ const assert = require('assert');
2
+ const vm = require('vm');
3
+ const babel = require('@babel/core');
4
+ const makeWrapPlugin = require('../tracer/wrap-plugin');
5
+ require('../tracer/runtime');
6
+
7
+ function transform(source, filename) {
8
+ return babel.transformSync(source, {
9
+ filename,
10
+ plugins: [makeWrapPlugin(filename, {})],
11
+ parserOpts: { sourceType: 'script', plugins: ['typescript'] },
12
+ generatorOpts: { retainLines: true },
13
+ }).code;
14
+ }
15
+
16
+ function loadInstrumentedModule(source, filename) {
17
+ const code = transform(source, filename);
18
+ const module = { exports: {} };
19
+ const context = {
20
+ module,
21
+ exports: module.exports,
22
+ require,
23
+ console,
24
+ __trace: global.__trace,
25
+ __repro_call: global.__repro_call,
26
+ Promise,
27
+ Buffer,
28
+ setTimeout,
29
+ clearTimeout,
30
+ setImmediate,
31
+ clearImmediate,
32
+ };
33
+
34
+ vm.runInNewContext(code, context, { filename });
35
+ return { exports: module.exports, code };
36
+ }
37
+
38
+ function toPlainJson(value) {
39
+ return JSON.parse(JSON.stringify(value));
40
+ }
41
+
42
+ async function main() {
43
+ const filename = '/tmp/wrap-plugin-super-call.fixture.js';
44
+ const { exports: mod, code } = loadInstrumentedModule(`
45
+ class BaseNotificationGenerator {
46
+ async resolveNotificationConfig(params) {
47
+ return { source: 'base', params };
48
+ }
49
+ }
50
+
51
+ class SubjectNotificationGenerator extends BaseNotificationGenerator {
52
+ async resolveNotificationConfig(params, preloaded) {
53
+ return preloaded
54
+ ? Promise.resolve(preloaded)
55
+ : super.resolveNotificationConfig(params);
56
+ }
57
+ }
58
+
59
+ module.exports = { SubjectNotificationGenerator };
60
+ `, filename);
61
+
62
+ assert(!code.includes('= super'), `transformed code must not assign super into a temp:\n${code}`);
63
+
64
+ const generator = new mod.SubjectNotificationGenerator();
65
+ assert.deepStrictEqual(
66
+ toPlainJson(await generator.resolveNotificationConfig({ id: 'S_1' }, null)),
67
+ { source: 'base', params: { id: 'S_1' } },
68
+ );
69
+ assert.deepStrictEqual(
70
+ toPlainJson(await generator.resolveNotificationConfig({ id: 'S_2' }, { source: 'preloaded' })),
71
+ { source: 'preloaded' },
72
+ );
73
+
74
+ console.log('wrap-plugin super call OK');
75
+ }
76
+
77
+ main().catch((error) => {
78
+ console.error(error);
79
+ process.exitCode = 1;
80
+ });
@@ -0,0 +1,116 @@
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
+ require('../tracer/runtime');
9
+
10
+ function transform(source, filename) {
11
+ return babel.transformSync(source, {
12
+ filename,
13
+ plugins: [makeWrapPlugin(filename, {})],
14
+ parserOpts: { sourceType: 'script', plugins: ['typescript'] },
15
+ generatorOpts: { retainLines: true },
16
+ }).code;
17
+ }
18
+
19
+ function loadInstrumentedModule(source, filename) {
20
+ const code = transform(source, filename);
21
+ const module = { exports: {} };
22
+ const context = {
23
+ module,
24
+ exports: module.exports,
25
+ require,
26
+ console,
27
+ __trace: global.__trace,
28
+ __repro_call: global.__repro_call,
29
+ Promise,
30
+ Buffer,
31
+ setTimeout,
32
+ clearTimeout,
33
+ setImmediate,
34
+ clearImmediate,
35
+ };
36
+
37
+ vm.runInNewContext(code, context, { filename });
38
+ return module.exports;
39
+ }
40
+
41
+ function toPlainJson(value) {
42
+ return JSON.parse(JSON.stringify(value));
43
+ }
44
+
45
+ async function startMongo() {
46
+ return new GenericContainer('mongo:7')
47
+ .withExposedPorts(27017)
48
+ .withTmpFs({ '/data/db': 'rw,noexec,nosuid,size=1g' })
49
+ .withCommand(['mongod', '--bind_ip_all', '--setParameter', 'diagnosticDataCollectionEnabled=false'])
50
+ .withWaitStrategy(Wait.forListeningPorts())
51
+ .start();
52
+ }
53
+
54
+ async function main() {
55
+ let container;
56
+ let connection;
57
+
58
+ try {
59
+ container = await startMongo();
60
+ const uri = `mongodb://${container.getHost()}:${container.getMappedPort(27017)}/repro_node_sdk_super_test`;
61
+ connection = await mongoose.createConnection(uri).asPromise();
62
+ const Notification = connection.model('Notification', new mongoose.Schema({
63
+ name: { type: String, required: true },
64
+ }));
65
+
66
+ const { SubjectNotificationGenerator } = loadInstrumentedModule(`
67
+ class BaseNotificationGenerator {
68
+ constructor(model) {
69
+ this.model = model;
70
+ }
71
+
72
+ async resolveNotificationConfig(params) {
73
+ await this.model.create({ name: params.name });
74
+ return { source: 'base', name: params.name };
75
+ }
76
+ }
77
+
78
+ class SubjectNotificationGenerator extends BaseNotificationGenerator {
79
+ async resolveNotificationConfig(params, preloaded) {
80
+ return preloaded
81
+ ? Promise.resolve(preloaded)
82
+ : super.resolveNotificationConfig(params);
83
+ }
84
+ }
85
+
86
+ module.exports = { SubjectNotificationGenerator };
87
+ `, '/tmp/wrap-plugin-super-call-container.fixture.js');
88
+
89
+ const generator = new SubjectNotificationGenerator(Notification);
90
+ assert.deepStrictEqual(
91
+ toPlainJson(await generator.resolveNotificationConfig({ name: 'created-through-super' }, null)),
92
+ { source: 'base', name: 'created-through-super' },
93
+ );
94
+ assert.deepStrictEqual(
95
+ toPlainJson(await generator.resolveNotificationConfig({ name: 'not-created' }, { source: 'preloaded' })),
96
+ { source: 'preloaded' },
97
+ );
98
+
99
+ assert.strictEqual(await Notification.countDocuments({ name: 'created-through-super' }), 1);
100
+ assert.strictEqual(await Notification.countDocuments({ name: 'not-created' }), 0);
101
+
102
+ console.log('wrap-plugin super call testcontainers OK');
103
+ } finally {
104
+ if (connection) {
105
+ await connection.close().catch(() => {});
106
+ }
107
+ if (container) {
108
+ await container.stop().catch(() => {});
109
+ }
110
+ }
111
+ }
112
+
113
+ main().catch((error) => {
114
+ console.error(error);
115
+ process.exitCode = 1;
116
+ });
package/tracer/runtime.js CHANGED
@@ -505,9 +505,14 @@ function forkAlsStoreForUnawaited(baseStore) {
505
505
  if (!global.__repro_call) {
506
506
  Object.defineProperty(global, '__repro_call', {
507
507
  value: function __repro_call(fn, thisArg, args, callFile, callLine, label, isUnawaitedCall) {
508
+ let didInvokeTarget = false;
509
+ const invokeTarget = (targetThis, targetArgs) => {
510
+ didInvokeTarget = true;
511
+ return fn.apply(targetThis, targetArgs);
512
+ };
508
513
  try {
509
514
  if (typeof fn !== 'function' || fn[SYM_SKIP_WRAP]) {
510
- return fn.apply(thisArg, args);
515
+ return invokeTarget(thisArg, args);
511
516
  }
512
517
 
513
518
  const currentStore = als.getStore();
@@ -516,13 +521,9 @@ if (!global.__repro_call) {
516
521
  // If no tracing context is active, bail out quickly to avoid touching app semantics
517
522
  // during module initialization or other untracked code paths.
518
523
  if (!currentStore) {
519
- try {
520
- const out = fn.apply(thisArg, args);
521
- if (isUnawaitedCall && isThenable(out)) markPromiseUnawaited(out);
522
- return out;
523
- } catch {
524
- return fn ? fn.apply(thisArg, args) : undefined;
525
- }
524
+ const out = invokeTarget(thisArg, args);
525
+ if (isUnawaitedCall && isThenable(out)) markPromiseUnawaited(out);
526
+ return out;
526
527
  }
527
528
 
528
529
  const sourceFile = fn[SYM_SRC_FILE];
@@ -643,7 +644,7 @@ if (!global.__repro_call) {
643
644
  const shouldWrap = !isAsyncLocalContextSetter(fn, thisArg);
644
645
  const argsForCall = shouldWrap ? wrapArgsWithStore(args, snapshotForArgs) : args;
645
646
  try {
646
- const out = fn.apply(thisArg, argsForCall);
647
+ const out = invokeTarget(thisArg, argsForCall);
647
648
 
648
649
  const isThenableValue = isThenable(out);
649
650
  const isQuery = isMongooseQuery(out);
@@ -755,10 +756,10 @@ if (!global.__repro_call) {
755
756
  let out;
756
757
  if (isolatedStore) {
757
758
  als.run(isolatedStore, () => {
758
- out = fn.apply(thisArg, argsForCall);
759
+ out = invokeTarget(thisArg, argsForCall);
759
760
  });
760
761
  } else {
761
- out = fn.apply(thisArg, argsForCall);
762
+ out = invokeTarget(thisArg, argsForCall);
762
763
  }
763
764
  if (isUnawaitedCall && isThenable(out)) markPromiseUnawaited(out);
764
765
  return out;
@@ -775,7 +776,10 @@ if (!global.__repro_call) {
775
776
  }
776
777
  return invokeWithTrace(forcedParentSpanId, callStoreForArgs);
777
778
  });
778
- } catch {
779
+ } catch (error) {
780
+ if (didInvokeTarget) {
781
+ throw error;
782
+ }
779
783
  return fn ? fn.apply(thisArg, args) : undefined;
780
784
  }
781
785
  },
@@ -49,6 +49,13 @@ module.exports = function makeWrapPlugin(filenameForMeta, opts = {}) {
49
49
  return '';
50
50
  }
51
51
 
52
+ function isSuperMemberCall(callee) {
53
+ if (!callee) return false;
54
+ if (t.isMemberExpression(callee) && t.isSuper(callee.object)) return true;
55
+ if (t.isOptionalMemberExpression && t.isOptionalMemberExpression(callee) && t.isSuper(callee.object)) return true;
56
+ return false;
57
+ }
58
+
52
59
  const obj = kv => t.objectExpression(
53
60
  Object.entries(kv)
54
61
  .filter(([, v]) => v != null)
@@ -451,6 +458,7 @@ module.exports = function makeWrapPlugin(filenameForMeta, opts = {}) {
451
458
  if (t.isIdentifier(n.callee, { name: '__awaiter' })) return;
452
459
  if (t.isIdentifier(n.callee, { name: '__generator' })) return;
453
460
  if (t.isSuper(n.callee)) return;
461
+ if (isSuperMemberCall(n.callee)) return;
454
462
  if (t.isImport(n.callee)) return;
455
463
 
456
464
  if (t.isMemberExpression(n.callee) && t.isIdentifier(n.callee.object, { name: '__trace' })) {
@@ -559,6 +567,7 @@ module.exports = function makeWrapPlugin(filenameForMeta, opts = {}) {
559
567
  if (t.isIdentifier(n.callee, { name: '__awaiter' })) return;
560
568
  if (t.isIdentifier(n.callee, { name: '__generator' })) return;
561
569
  if (t.isSuper(n.callee)) return;
570
+ if (isSuperMemberCall(n.callee)) return;
562
571
  if (t.isImport(n.callee)) return;
563
572
 
564
573
  const loc = n.loc?.start || null;