Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ For example, the config object may look similar to the following:
| eventCacheSize | Number | `1000` | The maximum number of events the cache can contain before dropping events. Each event is typically 1KB in size, so we recommend a default limit of 1000 events -> 1 MB to balance performance against capturing all observed events. If necessary, feel free to enable debug mode to get detailed logs on how to optimize cache size depending on your application behavior. |
| sessionLengthSeconds | Number | `1800` | The duration of a session (in seconds). |
| 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). |
| legacySPASupport | Boolean | `false` | When this field is `true`, the web client will use legacy virtual page load timing for single page applications. This feature tracks HTTP requests and DOM mutations to determine when a route change is complete. **Warning:** This is a legacy feature that is no longer supported. Please enable with caution in debugging mode to see if it is relevant for your web application. |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: we can mention the issues with SPA page load timing #723
Also might be good to mention that when legacySPASupport is false, web client does not send performance_navigation_event for SPA


## CookieAttributes

Expand Down
4 changes: 4 additions & 0 deletions src/orchestration/Orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const defaultConfig = (cookieAttributes: CookieAttributes): Config => {
useBeacon: true,
userIdRetentionDays: 30,
enableW3CTraceId: false,
legacySPASupport: false, // deprecated
...internalConfigOverrides
};
};
Expand Down Expand Up @@ -174,6 +175,9 @@ export interface Config {
alias?: string;
headers?: HeaderBag;
enableW3CTraceId: boolean;

// Deprecated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Not sure if the comment here is necessary. The class is deprecated, not the config

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it's worth to describe that the feature itself is deprecated. clarified the wording

legacySPASupport: boolean;
}

