Repository: instana/nodejs
Pull request #1602: test: added allow root exit span apps for bull and amqp
Author: login: abhilash-sivan
Created: 2025-03-03T11:02:36Z | Merged: 2025-03-04T18:08:18Z

This PR adds rootExitSpan tests for bull and amqp.

As per the current logic, `rootExitSpanApp.js` or by default `app.js` files were running in ESM test (logic in ProcessControls) as a replacement.

We are getting the flaky tests now because of the newly added ESM test for bull. As the bull test ran, the amqp rootExitSpanApp test was also running parallel causing some timing issues.

This weird issue was happening in the first place due to a combination of factors:

- the dynamic importing structure of ESM and 
- synchronous module loading of the commonjs app and 
- also the asynchronous behavior of message queues

So currently adding this ESM tests fixes this issue.


Diff:
diff --git a/packages/collector/test/tracing/messaging/amqp/allowRootExitSpanApp.mjs b/packages/collector/test/tracing/messaging/amqp/allowRootExitSpanApp.mjs
new file mode 100644
index 0000000000..b3efdcd30a
--- /dev/null
+++ b/packages/collector/test/tracing/messaging/amqp/allowRootExitSpanApp.mjs
@@ -0,0 +1,45 @@
+/*
+ * (c) Copyright IBM Corp. 2025
+ */
+
+'use strict';
+
+// NOTE: c8 bug https://github.com/bcoe/c8/issues/166
+process.on('SIGTERM', () => {
+  process.disconnect();
+  process.exit(0);
+});
+
+import delay from '../../../../../core/test/test_util/delay.js';
+import amqp from 'amqplib';
+import { exchange } from './amqpUtil.js';
+
+const logPrefix = `amqp Allow Root Exit Span App (${process.pid}):\t`;
+let connection;
+
+(async function run() {
+  // wait til Instana is ready
+  await delay(1000);
+
+  amqp
+    .connect(process.env.AMQP)
+    .then(_connection => {
+      connection = _connection;
+      return connection.createChannel();
+    })
+    .then(channel => {
+      log('amqp connection established');
+      channel.publish(exchange, '', Buffer.from('welcome'));
+      log('Sent message');
+      channel.close();
+      connection.close();
+    })
+    .catch(log);
+})();
+
+function log() {
+  /* eslint-disable no-console */
+  const args = Array.prototype.slice.call(arguments);
+  args[0] = logPrefix + args[0];
+  console.log.apply(console, args);
+}
diff --git a/packages/collector/test/tracing/messaging/amqp/test.js b/packages/collector/test/tracing/messaging/amqp/test.js
index 85879dd0a3..9c4075382e 100644
--- a/packages/collector/test/tracing/messaging/amqp/test.js
+++ b/packages/collector/test/tracing/messaging/amqp/test.js
@@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
+            env: {
+              INSTANA_ALLOW_ROOT_EXIT_SPAN: 1
+            }
           });
 
           await controls.start(null, null, true);
diff --git a/packages/collector/test/tracing/messaging/bull/allowRootExitSpanApp.mjs b/packages/collector/test/tracing/messaging/bull/allowRootExitSpanApp.mjs
new file mode 100644
index 0000000000..e5f24695cc
--- /dev/null
+++ b/packages/collector/test/tracing/messaging/bull/allowRootExitSpanApp.mjs
@@ -0,0 +1,37 @@
+/*
+ * (c) Copyright IBM Corp. 2025
+ */
+
+'use strict';
+
+// NOTE: c8 bug https://github.com/bcoe/c8/issues/166
+process.on('SIGTERM', () => {
+  process.disconnect();
+  process.exit(0);
+});
+
+import delay from '../../../../../core/test/test_util/delay.js';
+import Queue from 'bull';
+
+const logPrefix = `Bull Allow Root Exit Span App (${process.pid}):\t`;
+const queueName = process.env.BULL_QUEUE_NAME;
+const redisServer = process.env.REDIS_SERVER;
+const bullJobName = process.env.BULL_JOB_NAME;
+
+(async function run() {
+  // wait till Instana is ready
+  await delay(1000);
+
+  log(`Creating job queue ${queueName} with job name ${bullJobName} with redis server ${redisServer}`);
+  const sender = new Queue(queueName, redisServer);
+  sender.add(bullJobName, { foo: 'bar' });
+  log('Sent job to queue');
+  await sender.close();
+})();
+
+function log() {
+  /* eslint-disable no-console */
+  const args = Array.prototype.slice.call(arguments);
+  args[0] = logPrefix + args[0];
+  console.log.apply(console, args);
+}
diff --git a/packages/collector/test/tracing/messaging/bull/test.js b/packages/collector/test/tracing/messaging/bull/test.js
index 778f2f2f8e..6979d742bb 100644
--- a/packages/collector/test/tracing/messaging/bull/test.js
+++ b/packages/collector/test/tracing/messaging/bull/test.js
@@ -823,7 +823,8 @@ mochaSuiteFn('tracing/messaging/bull', function () {
         env: {
           REDIS_SERVER: `redis://${process.env.REDIS}`,
           BULL_QUEUE_NAME: queueName,
-          BULL_JOB_NAME: 'steve'
+          BULL_JOB_NAME: 'steve',
+          INSTANA_ALLOW_ROOT_EXIT_SPAN: 1
         }
       });
 


