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

Commit e5cc8b1

Browse files
committed
Merge
1 parent 72b8f8c commit e5cc8b1

15 files changed

Lines changed: 737 additions & 77 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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
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(
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
20+
/**
21+
* Get Browser's performance resource timing data associated to a XHR.
22+
* For this case, some XHR might have two or one performance resource
23+
* timings as one of them is CORS pre-flight request and the second is related
24+
* to the actual HTTP request.
25+
* The algorithm to select performance resource timings related to that xhr is
26+
* is composed in general by three steps:
27+
*
28+
* 1. Filter the Performance Resource Timings by the name (it should match the
29+
* XHR URL), additionally, the start/end timings of every performance entry
30+
* should fit within the span start/end timings. These filtered performance
31+
* resource entries are considered as possible entries associated to the xhr.
32+
* Those are possible as there might be more than two entries that pass the
33+
* filter.
34+
*
35+
* 2. As the XHR could cause a CORS pre-flight, we have to look for either
36+
* possible pairs of performance resource timings or a single performance
37+
* resource entry (a possible pair is when a resource timing entry does not
38+
* overlap timings with other resource timing entry. Also, a possible single
39+
* resource timing is when that resource timing entry is not paired with any
40+
* other entry). Thus, for this step traverse the array of possible resource
41+
* entries and for every entry try to pair it with the other possible entries.
42+
*
43+
* 3. Pick the best performance resource timing for the XHR: Using the possible
44+
* performance resource timing entries from previous step, the best entry will
45+
* be the one with the minimum gap to the span start/end timings. That is the
46+
* substraction between the entry `respondeEnd` value and the span
47+
* `endPerfTime` plus the substraction between the entry `startTime` and span
48+
* `startPerfTime`. In case it is a tuple, the `startTime` corresponds to the
49+
* first entry and the `responseEnd` is from second entry.
50+
* The performance resource timing entry with the minimum gap to the span
51+
* start/end timings points out that entry is the best fit for the span.
52+
*
53+
* @param xhrUrl
54+
* @param span
55+
*/
56+
export function getXhrPerfomanceData(
57+
xhrUrl: string,
58+
span: Span
59+
): XhrPerformanceResourceTiming | undefined {
60+
const filteredPerfEntries = getPerfResourceEntries(xhrUrl, span);
61+
const possibleEntries = getPossiblePerfResourceEntries(filteredPerfEntries);
62+
const bestEntry = getBestPerfResourceTiming(possibleEntries, span);
63+
return bestEntry;
64+
}
65+
66+
// Get Performance Resource Timings and filter them by matching the XHR url
67+
// with perfomance entry name. Additionally, the entry's start/end
68+
// timings must fit with in the span's start/end timings.
69+
export function getPerfResourceEntries(
70+
xhrUrl: string,
71+
span: Span
72+
): PerformanceResourceTiming[] {
73+
return performance
74+
.getEntriesByType('resource')
75+
.filter(entry =>
76+
isPerfEntryPartOfXhr(entry as PerformanceResourceTiming, xhrUrl, span)
77+
) as PerformanceResourceTiming[];
78+
}
79+
80+
export function getPossiblePerfResourceEntries(
81+
perfEntries: PerformanceResourceTiming[]
82+
): XhrPerformanceResourceTiming[] {
83+
const possiblePerfEntries = new Array<XhrPerformanceResourceTiming>();
84+
const pairedEntries = new Set<PerformanceResourceTiming>();
85+
let perfEntry1: PerformanceResourceTiming;
86+
let perfEntry2: PerformanceResourceTiming;
87+
// As this part of the algorithm traverses the array twice, although,
88+
// this array is not big as the performance resource entries is cleared
89+
// when there are no more running XHRs.
90+
for (let i = 0; i < perfEntries.length; i++) {
91+
perfEntry1 = perfEntries[i];
92+
// Compare every performance entry with its consecutive perfomance entries.
93+
// That way to avoid comparing twice the entries.
94+
for (let j = i + 1; j < perfEntries.length; j++) {
95+
perfEntry2 = perfEntries[j];
96+
if (!overlappingPerfResourceTimings(perfEntry1, perfEntry2)) {
97+
// As the entries are not overlapping, that means those timings
98+
// are possible perfomance timings related to the XHR.
99+
possiblePerfEntries.push([perfEntry1, perfEntry2]);
100+
pairedEntries.add(perfEntry1);
101+
pairedEntries.add(perfEntry2);
102+
}
103+
}
104+
// If the entry1 couldn't be paired with any other resource timing,
105+
// add it as a single resource timing. This is possible because this
106+
// single entry might be better that the other possible entries.
107+
if (!pairedEntries.has(perfEntry1)) {
108+
possiblePerfEntries.push(perfEntry1 as PerformanceResourceTiming);
109+
}
110+
}
111+
return possiblePerfEntries;
112+
}
113+
114+
// The best Performance Resource Timing Entry is considered the one with the
115+
// minimum gap the span end/start timings. That way we think that it fits
116+
// better to the XHR as it is the closest data to the actual XHR.
117+
function getBestPerfResourceTiming(
118+
perfEntries: XhrPerformanceResourceTiming[],
119+
span: Span
120+
): XhrPerformanceResourceTiming | undefined {
121+
let minimumGapToSpan = Number.MAX_VALUE;
122+
let bestPerfEntry: XhrPerformanceResourceTiming | undefined = undefined;
123+
let sumGapsToSpan: number;
124+
for (const perfEntry of perfEntries) {
125+
// As a Tuple is in the end an Array, check that perfEntry is instance of
126+
// Array is enough to know if this value refers to a Tuple.
127+
if (perfEntry instanceof Array) {
128+
sumGapsToSpan = Math.abs(perfEntry[0].startTime - span.startPerfTime);
129+
sumGapsToSpan += Math.abs(perfEntry[1].responseEnd - span.endPerfTime);
130+
} else {
131+
sumGapsToSpan = Math.abs(perfEntry.responseEnd - span.endPerfTime);
132+
sumGapsToSpan += Math.abs(perfEntry.startTime - span.startPerfTime);
133+
}
134+
// If there is a new minimum gap to the span, update the minimum and pick
135+
// the current performance entry as the best at this point.
136+
if (sumGapsToSpan < minimumGapToSpan) {
137+
minimumGapToSpan = sumGapsToSpan;
138+
bestPerfEntry = perfEntry;
139+
}
140+
}
141+
return bestPerfEntry;
142+
}
143+
144+
function isPerfEntryPartOfXhr(
145+
entry: PerformanceResourceTiming,
146+
xhrUrl: string,
147+
span: Span
148+
): boolean {
149+
return (
150+
entry.name === xhrUrl &&
151+
entry.startTime >= span.startPerfTime &&
152+
entry.responseEnd <= span.endPerfTime
153+
);
154+
}
155+
156+
function overlappingPerfResourceTimings(
157+
entry1: PerformanceResourceTiming,
158+
entry2: PerformanceResourceTiming
159+
): boolean {
160+
return (
161+
Math.min(entry1.responseEnd, entry2.responseEnd) >=
162+
Math.max(entry1.startTime, entry2.startTime)
163+
);
164+
}

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)