Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
379 lines
No EOL
16 KiB
Text
379 lines
No EOL
16 KiB
Text
// Copyright 2022 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
/**
|
|
* This handler stores page load metrics, including web vitals,
|
|
* and exports them in the shape of a map with the following shape:
|
|
* Map(FrameId -> Map(navigationID -> metrics) )
|
|
*
|
|
* It also exports all markers in a trace in an array.
|
|
*
|
|
* Some metrics are taken directly from a page load events (AKA markers) like DCL.
|
|
* Others require processing multiple events to be determined, like CLS and TBT.
|
|
*/
|
|
import * as Platform from '../../../core/platform/platform.js';
|
|
import * as Helpers from '../helpers/helpers.js';
|
|
import * as Types from '../types/types.js';
|
|
import { data as metaHandlerData } from './MetaHandler.js';
|
|
/**
|
|
* This represents the metric scores for all navigations, for all frames in a trace.
|
|
* Given a frame id, the map points to another map from navigation id to metric scores.
|
|
* The metric scores include the event related to the metric as well as the data regarding
|
|
* the score itself.
|
|
*/
|
|
const metricScoresByFrameId = new Map();
|
|
/**
|
|
* Page load events with no associated duration that happened in the
|
|
* main frame.
|
|
*/
|
|
let allMarkerEvents = [];
|
|
export function reset() {
|
|
metricScoresByFrameId.clear();
|
|
pageLoadEventsArray = [];
|
|
allMarkerEvents = [];
|
|
selectedLCPCandidateEvents.clear();
|
|
}
|
|
let pageLoadEventsArray = [];
|
|
// Once we've found the LCP events in the trace we want to fetch their DOM Node
|
|
// from the backend. We could do this by parsing through our Map of frame =>
|
|
// navigation => metric, but it's easier to keep a set of LCP events. As we
|
|
// parse the trace, any time we store an LCP candidate as the potential LCP
|
|
// event, we store the event here. If we later find a new candidate in the
|
|
// trace, we store that and delete the prior event. When we've parsed the
|
|
// entire trace this set will contain all the LCP events that were used - e.g.
|
|
// the candidates that were the actual LCP events.
|
|
const selectedLCPCandidateEvents = new Set();
|
|
export function handleEvent(event) {
|
|
if (!Types.Events.eventIsPageLoadEvent(event)) {
|
|
return;
|
|
}
|
|
pageLoadEventsArray.push(event);
|
|
}
|
|
function storePageLoadMetricAgainstNavigationId(navigation, event) {
|
|
const navigationId = navigation.args.data?.navigationId;
|
|
if (!navigationId) {
|
|
throw new Error('Navigation event unexpectedly had no navigation ID.');
|
|
}
|
|
const frameId = getFrameIdForPageLoadEvent(event);
|
|
const { rendererProcessesByFrame } = metaHandlerData();
|
|
// If either of these pieces of data do not exist, the most likely
|
|
// explanation is that the page load metric we found is for a frame/process
|
|
// combo that the MetaHandler discarded. This typically happens if we get a
|
|
// navigation event with an empty URL. Therefore, we will silently return and
|
|
// drop this metric. If we didn't care about the navigation, we certainly do
|
|
// not need to care about metrics for that navigation.
|
|
const rendererProcessesInFrame = rendererProcessesByFrame.get(frameId);
|
|
if (!rendererProcessesInFrame) {
|
|
return;
|
|
}
|
|
const processData = rendererProcessesInFrame.get(event.pid);
|
|
if (!processData) {
|
|
return;
|
|
}
|
|
if (Types.Events.isNavigationStart(event)) {
|
|
return;
|
|
}
|
|
if (Types.Events.isFirstContentfulPaint(event)) {
|
|
const fcpTime = Types.Timing.Micro(event.ts - navigation.ts);
|
|
const classification = scoreClassificationForFirstContentfulPaint(fcpTime);
|
|
const metricScore = { event, metricName: MetricName.FCP, classification, navigation, timing: fcpTime };
|
|
storeMetricScore(frameId, navigationId, metricScore);
|
|
return;
|
|
}
|
|
if (Types.Events.isFirstPaint(event)) {
|
|
const paintTime = Types.Timing.Micro(event.ts - navigation.ts);
|
|
const classification = ScoreClassification.UNCLASSIFIED;
|
|
const metricScore = { event, metricName: MetricName.FP, classification, navigation, timing: paintTime };
|
|
storeMetricScore(frameId, navigationId, metricScore);
|
|
return;
|
|
}
|
|
if (Types.Events.isMarkDOMContent(event)) {
|
|
const dclTime = Types.Timing.Micro(event.ts - navigation.ts);
|
|
const metricScore = {
|
|
event,
|
|
metricName: MetricName.DCL,
|
|
classification: scoreClassificationForDOMContentLoaded(dclTime),
|
|
navigation,
|
|
timing: dclTime,
|
|
};
|
|
storeMetricScore(frameId, navigationId, metricScore);
|
|
return;
|
|
}
|
|
if (Types.Events.isInteractiveTime(event)) {
|
|
const ttiValue = Types.Timing.Micro(event.ts - navigation.ts);
|
|
const tti = {
|
|
event,
|
|
metricName: MetricName.TTI,
|
|
classification: scoreClassificationForTimeToInteractive(ttiValue),
|
|
navigation,
|
|
timing: ttiValue,
|
|
};
|
|
storeMetricScore(frameId, navigationId, tti);
|
|
const tbtValue = Helpers.Timing.milliToMicro(Types.Timing.Milli(event.args.args.total_blocking_time_ms));
|
|
const tbt = {
|
|
event,
|
|
metricName: MetricName.TBT,
|
|
classification: scoreClassificationForTotalBlockingTime(tbtValue),
|
|
navigation,
|
|
timing: tbtValue,
|
|
};
|
|
storeMetricScore(frameId, navigationId, tbt);
|
|
return;
|
|
}
|
|
if (Types.Events.isMarkLoad(event)) {
|
|
const loadTime = Types.Timing.Micro(event.ts - navigation.ts);
|
|
const metricScore = {
|
|
event,
|
|
metricName: MetricName.L,
|
|
classification: ScoreClassification.UNCLASSIFIED,
|
|
navigation,
|
|
timing: loadTime,
|
|
};
|
|
storeMetricScore(frameId, navigationId, metricScore);
|
|
return;
|
|
}
|
|
if (Types.Events.isLargestContentfulPaintCandidate(event)) {
|
|
const candidateIndex = event.args.data?.candidateIndex;
|
|
if (!candidateIndex) {
|
|
throw new Error('Largest Contentful Paint unexpectedly had no candidateIndex.');
|
|
}
|
|
const lcpTime = Types.Timing.Micro(event.ts - navigation.ts);
|
|
const lcp = {
|
|
event,
|
|
metricName: MetricName.LCP,
|
|
classification: scoreClassificationForLargestContentfulPaint(lcpTime),
|
|
navigation,
|
|
timing: lcpTime,
|
|
};
|
|
const metricsByNavigation = Platform.MapUtilities.getWithDefault(metricScoresByFrameId, frameId, () => new Map());
|
|
const metrics = Platform.MapUtilities.getWithDefault(metricsByNavigation, navigationId, () => new Map());
|
|
const lastLCPCandidate = metrics.get(MetricName.LCP);
|
|
if (lastLCPCandidate === undefined) {
|
|
selectedLCPCandidateEvents.add(lcp.event);
|
|
storeMetricScore(frameId, navigationId, lcp);
|
|
return;
|
|
}
|
|
const lastLCPCandidateEvent = lastLCPCandidate.event;
|
|
if (!Types.Events.isLargestContentfulPaintCandidate(lastLCPCandidateEvent)) {
|
|
return;
|
|
}
|
|
const lastCandidateIndex = lastLCPCandidateEvent.args.data?.candidateIndex;
|
|
if (!lastCandidateIndex) {
|
|
// lastCandidateIndex cannot be undefined because we don't store candidates with
|
|
// with an undefined candidateIndex value. This check is only to make TypeScript
|
|
// treat the field as not undefined below.
|
|
return;
|
|
}
|
|
if (lastCandidateIndex < candidateIndex) {
|
|
selectedLCPCandidateEvents.delete(lastLCPCandidateEvent);
|
|
selectedLCPCandidateEvents.add(lcp.event);
|
|
storeMetricScore(frameId, navigationId, lcp);
|
|
}
|
|
return;
|
|
}
|
|
if (Types.Events.isLayoutShift(event)) {
|
|
return;
|
|
}
|
|
return Platform.assertNever(event, `Unexpected event type: ${event}`);
|
|
}
|
|
function storeMetricScore(frameId, navigationId, metricScore) {
|
|
const metricsByNavigation = Platform.MapUtilities.getWithDefault(metricScoresByFrameId, frameId, () => new Map());
|
|
const metrics = Platform.MapUtilities.getWithDefault(metricsByNavigation, navigationId, () => new Map());
|
|
// If an entry with that metric name is present, delete it so that the new entry that
|
|
// will replace it is added at the end of the map. This way we guarantee the map entries
|
|
// are ordered in ASC manner by timestamp.
|
|
metrics.delete(metricScore.metricName);
|
|
metrics.set(metricScore.metricName, metricScore);
|
|
}
|
|
export function getFrameIdForPageLoadEvent(event) {
|
|
if (Types.Events.isFirstContentfulPaint(event) || Types.Events.isInteractiveTime(event) ||
|
|
Types.Events.isLargestContentfulPaintCandidate(event) || Types.Events.isNavigationStart(event) ||
|
|
Types.Events.isLayoutShift(event) || Types.Events.isFirstPaint(event)) {
|
|
return event.args.frame;
|
|
}
|
|
if (Types.Events.isMarkDOMContent(event) || Types.Events.isMarkLoad(event)) {
|
|
const frameId = event.args.data?.frame;
|
|
if (!frameId) {
|
|
throw new Error('MarkDOMContent unexpectedly had no frame ID.');
|
|
}
|
|
return frameId;
|
|
}
|
|
Platform.assertNever(event, `Unexpected event type: ${event}`);
|
|
}
|
|
function getNavigationForPageLoadEvent(event) {
|
|
if (Types.Events.isFirstContentfulPaint(event) || Types.Events.isLargestContentfulPaintCandidate(event) ||
|
|
Types.Events.isFirstPaint(event)) {
|
|
const navigationId = event.args.data?.navigationId;
|
|
if (!navigationId) {
|
|
throw new Error('Trace event unexpectedly had no navigation ID.');
|
|
}
|
|
const { navigationsByNavigationId } = metaHandlerData();
|
|
const navigation = navigationsByNavigationId.get(navigationId);
|
|
if (!navigation) {
|
|
// This event's navigation has been filtered out by the meta handler as a noise event.
|
|
return null;
|
|
}
|
|
return navigation;
|
|
}
|
|
if (Types.Events.isMarkDOMContent(event) || Types.Events.isInteractiveTime(event) ||
|
|
Types.Events.isLayoutShift(event) || Types.Events.isMarkLoad(event)) {
|
|
const frameId = getFrameIdForPageLoadEvent(event);
|
|
const { navigationsByFrameId } = metaHandlerData();
|
|
return Helpers.Trace.getNavigationForTraceEvent(event, frameId, navigationsByFrameId);
|
|
}
|
|
if (Types.Events.isNavigationStart(event)) {
|
|
// We don't want to compute metrics of the navigation relative to itself, so we'll avoid avoid all that.
|
|
return null;
|
|
}
|
|
return Platform.assertNever(event, `Unexpected event type: ${event}`);
|
|
}
|
|
/**
|
|
* Classifications sourced from
|
|
* https://web.dev/fcp/
|
|
*/
|
|
export function scoreClassificationForFirstContentfulPaint(fcpScoreInMicroseconds) {
|
|
const FCP_GOOD_TIMING = Helpers.Timing.secondsToMicro(Types.Timing.Seconds(1.8));
|
|
const FCP_MEDIUM_TIMING = Helpers.Timing.secondsToMicro(Types.Timing.Seconds(3.0));
|
|
let scoreClassification = ScoreClassification.BAD;
|
|
if (fcpScoreInMicroseconds <= FCP_MEDIUM_TIMING) {
|
|
scoreClassification = ScoreClassification.OK;
|
|
}
|
|
if (fcpScoreInMicroseconds <= FCP_GOOD_TIMING) {
|
|
scoreClassification = ScoreClassification.GOOD;
|
|
}
|
|
return scoreClassification;
|
|
}
|
|
/**
|
|
* Classifications sourced from
|
|
* https://web.dev/interactive/#how-lighthouse-determines-your-tti-score
|
|
*/
|
|
export function scoreClassificationForTimeToInteractive(ttiTimeInMicroseconds) {
|
|
const TTI_GOOD_TIMING = Helpers.Timing.secondsToMicro(Types.Timing.Seconds(3.8));
|
|
const TTI_MEDIUM_TIMING = Helpers.Timing.secondsToMicro(Types.Timing.Seconds(7.3));
|
|
let scoreClassification = ScoreClassification.BAD;
|
|
if (ttiTimeInMicroseconds <= TTI_MEDIUM_TIMING) {
|
|
scoreClassification = ScoreClassification.OK;
|
|
}
|
|
if (ttiTimeInMicroseconds <= TTI_GOOD_TIMING) {
|
|
scoreClassification = ScoreClassification.GOOD;
|
|
}
|
|
return scoreClassification;
|
|
}
|
|
/**
|
|
* Classifications sourced from
|
|
* https://web.dev/lcp/#what-is-lcp
|
|
*/
|
|
export function scoreClassificationForLargestContentfulPaint(lcpTimeInMicroseconds) {
|
|
const LCP_GOOD_TIMING = Helpers.Timing.secondsToMicro(Types.Timing.Seconds(2.5));
|
|
const LCP_MEDIUM_TIMING = Helpers.Timing.secondsToMicro(Types.Timing.Seconds(4));
|
|
let scoreClassification = ScoreClassification.BAD;
|
|
if (lcpTimeInMicroseconds <= LCP_MEDIUM_TIMING) {
|
|
scoreClassification = ScoreClassification.OK;
|
|
}
|
|
if (lcpTimeInMicroseconds <= LCP_GOOD_TIMING) {
|
|
scoreClassification = ScoreClassification.GOOD;
|
|
}
|
|
return scoreClassification;
|
|
}
|
|
/**
|
|
* DCL does not have a classification.
|
|
*/
|
|
export function scoreClassificationForDOMContentLoaded(_dclTimeInMicroseconds) {
|
|
return ScoreClassification.UNCLASSIFIED;
|
|
}
|
|
/**
|
|
* Classifications sourced from
|
|
* https://web.dev/lighthouse-total-blocking-#time/
|
|
*/
|
|
export function scoreClassificationForTotalBlockingTime(tbtTimeInMicroseconds) {
|
|
const TBT_GOOD_TIMING = Helpers.Timing.milliToMicro(Types.Timing.Milli(200));
|
|
const TBT_MEDIUM_TIMING = Helpers.Timing.milliToMicro(Types.Timing.Milli(600));
|
|
let scoreClassification = ScoreClassification.BAD;
|
|
if (tbtTimeInMicroseconds <= TBT_MEDIUM_TIMING) {
|
|
scoreClassification = ScoreClassification.OK;
|
|
}
|
|
if (tbtTimeInMicroseconds <= TBT_GOOD_TIMING) {
|
|
scoreClassification = ScoreClassification.GOOD;
|
|
}
|
|
return scoreClassification;
|
|
}
|
|
/**
|
|
* Gets all the Largest Contentful Paint scores of all the frames in the
|
|
* trace.
|
|
*/
|
|
function gatherFinalLCPEvents() {
|
|
const allFinalLCPEvents = [];
|
|
const dataForAllFrames = [...metricScoresByFrameId.values()];
|
|
const dataForAllNavigations = dataForAllFrames.flatMap(frameData => [...frameData.values()]);
|
|
for (let i = 0; i < dataForAllNavigations.length; i++) {
|
|
const navigationData = dataForAllNavigations[i];
|
|
const lcpInNavigation = navigationData.get(MetricName.LCP);
|
|
if (!lcpInNavigation?.event) {
|
|
continue;
|
|
}
|
|
allFinalLCPEvents.push(lcpInNavigation.event);
|
|
}
|
|
return allFinalLCPEvents;
|
|
}
|
|
export async function finalize() {
|
|
pageLoadEventsArray.sort((a, b) => a.ts - b.ts);
|
|
for (const pageLoadEvent of pageLoadEventsArray) {
|
|
const navigation = getNavigationForPageLoadEvent(pageLoadEvent);
|
|
if (navigation) {
|
|
// Event's navigation was not filtered out as noise.
|
|
storePageLoadMetricAgainstNavigationId(navigation, pageLoadEvent);
|
|
}
|
|
}
|
|
// NOTE: if you are looking for the TBT calculation, it has temporarily been
|
|
// removed. See crbug.com/1424335 for details.
|
|
const allFinalLCPEvents = gatherFinalLCPEvents();
|
|
const mainFrame = metaHandlerData().mainFrameId;
|
|
// Filter out LCP candidates to use only definitive LCP values
|
|
const allEventsButLCP = pageLoadEventsArray.filter(event => !Types.Events.isLargestContentfulPaintCandidate(event));
|
|
const markerEvents = [...allFinalLCPEvents, ...allEventsButLCP].filter(Types.Events.isMarkerEvent);
|
|
// Filter by main frame and sort.
|
|
allMarkerEvents =
|
|
markerEvents.filter(event => getFrameIdForPageLoadEvent(event) === mainFrame).sort((a, b) => a.ts - b.ts);
|
|
}
|
|
export function data() {
|
|
return {
|
|
metricScoresByFrameId,
|
|
allMarkerEvents,
|
|
};
|
|
}
|
|
export function deps() {
|
|
return ['Meta'];
|
|
}
|
|
export var ScoreClassification;
|
|
(function (ScoreClassification) {
|
|
ScoreClassification["GOOD"] = "good";
|
|
ScoreClassification["OK"] = "ok";
|
|
ScoreClassification["BAD"] = "bad";
|
|
// Some metrics (such as DOMContentLoaded) don't have a Good/OK/Bad classification, hence this additional entry.
|
|
ScoreClassification["UNCLASSIFIED"] = "unclassified";
|
|
})(ScoreClassification || (ScoreClassification = {}));
|
|
export var MetricName;
|
|
(function (MetricName) {
|
|
// First Contentful Paint
|
|
MetricName["FCP"] = "FCP";
|
|
// First Paint
|
|
MetricName["FP"] = "FP";
|
|
// MarkLoad
|
|
MetricName["L"] = "L";
|
|
MetricName["LCP"] = "LCP";
|
|
// Mark DOM Content
|
|
MetricName["DCL"] = "DCL";
|
|
// Time To Interactive
|
|
MetricName["TTI"] = "TTI";
|
|
// Total Blocking Time
|
|
MetricName["TBT"] = "TBT";
|
|
// Cumulative Layout Shift
|
|
MetricName["CLS"] = "CLS";
|
|
// Navigation
|
|
MetricName["NAV"] = "Nav";
|
|
// Note: INP is handled in UserInteractionsHandler
|
|
})(MetricName || (MetricName = {}));
|
|
export function metricIsLCP(metric) {
|
|
return metric.metricName === MetricName.LCP;
|
|
}
|
|
//# sourceMappingURL=PageLoadMetricsHandler.js.map |