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 ( + + + +
+

This is my header

+
+
{children}
+ + + + ) +} diff --git a/merged/nextjs/tests/versioned/app-dir/app/page.js b/merged/nextjs/tests/versioned/app-dir/app/page.js new file mode 100644 index 00000000..b0b86e1a --- /dev/null +++ b/merged/nextjs/tests/versioned/app-dir/app/page.js @@ -0,0 +1,11 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export default function MyApp() { + return ( +
This is the homepage
+ ) +} + diff --git a/merged/nextjs/tests/versioned/app-dir/app/person/[id]/page.js b/merged/nextjs/tests/versioned/app-dir/app/person/[id]/page.js new file mode 100644 index 00000000..8a045a8c --- /dev/null +++ b/merged/nextjs/tests/versioned/app-dir/app/person/[id]/page.js @@ -0,0 +1,17 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getPerson } from '../../../lib/functions' + +export default async function Person({ params }) { + const user = await getPerson(params.id) + + return ( +
+
{JSON.stringify(user, null, 4)}
+
+ ) +} + diff --git a/merged/nextjs/tests/versioned/app-dir/app/static/dynamic/[value]/page.js b/merged/nextjs/tests/versioned/app-dir/app/static/dynamic/[value]/page.js new file mode 100644 index 00000000..868d8d92 --- /dev/null +++ b/merged/nextjs/tests/versioned/app-dir/app/static/dynamic/[value]/page.js @@ -0,0 +1,33 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import Head from 'next/head' + +export async function getProps(params) { + return { + title: 'This is a statically built dynamic route page.', + value: params.value + } +} + +export async function generateStaticPaths() { + return [ + { value: 'testing' } + ] +} + + +export default async function Standard({ params }) { + const { title, value } = await getProps(params) + return ( + <> + + {title} + +

{title}

+
Value: {value}
+ + ) +} diff --git a/merged/nextjs/tests/versioned/app-dir/app/static/standard/page.js b/merged/nextjs/tests/versioned/app-dir/app/static/standard/page.js new file mode 100644 index 00000000..70b027f0 --- /dev/null +++ b/merged/nextjs/tests/versioned/app-dir/app/static/standard/page.js @@ -0,0 +1,25 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import Head from 'next/head' + +export async function getProps() { + return { + title: 'This is a standard statically built page.' + } +} + + +export default async function Standard() { + const { title } = await getProps() + return ( + <> + + {title} + +

{title}

+ + ) +} diff --git a/merged/nextjs/tests/versioned/app-dir/data.js b/merged/nextjs/tests/versioned/app-dir/data.js new file mode 100644 index 00000000..2e1e4031 --- /dev/null +++ b/merged/nextjs/tests/versioned/app-dir/data.js @@ -0,0 +1,28 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const data = [ + { + id: 1, + firstName: 'LeBron', + middleName: 'Raymone', + lastName: 'James', + age: 36 + }, + { + id: 2, + firstName: 'Lil', + middleName: 'Nas', + lastName: 'X', + age: 22 + }, + { + id: 3, + firstName: 'Beyoncé', + middleName: 'Giselle', + lastName: 'Knowles-Carter', + age: 40 + } +] diff --git a/merged/nextjs/tests/versioned/app-dir/lib/data.js b/merged/nextjs/tests/versioned/app-dir/lib/data.js new file mode 100644 index 00000000..2e1e4031 --- /dev/null +++ b/merged/nextjs/tests/versioned/app-dir/lib/data.js @@ -0,0 +1,28 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const data = [ + { + id: 1, + firstName: 'LeBron', + middleName: 'Raymone', + lastName: 'James', + age: 36 + }, + { + id: 2, + firstName: 'Lil', + middleName: 'Nas', + lastName: 'X', + age: 22 + }, + { + id: 3, + firstName: 'Beyoncé', + middleName: 'Giselle', + lastName: 'Knowles-Carter', + age: 40 + } +] diff --git a/merged/nextjs/tests/versioned/app-dir/lib/functions.js b/merged/nextjs/tests/versioned/app-dir/lib/functions.js new file mode 100644 index 00000000..836af9a7 --- /dev/null +++ b/merged/nextjs/tests/versioned/app-dir/lib/functions.js @@ -0,0 +1,6 @@ +import { data } from '../data' +export async function getPerson(id) { + const person = data.find((datum) => datum.id.toString() === id) + + return person || `Could not find person with id of ${id}` +} diff --git a/merged/nextjs/tests/versioned/app/.gitignore b/merged/nextjs/tests/versioned/app/.gitignore deleted file mode 100644 index a680367e..00000000 --- a/merged/nextjs/tests/versioned/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.next diff --git a/merged/nextjs/tests/versioned/next.config.js b/merged/nextjs/tests/versioned/next.config.js new file mode 100644 index 00000000..5e0918f9 --- /dev/null +++ b/merged/nextjs/tests/versioned/next.config.js @@ -0,0 +1,17 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = { + eslint: { + // Warning: This allows production builds to successfully complete even if + // your project has ESLint errors. + ignoreDuringBuilds: true + }, + experimental: { + appDir: true + } +} diff --git a/merged/nextjs/tests/versioned/package.json b/merged/nextjs/tests/versioned/package.json index 8c826211..cc2c0fd9 100644 --- a/merged/nextjs/tests/versioned/package.json +++ b/merged/nextjs/tests/versioned/package.json @@ -18,6 +18,19 @@ "transaction-naming.tap.js" ] }, + { + "engines": { + "node": ">=18" + }, + "dependencies": { + "next": ">=13.4.19", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "files": [ + "app-dir.tap.js" + ] + }, { "engines": { "node": ">=18"