Skip to content

Commit 8bcd867

Browse files
authored
fix(nextjs): Fix nock wrapping conflict in integration tests (#4619)
In our integration tests for nextjs, we use `nock` to intercept outgoing http requests, which lets us both examine a request’s payload and mock its response. As part of its initialization, `nock` monkeypatches the `get` and `request` methods in both the `http` and `https` Node built-in modules. Our `Http` integration also monkeypatches those methods. The order in which the two monkeypatching operations happen matters. If `nock` monkeypatches before Sentry does we end up with `sentryWrapper(nockWrapper(originalGet))`, which means that no matter what `nock` does or doesn’t do with `originalGet`, our wrapper code will always run. But if `nock` monkeypatches after we do, we end up with `nockWrapper(sentryWrapper(originalGet))`, meaning that if `nock` chooses not to call the function it’s wrapping, our code never runs. Next 12.1 introduces a change in [this PR](vercel/next.js#23261), which causes Next to load the `_app` and `_document` pages as soon as the server starts, in the interest of serving the first requested page more quickly. This causes the order of the monkey patching to change, causing the http tests to fail for nextjs. This patch solves this by forcing `get` and `request` to be wrapped again by Sentry after they are wrapped by `nock`. There are some TODOs that need to be addressed, but merging this patch to unblock CI.
1 parent 1bf9883 commit 8bcd867

File tree

3 files changed

+75
-3
lines changed

3 files changed

+75
-3
lines changed

packages/nextjs/test/integration/test/server/tracingHttp.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ module.exports = async ({ url: urlBase, argv }) => {
4040
'tracingHttp',
4141
);
4242

43-
await getAsync(url);
43+
// The `true` causes `getAsync` to rewrap `http.get` in next 12, since it will have been overwritten by the import of
44+
// `nock` above. See https://github.com/getsentry/sentry-javascript/pull/4619.
45+
// TODO: see note in `getAsync` about removing the boolean
46+
await getAsync(url, true);
4447
await sleep(250);
4548

4649
assert.ok(capturedRequest.isDone(), 'Did not intercept expected request');

packages/nextjs/test/integration/test/utils/server.js

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
const { get } = require('http');
22
const nock = require('nock');
3+
const nodeSDK = require('@sentry/node');
34
const { logIf, parseEnvelope } = require('./common');
45

56
Error.stackTraceLimit = Infinity;
67

7-
const getAsync = url => {
8+
const getAsync = (url, rewrap = false) => {
9+
// Depending on what version of Nextjs we're testing, the wrapping which happens in the `Http` integration may have
10+
// happened too early and gotten overwritten by `nock`. This redoes the wrapping if so.
11+
//
12+
// TODO: This works but is pretty hacky in that it has the potential to wrap things multiple times, more even than the
13+
// double-wrapping which is discussed at length in the comment in `ensureWrappedGet` below, which is why we need
14+
// `rewrap`. Once we fix `fill` to not wrap things twice, we should be able to take this out and just always call
15+
// `ensureWrappedGet`.
16+
const wrappedGet = rewrap ? ensureWrappedGet(get, url) : get;
17+
818
return new Promise((resolve, reject) => {
9-
get(url, res => {
19+
wrappedGet(url, res => {
1020
res.setEncoding('utf8');
1121
let rawData = '';
1222
res.on('data', chunk => {
@@ -99,6 +109,60 @@ const objectMatches = (actual, expected) => {
99109
return true;
100110
};
101111

112+
/**
113+
* Rewrap `http.get` if the wrapped version has been overridden by `nock`.
114+
*
115+
* This is only relevant for Nextjs >= 12.1, which changed when `_app` is initialized, which in turn changed the order
116+
* in which our SDK and `nock` wrap `http.get`. See https://github.com/getsentry/sentry-javascript/pull/4619.
117+
*
118+
* TODO: We'll have to do this for `ClientRequest` also if we decide to start wrapping that.
119+
* TODO: Can we fix the wrapping-things-twice problem discussed in the comment below?
120+
*/
121+
function ensureWrappedGet(importedGet, url) {
122+
// we always test against the latest minor for any given Nextjs major version, so if we're testing Next 12, it's
123+
// definitely at least 12.1, making this check against the major version sufficient
124+
if (Number(process.env.NEXTJS_VERSION) < 12) {
125+
return importedGet;
126+
}
127+
128+
// As of Next 12.1, creating a `NextServer` instance (which we do immediately upon starting this test runner) loads
129+
// `_app`, which has the effect of initializing the SDK. So, unless something's gone wrong, we should always be able
130+
// to find the integration
131+
let httpIntegration;
132+
try {
133+
httpIntegration = nodeSDK.getCurrentHub().getClient().getIntegration(nodeSDK.Integrations.Http);
134+
} catch (err) {
135+
console.warn(`Warning: Sentry SDK not set up at \`NextServer\` initialization. Request URL: ${url}`);
136+
return importedGet;
137+
}
138+
139+
// This rewraps `http.get` and `http.request`, which, at this point, look like `nockWrapper(sentryWrapper(get))` and
140+
// `nockWrapper(sentryWrapper(request))`. By the time we're done with this function, they'll look like
141+
// `sentryWrapper(nockWrapper(sentryWrapper(get)))` and `sentryWrapper(nockWrapper(sentryWrapper(request)))`,
142+
// respectively. Though this seems less than ideal, we don't have to worry about our instrumentation being
143+
// (meaningfully) called twice because:
144+
//
145+
// 1) As long as we set up a `nock` interceptor for any outgoing http request, `nock`'s wrapper will call a replacement
146+
// function for that request rather than call the function it's wrapping (in other words, it will look more like
147+
// `sentryWrapper(nockWrapper(getSubstitute))` than `sentryWrapper(nockWrapper(sentryWrapper(get)))`), which means
148+
// our code is only called once.
149+
// 2) In cases where we don't set up an interceptor (such as for the `wrappedGet` call in `getAsync` above), it's true
150+
// that we can end up with `sentryWrapper(nockWrapper(sentryWrapper(get)))`, meaning our wrapper code will run
151+
// twice. For now that's okay because in those cases we're not in the middle of a transactoin and therefore
152+
// the two wrappers' respective attempts to start spans will both no-op.
153+
//
154+
// TL; DR - if the double-wrapping means you're seeing two spans where you really only want one, set up a nock
155+
// interceptor for the request.
156+
//
157+
// TODO: add in a "don't do this twice" check (in `fill`, maybe moved from `wrap`), so that we don't wrap the outer
158+
// wrapper with a third wrapper
159+
httpIntegration.setupOnce();
160+
161+
// now that we've rewrapped it, grab the correct version of `get` for use in our tests
162+
const httpModule = require('http');
163+
return httpModule.get;
164+
}
165+
102166
module.exports = {
103167
getAsync,
104168
interceptEventRequest,

packages/nextjs/test/run-integration-tests.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ mv next.config.js next.config.js.bak
3232

3333
for NEXTJS_VERSION in 10 11 12; do
3434

35+
# export this to the env so that we can behave differently depending on which version of next we're testing, without
36+
# having to pass this value from function to function to function to the one spot, deep in some callstack, where we
37+
# actually need it
38+
export NEXTJS_VERSION=$NEXTJS_VERSION
39+
3540
# Next 10 requires at least Node v10
3641
if [ "$NODE_MAJOR" -lt "10" ]; then
3742
echo "[nextjs] Next.js is not compatible with versions of Node older than v10. Current version $NODE_VERSION"

0 commit comments

Comments
 (0)