Skip to content

Commit 5fd50aa

Browse files
authored
feat: add debugging mode to expose data loss scenarios (#727)
1 parent a89152d commit 5fd50aa

File tree

13 files changed

+309
-21
lines changed

13 files changed

+309
-21
lines changed

docs/configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ For example, the config object may look similar to the following:
2121
| allowCookies | Boolean | `false` | Enable the web client to set and read two cookies: a session cookie named `cwr_s` and a user cookie named `cwr_u`.<br/><br/>`cwr_s` stores session data including an anonymous session ID (uuid v4) created by the web client. This allows CloudWatch RUM to compute sessionized metrics like errors per session.<br/><br/>`cwr_u` stores an anonymous user ID (uuid v4) created by the web client. This allows CloudWatch RUM to count return visitors.<br/><br/>`true`: the web client will use cookies<br/>`false`: the web client will not use cookies. |
2222
| releaseId | String | `undefined` | The releaseId will be used to retrieve source map(s), if any, when RUM service unminifies JavaScript error stack traces. It should be unique to each application release and match regex ^[a-zA-Z0-9_\-:/\.]{1,200}$, including size limit of 200. |
2323
| 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`. |
24+
| debug | Boolean | `false` | When this field is `true`, the web client will output detailed debug logs to the browser console. These logs include session lifecycle events, plugin operations, dispatch activities, and error details. Debug logs are prefixed with `[aws-rum-web:ClassName.methodName]` for easy identification.<br/><br/>**Note:** Debug mode should only be enabled during development or troubleshooting as it may impact performance and expose internal operations. |
2425
| sessionAttributes | [MetadataAttributes](#metadataattributes) | `{}` | Session attributes will be added the metadata of all events in the session. |
2526
| disableAutoPageView | Boolean | `false` | When this field is `false`, the web client will automatically record page views.<br/><br/>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`.<br/><br/>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. |
2627
| enableRumClient | Boolean | `true` | When this field is `true`, the web client will record and dispatch RUM events. |
@@ -43,7 +44,7 @@ For example, the config object may look similar to the following:
4344
| telemetries | [Telemetry Config Array](#telemetry-config-array) | `[]` | See [Telemetry Config Array](#telemetry-config-array) |
4445
| batchLimit | Number | `100` | The maximum number of events that will be sent in one batch of RUM events. |
4546
| dispatchInterval | Number | `5000` | The frequency (in milliseconds) in which the webclient will dispatch a batch of RUM events. RUM events are first cached and then automatically dispatched at this set interval. |
46-
| eventCacheSize | Number | `200` | The maximum number of events the cache can contain before dropping events. |
47+
| eventCacheSize | Number | `500` | The maximum number of events the cache can contain before dropping events. |
4748
| sessionLengthSeconds | Number | `1800` | The duration of a session (in seconds). |
4849
| headers | Object | `{}` | The **headers** configuration is optional and allows you to include custom headers in an HTTP request. For example, you can use it to pass `Authorization` and `x-api-key` headers.<br/><br/>For more details, see: [MDN - Request Headers](https://developer.mozilla.org/en-US/docs/Glossary/Request_header). |
4950

src/dispatch/BasicAuthentication.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AwsCredentialIdentity } from '@aws-sdk/types';
33
import { FetchHttpHandler } from '@smithy/fetch-http-handler';
44
import { StsClient } from './StsClient';
55
import { Authentication } from './Authentication';
6+
import { InternalLogger } from '../utils/InternalLogger';
67

78
export class BasicAuthentication extends Authentication {
89
private stsClient: StsClient;
@@ -57,11 +58,27 @@ export class BasicAuthentication extends Authentication {
5758
// Ignore
5859
}
5960

61+
if (this.config.debug) {
62+
InternalLogger.info(
63+
'AWS credentials fetched successfully'
64+
);
65+
}
6066
return credentials;
6167
} catch (e) {
6268
if (retries) {
6369
retries--;
70+
if (this.config.debug) {
71+
InternalLogger.warn(
72+
'AWS credential fetch failed, retrying'
73+
);
74+
}
6475
} else {
76+
if (this.config.debug) {
77+
InternalLogger.error(
78+
'AWS credential fetch failed:',
79+
e
80+
);
81+
}
6582
throw e;
6683
}
6784
}

src/dispatch/CognitoIdentityClient.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AwsCredentialIdentity } from '@aws-sdk/types';
44
import { responseToJson } from './utils';
55
import { IDENTITY_KEY } from '../utils/constants';
66
import { Config } from '../orchestration/Orchestration';
7+
import { InternalLogger } from '../utils/InternalLogger';
78

89
const METHOD = 'POST';
910
const CONTENT_TYPE = 'application/x-amz-json-1.1';
@@ -48,10 +49,12 @@ export class CognitoIdentityClient {
4849
private fetchRequestHandler: HttpHandler;
4950
private hostname: string;
5051
private identityStorageKey: string;
52+
private config?: Config;
5153

5254
constructor(config: CognitoIdentityClientConfig) {
5355
this.hostname = `cognito-identity.${config.region}.amazonaws.com`;
5456
this.fetchRequestHandler = config.fetchRequestHandler;
57+
this.config = config.clientConfig;
5558
this.identityStorageKey = config.clientConfig?.cookieAttributes.unique
5659
? `${IDENTITY_KEY}_${config.applicationId}`
5760
: IDENTITY_KEY;
@@ -65,7 +68,9 @@ export class CognitoIdentityClient {
6568
localStorage.getItem(this.identityStorageKey)!
6669
) as GetIdResponse | null;
6770
} catch (e) {
68-
// Ignore -- we will get a new identity Id from Cognito
71+
if (this.config?.debug) {
72+
InternalLogger.error('Failed to parse stored identity:', e);
73+
}
6974
}
7075

7176
if (getIdResponse && getIdResponse.IdentityId) {
@@ -89,7 +94,9 @@ export class CognitoIdentityClient {
8994
JSON.stringify({ IdentityId: getIdResponse.IdentityId })
9095
);
9196
} catch (e) {
92-
// Ignore
97+
if (this.config?.debug) {
98+
InternalLogger.error('Failed to store identity:', e);
99+
}
93100
}
94101
return getIdResponse;
95102
} catch (e) {

src/dispatch/Dispatch.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { PutRumEventsRequest } from './dataplane';
1111
import { Config } from '../orchestration/Orchestration';
1212
import { v4 } from 'uuid';
1313
import { RetryHttpHandler } from './RetryHttpHandler';
14+
import { InternalLogger } from '../utils/InternalLogger';
1415

1516
type SendFunction = (
1617
putRumEventsRequest: PutRumEventsRequest
@@ -83,6 +84,9 @@ export class Dispatch {
8384
public disable(): void {
8485
this.stopDispatchTimer();
8586
this.enabled = false;
87+
if (this.config.debug) {
88+
InternalLogger.warn('Dispatch disabled');
89+
}
8690
}
8791

8892
/**
@@ -115,9 +119,13 @@ export class Dispatch {
115119
{ response: HttpResponse } | undefined
116120
> => {
117121
if (this.doRequest()) {
118-
return this.rum
119-
.sendFetch(this.createRequest())
120-
.catch(this.handleReject);
122+
const request = this.createRequest();
123+
if (this.config.debug) {
124+
InternalLogger.info(
125+
`Dispatching ${request.RumEvents.length} events via fetch`
126+
);
127+
}
128+
return this.rum.sendFetch(request).catch(this.handleReject);
121129
}
122130
};
123131

@@ -129,6 +137,11 @@ export class Dispatch {
129137
> => {
130138
if (this.doRequest()) {
131139
const request: PutRumEventsRequest = this.createRequest();
140+
if (this.config.debug) {
141+
InternalLogger.info(
142+
`Dispatching ${request.RumEvents.length} events via beacon`
143+
);
144+
}
132145
return this.rum
133146
.sendBeacon(request)
134147
.catch(() => this.rum.sendFetch(request));
@@ -143,8 +156,11 @@ export class Dispatch {
143156
public dispatchFetchFailSilent = async (): Promise<{
144157
response: HttpResponse;
145158
} | void> => {
146-
// eslint-disable-next-line @typescript-eslint/no-empty-function
147-
return this.dispatchFetch().catch(() => {});
159+
return this.dispatchFetch().catch((e) => {
160+
if (this.config.debug) {
161+
InternalLogger.error('Dispatch fetch failed silently:', e);
162+
}
163+
});
148164
};
149165

150166
/**
@@ -155,8 +171,11 @@ export class Dispatch {
155171
public dispatchBeaconFailSilent = async (): Promise<{
156172
response: HttpResponse;
157173
} | void> => {
158-
// eslint-disable-next-line @typescript-eslint/no-empty-function
159-
return this.dispatchBeacon().catch(() => {});
174+
return this.dispatchBeacon().catch((e) => {
175+
if (this.config.debug) {
176+
InternalLogger.error('Dispatch beacon failed silently:', e);
177+
}
178+
});
160179
};
161180

162181
private flushSync: EventListener = () => {
@@ -170,10 +189,17 @@ export class Dispatch {
170189
}
171190

172191
const req = this.createRequest(true);
192+
if (this.config.debug) {
193+
InternalLogger.debug(
194+
`Flushing ${req.RumEvents.length} events on page hide`
195+
);
196+
}
173197
flush(req)
174198
.catch(() => backup(req))
175-
.catch(() => {
176-
// fail silent
199+
.catch((e) => {
200+
if (this.config.debug) {
201+
InternalLogger.error('Page hide flush failed:', e);
202+
}
177203
});
178204
}
179205
}
@@ -251,6 +277,12 @@ export class Dispatch {
251277
// RUM disables only when dispatch fails and we are certain
252278
// that subsequent attempts will not succeed, such as when
253279
// credentials are invalid or the app monitor does not exist.
280+
if (this.config.debug) {
281+
InternalLogger.error(
282+
'Dispatch failed with status code:',
283+
e.message
284+
);
285+
}
254286
this.disable();
255287
}
256288
throw e;

src/dispatch/EnhancedAuthentication.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Config } from '../orchestration/Orchestration';
22
import { AwsCredentialIdentity } from '@aws-sdk/types';
33
import { Authentication } from './Authentication';
4+
import { InternalLogger } from '../utils/InternalLogger';
45

56
export class EnhancedAuthentication extends Authentication {
67
constructor(config: Config, applicationId: string) {
@@ -40,11 +41,27 @@ export class EnhancedAuthentication extends Authentication {
4041
// Ignore
4142
}
4243

44+
if (this.config.debug) {
45+
InternalLogger.info(
46+
'AWS credentials fetched successfully'
47+
);
48+
}
4349
return credentials;
4450
} catch (e) {
4551
if (retries) {
4652
retries--;
53+
if (this.config.debug) {
54+
InternalLogger.warn(
55+
'AWS credential fetch failed, retrying'
56+
);
57+
}
4758
} else {
59+
if (this.config.debug) {
60+
InternalLogger.error(
61+
'AWS credential fetch failed:',
62+
e
63+
);
64+
}
4865
throw e;
4966
}
5067
}

src/event-cache/EventCache.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import EventBus, { Topic } from '../event-bus/EventBus';
1313
import { RecordEvent } from '../plugins/types';
1414
import { SESSION_START_EVENT_TYPE } from '../plugins/utils/constant';
15+
import { InternalLogger } from '../utils/InternalLogger';
1516

1617
const webClientVersion = '1.25.0';
1718

@@ -35,6 +36,10 @@ export class EventCache {
3536
private enabled: boolean;
3637
private installationMethod: string;
3738

39+
// Enable config.debug mode
40+
private droppedEvent = 0; // tracks dropped event count due to insufficient `eventCache`
41+
private sessionLimitExceeded = 0; // tracks dropped event count due to insufficieent `sessionEventLimit`
42+
3843
/**
3944
* @param applicationDetails Application identity and version.
4045
* @param batchLimit The maximum number of events that will be returned in a batch.
@@ -113,6 +118,8 @@ export class EventCache {
113118
if (this.sessionManager.canRecord()) {
114119
this.sessionManager.incrementSessionEventCount();
115120
this.addRecordToCache(type, eventData);
121+
} else if (this.config.debug) {
122+
this.sessionLimitExceeded++;
116123
}
117124
}
118125
};
@@ -165,6 +172,40 @@ export class EventCache {
165172
* Returns true if there are one or more events in the cache.
166173
*/
167174
public hasEvents(): boolean {
175+
// For debug mode
176+
if (this.config.debug && this.sessionLimitExceeded > 0) {
177+
const total = this.events.length + this.sessionLimitExceeded;
178+
if (this.sessionManager.isSampled()) {
179+
InternalLogger.warn(
180+
`Dropped ${
181+
this.sessionLimitExceeded
182+
} of ${total} recently observed events (${(
183+
(this.sessionLimitExceeded / total) *
184+
100
185+
).toFixed(
186+
2
187+
)}%) because the session limit has exceeded. Consider increasing sessionEventLimit (currently ${
188+
this.config.sessionEventLimit
189+
}) to ${total} or more to avoid data loss.`
190+
);
191+
} else {
192+
InternalLogger.warn(
193+
`Dropped ${
194+
this.sessionLimitExceeded
195+
} of ${total} recently observed events (${(
196+
(this.sessionLimitExceeded / total) *
197+
100
198+
).toFixed(
199+
2
200+
)}%) because current session is not sampled. Consider increasing sessionSampleRate (currently ${
201+
this.config.sessionSampleRate
202+
}) to capture more user sessions.`
203+
);
204+
}
205+
// reset
206+
this.sessionLimitExceeded = 0;
207+
}
208+
168209
return this.events.length !== 0;
169210
}
170211

@@ -179,6 +220,24 @@ export class EventCache {
179220
* Removes and returns the next batch of events.
180221
*/
181222
public getEventBatch(flushCandidates = false): RumEvent[] {
223+
if (this.config.debug && this.droppedEvent > 0) {
224+
const total = this.events.length + this.droppedEvent;
225+
InternalLogger.warn(
226+
`Dropped ${
227+
this.droppedEvent
228+
} of ${total} recently observed events (${(
229+
(this.droppedEvent / total) *
230+
100
231+
).toFixed(
232+
2
233+
)}%) due to insufficient in-memory queue. Increase eventCacheSize to ${total} (currently ${
234+
this.config.eventCacheSize
235+
}) to avoid data loss.`
236+
);
237+
// reset
238+
this.droppedEvent = 0;
239+
}
240+
182241
let batch: RumEvent[] = [];
183242

184243
// Prioritize candidates in the next event batch
@@ -260,7 +319,10 @@ export class EventCache {
260319
return;
261320
}
262321

263-
if (this.events.length === this.config.eventCacheSize) {
322+
if (this.events.length >= this.config.eventCacheSize) {
323+
if (this.config.debug) {
324+
this.droppedEvent += 1;
325+
}
264326
// Drop newest event and keep the older ones
265327
// 1. Older events tend to be more relevant, such as session start
266328
// or performance entries that are attributed to web vitals

src/orchestration/Orchestration.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { PageViewPlugin } from '../plugins/event-plugins/PageViewPlugin';
2929
import { PageAttributes } from '../sessions/PageManager';
3030
import { INSTALL_MODULE } from '../utils/constants';
3131
import EventBus, { Topic } from '../event-bus/EventBus';
32+
import { InternalLogger } from '../utils/InternalLogger';
3233

3334
const DEFAULT_REGION = 'us-west-2';
3435
const DEFAULT_ENDPOINT = `https://dataplane.rum.${DEFAULT_REGION}.amazonaws.com`;
@@ -72,6 +73,7 @@ export const defaultConfig = (cookieAttributes: CookieAttributes): Config => {
7273
batchLimit: 100,
7374
client: INSTALL_MODULE,
7475
cookieAttributes,
76+
debug: false,
7577
disableAutoPageView: false,
7678
dispatchInterval: 5 * 1000,
7779
enableRumClient: true,
@@ -122,6 +124,7 @@ export interface Config {
122124
clientBuilder?: ClientBuilder;
123125
cookieAttributes: CookieAttributes;
124126
sessionAttributes: { [k: string]: string | number | boolean };
127+
debug: boolean;
125128
disableAutoPageView: boolean;
126129
dispatchInterval: number;
127130
enableRumClient: boolean;
@@ -257,6 +260,17 @@ export class Orchestration {
257260
applicationVersion
258261
);
259262

263+
if (this.config.debug) {
264+
InternalLogger.info(
265+
`RUM client initialized for app: ${applicationId}`
266+
);
267+
InternalLogger.info(
268+
`Telemetries enabled: ${
269+
this.config.telemetries.join(', ') || 'none'
270+
}`
271+
);
272+
}
273+
260274
if (this.config.enableRumClient) {
261275
this.enable();
262276
} else {

0 commit comments

Comments
 (0)