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

Commit c8abcac

Browse files
authored
Merge pull request #132 from crdgonzalezca/join_browser_data
Join browser performance data with the XHR spans
2 parents 8758622 + a276a58 commit c8abcac

15 files changed

Lines changed: 871 additions & 200 deletions

File tree

examples/user_interaction/server/server.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ function handleRequest(request, response) {
7171
request.on('data', chunk => body.push(chunk));
7272

7373
// Necessary header because the Node.js and React dev servers run in different ports.
74-
response.setHeader('Access-Control-Allow-Origin', '*');
74+
response.setHeader('Access-Control-Allow-Origin', '*');
75+
// Header to show more info in the span annotations.
76+
response.setHeader('Timing-Allow-Origin', '*');
7577

7678
let result = '';
7779
let code = 200;

packages/opencensus-web-exporter-ocagent/src/adapters.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ const RECENT_EPOCH_MS = 1500000000000; // July 13, 2017.
2727
/**
2828
* Converts a RootSpan type from @opencensus/web-core to the Span JSON structure
2929
* expected by the OpenCensus Agent's HTTP/JSON (grpc-gateway) API.
30+
* Also adapts all its descendants.
3031
*/
3132
export function adaptRootSpan(rootSpan: webCore.RootSpan): apiTypes.Span[] {
32-
const adaptedSpans: apiTypes.Span[] = rootSpan.spans.map(adaptSpan);
33+
const adaptedSpans: apiTypes.Span[] = rootSpan
34+
.allDescendants()
35+
.map(span => adaptSpan(span as webCore.Span));
3336
adaptedSpans.unshift(adaptSpan(rootSpan));
3437
return adaptedSpans;
3538
}

packages/opencensus-web-instrumentation-perf/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ export {
2222
getPerfEntries,
2323
clearPerfEntries,
2424
} from './perf-grouper';
25+
export { annotationsForPerfTimeFields } from './util';
26+
export { PERFORMANCE_ENTRY_EVENTS, getResourceSpan } from './resource-span';

packages/opencensus-web-instrumentation-perf/src/resource-span.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { PerformanceResourceTimingExtended } from './perf-types';
1919
import { annotationsForPerfTimeFields } from './util';
2020

2121
/** PerformanceEntry time event fields to create as span annotations. */
22-
const PERFORMANCE_ENTRY_EVENTS = [
22+
export const PERFORMANCE_ENTRY_EVENTS = [
2323
'workerStart',
2424
'fetchStart',
2525
'domainLookupStart',

packages/opencensus-web-instrumentation-zone/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"dependencies": {
7070
"@opencensus/web-core": "^0.0.3",
7171
"@opencensus/web-exporter-ocagent": "^0.0.3",
72+
"@opencensus/web-instrumentation-perf": "0.0.3",
7273
"@opencensus/web-propagation-tracecontext": "0.0.3"
7374
},
7475
"peerDependencies": {

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

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,15 @@ import {
1919
tracing,
2020
SpanKind,
2121
RootSpan,
22-
ATTRIBUTE_HTTP_STATUS_CODE,
23-
ATTRIBUTE_HTTP_METHOD,
24-
parseUrl,
25-
Span,
2622
} from '@opencensus/web-core';
27-
import { AsyncTask, XHRWithUrl } from './zone-types';
23+
import { AsyncTask } from './zone-types';
2824
import {
2925
OnPageInteractionStopwatch,
3026
startOnPageInteraction,
3127
} from './on-page-interaction-stop-watch';
3228

33-
import { spanContextToTraceParent } from '@opencensus/web-propagation-tracecontext';
34-
import { traceOriginMatchesOrSameOrigin } from './util';
29+
import { isTrackedTask } from './util';
30+
import { interceptXhrTask } from './xhr-interceptor';
3531

3632
// Allows us to monkey patch Zone prototype without TS compiler errors.
3733
declare const Zone: ZoneType & { prototype: Zone };
@@ -57,10 +53,6 @@ export class InteractionTracker {
5753
[index: string]: number;
5854
} = {};
5955

60-
// Map intended to keep track of current XHR objects
61-
// associated to a span.
62-
private readonly xhrSpans = new Map<XHRWithUrl, Span>();
63-
6456
private static singletonInstance: InteractionTracker;
6557

6658
private constructor() {
@@ -102,7 +94,7 @@ export class InteractionTracker {
10294
}
10395
this.incrementTaskCount(getTraceId(task.zone));
10496
}
105-
this.interceptXhrTasks(task);
97+
interceptXhrTask(task);
10698
try {
10799
return runTask.call(task.zone as {}, task, applyThis, applyArgs);
108100
} finally {
@@ -198,52 +190,6 @@ export class InteractionTracker {
198190
});
199191
}
200192

201-
private interceptXhrTasks(task: AsyncTask) {
202-
if (!isTrackedTask(task)) return;
203-
if (!(task.target instanceof XMLHttpRequest)) return;
204-
205-
const xhr = task.target as XHRWithUrl;
206-
if (xhr.readyState === XMLHttpRequest.OPENED) {
207-
const rootSpan: RootSpan = task.zone.get('data').rootSpan;
208-
this.setTraceparentContextHeader(xhr, rootSpan);
209-
} else if (xhr.readyState === XMLHttpRequest.DONE) {
210-
this.endXhrSpan(xhr);
211-
}
212-
}
213-
214-
private setTraceparentContextHeader(xhr: XHRWithUrl, rootSpan: RootSpan) {
215-
// `__zone_symbol__xhrURL` is set by the Zone monkey-path.
216-
const xhrUrl = xhr.__zone_symbol__xhrURL;
217-
const childSpan = rootSpan.startChildSpan({
218-
name: parseUrl(xhrUrl).pathname,
219-
kind: SpanKind.CLIENT,
220-
});
221-
// Associate the child span to the XHR so it allows to
222-
// find the correct span when the request is DONE.
223-
this.xhrSpans.set(xhr, childSpan);
224-
if (traceOriginMatchesOrSameOrigin(xhrUrl)) {
225-
xhr.setRequestHeader(
226-
'traceparent',
227-
spanContextToTraceParent({
228-
traceId: rootSpan.traceId,
229-
spanId: childSpan.id,
230-
})
231-
);
232-
}
233-
}
234-
235-
private endXhrSpan(xhr: XHRWithUrl) {
236-
const childSpan = this.xhrSpans.get(xhr);
237-
if (childSpan) {
238-
// TODO: Investigate more to send the the status code a `number` rather than `string`
239-
// Once it is able to send as a number, change it.
240-
childSpan.addAttribute(ATTRIBUTE_HTTP_STATUS_CODE, xhr.status.toString());
241-
childSpan.addAttribute(ATTRIBUTE_HTTP_METHOD, xhr._ocweb_method);
242-
childSpan.end();
243-
this.xhrSpans.delete(xhr);
244-
}
245-
}
246-
247193
private resetCurrentTracingZone() {
248194
this.currentEventTracingZone = undefined;
249195
this.currentResetTracingZoneTimeout = undefined;
@@ -403,16 +349,6 @@ function getTrackedElement(task: AsyncTask): HTMLElement | null {
403349
return task.target as HTMLElement;
404350
}
405351

406-
/**
407-
* Whether or not a task is being tracked as part of an interaction.
408-
*/
409-
function isTrackedTask(task: Task): boolean {
410-
return !!(
411-
task.zone &&
412-
task.zone.get('data') &&
413-
task.zone.get('data').isTracingZone
414-
);
415-
}
416352
/**
417353
* Look for 'data-ocweb-id' attibute in the HTMLElement in order to
418354
* give a name to the user interaction and Root span. If this attibute is

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

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

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

1919
export function doPatching() {
20-
patchXMLHttpRequestOpen();
20+
patchXmlHttpRequestOpen();
2121
}
2222

2323
// Patch the `XMLHttpRequest.open` method to add method used for the request.
2424
// This patch is needed because Zone.js does not capture the method from XHR
2525
// the way that it captures URL as __zone_symbol__xhrURL.
26-
function patchXMLHttpRequestOpen() {
26+
function patchXmlHttpRequestOpen() {
2727
const open = XMLHttpRequest.prototype.open;
2828

2929
XMLHttpRequest.prototype.open = function(
@@ -38,6 +38,6 @@ function patchXMLHttpRequestOpen() {
3838
} else {
3939
open.call(this, method, url, true, null, null);
4040
}
41-
(this as XHRWithUrl)._ocweb_method = method;
41+
(this as XhrWithUrl)._ocweb_method = method;
4242
};
4343
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Copyright 2019, OpenCensus Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Span } from '@opencensus/web-core';
18+
import { XhrPerformanceResourceTiming } from './zone-types';
19+
import { alreadyAssignedPerfEntries } from './xhr-interceptor';
20+
21+
/**
22+
* Get Browser's performance resource timing data associated to a XHR.
23+
* Some XHRs might have two or one performance resource timings as one of them
24+
* is the CORS pre-flight request and the second is related to the actual HTTP
25+
* request.
26+
* In overall, the algorithm to get this data takes the best fit for the span,
27+
* this means the closest performance resource timing to the span start/end
28+
* performance times is the returned value.
29+
*/
30+
export function getXhrPerfomanceData(
31+
xhrUrl: string,
32+
span: Span
33+
): XhrPerformanceResourceTiming | undefined {
34+
const filteredSortedPerfEntries = getPerfSortedResourceEntries(xhrUrl, span);
35+
const possibleEntries = getPossiblePerfResourceEntries(
36+
filteredSortedPerfEntries
37+
);
38+
return getBestPerfResourceTiming(possibleEntries, span);
39+
}
40+
41+
/**
42+
* First step for the algorithm. Filter the Performance Resource Timings by the
43+
* name (it should match the XHR URL), additionally, the start/end timings of
44+
* every performance entry should fit within the span start/end timings. Also,
45+
* the entry should not be already assigned to a span.
46+
* These filtered performance resource entries are considered as possible
47+
* entries associated to the xhr.
48+
* Those are possible because there might be more than two entries that pass the
49+
* filter.
50+
* Additionally, the returned array is sorted by the entries' `startTime` as
51+
* getEntriesByType() already does it.
52+
* (https://developer.mozilla.org/en-US/docs/Web/API/Performance/getEntriesByType#Return_Value).
53+
*/
54+
export function getPerfSortedResourceEntries(
55+
xhrUrl: string,
56+
span: Span
57+
): PerformanceResourceTiming[] {
58+
return performance
59+
.getEntriesByType('resource')
60+
.filter(entry =>
61+
isPerfEntryPartOfXhr(entry as PerformanceResourceTiming, xhrUrl, span)
62+
) as PerformanceResourceTiming[];
63+
}
64+
65+
/**
66+
* Second step for the 'Performance resource timings selector algorithm'.
67+
* As the XHR could cause a CORS pre-flight request, we have to look for
68+
* possible performance entries either containing cors preflight timings or not.
69+
* A possible entry with cors data is when a resource timing entry does not
70+
* overlap timings with other resource timing entry. Also, every entry is
71+
* considered as possible XHR performance entry.
72+
* Thus, for this step traverse the array of resource entries and for every
73+
* entry check if it is a possible performance resource entry.
74+
* @param filteredSortedPerfEntries Sorted array of Performance Resource
75+
* Entries. This is sorted by the entries' `startTime`.
76+
*/
77+
export function getPossiblePerfResourceEntries(
78+
filteredSortedPerfEntries: PerformanceResourceTiming[]
79+
): XhrPerformanceResourceTiming[] {
80+
const possiblePerfEntries = new Array<XhrPerformanceResourceTiming>();
81+
// As this part of the algorithm uses nested for loops to examine pairs of
82+
// entries, although, this array is not large as the performance resource
83+
// entries buffer is cleared when there are no more running XHRs. Also, this
84+
// data is already filtered by URL and start/end time to be within the span.
85+
for (let i = 0; i < filteredSortedPerfEntries.length; i++) {
86+
const entryI = filteredSortedPerfEntries[i];
87+
// Consider the current entry as a possible entry without cors preflight
88+
// request. This is valid as this entry might be better than other possible
89+
// entries.
90+
possiblePerfEntries.push({ mainRequest: entryI });
91+
// Compare every performance entry with the perfomance entries in front of
92+
// it. This is possible as the entries are sorted by the startTime. That
93+
// way, we avoid comparing twice the entries and taking the wrong order.
94+
for (let j = i + 1; j < filteredSortedPerfEntries.length; j++) {
95+
const entryJ = filteredSortedPerfEntries[j];
96+
if (isPossibleCorsPair(entryI, entryJ)) {
97+
// As the entries are not overlapping, that means those timings
98+
// are possible perfomance timings related to the XHR.
99+
possiblePerfEntries.push({
100+
corsPreFlightRequest: entryI,
101+
mainRequest: entryJ,
102+
});
103+
}
104+
}
105+
}
106+
return possiblePerfEntries;
107+
}
108+
109+
/**
110+
* Pick the best performance resource timing for the XHR: Using the possible
111+
* performance resource timing entries from previous step, the best entry will
112+
* be the one with the minimum gap to the span start/end timings.
113+
* The performance resource timing entry with the minimum gap to the span
114+
* start/end timings points out that entry is the best fit for the span.
115+
*/
116+
function getBestPerfResourceTiming(
117+
perfEntries: XhrPerformanceResourceTiming[],
118+
span: Span
119+
): XhrPerformanceResourceTiming | undefined {
120+
let minimumGapToSpan = Number.MAX_VALUE;
121+
let bestPerfEntry: XhrPerformanceResourceTiming | undefined;
122+
for (const perfEntry of perfEntries) {
123+
// If the current entry has cors preflight data use its `startTime` to
124+
// calculate the gap to the span.
125+
const perfEntryStartTime = perfEntry.corsPreFlightRequest
126+
? perfEntry.corsPreFlightRequest.startTime
127+
: perfEntry.mainRequest.startTime;
128+
const gapToSpan =
129+
span.endPerfTime -
130+
perfEntry.mainRequest.responseEnd +
131+
(perfEntryStartTime - span.startPerfTime);
132+
133+
// If there is a new minimum gap to the span, update the minimum and pick
134+
// the current performance entry as the best at this point.
135+
if (gapToSpan < minimumGapToSpan) {
136+
minimumGapToSpan = gapToSpan;
137+
bestPerfEntry = perfEntry;
138+
}
139+
}
140+
return bestPerfEntry;
141+
}
142+
143+
/**
144+
* A Performance entry is part of a XHR if entry has not been assigned
145+
* previously to another XHR and the URL is the same as the XHR and the
146+
* entry's start/end times are within the span's start/end times.
147+
*/
148+
function isPerfEntryPartOfXhr(
149+
entry: PerformanceResourceTiming,
150+
xhrUrl: string,
151+
span: Span
152+
): boolean {
153+
return (
154+
!alreadyAssignedPerfEntries.has(entry) &&
155+
entry.name === xhrUrl &&
156+
entry.startTime >= span.startPerfTime &&
157+
entry.responseEnd <= span.endPerfTime
158+
);
159+
}
160+
161+
/**
162+
* A possible CORS pair is defined when the entries does not overlap in their
163+
* start/end times.
164+
*/
165+
function isPossibleCorsPair(
166+
maybePreflight: PerformanceResourceTiming,
167+
maybeMainRequest: PerformanceResourceTiming
168+
): boolean {
169+
// We can be sure that `maybePreflight` startTime is less than
170+
// `maybeMainRequest` startTime because of the sorting done by
171+
// `getEntriesByType`. Thus, to check the timings do not overlap, the
172+
// maybePreflight.respondeEnd must be less than maybeMainRequest.startTime.
173+
return maybePreflight.responseEnd < maybeMainRequest.startTime;
174+
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,14 @@ export function traceOriginMatchesOrSameOrigin(xhrUrl: string): boolean {
2727

2828
return !!(traceHeaderHostRegex && parsedUrl.host.match(traceHeaderHostRegex));
2929
}
30+
31+
/**
32+
* Whether or not a task is being tracked as part of an interaction.
33+
*/
34+
export function isTrackedTask(task: Task): boolean {
35+
return !!(
36+
task.zone &&
37+
task.zone.get('data') &&
38+
task.zone.get('data').isTracingZone
39+
);
40+
}

0 commit comments

Comments
 (0)