diff --git a/src/plugins/event-plugins/FetchPlugin.ts b/src/plugins/event-plugins/FetchPlugin.ts index af04eb9f..ee023a91 100644 --- a/src/plugins/event-plugins/FetchPlugin.ts +++ b/src/plugins/event-plugins/FetchPlugin.ts @@ -224,10 +224,16 @@ export class FetchPlugin extends MonkeyPatched { init?: RequestInit ): HttpEvent => { const request = input as Request; + const url = resourceToUrlString(input); + const normalizedUrl = + typeof this.config.eventURLNormalizer === 'function' + ? this.config.eventURLNormalizer(url) + : url; + return { version: '1.0.0', request: { - url: resourceToUrlString(input), + url: normalizedUrl, method: init?.method ? init.method : request.method diff --git a/src/plugins/event-plugins/XhrPlugin.ts b/src/plugins/event-plugins/XhrPlugin.ts index d2693e66..df635c00 100644 --- a/src/plugins/event-plugins/XhrPlugin.ts +++ b/src/plugins/event-plugins/XhrPlugin.ts @@ -250,14 +250,27 @@ export class XhrPlugin extends MonkeyPatched { return status >= 200 && status < 300; } + private createHttpEvent(xhrDetails: XhrDetails): HttpEvent { + const url = xhrDetails.url; + + const normalizedUrl = + typeof this.config.eventURLNormalizer === 'function' + ? this.config.eventURLNormalizer(url) + : url; + + return { + version: '1.0.0', + request: { method: xhrDetails.method, url: normalizedUrl } + }; + } + private recordHttpEventWithResponse( xhrDetails: XhrDetails, xhr: XMLHttpRequest ) { this.xhrMap.delete(xhr); const httpEvent: HttpEvent = { - version: '1.0.0', - request: { method: xhrDetails.method, url: xhrDetails.url }, + ...this.createHttpEvent(xhrDetails), response: { status: xhr.status, statusText: xhr.statusText } }; if (this.isTracingEnabled()) { @@ -276,8 +289,7 @@ export class XhrPlugin extends MonkeyPatched { ) { this.xhrMap.delete(xhr); const httpEvent: HttpEvent = { - version: '1.0.0', - request: { method: xhrDetails.method, url: xhrDetails.url }, + ...this.createHttpEvent(xhrDetails), error: errorEventToJsErrorEvent( { type: 'error', diff --git a/src/plugins/event-plugins/__tests__/FetchPlugin.test.ts b/src/plugins/event-plugins/__tests__/FetchPlugin.test.ts index 0cae163c..9225b71f 100644 --- a/src/plugins/event-plugins/__tests__/FetchPlugin.test.ts +++ b/src/plugins/event-plugins/__tests__/FetchPlugin.test.ts @@ -1094,4 +1094,37 @@ describe('FetchPlugin tests', () => { expect(record).toHaveBeenCalledTimes(1); expect(record.mock.calls[0][0]).toEqual(XRAY_TRACE_EVENT_TYPE); }); + + test('when eventURLNormalizer is present then HTTP event URL is modified by it', async () => { + // Init + const URL_NORMALIZED = 'example.com'; + const config: Partial = { + recordAllRequests: true, + eventURLNormalizer: () => { + return URL_NORMALIZED; + } + }; + + const plugin: FetchPlugin = new FetchPlugin(config); + plugin.load(xRayOffContext); + + // Run + await fetch(URL); + plugin.disable(); + + // Assert + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(record).toHaveBeenCalledTimes(1); + expect(record.mock.calls[0][0]).toEqual(HTTP_EVENT_TYPE); + expect(record.mock.calls[0][1]).toMatchObject({ + request: { + method: 'GET', + url: URL_NORMALIZED + }, + response: { + status: 200, + statusText: 'OK' + } + }); + }); }); diff --git a/src/plugins/event-plugins/__tests__/XhrPlugin.test.ts b/src/plugins/event-plugins/__tests__/XhrPlugin.test.ts index d4046b18..362fa988 100644 --- a/src/plugins/event-plugins/__tests__/XhrPlugin.test.ts +++ b/src/plugins/event-plugins/__tests__/XhrPlugin.test.ts @@ -1311,4 +1311,87 @@ describe('XhrPlugin tests', () => { expect(record).toHaveBeenCalledTimes(1); expect(record.mock.calls[0][0]).toEqual(XRAY_TRACE_EVENT_TYPE); }); + + test('when eventURLNormalizer is present then HTTP event URL is modified by it - On Success', async () => { + // Init + const URL_NORMALIZED = 'example.com'; + const config: Partial = { + recordAllRequests: true, + eventURLNormalizer: () => { + return URL_NORMALIZED; + } + }; + + mock.get(/.*/, { + body: JSON.stringify({ message: 'Hello World!' }) + }); + + const plugin: XhrPlugin = new XhrPlugin(config); + plugin.load(xRayOffContext); + + // Run + const xhr = new XMLHttpRequest(); + xhr.open('GET', './response.json', true); + xhr.send(); + + // Yield to the event queue so the event listeners can run + await new Promise((resolve) => setTimeout(resolve, 0)); + + plugin.disable(); + + // Assert + expect(record).toHaveBeenCalledTimes(1); + expect(record.mock.calls[0][0]).toEqual(HTTP_EVENT_TYPE); + expect(record.mock.calls[0][1]).toMatchObject({ + request: { + method: 'GET', + url: URL_NORMALIZED + }, + response: { + status: 200, + statusText: 'OK' + } + }); + }); + + test('when eventURLNormalizer is present then HTTP event URL is modified by it - On Error', async () => { + // Init + const URL_NORMALIZED = 'example.com'; + const config: Partial = { + recordAllRequests: true, + eventURLNormalizer: () => { + return URL_NORMALIZED; + } + }; + + mock.get(/.*/, () => Promise.reject(new Error('Network failure'))); + + const plugin: XhrPlugin = new XhrPlugin(config); + plugin.load(xRayOffContext); + + // Run + const xhr = new XMLHttpRequest(); + xhr.open('GET', './response.json', true); + xhr.send(); + + // Yield to the event queue so the event listeners can run + await new Promise((resolve) => setTimeout(resolve, 0)); + + plugin.disable(); + + // Assert + expect(record).toHaveBeenCalledTimes(1); + expect(record.mock.calls[0][0]).toEqual(HTTP_EVENT_TYPE); + expect(record.mock.calls[0][1]).toMatchObject({ + request: { + method: 'GET', + url: URL_NORMALIZED + }, + error: { + version: '1.0.0', + type: 'XMLHttpRequest error', + message: '0' + } + }); + }); }); diff --git a/src/plugins/utils/http-utils.ts b/src/plugins/utils/http-utils.ts index bfd491b3..5a759940 100644 --- a/src/plugins/utils/http-utils.ts +++ b/src/plugins/utils/http-utils.ts @@ -28,6 +28,24 @@ export type HttpPluginConfig = { // the X-Amzn-Trace-Id header should test their applications before enabling // it in a production environment. addXRayTraceIdHeader: boolean | RegExp[]; + /** + * Use this function to normalize URLs before recording the HTTP Event in RUM. + * This is useful when you want to obfuscate sensitive information in the URL or group URLs with similar patterns together (i.e. path parameters) + * Or even, to have a clearer naming for known services. + * + * Example use cases: + * + * @example normalizing path params + * /users/1234 + * /users/5678 + * can be normalized to + * /users/{userId} + * @example + * /users/1234 + * can be normalized to + * GetUsersById + */ + eventURLNormalizer?: (url: string) => string; }; export const isTraceIdHeaderEnabled = (