diff --git a/merged/nextjs/.eslintrc.js b/merged/nextjs/.eslintrc.js index 91d6277e..668fc531 100644 --- a/merged/nextjs/.eslintrc.js +++ b/merged/nextjs/.eslintrc.js @@ -9,5 +9,5 @@ module.exports = { parserOptions: { ecmaVersion: 2020 }, - ignorePatterns: ['tests/versioned/app'] + ignorePatterns: ['tests/versioned/app', 'tests/versioned/app-dir'] } diff --git a/merged/nextjs/lib/next-server.js b/merged/nextjs/lib/next-server.js index 69b39952..dd6595dd 100644 --- a/merged/nextjs/lib/next-server.js +++ b/merged/nextjs/lib/next-server.js @@ -27,7 +27,7 @@ module.exports = function initialize(shim, nextServer) { function wrapRenderToResponseWithComponents(shim, originalFn) { return function wrappedRenderToResponseWithComponents() { const [ctx, result] = arguments - const { pathname } = ctx + const { pathname, renderOpts } = ctx // this is not query params but instead url params for dynamic routes const { query, components } = result @@ -52,7 +52,7 @@ module.exports = function initialize(shim, nextServer) { shim.setTransactionUri(pathname) - const urlParams = extractRouteParams(ctx.query, query) + const urlParams = extractRouteParams(ctx.query, renderOpts?.params || query) assignParameters(shim, urlParams) return originalFn.apply(this, arguments) diff --git a/merged/nextjs/lib/utils.js b/merged/nextjs/lib/utils.js index dd4e62d3..95c0645a 100644 --- a/merged/nextjs/lib/utils.js +++ b/merged/nextjs/lib/utils.js @@ -41,7 +41,7 @@ const MAX_MW_SUPPORTED_VERSION = '13.4.12' utils.MAX_MW_SUPPORTED_VERSION = MAX_MW_SUPPORTED_VERSION utils.MIN_MW_SUPPORTED_VERSION = MIN_MW_SUPPORTED_VERSION /** - * Middlware instrumentation has had quite the journey for us. + * Middleware instrumentation has had quite the journey for us. * As of 8/7/23 it no longer functions because it is running in a worker thread. * Our instrumentation cannot propagate context in threads so for now we will no longer record this * span. diff --git a/merged/nextjs/tests/versioned/.gitignore b/merged/nextjs/tests/versioned/.gitignore index d8b83df9..dfba4152 100644 --- a/merged/nextjs/tests/versioned/.gitignore +++ b/merged/nextjs/tests/versioned/.gitignore @@ -1 +1,2 @@ -package-lock.json +app/.next +app-dir/.next diff --git a/merged/nextjs/tests/versioned/app-dir.tap.js b/merged/nextjs/tests/versioned/app-dir.tap.js new file mode 100644 index 00000000..a48a8bec --- /dev/null +++ b/merged/nextjs/tests/versioned/app-dir.tap.js @@ -0,0 +1,145 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const tap = require('tap') +const helpers = require('./helpers') +const utils = require('@newrelic/test-utilities') +const NEXT_TRANSACTION_PREFIX = 'WebTransaction/WebFrameworkUri/Nextjs/GET/' +const DESTINATIONS = { + NONE: 0x00, + TRANS_EVENT: 0x01, + TRANS_TRACE: 0x02, + ERROR_EVENT: 0x04, + BROWSER_EVENT: 0x08, + SPAN_EVENT: 0x10, + TRANS_SEGMENT: 0x20 +} + +tap.Test.prototype.addAssert('nextCLMAttrs', 1, function ({ segments, clmEnabled }) { + segments.forEach(({ segment, name, filepath }) => { + const attrs = segment.getAttributes() + if (clmEnabled) { + this.match( + attrs, + { + 'code.function': name, + 'code.filepath': filepath + }, + 'should add code.function and code.filepath when CLM is enabled.' + ) + } else { + this.notOk(attrs['code.function'], 'should not add code.function when CLM is disabled.') + this.notOk(attrs['code.filepath'], 'should not add code.filepath when CLM is disabled.') + } + }) +}) + +tap.test('Next.js', (t) => { + t.autoend() + let agent + let server + + t.before(async () => { + await helpers.build(__dirname, 'app-dir') + + agent = utils.TestAgent.makeInstrumented({ + attributes: { + include: ['request.parameters.*'] + } + }) + helpers.registerInstrumentation(agent) + + // TODO: would be nice to run a new server per test so there are not chained failures + // but currently has issues. Potentially due to module caching. + server = await helpers.start(__dirname, 'app-dir', '3002') + }) + + t.teardown(async () => { + await server.close() + agent.unload() + }) + + // since we setup agent in before we need to remove + // the transactionFinished listener between tests to avoid + // context leaking + function setupTransactionHandler(t) { + return new Promise((resolve) => { + function txHandler(transaction) { + resolve(transaction) + } + + agent.agent.on('transactionFinished', txHandler) + + t.teardown(() => { + agent.agent.removeListener('transactionFinished', txHandler) + }) + }) + } + + t.test('should capture query params for static, non-dynamic route, page', async (t) => { + const prom = setupTransactionHandler(t) + + const res = await helpers.makeRequest('/static/standard?first=one&second=two', 3002) + t.equal(res.statusCode, 200) + const tx = await prom + + const agentAttributes = getTransactionEventAgentAttributes(tx) + + t.match(agentAttributes, { + 'request.parameters.first': 'one', + 'request.parameters.second': 'two' + }) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/static/standard`) + }) + + t.test('should capture query and route params for static, dynamic route, page', async (t) => { + const prom = setupTransactionHandler(t) + + const res = await helpers.makeRequest('/static/dynamic/testing?queryParam=queryValue', 3002) + t.equal(res.statusCode, 200) + const tx = await prom + + const agentAttributes = getTransactionEventAgentAttributes(tx) + + t.match(agentAttributes, { + 'request.parameters.route.value': 'testing', // route [value] param + 'request.parameters.queryParam': 'queryValue' + }) + + t.notOk(agentAttributes['request.parameters.route.queryParam']) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/static/dynamic/[value]`) + }) + + t.test( + 'should capture query params for server-side rendered, non-dynamic route, page', + async (t) => { + const prom = setupTransactionHandler(t) + const res = await helpers.makeRequest('/person/1?first=one&second=two', 3002) + t.equal(res.statusCode, 200) + const tx = await prom + + const agentAttributes = getTransactionEventAgentAttributes(tx) + + t.match( + agentAttributes, + { + 'request.parameters.first': 'one', + 'request.parameters.second': 'two' + }, + 'should match transaction attributes' + ) + + t.notOk(agentAttributes['request.parameters.route.first']) + t.notOk(agentAttributes['request.parameters.route.second']) + t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/person/[id]`) + } + ) + + function getTransactionEventAgentAttributes(transaction) { + return transaction.trace.attributes.get(DESTINATIONS.TRANS_EVENT) + } +}) diff --git a/merged/nextjs/tests/versioned/app-dir/app/layout.js b/merged/nextjs/tests/versioned/app-dir/app/layout.js new file mode 100644 index 00000000..8c28c776 --- /dev/null +++ b/merged/nextjs/tests/versioned/app-dir/app/layout.js @@ -0,0 +1,17 @@ + +export default function Layout({ children }) { +return ( + +
+{JSON.stringify(user, null, 4)}+