Skip to content
This repository was archived by the owner on Oct 3, 2023. It is now read-only.

Commit 0fdebb3

Browse files
crdgonzalezcadraffensperger
authored andcommitted
Monkey-Patch XMLHttpRequest.send() to send traceparent header and generate XHR span when the this method is called (#142)
* Generate XHR span when send() method is called * Refactoring
1 parent c8abcac commit 0fdebb3

4 files changed

Lines changed: 74 additions & 16 deletions

File tree

packages/opencensus-web-instrumentation-zone/src/monkey-patching.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { XhrWithUrl } from './zone-types';
17+
import { XhrWithOcWebData } from './zone-types';
1818

1919
export function doPatching() {
2020
patchXmlHttpRequestOpen();
21+
patchXmlHttpRequestSend();
2122
}
2223

2324
// Patch the `XMLHttpRequest.open` method to add method used for the request.
@@ -38,6 +39,47 @@ function patchXmlHttpRequestOpen() {
3839
} else {
3940
open.call(this, method, url, true, null, null);
4041
}
41-
(this as XhrWithUrl)._ocweb_method = method;
42+
(this as XhrWithOcWebData)._ocweb_method = method;
4243
};
4344
}
45+
46+
/**
47+
* Patch `send()` method to detect when it has been detected in order to
48+
* dispatch an event and tell the xhr-interceptor that this method has been
49+
* called and it can now generate the XHR span and send the traceparent header.
50+
* This is necessary, as the XHR span generated in the xhr interceptor should
51+
* be created once this method is called.
52+
*/
53+
function patchXmlHttpRequestSend() {
54+
const open = XMLHttpRequest.prototype.send;
55+
56+
XMLHttpRequest.prototype.send = function(
57+
body?:
58+
| string
59+
| Document
60+
| Blob
61+
| ArrayBufferView
62+
| ArrayBuffer
63+
| FormData
64+
| URLSearchParams
65+
| ReadableStream<Uint8Array>
66+
| null
67+
) {
68+
setXhrAttributeHasCalledSend(this);
69+
open.call(this, body);
70+
};
71+
}
72+
73+
/**
74+
* Function to set attribute in the XHR that points out `send()` has been
75+
* called and dispatch the event before the actual `send` is called, then, the
76+
* readyState is still OPENED and xhr interceptor will be able to intercept it.
77+
*
78+
* This is exported to be called in testing as we want to avoid calling the
79+
* actual XHR's `send()`.
80+
*/
81+
export function setXhrAttributeHasCalledSend(xhr: XMLHttpRequest) {
82+
(xhr as XhrWithOcWebData)._ocweb_has_called_send = true;
83+
const event = new Event('readystatechange');
84+
xhr.dispatchEvent(event);
85+
}

