-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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): Create spans and route parameterization in server-side getInitialProps
#5587
Conversation
size-limit report 📦
|
2ab7218
to
ba8deb5
Compare
getInitialProps
getInitialProps
}, | ||
err => { | ||
// TODO: Can we somehow associate the error with the span? | ||
span.finish(); |
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 can associate the error with the specific span if the span is on the scope.
We should set span status here though.
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.
The span won't be on the scope, as it's one of (eventually) potentially many running simultaneously inside the transaction. But don't we have some function that associates errors to transactions? (I can dig it up later if you don't know what it's called off the top of your head.)
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 is how we do error linking in the product:
sentry-javascript/packages/hub/src/scope.ts
Lines 471 to 480 in bdd7fde
// We want to set the trace context for normal events only if there isn't already | |
// a trace context on the event. There is a product feature in place where we link | |
// errors with transaction and it relies on that. | |
if (this._span) { | |
event.contexts = { trace: this._span.getTraceContext(), ...event.contexts }; | |
const transactionName = this._span.transaction && this._span.transaction.name; | |
if (transactionName) { | |
event.tags = { transaction: transactionName, ...event.tags }; | |
} | |
} |
origGetServerSideProps: GetServerSideProps, | ||
route: string, | ||
): GetServerSideProps { | ||
return function (...getServerSidePropsArguments: Parameters<GetServerSideProps>): ReturnType<GetServerSideProps> { |
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.
Next has actual types for these, which is what's embedded in the hacky types objects I was using hoping I could get TS to be smart about the fact that they all went together. Any reason not to import them here, directly from next, and use them?
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 just safer. Assume Next.js released that makes getServerSideProps
accept an additional argument. The current implementation of the wrapper... breaks. It doesn't pass the new argument. The Parameters
is the most elegant way to type this. I wouldn't change this.
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.
Okay, your call. I do see your point when it comes to the arguments, since you're gathering them into an array, and you're probably right - as stable as this is, who needs another thing to update if nextjs changes their mind and radically rewrites everything (which they have been known to do).
// We do the following instead of `await`-ing the return value of `origFunction`, because that would require us to | ||
// make this function async which might in turn create a mismatch of function signatures between the original |
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 feels overly complicated to me. Whether the user provides a sync or an async function, all three of the data fetchers are set up to accept a promise:
getInitialProps
: https://github.com/vercel/next.js/blob/de7aa2d6e486c40b8be95a1327639cbed75a8782/packages/next/shared/lib/utils.ts#L20
getStaticProps
: https://github.com/vercel/next.js/blob/de7aa2d6e486c40b8be95a1327639cbed75a8782/packages/next/types/index.d.ts#L117-L123
getServerSideProps
: https://github.com/vercel/next.js/blob/de7aa2d6e486c40b8be95a1327639cbed75a8782/packages/next/types/index.d.ts#L170-L176
So I think it's fine that we're always creating an async function. To handle the case of errors, we can just use a try-catch-finally
. (It's a good thought - I'll update my PR to include it also.)
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 don't think this is overly complicated. If we have the opportunity not to change the function signature, why would we change it? It's not about what the Framework accepts, it's about not changing what the user provides. I don't really see the value behind adjusting this.
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.
Nextjs is going to have to await it, regardless, just in case it's async, though.
To me, the question isn't why would we change it but why shouldn't we change it, if it makes no difference to nextjs and it allows us to us to simplify our code? Regardless of whether the alternative is "overly" complicated, it is certainly more complicated than simply doing await original(...)
.
Having the simpler version is good for its own sake, but it will also make error handling simpler, because it'll allow us to just use try-catch
, and it brings us in line with what withSentry
already does. (Eventually, I'd like to meld that into this mix also.)
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.
Alright. Changed in f45ceb6
export async function callDataFetcherTraced<F extends (...args: any[]) => Promise<any> | any>( | ||
origFunction: F, | ||
origFunctionArgs: Parameters<F>, | ||
options: { |
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.
what do you think about making this the SpanContext
instead of options? we would make route
-> description
.
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 is blocked on #5564. I have to see what we're gonna do with op and description there before I make any more changes here.
Honestly, I wouldn't read too much into this PR. We're likely gonna scrap this wrapper anyways because it's too generic to be used across different data fetching methods.
01300bd
to
84cd837
Compare
f45ceb6
to
d996be7
Compare
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 LGTM. Let's get this in to unblock the release. As you wrote, this will probably change again in the future, so we can revisit stuff then.
@@ -111,6 +111,21 @@ export default function wrapDataFetchersLoader(this: LoaderThis<LoaderOptions>, | |||
// We know one or the other will be defined, depending on the version of webpack being used | |||
const { projectDir, pagesDir } = 'getOptions' in this ? this.getOptions() : this.query; | |||
|
|||
// Get the parameterized route name from this page's filepath | |||
const parameterizedRouteName = path |
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.
Nice chaining approach! I like it! (I wish it'd work as nicely in all our SDKs)
(EDIT, I'm referring to the lines below 115, I just botched the line selection in the review :D)
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.
Props go to Katie.
// Logic will be: If there is no active transaction, start one with correct name and source. If there is an active | ||
// transaction, create a child span with correct name and source. |
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.
Just for me to get this right: If we already have a transaction, this means that we started it beforehand in a previous data fetcher function (iirc you said that getInitialProps can e.g. be executed before getServerSideProps), right?
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.
Right now it's started in instrumentServer.ts
- but actually, that's not working so there never is a transaction. Soooo.... this PR currently doesn't do anything lol.
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, something is weird, because there IS in fact a transaction in our integration tests, otherwise they wouldn't pass. And there's a transaction if you run the server inside of the debugger. But seemingly not if you just run the server straight. It's very mysterious.
Ref: #5505
This PR mainly introduces span creation in the server-side
getInitialProps
wrapping (except for_app
,_document
,_error
pages) - secondly it does a minor refactor on the generic data fetcher wrapper we have, slightly cleaning up types.