From 07f08de051fb3699890e5588182233f5459633fb Mon Sep 17 00:00:00 2001
From: Bob Evans
Date: Fri, 27 Oct 2023 08:15:18 -0400
Subject: [PATCH] feat: Added a test suite for App Router.
---
merged/nextjs/.eslintrc.js | 2 +-
merged/nextjs/lib/next-server.js | 4 +-
merged/nextjs/lib/utils.js | 2 +-
merged/nextjs/tests/versioned/.gitignore | 3 +-
merged/nextjs/tests/versioned/app-dir.tap.js | 145 ++++++++++++++++++
.../tests/versioned/app-dir/app/layout.js | 17 ++
.../tests/versioned/app-dir/app/page.js | 11 ++
.../versioned/app-dir/app/person/[id]/page.js | 17 ++
.../app/static/dynamic/[value]/page.js | 33 ++++
.../app-dir/app/static/standard/page.js | 25 +++
merged/nextjs/tests/versioned/app-dir/data.js | 28 ++++
.../tests/versioned/app-dir/lib/data.js | 28 ++++
.../tests/versioned/app-dir/lib/functions.js | 6 +
merged/nextjs/tests/versioned/app/.gitignore | 1 -
merged/nextjs/tests/versioned/next.config.js | 17 ++
merged/nextjs/tests/versioned/package.json | 13 ++
16 files changed, 346 insertions(+), 6 deletions(-)
create mode 100644 merged/nextjs/tests/versioned/app-dir.tap.js
create mode 100644 merged/nextjs/tests/versioned/app-dir/app/layout.js
create mode 100644 merged/nextjs/tests/versioned/app-dir/app/page.js
create mode 100644 merged/nextjs/tests/versioned/app-dir/app/person/[id]/page.js
create mode 100644 merged/nextjs/tests/versioned/app-dir/app/static/dynamic/[value]/page.js
create mode 100644 merged/nextjs/tests/versioned/app-dir/app/static/standard/page.js
create mode 100644 merged/nextjs/tests/versioned/app-dir/data.js
create mode 100644 merged/nextjs/tests/versioned/app-dir/lib/data.js
create mode 100644 merged/nextjs/tests/versioned/app-dir/lib/functions.js
delete mode 100644 merged/nextjs/tests/versioned/app/.gitignore
create mode 100644 merged/nextjs/tests/versioned/next.config.js
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 (
+
+
+
+
+ {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 (
+
+ )
+}
+
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"