From 8489b898c72f85101fce5dc49600237a582cacff Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 27 Feb 2023 14:55:31 +0100 Subject: [PATCH 1/5] feat(browser): Allow to capture request payload/response --- .../browser/src/integrations/breadcrumbs.ts | 159 +++++++++++------- .../test/integration/suites/breadcrumbs.js | 1 + .../replay/src/coreHandlers/handleFetch.ts | 7 +- packages/replay/src/coreHandlers/handleXhr.ts | 5 + 4 files changed, 107 insertions(+), 65 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index b54aad03e9b2..678144f70849 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -14,8 +14,13 @@ import { import { WINDOW } from '../helpers'; +interface XhrFetchOptions { + captureRequestPayload: boolean; + captureResponsePayload: boolean; +} + /** JSDoc */ -interface BreadcrumbsOptions { +interface BreadcrumbsOptions extends XhrFetchOptions { console: boolean; dom: | boolean @@ -66,6 +71,8 @@ export class Breadcrumbs implements Integration { history: true, sentry: true, xhr: true, + captureRequestPayload: false, + captureResponsePayload: false, ...options, }; } @@ -86,10 +93,10 @@ export class Breadcrumbs implements Integration { addInstrumentationHandler('dom', _domBreadcrumb(this.options.dom)); } if (this.options.xhr) { - addInstrumentationHandler('xhr', _xhrBreadcrumb); + addInstrumentationHandler('xhr', _xhrBreadcrumb(this.options)); } if (this.options.fetch) { - addInstrumentationHandler('fetch', _fetchBreadcrumb); + addInstrumentationHandler('fetch', _fetchBreadcrumb(this.options)); } if (this.options.history) { addInstrumentationHandler('history', _historyBreadcrumb); @@ -217,79 +224,103 @@ function _consoleBreadcrumb(handlerData: { [key: string]: any }): void { * Creates breadcrumbs from XHR API calls */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -function _xhrBreadcrumb(handlerData: { [key: string]: any }): void { - if (handlerData.endTimestamp) { - // We only capture complete, non-sentry requests - if (handlerData.xhr.__sentry_own_request__) { - return; - } +function _xhrBreadcrumb(options: XhrFetchOptions): (handlerData: { [key: string]: any }) => void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (handlerData: { [key: string]: any }): void => { + if (handlerData.endTimestamp) { + // We only capture complete, non-sentry requests + if (handlerData.xhr.__sentry_own_request__) { + return; + } - const { method, url, status_code, body } = handlerData.xhr.__sentry_xhr__ || {}; + const { method, url, status_code, body } = handlerData.xhr.__sentry_xhr__ || {}; - getCurrentHub().addBreadcrumb( - { - category: 'xhr', - data: { - method, - url, - status_code, + const xhrData: { [key: string]: unknown } = { + method, + url, + status_code, + }; + + if (options.captureRequestPayload) { + xhrData.request_payload = body; + } + + if (options.captureResponsePayload) { + xhrData.response_payload = handlerData.xhr.responseText; + } + + getCurrentHub().addBreadcrumb( + { + category: 'xhr', + data: xhrData, + type: 'http', }, - type: 'http', - }, - { - xhr: handlerData.xhr, - input: body, - }, - ); + { + xhr: handlerData.xhr, + input: body, + }, + ); - return; - } + return; + } + }; } - /** * Creates breadcrumbs from fetch API calls */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -function _fetchBreadcrumb(handlerData: { [key: string]: any }): void { - // We only capture complete fetch requests - if (!handlerData.endTimestamp) { - return; - } +function _fetchBreadcrumb(options: XhrFetchOptions): (handlerData: { [key: string]: any }) => Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return async (handlerData: { [key: string]: any }): Promise => { + // We only capture complete fetch requests + if (!handlerData.endTimestamp) { + return; + } - if (handlerData.fetchData.url.match(/sentry_key/) && handlerData.fetchData.method === 'POST') { - // We will not create breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests) - return; - } + if (handlerData.fetchData.url.match(/sentry_key/) && handlerData.fetchData.method === 'POST') { + // We will not create breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests) + return; + } - if (handlerData.error) { - getCurrentHub().addBreadcrumb( - { - category: 'fetch', - data: handlerData.fetchData, - level: 'error', - type: 'http', - }, - { - data: handlerData.error, - input: handlerData.args, - }, - ); - } else { - getCurrentHub().addBreadcrumb( - { - category: 'fetch', - data: { - ...handlerData.fetchData, - status_code: handlerData.response.status, + if (options.captureRequestPayload) { + handlerData.fetchData.request_payload = handlerData.args[1] && handlerData.args[1].body; + } + + if (options.captureResponsePayload && handlerData.response) { + const response = handlerData.response.clone(); + handlerData.fetchData.response_payload = await response.text(); + } + + if (handlerData.error) { + getCurrentHub().addBreadcrumb( + { + category: 'fetch', + data: handlerData.fetchData, + level: 'error', + type: 'http', }, - type: 'http', - }, - { - input: handlerData.args, - response: handlerData.response, - }, - ); - } + { + data: handlerData.error, + input: handlerData.args, + }, + ); + } else { + getCurrentHub().addBreadcrumb( + { + category: 'fetch', + data: { + ...handlerData.fetchData, + status_code: handlerData.response.status, + }, + type: 'http', + }, + { + input: handlerData.args, + response: handlerData.response, + }, + ); + } + }; } /** diff --git a/packages/browser/test/integration/suites/breadcrumbs.js b/packages/browser/test/integration/suites/breadcrumbs.js index 5e5c2973efc7..1cf372f0381f 100644 --- a/packages/browser/test/integration/suites/breadcrumbs.js +++ b/packages/browser/test/integration/suites/breadcrumbs.js @@ -91,6 +91,7 @@ describe('breadcrumbs', function () { assert.equal(summary.breadcrumbs[0].category, 'xhr'); assert.equal(summary.breadcrumbs[0].data.method, 'POST'); assert.isUndefined(summary.breadcrumbs[0].data.input); + assert.isUndefined(summary.breadcrumbs[0].data.request_payload); assert.equal(summary.breadcrumbHints[0].input, '{"foo":"bar"}'); }); }); diff --git a/packages/replay/src/coreHandlers/handleFetch.ts b/packages/replay/src/coreHandlers/handleFetch.ts index 290a58d4531d..d259c1ee5d1c 100644 --- a/packages/replay/src/coreHandlers/handleFetch.ts +++ b/packages/replay/src/coreHandlers/handleFetch.ts @@ -7,6 +7,8 @@ interface FetchHandlerData { fetchData: { method: string; url: string; + request_payload?: string; + response_payload?: string; }; response: { type: string; @@ -26,6 +28,7 @@ export function handleFetch(handlerData: FetchHandlerData): null | ReplayPerform } const { startTimestamp, endTimestamp, fetchData, response } = handlerData; + const { method, request_payload: requestPayload, response_payload: responsePayload } = fetchData; return { type: 'resource.fetch', @@ -33,8 +36,10 @@ export function handleFetch(handlerData: FetchHandlerData): null | ReplayPerform end: endTimestamp / 1000, name: fetchData.url, data: { - method: fetchData.method, + method, statusCode: response.status, + requestPayload, + responsePayload, }, }; } diff --git a/packages/replay/src/coreHandlers/handleXhr.ts b/packages/replay/src/coreHandlers/handleXhr.ts index 406a4d3dc175..01128d4d3734 100644 --- a/packages/replay/src/coreHandlers/handleXhr.ts +++ b/packages/replay/src/coreHandlers/handleXhr.ts @@ -25,6 +25,8 @@ interface XhrHandlerData { xhr: SentryWrappedXMLHttpRequest; startTimestamp: number; endTimestamp?: number; + request_payload?: string; + response_payload?: string; } function handleXhr(handlerData: XhrHandlerData): ReplayPerformanceEntry | null { @@ -44,6 +46,7 @@ function handleXhr(handlerData: XhrHandlerData): ReplayPerformanceEntry | null { return null; } + const { request_payload: requestPayload, response_payload: responsePayload } = handlerData; const { method, url, status_code: statusCode } = handlerData.xhr.__sentry_xhr__ || {}; if (url === undefined) { @@ -62,6 +65,8 @@ function handleXhr(handlerData: XhrHandlerData): ReplayPerformanceEntry | null { data: { method, statusCode, + requestPayload, + responsePayload, }, }; } From 15f620be23ddef346d0d00f7d29ab0e3a2bfd1c8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 27 Feb 2023 15:41:44 +0100 Subject: [PATCH 2/5] add tests --- .../breadcrumbs/fetch/defaults/init.js | 14 +++++++ .../breadcrumbs/fetch/defaults/subject.js | 10 +++++ .../breadcrumbs/fetch/defaults/test.ts | 41 ++++++++++++++++++ .../breadcrumbs/fetch/requestPayload/init.js | 14 +++++++ .../fetch/requestPayload/subject.js | 10 +++++ .../breadcrumbs/fetch/requestPayload/test.ts | 42 +++++++++++++++++++ .../breadcrumbs/fetch/responsePayload/init.js | 14 +++++++ .../fetch/responsePayload/subject.js | 10 +++++ .../breadcrumbs/fetch/responsePayload/test.ts | 42 +++++++++++++++++++ .../browser/breadcrumbs/xhr/defaults/init.js | 14 +++++++ .../breadcrumbs/xhr/defaults/subject.js | 7 ++++ .../browser/breadcrumbs/xhr/defaults/test.ts | 41 ++++++++++++++++++ .../breadcrumbs/xhr/requestPayload/init.js | 14 +++++++ .../breadcrumbs/xhr/requestPayload/subject.js | 7 ++++ .../breadcrumbs/xhr/requestPayload/test.ts | 42 +++++++++++++++++++ .../breadcrumbs/xhr/responsePayload/init.js | 14 +++++++ .../xhr/responsePayload/subject.js | 7 ++++ .../breadcrumbs/xhr/responsePayload/test.ts | 42 +++++++++++++++++++ 18 files changed, 385 insertions(+) create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/init.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/subject.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/test.ts create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/init.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/subject.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/test.ts create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/init.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/subject.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/test.ts create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/init.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/subject.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/test.ts create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/init.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/subject.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/test.ts create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/init.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/subject.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/test.ts diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/init.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/init.js new file mode 100644 index 000000000000..f7e7c7a6f9d0 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; +import { Breadcrumbs } from '@sentry/browser'; + +window.Sentry = Sentry; +window.breadcrumbs = []; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Breadcrumbs()], + beforeBreadcrumb(breadcrumb) { + window.breadcrumbs.push(breadcrumb); + return breadcrumb; + }, +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/subject.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/subject.js new file mode 100644 index 000000000000..d313c69c7fbe --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/subject.js @@ -0,0 +1,10 @@ +fetch('http://localhost:7654/foo', { + method: 'POST', + body: '{"foo":"bar"}', +}) + .then(res => { + return res.json(); + }) + .then(json => { + // do something with the response + }); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/test.ts b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/test.ts new file mode 100644 index 000000000000..5588194c0720 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/defaults/test.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; +import type { Breadcrumb } from '@sentry/types'; + +import { sentryTest } from '../../../../../../utils/fixtures'; + +sentryTest('works with default options', async ({ getLocalTestPath, page }) => { + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + testApi: 'OK', + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const request = page.waitForRequest('**/foo'); + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await request; + + const breadcrumbs = await page.evaluate(() => { + return (window as unknown as Window & { breadcrumbs: Breadcrumb[] }).breadcrumbs; + }); + + expect(breadcrumbs).toEqual([ + { + category: 'fetch', + data: { + method: 'POST', + status_code: 200, + url: 'http://localhost:7654/foo', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ]); +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/init.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/init.js new file mode 100644 index 000000000000..fc3402315d8c --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; +import { Breadcrumbs } from '@sentry/browser'; + +window.Sentry = Sentry; +window.breadcrumbs = []; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Breadcrumbs({ captureRequestPayload: true })], + beforeBreadcrumb(breadcrumb) { + window.breadcrumbs.push(breadcrumb); + return breadcrumb; + }, +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/subject.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/subject.js new file mode 100644 index 000000000000..d313c69c7fbe --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/subject.js @@ -0,0 +1,10 @@ +fetch('http://localhost:7654/foo', { + method: 'POST', + body: '{"foo":"bar"}', +}) + .then(res => { + return res.json(); + }) + .then(json => { + // do something with the response + }); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/test.ts b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/test.ts new file mode 100644 index 000000000000..b2bf884c9041 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/requestPayload/test.ts @@ -0,0 +1,42 @@ +import { expect } from '@playwright/test'; +import type { Breadcrumb } from '@sentry/types'; + +import { sentryTest } from '../../../../../../utils/fixtures'; + +sentryTest('captures request_payload breadcrumb if configured', async ({ getLocalTestPath, page }) => { + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + testApi: 'OK', + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const request = page.waitForRequest('**/foo'); + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await request; + + const breadcrumbs = await page.evaluate(() => { + return (window as unknown as Window & { breadcrumbs: Breadcrumb[] }).breadcrumbs; + }); + + expect(breadcrumbs).toEqual([ + { + category: 'fetch', + data: { + method: 'POST', + request_payload: '{"foo":"bar"}', + status_code: 200, + url: 'http://localhost:7654/foo', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ]); +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/init.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/init.js new file mode 100644 index 000000000000..67a8757ad299 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; +import { Breadcrumbs } from '@sentry/browser'; + +window.Sentry = Sentry; +window.breadcrumbs = []; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Breadcrumbs({ captureResponsePayload: true })], + beforeBreadcrumb(breadcrumb) { + window.breadcrumbs.push(breadcrumb); + return breadcrumb; + }, +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/subject.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/subject.js new file mode 100644 index 000000000000..d313c69c7fbe --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/subject.js @@ -0,0 +1,10 @@ +fetch('http://localhost:7654/foo', { + method: 'POST', + body: '{"foo":"bar"}', +}) + .then(res => { + return res.json(); + }) + .then(json => { + // do something with the response + }); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/test.ts b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/test.ts new file mode 100644 index 000000000000..327b4d14baa1 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/responsePayload/test.ts @@ -0,0 +1,42 @@ +import { expect } from '@playwright/test'; +import type { Breadcrumb } from '@sentry/types'; + +import { sentryTest } from '../../../../../../utils/fixtures'; + +sentryTest('captures response_payload breadcrumb if configured', async ({ getLocalTestPath, page }) => { + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + testApi: 'OK', + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const request = page.waitForRequest('**/foo'); + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await request; + + const breadcrumbs = await page.evaluate(() => { + return (window as unknown as Window & { breadcrumbs: Breadcrumb[] }).breadcrumbs; + }); + + expect(breadcrumbs).toEqual([ + { + category: 'fetch', + data: { + method: 'POST', + response_payload: '{"testApi":"OK"}', + status_code: 200, + url: 'http://localhost:7654/foo', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ]); +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/init.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/init.js new file mode 100644 index 000000000000..f7e7c7a6f9d0 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; +import { Breadcrumbs } from '@sentry/browser'; + +window.Sentry = Sentry; +window.breadcrumbs = []; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Breadcrumbs()], + beforeBreadcrumb(breadcrumb) { + window.breadcrumbs.push(breadcrumb); + return breadcrumb; + }, +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/subject.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/subject.js new file mode 100644 index 000000000000..f7b0894901a9 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/subject.js @@ -0,0 +1,7 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('POST', 'http://localhost:7654/foo', true); +xhr.setRequestHeader('Accept', 'application/json'); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.setRequestHeader('Cache', 'no-cache'); +xhr.send('{"foo":"bar"}'); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/test.ts b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/test.ts new file mode 100644 index 000000000000..0edfe2c2d4dd --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/defaults/test.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; +import type { Breadcrumb } from '@sentry/types'; + +import { sentryTest } from '../../../../../../utils/fixtures'; + +sentryTest('works with default options', async ({ getLocalTestPath, page }) => { + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + testApi: 'OK', + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const request = page.waitForRequest('**/foo'); + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await request; + + const breadcrumbs = await page.evaluate(() => { + return (window as unknown as Window & { breadcrumbs: Breadcrumb[] }).breadcrumbs; + }); + + expect(breadcrumbs).toEqual([ + { + category: 'xhr', + data: { + method: 'POST', + status_code: 200, + url: 'http://localhost:7654/foo', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ]); +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/init.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/init.js new file mode 100644 index 000000000000..fc3402315d8c --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; +import { Breadcrumbs } from '@sentry/browser'; + +window.Sentry = Sentry; +window.breadcrumbs = []; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Breadcrumbs({ captureRequestPayload: true })], + beforeBreadcrumb(breadcrumb) { + window.breadcrumbs.push(breadcrumb); + return breadcrumb; + }, +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/subject.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/subject.js new file mode 100644 index 000000000000..f7b0894901a9 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/subject.js @@ -0,0 +1,7 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('POST', 'http://localhost:7654/foo', true); +xhr.setRequestHeader('Accept', 'application/json'); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.setRequestHeader('Cache', 'no-cache'); +xhr.send('{"foo":"bar"}'); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/test.ts b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/test.ts new file mode 100644 index 000000000000..0c89ff4d1a50 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/requestPayload/test.ts @@ -0,0 +1,42 @@ +import { expect } from '@playwright/test'; +import type { Breadcrumb } from '@sentry/types'; + +import { sentryTest } from '../../../../../../utils/fixtures'; + +sentryTest('captures request_payload breadcrumb if configured', async ({ getLocalTestPath, page }) => { + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + testApi: 'OK', + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const request = page.waitForRequest('**/foo'); + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await request; + + const breadcrumbs = await page.evaluate(() => { + return (window as unknown as Window & { breadcrumbs: Breadcrumb[] }).breadcrumbs; + }); + + expect(breadcrumbs).toEqual([ + { + category: 'xhr', + data: { + method: 'POST', + request_payload: '{"foo":"bar"}', + status_code: 200, + url: 'http://localhost:7654/foo', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ]); +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/init.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/init.js new file mode 100644 index 000000000000..67a8757ad299 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; +import { Breadcrumbs } from '@sentry/browser'; + +window.Sentry = Sentry; +window.breadcrumbs = []; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Breadcrumbs({ captureResponsePayload: true })], + beforeBreadcrumb(breadcrumb) { + window.breadcrumbs.push(breadcrumb); + return breadcrumb; + }, +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/subject.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/subject.js new file mode 100644 index 000000000000..f7b0894901a9 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/subject.js @@ -0,0 +1,7 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('POST', 'http://localhost:7654/foo', true); +xhr.setRequestHeader('Accept', 'application/json'); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.setRequestHeader('Cache', 'no-cache'); +xhr.send('{"foo":"bar"}'); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/test.ts b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/test.ts new file mode 100644 index 000000000000..eaa2ada32b40 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/responsePayload/test.ts @@ -0,0 +1,42 @@ +import { expect } from '@playwright/test'; +import type { Breadcrumb } from '@sentry/types'; + +import { sentryTest } from '../../../../../../utils/fixtures'; + +sentryTest('captures response_payload breadcrumb if configured', async ({ getLocalTestPath, page }) => { + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + testApi: 'OK', + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const request = page.waitForRequest('**/foo'); + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await request; + + const breadcrumbs = await page.evaluate(() => { + return (window as unknown as Window & { breadcrumbs: Breadcrumb[] }).breadcrumbs; + }); + + expect(breadcrumbs).toEqual([ + { + category: 'xhr', + data: { + method: 'POST', + response_payload: '{"testApi":"OK"}', + status_code: 200, + url: 'http://localhost:7654/foo', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ]); +}); From 67467f39b59cba0c684f265c172452fa252ddd96 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 27 Feb 2023 15:50:33 +0100 Subject: [PATCH 3/5] add try-catch --- packages/browser/src/integrations/breadcrumbs.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 678144f70849..4aebf2cbfcec 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -287,8 +287,12 @@ function _fetchBreadcrumb(options: XhrFetchOptions): (handlerData: { [key: strin } if (options.captureResponsePayload && handlerData.response) { - const response = handlerData.response.clone(); - handlerData.fetchData.response_payload = await response.text(); + try { + const response = handlerData.response.clone(); + handlerData.fetchData.response_payload = await response.text(); + } catch (error) { + // if something happens here, we don't want to break the whole breadcrumb + } } if (handlerData.error) { From 09c6e9c88154e2c02e9d3189203982e3f1a9021f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 27 Feb 2023 15:51:40 +0100 Subject: [PATCH 4/5] add comment --- packages/browser/src/integrations/breadcrumbs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 4aebf2cbfcec..cab9f093af3a 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -288,6 +288,7 @@ function _fetchBreadcrumb(options: XhrFetchOptions): (handlerData: { [key: strin if (options.captureResponsePayload && handlerData.response) { try { + // We need to clone() this in order to avoid consuming the original response body const response = handlerData.response.clone(); handlerData.fetchData.response_payload = await response.text(); } catch (error) { From 67d468f76a56def3773414c88355ff36a09f1643 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 27 Feb 2023 15:56:51 +0100 Subject: [PATCH 5/5] handle empty body --- .../browser/src/integrations/breadcrumbs.ts | 13 ++++--- .../breadcrumbs/fetch/withoutBody/init.js | 19 ++++++++++ .../breadcrumbs/fetch/withoutBody/subject.js | 7 ++++ .../breadcrumbs/fetch/withoutBody/test.ts | 38 +++++++++++++++++++ .../breadcrumbs/xhr/withoutBody/init.js | 19 ++++++++++ .../breadcrumbs/xhr/withoutBody/subject.js | 7 ++++ .../breadcrumbs/xhr/withoutBody/test.ts | 38 +++++++++++++++++++ 7 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/init.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/subject.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/test.ts create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/init.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/subject.js create mode 100644 packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/test.ts diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index cab9f093af3a..ccd14d1755bf 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -241,11 +241,11 @@ function _xhrBreadcrumb(options: XhrFetchOptions): (handlerData: { [key: string] status_code, }; - if (options.captureRequestPayload) { + if (options.captureRequestPayload && body) { xhrData.request_payload = body; } - if (options.captureResponsePayload) { + if (options.captureResponsePayload && handlerData.xhr.responseText) { xhrData.response_payload = handlerData.xhr.responseText; } @@ -282,15 +282,18 @@ function _fetchBreadcrumb(options: XhrFetchOptions): (handlerData: { [key: strin return; } - if (options.captureRequestPayload) { - handlerData.fetchData.request_payload = handlerData.args[1] && handlerData.args[1].body; + if (options.captureRequestPayload && handlerData.args[1] && handlerData.args[1].body) { + handlerData.fetchData.request_payload = handlerData.args[1].body; } if (options.captureResponsePayload && handlerData.response) { try { // We need to clone() this in order to avoid consuming the original response body const response = handlerData.response.clone(); - handlerData.fetchData.response_payload = await response.text(); + const text = await response.text(); + if (text) { + handlerData.fetchData.response_payload = text; + } } catch (error) { // if something happens here, we don't want to break the whole breadcrumb } diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/init.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/init.js new file mode 100644 index 000000000000..7cf5964226b3 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; +import { Breadcrumbs } from '@sentry/browser'; + +window.Sentry = Sentry; +window.breadcrumbs = []; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + new Breadcrumbs({ + captureRequestPayload: true, + captureResponsePayload: true, + }), + ], + beforeBreadcrumb(breadcrumb) { + window.breadcrumbs.push(breadcrumb); + return breadcrumb; + }, +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/subject.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/subject.js new file mode 100644 index 000000000000..1c6a9fa47eb7 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/subject.js @@ -0,0 +1,7 @@ +fetch('http://localhost:7654/foo') + .then(res => { + return res.json(); + }) + .then(json => { + // do something with the response + }); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/test.ts b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/test.ts new file mode 100644 index 000000000000..f6b70170b7f1 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/fetch/withoutBody/test.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; +import type { Breadcrumb } from '@sentry/types'; + +import { sentryTest } from '../../../../../../utils/fixtures'; + +sentryTest('works without a request/response body', async ({ getLocalTestPath, page }) => { + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const request = page.waitForRequest('**/foo'); + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await request; + + const breadcrumbs = await page.evaluate(() => { + return (window as unknown as Window & { breadcrumbs: Breadcrumb[] }).breadcrumbs; + }); + + expect(breadcrumbs).toEqual([ + { + category: 'fetch', + data: { + method: 'GET', + status_code: 200, + url: 'http://localhost:7654/foo', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ]); +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/init.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/init.js new file mode 100644 index 000000000000..7cf5964226b3 --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; +import { Breadcrumbs } from '@sentry/browser'; + +window.Sentry = Sentry; +window.breadcrumbs = []; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + new Breadcrumbs({ + captureRequestPayload: true, + captureResponsePayload: true, + }), + ], + beforeBreadcrumb(breadcrumb) { + window.breadcrumbs.push(breadcrumb); + return breadcrumb; + }, +}); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/subject.js b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/subject.js new file mode 100644 index 000000000000..3eb5fcc1b97e --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/subject.js @@ -0,0 +1,7 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('GET', 'http://localhost:7654/foo', true); +xhr.setRequestHeader('Accept', 'application/json'); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.setRequestHeader('Cache', 'no-cache'); +xhr.send(); diff --git a/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/test.ts b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/test.ts new file mode 100644 index 000000000000..51134e3e6ecd --- /dev/null +++ b/packages/integration-tests/suites/integrations/browser/breadcrumbs/xhr/withoutBody/test.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; +import type { Breadcrumb } from '@sentry/types'; + +import { sentryTest } from '../../../../../../utils/fixtures'; + +sentryTest('works without a request/response body', async ({ getLocalTestPath, page }) => { + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const request = page.waitForRequest('**/foo'); + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await request; + + const breadcrumbs = await page.evaluate(() => { + return (window as unknown as Window & { breadcrumbs: Breadcrumb[] }).breadcrumbs; + }); + + expect(breadcrumbs).toEqual([ + { + category: 'xhr', + data: { + method: 'GET', + status_code: 200, + url: 'http://localhost:7654/foo', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ]); +});