Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions injected/src/features/breakage-reporting.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import ContentFeature from '../content-feature';
import { getJsPerformanceMetrics } from './breakage-reporting/utils.js';
import { getExpandedPerformanceMetrics, getJsPerformanceMetrics } from './breakage-reporting/utils.js';

export default class BreakageReporting extends ContentFeature {
init() {
this.messaging.subscribe('getBreakageReportValues', () => {
const isExpandedPerformanceMetricsEnabled = this.getFeatureSettingEnabled('expandedPerformanceMetrics', 'enabled');
this.messaging.subscribe('getBreakageReportValues', async () => {
const jsPerformance = getJsPerformanceMetrics();
const referrer = document.referrer;

this.messaging.notify('breakageReportResult', {
const result = {
jsPerformance,
referrer,
});
};
if (isExpandedPerformanceMetricsEnabled) {
const expandedPerformanceMetrics = await getExpandedPerformanceMetrics();
if (expandedPerformanceMetrics.success) {
result.expandedPerformanceMetrics = expandedPerformanceMetrics.metrics;
}
}
this.messaging.notify('breakageReportResult', result);
});
}
}
121 changes: 121 additions & 0 deletions injected/src/features/breakage-reporting/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,124 @@ export function getJsPerformanceMetrics() {
const firstPaint = paintResources.find((entry) => entry.name === 'first-contentful-paint');
return firstPaint ? [firstPaint.startTime] : [];
}

/** @typedef {{error: string, success: false}} ErrorObject */
/** @typedef {{success: true, metrics: any}} PerformanceMetricsResponse */

/**
* Convenience function to return an error object
* @param {string} errorMessage
* @returns {ErrorObject}
*/
function returnError(errorMessage) {
return { error: errorMessage, success: false };
}

/**
* @returns {Promise<number | null>}
*/
function waitForLCP(timeoutMs = 500) {
return new Promise((resolve) => {
// eslint-disable-next-line prefer-const
let timeoutId;
// eslint-disable-next-line prefer-const
let observer;

const cleanup = () => {
if (observer) observer.disconnect();
if (timeoutId) clearTimeout(timeoutId);
};

// Set timeout
timeoutId = setTimeout(() => {
cleanup();
resolve(null); // Resolve with null instead of hanging
}, timeoutMs);

// Try to get existing LCP
observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry) {
cleanup();
resolve(lastEntry.startTime);
}
});

try {
observer.observe({ type: 'largest-contentful-paint', buffered: true });
} catch (error) {
// Handle browser compatibility issues
cleanup();
resolve(null);
}
});
}

/**
* Get the expanded performance metrics
* @returns {Promise<ErrorObject | PerformanceMetricsResponse>}
*/
export async function getExpandedPerformanceMetrics() {
try {
if (document.readyState !== 'complete') {
return returnError('Document not ready');
}

const navigation = /** @type {PerformanceNavigationTiming} */ (performance.getEntriesByType('navigation')[0]);
const paint = performance.getEntriesByType('paint');
const resources = /** @type {PerformanceResourceTiming[]} */ (performance.getEntriesByType('resource'));

// Find FCP
const fcp = paint.find((p) => p.name === 'first-contentful-paint');

// Get largest contentful paint if available
let largestContentfulPaint = null;
if (PerformanceObserver.supportedEntryTypes.includes('largest-contentful-paint')) {
largestContentfulPaint = await waitForLCP();
}

// Calculate total resource sizes
const totalResourceSize = resources.reduce((sum, r) => sum + (r.transferSize || 0), 0);

if (navigation) {
return {
success: true,
metrics: {
// Core timing metrics (in milliseconds)
loadComplete: navigation.loadEventEnd - navigation.fetchStart,
domComplete: navigation.domComplete - navigation.fetchStart,
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.fetchStart,
domInteractive: navigation.domInteractive - navigation.fetchStart,

// Paint metrics
firstContentfulPaint: fcp ? fcp.startTime : null,
largestContentfulPaint,

// Network metrics
timeToFirstByte: navigation.responseStart - navigation.fetchStart,
responseTime: navigation.responseEnd - navigation.responseStart,
serverTime: navigation.responseStart - navigation.requestStart,

// Size metrics (in octets)
transferSize: navigation.transferSize,
encodedBodySize: navigation.encodedBodySize,
decodedBodySize: navigation.decodedBodySize,

// Resource metrics
resourceCount: resources.length,
totalResourcesSize: totalResourceSize,

// Additional metadata
protocol: navigation.nextHopProtocol,
redirectCount: navigation.redirectCount,
navigationType: navigation.type,
},
};
}

return returnError('No navigation timing found');
} catch (e) {
return returnError('JavaScript execution error: ' + e.message);
}
}
26 changes: 25 additions & 1 deletion injected/src/features/performance-metrics.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
import ContentFeature from '../content-feature';
import { getJsPerformanceMetrics } from './breakage-reporting/utils.js';
import { getExpandedPerformanceMetrics, getJsPerformanceMetrics } from './breakage-reporting/utils.js';
import { isBeingFramed } from '../utils.js';

export default class PerformanceMetrics extends ContentFeature {
init() {
this.messaging.subscribe('getVitals', () => {
const vitals = getJsPerformanceMetrics();
this.messaging.notify('vitalsResult', { vitals });
});

// If the document is being framed, we don't want to collect expanded performance metrics
if (isBeingFramed()) return;

// If the feature is enabled, we want to collect expanded performance metrics
if (this.getFeatureSettingEnabled('expandedPerformanceMetricsOnLoad', 'enabled')) {
this.waitForPageLoad(() => {
this.triggerExpandedPerformanceMetrics();
});
}
}

waitForPageLoad(callback) {
if (document.readyState === 'complete') {
callback();
} else {
window.addEventListener('load', callback, { once: true });
}
}

async triggerExpandedPerformanceMetrics() {
const expandedPerformanceMetrics = await getExpandedPerformanceMetrics();
this.messaging.notify('expandedPerformanceMetricsResult', expandedPerformanceMetrics);
}
}
Loading