From 67ed7b3d07a5bea26367e1005aeec3edf3c57367 Mon Sep 17 00:00:00 2001 From: Billy Date: Fri, 15 Mar 2024 15:14:47 -0700 Subject: [PATCH 1/7] feat: disableOnFail --- src/dispatch/Dispatch.ts | 4 +- src/dispatch/__tests__/Dispatch.test.ts | 46 ++++++++++++++++++- src/orchestration/Orchestration.ts | 2 + .../__tests__/Orchestration.test.ts | 1 + 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/dispatch/Dispatch.ts b/src/dispatch/Dispatch.ts index b1682367..27b3d7b6 100644 --- a/src/dispatch/Dispatch.ts +++ b/src/dispatch/Dispatch.ts @@ -235,7 +235,9 @@ export class Dispatch { // The handler has run out of retries. We adhere to our convention to // fail safe by disabling dispatch. This ensures that we will not // continue to attempt requests when the problem is not recoverable. - this.disable(); + if (this.config.disableOnFail) { + this.disable(); + } throw e; }; diff --git a/src/dispatch/__tests__/Dispatch.test.ts b/src/dispatch/__tests__/Dispatch.test.ts index ca67ba97..2b16b7d1 100644 --- a/src/dispatch/__tests__/Dispatch.test.ts +++ b/src/dispatch/__tests__/Dispatch.test.ts @@ -419,7 +419,7 @@ describe('Dispatch tests', () => { ); }); - test('when a fetch request is rejected then dispatch is disabled', async () => { + test('when a fetch request is rejected and disableOnFaile=true then dispatch is disabled', async () => { // Init const ERROR = 'Something went wrong.'; const sendFetch = jest.fn(() => Promise.reject(ERROR)); @@ -438,7 +438,11 @@ describe('Dispatch tests', () => { eventCache, { ...DEFAULT_CONFIG, - ...{ dispatchInterval: Utils.AUTO_DISPATCH_OFF, retries: 0 } + ...{ + dispatchInterval: Utils.AUTO_DISPATCH_OFF, + retries: 0, + disableOnFail: true + } } ); dispatch.setAwsCredentials(Utils.createAwsCredentials()); @@ -449,6 +453,44 @@ describe('Dispatch tests', () => { // Assert await expect(dispatch.dispatchFetch()).resolves.toEqual(undefined); + expect((dispatch as unknown as any).enabled).toBe(false); + }); + + test('when a fetch request is rejected and disableOnFail=false then dispatch is not disabled', async () => { + // Init + const ERROR = 'Something went wrong.'; + const sendFetch = jest.fn(() => Promise.reject(ERROR)); + (DataPlaneClient as any).mockImplementation(() => { + return { + sendFetch + }; + }); + + const eventCache: EventCache = + Utils.createDefaultEventCacheWithEvents(); + + const dispatch = new Dispatch( + Utils.AWS_RUM_REGION, + Utils.AWS_RUM_ENDPOINT, + eventCache, + { + ...DEFAULT_CONFIG, + ...{ + dispatchInterval: Utils.AUTO_DISPATCH_OFF, + retries: 0, + disableOnFail: false + } + } + ); + dispatch.setAwsCredentials(Utils.createAwsCredentials()); + + // Run + await expect(dispatch.dispatchFetch()).rejects.toEqual(ERROR); + eventCache.recordEvent('com.amazon.rum.event1', {}); + + // Assert + await expect(dispatch.dispatchFetch()).rejects.toEqual(ERROR); + expect((dispatch as unknown as any).enabled).toBe(true); }); test('when signing is disabled then credentials are not needed for dispatch', async () => { diff --git a/src/orchestration/Orchestration.ts b/src/orchestration/Orchestration.ts index 4155bbd7..9cc88430 100644 --- a/src/orchestration/Orchestration.ts +++ b/src/orchestration/Orchestration.ts @@ -72,6 +72,7 @@ export const defaultConfig = (cookieAttributes: CookieAttributes): Config => { client: INSTALL_MODULE, cookieAttributes, disableAutoPageView: false, + disableOnFail: true, dispatchInterval: 5 * 1000, enableRumClient: true, enableXRay: false, @@ -115,6 +116,7 @@ export interface Config { cookieAttributes: CookieAttributes; sessionAttributes: { [k: string]: string | number | boolean }; disableAutoPageView: boolean; + disableOnFail: boolean; dispatchInterval: number; enableRumClient: boolean; enableXRay: boolean; diff --git a/src/orchestration/__tests__/Orchestration.test.ts b/src/orchestration/__tests__/Orchestration.test.ts index 429258f1..5c59bbe3 100644 --- a/src/orchestration/__tests__/Orchestration.test.ts +++ b/src/orchestration/__tests__/Orchestration.test.ts @@ -175,6 +175,7 @@ describe('Orchestration tests', () => { sessionAttributes: {}, telemetries: [], disableAutoPageView: false, + disableOnFail: true, dispatchInterval: 5000, enableXRay: false, endpoint: 'https://dataplane.rum.us-west-2.amazonaws.com', From ec1f74faf92f3033020234fdbbfffb89e35c0083 Mon Sep 17 00:00:00 2001 From: Billy Date: Fri, 15 Mar 2024 15:17:43 -0700 Subject: [PATCH 2/7] doc: add config doc --- docs/configuration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index ba9d9a35..195260db 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -24,6 +24,7 @@ For example, the config object may look similar to the following: | cookieAttributes | [CookieAttributes](#cookieattributes) | `{ domain: window.location.hostname, path: '/', sameSite: 'Strict', secure: true, unique: false } ` | Cookie attributes are applied to all cookies stored by the web client, including `cwr_s` and `cwr_u`. | | sessionAttributes | [MetadataAttributes](#metadataattributes) | `{}` | Session attributes will be added the metadata of all events in the session.| | disableAutoPageView | Boolean | `false` | When this field is `false`, the web client will automatically record page views.

By default, the web client records page views when (1) the page first loads and (2) the browser's [history API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) is called. The page ID is `window.location.pathname`.

In some cases, the web client's instrumentation will not record the desired page ID. In this case, the web client's page view automation must be disabled using the `disableAutoPageView` configuration, and the application must be instrumented to record page views using the `recordPageView` command. | +| disableOnFail | Boolean | `true` | When this field is `true`, the web client will disable itself when the PutRumEvents request fails and all retries have been exhausted. | | enableRumClient | Boolean | `true` | When this field is `true`, the web client will record and dispatch RUM events. | | enableXRay | Boolean | `false` | When this field is `true` **and** the `http` telemetry is used, the web client will record X-Ray traces for HTTP requests.

See the [HTTP telemetry configuration](#http) for more information, including how to connect client-side and server-side traces. | | endpoint | String | `'https://dataplane.rum.[region].amazonaws.com'`

`'https://[restapi_id].execute-api.[region].amazonaws.com/[stage_name]/'` | The URL of the CloudWatch RUM API where data will be sent.

You may include a path prefix like `/stage_name/` in the endpoint URL if there is a proxy between your web application and CloudWatch RUM. | @@ -210,4 +211,4 @@ telemetries: [ } ], ] -``` \ No newline at end of file +``` From a48455c6cb05a63979bcf25d12b4c13652f13685 Mon Sep 17 00:00:00 2001 From: Billy Date: Tue, 19 Mar 2024 18:44:16 -0700 Subject: [PATCH 3/7] Revert "doc: add config doc" This reverts commit ec1f74faf92f3033020234fdbbfffb89e35c0083. --- docs/configuration.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 195260db..ba9d9a35 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -24,7 +24,6 @@ For example, the config object may look similar to the following: | cookieAttributes | [CookieAttributes](#cookieattributes) | `{ domain: window.location.hostname, path: '/', sameSite: 'Strict', secure: true, unique: false } ` | Cookie attributes are applied to all cookies stored by the web client, including `cwr_s` and `cwr_u`. | | sessionAttributes | [MetadataAttributes](#metadataattributes) | `{}` | Session attributes will be added the metadata of all events in the session.| | disableAutoPageView | Boolean | `false` | When this field is `false`, the web client will automatically record page views.

By default, the web client records page views when (1) the page first loads and (2) the browser's [history API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) is called. The page ID is `window.location.pathname`.

In some cases, the web client's instrumentation will not record the desired page ID. In this case, the web client's page view automation must be disabled using the `disableAutoPageView` configuration, and the application must be instrumented to record page views using the `recordPageView` command. | -| disableOnFail | Boolean | `true` | When this field is `true`, the web client will disable itself when the PutRumEvents request fails and all retries have been exhausted. | | enableRumClient | Boolean | `true` | When this field is `true`, the web client will record and dispatch RUM events. | | enableXRay | Boolean | `false` | When this field is `true` **and** the `http` telemetry is used, the web client will record X-Ray traces for HTTP requests.

See the [HTTP telemetry configuration](#http) for more information, including how to connect client-side and server-side traces. | | endpoint | String | `'https://dataplane.rum.[region].amazonaws.com'`

`'https://[restapi_id].execute-api.[region].amazonaws.com/[stage_name]/'` | The URL of the CloudWatch RUM API where data will be sent.

You may include a path prefix like `/stage_name/` in the endpoint URL if there is a proxy between your web application and CloudWatch RUM. | @@ -211,4 +210,4 @@ telemetries: [ } ], ] -``` +``` \ No newline at end of file From f3346fb752e6096aee8ea5f7289188ab30437370 Mon Sep 17 00:00:00 2001 From: Billy Date: Tue, 19 Mar 2024 18:44:23 -0700 Subject: [PATCH 4/7] Revert "feat: disableOnFail" This reverts commit 67ed7b3d07a5bea26367e1005aeec3edf3c57367. --- src/dispatch/Dispatch.ts | 4 +- src/dispatch/__tests__/Dispatch.test.ts | 46 +------------------ src/orchestration/Orchestration.ts | 2 - .../__tests__/Orchestration.test.ts | 1 - 4 files changed, 3 insertions(+), 50 deletions(-) diff --git a/src/dispatch/Dispatch.ts b/src/dispatch/Dispatch.ts index 27b3d7b6..b1682367 100644 --- a/src/dispatch/Dispatch.ts +++ b/src/dispatch/Dispatch.ts @@ -235,9 +235,7 @@ export class Dispatch { // The handler has run out of retries. We adhere to our convention to // fail safe by disabling dispatch. This ensures that we will not // continue to attempt requests when the problem is not recoverable. - if (this.config.disableOnFail) { - this.disable(); - } + this.disable(); throw e; }; diff --git a/src/dispatch/__tests__/Dispatch.test.ts b/src/dispatch/__tests__/Dispatch.test.ts index 2b16b7d1..ca67ba97 100644 --- a/src/dispatch/__tests__/Dispatch.test.ts +++ b/src/dispatch/__tests__/Dispatch.test.ts @@ -419,7 +419,7 @@ describe('Dispatch tests', () => { ); }); - test('when a fetch request is rejected and disableOnFaile=true then dispatch is disabled', async () => { + test('when a fetch request is rejected then dispatch is disabled', async () => { // Init const ERROR = 'Something went wrong.'; const sendFetch = jest.fn(() => Promise.reject(ERROR)); @@ -438,11 +438,7 @@ describe('Dispatch tests', () => { eventCache, { ...DEFAULT_CONFIG, - ...{ - dispatchInterval: Utils.AUTO_DISPATCH_OFF, - retries: 0, - disableOnFail: true - } + ...{ dispatchInterval: Utils.AUTO_DISPATCH_OFF, retries: 0 } } ); dispatch.setAwsCredentials(Utils.createAwsCredentials()); @@ -453,44 +449,6 @@ describe('Dispatch tests', () => { // Assert await expect(dispatch.dispatchFetch()).resolves.toEqual(undefined); - expect((dispatch as unknown as any).enabled).toBe(false); - }); - - test('when a fetch request is rejected and disableOnFail=false then dispatch is not disabled', async () => { - // Init - const ERROR = 'Something went wrong.'; - const sendFetch = jest.fn(() => Promise.reject(ERROR)); - (DataPlaneClient as any).mockImplementation(() => { - return { - sendFetch - }; - }); - - const eventCache: EventCache = - Utils.createDefaultEventCacheWithEvents(); - - const dispatch = new Dispatch( - Utils.AWS_RUM_REGION, - Utils.AWS_RUM_ENDPOINT, - eventCache, - { - ...DEFAULT_CONFIG, - ...{ - dispatchInterval: Utils.AUTO_DISPATCH_OFF, - retries: 0, - disableOnFail: false - } - } - ); - dispatch.setAwsCredentials(Utils.createAwsCredentials()); - - // Run - await expect(dispatch.dispatchFetch()).rejects.toEqual(ERROR); - eventCache.recordEvent('com.amazon.rum.event1', {}); - - // Assert - await expect(dispatch.dispatchFetch()).rejects.toEqual(ERROR); - expect((dispatch as unknown as any).enabled).toBe(true); }); test('when signing is disabled then credentials are not needed for dispatch', async () => { diff --git a/src/orchestration/Orchestration.ts b/src/orchestration/Orchestration.ts index 9cc88430..4155bbd7 100644 --- a/src/orchestration/Orchestration.ts +++ b/src/orchestration/Orchestration.ts @@ -72,7 +72,6 @@ export const defaultConfig = (cookieAttributes: CookieAttributes): Config => { client: INSTALL_MODULE, cookieAttributes, disableAutoPageView: false, - disableOnFail: true, dispatchInterval: 5 * 1000, enableRumClient: true, enableXRay: false, @@ -116,7 +115,6 @@ export interface Config { cookieAttributes: CookieAttributes; sessionAttributes: { [k: string]: string | number | boolean }; disableAutoPageView: boolean; - disableOnFail: boolean; dispatchInterval: number; enableRumClient: boolean; enableXRay: boolean; diff --git a/src/orchestration/__tests__/Orchestration.test.ts b/src/orchestration/__tests__/Orchestration.test.ts index 5c59bbe3..429258f1 100644 --- a/src/orchestration/__tests__/Orchestration.test.ts +++ b/src/orchestration/__tests__/Orchestration.test.ts @@ -175,7 +175,6 @@ describe('Orchestration tests', () => { sessionAttributes: {}, telemetries: [], disableAutoPageView: false, - disableOnFail: true, dispatchInterval: 5000, enableXRay: false, endpoint: 'https://dataplane.rum.us-west-2.amazonaws.com', From e2377d3f9424bf0b4513479a1dec0a50b6018f70 Mon Sep 17 00:00:00 2001 From: Billy Date: Tue, 19 Mar 2024 19:09:26 -0700 Subject: [PATCH 5/7] feat: only disable RUM when dispatch fails with 403 or 404 --- src/dispatch/Dispatch.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/dispatch/Dispatch.ts b/src/dispatch/Dispatch.ts index b1682367..0b8cc5ea 100644 --- a/src/dispatch/Dispatch.ts +++ b/src/dispatch/Dispatch.ts @@ -38,6 +38,7 @@ export class Dispatch { private dispatchTimerId: number | undefined; private buildClient: ClientBuilder; private config: Config; + private disableCodes = ['403', '404']; constructor( region: string, @@ -232,10 +233,12 @@ export class Dispatch { } private handleReject = (e: any): { response: HttpResponse } => { - // The handler has run out of retries. We adhere to our convention to - // fail safe by disabling dispatch. This ensures that we will not - // continue to attempt requests when the problem is not recoverable. - this.disable(); + if (e instanceof Error && this.disableCodes.includes(e.message)) { + // RUM disables only when dispatch fails and we are certain + // that subsequent attempts will not succeed, such as when + // credentials are invalid or the app monitor does not exist. + this.disable(); + } throw e; }; From 7e4ae2713316f3dc41d238810615782dce566ec8 Mon Sep 17 00:00:00 2001 From: Billy Date: Wed, 20 Mar 2024 09:54:46 -0700 Subject: [PATCH 6/7] chore: add unit tests --- src/dispatch/__tests__/Dispatch.test.ts | 108 +++++++++++++++++++++--- 1 file changed, 98 insertions(+), 10 deletions(-) diff --git a/src/dispatch/__tests__/Dispatch.test.ts b/src/dispatch/__tests__/Dispatch.test.ts index ca67ba97..d1400386 100644 --- a/src/dispatch/__tests__/Dispatch.test.ts +++ b/src/dispatch/__tests__/Dispatch.test.ts @@ -419,15 +419,41 @@ describe('Dispatch tests', () => { ); }); - test('when a fetch request is rejected then dispatch is disabled', async () => { + test('when a fetch request is rejected with 429 then dispatch is NOT disabled', async () => { // Init - const ERROR = 'Something went wrong.'; - const sendFetch = jest.fn(() => Promise.reject(ERROR)); - (DataPlaneClient as any).mockImplementation(() => { - return { - sendFetch - }; - }); + (DataPlaneClient as any).mockImplementationOnce(() => ({ + sendFetch: () => Promise.reject(new Error('429')) + })); + + const eventCache: EventCache = + Utils.createDefaultEventCacheWithEvents(); + + const dispatch = new Dispatch( + Utils.AWS_RUM_REGION, + Utils.AWS_RUM_ENDPOINT, + eventCache, + { + ...DEFAULT_CONFIG, + ...{ dispatchInterval: Utils.AUTO_DISPATCH_OFF, retries: 0 } + } + ); + dispatch.setAwsCredentials(Utils.createAwsCredentials()); + + // Run + eventCache.recordEvent('com.amazon.rum.event1', {}); + + // Assert + await expect(dispatch.dispatchFetch()).rejects.toEqual( + new Error('429') + ); + expect((dispatch as unknown as any).enabled).toBe(true); + }); + + test('when a fetch request is rejected with 500 then dispatch is NOT disabled', async () => { + // Init + (DataPlaneClient as any).mockImplementationOnce(() => ({ + sendFetch: () => Promise.reject(new Error('500')) + })); const eventCache: EventCache = Utils.createDefaultEventCacheWithEvents(); @@ -444,11 +470,73 @@ describe('Dispatch tests', () => { dispatch.setAwsCredentials(Utils.createAwsCredentials()); // Run - await expect(dispatch.dispatchFetch()).rejects.toEqual(ERROR); eventCache.recordEvent('com.amazon.rum.event1', {}); // Assert - await expect(dispatch.dispatchFetch()).resolves.toEqual(undefined); + await expect(dispatch.dispatchFetch()).rejects.toEqual( + new Error('500') + ); + expect((dispatch as unknown as any).enabled).toBe(true); + }); + + test('when a fetch request is rejected with 403 then dispatch is disabled', async () => { + // Init + (DataPlaneClient as any).mockImplementationOnce(() => ({ + sendFetch: () => Promise.reject(new Error('403')) + })); + + const eventCache: EventCache = + Utils.createDefaultEventCacheWithEvents(); + + const dispatch = new Dispatch( + Utils.AWS_RUM_REGION, + Utils.AWS_RUM_ENDPOINT, + eventCache, + { + ...DEFAULT_CONFIG, + ...{ dispatchInterval: Utils.AUTO_DISPATCH_OFF, retries: 0 } + } + ); + dispatch.setAwsCredentials(Utils.createAwsCredentials()); + + // Run + eventCache.recordEvent('com.amazon.rum.event1', {}); + + // Assert + await expect(dispatch.dispatchFetch()).rejects.toEqual( + new Error('403') + ); + expect((dispatch as unknown as any).enabled).toBe(false); + }); + + test('when a fetch request is rejected with 404 then dispatch is disabled', async () => { + // Init + (DataPlaneClient as any).mockImplementationOnce(() => ({ + sendFetch: () => Promise.reject(new Error('404')) + })); + + const eventCache: EventCache = + Utils.createDefaultEventCacheWithEvents(); + + const dispatch = new Dispatch( + Utils.AWS_RUM_REGION, + Utils.AWS_RUM_ENDPOINT, + eventCache, + { + ...DEFAULT_CONFIG, + ...{ dispatchInterval: Utils.AUTO_DISPATCH_OFF, retries: 0 } + } + ); + dispatch.setAwsCredentials(Utils.createAwsCredentials()); + + // Run + eventCache.recordEvent('com.amazon.rum.event1', {}); + + // Assert + await expect(dispatch.dispatchFetch()).rejects.toEqual( + new Error('404') + ); + expect((dispatch as unknown as any).enabled).toBe(false); }); test('when signing is disabled then credentials are not needed for dispatch', async () => { From 064e88de3dc7ff1d0bc5b4980b72dac3e5abd9da Mon Sep 17 00:00:00 2001 From: Billy Date: Wed, 27 Mar 2024 10:56:32 -0700 Subject: [PATCH 7/7] feat: add 401 --- src/dispatch/Dispatch.ts | 2 +- src/dispatch/__tests__/Dispatch.test.ts | 30 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/dispatch/Dispatch.ts b/src/dispatch/Dispatch.ts index 0b8cc5ea..7fbcc1ce 100644 --- a/src/dispatch/Dispatch.ts +++ b/src/dispatch/Dispatch.ts @@ -38,7 +38,7 @@ export class Dispatch { private dispatchTimerId: number | undefined; private buildClient: ClientBuilder; private config: Config; - private disableCodes = ['403', '404']; + private disableCodes = ['401', '403', '404']; constructor( region: string, diff --git a/src/dispatch/__tests__/Dispatch.test.ts b/src/dispatch/__tests__/Dispatch.test.ts index d1400386..9433d2a9 100644 --- a/src/dispatch/__tests__/Dispatch.test.ts +++ b/src/dispatch/__tests__/Dispatch.test.ts @@ -479,6 +479,36 @@ describe('Dispatch tests', () => { expect((dispatch as unknown as any).enabled).toBe(true); }); + test('when a fetch request is rejected with 401 then dispatch is disabled', async () => { + // Init + (DataPlaneClient as any).mockImplementationOnce(() => ({ + sendFetch: () => Promise.reject(new Error('401')) + })); + + const eventCache: EventCache = + Utils.createDefaultEventCacheWithEvents(); + + const dispatch = new Dispatch( + Utils.AWS_RUM_REGION, + Utils.AWS_RUM_ENDPOINT, + eventCache, + { + ...DEFAULT_CONFIG, + ...{ dispatchInterval: Utils.AUTO_DISPATCH_OFF, retries: 0 } + } + ); + dispatch.setAwsCredentials(Utils.createAwsCredentials()); + + // Run + eventCache.recordEvent('com.amazon.rum.event1', {}); + + // Assert + await expect(dispatch.dispatchFetch()).rejects.toEqual( + new Error('401') + ); + expect((dispatch as unknown as any).enabled).toBe(false); + }); + test('when a fetch request is rejected with 403 then dispatch is disabled', async () => { // Init (DataPlaneClient as any).mockImplementationOnce(() => ({