Skip to content

Commit ece1306

Browse files
author
Reno
authored
fix: capture PerformanceNavigationTiming events even when window.load fires before plugin loads (#81)
1 parent 5492091 commit ece1306

File tree

5 files changed

+422
-15
lines changed

5 files changed

+422
-15
lines changed

app/delayed_page.html

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>RUM Integ Test</title>
5+
<link
6+
rel="icon"
7+
type="image/png"
8+
href="https://awsmedia.s3.amazonaws.com/favicon.ico"
9+
/>
10+
<script>
11+
function createJSError() {
12+
// TypeError: null has no properties
13+
null.foo;
14+
}
15+
16+
function onSubmitCommand() {
17+
const command = document.getElementById('command').value;
18+
if (document.getElementById('payload').value) {
19+
const payload = JSON.parse(
20+
document.getElementById('payload').value
21+
);
22+
cwr(command, payload);
23+
} else {
24+
cwr(command);
25+
}
26+
}
27+
</script>
28+
29+
<style>
30+
table {
31+
border-collapse: collapse;
32+
margin-top: 10px;
33+
margin-bottom: 10px;
34+
}
35+
36+
td,
37+
th {
38+
border: 1px solid black;
39+
text-align: left;
40+
padding: 8px;
41+
}
42+
</style>
43+
</head>
44+
45+
<body>
46+
<p id="welcome">This application is used for RUM integ testing.</p>
47+
<form>
48+
<label for="command">Command : </label>
49+
<input type="text" id="command" /><br /><br />
50+
<label for="payload">Payload : </label>
51+
<textarea id="payload"></textarea><br /><br />
52+
<input
53+
type="button"
54+
id="submit"
55+
value="Submit"
56+
onclick="onSubmitCommand()"
57+
/>
58+
</form>
59+
<br />
60+
<button id="button1">Test ClickEvent1</button>
61+
<button id="button2">Test ClickEvent2</button>
62+
<button id="createJSError" onclick="createJSError()">
63+
Create JS Error
64+
</button>
65+
<script>
66+
function createJSError() {
67+
// TypeError: null has no properties
68+
null.foo;
69+
}
70+
</script>
71+
<script>
72+
function createHTTPError() {
73+
var xhr = new XMLHttpRequest();
74+
xhr.open(
75+
'POST',
76+
'https://www.google-analytics.com/collect',
77+
true
78+
);
79+
xhr.setRequestHeader('Content-type', 'application/json');
80+
xhr.send();
81+
}
82+
</script>
83+
<script>
84+
function disallowCookies() {
85+
cwr('allowCookies', false);
86+
}
87+
</script>
88+
<button id="createHTTPError" onclick="createHTTPError()">
89+
Create HTTP Error
90+
</button>
91+
<button id="randomSessionClick">Random Session click</button>
92+
<button id="disallowCookies" onclick="disallowCookies()">
93+
Disallow Cookies
94+
</button>
95+
<span id="request"></span>
96+
<span id="response"></span>
97+
<table>
98+
<tr>
99+
<td>Request URL</td>
100+
<td id="request_url"></td>
101+
</tr>
102+
<tr>
103+
<td>Request Header</td>
104+
<td id="request_header"></td>
105+
</tr>
106+
<tr>
107+
<td>Request Body</td>
108+
<td id="request_body"></td>
109+
</tr>
110+
</table>
111+
<table>
112+
<tr>
113+
<td>Response Status Code</td>
114+
<td id="response_status"></td>
115+
</tr>
116+
<tr>
117+
<td>Response Header</td>
118+
<td id="response_header"></td>
119+
</tr>
120+
<tr>
121+
<td>Response Body</td>
122+
<td id="response_body"></td>
123+
</tr>
124+
</table>
125+
<script>
126+
window.onload = function () {
127+
var scriptElement = document.createElement('script');
128+
scriptElement.type = 'text/javascript';
129+
scriptElement.src = './loader_standard.js';
130+
document.head.appendChild(scriptElement);
131+
};
132+
</script>
133+
</body>
134+
</html>

src/plugins/event-plugins/NavigationPlugin.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,20 @@ export class NavigationPlugin implements Plugin {
2222
this.enabled = true;
2323
}
2424

25+
/**
26+
* If the client is initialized after the window has fired a load event,
27+
* invoke the callback method directly to trigger the record event.
28+
* Otherwise, keep the original implementation to add callback method to eventListener.
29+
* However, if the page finishes loading right before adding addEventListener, we still cannot provide data
30+
*/
2531
load(context: PluginContext): void {
2632
this.recordEvent = context.record;
2733
if (this.enabled) {
28-
window.addEventListener(LOAD, this.eventListener);
34+
if (this.hasTheWindowLoadEventFired()) {
35+
this.eventListener();
36+
} else {
37+
window.addEventListener(LOAD, this.eventListener);
38+
}
2939
}
3040
}
3141

@@ -47,6 +57,24 @@ export class NavigationPlugin implements Plugin {
4757
}
4858
}
4959