Reviews:
user: login: aryamohanan
body: Could you direct me to the fix for the flaky ESM tests? This change appears to be introducing new ESM test cases, but I couldn't find a fix for the existing issue.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T06:23:06Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T06:52:08Z
user: login: abhilash-sivan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:05:19Z
user: login: abhilash-sivan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:09:34Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:18:13Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:18:25Z
user: login: kirrg001
body: See comment 👍 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:18:43Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:19:47Z
user: login: abhilash-sivan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:21:36Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:24:32Z
user: login: abhilash-sivan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:28:45Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:31:48Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:36:44Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:37:07Z
user: login: abhilash-sivan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:44:47Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:48:05Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:49:35Z
user: login: abhilash-sivan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T09:03:58Z
user: login: kirrg001
body: I reconsidered. The fix for skipping a describe for mjs is too hard.
See https://github.com/instana/nodejs/pull/1614
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T18:01:37Z
user: login: kirrg001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T18:02:31Z

Review comments:
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: kirrg001
body: qs: As far as I can see, for this test no ESM test was running. Because "allowRootExitSpanApp.mjs" did not exist. 

See ProcessControls
> if (opts?.appPath) {
          const updatedPath = opts.appPath.endsWith('.js')
            ? opts.appPath.replace(/\.js$/, '.mjs')
            : `${opts.appPath}.mjs`;
          if (fs.existsSync(updatedPath)) {
            opts.execArgv = resolveEsmLoader();
            opts.appPath = updatedPath;
          }
        }

Could you explain why this is the cause of the flaky behavior? I read the PR description, but I did not understand it. 
created_at: 2025-03-04T06:52:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: abhilash-sivan
body: Even though there is no _allowRootExitSpanApp.mjs_ is present, the file _allowRootExitSpanApp.js_ will run for ESM, this is not limited to allowRootExitSpanApp files but also for the app.js.

This is an existing case as we are only modifying **opts.execArgv  & opts.appPath** for executing tests as ESM. But if no proper files are present, the app runs in CJS mode. (We do not exit from process controls without starting the server as of now)

see the logic in main branch:
```
    if (process.env.RUN_ESM && !opts.execArgv) {
      const resolveEsmLoader = () =>
        isLatestEsmSupportedVersion(process.versions.node)
          ? [`--import=${path.join(__dirname, '..', '..', 'esm-register.mjs')}`]
          : [`--experimental-loader=${path.join(__dirname, '..', '..', 'esm-loader.mjs')}`];

      try {
        // custom appPath is provided, use that
        if (opts?.appPath) {
          const updatedPath = opts.appPath.endsWith('.js')
            ? opts.appPath.replace(/\.js$/, '.mjs')
            : `${opts.appPath}.mjs`;
          if (fs.existsSync(updatedPath)) {
            opts.execArgv = resolveEsmLoader();
            opts.appPath = updatedPath;
          }
        } else if (opts?.dirname) {
          const esmApp = fs.readdirSync(opts.dirname).find(f => f.endsWith('.mjs'));
          if (esmApp) {
            opts.execArgv = resolveEsmLoader();
            opts.appPath = path.join(opts.dirname, 'app.mjs');
          }
        }
      } catch (err) {
        // eslint-disable-next-line no-console
        console.log('Unable to load the target app.mjs', err);
      }
    }

    this.collectorUninitialized = opts.collectorUninitialized;
```


