From 89561ae90f4c59a7352474e6dd09a96bfe770902 Mon Sep 17 00:00:00 2001 From: Dave Raffensperger Date: Wed, 13 Feb 2019 19:45:30 -0500 Subject: [PATCH] Performance timing recording and span conversion (#12) --- .../src/trace/model/attribute-keys.ts | 14 +- .../src/trace/model/span.ts | 6 +- .../opencensus-web-core/test/test-span.ts | 5 + .../src/initial-load-root-span.ts | 135 ++++++++ .../src/perf-recorder.ts | 98 ++++++ .../src/perf-types.ts | 164 ++++++++++ .../src/resource-span.ts | 89 ++++++ .../src/util.ts | 44 +++ .../test/index.ts | 9 +- .../test/test-initial-load-root-span.ts | 302 ++++++++++++++++++ .../test/test-perf-recorder.ts | 218 +++++++++++++ .../test/test-resource-span.ts | 146 +++++++++ 12 files changed, 1223 insertions(+), 7 deletions(-) create mode 100644 packages/opencensus-web-instrumentation-perf/src/initial-load-root-span.ts create mode 100644 packages/opencensus-web-instrumentation-perf/src/perf-recorder.ts create mode 100644 packages/opencensus-web-instrumentation-perf/src/perf-types.ts create mode 100644 packages/opencensus-web-instrumentation-perf/src/resource-span.ts create mode 100644 packages/opencensus-web-instrumentation-perf/src/util.ts create mode 100644 packages/opencensus-web-instrumentation-perf/test/test-initial-load-root-span.ts create mode 100644 packages/opencensus-web-instrumentation-perf/test/test-perf-recorder.ts create mode 100644 packages/opencensus-web-instrumentation-perf/test/test-resource-span.ts diff --git a/packages/opencensus-web-core/src/trace/model/attribute-keys.ts b/packages/opencensus-web-core/src/trace/model/attribute-keys.ts index b87f33be..b7ceb08b 100644 --- a/packages/opencensus-web-core/src/trace/model/attribute-keys.ts +++ b/packages/opencensus-web-core/src/trace/model/attribute-keys.ts @@ -68,8 +68,8 @@ export const ATTRIBUTE_HTTP_RESP_ENCODED_BODY_SIZE = export const ATTRIBUTE_HTTP_RESP_DECODED_BODY_SIZE = `${HTTP_PREFIX}resp_decoded_body_size`; +/** Attribute prefix for spans that represent navigations in the browser. */ const NAVIGATION_PREFIX = 'nav.'; - /** * The type of browser navigation. See * https://www.w3.org/TR/navigation-timing-2/#sec-performance-navigation-types @@ -81,3 +81,15 @@ export const ATTRIBUTE_NAV_TYPE = `${NAVIGATION_PREFIX}type`; */ export const ATTRIBUTE_NAV_REDIRECT_COUNT = `${NAVIGATION_PREFIX}redirect_count`; + +/** + * Attribute prefix for spans that are for "long tasks" (long JS event loops). + * See https://www.w3.org/TR/longtasks/ + */ +export const LONG_TASK_PREFIX = 'long_task.'; +/** + * A JSON string of the `attribution` field of a long task timing. This gives + * a little additional information about what on the page may have caused the + * long task. + */ +export const ATTRIBUTE_LONG_TASK_ATTRIBUTION = `${LONG_TASK_PREFIX}attribution`; diff --git a/packages/opencensus-web-core/src/trace/model/span.ts b/packages/opencensus-web-core/src/trace/model/span.ts index 78ef2971..b81fe92d 100644 --- a/packages/opencensus-web-core/src/trace/model/span.ts +++ b/packages/opencensus-web-core/src/trace/model/span.ts @@ -15,11 +15,9 @@ */ import * as coreTypes from '@opencensus/core'; - import {LOGGER} from '../../common/console-logger'; import {getDateForPerfTime} from '../../common/time-util'; import {randomSpanId} from '../../internal/util'; - import {MessageEvent, SpanKind} from './types'; /** Default span name if none is specified. */ @@ -27,7 +25,9 @@ const DEFAULT_SPAN_NAME = 'unnamed'; /** A span represents a single operation within a trace. */ export class Span implements coreTypes.Span { - id = randomSpanId(); + constructor( + /** The ID of this span. Defaults to a random span ID. */ + public id = randomSpanId()) {} /** If the parent span is in another process. */ remoteParent = false; diff --git a/packages/opencensus-web-core/test/test-span.ts b/packages/opencensus-web-core/test/test-span.ts index 84cc50f0..911c997d 100644 --- a/packages/opencensus-web-core/test/test-span.ts +++ b/packages/opencensus-web-core/test/test-span.ts @@ -28,6 +28,11 @@ describe('Span', () => { expect(span.id).toMatch('^[a-z0-9]{16}$'); }); + it('allows initializing id in constructor', () => { + const span = new Span('000000000000000b'); + expect(span.id).toBe('000000000000000b'); + }); + it('calculates time fields based on startPerfTime/endPerfTime', () => { expect(span.ended).toBe(false); diff --git a/packages/opencensus-web-instrumentation-perf/src/initial-load-root-span.ts b/packages/opencensus-web-instrumentation-perf/src/initial-load-root-span.ts new file mode 100644 index 00000000..7ada5471 --- /dev/null +++ b/packages/opencensus-web-instrumentation-perf/src/initial-load-root-span.ts @@ -0,0 +1,135 @@ +/** + * Copyright 2019, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Annotation, ATTRIBUTE_HTTP_URL, ATTRIBUTE_HTTP_USER_AGENT, ATTRIBUTE_LONG_TASK_ATTRIBUTION, ATTRIBUTE_NAV_TYPE, parseUrl, RootSpan, Span, SpanKind, Tracer} from '@opencensus/web-core'; +import {GroupedPerfEntries} from './perf-recorder'; +import {PerformanceLongTaskTiming, PerformanceNavigationTimingExtended} from './perf-types'; +import {getResourceSpan} from './resource-span'; +import {annotationsForPerfTimeFields} from './util'; + +/** + * These are properties of PerformanceNavigationTiming that will be turned + * into span annotations on the navigation span. + */ +const NAVIGATION_TIMING_EVENTS = [ + 'domLoading', + 'domInteractive', + 'domContentLoaded', + 'domComplete', + 'loadEventStart', + 'loadEventEnd', + 'unloadEventStart', + 'unloadEventEnd', +]; + +/** + * Returns a root span for the initial page load along with child spans for + * resource timings and long tasks that were part of the load. The root span + * represents the full time between when the navigation was initiated in the + * browser to when the `load` event fires. + * @param tracer The tracer to associate the root span with + * @param perfEntries Performance timing entries grouped by type. These should + * include the entries up to soon after the `load` browser event fires. + * @param navigationFetchSpanId This is the ID for the span that represents the + * HTTP request to get the initial HTML. This should be sent back from the + * server to enable linking the server and client side spans for the initial + * HTML fetch. + * @param traceId The trace ID that all spans returned should have. This should + * also be specified by the server to enable linking between the server and + * client spans for the initial HTML fetch. + */ +export function getInitialLoadRootSpan( + tracer: Tracer, perfEntries: GroupedPerfEntries, + navigationFetchSpanId: string, traceId: string): RootSpan { + const navTiming = perfEntries.navigationTiming; + const navigationUrl = navTiming ? navTiming.name : location.href; + const parsedNavigationUrl = parseUrl(navigationUrl); + const navigationPath = parsedNavigationUrl.pathname; + const root = new RootSpan(tracer, { + name: `Nav.${navigationPath}`, + spanContext: { + traceId, + // This becomes the parentSpanId field of the root span, and the actual + // span ID for the root span gets assigned to a random number. + spanId: '', + }, + kind: SpanKind.UNSPECIFIED, + }); + root.startPerfTime = 0; + root.annotations = getNavigationAnnotations(perfEntries); + root.attributes[ATTRIBUTE_HTTP_URL] = navigationUrl; + root.attributes[ATTRIBUTE_HTTP_USER_AGENT] = navigator.userAgent; + + if (navTiming) { + root.endPerfTime = navTiming.loadEventEnd; + root.attributes[ATTRIBUTE_NAV_TYPE] = navTiming.type; + const navFetchSpan = getNavigationFetchSpan( + navTiming, navigationUrl, traceId, root.id, navigationFetchSpanId); + root.spans.push(navFetchSpan); + } + + const resourceSpans = perfEntries.resourceTimings.map( + (resourceTiming) => getResourceSpan(resourceTiming, traceId, root.id)); + const longTaskSpans = perfEntries.longTasks.map( + (longTaskTiming) => getLongTaskSpan(longTaskTiming, traceId, root.id)); + + root.spans = root.spans.concat(resourceSpans, longTaskSpans); + return root; +} + +/** Returns a parent span for the HTTP request to retrieve the initial HTML. */ +function getNavigationFetchSpan( + navigationTiming: PerformanceNavigationTimingExtended, + navigationName: string, traceId: string, parentSpanId: string, + spanId: string): Span { + const span = getResourceSpan(navigationTiming, traceId, parentSpanId, spanId); + span.startPerfTime = navigationTiming.fetchStart; + return span; +} + +/** Formats a performance long task event as a span. */ +function getLongTaskSpan( + longTask: PerformanceLongTaskTiming, traceId: string, + parentSpanId: string): Span { + const span = new Span(); + span.traceId = traceId; + span.parentSpanId = parentSpanId; + span.name = 'Long JS task'; + span.startPerfTime = longTask.startTime; + span.endPerfTime = longTask.startTime + longTask.duration; + span.attributes[ATTRIBUTE_LONG_TASK_ATTRIBUTION] = + JSON.stringify(longTask.attribution); + return span; +} + +/** Gets annotations for a navigation span including paint timings. */ +function getNavigationAnnotations(perfEntries: GroupedPerfEntries): + Annotation[] { + const navigation = perfEntries.navigationTiming; + if (!navigation) return []; + + const navAnnotations = + annotationsForPerfTimeFields(navigation, NAVIGATION_TIMING_EVENTS); + + for (const paintTiming of perfEntries.paintTimings) { + navAnnotations.push({ + timestamp: paintTiming.startTime, + description: paintTiming.name, + attributes: {}, + }); + } + return navAnnotations; +} diff --git a/packages/opencensus-web-instrumentation-perf/src/perf-recorder.ts b/packages/opencensus-web-instrumentation-perf/src/perf-recorder.ts new file mode 100644 index 00000000..eb8fe21f --- /dev/null +++ b/packages/opencensus-web-instrumentation-perf/src/perf-recorder.ts @@ -0,0 +1,98 @@ +/** + * Copyright 2019, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {PerformanceLongTaskTiming, PerformanceNavigationTimingExtended, PerformanceObserverEntryList, PerformancePaintTiming, PerformanceResourceTimingExtended, WindowWithPerformanceObserver} from './perf-types'; + +/** Cast `window` to have PerformanceObserver. */ +const windowWithPerfObserver = window as WindowWithPerformanceObserver; + +/** Store long task performance entries recorded with PerformanceObserver. */ +const longTasks: PerformanceLongTaskTiming[] = []; + +/** How big to set the performance timing buffer so timings aren't lost. */ +const RESOURCE_TIMING_BUFFER_SIZE = 2000; + +/** Represent all collected performance timings grouped by type. */ +export interface GroupedPerfEntries { + navigationTiming?: PerformanceNavigationTimingExtended; + paintTimings: PerformancePaintTiming[]; + resourceTimings: PerformanceResourceTimingExtended[]; + longTasks: PerformanceLongTaskTiming[]; +} + +/** + * Begin recording performance timings. This starts tracking `longtask` timings + * and also increases the resource timing buffer size. + */ +export function recordPerfEntries() { + if (!windowWithPerfObserver.performance) return; + + if (performance.setResourceTimingBufferSize) { + performance.setResourceTimingBufferSize(RESOURCE_TIMING_BUFFER_SIZE); + } + + if (windowWithPerfObserver.PerformanceObserver) { + const longTaskObserver = + new windowWithPerfObserver.PerformanceObserver(onLongTasks); + longTaskObserver.observe({entryTypes: ['longtask']}); + } +} + +function onLongTasks(entryList: PerformanceObserverEntryList) { + // These must be PerformanceLongTaskTiming objects because we only observe + // 'longtask' above. + longTasks.push(...(entryList.getEntries() as PerformanceLongTaskTiming[])); +} + +/** Returns the recorded performance entries but does not clear them. */ +export function getPerfEntries(): GroupedPerfEntries { + if (!windowWithPerfObserver.performance) { + return { + resourceTimings: [], + longTasks: [], + paintTimings: [], + }; + } + + const perf = windowWithPerfObserver.performance; + + const entries: GroupedPerfEntries = { + resourceTimings: perf.getEntriesByType('resource') as + PerformanceResourceTimingExtended[], + paintTimings: perf.getEntriesByType('paint') as PerformancePaintTiming[], + longTasks: longTasks.slice(), + }; + + const navEntries = perf.getEntriesByType('navigation'); + if (navEntries.length) { + entries.navigationTiming = + navEntries[0] as PerformanceNavigationTimingExtended; + } + + return entries; +} + +/** Clears resource timings, marks, measures and stored long task timings. */ +export function clearPerfEntries() { + if (!windowWithPerfObserver.performance) return; + longTasks.length = 0; + windowWithPerfObserver.performance.clearResourceTimings(); + windowWithPerfObserver.performance.clearMarks(); + windowWithPerfObserver.performance.clearMeasures(); +} + +/** Expose the resource timing buffer size for unit test. */ +export const TEST_ONLY = {RESOURCE_TIMING_BUFFER_SIZE}; diff --git a/packages/opencensus-web-instrumentation-perf/src/perf-types.ts b/packages/opencensus-web-instrumentation-perf/src/perf-types.ts new file mode 100644 index 00000000..9cd6dc1e --- /dev/null +++ b/packages/opencensus-web-instrumentation-perf/src/perf-types.ts @@ -0,0 +1,164 @@ +/** + * Copyright 2019, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview These are TypeScript interfaces for performance timing API. + * These are needed because not all performance API types are included in TS. + * See: https://github.com/Microsoft/TypeScript/issues/19816 + * Many of these were adapted from here: + * https://github.com/Microsoft/TypeScript/blame/cca2631a90fb414f7c830f2d2895a3b5f0db896f/lib/lib.webworker.d.ts#L1741httpf://github.com/Microsoft/TypeScript/blame/cca2631a90fb414f7c830f2d2895a3b5f0db896f/lib/lib.webworker.d.ts#L1741 + * + * These interfaces use `declare` as an indicator that their properties should + * not be renamed by minifiers e.g. the Closure Compiler, which will be relevant + * once OpenCensus Web can produce optimized builds. + * See: https://github.com/angular/tsickle#declare + */ + +/** + * Performance information sent from server in the `Server-Timing` header. + * See https://developer.mozilla.org/en-US/docs/Web/API/PerformanceServerTiming + */ +export declare interface PerformanceServerTiming { + readonly description: string; + readonly duration: number; + readonly name: string; +} + +/** Type for the `toJSON` function that is included in performance types. */ +export type toJSONFunction = () => {}; + +/** + * Performance timing for resources fetched for the page e.g. CSS, XHRs, etc. + * See: + * https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming + */ +export declare interface PerformanceResourceTimingExtended extends + PerformanceEntry { + /** Index signature allows annotations generation from a list of fields. */ + [index: string]: number|undefined|PerformanceServerTiming[]|string| + toJSONFunction; + + readonly serverTiming?: PerformanceServerTiming[]; + + readonly connectEnd: number; + readonly connectStart: number; + readonly decodedBodySize: number; + readonly domainLookupEnd: number; + readonly domainLookupStart: number; + readonly encodedBodySize: number; + readonly fetchStart: number; + readonly initiatorType: string; + readonly nextHopProtocol: string; + readonly redirectEnd: number; + readonly redirectStart: number; + readonly requestStart: number; + readonly responseEnd: number; + readonly responseStart: number; + readonly secureConnectionStart: number; + readonly transferSize: number; + readonly workerStart: number; + toJSON: toJSONFunction; +} + +/** + * Performance timing for the initial user navigation and HTML load. + * See: + * https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming + */ +export declare interface PerformanceNavigationTimingExtended extends + PerformanceResourceTimingExtended { + readonly initiatorType: 'navigation'; + + readonly domComplete: number; + readonly domContentLoadedEventEnd: number; + readonly domContentLoadedEventStart: number; + readonly domInteractive: number; + readonly loadEventEnd: number; + readonly loadEventStart: number; + readonly type: NavigationType; + readonly unloadEventEnd: number; + readonly unloadEventStart: number; +} + +/** + * Interface for PeformanceObserver, a utility to be notified for performance + * events. This is the only way to record long task timings. + * See https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver + */ +export declare interface PerformanceObserver { + new(callback: + (entries: PerformanceObserverEntryList, + observer: PerformanceObserver) => void): PerformanceObserver; + disconnect(): void; + observe(options: PerformanceObserverConfig): void; + takeRecords(): PerformanceEntry[]; +} + +/** Types of entries that a PerformanceObserver can observe. */ +export type PerformanceObserverEntryType = + 'frame'|'navigation'|'resource'|'mark'|'measure'|'paint'|'longtask'; + +/** Type for the config passed to the PerformanceObserver.observe method. */ +export declare interface PerformanceObserverConfig { + readonly buffered?: boolean; + readonly entryTypes: PerformanceObserverEntryType[]; +} + +/** Type for the performance entry list sent to performance observers. */ +export declare interface PerformanceObserverEntryList { + getEntries(): PerformanceEntry[]; + getEntriesByName(name: string, type?: string): PerformanceEntry[]; + getEntriesByType(type: string): PerformanceEntry[]; +} + +/** + * Performance timing entry for paint events, e.g. first contentful paint. + * See: https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming + */ +export declare interface PerformancePaintTiming extends PerformanceEntry { + readonly entryType: 'paint'; + readonly name: 'first-paint'|'first-contentful-paint'; +} + +/** + * Additional attribution information about long task timings. + * See https://developer.mozilla.org/en-US/docs/Web/API/TaskAttributionTiming + */ +export declare interface TaskAttributionTiming { + readonly containerType: 'iframe'|'embed'|'object'; + readonly containerSrc: string; + readonly containerId: string; + readonly containerName: string; + readonly name: string; + readonly entryType: 'taskattribution'; + readonly startTime: number; + readonly duration: number; +} + +/** + * Performance entry for "long tasks", that is JS event loops that take > 50ms. + * See + * https://developer.mozilla.org/en-US/docs/Web/API/PerformanceLongTaskTiming + */ +export declare interface PerformanceLongTaskTiming extends PerformanceEntry { + readonly entryType: 'longtask'; + readonly attribution: TaskAttributionTiming[]; +} + +/** Enables casting `window` to have optional `PerformanceObserver` field. */ +export declare type WindowWithPerformanceObserver = Window & { + readonly PerformanceObserver?: PerformanceObserver; +}; diff --git a/packages/opencensus-web-instrumentation-perf/src/resource-span.ts b/packages/opencensus-web-instrumentation-perf/src/resource-span.ts new file mode 100644 index 00000000..245a40a9 --- /dev/null +++ b/packages/opencensus-web-instrumentation-perf/src/resource-span.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2019, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as webCore from '@opencensus/web-core'; +import {PerformanceResourceTimingExtended} from './perf-types'; +import {annotationsForPerfTimeFields} from './util'; + +/** PerformanceEntry time event fields to create as span annotations. */ +const PERFORMANCE_ENTRY_EVENTS = [ + 'workerStart', + 'fetchStart', + 'domainLookupStart', + 'domainLookupEnd', + 'connectStart', + 'connectEnd', + 'secureConnectionStart', + 'redirectStart', + 'redirectEnd', + 'requestStart', + 'responseStart', + 'responseEnd', +]; + +/** Returns a `Span` based on a browser performance API resource timing. */ +export function getResourceSpan( + resourceTiming: PerformanceResourceTimingExtended, traceId: string, + parentSpanId: string, spanId?: string): webCore.Span { + const span = new webCore.Span(spanId); + span.traceId = traceId; + span.parentSpanId = parentSpanId; + const parsedUrl = webCore.parseUrl(resourceTiming.name); + span.name = parsedUrl.pathname; + span.startPerfTime = resourceTiming.startTime; + span.kind = webCore.SpanKind.CLIENT; + span.endPerfTime = resourceTiming.responseEnd; + span.attributes = getResourceSpanAttributes(resourceTiming, parsedUrl); + span.annotations = + annotationsForPerfTimeFields(resourceTiming, PERFORMANCE_ENTRY_EVENTS); + return span; +} + +function getResourceSpanAttributes( + resourceTiming: PerformanceResourceTimingExtended, + parsedUrl: webCore.ParsedUrl): webCore.Attributes { + const attrs: webCore.Attributes = {}; + attrs[webCore.ATTRIBUTE_HTTP_URL] = resourceTiming.name; + attrs[webCore.ATTRIBUTE_HTTP_HOST] = parsedUrl.host; + attrs[webCore.ATTRIBUTE_HTTP_PATH] = parsedUrl.pathname; + attrs[webCore.ATTRIBUTE_HTTP_USER_AGENT] = navigator.userAgent; + + if (resourceTiming.nextHopProtocol) { + attrs[webCore.ATTRIBUTE_HTTP_NEXT_HOP_PROTOCOL] = + resourceTiming.nextHopProtocol; + } + + const initiatorType = resourceTiming.initiatorType; + if (initiatorType) { + if (initiatorType !== 'xmlhttprequest' && initiatorType !== 'fetch') { + attrs[webCore.ATTRIBUTE_HTTP_METHOD] = 'GET'; + } + attrs[webCore.ATTRIBUTE_HTTP_INITIATOR_TYPE] = initiatorType; + } + + if (resourceTiming.transferSize) { + attrs[webCore.ATTRIBUTE_HTTP_RESP_SIZE] = resourceTiming.transferSize; + } + if (resourceTiming.encodedBodySize) { + attrs[webCore.ATTRIBUTE_HTTP_RESP_ENCODED_BODY_SIZE] = + resourceTiming.encodedBodySize; + } + if (resourceTiming.decodedBodySize) { + attrs[webCore.ATTRIBUTE_HTTP_RESP_DECODED_BODY_SIZE] = + resourceTiming.decodedBodySize; + } + return attrs; +} diff --git a/packages/opencensus-web-instrumentation-perf/src/util.ts b/packages/opencensus-web-instrumentation-perf/src/util.ts new file mode 100644 index 00000000..b4748fdd --- /dev/null +++ b/packages/opencensus-web-instrumentation-perf/src/util.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2019, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Annotation} from '@opencensus/web-core'; +import {PerformanceNavigationTimingExtended, PerformanceResourceTimingExtended} from './perf-types'; + +/** + * Returns annotations based on fields of a performance entry. + * @param perfEntry The performance entry with fields that are either 0 to + * signify missing data, or a point event value in browser performance + * clock time for an event that occurred e.g. `connectStart`. + * @param annotationFields The fields to extract from the performance timing as + * annotations. The description of each annotation will be set to the field + * name. + */ +export function annotationsForPerfTimeFields( + perfEntry: PerformanceResourceTimingExtended| + PerformanceNavigationTimingExtended, + annotationsFields: string[]): Annotation[] { + const annotations: Annotation[] = []; + for (const annotationField of annotationsFields) { + const maybeTime = perfEntry[annotationField] as number | undefined; + // Either a value of 0 or `undefined` represents missing data for browser + // performance timing fields. + if (maybeTime) { + annotations.push( + {timestamp: maybeTime, description: annotationField, attributes: {}}); + } + } + return annotations; +} diff --git a/packages/opencensus-web-instrumentation-perf/test/index.ts b/packages/opencensus-web-instrumentation-perf/test/index.ts index ea3dfdf4..5609c15a 100644 --- a/packages/opencensus-web-instrumentation-perf/test/index.ts +++ b/packages/opencensus-web-instrumentation-perf/test/index.ts @@ -14,6 +14,9 @@ * limitations under the License. */ -describe('the empty library', () => { - it('does nothing', () => {}); -}); +// This file is an entry point for the webpack test configuration, so this +// should import from all test files. + +import './test-initial-load-root-span'; +import './test-perf-recorder'; +import './test-resource-span'; diff --git a/packages/opencensus-web-instrumentation-perf/test/test-initial-load-root-span.ts b/packages/opencensus-web-instrumentation-perf/test/test-initial-load-root-span.ts new file mode 100644 index 00000000..10667bcf --- /dev/null +++ b/packages/opencensus-web-instrumentation-perf/test/test-initial-load-root-span.ts @@ -0,0 +1,302 @@ +/** + * Copyright 2019, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Annotation, Attributes, SpanKind, Tracer} from '@opencensus/web-core'; +import {getInitialLoadRootSpan} from '../src/initial-load-root-span'; +import {GroupedPerfEntries} from '../src/perf-recorder'; + +const SPAN_ID_REGEX = /[0-9a-f]{16}/; +const USER_AGENT = 'Mozilla/5.0 TEST'; + +const PERF_ENTRIES: GroupedPerfEntries = { + resourceTimings: [ + { + connectEnd: 0, + connectStart: 0, + decodedBodySize: 0, + domainLookupEnd: 0, + domainLookupStart: 0, + duration: 13.100000011036173, + encodedBodySize: 0, + entryType: 'resource', + fetchStart: 266.9999999925494, + initiatorType: 'link', + name: 'http://localhost:4200/resource', + nextHopProtocol: 'h2', + redirectEnd: 0, + redirectStart: 0, + requestStart: 0, + responseEnd: 280.1000000035856, + responseStart: 0, + secureConnectionStart: 0, + serverTiming: [], + startTime: 266.9999999925494, + transferSize: 0, + workerStart: 0, + toJSON: () => ({}), + }, + ], + longTasks: [ + { + name: 'self', + entryType: 'longtask', + startTime: 11870.39999999979, + duration: 1063.7000000024273, + attribution: [{ + name: 'script', + entryType: 'taskattribution', + startTime: 0, + duration: 0, + containerType: 'iframe', + containerSrc: '', + containerId: '', + containerName: '', + }], + toJSON: () => ({}), + }, + ], + navigationTiming: { + name: 'http://localhost:4200/', + entryType: 'navigation', + startTime: 0, + duration: 20985.30000000028, + initiatorType: 'navigation', + nextHopProtocol: 'http/1.1', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 25.100000002566958, + domainLookupStart: 28.09999999954016, + domainLookupEnd: 28.09999999954016, + connectStart: 28.09999999954016, + connectEnd: 28.300000001763692, + secureConnectionStart: 0, + requestStart: 28.500000000349246, + responseStart: 384.6000000012282, + responseEnd: 394.20000000245636, + transferSize: 4667, + encodedBodySize: 4404, + decodedBodySize: 4404, + serverTiming: [], + unloadEventStart: 0, + unloadEventEnd: 0, + domInteractive: 12126.70000000071, + domContentLoadedEventStart: 12126.70000000071, + domContentLoadedEventEnd: 12933.80000000252, + domComplete: 20967.70000000106, + loadEventStart: 20967.899999999645, + loadEventEnd: 20985.30000000028, + type: 'navigate', + redirectCount: 0, + toJSON: () => ({}), + }, + paintTimings: [ + { + name: 'first-paint', + entryType: 'paint', + startTime: 606.9000000024971, + duration: 0, + toJSON: () => ({}), + }, + { + name: 'first-contentful-paint', + entryType: 'paint', + startTime: 606.9000000024971, + duration: 0, + toJSON: () => ({}), + }, + ], +}; + +const EXPECTED_ROOT_ATTRIBUTES: Attributes = { + 'http.url': 'http://localhost:4200/', + 'http.user_agent': USER_AGENT, + 'nav.type': 'navigate', +}; +const EXPECTED_ROOT_ANNOTATIONS: Annotation[] = [ + { + timestamp: 12126.70000000071, + description: 'domInteractive', + attributes: {}, + }, + { + timestamp: 20967.70000000106, + description: 'domComplete', + attributes: {}, + }, + { + timestamp: 20967.899999999645, + description: 'loadEventStart', + attributes: {}, + }, + { + timestamp: 20985.30000000028, + description: 'loadEventEnd', + attributes: {}, + }, + { + timestamp: 606.9000000024971, + description: 'first-paint', + attributes: {}, + }, + { + timestamp: 606.9000000024971, + description: 'first-contentful-paint', + attributes: {}, + }, +]; + +const EXPECTED_NAV_FETCH_ATTRIBUTES: Attributes = { + 'http.url': 'http://localhost:4200/', + 'http.host': 'localhost:4200', + 'http.path': '/', + 'http.user_agent': USER_AGENT, + 'http.method': 'GET', + 'http.initiator_type': 'navigation', + 'http.next_hop_protocol': 'http/1.1', + 'http.resp_size': 4667, + 'http.resp_encoded_body_size': 4404, + 'http.resp_decoded_body_size': 4404, +}; + +const EXPECTED_NAV_FETCH_ANNOTATIONS: Annotation[] = [ + { + timestamp: 25.100000002566958, + description: 'fetchStart', + attributes: {}, + }, + { + timestamp: 28.09999999954016, + description: 'domainLookupStart', + attributes: {}, + }, + { + timestamp: 28.09999999954016, + description: 'domainLookupEnd', + attributes: {}, + }, + { + timestamp: 28.09999999954016, + description: 'connectStart', + attributes: {}, + }, + { + timestamp: 28.300000001763692, + description: 'connectEnd', + attributes: {}, + }, + { + timestamp: 28.500000000349246, + description: 'requestStart', + attributes: {}, + }, + { + timestamp: 384.6000000012282, + description: 'responseStart', + attributes: {}, + }, + { + timestamp: 394.20000000245636, + description: 'responseEnd', + attributes: {}, + }, +]; + +const EXPECTED_RESOURCE_ATTRIBUTES: Attributes = { + 'http.url': 'http://localhost:4200/resource', + 'http.host': 'localhost:4200', + 'http.path': '/resource', + 'http.user_agent': 'Mozilla/5.0 TEST', + 'http.next_hop_protocol': 'h2', + 'http.method': 'GET', + 'http.initiator_type': 'link', +}; +const EXPECTED_RESOURCE_ANNOTATIONS: Annotation[] = [ + { + 'timestamp': 266.9999999925494, + 'description': 'fetchStart', + 'attributes': {}, + }, + { + 'timestamp': 280.1000000035856, + 'description': 'responseEnd', + 'attributes': {}, + }, +]; + +describe('getInitialLoadRootSpan', () => { + beforeEach(() => { + spyOnProperty(navigator, 'userAgent').and.returnValue(USER_AGENT); + }); + + it('creates a parent span for overall load and child spans', () => { + const navigationFetchSpanId = '000000000000000a'; + const traceId = '0000000000000000000000000000000b'; + + const root = getInitialLoadRootSpan( + new Tracer(), PERF_ENTRIES, navigationFetchSpanId, traceId); + + expect(root.name).toBe('Nav./'); + expect(root.kind).toBe(SpanKind.UNSPECIFIED); + expect(root.parentSpanId).toBe(''); + expect(root.id).toMatch(SPAN_ID_REGEX); + expect(root.traceId).toBe(traceId); + expect(root.startPerfTime).toBe(0); + expect(root.endPerfTime).toBe(20985.30000000028); + expect(root.attributes).toEqual(EXPECTED_ROOT_ATTRIBUTES); + expect(root.annotations).toEqual(EXPECTED_ROOT_ANNOTATIONS); + + expect(root.spans.length).toBe(3); + const [navigationFetchSpan, resourceSpan, longTaskSpan] = root.spans; + + expect(navigationFetchSpan.traceId).toBe(traceId); + expect(navigationFetchSpan.id).toMatch(SPAN_ID_REGEX); + expect(navigationFetchSpan.parentSpanId).toBe(root.id); + expect(navigationFetchSpan.name).toBe('/'); + expect(navigationFetchSpan.kind).toBe(SpanKind.CLIENT); + expect(navigationFetchSpan.startPerfTime).toBe(25.100000002566958); + expect(navigationFetchSpan.endPerfTime).toBe(394.20000000245636); + expect(navigationFetchSpan.attributes) + .toEqual(EXPECTED_NAV_FETCH_ATTRIBUTES); + expect(navigationFetchSpan.annotations) + .toEqual(EXPECTED_NAV_FETCH_ANNOTATIONS); + + expect(resourceSpan.traceId).toBe(traceId); + expect(resourceSpan.id).toMatch(SPAN_ID_REGEX); + expect(resourceSpan.parentSpanId).toBe(root.id); + expect(resourceSpan.name).toBe('/resource'); + expect(resourceSpan.kind).toBe(SpanKind.CLIENT); + expect(resourceSpan.startPerfTime).toBe(266.9999999925494); + expect(resourceSpan.endPerfTime).toBe(280.1000000035856); + expect(resourceSpan.attributes).toEqual(EXPECTED_RESOURCE_ATTRIBUTES); + expect(resourceSpan.annotations).toEqual(EXPECTED_RESOURCE_ANNOTATIONS); + + expect(longTaskSpan.traceId).toBe(traceId); + expect(longTaskSpan.id).toMatch(SPAN_ID_REGEX); + expect(longTaskSpan.parentSpanId).toBe(root.id); + expect(longTaskSpan.name).toBe('Long JS task'); + expect(longTaskSpan.kind).toBe(SpanKind.UNSPECIFIED); + expect(longTaskSpan.startPerfTime).toBe(11870.39999999979); + expect(longTaskSpan.endPerfTime).toBe(12934.100000002218); + expect(longTaskSpan.attributes).toEqual({ + 'long_task.attribution': + '[{"name":"script","entryType":"taskattribution","startTime":0,' + + '"duration":0,"containerType":"iframe","containerSrc":"",' + + '"containerId":"","containerName":""}]', + }); + expect(longTaskSpan.annotations).toEqual([]); + }); +}); diff --git a/packages/opencensus-web-instrumentation-perf/test/test-perf-recorder.ts b/packages/opencensus-web-instrumentation-perf/test/test-perf-recorder.ts new file mode 100644 index 00000000..24fdf7a6 --- /dev/null +++ b/packages/opencensus-web-instrumentation-perf/test/test-perf-recorder.ts @@ -0,0 +1,218 @@ +/** + * Copyright 2019, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {clearPerfEntries, getPerfEntries, recordPerfEntries, TEST_ONLY} from '../src/perf-recorder'; +import {PerformanceLongTaskTiming, PerformanceNavigationTimingExtended, PerformanceObserver, PerformanceObserverConfig, PerformanceObserverEntryList, PerformancePaintTiming, PerformanceResourceTimingExtended} from '../src/perf-types'; + +const LONG_TASK_1: PerformanceLongTaskTiming = { + name: 'self', + entryType: 'longtask', + startTime: 1, + duration: 2, + attribution: [], + toJSON: () => ({}), +}; +const LONG_TASK_2: PerformanceLongTaskTiming = { + name: 'self', + entryType: 'longtask', + startTime: 3, + duration: 4, + attribution: [], + toJSON: () => ({}), +}; +const NAVIGATION_ENTRY: PerformanceNavigationTimingExtended = { + name: 'http://localhost:4200/', + entryType: 'navigation', + startTime: 0, + duration: 20985.30000000028, + initiatorType: 'navigation', + nextHopProtocol: 'http/1.1', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 25.100000002566958, + domainLookupStart: 28.09999999954016, + domainLookupEnd: 28.09999999954016, + connectStart: 28.09999999954016, + connectEnd: 28.300000001763692, + secureConnectionStart: 0, + requestStart: 28.500000000349246, + responseStart: 384.6000000012282, + responseEnd: 394.20000000245636, + transferSize: 4667, + encodedBodySize: 4404, + decodedBodySize: 4404, + serverTiming: [], + unloadEventStart: 0, + unloadEventEnd: 0, + domInteractive: 12126.70000000071, + domContentLoadedEventStart: 12126.70000000071, + domContentLoadedEventEnd: 12933.80000000252, + domComplete: 20967.70000000106, + loadEventStart: 20967.899999999645, + loadEventEnd: 20985.30000000028, + type: 'navigate', + redirectCount: 0, + toJSON: () => ({}), +}; +const PAINT_ENTRY: PerformancePaintTiming = { + name: 'first-paint', + entryType: 'paint', + startTime: 606.9000000024971, + duration: 0, + toJSON: () => ({}), +}; +const RESOURCE_ENTRY: PerformanceResourceTimingExtended = { + connectEnd: 0, + connectStart: 0, + decodedBodySize: 0, + domainLookupEnd: 0, + domainLookupStart: 0, + duration: 13.100000011036173, + encodedBodySize: 0, + entryType: 'resource', + fetchStart: 266.9999999925494, + initiatorType: 'link', + name: 'http://localhost:4200/resource', + nextHopProtocol: 'h2', + redirectEnd: 0, + redirectStart: 0, + requestStart: 0, + responseEnd: 280.1000000035856, + responseStart: 0, + secureConnectionStart: 0, + serverTiming: [], + startTime: 266.9999999925494, + transferSize: 0, + workerStart: 0, + toJSON: () => ({}), +}; +const PERF_ENTRIES_BY_TYPE = new Map([ + ['navigation', [NAVIGATION_ENTRY]], + ['paint', [PAINT_ENTRY]], + ['resource', [RESOURCE_ENTRY]], +]); + +declare type WindowWithMutablePerformanceObserver = Window & { + PerformanceObserver?: PerformanceObserver; +}; + +const windowWithPerfObserver = window as WindowWithMutablePerformanceObserver; + +describe('performance recorder functions', () => { + class MockPerfEntryList implements PerformanceObserverEntryList { + constructor(readonly entries: PerformanceEntry[]) {} + getEntries() { + return this.entries; + } + getEntriesByName(): never { + throw new Error('MockPerfEntryList.getEntriesByName unexpectedly called'); + } + getEntriesByType(): never { + throw new Error('MockPerfEntryList.getEntriesByType unexpectedly called'); + } + } + + class MockPerformanceObserver { + config?: PerformanceObserverConfig; + constructor( + readonly callback: + (entries: PerformanceObserverEntryList, + observer: PerformanceObserver) => void) { + performanceObserver = this; + } + observe(config: PerformanceObserverConfig) { + this.config = config; + } + sendMockPerfEntries(entryList: PerformanceObserverEntryList) { + // The cast is needed because TS interfaces with `new` can't be + // implemented with classes. See + // https://stackoverflow.com/questions/13407036/how-does-typescript-interfaces-with-construct-signatures-work + this.callback(entryList, this as unknown as PerformanceObserver); + } + } + + const realPerformanceObserver = windowWithPerfObserver.PerformanceObserver; + let performanceObserver: MockPerformanceObserver|undefined; + + beforeEach(() => { + clearPerfEntries(); // Needed to reset long tasks list. + windowWithPerfObserver.PerformanceObserver = + MockPerformanceObserver as unknown as PerformanceObserver; + }); + afterEach(() => { + windowWithPerfObserver.PerformanceObserver = realPerformanceObserver; + }); + + describe('recordPerfEntries', () => { + it('increases the resource buffer size', () => { + spyOn(performance, 'setResourceTimingBufferSize'); + recordPerfEntries(); + expect(performance.setResourceTimingBufferSize) + .toHaveBeenCalledWith(TEST_ONLY.RESOURCE_TIMING_BUFFER_SIZE); + }); + + it('starts tracking long tasks', () => { + recordPerfEntries(); + expect(performanceObserver).toBeDefined(); + expect(performanceObserver!.config).toEqual({entryTypes: ['longtask']}); + }); + }); + + describe('getPerfEntries', () => { + it('combines perf entries for nav, paint, resource and long tasks', () => { + recordPerfEntries(); + performanceObserver!.sendMockPerfEntries( + new MockPerfEntryList([LONG_TASK_1, LONG_TASK_2])); + spyOn(performance, 'getEntriesByType') + .and.callFake((entryType: string) => { + return PERF_ENTRIES_BY_TYPE.get(entryType); + }); + + expect(getPerfEntries()).toEqual({ + navigationTiming: NAVIGATION_ENTRY, + paintTimings: [PAINT_ENTRY], + resourceTimings: [RESOURCE_ENTRY], + longTasks: [LONG_TASK_1, LONG_TASK_2], + }); + }); + }); + + describe('clearPerfEntries', () => { + it('clears resource timings, marks and measures', () => { + spyOn(performance, 'clearResourceTimings'); + spyOn(performance, 'clearMarks'); + spyOn(performance, 'clearMeasures'); + + clearPerfEntries(); + + expect(performance.clearResourceTimings).toHaveBeenCalled(); + expect(performance.clearMarks).toHaveBeenCalled(); + expect(performance.clearMeasures).toHaveBeenCalled(); + }); + + it('clears stored long tasks', () => { + recordPerfEntries(); + performanceObserver!.sendMockPerfEntries( + new MockPerfEntryList([LONG_TASK_1])); + expect(getPerfEntries().longTasks.length).toBe(1); + + clearPerfEntries(); + + expect(getPerfEntries().longTasks.length).toBe(0); + }); + }); +}); diff --git a/packages/opencensus-web-instrumentation-perf/test/test-resource-span.ts b/packages/opencensus-web-instrumentation-perf/test/test-resource-span.ts new file mode 100644 index 00000000..8d50499c --- /dev/null +++ b/packages/opencensus-web-instrumentation-perf/test/test-resource-span.ts @@ -0,0 +1,146 @@ +/** + * Copyright 2019, OpenCensus Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {SpanKind} from '@opencensus/web-core'; +import {PerformanceResourceTimingExtended} from '../src/perf-types'; +import {getResourceSpan} from '../src/resource-span'; + +const SPAN_ID_REGEX = /[0-9a-f]{16}/; +const USER_AGENT = 'Mozilla/5.0 TEST'; +const TRACE_ID = '00000000000000000000000000000001'; +const PARENT_SPAN_ID = '000000000000000a'; + +describe('getResourceSpan', () => { + beforeEach(() => { + spyOnProperty(navigator, 'userAgent').and.returnValue(USER_AGENT); + }); + + it('generates span for resource timing without detailed timestamps', () => { + // This is similar to what a resource timing looks like from a different + // origin without the `Timing-Allow-Origin`: + // See: https://w3c.github.io/resource-timing/#sec-cross-origin-resources + const resourceTimingWithoutDetails: PerformanceResourceTimingExtended = { + connectEnd: 0, + connectStart: 0, + decodedBodySize: 0, + domainLookupEnd: 0, + domainLookupStart: 0, + duration: 13.1, + encodedBodySize: 0, + entryType: 'resource', + fetchStart: 266.9, + initiatorType: 'link', // Implies GET method + name: 'http://localhost:4200/style.css', + nextHopProtocol: 'h2', + redirectEnd: 0, + redirectStart: 0, + requestStart: 0, + responseEnd: 280.1, + responseStart: 0, + secureConnectionStart: 0, + serverTiming: [], + startTime: 266.9, + transferSize: 0, + workerStart: 0, + toJSON: () => ({}), + }; + + const span = + getResourceSpan(resourceTimingWithoutDetails, TRACE_ID, PARENT_SPAN_ID); + expect(span.id).toMatch(SPAN_ID_REGEX); + expect(span.traceId).toBe(TRACE_ID); + expect(span.parentSpanId).toBe(PARENT_SPAN_ID); + expect(span.name).toBe('/style.css'); + expect(span.kind).toBe(SpanKind.CLIENT); + expect(span.startPerfTime).toBe(resourceTimingWithoutDetails.startTime); + expect(span.endPerfTime).toBe(resourceTimingWithoutDetails.responseEnd); + expect(span.annotations).toEqual([ + {timestamp: 266.9, description: 'fetchStart', attributes: {}}, + {timestamp: 280.1, description: 'responseEnd', attributes: {}}, + ]); + expect(span.attributes).toEqual({ + 'http.host': 'localhost:4200', + 'http.user_agent': 'Mozilla/5.0 TEST', + 'http.next_hop_protocol': 'h2', + 'http.method': 'GET', + 'http.path': '/style.css', + 'http.initiator_type': 'link', + 'http.url': 'http://localhost:4200/style.css', + }); + }); + + it('generates events for resource timing with detailed timestamps', () => { + // This is similar to what a resource timing from the same origin. + const resourceTimingWithDetails: PerformanceResourceTimingExtended = { + startTime: 1, + fetchStart: 1, + domainLookupStart: 2, + domainLookupEnd: 3, + connectStart: 4, + connectEnd: 5, + secureConnectionStart: 6, + redirectStart: 7, + redirectEnd: 8, + requestStart: 9, + responseStart: 100, + responseEnd: 11, + duration: 11, + transferSize: 1300, + encodedBodySize: 1100, + decodedBodySize: 1000, + entryType: 'xmlhttprequest', // May not be a GET + initiatorType: 'link', + name: 'http://localhost:4200/style.css', + nextHopProtocol: 'h2', + serverTiming: [], + workerStart: 0, + toJSON: () => ({}), + }; + + const span = + getResourceSpan(resourceTimingWithDetails, TRACE_ID, PARENT_SPAN_ID); + expect(span.id).toMatch(SPAN_ID_REGEX); + expect(span.annotations).toEqual([ + {timestamp: 1, description: 'fetchStart', attributes: {}}, + {timestamp: 2, description: 'domainLookupStart', attributes: {}}, + {timestamp: 3, description: 'domainLookupEnd', attributes: {}}, + {timestamp: 4, description: 'connectStart', attributes: {}}, + {timestamp: 5, description: 'connectEnd', attributes: {}}, + { + timestamp: 6, + description: 'secureConnectionStart', + attributes: {}, + }, + {timestamp: 7, description: 'redirectStart', attributes: {}}, + {timestamp: 8, description: 'redirectEnd', attributes: {}}, + {timestamp: 9, description: 'requestStart', attributes: {}}, + {timestamp: 100, description: 'responseStart', attributes: {}}, + {timestamp: 11, description: 'responseEnd', attributes: {}}, + ]); + expect(span.attributes).toEqual({ + 'http.url': 'http://localhost:4200/style.css', + 'http.host': 'localhost:4200', + 'http.path': '/style.css', + 'http.user_agent': 'Mozilla/5.0 TEST', + 'http.next_hop_protocol': 'h2', + 'http.method': 'GET', + 'http.initiator_type': 'link', + 'http.resp_size': 1300, + 'http.resp_encoded_body_size': 1100, + 'http.resp_decoded_body_size': 1000, + }); + }); +});