diff --git a/packages/opencensus-web-instrumentation-zone/src/monkey-patching.ts b/packages/opencensus-web-instrumentation-zone/src/monkey-patching.ts index 747ea6b8..2eb778e1 100644 --- a/packages/opencensus-web-instrumentation-zone/src/monkey-patching.ts +++ b/packages/opencensus-web-instrumentation-zone/src/monkey-patching.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { XHRWithUrl } from './zone-types'; +import { XhrWithUrl } from './zone-types'; export function doPatching() { patchXmlHttpRequestOpen(); @@ -38,6 +38,6 @@ function patchXmlHttpRequestOpen() { } else { open.call(this, method, url, true, null, null); } - (this as XHRWithUrl)._ocweb_method = method; + (this as XhrWithUrl)._ocweb_method = method; }; } diff --git a/packages/opencensus-web-instrumentation-zone/src/perf-resource-timing-selector.ts b/packages/opencensus-web-instrumentation-zone/src/perf-resource-timing-selector.ts index 9c2b9ca3..7d3ab9a8 100644 --- a/packages/opencensus-web-instrumentation-zone/src/perf-resource-timing-selector.ts +++ b/packages/opencensus-web-instrumentation-zone/src/perf-resource-timing-selector.ts @@ -19,37 +19,12 @@ import { XhrPerformanceResourceTiming } from './zone-types'; /** * Get Browser's performance resource timing data associated to a XHR. - * For this case, some XHR might have two or one performance resource - * timings as one of them is CORS pre-flight request and the second is related - * to the actual HTTP request. - * The algorithm to select performance resource timings related to that xhr is - * is composed in general by three steps: - * - * 1. Filter the Performance Resource Timings by the name (it should match the - * XHR URL), additionally, the start/end timings of every performance entry - * should fit within the span start/end timings. These filtered performance - * resource entries are considered as possible entries associated to the xhr. - * Those are possible as there might be more than two entries that pass the - * filter. - * - * 2. As the XHR could cause a CORS pre-flight, we have to look for either - * possible pairs of performance resource timings or a single performance - * resource entry (a possible pair is when a resource timing entry does not - * overlap timings with other resource timing entry. Also, a possible single - * resource timing is when that resource timing entry is not paired with any - * other entry). Thus, for this step traverse the array of possible resource - * entries and for every entry try to pair it with the other possible entries. - * - * 3. Pick the best performance resource timing for the XHR: Using the possible - * performance resource timing entries from previous step, the best entry will - * be the one with the minimum gap to the span start/end timings. That is the - * substraction between the entry `respondeEnd` value and the span - * `endPerfTime` plus the substraction between the entry `startTime` and span - * `startPerfTime`. In case it is a tuple, the `startTime` corresponds to the - * first entry and the `responseEnd` is from second entry. - * The performance resource timing entry with the minimum gap to the span - * start/end timings points out that entry is the best fit for the span. - * + * Some XHRs might have two or one performance resource timings as one of them + * is the CORS pre-flight request and the second is related to the actual HTTP + * request. + * In overall, the algorithm to get this data takes the best fit for the span, + * this means the closest performance resource timing to the span start/end + * performance times is the returned value. * @param xhrUrl * @param span */ @@ -63,9 +38,17 @@ export function getXhrPerfomanceData( return bestEntry; } -// Get Performance Resource Timings and filter them by matching the XHR url -// with perfomance entry name. Additionally, the entry's start/end -// timings must fit with in the span's start/end timings. +/** + * First step for the algorithm. Filter the Performance Resource Timings by the + * name (it should match the XHR URL), additionally, the start/end timings of + * every performance entry should fit within the span start/end timings. + * These filtered performance resource entries are considered as possible + * entries associated to the xhr. + * Those are possible because there might be more than two entries that pass the + * filter. + * @param xhrUrl + * @param span + */ export function getPerfResourceEntries( xhrUrl: string, span: Span @@ -77,64 +60,86 @@ export function getPerfResourceEntries( ) as PerformanceResourceTiming[]; } +/** + * Second step for the 'Performance resource timings selector algorithm'. + * As the XHR could cause a CORS pre-flight request, we have to look for + * possible performance entries either containing cors preflight timings or not. + * A possible entry with cors data is when a resource timing entry does not + * overlap timings with other resource timing entry. Also, a possible entry + * without cors resource timing is when that resource timing entry is not + * 'paired' with any other entry. + * Thus, for this step traverse the array of resource entries and for every + * entry check if it is a possible performance resource entry. + * @param perfEntries + */ export function getPossiblePerfResourceEntries( perfEntries: PerformanceResourceTiming[] ): XhrPerformanceResourceTiming[] { const possiblePerfEntries = new Array(); const pairedEntries = new Set(); - let perfEntry1: PerformanceResourceTiming; - let perfEntry2: PerformanceResourceTiming; + let entryI: PerformanceResourceTiming; + let entryJ: PerformanceResourceTiming; // As this part of the algorithm traverses the array twice, although, - // this array is not big as the performance resource entries is cleared - // when there are no more running XHRs. + // this array is not large as the performance resource entries buffer is + // cleared when there are no more running XHRs. for (let i = 0; i < perfEntries.length; i++) { - perfEntry1 = perfEntries[i]; + entryI = perfEntries[i]; // Compare every performance entry with its consecutive perfomance entries. // That way to avoid comparing twice the entries. for (let j = i + 1; j < perfEntries.length; j++) { - perfEntry2 = perfEntries[j]; - if (!overlappingPerfResourceTimings(perfEntry1, perfEntry2)) { + entryJ = perfEntries[j]; + if (!overlappingPerfResourceTimings(entryI, entryJ)) { // As the entries are not overlapping, that means those timings // are possible perfomance timings related to the XHR. - possiblePerfEntries.push([perfEntry1, perfEntry2]); - pairedEntries.add(perfEntry1); - pairedEntries.add(perfEntry2); + possiblePerfEntries.push({ + corsPreFlightRequest: entryI, + mainRequest: entryJ, + }); + pairedEntries.add(entryI); + pairedEntries.add(entryJ); } } - // If the entry1 couldn't be paired with any other resource timing, - // add it as a single resource timing. This is possible because this - // single entry might be better that the other possible entries. - if (!pairedEntries.has(perfEntry1)) { - possiblePerfEntries.push(perfEntry1 as PerformanceResourceTiming); + // If the entry couldn't be paired with any other resource timing, + // add it as a possible resource timing without cors preflight data. + // This is possible because this entry might be better than the other + // possible entries. + if (!pairedEntries.has(entryI)) { + possiblePerfEntries.push({ mainRequest: entryI }); } } return possiblePerfEntries; } -// The best Performance Resource Timing Entry is considered the one with the -// minimum gap the span end/start timings. That way we think that it fits -// better to the XHR as it is the closest data to the actual XHR. +// Pick the best performance resource timing for the XHR: Using the possible +// performance resource timing entries from previous step, the best entry will +// be the one with the minimum gap to the span start/end timings. +// The performance resource timing entry with the minimum gap to the span +// start/end timings points out that entry is the best fit for the span. function getBestPerfResourceTiming( perfEntries: XhrPerformanceResourceTiming[], span: Span ): XhrPerformanceResourceTiming | undefined { let minimumGapToSpan = Number.MAX_VALUE; let bestPerfEntry: XhrPerformanceResourceTiming | undefined = undefined; - let sumGapsToSpan: number; for (const perfEntry of perfEntries) { - // As a Tuple is in the end an Array, check that perfEntry is instance of - // Array is enough to know if this value refers to a Tuple. - if (perfEntry instanceof Array) { - sumGapsToSpan = Math.abs(perfEntry[0].startTime - span.startPerfTime); - sumGapsToSpan += Math.abs(perfEntry[1].responseEnd - span.endPerfTime); + let gapToSpan = Math.abs( + perfEntry.mainRequest.responseEnd - span.endPerfTime + ); + // If the current entry has cors preflight data use its `startTime` to + // calculate the gap to the span. + if (perfEntry.corsPreFlightRequest) { + gapToSpan += Math.abs( + perfEntry.corsPreFlightRequest.startTime - span.startPerfTime + ); } else { - sumGapsToSpan = Math.abs(perfEntry.responseEnd - span.endPerfTime); - sumGapsToSpan += Math.abs(perfEntry.startTime - span.startPerfTime); + gapToSpan += Math.abs( + perfEntry.mainRequest.startTime - span.startPerfTime + ); } // If there is a new minimum gap to the span, update the minimum and pick // the current performance entry as the best at this point. - if (sumGapsToSpan < minimumGapToSpan) { - minimumGapToSpan = sumGapsToSpan; + if (gapToSpan < minimumGapToSpan) { + minimumGapToSpan = gapToSpan; bestPerfEntry = perfEntry; } } diff --git a/packages/opencensus-web-instrumentation-zone/src/xhr-interceptor.ts b/packages/opencensus-web-instrumentation-zone/src/xhr-interceptor.ts index b74a5497..d0fbc622 100644 --- a/packages/opencensus-web-instrumentation-zone/src/xhr-interceptor.ts +++ b/packages/opencensus-web-instrumentation-zone/src/xhr-interceptor.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AsyncTask, XHRWithUrl } from './zone-types'; +import { AsyncTask, XhrWithUrl } from './zone-types'; import { RootSpan, Span, @@ -33,9 +33,11 @@ import { getResourceSpan, } from '@opencensus/web-instrumentation-perf'; +const TRACEPARENT_HEADER = 'traceparent'; + // Map intended to keep track of current XHR objects // associated to a span. -const xhrSpans = new Map(); +const xhrSpans = new Map(); // Keeps track of the current xhr tasks that are running. This is // useful to clear the Performance Resource Timing entries when no @@ -54,7 +56,7 @@ export function interceptXhrTask(task: AsyncTask) { if (!isTrackedTask(task)) return; if (!(task.target instanceof XMLHttpRequest)) return; - const xhr = task.target as XHRWithUrl; + const xhr = task.target as XhrWithUrl; if (xhr.readyState === XMLHttpRequest.OPENED) { incrementXhrTaskCount(); const rootSpan: RootSpan = task.zone.get('data').rootSpan; @@ -66,7 +68,7 @@ export function interceptXhrTask(task: AsyncTask) { } function setTraceparentContextHeader( - xhr: XHRWithUrl, + xhr: XhrWithUrl, rootSpan: RootSpan ): void { // `__zone_symbol__xhrURL` is set by the Zone monkey-path. @@ -80,7 +82,7 @@ function setTraceparentContextHeader( xhrSpans.set(xhr, childSpan); if (traceOriginMatchesOrSameOrigin(xhrUrl)) { xhr.setRequestHeader( - 'traceparent', + TRACEPARENT_HEADER, spanContextToTraceParent({ traceId: rootSpan.traceId, spanId: childSpan.id, @@ -89,7 +91,7 @@ function setTraceparentContextHeader( } } -function endXhrSpan(xhr: XHRWithUrl): void { +function endXhrSpan(xhr: XhrWithUrl): void { const span = xhrSpans.get(xhr); if (span) { // TODO: Investigate more to send the the status code a `number` rather @@ -109,24 +111,15 @@ function maybeClearPerfResourceBuffer(): void { if (xhrTasksCount === 0) performance.clearResourceTimings(); } -function joinPerfResourceDataToSpan(xhr: XHRWithUrl, span: Span) { - const perfResourceTimings = getXhrPerfomanceData(xhr.responseURL, span); - if (perfResourceTimings instanceof Array) { - // This case is true when the resource timings data associates two entries - // to the span, where the first entry is the CORS pre-flight request and - // the second is the actual HTTP request. Create a child span which is - // related to the CORS pre-flight and use the second entry to add - // annotations to the span. - const corsPerfTiming = perfResourceTimings[0] as PerformanceResourceTimingExtended; - const actualXhrPerfTiming = perfResourceTimings[1] as PerformanceResourceTimingExtended; - setCorsPerfTimingAsChildSpan(corsPerfTiming, span); - span.annotations = annotationsForPerfTimeFields( - actualXhrPerfTiming, - PERFORMANCE_ENTRY_EVENTS - ); - } else if (perfResourceTimings) { +function joinPerfResourceDataToSpan(xhr: XhrWithUrl, span: Span) { + const xhrPerfResourceTiming = getXhrPerfomanceData(xhr.responseURL, span); + if (xhrPerfResourceTiming) { + if (xhrPerfResourceTiming.corsPreFlightRequest) { + const corsPerfTiming = xhrPerfResourceTiming.corsPreFlightRequest as PerformanceResourceTimingExtended; + setCorsPerfTimingAsChildSpan(corsPerfTiming, span); + } span.annotations = annotationsForPerfTimeFields( - perfResourceTimings as PerformanceResourceTimingExtended, + xhrPerfResourceTiming.mainRequest as PerformanceResourceTimingExtended, PERFORMANCE_ENTRY_EVENTS ); } @@ -137,7 +130,7 @@ function setCorsPerfTimingAsChildSpan( span: Span ): void { const corsSpan = getResourceSpan(performanceTiming, span.traceId, span.id); - corsSpan.name = 'CORS'; + corsSpan.name = 'CORS Preflight'; span.spans.push(corsSpan); } diff --git a/packages/opencensus-web-instrumentation-zone/src/zone-types.ts b/packages/opencensus-web-instrumentation-zone/src/zone-types.ts index 54dfe847..b3511009 100644 --- a/packages/opencensus-web-instrumentation-zone/src/zone-types.ts +++ b/packages/opencensus-web-instrumentation-zone/src/zone-types.ts @@ -43,7 +43,7 @@ export interface OnPageInteractionData { * `HTMLElement` is necessary when the xhr is captured from the tasks target * as the Zone monkey-patch parses xhrs as `HTMLElement & XMLHttpRequest`. */ -export type XHRWithUrl = HTMLElement & +export type XhrWithUrl = HTMLElement & XMLHttpRequest & { __zone_symbol__xhrURL: string; _ocweb_method: string; @@ -71,20 +71,10 @@ export declare interface WindowWithOcwGlobals extends Window { /** * Allows to keep track of performance entries related to a XHR. * As some XHRs might generate a CORS pre-flight request, the XHR - * might have either two performance resource entries or a single - * performance resource entry. + * might have a cors preflight performance resource timing entry or only the + * main request performance resource timing. */ -export type XhrPerformanceResourceTiming = - | PerformanceResourceTimingTuple - | PerformanceResourceTiming; - -/** - * Tuple type to associate two `PerformanceResourceTiming` objects as a pair. - * Used to select performance resource timing data associated to an XHR. In - * general, the first value points out it is a CORS pre-flight request data and - * the second value corresponds to the actual HTTP request. - */ -type PerformanceResourceTimingTuple = [ - PerformanceResourceTiming, - PerformanceResourceTiming -]; +export interface XhrPerformanceResourceTiming { + corsPreFlightRequest?: PerformanceResourceTiming; + mainRequest: PerformanceResourceTiming; +} diff --git a/packages/opencensus-web-instrumentation-zone/test/test-interaction-tracker.ts b/packages/opencensus-web-instrumentation-zone/test/test-interaction-tracker.ts index d6107cc3..be5081b7 100644 --- a/packages/opencensus-web-instrumentation-zone/test/test-interaction-tracker.ts +++ b/packages/opencensus-web-instrumentation-zone/test/test-interaction-tracker.ts @@ -29,12 +29,6 @@ import { doPatching } from '../src/monkey-patching'; import { WindowWithOcwGlobals } from '../src/zone-types'; import { spanContextToTraceParent } from '@opencensus/web-propagation-tracecontext'; import { createFakePerfResourceEntry, spyPerfEntryByType } from './util'; -import { - annotationsForPerfTimeFields, - PERFORMANCE_ENTRY_EVENTS, - PerformanceResourceTimingExtended, -} from '@opencensus/web-instrumentation-perf'; -import { getXhrPerfomanceData } from '../src/perf-resource-timing-selector'; describe('InteractionTracker', () => { doPatching(); @@ -286,180 +280,262 @@ describe('InteractionTracker', () => { }); }); - it('should handle HTTP requets and do not set Trace Context Header', done => { - // Set a diferent ocTraceHeaderHostRegex to test that the trace context header is not - // sent as the url request does not match the regex. - (window as WindowWithOcwGlobals).ocTraceHeaderHostRegex = /"http:\/\/test-host".*/; - const setRequestHeaderSpy = spyOn( - XMLHttpRequest.prototype, - 'setRequestHeader' - ).and.callThrough(); - const urlRequest = 'http://localhost:8000/test'; - const onclick = () => { - doHttpRequest(urlRequest); - }; - fakeInteraction(onclick); + describe('HTTP requests', () => { + // Value to be full when the XMLHttpRequest.send method is faked, + // That way the perfornamce resource entries have a accurate timing. + let perfResourceEntries: PerformanceResourceTiming[]; + beforeEach(() => { + perfResourceEntries = []; + }); - onEndSpanSpy.and.callFake((rootSpan: Span) => { - expect(rootSpan.name).toBe('test interaction'); - expect(rootSpan.attributes['EventType']).toBe('click'); - expect(rootSpan.attributes['TargetElement']).toBe(BUTTON_TAG_NAME); - expect(rootSpan.ended).toBeTruthy(); - expect(rootSpan.duration).toBeGreaterThanOrEqual(XHR_TIME); - expect(rootSpan.duration).toBeLessThanOrEqual(XHR_TIME + TIME_BUFFER); - - expect(rootSpan.spans.length).toBe(1); - const childSpan = rootSpan.spans[0]; - // Check the `traceparent` header is not set as Trace Header Host does not match. - expect(setRequestHeaderSpy).not.toHaveBeenCalled(); - expect(childSpan.name).toBe('/test'); - expect(childSpan.attributes[ATTRIBUTE_HTTP_STATUS_CODE]).toBe('200'); - expect(childSpan.attributes[ATTRIBUTE_HTTP_METHOD]).toBe('GET'); - expect(childSpan.ended).toBeTruthy(); - const childSpanperformanceEntries = getXhrPerfomanceData( - urlRequest, - childSpan - ); - expect(childSpanperformanceEntries).toBeTruthy(); - if (childSpanperformanceEntries) { - expect(childSpan.annotations).toEqual( - annotationsForPerfTimeFields( - childSpanperformanceEntries as PerformanceResourceTimingExtended, - PERFORMANCE_ENTRY_EVENTS - ) + it('should handle HTTP requets and do not set Trace Context Header', done => { + // Set a diferent ocTraceHeaderHostRegex to test that the trace context header is not + // sent as the url request does not match the regex. + (window as WindowWithOcwGlobals).ocTraceHeaderHostRegex = /"http:\/\/test-host".*/; + const setRequestHeaderSpy = spyOn( + XMLHttpRequest.prototype, + 'setRequestHeader' + ).and.callThrough(); + const requestUrl = 'http://localhost:8000/test'; + const onclick = () => { + doHttpRequest(requestUrl); + }; + fakeInteraction(onclick); + + onEndSpanSpy.and.callFake((rootSpan: Span) => { + expect(rootSpan.name).toBe('test interaction'); + expect(rootSpan.attributes['EventType']).toBe('click'); + expect(rootSpan.attributes['TargetElement']).toBe(BUTTON_TAG_NAME); + expect(rootSpan.ended).toBeTruthy(); + expect(rootSpan.duration).toBeGreaterThanOrEqual(XHR_TIME); + expect(rootSpan.duration).toBeLessThanOrEqual(XHR_TIME + TIME_BUFFER); + + expect(rootSpan.spans.length).toBe(1); + const childSpan = rootSpan.spans[0]; + // Check the `traceparent` header is not set as Trace Header Host does not match. + expect(setRequestHeaderSpy).not.toHaveBeenCalled(); + expect(childSpan.name).toBe('/test'); + expect(childSpan.attributes[ATTRIBUTE_HTTP_STATUS_CODE]).toBe('200'); + expect(childSpan.attributes[ATTRIBUTE_HTTP_METHOD]).toBe('GET'); + expect(childSpan.ended).toBeTruthy(); + const expectedAnnotations = [ + { + description: 'fetchStart', + timestamp: perfResourceEntries[0].fetchStart, + attributes: {}, + }, + { + description: 'responseEnd', + timestamp: perfResourceEntries[0].responseEnd, + attributes: {}, + }, + ]; + expect(childSpan.annotations).toEqual(expectedAnnotations); + // Check the CORS span is not created as this XHR does not send CORS + // pre-flight request. + expect(childSpan.spans.length).toBe(0); + expect(childSpan.duration).toBeGreaterThanOrEqual(XHR_TIME); + expect(childSpan.duration).toBeLessThanOrEqual(XHR_TIME + TIME_BUFFER); + done(); + }); + }); + + it('should handle HTTP requets and set Trace Context Header', done => { + // Set the ocTraceHeaderHostRegex value so the `traceparent` context header is set. + (window as WindowWithOcwGlobals).ocTraceHeaderHostRegex = /.*/; + const setRequestHeaderSpy = spyOn( + XMLHttpRequest.prototype, + 'setRequestHeader' + ).and.callThrough(); + const requestUrl = 'http://localhost:8000/test'; + const onclick = () => { + doHttpRequest(requestUrl, true); + }; + fakeInteraction(onclick); + + onEndSpanSpy.and.callFake((rootSpan: Span) => { + expect(rootSpan.name).toBe('test interaction'); + expect(rootSpan.attributes['EventType']).toBe('click'); + expect(rootSpan.attributes['TargetElement']).toBe(BUTTON_TAG_NAME); + expect(rootSpan.ended).toBeTruthy(); + expect(rootSpan.duration).toBeGreaterThanOrEqual(XHR_TIME); + expect(rootSpan.duration).toBeLessThanOrEqual(XHR_TIME + TIME_BUFFER); + + expect(rootSpan.spans.length).toBe(1); + const childSpan = rootSpan.spans[0]; + expect(setRequestHeaderSpy).toHaveBeenCalledWith( + 'traceparent', + spanContextToTraceParent({ + traceId: rootSpan.traceId, + spanId: childSpan.id, + }) ); - } - // Check the CORS span is not created as this XHR does not send CORS - // pre-flight request. - expect(childSpan.spans.length).toBe(0); - expect(childSpan.duration).toBeGreaterThanOrEqual(XHR_TIME); - expect(childSpan.duration).toBeLessThanOrEqual(XHR_TIME + TIME_BUFFER); - done(); + expect(childSpan.name).toBe('/test'); + expect(childSpan.attributes[ATTRIBUTE_HTTP_STATUS_CODE]).toBe('200'); + expect(childSpan.attributes[ATTRIBUTE_HTTP_METHOD]).toBe('GET'); + expect(childSpan.ended).toBeTruthy(); + const mainRequestPerfTiming = perfResourceEntries[1]; + const expectedChildSpanAnnotations = [ + { + description: 'fetchStart', + timestamp: mainRequestPerfTiming.fetchStart, + attributes: {}, + }, + { + description: 'responseEnd', + timestamp: mainRequestPerfTiming.responseEnd, + attributes: {}, + }, + ]; + expect(childSpan.annotations).toEqual(expectedChildSpanAnnotations); + // Check the CORS span is created with the correct annotations. + const corsPerfTiming = perfResourceEntries[0]; + const expectedCorsSpanAnnotations = [ + { + description: 'fetchStart', + timestamp: corsPerfTiming.fetchStart, + attributes: {}, + }, + { + description: 'responseEnd', + timestamp: corsPerfTiming.responseEnd, + attributes: {}, + }, + ]; + expect(childSpan.spans.length).toBe(1); + const corsSpan = childSpan.spans[0]; + expect(corsSpan.name).toBe('CORS Preflight'); + expect(corsSpan.annotations).toEqual(expectedCorsSpanAnnotations); + expect(childSpan.duration).toBeGreaterThanOrEqual(XHR_TIME); + expect(childSpan.duration).toBeLessThanOrEqual(XHR_TIME + TIME_BUFFER); + done(); + }); }); - }); - it('should handle HTTP requets and set Trace Context Header', done => { - // Set the ocTraceHeaderHostRegex value so the `traceparent` context header is set. - (window as WindowWithOcwGlobals).ocTraceHeaderHostRegex = /.*/; - const setRequestHeaderSpy = spyOn( - XMLHttpRequest.prototype, - 'setRequestHeader' - ).and.callThrough(); - const urlRequest = 'http://localhost:8000/test'; - const onclick = () => { - doHttpRequest(urlRequest, true); - }; - fakeInteraction(onclick); + it('should handle cascading tasks', done => { + const setRequestHeaderSpy = spyOn( + XMLHttpRequest.prototype, + 'setRequestHeader' + ).and.callThrough(); + const requestUrl = '/test'; + const onclick = () => { + const promise = getPromise(); + promise.then(() => { + setTimeout(() => { + doHttpRequest(requestUrl, true); + }, SET_TIMEOUT_TIME); + }); + }; + fakeInteraction(onclick); + + const interactionTime = SET_TIMEOUT_TIME + XHR_TIME; + onEndSpanSpy.and.callFake((rootSpan: Span) => { + expect(rootSpan.name).toBe('test interaction'); + expect(rootSpan.attributes['EventType']).toBe('click'); + expect(rootSpan.attributes['TargetElement']).toBe(BUTTON_TAG_NAME); + expect(rootSpan.ended).toBeTruthy(); + expect(rootSpan.duration).toBeGreaterThanOrEqual(interactionTime); + expect(rootSpan.duration).toBeLessThanOrEqual( + interactionTime + TIME_BUFFER + ); - onEndSpanSpy.and.callFake((rootSpan: Span) => { - expect(rootSpan.name).toBe('test interaction'); - expect(rootSpan.attributes['EventType']).toBe('click'); - expect(rootSpan.attributes['TargetElement']).toBe(BUTTON_TAG_NAME); - expect(rootSpan.ended).toBeTruthy(); - expect(rootSpan.duration).toBeGreaterThanOrEqual(XHR_TIME); - expect(rootSpan.duration).toBeLessThanOrEqual(XHR_TIME + TIME_BUFFER); - - expect(rootSpan.spans.length).toBe(1); - const childSpan = rootSpan.spans[0]; - expect(setRequestHeaderSpy).toHaveBeenCalledWith( - 'traceparent', - spanContextToTraceParent({ - traceId: rootSpan.traceId, - spanId: childSpan.id, - }) - ); - expect(childSpan.name).toBe('/test'); - expect(childSpan.attributes[ATTRIBUTE_HTTP_STATUS_CODE]).toBe('200'); - expect(childSpan.attributes[ATTRIBUTE_HTTP_METHOD]).toBe('GET'); - expect(childSpan.ended).toBeTruthy(); - const childSpanPerfEntries = getXhrPerfomanceData(urlRequest, childSpan); - expect(childSpanPerfEntries).toBeTruthy(); - if (childSpanPerfEntries instanceof Array) { - const corsPerfTiming = childSpanPerfEntries[0] as PerformanceResourceTimingExtended; - const actualXhrPerfTiming = childSpanPerfEntries[1] as PerformanceResourceTimingExtended; - expect(childSpan.annotations).toEqual( - annotationsForPerfTimeFields( - actualXhrPerfTiming, - PERFORMANCE_ENTRY_EVENTS - ) + expect(rootSpan.spans.length).toBe(1); + const childSpan = rootSpan.spans[0]; + expect(setRequestHeaderSpy).toHaveBeenCalledWith( + 'traceparent', + spanContextToTraceParent({ + traceId: rootSpan.traceId, + spanId: childSpan.id, + }) ); + expect(childSpan.name).toBe('/test'); + expect(childSpan.attributes[ATTRIBUTE_HTTP_STATUS_CODE]).toBe('200'); + expect(childSpan.attributes[ATTRIBUTE_HTTP_METHOD]).toBe('GET'); + expect(childSpan.ended).toBeTruthy(); + const mainRequestPerfTiming = perfResourceEntries[1]; + const expectedChildSpanAnnotations = [ + { + description: 'fetchStart', + timestamp: mainRequestPerfTiming.fetchStart, + attributes: {}, + }, + { + description: 'responseEnd', + timestamp: mainRequestPerfTiming.responseEnd, + attributes: {}, + }, + ]; + expect(childSpan.annotations).toEqual(expectedChildSpanAnnotations); // Check the CORS span is created with the correct annotations. + const corsPerfTiming = perfResourceEntries[0]; + const expectedCorsSpanAnnotations = [ + { + description: 'fetchStart', + timestamp: corsPerfTiming.fetchStart, + attributes: {}, + }, + { + description: 'responseEnd', + timestamp: corsPerfTiming.responseEnd, + attributes: {}, + }, + ]; expect(childSpan.spans.length).toBe(1); const corsSpan = childSpan.spans[0]; - expect(corsSpan.name).toBe('CORS'); - expect(corsSpan.annotations).toEqual( - annotationsForPerfTimeFields(corsPerfTiming, PERFORMANCE_ENTRY_EVENTS) - ); - } - expect(childSpan.duration).toBeGreaterThanOrEqual(XHR_TIME); - expect(childSpan.duration).toBeLessThanOrEqual(XHR_TIME + TIME_BUFFER); - done(); + expect(corsSpan.name).toBe('CORS Preflight'); + expect(corsSpan.annotations).toEqual(expectedCorsSpanAnnotations); + expect(childSpan.duration).toBeGreaterThanOrEqual(XHR_TIME); + expect(childSpan.duration).toBeLessThanOrEqual(XHR_TIME + TIME_BUFFER); + done(); + }); }); - }); - it('should handle cascading tasks', done => { - const setRequestHeaderSpy = spyOn( - XMLHttpRequest.prototype, - 'setRequestHeader' - ).and.callThrough(); - const urlRequest = '/test'; - const onclick = () => { - const promise = getPromise(); - promise.then(() => { + function doHttpRequest(urlRequest = '/test', xhrHasCorsData = false) { + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = noop; + spyOn(xhr, 'send').and.callFake(() => { setTimeout(() => { - doHttpRequest(urlRequest, true); - }, SET_TIMEOUT_TIME); + spyOnProperty(xhr, 'status').and.returnValue(200); + const event = new Event('readystatechange'); + xhr.dispatchEvent(event); + }, XHR_TIME); + // Create the performance entries at this point in order to have a + // similar timing as span. + createFakePerformanceEntries(urlRequest, xhrHasCorsData); + spyPerfEntryByType(perfResourceEntries); + spyOnProperty(xhr, 'responseURL').and.returnValue(urlRequest); }); - }; - fakeInteraction(onclick); - const interactionTime = SET_TIMEOUT_TIME + XHR_TIME; - onEndSpanSpy.and.callFake((rootSpan: Span) => { - expect(rootSpan.name).toBe('test interaction'); - expect(rootSpan.attributes['EventType']).toBe('click'); - expect(rootSpan.attributes['TargetElement']).toBe(BUTTON_TAG_NAME); - expect(rootSpan.ended).toBeTruthy(); - expect(rootSpan.duration).toBeGreaterThanOrEqual(interactionTime); - expect(rootSpan.duration).toBeLessThanOrEqual( - interactionTime + TIME_BUFFER - ); + xhr.open('GET', urlRequest); + // Spy on `readystate` property after open, so that way while intercepting + // the XHR will detect OPENED and DONE states. + spyOnProperty(xhr, 'readyState').and.returnValue(XMLHttpRequest.DONE); + xhr.send(); + } - expect(rootSpan.spans.length).toBe(1); - const childSpan = rootSpan.spans[0]; - expect(setRequestHeaderSpy).toHaveBeenCalledWith( - 'traceparent', - spanContextToTraceParent({ - traceId: rootSpan.traceId, - spanId: childSpan.id, - }) - ); - expect(childSpan.name).toBe('/test'); - expect(childSpan.attributes[ATTRIBUTE_HTTP_STATUS_CODE]).toBe('200'); - expect(childSpan.attributes[ATTRIBUTE_HTTP_METHOD]).toBe('GET'); - expect(childSpan.ended).toBeTruthy(); - const childSpanPerfEntries = getXhrPerfomanceData(urlRequest, childSpan); - expect(childSpanPerfEntries).toBeTruthy(); - if (childSpanPerfEntries instanceof Array) { - const corsPerfTiming = childSpanPerfEntries[0] as PerformanceResourceTimingExtended; - const actualXhrPerfTiming = childSpanPerfEntries[1] as PerformanceResourceTimingExtended; - expect(childSpan.annotations).toEqual( - annotationsForPerfTimeFields( - actualXhrPerfTiming, - PERFORMANCE_ENTRY_EVENTS - ) - ); - // Check the CORS span is created with the correct annotations. - expect(childSpan.spans.length).toBe(1); - const corsSpan = childSpan.spans[0]; - expect(corsSpan.name).toBe('CORS'); - expect(corsSpan.annotations).toEqual( - annotationsForPerfTimeFields(corsPerfTiming, PERFORMANCE_ENTRY_EVENTS) + function createFakePerformanceEntries( + urlRequest: string, + xhrHasCorsData: boolean + ) { + const xhrPerfStart = performance.now(); + let actualRequestStartTime = xhrPerfStart; + if (xhrHasCorsData) { + const corsEntry = createFakePerfResourceEntry( + xhrPerfStart, + xhrPerfStart + 1, + urlRequest ); + // Start the other request a bit after the CORS finished. + actualRequestStartTime = xhrPerfStart + 1; + perfResourceEntries.push(corsEntry); } - expect(childSpan.duration).toBeGreaterThanOrEqual(XHR_TIME); - expect(childSpan.duration).toBeLessThanOrEqual(XHR_TIME + TIME_BUFFER); - done(); - }); + const actualRequestEntry = createFakePerfResourceEntry( + actualRequestStartTime + 1, + actualRequestStartTime + XHR_TIME - 2, + urlRequest + ); + perfResourceEntries.push(actualRequestEntry); + } }); function fakeInteraction(callback: Function, elem?: HTMLElement) { @@ -487,47 +563,4 @@ describe('InteractionTracker', () => { resolve(); }); } - - function doHttpRequest(urlRequest = '/test', xhrHasCorsData = false) { - const xhr = new XMLHttpRequest(); - xhr.onreadystatechange = noop; - spyOn(xhr, 'send').and.callFake(() => { - setTimeout(() => { - spyOnProperty(xhr, 'status').and.returnValue(200); - const event = new Event('readystatechange'); - xhr.dispatchEvent(event); - }, XHR_TIME); - fakePerformanceEntries(urlRequest, xhrHasCorsData); - spyOnProperty(xhr, 'responseURL').and.returnValue(urlRequest); - }); - - xhr.open('GET', urlRequest); - // Spy on `readystate` property after open, so that way while intercepting - // the XHR will detect OPENED and DONE states. - spyOnProperty(xhr, 'readyState').and.returnValue(XMLHttpRequest.DONE); - xhr.send(); - } - - function fakePerformanceEntries(urlRequest: string, xhrHasCorsData: boolean) { - const xhrPerfStart = performance.now(); - const perfResourceEntries: PerformanceResourceTiming[] = []; - let actualRequestStartTime = xhrPerfStart; - if (xhrHasCorsData) { - const corsEntry = createFakePerfResourceEntry( - xhrPerfStart, - xhrPerfStart + 1, - urlRequest - ); - // Start the other request a bit after the CORS finished. - actualRequestStartTime = xhrPerfStart + 1; - perfResourceEntries.push(corsEntry); - } - const actualRequestEntry = createFakePerfResourceEntry( - actualRequestStartTime + 1, - actualRequestStartTime + XHR_TIME - 2, - urlRequest - ); - perfResourceEntries.push(actualRequestEntry); - spyPerfEntryByType(perfResourceEntries); - } }); diff --git a/packages/opencensus-web-instrumentation-zone/test/test-perf-resource-timing-selector.ts b/packages/opencensus-web-instrumentation-zone/test/test-perf-resource-timing-selector.ts index bb5aee5b..75d66e37 100644 --- a/packages/opencensus-web-instrumentation-zone/test/test-perf-resource-timing-selector.ts +++ b/packages/opencensus-web-instrumentation-zone/test/test-perf-resource-timing-selector.ts @@ -21,10 +21,11 @@ import { getXhrPerfomanceData, } from '../src/perf-resource-timing-selector'; import { spyPerfEntryByType, createFakePerfResourceEntry } from './util'; +import { XhrPerformanceResourceTiming } from '../src/zone-types'; describe('Perf Resource Timing Selector', () => { describe('getPerfResourceEntries', () => { - it('Should filter properly the resource entries', () => { + it('Should filter by the url and ignore entries not fitting in span', () => { // Should ignore this as it has a different name const entry1 = createFakePerfResourceEntry( 13.100000011036173, @@ -60,46 +61,50 @@ describe('Perf Resource Timing Selector', () => { }); describe('getPossiblePerfResourceEntries', () => { - it('Should pair no overlapping entries', () => { + it('Should add cors preflight value to non overlapping entries and not add cors value to overlapping entries', () => { const entry1 = createFakePerfResourceEntry( 2.100000011036173, 5.500000011036173 ); const entry2 = createFakePerfResourceEntry( - 5.500000011036173, + 5.600000011036173, 10.200000011036173 ); const entry3 = createFakePerfResourceEntry( 8.200000011036173, 15.500000011036173 ); - const entry4 = createFakePerfResourceEntry( - 13.500000011036173, - 20.000000000000003 - ); - const perfResourceEntries = [entry1, entry2, entry3, entry4]; + const perfResourceEntries = [entry1, entry2, entry3]; const filteredPerfEntries = getPossiblePerfResourceEntries( perfResourceEntries ); - expect(filteredPerfEntries.length).toBe(3); - // Entry1 and entry2 overlap as entry1 startTime is equal to entry2 - // endTime. - expect(filteredPerfEntries).not.toContain([entry1, entry2]); - expect(filteredPerfEntries).toContain([entry1, entry3]); - expect(filteredPerfEntries).toContain([entry1, entry4]); - expect(filteredPerfEntries).not.toContain([entry2, entry3]); - expect(filteredPerfEntries).toContain([entry2, entry4]); - expect(filteredPerfEntries).not.toContain([entry3, entry4]); + const nonOverlappingEntry1 = { + corsPreFlightRequest: entry1, + mainRequest: entry2, + } as XhrPerformanceResourceTiming; + const nonOverlappingEntry2 = { + corsPreFlightRequest: entry1, + mainRequest: entry3, + } as XhrPerformanceResourceTiming; + const overlappingEntry3 = { + corsPreFlightRequest: entry2, + mainRequest: entry3, + } as XhrPerformanceResourceTiming; + + expect(filteredPerfEntries.length).toBe(2); + expect(filteredPerfEntries).toContain(nonOverlappingEntry1); + expect(filteredPerfEntries).toContain(nonOverlappingEntry2); + expect(filteredPerfEntries).not.toContain(overlappingEntry3); }); - it('Should take single resource timing entries', () => { + it('Should not add cors preflight value as all the entries overlap each other', () => { const entry1 = createFakePerfResourceEntry( 2.100000011036173, 5.500000011036173 ); const entry2 = createFakePerfResourceEntry( - 5.500000011036173, + 3.500000011036173, 10.200000011036173 ); const entry3 = createFakePerfResourceEntry( @@ -110,100 +115,57 @@ describe('Perf Resource Timing Selector', () => { const filteredPerfEntries = getPossiblePerfResourceEntries( perfResourceEntries ); - expect(filteredPerfEntries.length).toBe(3); - expect(filteredPerfEntries).toContain(entry1); - expect(filteredPerfEntries).toContain(entry2); - expect(filteredPerfEntries).toContain(entry3); - }); - it('Should take single and tuples of resource timing entries', () => { - const entry1 = createFakePerfResourceEntry( - 2.100000011036173, - 5.500000011036173 - ); - const entry2 = createFakePerfResourceEntry( - 6.500000011036173, - 10.200000011036173 - ); - const entry3 = createFakePerfResourceEntry( - 8.500000011036173, - 20.000000000000003 - ); - const entry4 = createFakePerfResourceEntry( - 1.500000011036173, - 25.000000000000003 - ); - const perfResourceEntries = [entry1, entry2, entry3, entry4]; - const filteredPerfEntries = getPossiblePerfResourceEntries( - perfResourceEntries - ); + const overlappingEntry1 = { + mainRequest: entry1, + } as XhrPerformanceResourceTiming; + const overlappingEntry2 = { + mainRequest: entry2, + } as XhrPerformanceResourceTiming; + const overlappingEntry3 = { + mainRequest: entry3, + } as XhrPerformanceResourceTiming; expect(filteredPerfEntries.length).toBe(3); - expect(filteredPerfEntries).toContain([entry1, entry2]); - expect(filteredPerfEntries).toContain([entry1, entry3]); - expect(filteredPerfEntries).not.toContain([entry1, entry4]); - expect(filteredPerfEntries).not.toContain([entry2, entry3]); - expect(filteredPerfEntries).not.toContain([entry2, entry4]); - // Should not contain the single resource entries as they already were - // paired. - expect(filteredPerfEntries).not.toContain(entry1); - expect(filteredPerfEntries).not.toContain(entry2); - expect(filteredPerfEntries).not.toContain(entry3); - // As entry4 has not been paired, should contain this a single entry. - expect(filteredPerfEntries).toContain(entry4); + expect(filteredPerfEntries).toContain(overlappingEntry1); + expect(filteredPerfEntries).toContain(overlappingEntry2); + expect(filteredPerfEntries).toContain(overlappingEntry3); }); }); describe('getXhrPerfomanceData', () => { - it('Should take the tuple with the minimum gap to the span as the best resource timing entry', () => { - const entry1 = createFakePerfResourceEntry( - 13.100000011036173, - 15.100000011036173 - ); - const entry2 = createFakePerfResourceEntry( - 13.200000011036173, - 15.200000011036173 - ); - const entry3 = createFakePerfResourceEntry( - 16.100000011036173, - 18.100000011036173 - ); - const entry4 = createFakePerfResourceEntry( - 16.200000011036173, - 18.200000011036173 - ); + it('Should return entry with cors preflight value as the best performance timing entry', () => { + const entry1 = createFakePerfResourceEntry(13.1, 15.1); + const entry2 = createFakePerfResourceEntry(14.2, 16.2); + const entry3 = createFakePerfResourceEntry(17.1, 19.1); + const entry4 = createFakePerfResourceEntry(18.2, 20.2); // Ignore this as its out of the span end time. - const entry5 = createFakePerfResourceEntry( - 35.100000011036173, - 40.100000011036173 - ); + const entry5 = createFakePerfResourceEntry(15.1, 40.1); const perfResourceEntries = [entry1, entry2, entry3, entry4, entry5]; spyPerfEntryByType(perfResourceEntries); - const span = createSpan(13.1, 18.3); + const span = createSpan(13.1, 20.3); + const expectedXhrPerformanceData = { + corsPreFlightRequest: entry1, + mainRequest: entry4, + } as XhrPerformanceResourceTiming; const filteredPerfEntries = getXhrPerfomanceData('/test', span); - expect(filteredPerfEntries).toEqual([entry1, entry4]); + expect(filteredPerfEntries).toEqual(expectedXhrPerformanceData); }); - it('Should take the single resource entry with the minimum gap to the span as the best entry', () => { - const entry1 = createFakePerfResourceEntry( - 5.100000011036173, - 20.100000011036173 - ); - const entry2 = createFakePerfResourceEntry( - 10.100000011036173, - 13.100000011036173 - ); - const entry3 = createFakePerfResourceEntry( - 14.200000011036173, - 20.000000011036173 - ); + it('Should return entry without cors preflight value as the best entry', () => { + const entry1 = createFakePerfResourceEntry(5.1, 20.1); + const entry2 = createFakePerfResourceEntry(10.1, 13.1); + const entry3 = createFakePerfResourceEntry(14.2, 20.0); const perfResourceEntries = [entry1, entry2, entry3]; spyPerfEntryByType(perfResourceEntries); const span = createSpan(5.1, 20.2); const filteredPerfEntries = getXhrPerfomanceData('/test', span); - expect(filteredPerfEntries).toEqual(entry1); + const expectedXhrPerformanceData = { + mainRequest: entry1, + } as XhrPerformanceResourceTiming; + expect(filteredPerfEntries).toEqual(expectedXhrPerformanceData); }); }); });