See an example [log](https://cloud.ibm.com/devops/pipelines/tekton/c2cd6a8d-ea5a-47b0-913e-cd172d63833f/runs/28f91470-fbf7-4d55-8ff1-f419a07fea4d/test-ci-collector-tracing-messaging-task/run-test-group?env_id=ibm:yp:eu-de):

> @instana/collector: [ProcessControls] start with port: 6695, agentPort: 3138, appPath: /artifacts/packages/collector/test/tracing/messaging/bull/receiver.mjs
> @instana/collector: amqp Allow Root Exit Span App (569):	amqp connection established
> @instana/collector: amqp Allow Root Exit Span App (569):	Sent message
> @instana/collector: Bull Receiver ESM (612):	Job named steve and concurrent
> @instana/collector: Bull Receiver ESM (612):	ESM Bull receiver app listening on port 6695
> @instana/collector: [ProcessControls] server is up.
> @instana/collector: [ProcessControls] started with port: 6695, agentPort: 3138, appPath: /artifacts/packages/collector/test/tracing/messaging/bull/receiver.mjs, pid: 612
> @instana/collector:         sendOption: default
> @instana/collector:           without error
> @instana/collector: send: default; receive: Promise; error: false -- UTC: 2025-03-03T09:20:43.034Z (1740993643034)
> @instana/collector: Bull Receiver ESM (612):	Consuming: promise: {"testId":"ec4b9c30-a835-4f86-8ece-b9dfcf4b39af","name":"Job with random number: 49747237.80468326","withError":false}
> @instana/collector: Bull Receiver ESM (612):	The follow up request after receiving a message has happened.
> @instana/collector:             ✓ send: default; receive: Promise; error: false (1023ms)
> @instana/collector:           with error
> @instana/collector: send: default; receive: Promise; error: true -- UTC: 2025-03-03T09:20:46.071Z (1740993646071)
> @instana/collector:             1) send: default; receive: Promise; error: true
> @instana/collector: ##### Test failed: AssertionError: expected 2 to equal 9
> @instana/collector: ##### Error stack: AssertionError: expected 2 to equal 9
> @instana/collector:     at /artifacts/packages/collector/test/tracing/messaging/bull/test.js:508:35
> @instana/collector:     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
> @instana/collector: npm error Lifecycle script `test:ci:tracing:messaging` failed with error:
> @instana/collector: npm error code 1
> @instana/collector: npm error path /artifacts/packages/collector
> @instana/collector: npm error workspace @instana/collector@4.6.2
> @instana/collector: npm error location /artifacts/packages/collector
> @instana/collector: npm error command failed

here 

> @instana/collector: amqp Allow Root Exit Span App (569): amqp connection established
> @instana/collector: amqp Allow Root Exit Span App (569): Sent message
> @instana/collector: Bull Receiver ESM (612): Job named steve and concurrent
> @instana/collector: Bull Receiver ESM (612): ESM Bull receiver app listening on port 6695

The amqp (CJS) is running (which should be limited actually) and BULL ESM is running

created_at: 2025-03-04T07:05:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: abhilash-sivan
body: So currently adding proper ESM files fixes the flaky behavior as everything will run in ESM (async behavior).

There could be a fix that we can think of in the ProcessControls logic but that could break lots of ESM tests as many tests might still be running app.js for ESM. (And the fix would be exiting processcontrols without starting server if no proper ESM files are present.)

But this is not in the scope of this PR and the said resolution would be a full refactoring of the current ESM running logic
created_at: 2025-03-04T07:09:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: kirrg001
body: But this is the problem. The allowRootExitSpanApp.js should not run in ESM.

The problem is in packages/collector/test/hooks.js.

We skip the target application if there is no .mjs version.

Please tweak hooks.js 👍 
created_at: 2025-03-04T07:18:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: kirrg001
body: >    if (process.env.RUN_ESM) {
      const files = fs.readdirSync(testFile.split('/').slice(0, -1).join('/'));
      const esmApp = files.find(f => f.indexOf('.mjs') !== -1);

      if (!esmApp) {
        this.skip();
        return;
      }
    }
