-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
ref(nextjs): Use flush code from withSentry
in all backend wrappers
#5814
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
Merged
lobsterkatie
merged 2 commits into
master
from
kmclb-nextjs-enable-flush-in-data-fetchers-wrapper
Sep 28, 2022
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { flush } from '@sentry/node'; | ||
import { Transaction } from '@sentry/types'; | ||
import { fill, logger } from '@sentry/utils'; | ||
import { ServerResponse } from 'http'; | ||
|
||
import { ResponseEndMethod, WrappedResponseEndMethod } from '../types'; | ||
|
||
/** | ||
* Wrap `res.end()` so that it closes the transaction and flushes events before letting the request finish. | ||
* | ||
* Note: This wraps a sync method with an async method. While in general that's not a great idea in terms of keeping | ||
* things in the right order, in this case it's safe, because the native `.end()` actually *is* (effectively) async, and | ||
* its run actually *is* (literally) awaited, just manually so (which reflects the fact that the core of the | ||
* request/response code in Node by far predates the introduction of `async`/`await`). When `.end()` is done, it emits | ||
* the `prefinish` event, and only once that fires does request processing continue. See | ||
* https://github.com/nodejs/node/commit/7c9b607048f13741173d397795bac37707405ba7. | ||
* | ||
* Also note: `res.end()` isn't called until *after* all response data and headers have been sent, so blocking inside of | ||
* `end` doesn't delay data getting to the end user. See | ||
* https://nodejs.org/api/http.html#responseenddata-encoding-callback. | ||
* | ||
* @param transaction The transaction tracing request handling | ||
* @param res: The request's corresponding response | ||
*/ | ||
export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: ServerResponse): void { | ||
const wrapEndMethod = (origEnd: ResponseEndMethod): WrappedResponseEndMethod => { | ||
return async function sentryWrappedEnd(this: ServerResponse, ...args: unknown[]) { | ||
await finishTransaction(transaction, this); | ||
await flushQueue(); | ||
|
||
return origEnd.call(this, ...args); | ||
}; | ||
}; | ||
|
||
// Prevent double-wrapping | ||
if (!(res.end as WrappedResponseEndMethod).__sentry_original__) { | ||
fill(res, 'end', wrapEndMethod); | ||
} | ||
} | ||
|
||
/** Finish the given response's transaction and set HTTP status data */ | ||
export async function finishTransaction(transaction: Transaction | undefined, res: ServerResponse): Promise<void> { | ||
if (transaction) { | ||
transaction.setHttpStatus(res.statusCode); | ||
|
||
// If any open spans are set to finish when the response ends, it sets up a race condition between their `finish` | ||
// calls and the transaction's `finish` call - and any spans which lose the race will get dropped from the | ||
// transaction. To prevent this, push `transaction.finish` to the next event loop so that it's guaranteed to lose | ||
// the race, and wait for it to be done before flushing events. | ||
const transactionFinished: Promise<void> = new Promise(resolve => { | ||
setImmediate(() => { | ||
transaction.finish(); | ||
resolve(); | ||
}); | ||
}); | ||
await transactionFinished; | ||
} | ||
} | ||
|
||
/** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */ | ||
export async function flushQueue(): Promise<void> { | ||
try { | ||
__DEBUG_BUILD__ && logger.log('Flushing events...'); | ||
await flush(2000); | ||
__DEBUG_BUILD__ && logger.log('Done flushing events'); | ||
} catch (e) { | ||
__DEBUG_BUILD__ && logger.log('Error while flushing events:\n', e); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
Apparently
res.end
can beundefined
. It crashes my deployment static pages withgetInitialProps
on_app.js
:Page:
_app:
Logs:
Uh oh!
There was an error while loading. Please reload this page.
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.
Huh. What version of next are you using? Do you have anything special set in your
next.config.js
? I just used both of the above pages and neither my local build nor my vercel build broke, and I was able to visit the page both on localhost and my vercel deployment.(I mean, I can check for
res.end
easily enough, but this is still confusing.)UPDATE: Okay, something is wrong. Nextjs itself calls
res.end()
all over the place without checking that it exists - during build, during runtime for normal pages, and during runtime for API pages. If there were any possibility of it being undefined, it seems to me that would have backfired on them long ago.Uh oh!
There was an error while loading. Please reload this page.
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.
P.S. Here are the links for my test:
_app: https://github.com/lobsterkatie/nextjs-test-app/blob/cb86d720feb791c9f869e13b57533be355f386e3/pages/_app.js
test page: https://github.com/lobsterkatie/nextjs-test-app/blob/cb86d720feb791c9f869e13b57533be355f386e3/pages/boring/lucaTest.js
deployed test page: https://nextjs-test-cyorm8xox-lobsterkatie.vercel.app/boring/lucaTest
Note that to be fair, I didn't use your exact _app page, because I don't have the
Nav
component, so I removed it. But I can't see how that changes anything with respect toend
being defined (or not).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 think I know what is going on. In short: Our
isBuild
function is broken for anybody using a different build command than the defaultyarn build
ornext build
(or as a matter of fact any command that has an isolated argumentbuild
in it).In my vercel test setup I am using the command
build:vercel
for which the checkprocess.argv.includes('build')
fails. This is probably why it was working for me locally (where I use the build commandyarn build
) and failing on vercel - because locally the instrumentation wasn't used during build, but on vercel it was. I'll try to push a fix in a separate PR.Anyhow, it still seems that during build
.end
might be undefined - maybe we're missing something in the Next codebase. Should we check for its existence before we assume its there or is this too edgecasey?On a somewhat unrelated note: What I will definitely do is change our wrapping functions to not assume that
req
/res
is always there. It seemed fine at first but this whole thing sketched me out a bit.Uh oh!
There was an error while loading. Please reload this page.
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 did discover in my testing that
isBuild
wasn't running on vercel at all, because theif
inindex.server.ts
was short-circuiting. That fix is a one-liner, so lemme push that right now. The question of what to look for is a bigger one, which I can't test at the moment because suddenly my dev env can't find eithernode
oryarn
. 🤦🏻♀️ I can look at it tomorrow, though.UPDATE: The first part is done in #5828.