60+
/**
61+
* Use the loadEventEnd field from window.performance to check if the website
62+
* has loaded already.
63+
* @returns boolean
64+
*/
65+
hasTheWindowLoadEventFired() {
66+
if (
67+
window.performance &&
68+
window.performance.getEntriesByType(NAVIGATION).length
69+
) {
70+
const navData = window.performance.getEntriesByType(
71+
NAVIGATION
72+
)[0] as PerformanceNavigationTiming;
73+
return Boolean(navData.loadEventEnd);
74+
}
75+
return false;
76+
}
77+
5078
getPluginId(): string {
5179
return this.pluginId;
5280
}
@@ -57,23 +85,18 @@ export class NavigationPlugin implements Plugin {
5785
*
5886
* If browser provides support, use Navigation Timing Level 2 specification -
5987
* https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming
88+
*
89+
* Only the current document resource is included in the performance timeline;
90+
* there is only one PerformanceNavigationTiming object in the performance timeline.
91+
* https://www.w3.org/TR/navigation-timing-2/
6092
*/
6193
eventListener = () => {
6294
if (performance.getEntriesByType(NAVIGATION).length === 0) {
6395
this.performanceNavigationEventHandlerTimingLevel1();
6496
} else {
65-
const navigationObserver = new PerformanceObserver((list) => {
66-
list.getEntries().forEach((event) => {
67-
if (event.entryType === NAVIGATION) {
68-
this.performanceNavigationEventHandlerTimingLevel2(
69-
event
70-
);
71-
}
72-
});
73-
});
74-
navigationObserver.observe({
75-
entryTypes: [NAVIGATION]
76-
});
97+
this.performanceNavigationEventHandlerTimingLevel2(
98+
performance.getEntriesByType(NAVIGATION)[0]
99+
);
77100
}
78101
};
79102

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import {
2+
STATUS_202,
3+
DISPATCH_COMMAND,
4+
COMMAND,
5+
PAYLOAD,
6+
SUBMIT,
7+
REQUEST_BODY,
8+
RESPONSE_STATUS,
9+
ID,
10+
TIMESTAMP
11+
} from '../../../test-utils/integ-test-utils';
12+
import { PERFORMANCE_NAVIGATION_EVENT_TYPE } from '../../utils/constant';
13+
14+
const INITIATOR_TYPE = 'initiatorType';
15+
const NAVIGATION_TYPE = 'navigationType';
16+
const START_TIME = 'startTime';
17+
const UNLOAD_EVENT_START = 'unloadEventStart';
18+
const PROMPT_FOR_UNLOAD = 'promptForUnload';
19+
const REDIRECT_COUNT = 'redirectCount';
20+
const REDIRECT_START = 'redirectStart';
21+
const REDIRECT_TIME = 'redirectTime';
22+
const WORKER_START = 'workerStart';
23+
const WORKER_TIME = 'workerTime';
24+
const FETCH_START = 'fetchStart';
25+
const DOMAIN_LOOKUP_START = 'domainLookupStart';
26+
const DNS = 'dns';
27+
const NEXT_HOP_PROTOCOL = 'nextHopProtocol';
28+
const CONNECT_START = 'connectStart';
29+
const CONNECT = 'connect';
30+
const SECURE_CONNECTION_START = 'secureConnectionStart';
31+
const TLS_TIME = 'tlsTime';
32+
const REQUEST_START = 'requestStart';
33+
const TIME_TO_FIRST_BYTE = 'timeToFirstByte';
34+
const RESPONSE_START = 'responseStart';
35+
const RESPONSE_TIME = 'responseTime';
36+
const DOM_INTERACTIVE = 'domInteractive';
37+
const DOM_CONTENT_LOADED_EVENT_START = 'domContentLoadedEventStart';
38+
const DOM_CONTENT_LOADED = 'domContentLoaded';
39+
const DOM_COMPLETE = 'domComplete';
40+
const DOM_PROCESSING_TIME = 'domProcessingTime';
41+
const LOAD_EVENT_START = 'loadEventStart';
42+
const LOAD_EVENT_TIME = 'loadEventTime';
43+
const DURATION = 'duration';
44+
const HEADER_SIZE = 'headerSize';
45+
const TRANSFER_SIZE = 'transferSize';
46+
const COMPRESSION_RATIO = 'compressionRatio';
47+
const SAFARI = 'Safari';
48+
49+
fixture('NagivationEvent Plugin').page(
50+
'http://localhost:8080/delayed_page.html'
51+
);
52+
53+
test('when plugin loads after window.load then navigation timing events are recorded', async (t: TestController) => {
54+
await t
55+
.typeText(COMMAND, DISPATCH_COMMAND, { replace: true })
56+
.click(PAYLOAD)
57+
.pressKey('ctrl+a delete')
58+
.click(SUBMIT);
59+
60+
const isBrowserSafari =
61+
(await REQUEST_BODY.textContent).indexOf(SAFARI) > -1;
62+
63+
await t
64+
.expect(REQUEST_BODY.textContent)
65+
.contains(PERFORMANCE_NAVIGATION_EVENT_TYPE)
66+
.expect(REQUEST_BODY.textContent)
67+
.contains(ID)
68+
.expect(REQUEST_BODY.textContent)
69+
.contains(TIMESTAMP)
70+
71+
.expect(REQUEST_BODY.textContent)
72+
.contains(INITIATOR_TYPE)
73+
.expect(REQUEST_BODY.textContent)
74+
.contains(START_TIME)
75+
.expect(REQUEST_BODY.textContent)
76+
.contains(UNLOAD_EVENT_START)
77+
.expect(REQUEST_BODY.textContent)
78+
.contains(PROMPT_FOR_UNLOAD)
79+
.expect(REQUEST_BODY.textContent)
80+
.contains(REDIRECT_START)
81+
.expect(REQUEST_BODY.textContent)
82+
.contains(REDIRECT_TIME)
83+
84+
.expect(REQUEST_BODY.textContent)
85+
.contains(FETCH_START)
86+
.expect(REQUEST_BODY.textContent)
87+
.contains(DOMAIN_LOOKUP_START)
88+
.expect(REQUEST_BODY.textContent)
89+
.contains(DNS)
90+
91+
.expect(REQUEST_BODY.textContent)
92+
.contains(CONNECT_START)
93+
.expect(REQUEST_BODY.textContent)
94+
.contains(CONNECT)
95+
.expect(REQUEST_BODY.textContent)
96+
.contains(SECURE_CONNECTION_START)
97+
.expect(REQUEST_BODY.textContent)
98+
.contains(TLS_TIME)
99+
.expect(REQUEST_BODY.textContent)
100+
.contains(REQUEST_START)
101+
.expect(REQUEST_BODY.textContent)
102+
.contains(TIME_TO_FIRST_BYTE)
103+
.expect(REQUEST_BODY.textContent)
104+
.contains(RESPONSE_START)
105+
.expect(REQUEST_BODY.textContent)
106+
.contains(RESPONSE_TIME)
107+
.expect(REQUEST_BODY.textContent)
108+
.contains(DOM_INTERACTIVE)
109+
.expect(REQUEST_BODY.textContent)
110+
.contains(DOM_CONTENT_LOADED_EVENT_START)
111+
.expect(REQUEST_BODY.textContent)
112+
.contains(DOM_CONTENT_LOADED)
113+
.expect(REQUEST_BODY.textContent)
114+
.contains(DOM_COMPLETE)
115+
.expect(REQUEST_BODY.textContent)
116+
.contains(DOM_PROCESSING_TIME)
117+
.expect(REQUEST_BODY.textContent)
118+
.contains(LOAD_EVENT_START)
119+
.expect(REQUEST_BODY.textContent)
120+
.contains(LOAD_EVENT_TIME)
121+
.expect(REQUEST_BODY.textContent)
122+
.contains(DURATION)
123+
124+
.expect(RESPONSE_STATUS.textContent)
125+
.eql(STATUS_202.toString());
126+
127+
/**
128+
* Deprecated Timing Level1 used for Safari browser do not contain following attributes
129+
* https://nicj.net/navigationtiming-in-practice/
130+
*/
131+
if (!isBrowserSafari) {
132+
await t
133+
.expect(REQUEST_BODY.textContent)
134+
.contains(REDIRECT_COUNT)
135+
.expect(REQUEST_BODY.textContent)
136+
.contains(NAVIGATION_TYPE)
137+
.expect(REQUEST_BODY.textContent)
138+
.contains(WORKER_START)
139+
.expect(REQUEST_BODY.textContent)
140+
.contains(WORKER_TIME)
141+
.expect(REQUEST_BODY.textContent)
142+
.contains(NEXT_HOP_PROTOCOL)
143+
.expect(REQUEST_BODY.textContent)
144+
.contains(HEADER_SIZE)
145+
.expect(REQUEST_BODY.textContent)
146+
.contains(TRANSFER_SIZE)
147+
.expect(REQUEST_BODY.textContent)
148+
.contains(COMPRESSION_RATIO);
149+
}
150+
});

0 commit comments

Comments
 (0)