diff --git a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts index 59324031e336..20fa00a633eb 100644 --- a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts +++ b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts @@ -31,13 +31,19 @@ export function handleNetworkBreadcrumbs(replay: ReplayContainer): void { try { const textEncoder = new TextEncoder(); - const { networkDetailAllowUrls, networkCaptureBodies, networkRequestHeaders, networkResponseHeaders } = - replay.getOptions(); + const { + networkDetailAllowUrls, + networkDetailDenyUrls, + networkCaptureBodies, + networkRequestHeaders, + networkResponseHeaders, + } = replay.getOptions(); const options: ExtendedNetworkBreadcrumbsOptions = { replay, textEncoder, networkDetailAllowUrls, + networkDetailDenyUrls, networkCaptureBodies, networkRequestHeaders, networkResponseHeaders, diff --git a/packages/replay/src/coreHandlers/util/fetchUtils.ts b/packages/replay/src/coreHandlers/util/fetchUtils.ts index 9411332d0197..491fe727b1b8 100644 --- a/packages/replay/src/coreHandlers/util/fetchUtils.ts +++ b/packages/replay/src/coreHandlers/util/fetchUtils.ts @@ -85,7 +85,8 @@ async function _prepareFetchData( response_body_size: responseBodySize, } = breadcrumb.data; - const captureDetails = urlMatches(url, options.networkDetailAllowUrls); + const captureDetails = + urlMatches(url, options.networkDetailAllowUrls) && !urlMatches(url, options.networkDetailDenyUrls); const request = captureDetails ? _getRequestInfo(options, hint.input, requestBodySize) diff --git a/packages/replay/src/coreHandlers/util/xhrUtils.ts b/packages/replay/src/coreHandlers/util/xhrUtils.ts index dfb1ccaf3ffc..638af2cbdb5c 100644 --- a/packages/replay/src/coreHandlers/util/xhrUtils.ts +++ b/packages/replay/src/coreHandlers/util/xhrUtils.ts @@ -78,7 +78,7 @@ function _prepareXhrData( return null; } - if (!urlMatches(url, options.networkDetailAllowUrls)) { + if (!urlMatches(url, options.networkDetailAllowUrls) || urlMatches(url, options.networkDetailDenyUrls)) { const request = buildSkippedNetworkRequestOrResponse(requestBodySize); const response = buildSkippedNetworkRequestOrResponse(responseBodySize); return { diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 2baf117b5c38..2e795829894b 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -67,6 +67,7 @@ export class Replay implements Integration { slowClickIgnoreSelectors = [], networkDetailAllowUrls = [], + networkDetailDenyUrls = [], networkCaptureBodies = true, networkRequestHeaders = [], networkResponseHeaders = [], @@ -138,6 +139,7 @@ export class Replay implements Integration { slowClickTimeout, slowClickIgnoreSelectors, networkDetailAllowUrls, + networkDetailDenyUrls, networkCaptureBodies, networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders), networkResponseHeaders: _getMergedNetworkHeaders(networkResponseHeaders), diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index e2cfddd0f525..494d9408f292 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -70,23 +70,32 @@ export interface ReplayNetworkOptions { */ networkDetailAllowUrls: (string | RegExp)[]; + /** + * Deny request/response details for XHR/Fetch requests that match the given URLs. + * The URLs can be strings or regular expressions. + * When provided a string, we will deny any URL that contains the given string. + * You can use a Regex to handle exact matches or more complex matching. + * URLs matching these patterns will not have bodies & additional headers captured. + */ + networkDetailDenyUrls: (string | RegExp)[]; + /** * If request & response bodies should be captured. - * Only applies to URLs matched by `networkDetailAllowUrls`. + * Only applies to URLs matched by `networkDetailAllowUrls` and not matched by `networkDetailDenyUrls`. * Defaults to true. */ networkCaptureBodies: boolean; /** * Capture the following request headers, in addition to the default ones. - * Only applies to URLs matched by `networkDetailAllowUrls`. + * Only applies to URLs matched by `networkDetailAllowUrls` and not matched by `networkDetailDenyUrls`. * Any headers defined here will be captured in addition to the default headers. */ networkRequestHeaders: string[]; /** * Capture the following response headers, in addition to the default ones. - * Only applies to URLs matched by `networkDetailAllowUrls`. + * Only applies to URLs matched by `networkDetailAllowUrls` and not matched by `networkDetailDenyUrls`. * Any headers defined here will be captured in addition to the default headers. */ networkResponseHeaders: string[]; diff --git a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts index 825c6e4eb452..c0ad9ef59b49 100644 --- a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts +++ b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts @@ -63,6 +63,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => { textEncoder: new TextEncoder(), replay: setupReplayContainer(), networkDetailAllowUrls: ['https://example.com'], + networkDetailDenyUrls: ['http://localhost:8080'], networkCaptureBodies: false, networkRequestHeaders: ['content-type', 'accept', 'x-custom-header'], networkResponseHeaders: ['content-type', 'accept', 'x-custom-header'], @@ -1382,5 +1383,166 @@ other-header: test`; ]); }); }); + + describe.each([ + ['exact string match', 'https://example.com/foo'], + ['partial string match', 'https://example.com/bar/what'], + ['exact regex match', 'http://example.com/exact'], + ['partial regex match', 'http://example.com/partial/string'], + ])('matching URL %s', (_label, url) => { + it('correctly deny URL for fetch request', async () => { + options.networkDetailDenyUrls = [ + 'https://example.com/foo', + 'com/bar', + /^http:\/\/example.com\/exact$/, + /^http:\/\/example.com\/partial/, + ]; + + const breadcrumb: Breadcrumb = { + category: 'fetch', + data: { + method: 'GET', + url, + status_code: 200, + }, + }; + + const mockResponse = getMockResponse('13', 'test response'); + + const hint: FetchBreadcrumbHint = { + input: ['GET', { body: 'test input' }], + response: mockResponse, + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + beforeAddNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'fetch', + data: { + method: 'GET', + request_body_size: 10, + response_body_size: 13, + status_code: 200, + url, + }, + }); + + await waitForReplayEventBuffer(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + data: { + payload: { + data: { + method: 'GET', + request: { + _meta: { + warnings: ['URL_SKIPPED'], + }, + headers: {}, + size: 10, + }, + response: { + _meta: { + warnings: ['URL_SKIPPED'], + }, + headers: {}, + size: 13, + }, + statusCode: 200, + }, + description: url, + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.fetch', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + tag: 'performanceSpan', + }, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + type: 5, + }, + ]); + }); + + it('correctly deny URL for xhr request', async () => { + options.networkDetailDenyUrls = [ + 'https://example.com/foo', + 'com/bar', + /^http:\/\/example.com\/exact$/, + /^http:\/\/example.com\/partial/, + ]; + + const breadcrumb: Breadcrumb = { + category: 'xhr', + data: { + method: 'GET', + url, + status_code: 200, + }, + }; + const xhr = new XMLHttpRequest(); + Object.defineProperty(xhr, 'response', { + value: 'test response', + }); + Object.defineProperty(xhr, 'responseText', { + value: 'test response', + }); + const hint: XhrBreadcrumbHint = { + xhr, + input: 'test input', + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + beforeAddNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'xhr', + data: { + method: 'GET', + request_body_size: 10, + response_body_size: 13, + status_code: 200, + url, + }, + }); + + await waitForReplayEventBuffer(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + data: { + payload: { + data: { + method: 'GET', + request: { + _meta: { + warnings: ['URL_SKIPPED'], + }, + headers: {}, + size: 10, + }, + response: { + _meta: { + warnings: ['URL_SKIPPED'], + }, + headers: {}, + size: 13, + }, + statusCode: 200, + }, + description: url, + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.xhr', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + tag: 'performanceSpan', + }, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + type: 5, + }, + ]); + }); + }); }); }); diff --git a/packages/replay/test/utils/setupReplayContainer.ts b/packages/replay/test/utils/setupReplayContainer.ts index 0237afddb538..e2a49052a799 100644 --- a/packages/replay/test/utils/setupReplayContainer.ts +++ b/packages/replay/test/utils/setupReplayContainer.ts @@ -12,6 +12,7 @@ const DEFAULT_OPTIONS = { useCompression: false, blockAllMedia: true, networkDetailAllowUrls: [], + networkDetailDenyUrls: [], networkCaptureBodies: true, networkRequestHeaders: [], networkResponseHeaders: [],