Skip to content

Commit 91e1303

Browse files
author
Reno
authored
feat: Capture SPA route change timing (#134)
1 parent 746bb3e commit 91e1303

File tree

22 files changed

+1570
-100
lines changed

22 files changed

+1570
-100
lines changed

app/spa.html

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>RUM Integ Test for SPA</title>
5+
<script src="./loader_spa.js"></script>
6+
7+
<link
8+
rel="icon"
9+
type="image/png"
10+
href="https://awsmedia.s3.amazonaws.com/favicon.ico"
11+
/>
12+
<script>
13+
function pushStateOneToHistory() {
14+
createXMLRequest();
15+
window.history.pushState(
16+
{ state: 'one' },
17+
'Page One',
18+
'/page_view_one?search=foo#hash1'
19+
);
20+
createXMLRequest();
21+
beginMutation(50);
22+
}
23+
24+
function pushStateTwoToHistory() {
25+
createXMLRequest();
26+
window.history.pushState(
27+
{ state: 'two' },
28+
'Page Two',
29+
'/page_view_two?search=bar#hash2'
30+
);
31+
createXMLRequest();
32+
beginMutation(50);
33+
}
34+
35+
function replaceState() {
36+
createXMLRequest();
37+
window.history.replaceState(
38+
{ state: 'one' },
39+
'Page Ten',
40+
'/page_view_Ten?search=bar#asdf'
41+
);
42+
createXMLRequest();
43+
beginMutation(50);
44+
}
45+
46+
function defaultState() {
47+
createXMLRequest();
48+
window.history.replaceState(
49+
{ state: 'one' },
50+
'Page Ten',
51+
'/page_event.html'
52+
);
53+
createXMLRequest();
54+
beginMutation(50);
55+
}
56+
57+
function createHashChange() {
58+
createXMLRequest();
59+
location.hash = 'hash_change';
60+
createXMLRequest();
61+
beginMutation(50);
62+
}
63+
64+
function createHashChangeWithPushState() {
65+
createXMLRequest();
66+
window.history.pushState(
67+
{ state: 'two' },
68+
'Page Two',
69+
'#hash_changed_pushState'
70+
);
71+
createXMLRequest();
72+
beginMutation(50);
73+
}
74+
function back() {
75+
createXMLRequest();
76+
window.history.back();
77+
createXMLRequest();
78+
beginMutation(50);
79+
}
80+
81+
function back() {
82+
createXMLRequest();
83+
window.history.back();
84+
createXMLRequest();
85+
beginMutation(50);
86+
}
87+
88+
function forward() {
89+
createXMLRequest();
90+
window.history.forward();
91+
createXMLRequest();
92+
beginMutation(50);
93+
}
94+
95+
function go(number) {
96+
createXMLRequest();
97+
window.history.go(number);
98+
createXMLRequest();
99+
beginMutation(50);
100+
}
101+
102+
function beginMutation(epoch) {
103+
let i = 0;
104+
function helper() {
105+
domMutation();
106+
if (i++ < epoch) {
107+
console.log('MUTATING');
108+
setTimeout(helper, 10);
109+
}
110+
}
111+
helper();
112+
}
113+
114+
function dispatch() {
115+
cwr('dispatch');
116+
}
117+
118+
function clearRequestResponse() {
119+
document.getElementById('request_url').innerText = '';
120+
document.getElementById('request_header').innerText = '';
121+
document.getElementById('request_body').innerText = '';
122+
123+
document.getElementById('response_status').innerText = '';
124+
document.getElementById('response_header').innerText = '';
125+
document.getElementById('response_body').innerText = '';
126+
}
127+
128+
function timeoutLoad() {
129+
createXMLRequest();
130+
window.history.pushState(
131+
{ state: 'two' },
132+
'Page Two',
133+
'/page_view_two?search=foo#hash2'
134+
);
135+
beginMutation(200);
136+
}
137+
138+
function createXMLRequest() {
139+
var xhr = new XMLHttpRequest((async = true));
140+
var url = 'https://aws.amazon.com';
141+
xhr.open('GET', url, true);
142+
xhr.send();
143+
144+
setTimeout(sendFetchWithParam, 20);
145+
}
146+
147+
function sendFetchWithParam() {
148+
fetch('https://aws.amazon.com', {
149+
method: 'POST',
150+
body: JSON.stringify('data'),
151+
headers: new Headers({
152+
'Content-Type': 'application/json; charset=UTF-8'
153+
})
154+
});
155+
156+
setTimeout(sendFetchWithoutParam, 20);
157+
}
158+
159+
function sendFetchWithoutParam() {
160+
fetch('https://aws.amazon.com');
161+
}
162+
163+
function domMutation() {
164+
createElement();
165+
deleteElement();
166+
}
167+
168+
function createElement() {
169+
const div = document.createElement('div');
170+
div.setAttribute('id', 'mockMutation');
171+
const welcome = document.getElementById('welcome');
172+
document.body.insertBefore(div, welcome);
173+
}
174+
175+
function deleteElement() {
176+
const div = document.getElementById('mockMutation');
177+
div.remove();
178+
}
179+
</script>
180+
181+
<style>
182+
table {
183+
border-collapse: collapse;
184+
margin-top: 10px;
185+
margin-bottom: 10px;
186+
}
187+
188+
td,
189+
th {
190+
border: 1px solid black;
191+
text-align: left;
192+
padding: 8px;
193+
}
194+
</style>
195+
</head>
196+
197+
<body>
198+
<p id="welcome">This application is used for RUM integ testing.</p>
199+
<hr />
200+
<button id="pushStateOneToHistory" onclick="pushStateOneToHistory()">
201+
Push State One to History
202+
</button>
203+
<button id="pushStateTwoToHistory" onclick="pushStateTwoToHistory()">
204+
Push State Two to History
205+
</button>
206+
<button id="replaceState" onclick="replaceState()">
207+
Replace current state in History
208+
</button>
209+
<button id="replaceDefault" onclick="defaultState()">
210+
Return to default
211+
</button>
212+
<button id="createHashChange" onclick="createHashChange()">
213+
Create HashChange
214+
</button>
215+
<button id="timeOutLoad" onclick="timeoutLoad()">
216+
Create TimeoutLoad
217+
</button>
218+
<button id="back" onclick="back()">Back</button>
219+
<button id="forward" onclick="forward()">Forward</button>
220+
<button id="go-back" onclick="go(-2)">Go (back two pages)</button>
221+
<button id="go-forward" onclick="go(2)">Go (forward two pages)</button>
222+
<hr />
223+
<button id="dispatch" onclick="dispatch()">Dispatch</button>
224+
<button id="clearRequestResponse" onclick="clearRequestResponse()">
225+
Clear
226+
</button>
227+
<hr />
228+
<span id="request"></span>
229+
<span id="response"></span>
230+
<table>
231+
<tr>
232+
<td>Request URL</td>
233+
<td id="request_url"></td>
234+
</tr>
235+
<tr>
236+
<td>Request Header</td>
237+
<td id="request_header"></td>
238+
</tr>
239+
<tr>
240+
<td>Request Body</td>
241+
<td id="request_body"></td>
242+
</tr>
243+
</table>
244+
<table>
245+
<tr>
246+
<td>Response Status Code</td>
247+
<td id="response_status"></td>
248+
</tr>
249+
<tr>
250+
<td>Response Header</td>
251+
<td id="response_header"></td>
252+
</tr>
253+
<tr>
254+
<td>Response Body</td>
255+
<td id="response_body"></td>
256+
</tr>
257+
</table>
258+
</body>
259+
</html>

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ For example, the config object may look similar to the following:
3232
| 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. |
3333
| 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. |
3434
| 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. |
35+
| 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`. |
36+
| 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. |
3537
| sessionEventLimit | Number | `200` | The maximum number of events to record during a single session. |
3638
| 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. |
3739
| telemetries | [Telemetry Config Array](#telemetry-config-array) | `[]` | See [Telemetry Config Array](#telemetry-config-array) |

nightwatch.conf.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ module.exports = {
77
// An array of folders (excluding subfolders) where your tests are located;
88
// if this is not specified, the test source must be passed as the second argument to the test runner.
99
src_folders: [
10-
'src/plugins/event-plugins/__nightwatch__/PageViewPlugin.test.js'
10+
'src/plugins/event-plugins/__nightwatch__/PageViewPlugin.test.js',
11+
'src/sessions/__nightwatch__/VirtualPageLoadTimer.test.js'
1112
],
1213

1314
// See https://nightwatchjs.org/guide/working-with-page-objects/

src/dispatch/__tests__/Dispatch.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { Dispatch } from '../Dispatch';
22
import * as Utils from '../../test-utils/test-utils';
33
import { DataPlaneClient } from '../DataPlaneClient';
44
import { CredentialProvider } from '@aws-sdk/types';
5-
import { DEFAULT_CONFIG } from '../../test-utils/test-utils';
5+
import { DEFAULT_CONFIG, mockFetch } from '../../test-utils/test-utils';
66

7+
global.fetch = mockFetch;
78
const sendFetch = jest.fn(() => Promise.resolve());
89
const sendBeacon = jest.fn(() => Promise.resolve());
910
jest.mock('../DataPlaneClient', () => ({

src/event-cache/__tests__/EventCache.integ.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { EventCache } from '../EventCache';
22
import { advanceTo } from 'jest-date-mock';
33
import * as Utils from '../../test-utils/test-utils';
44
import { RumEvent } from '../../dispatch/dataplane';
5-
import { DEFAULT_CONFIG } from '../../test-utils/test-utils';
5+
import { DEFAULT_CONFIG, mockFetch } from '../../test-utils/test-utils';
66
import { SESSION_START_EVENT_TYPE } from '../../sessions/SessionManager';
77

8+
global.fetch = mockFetch;
89
describe('EventCache tests', () => {
910
beforeAll(() => {
1011
advanceTo(0);

src/event-cache/__tests__/EventCache.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { advanceTo } from 'jest-date-mock';
33
import * as Utils from '../../test-utils/test-utils';
44
import { SessionManager } from '../../sessions/SessionManager';
55
import { RumEvent } from '../../dispatch/dataplane';
6-
import { DEFAULT_CONFIG } from '../../test-utils/test-utils';
6+
import { DEFAULT_CONFIG, mockFetch } from '../../test-utils/test-utils';
77

8+
global.fetch = mockFetch;
89
const getSession = jest.fn(() => ({
910
sessionId: 'a',
1011
record: true,

src/event-schemas/navigation-event.json

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,9 @@
99
"type": "string",
1010
"description": "Schema version."
1111
},
12-
"targetUrl": {
13-
"description": "Page URL",
14-
"type": "string"
15-
},
1612
"initiatorType": {
1713
"type": "string",
18-
"enum": ["navigation"]
14+
"enum": ["navigation", "route_change"]
1915
},
2016
"navigationType": {
2117
"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)",
@@ -120,33 +116,5 @@
120116
}
121117
},
122118
"additionalProperties": false,
123-
"required": [
124-
"version",
125-
"initiatorType",
126-
"startTime",
127-
"unloadEventStart",
128-
"promptForUnload",
129-
"redirectStart",
130-
"redirectTime",
131-
"fetchStart",
132-
"domainLookupStart",
133-
"dns",
134-
"connectStart",
135-
"connect",
136-
"secureConnectionStart",
137-
"tlsTime",
138-
"requestStart",
139-
"timeToFirstByte",
140-
"responseStart",
141-
"responseTime",
142-
"domInteractive",
143-
"domContentLoadedEventStart",
144-
"domContentLoaded",
145-
"domComplete",
146-
"domProcessingTime",
147-
"loadEventStart",
148-
"loadEventTime",
149-
"duration",
150-
"navigationTimingLevel"
151-
]
119+
"required": ["version", "initiatorType", "startTime", "duration"]
152120
}

src/loader/loader-spa.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { loader } from '../loader/loader';
2+
import { showRequestClientBuilder } from '../test-utils/mock-http-handler';
3+
loader('cwr', 'abc123', '1.0', 'us-west-2', './rum_javascript_telemetry.js', {
4+
allowCookies: true,
5+
dispatchInterval: 0,
6+
disableAutoPageView: false,
7+
pagesToExclude: [/\/page_view_do_not_record/],
8+
telemetries: ['performance'],
9+
pageIdFormat: 'PATH_AND_HASH',
10+
clientBuilder: showRequestClientBuilder,
11+
routeChangeTimeout: 1000
12+
});
13+
window.cwr('setAwsCredentials', {
14+
accessKeyId: 'a',
15+
secretAccessKey: 'b',
16+
sessionToken: 'c'
17+
});

0 commit comments

Comments
 (0)