Skip to content

Commit

Permalink
Merge pull request newrelic#176 from bizob2828/app-dir
Browse files Browse the repository at this point in the history
feat: Added a test suite for App Router.
  • Loading branch information
bizob2828 authored Mar 19, 2024
2 parents 3d104e2 + 07f08de commit 9699c33
Show file tree
Hide file tree
Showing 16 changed files with 346 additions and 6 deletions.
2 changes: 1 addition & 1 deletion merged/nextjs/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ module.exports = {
parserOptions: {
ecmaVersion: 2020
},
ignorePatterns: ['tests/versioned/app']
ignorePatterns: ['tests/versioned/app', 'tests/versioned/app-dir']
}
4 changes: 2 additions & 2 deletions merged/nextjs/lib/next-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion merged/nextjs/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion merged/nextjs/tests/versioned/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
package-lock.json
app/.next
app-dir/.next
145 changes: 145 additions & 0 deletions merged/nextjs/tests/versioned/app-dir.tap.js
Original file line number Diff line number Diff line change
@@ -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)
}
})
17 changes: 17 additions & 0 deletions merged/nextjs/tests/versioned/app-dir/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

export default function Layout({ children }) {
return (
<html lang="en">
<head />
<body>
<header>
<h1>This is my header</h1>
</header>
<main>{children}</main>
<footer>
<p>This is my footer</p>
</footer>
</body>
</html>
)
}
11 changes: 11 additions & 0 deletions merged/nextjs/tests/versioned/app-dir/app/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright 2022 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

export default function MyApp() {
return (
<section>This is the homepage</section>
)
}

17 changes: 17 additions & 0 deletions merged/nextjs/tests/versioned/app-dir/app/person/[id]/page.js
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<pre>{JSON.stringify(user, null, 4)}</pre>
</div>
)
}

Original file line number Diff line number Diff line change
@@ -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 (
<>
<Head>
<title>{title}</title>
</Head>
<h1>{title}</h1>
<div>Value: {value}</div>
</>
)
}
25 changes: 25 additions & 0 deletions merged/nextjs/tests/versioned/app-dir/app/static/standard/page.js
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Head>
<title>{title}</title>
</Head>
<h1>{title}</h1>
</>
)
}
28 changes: 28 additions & 0 deletions merged/nextjs/tests/versioned/app-dir/data.js
Original file line number Diff line number Diff line change
@@ -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
}
]
28 changes: 28 additions & 0 deletions merged/nextjs/tests/versioned/app-dir/lib/data.js
Original file line number Diff line number Diff line change
@@ -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
}
]
6 changes: 6 additions & 0 deletions merged/nextjs/tests/versioned/app-dir/lib/functions.js
Original file line number Diff line number Diff line change
@@ -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}`
}
1 change: 0 additions & 1 deletion merged/nextjs/tests/versioned/app/.gitignore

This file was deleted.

17 changes: 17 additions & 0 deletions merged/nextjs/tests/versioned/next.config.js
Original file line number Diff line number Diff line change
@@ -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
}
}
13 changes: 13 additions & 0 deletions merged/nextjs/tests/versioned/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 9699c33

Please sign in to comment.