Skip to content

Commit 4a0dab7

Browse files
authored
feat(instrumentation-document-load): performance paint timing events (#484)
* feat(instrumentation-document-load): performance paint timing events * chore: fixed lint
1 parent 2a30f62 commit 4a0dab7

4 files changed

Lines changed: 143 additions & 43 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export enum EventNames {
18+
FIRST_PAINT = 'firstPaint',
19+
FIRST_CONTENTFUL_PAINT = 'firstContentfulPaint',
20+
}

plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import {
2727
addSpanNetworkEvents,
2828
hasKey,
2929
PerformanceEntries,
30-
PerformanceLegacy,
3130
PerformanceTimingNames as PTN,
3231
} from '@opentelemetry/web';
3332
import {
@@ -37,6 +36,10 @@ import {
3736
import { AttributeNames } from './enums/AttributeNames';
3837
import { VERSION } from './version';
3938
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
39+
import {
40+
addSpanPerformancePaintEvents,
41+
getPerformanceNavigationEntries,
42+
} from './utils';
4043

4144
/**
4245
* This class represents a document load plugin
@@ -90,7 +93,7 @@ export class DocumentLoadInstrumentation extends InstrumentationBase<unknown> {
9093
const metaElement = [...document.getElementsByTagName('meta')].find(
9194
e => e.getAttribute('name') === TRACE_PARENT_HEADER
9295
);
93-
const entries = this._getEntries();
96+
const entries = getPerformanceNavigationEntries();
9497
const traceparent = (metaElement && metaElement.content) || '';
9598
context.with(propagation.extract(ROOT_CONTEXT, { traceparent }), () => {
9699
const rootSpan = this._startSpan(
@@ -137,6 +140,8 @@ export class DocumentLoadInstrumentation extends InstrumentationBase<unknown> {
137140
addSpanNetworkEvent(rootSpan, PTN.LOAD_EVENT_START, entries);
138141
addSpanNetworkEvent(rootSpan, PTN.LOAD_EVENT_END, entries);
139142

143+
addSpanPerformancePaintEvents(rootSpan);
144+
140145
this._endSpan(rootSpan, PTN.LOAD_EVENT_END, entries);
141146
});
142147
}
@@ -163,44 +168,6 @@ export class DocumentLoadInstrumentation extends InstrumentationBase<unknown> {
163168
}
164169
}
165170

166-
/**
167-
* gets performance entries of navigation
168-
*/
169-
private _getEntries() {
170-
const entries: PerformanceEntries = {};
171-
const performanceNavigationTiming = (
172-
otperformance as unknown as Performance
173-
).getEntriesByType?.('navigation')[0] as PerformanceEntries;
174-
175-
if (performanceNavigationTiming) {
176-
const keys = Object.values(PTN);
177-
keys.forEach((key: string) => {
178-
if (hasKey(performanceNavigationTiming, key)) {
179-
const value = performanceNavigationTiming[key];
180-
if (typeof value === 'number') {
181-
entries[key] = value;
182-
}
183-
}
184-
});
185-
} else {
186-
// // fallback to previous version
187-
const perf: typeof otperformance & PerformanceLegacy = otperformance;
188-
const performanceTiming = perf.timing;
189-
if (performanceTiming) {
190-
const keys = Object.values(PTN);
191-
keys.forEach((key: string) => {
192-
if (hasKey(performanceTiming, key)) {
193-
const value = performanceTiming[key];
194-
if (typeof value === 'number') {
195-
entries[key] = value;
196-
}
197-
}
198-
});
199-
}
200-
}
201-
return entries;
202-
}
203-
204171
/**
205172
* Creates and ends a span with network information about resource added as timed events
206173
* @param resource
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Span } from '@opentelemetry/api';
18+
import { otperformance } from '@opentelemetry/core';
19+
import {
20+
hasKey,
21+
PerformanceEntries,
22+
PerformanceLegacy,
23+
PerformanceTimingNames as PTN,
24+
} from '@opentelemetry/web';
25+
import { EventNames } from './enums/EventNames';
26+
27+
export const getPerformanceNavigationEntries = (): PerformanceEntries => {
28+
const entries: PerformanceEntries = {};
29+
const performanceNavigationTiming = (
30+
otperformance as unknown as Performance
31+
).getEntriesByType?.('navigation')[0] as PerformanceEntries;
32+
33+
if (performanceNavigationTiming) {
34+
const keys = Object.values(PTN);
35+
keys.forEach((key: string) => {
36+
if (hasKey(performanceNavigationTiming, key)) {
37+
const value = performanceNavigationTiming[key];
38+
if (typeof value === 'number') {
39+
entries[key] = value;
40+
}
41+
}
42+
});
43+
} else {
44+
// // fallback to previous version
45+
const perf: typeof otperformance & PerformanceLegacy = otperformance;
46+
const performanceTiming = perf.timing;
47+
if (performanceTiming) {
48+
const keys = Object.values(PTN);
49+
keys.forEach((key: string) => {
50+
if (hasKey(performanceTiming, key)) {
51+
const value = performanceTiming[key];
52+
if (typeof value === 'number') {
53+
entries[key] = value;
54+
}
55+
}
56+
});
57+
}
58+
}
59+
60+
return entries;
61+
};
62+
63+
const performancePaintNames = {
64+
'first-paint': EventNames.FIRST_PAINT,
65+
'first-contentful-paint': EventNames.FIRST_CONTENTFUL_PAINT,
66+
};
67+
68+
export const addSpanPerformancePaintEvents = (span: Span) => {
69+
const performancePaintTiming = (
70+
otperformance as unknown as Performance
71+
).getEntriesByType?.('paint');
72+
if (performancePaintTiming) {
73+
performancePaintTiming.forEach(({ name, startTime }) => {
74+
if (hasKey(performancePaintNames, name)) {
75+
span.addEvent(performancePaintNames[name], startTime);
76+
}
77+
});
78+
}
79+
};

plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import * as assert from 'assert';
3939
import * as sinon from 'sinon';
4040
import { DocumentLoadInstrumentation } from '../src';
4141
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
42+
import { EventNames } from '../src/enums/EventNames';
4243

4344
const exporter = new InMemorySpanExporter();
4445
const provider = new BasicTracerProvider();
@@ -182,6 +183,25 @@ const entriesFallback = {
182183
loadEventEnd: 1571078170394,
183184
} as any;
184185

186+
const paintEntries: PerformanceEntryList = [
187+
{
188+
duration: 0,
189+
entryType: 'paint',
190+
name: 'first-paint',
191+
startTime: 7.480000003241003,
192+
toJSON() {},
193+
},
194+
{
195+
duration: 0,
196+
entryType: 'paint',
197+
name: 'first-contentful-paint',
198+
startTime: 8.480000003241003,
199+
toJSON() {},
200+
},
201+
];
202+
203+
performance.getEntriesByType;
204+
185205
const userAgent =
186206
'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';
187207

@@ -246,6 +266,7 @@ describe('DocumentLoad Instrumentation', () => {
246266
spyEntries = sandbox.stub(window.performance, 'getEntriesByType');
247267
spyEntries.withArgs('navigation').returns([entries]);
248268
spyEntries.withArgs('resource').returns([]);
269+
spyEntries.withArgs('paint').returns([]);
249270
});
250271
afterEach(() => {
251272
spyEntries.restore();
@@ -254,7 +275,7 @@ describe('DocumentLoad Instrumentation', () => {
254275
plugin.enable();
255276
setTimeout(() => {
256277
assert.strictEqual(window.document.readyState, 'complete');
257-
assert.strictEqual(spyEntries.callCount, 2);
278+
assert.strictEqual(spyEntries.callCount, 3);
258279
done();
259280
});
260281
});
@@ -270,6 +291,7 @@ describe('DocumentLoad Instrumentation', () => {
270291
spyEntries = sandbox.stub(window.performance, 'getEntriesByType');
271292
spyEntries.withArgs('navigation').returns([entries]);
272293
spyEntries.withArgs('resource').returns([]);
294+
spyEntries.withArgs('paint').returns([]);
273295
});
274296
afterEach(() => {
275297
spyEntries.restore();
@@ -293,7 +315,7 @@ describe('DocumentLoad Instrumentation', () => {
293315
})
294316
);
295317
setTimeout(() => {
296-
assert.strictEqual(spyEntries.callCount, 2);
318+
assert.strictEqual(spyEntries.callCount, 3);
297319
done();
298320
});
299321
});
@@ -305,6 +327,7 @@ describe('DocumentLoad Instrumentation', () => {
305327
spyEntries = sandbox.stub(window.performance, 'getEntriesByType');
306328
spyEntries.withArgs('navigation').returns([entries]);
307329
spyEntries.withArgs('resource').returns([]);
330+
spyEntries.withArgs('paint').returns(paintEntries);
308331
});
309332
afterEach(() => {
310333
spyEntries.restore();
@@ -328,6 +351,12 @@ describe('DocumentLoad Instrumentation', () => {
328351
assert.strictEqual(fetchSpan.name, 'documentLoad');
329352
ensureNetworkEventsExists(rsEvents);
330353

354+
assert.strictEqual(fsEvents[9].name, EventNames.FIRST_PAINT);
355+
assert.strictEqual(
356+
fsEvents[10].name,
357+
EventNames.FIRST_CONTENTFUL_PAINT
358+
);
359+
331360
assert.strictEqual(fsEvents[0].name, PTN.FETCH_START);
332361
assert.strictEqual(fsEvents[1].name, PTN.UNLOAD_EVENT_START);
333362
assert.strictEqual(fsEvents[2].name, PTN.UNLOAD_EVENT_END);
@@ -342,7 +371,7 @@ describe('DocumentLoad Instrumentation', () => {
342371
assert.strictEqual(fsEvents[8].name, PTN.LOAD_EVENT_END);
343372

344373
assert.strictEqual(rsEvents.length, 9);
345-
assert.strictEqual(fsEvents.length, 9);
374+
assert.strictEqual(fsEvents.length, 11);
346375
assert.strictEqual(exporter.getFinishedSpans().length, 2);
347376
done();
348377
});
@@ -401,6 +430,7 @@ describe('DocumentLoad Instrumentation', () => {
401430
spyEntries = sandbox.stub(window.performance, 'getEntriesByType');
402431
spyEntries.withArgs('navigation').returns([entries]);
403432
spyEntries.withArgs('resource').returns(resources);
433+
spyEntries.withArgs('paint').returns([]);
404434
});
405435
afterEach(() => {
406436
spyEntries.restore();
@@ -438,6 +468,7 @@ describe('DocumentLoad Instrumentation', () => {
438468
spyEntries = sandbox.stub(window.performance, 'getEntriesByType');
439469
spyEntries.withArgs('navigation').returns([entries]);
440470
spyEntries.withArgs('resource').returns(resourcesNoSecureConnectionStart);
471+
spyEntries.withArgs('paint').returns([]);
441472
});
442473
afterEach(() => {
443474
spyEntries.restore();
@@ -479,6 +510,7 @@ describe('DocumentLoad Instrumentation', () => {
479510
spyEntries = sandbox.stub(window.performance, 'getEntriesByType');
480511
spyEntries.withArgs('navigation').returns([entriesWithoutLoadEventEnd]);
481512
spyEntries.withArgs('resource').returns([]);
513+
spyEntries.withArgs('paint').returns([]);
482514
});
483515
afterEach(() => {
484516
spyEntries.restore();
@@ -603,6 +635,8 @@ describe('DocumentLoad Instrumentation', () => {
603635
.withArgs('navigation')
604636
.returns([navEntriesWithNegativeFetch])
605637
.withArgs('resource')
638+
.returns([])
639+
.withArgs('paint')
606640
.returns([]);
607641

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

0 commit comments

Comments
 (0)