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/instrumentation.ts b/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts index 49eda67d54..27712e63cc 100644 --- a/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts +++ b/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.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 @@ -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/utils.ts b/plugins/web/opentelemetry-instrumentation-document-load/src/utils.ts new file mode 100644 index 0000000000..bdb0b42908 --- /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 e27a7d7a18..a04e679004 100644 --- a/plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts +++ b/plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts @@ -39,6 +39,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(); @@ -182,6 +183,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'; @@ -246,6 +266,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(); @@ -254,7 +275,7 @@ describe('DocumentLoad Instrumentation', () => { plugin.enable(); setTimeout(() => { assert.strictEqual(window.document.readyState, 'complete'); - assert.strictEqual(spyEntries.callCount, 2); + assert.strictEqual(spyEntries.callCount, 3); done(); }); }); @@ -270,6 +291,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(); @@ -293,7 +315,7 @@ describe('DocumentLoad Instrumentation', () => { }) ); setTimeout(() => { - assert.strictEqual(spyEntries.callCount, 2); + assert.strictEqual(spyEntries.callCount, 3); done(); }); }); @@ -305,6 +327,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(); @@ -328,6 +351,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); @@ -342,7 +371,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(); }); @@ -401,6 +430,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(); @@ -438,6 +468,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(); @@ -479,6 +510,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(); @@ -603,6 +635,8 @@ describe('DocumentLoad Instrumentation', () => { .withArgs('navigation') .returns([navEntriesWithNegativeFetch]) .withArgs('resource') + .returns([]) + .withArgs('paint') .returns([]); sandbox.stub(window.performance, 'timing').get(() => {