export interface PartialConfig
Expand Down
1 change: 1 addition & 0 deletions src/orchestration/__tests__/Orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,5 +572,6 @@ describe('defaultConfig tests', () => {
expect(config.userIdRetentionDays).toBe(30);
expect(config.enableW3CTraceId).toBe(false);
expect(config.candidatesCacheSize).toBe(10);
expect(config.legacySPASupport).toBe(false);
});
});
63 changes: 51 additions & 12 deletions src/sessions/PageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class PageManager {
private page: Page | undefined;
private resumed: boolean;
private attributes: Attributes | undefined;
private virtualPageLoadTimer: VirtualPageLoadTimer;
private virtualPageLoadTimer: VirtualPageLoadTimer | undefined;
private TIMEOUT = 1000;
private timeOnParentPage: number | undefined;

Expand All @@ -68,11 +68,18 @@ export class PageManager {
this.page = undefined;
this.resumed = false;
this.recordInteraction = false;
this.virtualPageLoadTimer = new VirtualPageLoadTimer(
this,
config,
record
);
if (config.legacySPASupport) {
if (config.debug) {
InternalLogger.warn(
'VirtualPageLoadTiming (deprecated) is enabled. Please use with caution after reviewing https://github.com/aws-observability/aws-rum-web/issues/723'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: Should this message be a bit more explicit about the implication of it being enabled?
For example, "Keeping this enabled will result in inaccurate page load timing with single page applications"

I know you have linked the github issue, but perhaps something more explicit can make it clearer to the users of the implication of this and get them to disable it.

);
}
this.virtualPageLoadTimer = new VirtualPageLoadTimer(
this,
config,
record
);
}
}

public getPage(): Page | undefined {
Expand All @@ -99,28 +106,38 @@ export class PageManager {
pageId = payload.pageId;
}

if (this.config.debug) {
InternalLogger.info(`recordPageView called with pageId: ${pageId}`);
}

if (this.useCookies()) {
this.recordInteraction = true;
}

if (!this.page) {
this.createLandingPage(pageId);
if (this.config.debug) {
InternalLogger.info(`Landing page recorded: ${pageId}`);
InternalLogger.info(`Landing page created: ${pageId}`);
}
} else if (this.page.pageId !== pageId) {
this.createNextPage(this.page, pageId);
if (this.config.debug) {
InternalLogger.info(`Navigation to page: ${pageId}`);
InternalLogger.info(`Navigation to new page: ${pageId}`);
}
} else if (this.resumed) {
if (this.config.debug) {
InternalLogger.info(`Resumed session for page: ${pageId}`);
}
// Update attributes state in PageManager for event metadata
this.collectAttributes(
this.page as Page,
typeof payload === 'object' ? payload : undefined
);
return;
} else {
if (this.config.debug) {
InternalLogger.info(`No page change detected for: ${pageId}`);
}
// The view has not changed.
return;
}
Expand All @@ -139,7 +156,14 @@ export class PageManager {

private createNextPage(currentPage: Page, pageId: string) {
let startTime = Date.now();
const interactionTime = this.virtualPageLoadTimer.latestInteractionTime;

if (this.config.debug) {
InternalLogger.info(
`Creating next page ${pageId}, interaction: ${
currentPage.interaction + 1
}`
);
}

// The latest interaction time (latest) is not guaranteed to be the
// interaction that triggered the route change (actual). There are two
Expand All @@ -160,9 +184,24 @@ export class PageManager {
//
// We do not believe that case (2) has a high risk of skewing route
// change timing, and therefore ignore case (2).
if (!this.resumed && startTime - interactionTime <= this.TIMEOUT) {
startTime = interactionTime;
this.virtualPageLoadTimer.startTiming();
if (this.config.legacySPASupport && this.virtualPageLoadTimer) {
const interactionTime =
this.virtualPageLoadTimer.latestInteractionTime;
if (!this.resumed && startTime - interactionTime <= this.TIMEOUT) {
startTime = interactionTime;
this.virtualPageLoadTimer.startTiming();
if (this.config.debug) {
InternalLogger.info(
`Started virtual page load timing for ${pageId}`
);
}
} else if (this.config.debug) {
InternalLogger.info(
`Skipped virtual page load timing for ${pageId} (resumed: ${
this.resumed
}, timeDiff: ${startTime - interactionTime}ms)`
);
}
}

this.timeOnParentPage = startTime - currentPage.start;
Expand Down
47 changes: 47 additions & 0 deletions src/sessions/VirtualPageLoadTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MonkeyPatched } from '../plugins/MonkeyPatched';
import { Config } from '../orchestration/Orchestration';
import { RecordEvent } from '../plugins/types';
import { PageManager, Page } from './PageManager';
import { InternalLogger } from '../utils/InternalLogger';

type Fetch = typeof fetch;
type Send = () => void;
Expand All @@ -13,6 +14,10 @@ type Patching = Pick<XMLHttpRequest & Window, 'fetch' | 'send'>;
* (1) Holds all virtual page load timing related resources
* (2) Intercepts outgoing XMLHttpRequests and Fetch requests and listens for DOM changes
* (3) Records virtual page load
*
* @deprecated This class is deprecated and will be removed in a future version.
* For now, it can still be enabled via the legacySPASupport configuration. If you would like to opt-in
* to this legacy feature, then please first carefully review https://github.com/aws-observability/aws-rum-web/issues/723
*/
export class VirtualPageLoadTimer extends MonkeyPatched<
Patching,
Expand Down Expand Up @@ -84,6 +89,11 @@ export class VirtualPageLoadTimer extends MonkeyPatched<
/** Initializes timing related resources for current page. */
public startTiming() {
this.latestEndTime = Date.now();

if (this.config.debug) {
InternalLogger.info('Starting timing for virtual page load');
}

// Clean up existing timer objects and mutationObserver
if (this.periodicCheckerId) {
clearInterval(this.periodicCheckerId);
Expand Down Expand Up @@ -114,6 +124,12 @@ export class VirtualPageLoadTimer extends MonkeyPatched<
this.isPageLoaded = false;
this.requestBuffer.forEach(this.moveItemsFromBuffer);
this.requestBuffer.clear();

if (this.config.debug) {
InternalLogger.info(
`Moved ${this.requestBuffer.size} requests from buffer to ongoing`
);
}
}

private sendWrapper = (): ((original: Send) => Send) => {
Expand All @@ -132,8 +148,18 @@ export class VirtualPageLoadTimer extends MonkeyPatched<
const page = this.pageManager.getPage();
if (page && this.isPageLoaded === false) {
this.ongoingRequests.add(item);
if (this.config.debug) {
InternalLogger.info(
`Added XHR to ongoing requests (total: ${this.ongoingRequests.size})`
);
}
} else {
this.requestBuffer.add(item);
if (this.config.debug) {
InternalLogger.info(
`VirtualPageLoadTimer: Added XHR to buffer (total: ${this.requestBuffer.size})`
);
}
}
}

Expand Down Expand Up @@ -204,7 +230,19 @@ export class VirtualPageLoadTimer extends MonkeyPatched<
* (3) Indicate current page has finished loading
*/
private checkLoadStatus = () => {
if (this.config.debug) {
InternalLogger.info(
`VirtualPageLoadTimer: Checking load status - XHR: ${this.ongoingRequests.size}, Fetch: ${this.fetchCounter}`
);
}

if (this.ongoingRequests.size === 0 && this.fetchCounter === 0) {
if (this.config.debug) {
InternalLogger.info(
'Page load completed, recording navigation event'
);
}

clearInterval(this.periodicCheckerId);
clearTimeout(this.timeoutCheckerId);
this.domMutationObserver.disconnect();
Expand All @@ -219,6 +257,10 @@ export class VirtualPageLoadTimer extends MonkeyPatched<

/** Clears timers and disconnects observer to stop page timing. */
private declareTimeout = () => {
if (this.config.debug) {
InternalLogger.info('Page load timed out');
}

clearInterval(this.periodicCheckerId);
this.periodicCheckerId = undefined;
this.timeoutCheckerId = undefined;
Expand Down Expand Up @@ -257,5 +299,10 @@ export class VirtualPageLoadTimer extends MonkeyPatched<

private updateLatestInteractionTime = (e: Event) => {
this.latestInteractionTime = Date.now();
if (this.config.debug) {
InternalLogger.info(
`Updated interaction time: ${this.latestInteractionTime}`
);
}
};
}
64 changes: 59 additions & 5 deletions src/sessions/__tests__/PageManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,8 @@ describe('PageManager tests', () => {
const pageManager: PageManager = new PageManager(
{
...DEFAULT_CONFIG,
allowCookies: true
allowCookies: true,
legacySPASupport: true
},
record
);
Expand Down Expand Up @@ -519,7 +520,8 @@ describe('PageManager tests', () => {
const pageManager: PageManager = new PageManager(
{
...DEFAULT_CONFIG,
allowCookies: true
allowCookies: true,
legacySPASupport: true
},
record
);
Expand Down Expand Up @@ -554,7 +556,8 @@ describe('PageManager tests', () => {
const pageManager: PageManager = new PageManager(
{
...DEFAULT_CONFIG,
allowCookies: true
allowCookies: true,
legacySPASupport: true
},
record
);
Expand Down Expand Up @@ -584,7 +587,8 @@ describe('PageManager tests', () => {
// Init
const config: Config = {
...DEFAULT_CONFIG,
allowCookies: true
allowCookies: true,
legacySPASupport: true
};
const pageManager: PageManager = new PageManager(config, record);
const helper = pageManager['virtualPageLoadTimer'];
Expand All @@ -608,11 +612,61 @@ describe('PageManager tests', () => {
expect(pageManager.getPage().start).toEqual(3000);
});

test('when legacySPASupport is false then virtualPageLoadTimer is not created', async () => {
// Init
const pageManager: PageManager = new PageManager(
{
...DEFAULT_CONFIG,
allowCookies: true,
legacySPASupport: false
},
record
);

// Assert
expect(pageManager['virtualPageLoadTimer']).toBeUndefined();

window.removeEventListener(
'popstate',
(pageManager as any).popstateListener
);
});

test('when legacySPASupport is false then page navigation works without virtual timing', async () => {
// Init
const pageManager: PageManager = new PageManager(
{
...DEFAULT_CONFIG,
allowCookies: true,
legacySPASupport: false
},
record
);

// Run
pageManager.recordPageView('/rum/home');
pageManager.recordPageView('/console/home');

// Assert - page view events are still recorded
expect(record.mock.calls.length).toEqual(2);
expect(record.mock.calls[1][0]).toEqual(PAGE_VIEW_EVENT_TYPE);
expect(record.mock.calls[1][1]).toMatchObject({
pageId: '/console/home',
interaction: 1
});

window.removeEventListener(
'popstate',
(pageManager as any).popstateListener
);
});

test('when latestInteractionTime is within the scope of routeChangeTimeout then page.start is latestInteractionTime', async () => {
// Init
const config: Config = {
...DEFAULT_CONFIG,
allowCookies: true
allowCookies: true,
legacySPASupport: true
};
const pageManager: PageManager = new PageManager(config, record);
const helper = pageManager['virtualPageLoadTimer'];
Expand Down