created_at: 2025-03-04T07:18:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: kirrg001
body: Furthermore: please explain why we have the flakyness with this bug. I still don't understand it 🤔 

created_at: 2025-03-04T07:19:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: abhilash-sivan
body: This weird issue was happening in the first place due to a combination of factors:

    the dynamic importing structure of ESM and
    synchronous module loading of the commonjs app and
    also the asynchronous behavior of message queues


the span length assertion was failing due to the presence of unexpected spans
created_at: 2025-03-04T07:21:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: kirrg001
body: Usually unexpected spans happen if the test was written flaky and e.g. we do not cleanup correctly.
created_at: 2025-03-04T07:24:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: abhilash-sivan
body: Yes exactly! 
Here it was failing here because of the following:
The amqp (CJS) was running (synchronous module import) and BULL ESM (dynamic import) was running.
And at the time when bull esm test is checking the span length assertion, amqp (CJS) is still running and the clean up is not completed!
created_at: 2025-03-04T07:28:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: kirrg001
body: But this is a design problem of the test.
How would a mjs rootExitSpanApp solve this? The problem of parallismn still exists?
created_at: 2025-03-04T07:31:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,12 +71,18 @@ class ProcessControls {
           if (fs.existsSync(updatedPath)) {
             opts.execArgv = resolveEsmLoader();
             opts.appPath = updatedPath;
+          } else {
+            this.skip();
path: packages/collector/test/test_util/ProcessControls.js
user: login: kirrg001
body: Please use hooks.js. This is the wrong place.
Please do a PR for each problem. Thanks 👍 
created_at: 2025-03-04T07:36:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,12 +71,18 @@ class ProcessControls {
           if (fs.existsSync(updatedPath)) {
             opts.execArgv = resolveEsmLoader();
             opts.appPath = updatedPath;
+          } else {
+            this.skip();
path: packages/collector/test/test_util/ProcessControls.js
user: login: kirrg001
body: > This is the wrong place.

ProcessControls is initiated in `before`. Skipping here is too late.
created_at: 2025-03-04T07:37:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: abhilash-sivan
body: Yeah. It was running properly after adding the ESM file without the parallellism
created_at: 2025-03-04T07:44:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: kirrg001
body: Adding two new mjs apps is not a fix, its a workaround.
created_at: 2025-03-04T07:48:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,12 +71,18 @@ class ProcessControls {
           if (fs.existsSync(updatedPath)) {
             opts.execArgv = resolveEsmLoader();
             opts.appPath = updatedPath;
+          } else {
+            this.skip();
path: packages/collector/test/test_util/ProcessControls.js
user: login: kirrg001
body: If possible, a tiny test utility for checking the presence of .mjs files would be helpful.
created_at: 2025-03-04T07:49:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,12 +71,18 @@ class ProcessControls {
           if (fs.existsSync(updatedPath)) {
             opts.execArgv = resolveEsmLoader();
             opts.appPath = updatedPath;
+          } else {
+            this.skip();
path: packages/collector/test/test_util/ProcessControls.js
user: login: abhilash-sivan
body: sure

created_at: 2025-03-04T09:03:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
         before(async () => {
           controls = new ProcessControls({
             useGlobalAgent: true,
-            appPath: path.join(__dirname, 'allowRootExitSpanApp')
+            appPath: path.join(__dirname, 'allowRootExitSpanApp'),
path: packages/collector/test/tracing/messaging/amqp/test.js
user: login: kirrg001
body: I reconsidered. Its too hard to add a fix for that problem. We have to add the app.
created_at: 2025-03-04T18:02:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: abhilash-sivan
created_at: 2025-03-04T06:39:58Z
body: > Could you direct me to the fix for the flaky ESM tests? This change appears to be introducing new ESM test cases, but I couldn't find a fix for the existing issue.

Please see the description
author_association: CONTRIBUTOR
user: login: abhilash-sivan
created_at: 2025-03-04T07:17:06Z
body: Merge as:

test: added allow root exit span apps in bull and amqp for ESM
author_association: CONTRIBUTOR