Skip to content

Commit c5cfa9f

Browse files
Get expanded performance results (#1989)
* Get expanded performance results * Shorthand lint fix * lint fix * Remove listener * Simplify breakage report. Don't trigger expanded on frame * Resolve LCP if exists * Disable lint * Call on load
1 parent 6f948b2 commit c5cfa9f

File tree

3 files changed

+158
-6
lines changed

3 files changed

+158
-6
lines changed
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import ContentFeature from '../content-feature';
2-
import { getJsPerformanceMetrics } from './breakage-reporting/utils.js';
2+
import { getExpandedPerformanceMetrics, getJsPerformanceMetrics } from './breakage-reporting/utils.js';
33

44
export default class BreakageReporting extends ContentFeature {
55
init() {
6-
this.messaging.subscribe('getBreakageReportValues', () => {
6+
const isExpandedPerformanceMetricsEnabled = this.getFeatureSettingEnabled('expandedPerformanceMetrics', 'enabled');
7+
this.messaging.subscribe('getBreakageReportValues', async () => {
78
const jsPerformance = getJsPerformanceMetrics();
89
const referrer = document.referrer;
9-
10-
this.messaging.notify('breakageReportResult', {
10+
const result = {
1111
jsPerformance,
1212
referrer,
13-
});
13+
};
14+
if (isExpandedPerformanceMetricsEnabled) {
15+
const expandedPerformanceMetrics = await getExpandedPerformanceMetrics();
16+
if (expandedPerformanceMetrics.success) {
17+
result.expandedPerformanceMetrics = expandedPerformanceMetrics.metrics;
18+
}
19+
}
20+
this.messaging.notify('breakageReportResult', result);
1421
});
1522
}
1623
}

injected/src/features/breakage-reporting/utils.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,124 @@ export function getJsPerformanceMetrics() {
66
const firstPaint = paintResources.find((entry) => entry.name === 'first-contentful-paint');
77
return firstPaint ? [firstPaint.startTime] : [];
88
}
9+
10+
/** @typedef {{error: string, success: false}} ErrorObject */
11+
/** @typedef {{success: true, metrics: any}} PerformanceMetricsResponse */
12+
13+
/**
14+
* Convenience function to return an error object
15+
* @param {string} errorMessage
16+
* @returns {ErrorObject}
17+
*/
18+
function returnError(errorMessage) {
19+
return { error: errorMessage, success: false };
20+
}
21+
22+
/**
23+
* @returns {Promise<number | null>}
24+
*/
25+
function waitForLCP(timeoutMs = 500) {
26+
return new Promise((resolve) => {
27+
// eslint-disable-next-line prefer-const
28+
let timeoutId;
29+
// eslint-disable-next-line prefer-const
30+
let observer;
31+
32+
const cleanup = () => {
33+
if (observer) observer.disconnect();
34+
if (timeoutId) clearTimeout(timeoutId);
35+
};
36+
37+
// Set timeout
38+
timeoutId = setTimeout(() => {
39+
cleanup();
40+
resolve(null); // Resolve with null instead of hanging
41+
}, timeoutMs);
42+
43+
// Try to get existing LCP
44+
observer = new PerformanceObserver((list) => {
45+
const entries = list.getEntries();
46+
const lastEntry = entries[entries.length - 1];
47+
if (lastEntry) {
48+
cleanup();
49+
resolve(lastEntry.startTime);
50+
}
51+
});
52+
53+
try {
54+
observer.observe({ type: 'largest-contentful-paint', buffered: true });
55+
} catch (error) {
56+
// Handle browser compatibility issues
57+
cleanup();
58+
resolve(null);
59+
}
60+
});
61+
}
62+
63+
/**
64+
* Get the expanded performance metrics
65+
* @returns {Promise<ErrorObject | PerformanceMetricsResponse>}
66+
*/
67+
export async function getExpandedPerformanceMetrics() {
68+
try {
69+
if (document.readyState !== 'complete') {
70+
return returnError('Document not ready');
71+
}
72+
73+
const navigation = /** @type {PerformanceNavigationTiming} */ (performance.getEntriesByType('navigation')[0]);
74+
const paint = performance.getEntriesByType('paint');
75+
const resources = /** @type {PerformanceResourceTiming[]} */ (performance.getEntriesByType('resource'));
76+
77+
// Find FCP
78+
const fcp = paint.find((p) => p.name === 'first-contentful-paint');
79+
80+
// Get largest contentful paint if available
81+
let largestContentfulPaint = null;
82+
if (PerformanceObserver.supportedEntryTypes.includes('largest-contentful-paint')) {
83+
largestContentfulPaint = await waitForLCP();
84+
}
85+
86+
// Calculate total resource sizes
87+
const totalResourceSize = resources.reduce((sum, r) => sum + (r.transferSize || 0), 0);
88+
89+
if (navigation) {
90+
return {
91+
success: true,
92+
metrics: {
93+
// Core timing metrics (in milliseconds)
94+
loadComplete: navigation.loadEventEnd - navigation.fetchStart,
95+
domComplete: navigation.domComplete - navigation.fetchStart,
96+
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.fetchStart,
97+
domInteractive: navigation.domInteractive - navigation.fetchStart,
98+
99+
// Paint metrics
100+
firstContentfulPaint: fcp ? fcp.startTime : null,
101+
largestContentfulPaint,
102+
103+
// Network metrics
104+
timeToFirstByte: navigation.responseStart - navigation.fetchStart,
105+
responseTime: navigation.responseEnd - navigation.responseStart,
106+
serverTime: navigation.responseStart - navigation.requestStart,
107+
108+
// Size metrics (in octets)
109+
transferSize: navigation.transferSize,
110+
encodedBodySize: navigation.encodedBodySize,
111+
decodedBodySize: navigation.decodedBodySize,
112+
113+
// Resource metrics
114+
resourceCount: resources.length,
115+
totalResourcesSize: totalResourceSize,
116+
117+
// Additional metadata
118+
protocol: navigation.nextHopProtocol,
119+
redirectCount: navigation.redirectCount,
120+
navigationType: navigation.type,
121+
},
122+
};
123+
}
124+
125+
return returnError('No navigation timing found');
126+
} catch (e) {
127+
return returnError('JavaScript execution error: ' + e.message);
128+
}
129+
}
Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,35 @@
11
import ContentFeature from '../content-feature';
2-
import { getJsPerformanceMetrics } from './breakage-reporting/utils.js';
2+
import { getExpandedPerformanceMetrics, getJsPerformanceMetrics } from './breakage-reporting/utils.js';
3+
import { isBeingFramed } from '../utils.js';
34

45
export default class PerformanceMetrics extends ContentFeature {
56
init() {
67
this.messaging.subscribe('getVitals', () => {
78
const vitals = getJsPerformanceMetrics();
89
this.messaging.notify('vitalsResult', { vitals });
910
});
11+
12+
// If the document is being framed, we don't want to collect expanded performance metrics
13+
if (isBeingFramed()) return;
14+
15+
// If the feature is enabled, we want to collect expanded performance metrics
16+
if (this.getFeatureSettingEnabled('expandedPerformanceMetricsOnLoad', 'enabled')) {
17+
this.waitForPageLoad(() => {
18+
this.triggerExpandedPerformanceMetrics();
19+
});
20+
}
21+
}
22+
23+
waitForPageLoad(callback) {
24+
if (document.readyState === 'complete') {
25+
callback();
26+
} else {
27+
window.addEventListener('load', callback, { once: true });
28+
}
29+
}
30+
31+
async triggerExpandedPerformanceMetrics() {
32+
const expandedPerformanceMetrics = await getExpandedPerformanceMetrics();
33+
this.messaging.notify('expandedPerformanceMetricsResult', expandedPerformanceMetrics);
1034
}
1135
}

0 commit comments

Comments
 (0)