diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index b344fb9c22..5822d446b1 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -68,7 +68,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 @@ -86,6 +86,7 @@ jobs: name: unit-tests-${{ matrix.node-version }} path: ./coverage/unit/lcov.info - name: Run ESM Unit Tests + if: matrix.node-version != '20.x' run: npm run unit:esm - name: Archive ESM Unit Test Coverage uses: actions/upload-artifact@v3 @@ -100,7 +101,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 @@ -127,7 +128,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 @@ -139,16 +140,8 @@ jobs: run: npm ci - name: Run Docker Services run: npm run services - - name: Run Versioned Tests (npm v6 / Node 12/14) - if: ${{ matrix.node-version == '14.x' }} - run: TEST_CHILD_TIMEOUT=600000 npm run versioned:npm6 - env: - VERSIONED_MODE: ${{ github.ref == 'refs/heads/main' && '--minor' || '--major' }} - JOBS: 4 # 2 per CPU seems to be the sweet spot in GHA (July 2022) - C8_REPORTER: lcovonly - - name: Run Versioned Tests (npm v7 / Node 16+) - if: ${{ matrix.node-version != '14.x' }} - run: TEST_CHILD_TIMEOUT=600000 npm run versioned:npm7 + - name: Run Versioned Tests + run: TEST_CHILD_TIMEOUT=600000 npm run versioned env: VERSIONED_MODE: ${{ github.ref == 'refs/heads/main' && '--minor' || '--major' }} JOBS: 4 # 2 per CPU seems to be the sweet spot in GHA (July 2022) @@ -165,7 +158,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 @@ -194,4 +187,4 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} directory: versioned-tests-${{ matrix.node-version }} - flags: versioned-tests-${{ matrix.node-version }} \ No newline at end of file + flags: versioned-tests-${{ matrix.node-version }} diff --git a/.github/workflows/versioned-coverage.yml b/.github/workflows/versioned-coverage.yml index 7a0ceac46a..6815e58044 100644 --- a/.github/workflows/versioned-coverage.yml +++ b/.github/workflows/versioned-coverage.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x] + node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 diff --git a/index.js b/index.js index 8816090545..bcfe8d2b7e 100644 --- a/index.js +++ b/index.js @@ -53,8 +53,8 @@ function initialize() { throw new Error(message) } - // TODO: Update this check when Node v20 support is added - if (psemver.satisfies('>=19.0.0')) { + // TODO: Update this check when Node v22 support is added + if (psemver.satisfies('>=21.0.0')) { logger.warn( 'New Relic for Node.js %s has not been tested on Node.js %s. Please ' + 'update the agent or downgrade your version of Node.js', diff --git a/lib/environment.js b/lib/environment.js index da9c3901cb..694e679b45 100644 --- a/lib/environment.js +++ b/lib/environment.js @@ -12,6 +12,7 @@ const logger = require('./logger').child({ component: 'environment' }) const stringify = require('json-stringify-safe') const asyncEachLimit = require('./util/async-each-limit') const DISPATCHER_VERSION = 'Dispatcher Version' +const semver = require('semver') // As of 1.7.0 you can no longer dynamically link v8 // https://github.com/nodejs/io.js/commit/d726a177ed @@ -260,7 +261,7 @@ function flattenVersions(packages) { try { return stringify(pair) } catch (err) { - logger.debug(err, 'Unabled to stringify package version') + logger.debug(err, 'Unable to stringify package version') return '' } }) @@ -291,6 +292,19 @@ function remapConfigSettings() { addSetting(remapping[key], value) } }) + + maybeAddMissingProcessVars() + } +} + +/** + * As of Node 19 DTrace and ETW are no longer bundled + * see: https://nodejs.org/en/blog/announcements/v19-release-announce#dtrace/systemtap/etw-support + */ +function maybeAddMissingProcessVars() { + if (semver.gte(process.version, '19.0.0')) { + addSetting(remapping.node_use_dtrace, 'no') + addSetting(remapping.node_use_etw, 'no') } } diff --git a/package.json b/package.json index 40faaa2ef7..76ed4716ca 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ ], "homepage": "https://github.com/newrelic/node-newrelic", "engines": { - "node": ">=14", + "node": ">=16", "npm": ">=6.0.0" }, "directories": { @@ -168,13 +168,11 @@ "versioned-tests": "./bin/run-versioned-tests.sh", "update-changelog-version": "node ./bin/update-changelog-version", "checkout-external-versioned": "node ./test/versioned-external/checkout-external-tests.js", - "versioned": "npm run versioned:npm7", - "versioned:major": "VERSIONED_MODE=--major npm run versioned:npm7", - "versioned:npm6": "npm run checkout-external-versioned && npm run prepare-test && time ./bin/run-versioned-tests.sh", - "versioned:npm7": "npm run checkout-external-versioned && npm run prepare-test && NPM7=1 time ./bin/run-versioned-tests.sh", - "versioned:async-local": "NEW_RELIC_FEATURE_FLAG_ASYNC_LOCAL_CONTEXT=1 npm run versioned:npm7", + "versioned:major": "VERSIONED_MODE=--major npm run versioned", + "versioned": "npm run checkout-external-versioned && npm run prepare-test && NPM7=1 time ./bin/run-versioned-tests.sh", + "versioned:async-local": "NEW_RELIC_FEATURE_FLAG_ASYNC_LOCAL_CONTEXT=1 npm run versioned", "versioned:async-local:major": "NEW_RELIC_FEATURE_FLAG_ASYNC_LOCAL_CONTEXT=1 npm run versioned:major", - "versioned:security": "NEW_RELIC_SECURITY_AGENT_ENABLED=true npm run versioned:npm7", + "versioned:security": "NEW_RELIC_SECURITY_AGENT_ENABLED=true npm run versioned", "versioned:security:major": "NEW_RELIC_SECURITY_AGENT_ENABLED=true npm run versioned:major", "prepare": "husky install" }, diff --git a/test/integration/distributed-tracing/dt.tap.js b/test/integration/distributed-tracing/dt.tap.js index ad6d1bd133..c9c2ffc197 100644 --- a/test/integration/distributed-tracing/dt.tap.js +++ b/test/integration/distributed-tracing/dt.tap.js @@ -10,9 +10,6 @@ const helper = require('../../lib/agent_helper') const tap = require('tap') const url = require('url') -const START_PORT = 10000 -const MIDDLE_PORT = 10001 -const END_PORT = 10002 const ACCOUNT_ID = '1337' const APP_ID = '7331' const EXPECTED_DT_METRICS = ['DurationByCaller', 'TransportDuration'] @@ -54,9 +51,14 @@ tap.test('distributed tracing full integration', (t) => { } } + // eslint-disable-next-line prefer-const + let MIDDLE_PORT + // eslint-disable-next-line prefer-const + let END_PORT + // Naming is how the requests will flow through the system, to test that all // metrics are generated as expected as well as the dirac events. - const start = generateServer(http, api, START_PORT, started, (req, res) => { + const start = generateServer(http, api, started, (req, res) => { const tx = agent.tracer.getTransaction() tx.nameState.appendPath('foobar') http.get(generateUrl(MIDDLE_PORT, 'start/middle'), (externRes) => { @@ -69,7 +71,9 @@ tap.test('distributed tracing full integration', (t) => { }) }) - const middle = generateServer(http, api, MIDDLE_PORT, started, (req, res) => { + const START_PORT = start.address().port + + const middle = generateServer(http, api, started, (req, res) => { t.ok(req.headers.newrelic, 'middle received newrelic from start') const tx = agent.tracer.getTransaction() @@ -84,11 +88,15 @@ tap.test('distributed tracing full integration', (t) => { }) }) - const end = generateServer(http, api, END_PORT, started, (req, res) => { + MIDDLE_PORT = middle.address().port + + const end = generateServer(http, api, started, (req, res) => { t.ok(req.headers.newrelic, 'end received newrelic from middle') res.end() }) + END_PORT = end.address().port + t.teardown(() => { start.close() middle.close() @@ -147,7 +155,7 @@ tap.test('distributed tracing full integration', (t) => { t.equal(scopedKeys.length, 1, 'middle should only be the inbound and outbound request.') t.same( scopedKeys, - ['External/localhost:10002/http'], + [`External/localhost:${END_PORT}/http`], 'should have expected scoped metric name' ) @@ -186,7 +194,7 @@ tap.test('distributed tracing full integration', (t) => { t.equal(scopedKeys.length, 1, 'start should only be the inbound and outbound request.') t.same( scopedKeys, - ['External/localhost:10001/http'], + [`External/localhost:${MIDDLE_PORT}/http`], 'should have expected scoped metric name' ) @@ -242,6 +250,9 @@ tap.test('distributed tracing', (t) => { let start = null let middle = null let end = null + let START_PORT + let MIDDLE_PORT + let END_PORT t.autoend() @@ -271,15 +282,19 @@ tap.test('distributed tracing', (t) => { }) } - start = generateServer(http, api, START_PORT, cb, (req, res) => { + start = generateServer(http, api, cb, (req, res) => { return getNextUrl('start/middle', 'start', MIDDLE_PORT, req, res) }) - middle = generateServer(http, api, MIDDLE_PORT, cb, (req, res) => { + + START_PORT = start.address().port + middle = generateServer(http, api, cb, (req, res) => { return getNextUrl('middle/end', 'middle', END_PORT, req, res) }) - end = generateServer(http, api, END_PORT, cb, (req, res) => { + MIDDLE_PORT = middle.address().port + end = generateServer(http, api, cb, (req, res) => { return createResponse(req, res, {}, 'end') }) + END_PORT = end.address().port }) t.afterEach(async () => { @@ -322,14 +337,14 @@ tap.test('distributed tracing', (t) => { }) }) -function generateServer(http, api, port, started, responseHandler) { +function generateServer(http, api, started, responseHandler) { const server = http.createServer((req, res) => { const tx = api.agent.getTransaction() tx.nameState.appendPath(req.url) req.resume() responseHandler(req, res) }) - server.listen(port, () => started()) + server.listen(() => started()) return server } diff --git a/test/integration/logger.tap.js b/test/integration/logger.tap.js index 2cc2ad44d3..6d562aa515 100644 --- a/test/integration/logger.tap.js +++ b/test/integration/logger.tap.js @@ -11,6 +11,7 @@ const tap = require('tap') const rimraf = require('rimraf') const util = require('util') const exec = util.promisify(require('child_process').exec) +const { isSupportedVersion } = require('../lib/agent_helper') const DIRNAME = 'XXXNOCONFTEST' @@ -56,10 +57,13 @@ tap.test('logger', function (t) { tap.test('Logger output', (t) => { t.autoend() - const execArgs = [ - { opt: '-r', arg: '../../../index.js' }, - { opt: '--experimental-loader', arg: '../../../esm-loader.mjs' } - ] + const execArgs = [{ opt: '-r', arg: '../../../index.js' }] + + // TODO: add back to array when we fix ESM loader + if (!isSupportedVersion('v19.0.0')) { + execArgs.push({ opt: '--experimental-loader', arg: '../../../esm-loader.mjs' }) + } + for (const pair of execArgs) { const { opt, arg } = pair t.test(`Check for ${opt} in logger output at debug level`, async (t) => { diff --git a/test/lib/agent_helper.js b/test/lib/agent_helper.js index 268bdcfe1e..88ec6ae182 100644 --- a/test/lib/agent_helper.js +++ b/test/lib/agent_helper.js @@ -19,6 +19,7 @@ const Transaction = require('../../lib/transaction') const symbols = require('../../lib/symbols') const http = require('http') const https = require('https') +const semver = require('semver') const KEYPATH = path.join(__dirname, 'test-key.key') const CERTPATH = path.join(__dirname, 'self-signed-test-certificate.crt') @@ -686,5 +687,13 @@ const helper = (module.exports = { : original[symbols.original] } return original + }, + /** + * Util that checks if current node version is supported + * @param {string} version semver version string + * @returns {boolean} if version is supported + */ + isSupportedVersion(version) { + return semver.gt(process.version, version) } }) diff --git a/test/unit/collector/remote-method.test.js b/test/unit/collector/remote-method.test.js index 0d6e74bab7..5995278891 100644 --- a/test/unit/collector/remote-method.test.js +++ b/test/unit/collector/remote-method.test.js @@ -254,7 +254,7 @@ tap.test('when the connection fails', (t) => { method.invoke({ message: 'none' }, {}, (error) => { t.ok(error) // regex for either ipv4 or ipv6 localhost - t.match(error.message, /connect ECONNREFUSED (127\.0\.0\.1|::1):8765/) + t.equal(error.code, 'ECONNREFUSED') t.end() }) diff --git a/test/unit/config/config-formatters.test.js b/test/unit/config/config-formatters.test.js index 9e2a09bb38..67c9608433 100644 --- a/test/unit/config/config-formatters.test.js +++ b/test/unit/config/config-formatters.test.js @@ -109,7 +109,7 @@ tap.test('config formatters', (t) => { const val = 'invalid' t.notOk(formatters.object(val, loggerMock)) t.equal(loggerMock.error.args[0][0], 'New Relic configurator could not deserialize object:') - t.match(loggerMock.error.args[1][0], /SyntaxError: Unexpected token i in JSON at position/) + t.match(loggerMock.error.args[1][0], /SyntaxError: Unexpected token/) t.end() }) }) @@ -132,7 +132,7 @@ tap.test('config formatters', (t) => { loggerMock.error.args[0][0], 'New Relic configurator could not deserialize object list:' ) - t.match(loggerMock.error.args[1][0], /SyntaxError: Unexpected token i in JSON at position/) + t.match(loggerMock.error.args[1][0], /SyntaxError: Unexpected token/) t.end() }) }) diff --git a/test/unit/environment.test.js b/test/unit/environment.test.js index c559704a25..f761eb5059 100644 --- a/test/unit/environment.test.js +++ b/test/unit/environment.test.js @@ -15,6 +15,7 @@ const path = require('path') const fs = require('fs/promises') const spawn = require('child_process').spawn const environment = require('../../lib/environment') +const { isSupportedVersion } = require('../lib/agent_helper') function find(settings, name) { const items = settings.filter(function (candidate) { @@ -136,7 +137,8 @@ tap.test('the environment scraper', (t) => { t.end() }) - t.test('without process.config', (t) => { + // TODO: remove tests when we drop support for node 18 + t.test('without process.config', { skip: isSupportedVersion('v19.0.0') }, (t) => { let conf = null t.before(() => { @@ -216,8 +218,10 @@ tap.test('the environment scraper', (t) => { t.end() }) + // TODO: remove this test when we drop support for node 18 t.test( 'should resolve refresh where deps and deps of deps are symlinked to each other', + { skip: isSupportedVersion('v19.0.0') }, async (t) => { process.config.variables.node_prefix = path.join(__dirname, '../lib/example-deps') const data = await environment.getJSON() diff --git a/test/unit/instrumentation/http/outbound.test.js b/test/unit/instrumentation/http/outbound.test.js index 09294d19bc..e77c1ad6b5 100644 --- a/test/unit/instrumentation/http/outbound.test.js +++ b/test/unit/instrumentation/http/outbound.test.js @@ -379,7 +379,7 @@ tap.test('should add data from cat header to segment', (t) => { }) helper.runInTransaction(agent, handled) - const errRegex = /connect ECONNREFUSED( 127.0.0.1:12345)?/ + const expectedCode = 'ECONNREFUSED' function handled(transaction) { const req = http.get({ host: 'localhost', port: 12345 }, function () {}) @@ -390,7 +390,7 @@ tap.test('should add data from cat header to segment', (t) => { }) req.on('error', function (err) { - t.match(err.message, errRegex) + t.equal(err.code, expectedCode) }) req.end() @@ -401,7 +401,7 @@ tap.test('should add data from cat header to segment', (t) => { req.on('close', function () { t.equal(transaction.exceptions.length, 1) - t.match(transaction.exceptions[0].error.message, errRegex) + t.equal(transaction.exceptions[0].error.code, expectedCode) t.end() }) diff --git a/test/unit/instrumentation/http/synthetics.test.js b/test/unit/instrumentation/http/synthetics.test.js index 56ee1c55f5..ecb1e128fb 100644 --- a/test/unit/instrumentation/http/synthetics.test.js +++ b/test/unit/instrumentation/http/synthetics.test.js @@ -34,10 +34,9 @@ tap.test('synthetics outbound header', (t) => { ENCODING_KEY ) - const PORT = 9873 + let port = null const CONNECT_PARAMS = { - hostname: 'localhost', - port: PORT + hostname: 'localhost' } t.beforeEach(() => { @@ -53,7 +52,10 @@ tap.test('synthetics outbound header', (t) => { }) return new Promise((resolve) => { - server.listen(PORT, resolve) + server.listen(0, function () { + ;({ port } = this.address()) + resolve() + }) }) }) @@ -64,10 +66,11 @@ tap.test('synthetics outbound header', (t) => { }) }) - t.test('should be propegated if on tx', (t) => { + t.test('should be propagated if on tx', (t) => { helper.runInTransaction(agent, function (transaction) { transaction.syntheticsData = SYNTHETICS_DATA transaction.syntheticsHeader = SYNTHETICS_HEADER + CONNECT_PARAMS.port = port const req = http.request(CONNECT_PARAMS, function (res) { res.resume() transaction.end() @@ -78,8 +81,9 @@ tap.test('synthetics outbound header', (t) => { }) }) - t.test('should not be propegated if not on tx', (t) => { + t.test('should not be propagated if not on tx', (t) => { helper.runInTransaction(agent, function (transaction) { + CONNECT_PARAMS.port = port http.get(CONNECT_PARAMS, function (res) { res.resume() transaction.end() @@ -99,11 +103,8 @@ tap.test('should add synthetics inbound header to transaction', (t) => { let synthData const ENCODING_KEY = 'Old Spice' - - const PORT = 9873 const CONNECT_PARAMS = { - hostname: 'localhost', - port: PORT + hostname: 'localhost' } function createServer(cb, requestHandler) { @@ -113,7 +114,7 @@ tap.test('should add synthetics inbound header to transaction', (t) => { res.end() req.resume() }) - s.listen(PORT, cb) + s.listen(0, cb) return s } @@ -150,6 +151,7 @@ tap.test('should add synthetics inbound header to transaction', (t) => { } server = createServer( function onListen() { + options.port = this.address().port http.get(options, function (res) { res.resume() }) @@ -177,7 +179,7 @@ tap.test('should add synthetics inbound header to transaction', (t) => { ) }) - t.test('should propegate inbound synthetics header on response', (t) => { + t.test('should propagate inbound synthetics header on response', (t) => { const synthHeader = hashes.obfuscateNameUsingKey(JSON.stringify(synthData), ENCODING_KEY) const options = Object.assign({}, CONNECT_PARAMS) options.headers = { @@ -185,6 +187,7 @@ tap.test('should add synthetics inbound header to transaction', (t) => { } server = createServer( function onListen() { + options.port = this.address().port http.get(options, function (res) { res.resume() }) diff --git a/test/versioned/esm-package/package.json b/test/versioned/esm-package/package.json index 81f68e1f85..01a3832b32 100644 --- a/test/versioned/esm-package/package.json +++ b/test/versioned/esm-package/package.json @@ -7,7 +7,7 @@ "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=16.12.0 <20" }, "dependencies": { "parse-json": "6.0.2" diff --git a/test/versioned/express-esm/package.json b/test/versioned/express-esm/package.json index 61c7fa2bbc..ce64cb3024 100644 --- a/test/versioned/express-esm/package.json +++ b/test/versioned/express-esm/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=16.12.0 <20" }, "dependencies": { "express": ">=4.6.0", diff --git a/test/versioned/grpc-esm/package.json b/test/versioned/grpc-esm/package.json index 6f364d0902..2a7ef29ea8 100644 --- a/test/versioned/grpc-esm/package.json +++ b/test/versioned/grpc-esm/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=16.12.0 <20" }, "dependencies": { "@grpc/grpc-js": ">=1.4.0" diff --git a/test/versioned/mongodb-esm/package.json b/test/versioned/mongodb-esm/package.json index 9b58ac5a9d..ffc5d2e6af 100644 --- a/test/versioned/mongodb-esm/package.json +++ b/test/versioned/mongodb-esm/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=16.12.0 <20" }, "dependencies": { "mongodb": ">=2.1 < 4.0.0 || >= 4.1.4 < 5" diff --git a/test/versioned/nestjs/nest.tap.js b/test/versioned/nestjs/nest.tap.js index 45523945db..19649b7aee 100644 --- a/test/versioned/nestjs/nest.tap.js +++ b/test/versioned/nestjs/nest.tap.js @@ -20,19 +20,12 @@ tap.test('Verify the Nest.js instrumentation', (t) => { t.before(async () => { await initNestApp() - }) - - t.teardown(async () => { - await deleteNestApp() - }) - - t.beforeEach(async () => { agent = helper.instrumentMockedAgent() const { bootstrap } = require('./test-app/dist/main.js') app = await bootstrap(port) }) - t.afterEach(() => { + t.teardown(async () => { app.close() helper.unloadAgent(agent) Object.keys(require.cache).forEach((key) => { @@ -40,8 +33,7 @@ tap.test('Verify the Nest.js instrumentation', (t) => { delete require.cache[key] } }) - agent = null - app = null + await deleteNestApp() }) t.test('should record a transaction in the base case', async (t) => { diff --git a/test/versioned/pg-esm/package.json b/test/versioned/pg-esm/package.json index 60c6e2ec19..0661f06903 100644 --- a/test/versioned/pg-esm/package.json +++ b/test/versioned/pg-esm/package.json @@ -6,7 +6,7 @@ "tests": [ { "engines": { - "node": ">=16.12.0" + "node": ">=16.12.0 <20" }, "dependencies": { "pg": ">=8.2 <8.8", @@ -20,7 +20,7 @@ }, { "engines": { - "node": ">=16.12.0" + "node": ">=16.12.0 <20" }, "dependencies": { "pg": ">=8.8",