Skip to content

Commit

Permalink
feat:Time Spent on Page (#341)
Browse files Browse the repository at this point in the history
- This change allows AWS RUM to collect time on page data
- Time on page will be collected in the timeOnParentPage field of the event details for a page view event
  • Loading branch information
ps863 committed Feb 1, 2023
1 parent 97c543a commit d1c3b17
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 48 deletions.
14 changes: 2 additions & 12 deletions src/__integ__/cookiesEnabled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,8 @@ test('when page is re-loaded with cookies enabled, session start is not dispatch
.click(clear)
.eval(() => location.reload());

await t
.wait(300)
.click(dispatch)
.expect(REQUEST_BODY.textContent)
.contains('BatchId');

const jsonB = JSON.parse(await REQUEST_BODY.textContent);
const sessionStartEventsB = jsonB.RumEvents.filter(
(e) => e.type === SESSION_START_EVENT_TYPE
);

await t.expect(sessionStartEventsB.length).eql(0);
// No new events should be recorded, thus no request body
await t.wait(300).click(dispatch).expect(REQUEST_BODY.textContent).eql('');
});

test('when page is loaded with cookies enabled, session start includes meta data', async (t: TestController) => {
Expand Down
1 change: 1 addition & 0 deletions src/event-schemas/page-view-event.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"pageInteractionId": { "type": "string" },
"interaction": { "type": "number" },
"parentPageInteractionId": { "type": "string" },
"timeOnParentPage": { "type": "number" },
"referrer": { "type": "string" },
"referrerDomain": { "type": "string" }
},
Expand Down
39 changes: 15 additions & 24 deletions src/sessions/PageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ export class PageManager {
private config: Config;
private record: RecordEvent;
private page: Page | undefined;
private resumed: Page | undefined;
private resumed: boolean;
private attributes: Attributes | undefined;
private virtualPageLoadTimer: VirtualPageLoadTimer;
private TIMEOUT = 1000;
private timeOnParentPage: number | undefined;

/**
* A flag which keeps track of whether or not cookies have been enabled.
Expand All @@ -65,7 +66,7 @@ export class PageManager {
this.config = config;
this.record = record;
this.page = undefined;
this.resumed = undefined;
this.resumed = false;
this.recordInteraction = false;
this.virtualPageLoadTimer = new VirtualPageLoadTimer(
this,
Expand All @@ -82,13 +83,12 @@ export class PageManager {
return this.attributes;
}

public resumeSession(pageId: string, interaction: number) {
public resumeSession(page: Page | undefined) {
this.recordInteraction = true;
this.resumed = {
pageId,
interaction,
start: 0
};
if (page) {
this.page = page;
this.resumed = true;
}
}

public recordPageView(payload: string | PageAttributes) {
Expand All @@ -103,9 +103,7 @@ export class PageManager {
this.recordInteraction = true;
}

if (!this.page && this.resumed) {
this.createResumedPage(pageId, this.resumed);
} else if (!this.page) {
if (!this.page) {
this.createLandingPage(pageId);
} else if (this.page.pageId !== pageId) {
this.createNextPage(this.page, pageId);
Expand All @@ -126,18 +124,6 @@ export class PageManager {
this.recordPageViewEvent(this.page as Page);
}

private createResumedPage(pageId: string, resumed: Page) {
this.page = {
pageId,
parentPageId: resumed.pageId,
interaction: resumed.interaction + 1,
referrer: document.referrer,
referrerDomain: this.getDomainFromReferrer(),
start: Date.now()
};
this.resumed = undefined;
}

private createNextPage(currentPage: Page, pageId: string) {
let startTime = Date.now();
const interactionTime = this.virtualPageLoadTimer.latestInteractionTime;
Expand All @@ -161,10 +147,14 @@ 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 (startTime - interactionTime <= this.TIMEOUT) {
if (!this.resumed && startTime - interactionTime <= this.TIMEOUT) {
startTime = interactionTime;
this.virtualPageLoadTimer.startTiming();
}

this.timeOnParentPage = startTime - currentPage.start;
this.resumed = false;

this.page = {
pageId,
parentPageId: currentPage.pageId,
Expand Down Expand Up @@ -227,6 +217,7 @@ export class PageManager {
if (page.parentPageId !== undefined) {
pageViewEvent.parentPageInteractionId =
page.parentPageId + '-' + (page.interaction - 1);
pageViewEvent.timeOnParentPage = this.timeOnParentPage;
}

pageViewEvent.referrer = document.referrer;
Expand Down
7 changes: 2 additions & 5 deletions src/sessions/SessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,8 @@ export class SessionManager {

if (cookie && atob) {
try {
this.session = JSON.parse(atob(cookie));
this.pageManager.resumeSession(
this.session.page!.pageId,
this.session.page!.interaction
);
this.session = JSON.parse(atob(cookie)) as Session;
this.pageManager.resumeSession(this.session.page);
} catch (e) {
// Error decoding or parsing the cookie -- ignore
}
Expand Down
32 changes: 32 additions & 0 deletions src/sessions/__integ__/PageManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const recordPageViewWithCustomPageAttributes: Selector = Selector(
const dispatch: Selector = Selector(`#dispatch`);
const clear: Selector = Selector(`#clearRequestResponse`);
const doNotRecordPageView = Selector(`#doNotRecordPageView`);
const pushStateOne = '#pushStateOneToHistory';
const pushStateTwo = '#pushStateTwoToHistory';
const back = '#back';
const createReferrer: Selector = Selector(`#createReferrer`);

fixture('PageViewEventPlugin').page('http://localhost:8080/page_event.html');
Expand Down Expand Up @@ -208,6 +211,35 @@ test('when custom page attributes are set when manually recording page view even
});
});

test('when previous page views occur, time spent is recorded in the subsequent page view event', async (t: TestController) => {
// If we click too soon, the client/event collector plugin will not be loaded and will not record the click.
// This could be a symptom of an issue with RUM web client load speed, or prioritization of script execution.

await t
.wait(300)
.click(pushStateOne)
.click(pushStateTwo)
.click(back)
.click(back)
.click(dispatch)
.expect(REQUEST_BODY.textContent)
.contains('BatchId');

const requestBody = JSON.parse(await REQUEST_BODY.textContent);

const pages = requestBody.RumEvents.filter(
(e) => e.type === PAGE_VIEW_EVENT_TYPE
).map((e) => JSON.parse(e.details));

await t
.expect(pages[0]['timeOnParentPage'])
.typeOf('undefined')
.expect(pages[1]['timeOnParentPage'])
.gte(0)
.expect(pages[2]['timeOnParentPage'])
.gte(0);
});

test('when referrer exists, then page view event details records it', async (t: TestController) => {
// If we click too soon, the client/event collector plugin will not be loaded and will not record the click.
// This could be a symptom of an issue with RUM web client load speed, or prioritization of script execution.
Expand Down

0 comments on commit d1c3b17

Please sign in to comment.