diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/get/subject.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/get/subject.js index f6e1e21e4611..1ba7e011aca2 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/get/subject.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/get/subject.js @@ -1,3 +1,5 @@ -fetch('http://localhost:7654/foo').then(() => { - Sentry.captureException('test error'); -}); +fetch('http://localhost:7654/foo') + .then(res => res.text()) + .then(() => { + Sentry.captureException('test error'); + }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/getWithRequestObj/subject.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/getWithRequestObj/subject.js index 0ca20f1b5acb..6c3eaf0ddf25 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/getWithRequestObj/subject.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/getWithRequestObj/subject.js @@ -1,3 +1,5 @@ -fetch(new Request('http://localhost:7654/foo')).then(() => { - Sentry.captureException('test error'); -}); +fetch(new Request('http://localhost:7654/foo')) + .then(res => res.text()) + .then(() => { + Sentry.captureException('test error'); + }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/post/subject.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/post/subject.js index ea1bf44bc905..948b569e3c03 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/post/subject.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/post/subject.js @@ -6,6 +6,8 @@ fetch('http://localhost:7654/foo', { 'Content-Type': 'application/json', Cache: 'no-cache', }, -}).then(() => { - Sentry.captureException('test error'); -}); +}) + .then(res => res.text()) + .then(() => { + Sentry.captureException('test error'); + }); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts index bd8050b740aa..c54b88df1f8c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts @@ -42,10 +42,12 @@ sentryTest('captures text request body', async ({ getLocalTestPath, page, browse fetch('http://localhost:7654/foo', { method: 'POST', body: 'input body', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -122,10 +124,12 @@ sentryTest('captures JSON request body', async ({ getLocalTestPath, page, browse fetch('http://localhost:7654/foo', { method: 'POST', body: '{"foo":"bar"}', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -206,10 +210,12 @@ sentryTest('captures non-text request body', async ({ getLocalTestPath, page, br fetch('http://localhost:7654/foo', { method: 'POST', body: body, - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -286,10 +292,12 @@ sentryTest('captures text request body when matching relative URL', async ({ get fetch('/foo', { method: 'POST', body: 'input body', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -364,10 +372,12 @@ sentryTest('does not capture request body when URL does not match', async ({ get fetch('http://localhost:7654/bar', { method: 'POST', body: 'input body', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts index 0b0b37fb1cf6..2ae0f97e48eb 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts @@ -38,14 +38,14 @@ sentryTest.skip('handles empty/missing request headers', async ({ getLocalTestPa await page.goto(url); await page.evaluate(() => { - /* eslint-disable */ fetch('http://localhost:7654/foo', { method: 'POST', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); - /* eslint-enable */ + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); }); const request = await requestPromise; @@ -121,10 +121,12 @@ sentryTest('captures request headers as POJO', async ({ getLocalTestPath, page, 'X-Custom-Header': 'foo', 'X-Test-Header': 'test-value', }, - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -206,10 +208,12 @@ sentryTest('captures request headers on Request', async ({ getLocalTestPath, pag }, }); /* eslint-disable */ - fetch(request).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + fetch(request) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -291,10 +295,12 @@ sentryTest('captures request headers as Headers instance', async ({ getLocalTest fetch('http://localhost:7654/foo', { method: 'POST', headers, - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -375,10 +381,12 @@ sentryTest('does not captures request headers if URL does not match', async ({ g 'X-Custom-Header': 'foo', 'X-Test-Header': 'test-value', }, - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts index 52857d17479d..7e3d0da80d30 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts @@ -42,10 +42,12 @@ sentryTest.skip('captures request body size when body is sent', async ({ getLoca fetch('http://localhost:7654/foo', { method: 'POST', body: '{"foo":"bar"}', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -129,10 +131,12 @@ sentryTest('captures request size from non-text request body', async ({ getLocal fetch('http://localhost:7654/foo', { method: 'POST', body: blob, - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts index 5e58b63218ef..8861f75db2ca 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts @@ -42,10 +42,12 @@ sentryTest('captures text response body', async ({ getLocalTestPath, page, brows /* eslint-disable */ fetch('http://localhost:7654/foo', { method: 'POST', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -124,10 +126,12 @@ sentryTest('captures JSON response body', async ({ getLocalTestPath, page, brows /* eslint-disable */ fetch('http://localhost:7654/foo', { method: 'POST', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -206,10 +210,12 @@ sentryTest('captures non-text response body', async ({ getLocalTestPath, page, b /* eslint-disable */ fetch('http://localhost:7654/foo', { method: 'POST', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -288,10 +294,12 @@ sentryTest.skip('does not capture response body when URL does not match', async /* eslint-disable */ fetch('http://localhost:7654/bar', { method: 'POST', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts index 8f098627c120..0614aec77d00 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts @@ -38,10 +38,12 @@ sentryTest('handles empty headers', async ({ getLocalTestPath, page, browserName await page.goto(url); await page.evaluate(() => { - fetch('http://localhost:7654/foo').then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + fetch('http://localhost:7654/foo') + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); }); const request = await requestPromise; @@ -112,10 +114,12 @@ sentryTest('captures response headers', async ({ getLocalTestPath, page }) => { await page.goto(url); await page.evaluate(() => { - fetch('http://localhost:7654/foo').then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + fetch('http://localhost:7654/foo') + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); }); const request = await requestPromise; @@ -192,10 +196,12 @@ sentryTest('does not capture response headers if URL does not match', async ({ g await page.goto(url); await page.evaluate(() => { - fetch('http://localhost:7654/bar').then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + fetch('http://localhost:7654/bar') + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); }); const request = await requestPromise; diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts index ad3aafe34562..0dc4b7392b8d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts @@ -45,10 +45,12 @@ sentryTest('captures response size from Content-Length header if available', asy await page.evaluate(() => { /* eslint-disable */ - fetch('http://localhost:7654/foo').then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + fetch('http://localhost:7654/foo') + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -135,10 +137,12 @@ sentryTest('captures response size without Content-Length header', async ({ getL await page.evaluate(() => { /* eslint-disable */ - fetch('http://localhost:7654/foo').then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + fetch('http://localhost:7654/foo') + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); @@ -224,10 +228,12 @@ sentryTest('captures response size from non-text response body', async ({ getLoc /* eslint-disable */ fetch('http://localhost:7654/foo', { method: 'POST', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts index cce931062770..6e3413b1e54f 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts @@ -42,10 +42,12 @@ sentryTest('captures correct timestamps', async ({ getLocalTestPath, page, brows fetch('http://localhost:7654/foo', { method: 'POST', body: '{"foo":"bar"}', - }).then(() => { - // @ts-expect-error Sentry is a global - Sentry.captureException('test error'); - }); + }) + .then(res => res.text()) + .then(() => { + // @ts-expect-error Sentry is a global + Sentry.captureException('test error'); + }); /* eslint-enable */ }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts index 6096fcfb1493..c46af6d229b5 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts @@ -12,7 +12,7 @@ export async function middleware(request: NextRequest) { } if (request.headers.has('x-should-make-request')) { - await fetch('http://localhost:3030/'); + await fetch('http://localhost:3030/').then(res => res.text()); } return NextResponse.next(); diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index 9c663f88baf0..9a0a959c4c3a 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { HandlerDataFetch } from '@sentry/types'; +import { DEBUG_BUILD } from '../debug-build'; +import { logger } from '../logger'; import { fill } from '../object'; import { supportsNativeFetch } from '../supports'; import { GLOBAL_OBJ } from '../worldwide'; @@ -47,14 +49,59 @@ function instrumentFetch(): void { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return originalFetch.apply(GLOBAL_OBJ, args).then( (response: Response) => { - const finishedHandlerData: HandlerDataFetch = { - ...handlerData, - endTimestamp: Date.now(), - response, - }; - - triggerHandlers('fetch', finishedHandlerData); - return response; + // We need to immediately clone the response, so that if the user reads the body before we call the handlers, + // we cannot clone the response inside the handlers since it would throw. The Replay integration for instance + // needs to clone the response body inside a handler to collect response size and breadcrumbs. + // If the cloning fails for whatever reason, we still pass the original response because it could be used for + // status. + let responseForHandlers = response; + let clonedResponseForResolving; + try { + responseForHandlers = response.clone(); + clonedResponseForResolving = response.clone(); + } catch (e) { + // noop + DEBUG_BUILD && logger.warn('Failed to clone response body.'); + } + + if (clonedResponseForResolving && clonedResponseForResolving.body) { + const responseReader = clonedResponseForResolving.body.getReader(); + + // eslint-disable-next-line no-inner-declarations + function consumeChunks({ done }: { done: boolean }): Promise { + if (!done) { + return responseReader.read().then(consumeChunks); + } else { + return Promise.resolve(); + } + } + + responseReader + .read() + .then(consumeChunks) + .then(() => { + triggerHandlers('fetch', { + ...handlerData, + endTimestamp: Date.now(), + response: responseForHandlers, + }); + }) + .catch(() => { + // noop + }); + } else { + triggerHandlers('fetch', { + ...handlerData, + endTimestamp: Date.now(), + response: responseForHandlers, + }); + } + + return new Promise(resolve => { + setTimeout(() => { + resolve(response); + }, 0); + }); }, (error: Error) => { const erroredHandlerData: HandlerDataFetch = {