packages/opencensus-web-instrumentation-zone/src/xhr-interceptor.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { AsyncTask, XhrWithUrl } from './zone-types';
17+
import { AsyncTask, XhrWithOcWebData } from './zone-types';
1818
import {
1919
RootSpan,
2020
Span,
@@ -38,7 +38,7 @@ const TRACEPARENT_HEADER = 'traceparent';
3838
/**
3939
* Map intended to keep track of current XHR objects associated to a span.
4040
*/
41-
const xhrSpans = new Map<XhrWithUrl, Span>();
41+
const xhrSpans = new Map<XhrWithOcWebData, Span>();
4242

4343
// Keeps track of the current xhr tasks that are running. This is
4444
// useful to clear the Performance Resource Timing entries when no
@@ -56,15 +56,23 @@ export const alreadyAssignedPerfEntries = new Set<PerformanceResourceTiming>();
5656
* Intercepts task as XHR if it is a tracked task and its target object is
5757
* instance of `XMLHttpRequest`.
5858
* In case the task is intercepted, sets the Trace Context Header to it and
59-
* creates a child span related to this XHR in case it is OPENED.
59+
* creates a child span related to this XHR in case it is OPENED and `send()`
60+
* has been called.
6061
* In case the XHR is DONE, end the child span.
6162
*/
6263
export function interceptXhrTask(task: AsyncTask) {
6364
if (!isTrackedTask(task)) return;
6465
if (!(task.target instanceof XMLHttpRequest)) return;
6566

66-
const xhr = task.target as XhrWithUrl;
67-
if (xhr.readyState === XMLHttpRequest.OPENED) {
67+
const xhr = task.target as XhrWithOcWebData;
68+
if (xhr.readyState === XMLHttpRequest.OPENED && xhr._ocweb_has_called_send) {
69+
// Only generate the XHR and send the tracer if it is OPENED and the
70+
// `send()` method has beend called.
71+
// This avoids associating the wrong performance resource timing entries
72+
// to the XHR when `send()` is not called right after `open()` is called.
73+
// This is because there might be a long gap between `open()` and `send()`
74+
// methods are called, and within this gap there might be other HTTP
75+
// requests causing more entries to the Performance Resource buffer.
6876
incrementXhrTaskCount();
6977
const rootSpan: RootSpan = task.zone.get('data').rootSpan;
7078
setTraceparentContextHeader(xhr, rootSpan);
@@ -75,7 +83,7 @@ export function interceptXhrTask(task: AsyncTask) {
7583
}
7684

7785
function setTraceparentContextHeader(
78-
xhr: XhrWithUrl,
86+
xhr: XhrWithOcWebData,
7987
rootSpan: RootSpan
8088
): void {
8189
// `__zone_symbol__xhrURL` is set by the Zone monkey-path.
@@ -98,7 +106,7 @@ function setTraceparentContextHeader(
98106
}
99107
}
100108

101-
function endXhrSpan(xhr: XhrWithUrl): void {
109+
function endXhrSpan(xhr: XhrWithOcWebData): void {
102110
const span = xhrSpans.get(xhr);
103111
if (span) {
104112
// TODO: Investigate more to send the the status code a `number` rather
@@ -124,7 +132,7 @@ function maybeClearPerfResourceBuffer(): void {
124132
}
125133
}
126134

127-
function joinPerfResourceDataToSpan(xhr: XhrWithUrl, span: Span) {
135+
function joinPerfResourceDataToSpan(xhr: XhrWithOcWebData, span: Span) {
128136
const xhrPerfResourceTiming = getXhrPerfomanceData(xhr.responseURL, span);
129137
if (!xhrPerfResourceTiming) return;
130138

packages/opencensus-web-instrumentation-zone/src/zone-types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,18 @@ export interface OnPageInteractionData {
3939
}
4040

4141
/**
42-
* Allows monkey-patching XMLHttpRequest and to obtain the request URL.
42+
* Allows monkey-patching XMLHttpRequest and obtain important data for
43+
* OpenCensus Web such as the request URL or the HTTP method.
4344
* `HTMLElement` is necessary when the xhr is captured from the tasks target
4445
* as the Zone monkey-patch parses xhrs as `HTMLElement & XMLHttpRequest`.
4546
*/
46-
export type XhrWithUrl = HTMLElement &
47+
export type XhrWithOcWebData = HTMLElement &
4748
XMLHttpRequest & {
4849
__zone_symbol__xhrURL: string;
4950
_ocweb_method: string;
51+
// Attribute to tell that `send()` method has been called before it sends
52+
// any HTTP request.
53+
_ocweb_has_called_send: boolean;
5054
};
5155

5256
/** Type for `window` object with variables OpenCensus Web interacts with. */

packages/opencensus-web-instrumentation-zone/test/test-interaction-tracker.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import {
2525
InteractionTracker,
2626
RESET_TRACING_ZONE_DELAY,
2727
} from '../src/interaction-tracker';
28-
import { doPatching } from '../src/monkey-patching';
28+
import {
29+
doPatching,
30+
setXhrAttributeHasCalledSend,
31+
} from '../src/monkey-patching';
2932
import { WindowWithOcwGlobals } from '../src/zone-types';
3033
import { spanContextToTraceParent } from '@opencensus/web-propagation-tracecontext';
3134
import { createFakePerfResourceEntry, spyPerfEntryByType } from './util';
@@ -494,8 +497,12 @@ describe('InteractionTracker', () => {
494497
const xhr = new XMLHttpRequest();
495498
xhr.onreadystatechange = noop;
496499
spyOn(xhr, 'send').and.callFake(() => {
500+
setXhrAttributeHasCalledSend(xhr);
497501
setTimeout(() => {
498502
spyOnProperty(xhr, 'status').and.returnValue(200);
503+
// Fake the readyState as DONE so the xhr interceptor knows when the
504+
// XHR finished.
505+
spyOnProperty(xhr, 'readyState').and.returnValue(XMLHttpRequest.DONE);
499506
const event = new Event('readystatechange');
500507
xhr.dispatchEvent(event);
501508
}, XHR_TIME);
@@ -507,9 +514,6 @@ describe('InteractionTracker', () => {
507514
});
508515

509516
xhr.open('GET', urlRequest);
510-
// Spy on `readystate` property after open, so that way while intercepting
511-
// the XHR will detect OPENED and DONE states.
512-
spyOnProperty(xhr, 'readyState').and.returnValue(XMLHttpRequest.DONE);
513517
xhr.send();
514518
}
515519

0 commit comments

Comments
 (0)