Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(instrumentation-document-load): performance paint timing events #484

Merged
merged 7 commits into from
Jun 8, 2021
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
addSpanNetworkEvents,
hasKey,
PerformanceEntries,
PerformanceLegacy,
PerformanceTimingNames as PTN,
} from '@opentelemetry/web';
import {
Expand All @@ -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
Expand Down Expand Up @@ -73,9 +76,9 @@ export class DocumentLoadInstrumentation extends InstrumentationBase<unknown> {
* @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);
Expand All @@ -90,7 +93,7 @@ export class DocumentLoadInstrumentation extends InstrumentationBase<unknown> {
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(
Expand Down Expand Up @@ -137,6 +140,8 @@ export class DocumentLoadInstrumentation extends InstrumentationBase<unknown> {
addSpanNetworkEvent(rootSpan, PTN.LOAD_EVENT_START, entries);
addSpanNetworkEvent(rootSpan, PTN.LOAD_EVENT_END, entries);

addSpanPerformancePaintEvents(rootSpan);

this._endSpan(rootSpan, PTN.LOAD_EVENT_END, entries);
});
}
Expand All @@ -163,44 +168,6 @@ export class DocumentLoadInstrumentation extends InstrumentationBase<unknown> {
}
}

/**
* 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
}
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a lot of dashes in names/semantic conventions in otel... . and _ yes, but not -. Perhaps the camel case names are fine as they are? Most other browser performance event names are passed through unchanged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So API defines them as first-paint and I proposed to use firstPaint.
The EventNames enum is for our OT enum names and here I define how to map them.
Other events are camel cased and I wanted to follow this convention.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I was reading it backwards! Apologies!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnbley does it mean you are ok with this changes then ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it looks good. Sorry I didn't officially, approve, will do so.

'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);
}
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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();
Expand All @@ -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();
});
});
Expand All @@ -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();
Expand All @@ -290,7 +312,7 @@ describe('DocumentLoad Instrumentation', () => {
})
);
setTimeout(() => {
assert.strictEqual(spyEntries.callCount, 2);
assert.strictEqual(spyEntries.callCount, 3);
done();
});
});
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -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();
});
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -600,6 +632,8 @@ describe('DocumentLoad Instrumentation', () => {
.withArgs('navigation')
.returns([navEntriesWithNegativeFetch])
.withArgs('resource')
.returns([])
.withArgs('paint')
.returns([]);

sandbox.stub(window.performance, 'timing').get(() => {
Expand Down