New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(nextjs): Add performance monitoring to server components #7242
Conversation
…server-components
This comment was marked as outdated.
This comment was marked as outdated.
const sentryRequest = https.request( | ||
sentryIngestUrl, | ||
{ headers: proxyRequest.headers, method: proxyRequest.method }, | ||
sentryResponse => { | ||
sentryResponse.addListener('data', (chunk: Buffer) => { | ||
proxyResponse.write(chunk, 'binary'); | ||
sentryResponseChunks.push(chunk); | ||
}); | ||
|
||
sentryResponse.addListener('end', () => { | ||
eventCallbackListeners.forEach(listener => { | ||
const rawProxyRequestBody = Buffer.concat(proxyRequestChunks).toString(); | ||
const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); | ||
|
||
const data: SentryRequestCallbackData = { | ||
envelope: parseEnvelope(rawProxyRequestBody, new TextEncoder(), new TextDecoder()), | ||
rawProxyRequestBody, | ||
rawSentryResponseBody, | ||
sentryResponseStatusCode: sentryResponse.statusCode, | ||
}; | ||
|
||
listener(Buffer.from(JSON.stringify(data)).toString('base64')); | ||
}); | ||
proxyResponse.end(); | ||
}); | ||
|
||
sentryResponse.addListener('error', err => { | ||
throw err; | ||
}); | ||
|
||
proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); | ||
}, | ||
); |
Check failure
Code scanning / CodeQL
Server-side request forgery
let appDirPath: string; | ||
if (fs.existsSync(path.join(projectDir, 'app')) && fs.lstatSync(path.join(projectDir, 'app')).isDirectory()) { | ||
appDirPath = path.join(projectDir, 'app'); | ||
} else { | ||
appDirPath = path.join(projectDir, 'src', 'app'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This actually caused a bug and was painful af to find....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out tests help at finding bugs. Who would have thought 🤔
…ment-server-components
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there links to what this looks like? Does backend <-> frontend tracing work? Screenshots in the PR would also suffice.
@@ -34,7 +34,7 @@ type LoaderOptions = { | |||
appDir: string; | |||
pageExtensionRegex: string; | |||
excludeServerRoutes: Array<RegExp | string>; | |||
wrappingTargetKind: 'page' | 'api-route' | 'middleware' | 'page-server-component'; | |||
wrappingTargetKind: 'page' | 'api-route' | 'middleware' | 'server-component'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lowest l possible: Would be nice if this was a string enum
import { startEventProxyServer, waitForTransaction } from '../../test-utils/event-proxy-server'; | ||
|
||
startEventProxyServer({ | ||
port: 27496, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are these ports random? Should they not be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's kinda tricky. For Next.js we need to know the port of the proxy server at runtime AND at build time. Playwright (and implicitly the proxy server) are started after the project is built (obviously).
Instead of writing some convoluted logic that picks a port and then passes it everywhere, for now, I just opted to pick some port statically. Of course, this is somewhat error-prone but usually, it is just a "write once and forget" kinda thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good enough for me!
|
||
Sentry.init({ | ||
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, | ||
tunnel: 'http://localhost:27496/', // proxy server |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
l: maybe we can inject the tunnel in via a environmental variable?
const transactionEvent = await serverComponentTransactionPromise; | ||
const transactionEventId = transactionEvent.event_id; | ||
|
||
console.log(`Polling for server component transaction eventId: ${transactionEventId}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m: Could we have waitForTransaction
emit these logs? I'd like to keep the test surface as clean as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That in particular won't work since we use waitForTransaction
to get the transactionEventId
. If it is about the logging statement I can just remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But we can just log it out once the serverComponentTransactionPromise
resolves internally?
Ideally the tests themselves have no logs. And if it does, it's optional debug logging that can be turned on.
|
||
const transaction = startTransaction({ | ||
op: 'function.nextjs', | ||
name: `${componentType} Server Component (${componentRoute})`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this is the name we should choose for this 🤔
Maybe we try to have the componentRoute
at the start?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to pick something that makes it immediately obvious what the transaction is measuring. Right now it is already super confusing to see API route transactions like GET /api/my/route
intertwined with pageload transactions like /my/route
.
IMO having the transacitons show up as Page Server Component (/user/[id])
and Layout Server Component (/user/[id])
is very clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's fair, makes sense to me.
|
||
const currentScope = getCurrentHub().getScope(); | ||
if (currentScope) { | ||
currentScope.setSpan(transaction); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if you throw some React.withProfiler calls around child components if they'll show up attached as spans!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could try it out at some point. Right now, Sentry.withProfiler
is still littered with browser code which is a big nono in server components.
@AbhiPrasad Nope doesn't work, unfortunately. As soon as we use the More info on static vs dynamic: https://beta.nextjs.org/docs/rendering/static-and-dynamic-rendering |
Could we inject in a meta tag for backend -> frontend tracing?
We can monkey patch this away :))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only thing here is if there is a lot of value here is there is not many child spans on the transaction.
The interesting aspect when looking at server component traces is gonna be how long the fetches or db queries inside the component take. Right now we're missing the fetch instrumentation but as soon as that is done the transactions will be super useful. |
With this in mind IMO instrumenting server-side fetch is should be higher prio if we want this to be adopted/used. |
This PR adds performance monitoring to Next.js 13 server components.
The majority of changes is adding E2E tests for this.
Resolves #7176