Skip to content

Commit

Permalink
feat: Capture SPA route change timing (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
adebayor123 committed Apr 14, 2022
1 parent 746bb3e commit 91e1303
Show file tree
Hide file tree
Showing 22 changed files with 1,570 additions and 100 deletions.
259 changes: 259 additions & 0 deletions app/spa.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
<!DOCTYPE html>
<html>
<head>
<title>RUM Integ Test for SPA</title>
<script src="./loader_spa.js"></script>

<link
rel="icon"
type="image/png"
href="https://awsmedia.s3.amazonaws.com/favicon.ico"
/>
<script>
function pushStateOneToHistory() {
createXMLRequest();
window.history.pushState(
{ state: 'one' },
'Page One',
'/page_view_one?search=foo#hash1'
);
createXMLRequest();
beginMutation(50);
}

function pushStateTwoToHistory() {
createXMLRequest();
window.history.pushState(
{ state: 'two' },
'Page Two',
'/page_view_two?search=bar#hash2'
);
createXMLRequest();
beginMutation(50);
}

function replaceState() {
createXMLRequest();
window.history.replaceState(
{ state: 'one' },
'Page Ten',
'/page_view_Ten?search=bar#asdf'
);
createXMLRequest();
beginMutation(50);
}

function defaultState() {
createXMLRequest();
window.history.replaceState(
{ state: 'one' },
'Page Ten',
'/page_event.html'
);
createXMLRequest();
beginMutation(50);
}

function createHashChange() {
createXMLRequest();
location.hash = 'hash_change';
createXMLRequest();
beginMutation(50);
}

function createHashChangeWithPushState() {
createXMLRequest();
window.history.pushState(
{ state: 'two' },
'Page Two',
'#hash_changed_pushState'
);
createXMLRequest();
beginMutation(50);
}
function back() {
createXMLRequest();
window.history.back();
createXMLRequest();
beginMutation(50);
}

function back() {
createXMLRequest();
window.history.back();
createXMLRequest();
beginMutation(50);
}

function forward() {
createXMLRequest();
window.history.forward();
createXMLRequest();
beginMutation(50);
}

function go(number) {
createXMLRequest();
window.history.go(number);
createXMLRequest();
beginMutation(50);
}

function beginMutation(epoch) {
let i = 0;
function helper() {
domMutation();
if (i++ < epoch) {
console.log('MUTATING');
setTimeout(helper, 10);
}
}
helper();
}

function dispatch() {
cwr('dispatch');
}

function clearRequestResponse() {
document.getElementById('request_url').innerText = '';
document.getElementById('request_header').innerText = '';
document.getElementById('request_body').innerText = '';

document.getElementById('response_status').innerText = '';
document.getElementById('response_header').innerText = '';
document.getElementById('response_body').innerText = '';
}

function timeoutLoad() {
createXMLRequest();
window.history.pushState(
{ state: 'two' },
'Page Two',
'/page_view_two?search=foo#hash2'
);
beginMutation(200);
}

function createXMLRequest() {
var xhr = new XMLHttpRequest((async = true));
var url = 'https://aws.amazon.com';
xhr.open('GET', url, true);
xhr.send();

setTimeout(sendFetchWithParam, 20);
}

function sendFetchWithParam() {
fetch('https://aws.amazon.com', {
method: 'POST',
body: JSON.stringify('data'),
headers: new Headers({
'Content-Type': 'application/json; charset=UTF-8'
})
});

setTimeout(sendFetchWithoutParam, 20);
}

function sendFetchWithoutParam() {
fetch('https://aws.amazon.com');
}

function domMutation() {
createElement();
deleteElement();
}

function createElement() {
const div = document.createElement('div');
div.setAttribute('id', 'mockMutation');
const welcome = document.getElementById('welcome');
document.body.insertBefore(div, welcome);
}

function deleteElement() {
const div = document.getElementById('mockMutation');
div.remove();
}
</script>

<style>
table {
border-collapse: collapse;
margin-top: 10px;
margin-bottom: 10px;
}

td,
th {
border: 1px solid black;
text-align: left;
padding: 8px;
}
</style>
</head>

<body>
<p id="welcome">This application is used for RUM integ testing.</p>
<hr />
<button id="pushStateOneToHistory" onclick="pushStateOneToHistory()">
Push State One to History
</button>
<button id="pushStateTwoToHistory" onclick="pushStateTwoToHistory()">
Push State Two to History
</button>
<button id="replaceState" onclick="replaceState()">
Replace current state in History
</button>
<button id="replaceDefault" onclick="defaultState()">
Return to default
</button>
<button id="createHashChange" onclick="createHashChange()">
Create HashChange
</button>
<button id="timeOutLoad" onclick="timeoutLoad()">
Create TimeoutLoad
</button>
<button id="back" onclick="back()">Back</button>
<button id="forward" onclick="forward()">Forward</button>
<button id="go-back" onclick="go(-2)">Go (back two pages)</button>
<button id="go-forward" onclick="go(2)">Go (forward two pages)</button>
<hr />
<button id="dispatch" onclick="dispatch()">Dispatch</button>
<button id="clearRequestResponse" onclick="clearRequestResponse()">
Clear
</button>
<hr />
<span id="request"></span>
<span id="response"></span>
<table>
<tr>
<td>Request URL</td>
<td id="request_url"></td>
</tr>
<tr>
<td>Request Header</td>
<td id="request_header"></td>
</tr>
<tr>
<td>Request Body</td>
<td id="request_body"></td>
</tr>
</table>
<table>
<tr>
<td>Response Status Code</td>
<td id="response_status"></td>
</tr>
<tr>
<td>Response Header</td>
<td id="response_header"></td>
</tr>
<tr>
<td>Response Body</td>
<td id="response_body"></td>
</tr>
</table>
</body>
</html>
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ For example, the config object may look similar to the following:
| pagesToInclude | RegExp[] | `[]` | A list of regular expressions which specify the `window.location` values for which the web client will record data. Pages are matched using the `RegExp.test()` function.<br/><br/>For example, when `pagesToInclude: [ /\/home/ ]`, then data from `https://amazonaws.com/home` will be included, and `https://amazonaws.com/` will not be included. |
| pagesToExclude | RegExp[] | `[]` | A list of regular expressions which specify the `window.location` values for which the web client will record data. Pages are matched using the `RegExp.test()` function.<br/><br/>For example, when `pagesToExclude: [ /\/home/ ]`, then data from `https://amazonaws.com/home` will be excluded, and `https://amazonaws.com/` will not be excluded. |
| recordResourceUrl | Boolean | `true` | When this field is `false`, the web client will not record the URLs of resources downloaded by your application.<br/><br/> Some types of resources (e.g., profile images) may be referenced by URLs which contain PII. If this applies to your application, you must set this field to `false` to comply with CloudWatch RUM's shared responsibility model. |
| routeChangeComplete | Number | `100` | The interval (in milliseconds) for which when no HTTP or DOM activity has been observed, an active route change is marked as complete. Note that `routeChangeComplete` must be strictly less than `routeChangeTimeout`. |
| routeChangeTimeout | Number | `10000` | The maximum time (in milliseconds) a route change may take. If a route change does not complete before the timeout, no timing data is recorded for the route change. If your application's route changes may take longer than the default timeout (i.e., more than 10 second), you should increase the value of the timeout. |
| sessionEventLimit | Number | `200` | The maximum number of events to record during a single session. |
| sessionSampleRate | Number | `1` | The proportion of sessions that will be recorded by the web client, specified as a unit interval (a number greater than or equal to 0 and less than or equal to 1). When this field is `0`, no sessions will be recorded. When this field is `1`, all sessions will be recorded. |
| telemetries | [Telemetry Config Array](#telemetry-config-array) | `[]` | See [Telemetry Config Array](#telemetry-config-array) |
Expand Down
3 changes: 2 additions & 1 deletion nightwatch.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ module.exports = {
// An array of folders (excluding subfolders) where your tests are located;
// if this is not specified, the test source must be passed as the second argument to the test runner.
src_folders: [
'src/plugins/event-plugins/__nightwatch__/PageViewPlugin.test.js'
'src/plugins/event-plugins/__nightwatch__/PageViewPlugin.test.js',
'src/sessions/__nightwatch__/VirtualPageLoadTimer.test.js'
],

// See https://nightwatchjs.org/guide/working-with-page-objects/
Expand Down
3 changes: 2 additions & 1 deletion src/dispatch/__tests__/Dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Dispatch } from '../Dispatch';
import * as Utils from '../../test-utils/test-utils';
import { DataPlaneClient } from '../DataPlaneClient';
import { CredentialProvider } from '@aws-sdk/types';
import { DEFAULT_CONFIG } from '../../test-utils/test-utils';
import { DEFAULT_CONFIG, mockFetch } from '../../test-utils/test-utils';

global.fetch = mockFetch;
const sendFetch = jest.fn(() => Promise.resolve());
const sendBeacon = jest.fn(() => Promise.resolve());
jest.mock('../DataPlaneClient', () => ({
Expand Down
3 changes: 2 additions & 1 deletion src/event-cache/__tests__/EventCache.integ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { EventCache } from '../EventCache';
import { advanceTo } from 'jest-date-mock';
import * as Utils from '../../test-utils/test-utils';
import { RumEvent } from '../../dispatch/dataplane';
import { DEFAULT_CONFIG } from '../../test-utils/test-utils';
import { DEFAULT_CONFIG, mockFetch } from '../../test-utils/test-utils';
import { SESSION_START_EVENT_TYPE } from '../../sessions/SessionManager';

global.fetch = mockFetch;
describe('EventCache tests', () => {
beforeAll(() => {
advanceTo(0);
Expand Down
3 changes: 2 additions & 1 deletion src/event-cache/__tests__/EventCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { advanceTo } from 'jest-date-mock';
import * as Utils from '../../test-utils/test-utils';
import { SessionManager } from '../../sessions/SessionManager';
import { RumEvent } from '../../dispatch/dataplane';
import { DEFAULT_CONFIG } from '../../test-utils/test-utils';
import { DEFAULT_CONFIG, mockFetch } from '../../test-utils/test-utils';

global.fetch = mockFetch;
const getSession = jest.fn(() => ({
sessionId: 'a',
record: true,
Expand Down
36 changes: 2 additions & 34 deletions src/event-schemas/navigation-event.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,9 @@
"type": "string",
"description": "Schema version."
},
"targetUrl": {
"description": "Page URL",
"type": "string"
},
"initiatorType": {
"type": "string",
"enum": ["navigation"]
"enum": ["navigation", "route_change"]
},
"navigationType": {
"description": "An unsigned short which indicates how the navigation to this page was done. Possible values are:TYPE_NAVIGATE (0), TYPE_RELOAD (1), TYPE_BACK_FORWARD (2), TYPE_RESERVED (255)",
Expand Down Expand Up @@ -120,33 +116,5 @@
}
},
"additionalProperties": false,
"required": [
"version",
"initiatorType",
"startTime",
"unloadEventStart",
"promptForUnload",
"redirectStart",
"redirectTime",
"fetchStart",
"domainLookupStart",
"dns",
"connectStart",
"connect",
"secureConnectionStart",
"tlsTime",
"requestStart",
"timeToFirstByte",
"responseStart",
"responseTime",
"domInteractive",
"domContentLoadedEventStart",
"domContentLoaded",
"domComplete",
"domProcessingTime",
"loadEventStart",
"loadEventTime",
"duration",
"navigationTimingLevel"
]
"required": ["version", "initiatorType", "startTime", "duration"]
}
17 changes: 17 additions & 0 deletions src/loader/loader-spa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { loader } from '../loader/loader';
import { showRequestClientBuilder } from '../test-utils/mock-http-handler';
loader('cwr', 'abc123', '1.0', 'us-west-2', './rum_javascript_telemetry.js', {
allowCookies: true,
dispatchInterval: 0,
disableAutoPageView: false,
pagesToExclude: [/\/page_view_do_not_record/],
telemetries: ['performance'],
pageIdFormat: 'PATH_AND_HASH',
clientBuilder: showRequestClientBuilder,
routeChangeTimeout: 1000
});
window.cwr('setAwsCredentials', {
accessKeyId: 'a',
secretAccessKey: 'b',
sessionToken: 'c'
});

0 comments on commit 91e1303

Please sign in to comment.