From 3b38ed068e22ed32ece0fea2b67295a649d28438 Mon Sep 17 00:00:00 2001 From: Krystian Kruk Date: Mon, 26 Apr 2021 22:59:28 +0200 Subject: [PATCH 1/2] feat(instrumentation-document-load): performance paint timing events --- .../src/documentLoad.ts | 53 +++---------- .../src/enums/EventNames.ts | 20 +++++ .../src/utils.ts | 79 +++++++++++++++++++ .../test/documentLoad.test.ts | 40 +++++++++- 4 files changed, 146 insertions(+), 46 deletions(-) create mode 100644 plugins/web/opentelemetry-instrumentation-document-load/src/enums/EventNames.ts create mode 100644 plugins/web/opentelemetry-instrumentation-document-load/src/utils.ts diff --git a/plugins/web/opentelemetry-instrumentation-document-load/src/documentLoad.ts b/plugins/web/opentelemetry-instrumentation-document-load/src/documentLoad.ts index f54cd16b79..04058a76eb 100644 --- a/plugins/web/opentelemetry-instrumentation-document-load/src/documentLoad.ts +++ b/plugins/web/opentelemetry-instrumentation-document-load/src/documentLoad.ts @@ -27,7 +27,6 @@ import { addSpanNetworkEvents, hasKey, PerformanceEntries, - PerformanceLegacy, PerformanceTimingNames as PTN, } from '@opentelemetry/web'; import { @@ -37,6 +36,10 @@ import { import { AttributeNames } from './enums/AttributeNames'; import { VERSION } from './version'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { + addSpanPerformancePaintEvents, + getPerformanceNavigationEntries, +} from './utils'; /** * This class represents a document load plugin @@ -73,9 +76,9 @@ export class DocumentLoadInstrumentation extends InstrumentationBase { * @param rootSpan */ private _addResourcesSpans(rootSpan: Span): void { - const resources: PerformanceResourceTiming[] = ( - otperformance as unknown as Performance - ).getEntriesByType?.('resource') as PerformanceResourceTiming[]; + const resources: PerformanceResourceTiming[] = ((otperformance as unknown) as Performance).getEntriesByType?.( + 'resource' + ) as PerformanceResourceTiming[]; if (resources) { resources.forEach(resource => { this._initResourceSpan(resource, rootSpan); @@ -90,7 +93,7 @@ export class DocumentLoadInstrumentation extends InstrumentationBase { const metaElement = [...document.getElementsByTagName('meta')].find( e => e.getAttribute('name') === TRACE_PARENT_HEADER ); - const entries = this._getEntries(); + const entries = getPerformanceNavigationEntries(); const traceparent = (metaElement && metaElement.content) || ''; context.with(propagation.extract(ROOT_CONTEXT, { traceparent }), () => { const rootSpan = this._startSpan( @@ -137,6 +140,8 @@ export class DocumentLoadInstrumentation extends InstrumentationBase { addSpanNetworkEvent(rootSpan, PTN.LOAD_EVENT_START, entries); addSpanNetworkEvent(rootSpan, PTN.LOAD_EVENT_END, entries); + addSpanPerformancePaintEvents(rootSpan); + this._endSpan(rootSpan, PTN.LOAD_EVENT_END, entries); }); } @@ -163,44 +168,6 @@ export class DocumentLoadInstrumentation extends InstrumentationBase { } } - /** - * gets performance entries of navigation - */ - private _getEntries() { - const entries: PerformanceEntries = {}; - const performanceNavigationTiming = ( - otperformance as unknown as Performance - ).getEntriesByType?.('navigation')[0] as PerformanceEntries; - - if (performanceNavigationTiming) { - const keys = Object.values(PTN); - keys.forEach((key: string) => { - if (hasKey(performanceNavigationTiming, key)) { - const value = performanceNavigationTiming[key]; - if (typeof value === 'number') { - entries[key] = value; - } - } - }); - } else { - // // fallback to previous version - const perf: typeof otperformance & PerformanceLegacy = otperformance; - const performanceTiming = perf.timing; - if (performanceTiming) { - const keys = Object.values(PTN); - keys.forEach((key: string) => { - if (hasKey(performanceTiming, key)) { - const value = performanceTiming[key]; - if (typeof value === 'number') { - entries[key] = value; - } - } - }); - } - } - return entries; - } - /** * Creates and ends a span with network information about resource added as timed events * @param resource diff --git a/plugins/web/opentelemetry-instrumentation-document-load/src/enums/EventNames.ts b/plugins/web/opentelemetry-instrumentation-document-load/src/enums/EventNames.ts new file mode 100644 index 0000000000..816a3df783 --- /dev/null +++ b/plugins/web/opentelemetry-instrumentation-document-load/src/enums/EventNames.ts @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry 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 + * + * https://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. + */ + +export enum EventNames { + FIRST_PAINT = 'firstPaint', + FIRST_CONTENTFUL_PAINT = 'firstContentfulPaint', +} diff --git a/plugins/web/opentelemetry-instrumentation-document-load/src/utils.ts b/plugins/web/opentelemetry-instrumentation-document-load/src/utils.ts new file mode 100644 index 0000000000..e31542ccc5 --- /dev/null +++ b/plugins/web/opentelemetry-instrumentation-document-load/src/utils.ts @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry 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 + * + * https://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 { Span } from '@opentelemetry/api'; +import { otperformance } from '@opentelemetry/core'; +import { + hasKey, + PerformanceEntries, + PerformanceLegacy, + PerformanceTimingNames as PTN, +} from '@opentelemetry/web'; +import { EventNames } from './enums/EventNames'; + +export const getPerformanceNavigationEntries = (): PerformanceEntries => { + const entries: PerformanceEntries = {}; + const performanceNavigationTiming = ((otperformance as unknown) as Performance).getEntriesByType?.( + 'navigation' + )[0] as PerformanceEntries; + + if (performanceNavigationTiming) { + const keys = Object.values(PTN); + keys.forEach((key: string) => { + if (hasKey(performanceNavigationTiming, key)) { + const value = performanceNavigationTiming[key]; + if (typeof value === 'number') { + entries[key] = value; + } + } + }); + } else { + // // fallback to previous version + const perf: typeof otperformance & PerformanceLegacy = otperformance; + const performanceTiming = perf.timing; + if (performanceTiming) { + const keys = Object.values(PTN); + keys.forEach((key: string) => { + if (hasKey(performanceTiming, key)) { + const value = performanceTiming[key]; + if (typeof value === 'number') { + entries[key] = value; + } + } + }); + } + } + + return entries; +}; + +const performancePaintNames = { + 'first-paint': EventNames.FIRST_PAINT, + 'first-contentful-paint': EventNames.FIRST_CONTENTFUL_PAINT, +}; + +export const addSpanPerformancePaintEvents = (span: Span) => { + const performancePaintTiming = ((otperformance as unknown) as Performance).getEntriesByType?.( + 'paint' + ); + if (performancePaintTiming) { + performancePaintTiming.forEach(({ name, startTime }) => { + if (hasKey(performancePaintNames, name)) { + span.addEvent(performancePaintNames[name], startTime); + } + }); + } +}; diff --git a/plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts b/plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts index fbef8427fd..2fe5e12a3b 100644 --- a/plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts +++ b/plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts @@ -36,6 +36,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { DocumentLoadInstrumentation } from '../src'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { EventNames } from '../src/enums/EventNames'; const exporter = new InMemorySpanExporter(); const provider = new BasicTracerProvider(); @@ -179,6 +180,25 @@ const entriesFallback = { loadEventEnd: 1571078170394, } as any; +const paintEntries: PerformanceEntryList = [ + { + duration: 0, + entryType: 'paint', + name: 'first-paint', + startTime: 7.480000003241003, + toJSON() {}, + }, + { + duration: 0, + entryType: 'paint', + name: 'first-contentful-paint', + startTime: 8.480000003241003, + toJSON() {}, + }, +]; + +performance.getEntriesByType; + const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'; @@ -243,6 +263,7 @@ describe('DocumentLoad Instrumentation', () => { spyEntries = sandbox.stub(window.performance, 'getEntriesByType'); spyEntries.withArgs('navigation').returns([entries]); spyEntries.withArgs('resource').returns([]); + spyEntries.withArgs('paint').returns([]); }); afterEach(() => { spyEntries.restore(); @@ -251,7 +272,7 @@ describe('DocumentLoad Instrumentation', () => { plugin.enable(); setTimeout(() => { assert.strictEqual(window.document.readyState, 'complete'); - assert.strictEqual(spyEntries.callCount, 2); + assert.strictEqual(spyEntries.callCount, 3); done(); }); }); @@ -267,6 +288,7 @@ describe('DocumentLoad Instrumentation', () => { spyEntries = sandbox.stub(window.performance, 'getEntriesByType'); spyEntries.withArgs('navigation').returns([entries]); spyEntries.withArgs('resource').returns([]); + spyEntries.withArgs('paint').returns([]); }); afterEach(() => { spyEntries.restore(); @@ -290,7 +312,7 @@ describe('DocumentLoad Instrumentation', () => { }) ); setTimeout(() => { - assert.strictEqual(spyEntries.callCount, 2); + assert.strictEqual(spyEntries.callCount, 3); done(); }); }); @@ -302,6 +324,7 @@ describe('DocumentLoad Instrumentation', () => { spyEntries = sandbox.stub(window.performance, 'getEntriesByType'); spyEntries.withArgs('navigation').returns([entries]); spyEntries.withArgs('resource').returns([]); + spyEntries.withArgs('paint').returns(paintEntries); }); afterEach(() => { spyEntries.restore(); @@ -325,6 +348,12 @@ describe('DocumentLoad Instrumentation', () => { assert.strictEqual(fetchSpan.name, 'documentLoad'); ensureNetworkEventsExists(rsEvents); + assert.strictEqual(fsEvents[9].name, EventNames.FIRST_PAINT); + assert.strictEqual( + fsEvents[10].name, + EventNames.FIRST_CONTENTFUL_PAINT + ); + assert.strictEqual(fsEvents[0].name, PTN.FETCH_START); assert.strictEqual(fsEvents[1].name, PTN.UNLOAD_EVENT_START); assert.strictEqual(fsEvents[2].name, PTN.UNLOAD_EVENT_END); @@ -339,7 +368,7 @@ describe('DocumentLoad Instrumentation', () => { assert.strictEqual(fsEvents[8].name, PTN.LOAD_EVENT_END); assert.strictEqual(rsEvents.length, 9); - assert.strictEqual(fsEvents.length, 9); + assert.strictEqual(fsEvents.length, 11); assert.strictEqual(exporter.getFinishedSpans().length, 2); done(); }); @@ -398,6 +427,7 @@ describe('DocumentLoad Instrumentation', () => { spyEntries = sandbox.stub(window.performance, 'getEntriesByType'); spyEntries.withArgs('navigation').returns([entries]); spyEntries.withArgs('resource').returns(resources); + spyEntries.withArgs('paint').returns([]); }); afterEach(() => { spyEntries.restore(); @@ -435,6 +465,7 @@ describe('DocumentLoad Instrumentation', () => { spyEntries = sandbox.stub(window.performance, 'getEntriesByType'); spyEntries.withArgs('navigation').returns([entries]); spyEntries.withArgs('resource').returns(resourcesNoSecureConnectionStart); + spyEntries.withArgs('paint').returns([]); }); afterEach(() => { spyEntries.restore(); @@ -476,6 +507,7 @@ describe('DocumentLoad Instrumentation', () => { spyEntries = sandbox.stub(window.performance, 'getEntriesByType'); spyEntries.withArgs('navigation').returns([entriesWithoutLoadEventEnd]); spyEntries.withArgs('resource').returns([]); + spyEntries.withArgs('paint').returns([]); }); afterEach(() => { spyEntries.restore(); @@ -600,6 +632,8 @@ describe('DocumentLoad Instrumentation', () => { .withArgs('navigation') .returns([navEntriesWithNegativeFetch]) .withArgs('resource') + .returns([]) + .withArgs('paint') .returns([]); sandbox.stub(window.performance, 'timing').get(() => { From 4cc06db8131f9c74f544e660400af5629870d7de Mon Sep 17 00:00:00 2001 From: Krystian Kruk Date: Mon, 24 May 2021 18:48:05 +0200 Subject: [PATCH 2/2] chore: fixed lint --- .../src/instrumentation.ts | 6 +++--- .../src/utils.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts b/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts index 04058a76eb..1dd70bf04d 100644 --- a/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts +++ b/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts @@ -76,9 +76,9 @@ export class DocumentLoadInstrumentation extends InstrumentationBase { * @param rootSpan */ private _addResourcesSpans(rootSpan: Span): void { - const resources: PerformanceResourceTiming[] = ((otperformance as unknown) as Performance).getEntriesByType?.( - 'resource' - ) as PerformanceResourceTiming[]; + const resources: PerformanceResourceTiming[] = ( + otperformance as unknown as Performance + ).getEntriesByType?.('resource') as PerformanceResourceTiming[]; if (resources) { resources.forEach(resource => { this._initResourceSpan(resource, rootSpan); diff --git a/plugins/web/opentelemetry-instrumentation-document-load/src/utils.ts b/plugins/web/opentelemetry-instrumentation-document-load/src/utils.ts index e31542ccc5..bdb0b42908 100644 --- a/plugins/web/opentelemetry-instrumentation-document-load/src/utils.ts +++ b/plugins/web/opentelemetry-instrumentation-document-load/src/utils.ts @@ -26,9 +26,9 @@ import { EventNames } from './enums/EventNames'; export const getPerformanceNavigationEntries = (): PerformanceEntries => { const entries: PerformanceEntries = {}; - const performanceNavigationTiming = ((otperformance as unknown) as Performance).getEntriesByType?.( - 'navigation' - )[0] as PerformanceEntries; + const performanceNavigationTiming = ( + otperformance as unknown as Performance + ).getEntriesByType?.('navigation')[0] as PerformanceEntries; if (performanceNavigationTiming) { const keys = Object.values(PTN); @@ -66,9 +66,9 @@ const performancePaintNames = { }; export const addSpanPerformancePaintEvents = (span: Span) => { - const performancePaintTiming = ((otperformance as unknown) as Performance).getEntriesByType?.( - 'paint' - ); + const performancePaintTiming = ( + otperformance as unknown as Performance + ).getEntriesByType?.('paint'); if (performancePaintTiming) { performancePaintTiming.forEach(({ name, startTime }) => { if (hasKey(performancePaintNames, name)) {