diff --git a/package-lock.json b/package-lock.json index f43fe71308..54b4f6158f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,8 +103,7 @@ "semver": "7.5.4", "sinon": "15.2.0", "sinon-chai": "3.7.0", - "square-calc": "3.2.1", - "square-calc-v2": "npm:square-calc@2.4.0", + "square-calc": "3.3.1", "stealthy-require": "1.1.1", "typescript": "5.9.3", "underscore": "1.13.1", @@ -31195,17 +31194,11 @@ "dev": true }, "node_modules/square-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/square-calc/-/square-calc-3.2.1.tgz", - "integrity": "sha512-43TtHbgIdE5V9hPsIMRPXFcxQvP0QXZ2MjoI/xak3YiIJ4GMsdhFwXnShM3CCcIovaOd35KzF+xxQX0urACHqw==", - "dev": true - }, - "node_modules/square-calc-v2": { - "name": "square-calc", - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/square-calc/-/square-calc-2.4.0.tgz", - "integrity": "sha512-9ud3FeFngJmIpEsiep4lvNhw1NvKLtirowvaMomMeHjyzJckDRbSbLkFv3Jjae6j+ZtxTd2eJvO8izkdpGCDDA==", - "dev": true + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/square-calc/-/square-calc-3.3.1.tgz", + "integrity": "sha512-x329+gUanrlP77wqRv3ckVG/KP6MEg6xrPfr6tAeKLvxX+cv6tXbURgeV7CevzrsKpjV9FvG9FPljqBbED7DEw==", + "dev": true, + "license": "MIT" }, "node_modules/ssri": { "version": "12.0.0", diff --git a/package.json b/package.json index aa0f6f0b34..90329bc600 100644 --- a/package.json +++ b/package.json @@ -160,8 +160,7 @@ "semver": "7.5.4", "sinon": "15.2.0", "sinon-chai": "3.7.0", - "square-calc": "3.2.1", - "square-calc-v2": "npm:square-calc@2.4.0", + "square-calc": "3.3.1", "stealthy-require": "1.1.1", "typescript": "5.9.3", "underscore": "1.13.1", diff --git a/packages/collector/test/integration/misc/native_esm/app.js b/packages/collector/test/integration/misc/native_esm/app.js index 7b89fddee4..0ba6db94b5 100644 --- a/packages/collector/test/integration/misc/native_esm/app.js +++ b/packages/collector/test/integration/misc/native_esm/app.js @@ -10,8 +10,6 @@ process.on('SIGTERM', () => { process.exit(0); }); -const mock = require('@_local/core/test/test_util/mockRequire'); -mock('square-calc', 'square-calc-v2'); require('@instana/collector')(); const express = require('express'); const morgan = require('morgan'); diff --git a/packages/collector/test/tracing/misc/require-esm/app.js b/packages/collector/test/tracing/misc/require-esm/app.js new file mode 100644 index 0000000000..dfd1d2335d --- /dev/null +++ b/packages/collector/test/tracing/misc/require-esm/app.js @@ -0,0 +1,63 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +// NOTE: c8 bug https://github.com/bcoe/c8/issues/166 +process.on('SIGTERM', () => { + process.disconnect(); + process.exit(0); +}); + +const agentPort = process.env.INSTANA_AGENT_PORT; + +require('@instana/collector')(); + +// Load ESM-only dependency using Node.js native require(esm) support +// 'got' is pure ESM from v14+. Since require(esm) returns the full Module Namespace Object +// rather than an unwrapped default function, we must explicitly destructure `.default`. +const { default: got } = require('got'); + +const express = require('express'); +const port = require('@_local/collector/test/test_util/app-port')(); +const app = express(); +const logPrefix = `require(esm) test app (${process.pid}):\t`; + +function log() { + const args = Array.prototype.slice.call(arguments); + args[0] = `${logPrefix}${args[0]}`; + // eslint-disable-next-line no-console + console.log.apply(console, args); +} + +app.get('/', (req, res) => { + res.send('OK'); +}); + +app.get('/make-request', async (req, res) => { + const targetUrl = `http://127.0.0.1:${agentPort}/`; + + try { + const response = await got(targetUrl); + + log(`Request successful, status: ${response.statusCode}`); + res.json({ + success: true, + statusCode: response.statusCode, + url: targetUrl, + bodyLength: response.body.length + }); + } catch (error) { + log(`Request failed: ${error.message}`); + res.status(500).json({ + success: false, + error: error.message, + url: targetUrl + }); + } +}); + +app.listen(port, () => { + log(`Listening on port: ${port}`); +}); diff --git a/packages/collector/test/tracing/misc/require-esm/package.json b/packages/collector/test/tracing/misc/require-esm/package.json new file mode 100644 index 0000000000..a902b9e131 --- /dev/null +++ b/packages/collector/test/tracing/misc/require-esm/package.json @@ -0,0 +1,9 @@ +{ + "name": "require-esm-test", + "version": "1.0.0", + "description": "Test for Node.js require(esm) support", + "main": "app.js", + "dependencies": { + "got": "latest" + } +} diff --git a/packages/collector/test/tracing/misc/require-esm/test.js b/packages/collector/test/tracing/misc/require-esm/test.js new file mode 100644 index 0000000000..bca0b0680e --- /dev/null +++ b/packages/collector/test/tracing/misc/require-esm/test.js @@ -0,0 +1,13 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +const config = require('@instana/core/test/config'); + +describe('tracing/misc/require-esm', function () { + this.timeout(config.getTestTimeout() * 2); + + require('./test_base')(); +}); diff --git a/packages/collector/test/tracing/misc/require-esm/test_base.js b/packages/collector/test/tracing/misc/require-esm/test_base.js new file mode 100644 index 0000000000..8d44266356 --- /dev/null +++ b/packages/collector/test/tracing/misc/require-esm/test_base.js @@ -0,0 +1,124 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +const semver = require('semver'); +const expect = require('chai').expect; +const { supportedVersion } = require('@instana/core').tracing; +const config = require('@instana/core/test/config'); +const { retry, expectExactlyOneMatching } = require('@instana/core/test/test_util'); +const ProcessControls = require('@instana/collector/test/test_util/ProcessControls'); +const globalAgent = require('@instana/collector/test/globalAgent'); + +const supportsRequireESM = semver.gte(process.versions.node, '20.19.0'); +const mochaSuiteFn = supportedVersion(process.versions.node) && supportsRequireESM ? describe : describe.skip; + +module.exports = function () { + mochaSuiteFn('tracing/require(esm)', function () { + this.timeout(config.getTestTimeout() * 2); + + globalAgent.setUpCleanUpHooks(); + const agentControls = globalAgent.instance; + + describe('tracing enabled, no suppression', function () { + let controls; + + before(async () => { + controls = new ProcessControls({ + dirname: __dirname, + useGlobalAgent: true + }); + + await controls.startAndWaitForAgentConnection(); + }); + + beforeEach(async () => { + await agentControls.clearReceivedTraceData(); + }); + + after(async () => { + await controls.stop(); + }); + + afterEach(async () => { + await controls.clearIpcMessages(); + }); + + it('must trace HTTP requests made with ESM-only package loaded via require()', async () => { + const response = await controls.sendRequest({ + method: 'GET', + path: '/make-request' + }); + + expect(response.success).to.equal(true); + expect(response.statusCode).to.equal(200); + + await retry(async () => { + const spans = await agentControls.getSpans(); + + expect(spans).to.have.lengthOf(2); + + const httpEntry = expectExactlyOneMatching(spans, [ + span => expect(span.n).to.equal('node.http.server'), + span => expect(span.k).to.equal(1), + span => expect(span.data.http.method).to.equal('GET'), + span => expect(span.data.http.url).to.match(/\/make-request/) + ]); + + const httpExit = expectExactlyOneMatching(spans, [ + span => expect(span.n).to.equal('node.http.client'), + span => expect(span.k).to.equal(2), + span => expect(span.t).to.equal(httpEntry.t), + span => expect(span.p).to.equal(httpEntry.s), + span => expect(span.data.http.method).to.equal('GET'), + span => expect(span.data.http.url).to.include('127.0.0.1') + ]); + + expect(httpExit.data.http.status).to.equal(200); + expect(httpExit.ec).to.equal(0); + expect(httpEntry.ec).to.equal(0); + }); + }); + }); + + describe('tracing suppressed', function () { + let controls; + + before(async () => { + controls = new ProcessControls({ + dirname: __dirname, + useGlobalAgent: true + }); + + await controls.startAndWaitForAgentConnection(); + }); + + beforeEach(async () => { + await agentControls.clearReceivedTraceData(); + }); + + after(async () => { + await controls.stop(); + }); + + afterEach(async () => { + await controls.clearIpcMessages(); + }); + + it('should not trace when suppressed', async () => { + await controls.sendRequest({ + method: 'GET', + path: '/make-request', + suppressTracing: true + }); + + await retry(async () => { + const spans = await agentControls.getSpans(); + expect(spans).to.have.lengthOf(0); + }); + }); + }); + }); +};