Skip to content

Commit 9699c33

Browse files
authored
Merge pull request newrelic#176 from bizob2828/app-dir
feat: Added a test suite for App Router.
2 parents 3d104e2 + 07f08de commit 9699c33

File tree

16 files changed

+346
-6
lines changed

16 files changed

+346
-6
lines changed

merged/nextjs/.eslintrc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ module.exports = {
99
parserOptions: {
1010
ecmaVersion: 2020
1111
},
12-
ignorePatterns: ['tests/versioned/app']
12+
ignorePatterns: ['tests/versioned/app', 'tests/versioned/app-dir']
1313
}

merged/nextjs/lib/next-server.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ module.exports = function initialize(shim, nextServer) {
2727
function wrapRenderToResponseWithComponents(shim, originalFn) {
2828
return function wrappedRenderToResponseWithComponents() {
2929
const [ctx, result] = arguments
30-
const { pathname } = ctx
30+
const { pathname, renderOpts } = ctx
3131
// this is not query params but instead url params for dynamic routes
3232
const { query, components } = result
3333

@@ -52,7 +52,7 @@ module.exports = function initialize(shim, nextServer) {
5252

5353
shim.setTransactionUri(pathname)
5454

55-
const urlParams = extractRouteParams(ctx.query, query)
55+
const urlParams = extractRouteParams(ctx.query, renderOpts?.params || query)
5656
assignParameters(shim, urlParams)
5757

5858
return originalFn.apply(this, arguments)

merged/nextjs/lib/utils.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const MAX_MW_SUPPORTED_VERSION = '13.4.12'
4141
utils.MAX_MW_SUPPORTED_VERSION = MAX_MW_SUPPORTED_VERSION
4242
utils.MIN_MW_SUPPORTED_VERSION = MIN_MW_SUPPORTED_VERSION
4343
/**
44-
* Middlware instrumentation has had quite the journey for us.
44+
* Middleware instrumentation has had quite the journey for us.
4545
* As of 8/7/23 it no longer functions because it is running in a worker thread.
4646
* Our instrumentation cannot propagate context in threads so for now we will no longer record this
4747
* span.
+2-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
package-lock.json
1+
app/.next
2+
app-dir/.next
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2022 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const tap = require('tap')
9+
const helpers = require('./helpers')
10+
const utils = require('@newrelic/test-utilities')
11+
const NEXT_TRANSACTION_PREFIX = 'WebTransaction/WebFrameworkUri/Nextjs/GET/'
12+
const DESTINATIONS = {
13+
NONE: 0x00,
14+
TRANS_EVENT: 0x01,
15+
TRANS_TRACE: 0x02,
16+
ERROR_EVENT: 0x04,
17+
BROWSER_EVENT: 0x08,
18+
SPAN_EVENT: 0x10,
19+
TRANS_SEGMENT: 0x20
20+
}
21+
22+
tap.Test.prototype.addAssert('nextCLMAttrs', 1, function ({ segments, clmEnabled }) {
23+
segments.forEach(({ segment, name, filepath }) => {
24+
const attrs = segment.getAttributes()
25+
if (clmEnabled) {
26+
this.match(
27+
attrs,
28+
{
29+
'code.function': name,
30+
'code.filepath': filepath
31+
},
32+
'should add code.function and code.filepath when CLM is enabled.'
33+
)
34+
} else {
35+
this.notOk(attrs['code.function'], 'should not add code.function when CLM is disabled.')
36+
this.notOk(attrs['code.filepath'], 'should not add code.filepath when CLM is disabled.')
37+
}
38+
})
39+
})
40+
41+
tap.test('Next.js', (t) => {
42+
t.autoend()
43+
let agent
44+
let server
45+
46+
t.before(async () => {
47+
await helpers.build(__dirname, 'app-dir')
48+
49+
agent = utils.TestAgent.makeInstrumented({
50+
attributes: {
51+
include: ['request.parameters.*']
52+
}
53+
})
54+
helpers.registerInstrumentation(agent)
55+
56+
// TODO: would be nice to run a new server per test so there are not chained failures
57+
// but currently has issues. Potentially due to module caching.
58+
server = await helpers.start(__dirname, 'app-dir', '3002')
59+
})
60+
61+
t.teardown(async () => {
62+
await server.close()
63+
agent.unload()
64+
})
65+
66+
// since we setup agent in before we need to remove
67+
// the transactionFinished listener between tests to avoid
68+
// context leaking
69+
function setupTransactionHandler(t) {
70+
return new Promise((resolve) => {
71+
function txHandler(transaction) {
72+
resolve(transaction)
73+
}
74+
75+
agent.agent.on('transactionFinished', txHandler)
76+
77+
t.teardown(() => {
78+
agent.agent.removeListener('transactionFinished', txHandler)
79+
})
80+
})
81+
}
82+
83+
t.test('should capture query params for static, non-dynamic route, page', async (t) => {
84+
const prom = setupTransactionHandler(t)
85+
86+
const res = await helpers.makeRequest('/static/standard?first=one&second=two', 3002)
87+
t.equal(res.statusCode, 200)
88+
const tx = await prom
89+
90+
const agentAttributes = getTransactionEventAgentAttributes(tx)
91+
92+
t.match(agentAttributes, {
93+
'request.parameters.first': 'one',
94+
'request.parameters.second': 'two'
95+
})
96+
t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/static/standard`)
97+
})
98+
99+
t.test('should capture query and route params for static, dynamic route, page', async (t) => {
100+
const prom = setupTransactionHandler(t)
101+
102+
const res = await helpers.makeRequest('/static/dynamic/testing?queryParam=queryValue', 3002)
103+
t.equal(res.statusCode, 200)
104+
const tx = await prom
105+
106+
const agentAttributes = getTransactionEventAgentAttributes(tx)
107+
108+
t.match(agentAttributes, {
109+
'request.parameters.route.value': 'testing', // route [value] param
110+
'request.parameters.queryParam': 'queryValue'
111+
})
112+
113+
t.notOk(agentAttributes['request.parameters.route.queryParam'])
114+
t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/static/dynamic/[value]`)
115+
})
116+
117+
t.test(
118+
'should capture query params for server-side rendered, non-dynamic route, page',
119+
async (t) => {
120+
const prom = setupTransactionHandler(t)
121+
const res = await helpers.makeRequest('/person/1?first=one&second=two', 3002)
122+
t.equal(res.statusCode, 200)
123+
const tx = await prom
124+
125+
const agentAttributes = getTransactionEventAgentAttributes(tx)
126+
127+
t.match(
128+
agentAttributes,
129+
{
130+
'request.parameters.first': 'one',
131+
'request.parameters.second': 'two'
132+
},
133+
'should match transaction attributes'
134+
)
135+
136+
t.notOk(agentAttributes['request.parameters.route.first'])
137+
t.notOk(agentAttributes['request.parameters.route.second'])
138+
t.equal(tx.name, `${NEXT_TRANSACTION_PREFIX}/person/[id]`)
139+
}
140+
)
141+
142+
function getTransactionEventAgentAttributes(transaction) {
143+
return transaction.trace.attributes.get(DESTINATIONS.TRANS_EVENT)
144+
}
145+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
export default function Layout({ children }) {
3+
return (
4+
<html lang="en">
5+
<head />
6+
<body>
7+
<header>
8+
<h1>This is my header</h1>
9+
</header>
10+
<main>{children}</main>
11+
<footer>
12+
<p>This is my footer</p>
13+
</footer>
14+
</body>
15+
</html>
16+
)
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright 2022 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export default function MyApp() {
7+
return (
8+
<section>This is the homepage</section>
9+
)
10+
}
11+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2022 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { getPerson } from '../../../lib/functions'
7+
8+
export default async function Person({ params }) {
9+
const user = await getPerson(params.id)
10+
11+
return (
12+
<div>
13+
<pre>{JSON.stringify(user, null, 4)}</pre>
14+
</div>
15+
)
16+
}
17+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2022 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import Head from 'next/head'
7+
8+
export async function getProps(params) {
9+
return {
10+
title: 'This is a statically built dynamic route page.',
11+
value: params.value
12+
}
13+
}
14+
15+
export async function generateStaticPaths() {
16+
return [
17+
{ value: 'testing' }
18+
]
19+
}
20+
21+
22+
export default async function Standard({ params }) {
23+
const { title, value } = await getProps(params)
24+
return (
25+
<>
26+
<Head>
27+
<title>{title}</title>
28+
</Head>
29+
<h1>{title}</h1>
30+
<div>Value: {value}</div>
31+
</>
32+
)
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2022 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import Head from 'next/head'
7+
8+
export async function getProps() {
9+
return {
10+
title: 'This is a standard statically built page.'
11+
}
12+
}
13+
14+
15+
export default async function Standard() {
16+
const { title } = await getProps()
17+
return (
18+
<>
19+
<Head>
20+
<title>{title}</title>
21+
</Head>
22+
<h1>{title}</h1>
23+
</>
24+
)
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2022 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export const data = [
7+
{
8+
id: 1,
9+
firstName: 'LeBron',
10+
middleName: 'Raymone',
11+
lastName: 'James',
12+
age: 36
13+
},
14+
{
15+
id: 2,
16+
firstName: 'Lil',
17+
middleName: 'Nas',
18+
lastName: 'X',
19+
age: 22
20+
},
21+
{
22+
id: 3,
23+
firstName: 'Beyoncé',
24+
middleName: 'Giselle',
25+
lastName: 'Knowles-Carter',
26+
age: 40
27+
}
28+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2022 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export const data = [
7+
{
8+
id: 1,
9+
firstName: 'LeBron',
10+
middleName: 'Raymone',
11+
lastName: 'James',
12+
age: 36
13+
},
14+
{
15+
id: 2,
16+
firstName: 'Lil',
17+
middleName: 'Nas',
18+
lastName: 'X',
19+
age: 22
20+
},
21+
{
22+
id: 3,
23+
firstName: 'Beyoncé',
24+
middleName: 'Giselle',
25+
lastName: 'Knowles-Carter',
26+
age: 40
27+
}
28+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { data } from '../data'
2+
export async function getPerson(id) {
3+
const person = data.find((datum) => datum.id.toString() === id)
4+
5+
return person || `Could not find person with id of ${id}`
6+
}

merged/nextjs/tests/versioned/app/.gitignore

-1
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
module.exports = {
9+
eslint: {
10+
// Warning: This allows production builds to successfully complete even if
11+
// your project has ESLint errors.
12+
ignoreDuringBuilds: true
13+
},
14+
experimental: {
15+
appDir: true
16+
}
17+
}

merged/nextjs/tests/versioned/package.json

+13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@
1818
"transaction-naming.tap.js"
1919
]
2020
},
21+
{
22+
"engines": {
23+
"node": ">=18"
24+
},
25+
"dependencies": {
26+
"next": ">=13.4.19",
27+
"react": "18.2.0",
28+
"react-dom": "18.2.0"
29+
},
30+
"files": [
31+
"app-dir.tap.js"
32+
]
33+
},
2134
{
2235
"engines": {
2336
"node": ">=18"

0 commit comments

Comments
 (0)