Rocky_Mountain_Vending/.pnpm-store/v10/files/14/7da801f0dbc96f4e608ad3e2a1e7a10fe18e7e6f27f3ef42060cbf95030cd4ce7c5cc2a0b3d59e7c0c0903de41b8cba1a029d0078c27fbea599aa7d971c6f1
DMleadgen 46d973904b
Initial commit: Rocky Mountain Vending website
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>
2026-02-12 16:22:15 -07:00

455 lines
No EOL
22 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.
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';
import { ScoreClassification } from './PageLoadMetricsHandler.js';
import { data as screenshotsHandlerData } from './ScreenshotsHandler.js';
// This represents the maximum #time we will allow a cluster to go before we
// reset it.
export const MAX_CLUSTER_DURATION = Helpers.Timing.milliToMicro(Types.Timing.Milli(5000));
// This represents the maximum #time we will allow between layout shift events
// before considering it to be the start of a new cluster.
export const MAX_SHIFT_TIME_DELTA = Helpers.Timing.milliToMicro(Types.Timing.Milli(1000));
// Layout shifts are reported globally to the developer, irrespective of which
// frame they originated in. However, each process does have its own individual
// CLS score, so we need to segment by process. This means Layout Shifts from
// sites with one process (no subframes, or subframes from the same origin)
// will be reported together. In the case of multiple renderers (frames across
// different origins), we offer the developer the ability to switch renderer in
// the UI.
const layoutShiftEvents = [];
// These events denote potential node resizings. We store them to link captured
// layout shifts to the resizing of unsized elements.
const layoutInvalidationEvents = [];
const scheduleStyleInvalidationEvents = [];
const styleRecalcInvalidationEvents = [];
const renderFrameImplCreateChildFrameEvents = [];
const domLoadingEvents = [];
const layoutImageUnsizedEvents = [];
const remoteFonts = [];
const backendNodeIds = new Set();
// Layout shifts happen during PrePaint as part of the rendering lifecycle.
// We determine if a LayoutInvalidation event is a potential root cause of a layout
// shift if the next PrePaint after the LayoutInvalidation is the parent
// node of such shift.
const prePaintEvents = [];
const paintImageEvents = [];
let sessionMaxScore = 0;
let clsWindowID = -1;
const clusters = [];
const clustersByNavigationId = new Map();
// The complete timeline of LS score changes in a trace.
// Includes drops to 0 when session windows end.
const scoreRecords = [];
export function reset() {
layoutShiftEvents.length = 0;
layoutInvalidationEvents.length = 0;
scheduleStyleInvalidationEvents.length = 0;
styleRecalcInvalidationEvents.length = 0;
prePaintEvents.length = 0;
paintImageEvents.length = 0;
renderFrameImplCreateChildFrameEvents.length = 0;
layoutImageUnsizedEvents.length = 0;
domLoadingEvents.length = 0;
remoteFonts.length = 0;
backendNodeIds.clear();
clusters.length = 0;
sessionMaxScore = 0;
scoreRecords.length = 0;
clsWindowID = -1;
clustersByNavigationId.clear();
}
export function handleEvent(event) {
if (Types.Events.isLayoutShift(event) && !event.args.data?.had_recent_input) {
layoutShiftEvents.push(event);
return;
}
if (Types.Events.isLayoutInvalidationTracking(event)) {
layoutInvalidationEvents.push(event);
return;
}
if (Types.Events.isScheduleStyleInvalidationTracking(event)) {
scheduleStyleInvalidationEvents.push(event);
}
if (Types.Events.isStyleRecalcInvalidationTracking(event)) {
styleRecalcInvalidationEvents.push(event);
}
if (Types.Events.isPrePaint(event)) {
prePaintEvents.push(event);
return;
}
if (Types.Events.isRenderFrameImplCreateChildFrame(event)) {
renderFrameImplCreateChildFrameEvents.push(event);
}
if (Types.Events.isDomLoading(event)) {
domLoadingEvents.push(event);
}
if (Types.Events.isLayoutImageUnsized(event)) {
layoutImageUnsizedEvents.push(event);
}
if (Types.Events.isBeginRemoteFontLoad(event)) {
remoteFonts.push({
display: event.args.display,
url: event.args.url,
beginRemoteFontLoadEvent: event,
});
}
if (Types.Events.isRemoteFontLoaded(event)) {
for (const remoteFont of remoteFonts) {
if (remoteFont.url === event.args.url) {
remoteFont.name = event.args.name;
}
}
}
if (Types.Events.isPaintImage(event)) {
paintImageEvents.push(event);
}
}
function traceWindowFromTime(time) {
return {
min: time,
max: time,
range: Types.Timing.Micro(0),
};
}
function updateTraceWindowMax(traceWindow, newMax) {
traceWindow.max = newMax;
traceWindow.range = Types.Timing.Micro(traceWindow.max - traceWindow.min);
}
function findScreenshots(timestamp) {
const data = screenshotsHandlerData();
if (data.screenshots) {
const before = Helpers.Trace.findPreviousEventBeforeTimestamp(data.screenshots, timestamp);
const after = before ? data.screenshots[data.screenshots.indexOf(before) + 1] : null;
return { before, after };
}
if (data.legacySyntheticScreenshots) {
const before = Helpers.Trace.findPreviousEventBeforeTimestamp(data.legacySyntheticScreenshots, timestamp);
const after = before ? data.legacySyntheticScreenshots[data.legacySyntheticScreenshots.indexOf(before) + 1] : null;
return { before, after };
}
// No screenshots
return { before: null, after: null };
}
function buildScoreRecords() {
const { traceBounds } = metaHandlerData();
scoreRecords.push({ ts: traceBounds.min, score: 0 });
for (const cluster of clusters) {
let clusterScore = 0;
if (cluster.events[0].args.data) {
scoreRecords.push({ ts: cluster.clusterWindow.min, score: cluster.events[0].args.data.weighted_score_delta });
}
for (let i = 0; i < cluster.events.length; i++) {
const event = cluster.events[i];
if (!event.args.data) {
continue;
}
clusterScore += event.args.data.weighted_score_delta;
scoreRecords.push({ ts: event.ts, score: clusterScore });
}
scoreRecords.push({ ts: cluster.clusterWindow.max, score: 0 });
}
}
/**
* Collects backend node ids coming from LayoutShift and LayoutInvalidation
* events.
*/
function collectNodes() {
backendNodeIds.clear();
// Collect the node ids present in the shifts.
for (const layoutShift of layoutShiftEvents) {
if (!layoutShift.args.data?.impacted_nodes) {
continue;
}
for (const node of layoutShift.args.data.impacted_nodes) {
backendNodeIds.add(node.node_id);
}
}
// Collect the node ids present in LayoutInvalidation & scheduleStyleInvalidation events.
for (const layoutInvalidation of layoutInvalidationEvents) {
if (!layoutInvalidation.args.data?.nodeId) {
continue;
}
backendNodeIds.add(layoutInvalidation.args.data.nodeId);
}
for (const scheduleStyleInvalidation of scheduleStyleInvalidationEvents) {
if (!scheduleStyleInvalidation.args.data?.nodeId) {
continue;
}
backendNodeIds.add(scheduleStyleInvalidation.args.data.nodeId);
}
}
export async function finalize() {
// Ensure the events are sorted by #time ascending.
layoutShiftEvents.sort((a, b) => a.ts - b.ts);
prePaintEvents.sort((a, b) => a.ts - b.ts);
layoutInvalidationEvents.sort((a, b) => a.ts - b.ts);
renderFrameImplCreateChildFrameEvents.sort((a, b) => a.ts - b.ts);
domLoadingEvents.sort((a, b) => a.ts - b.ts);
layoutImageUnsizedEvents.sort((a, b) => a.ts - b.ts);
remoteFonts.sort((a, b) => a.beginRemoteFontLoadEvent.ts - b.beginRemoteFontLoadEvent.ts);
paintImageEvents.sort((a, b) => a.ts - b.ts);
// Each function transforms the data used by the next, as such the invoke order
// is important.
await buildLayoutShiftsClusters();
buildScoreRecords();
collectNodes();
}
async function buildLayoutShiftsClusters() {
const { navigationsByFrameId, mainFrameId, traceBounds } = metaHandlerData();
const navigations = navigationsByFrameId.get(mainFrameId) || [];
if (layoutShiftEvents.length === 0) {
return;
}
let firstShiftTime = layoutShiftEvents[0].ts;
let lastShiftTime = layoutShiftEvents[0].ts;
let lastShiftNavigation = null;
// Now step through each and create clusters.
// A cluster is equivalent to a session window (see https://web.dev/cls/#what-is-cls).
// To make the line chart clear, we explicitly demark the limits of each session window
// by starting the cumulative score of the window at the time of the first layout shift
// and ending it (dropping the line back to 0) when the window ends according to the
// thresholds (MAX_CLUSTER_DURATION, MAX_SHIFT_TIME_DELTA).
for (const event of layoutShiftEvents) {
// First detect if either the cluster duration or the #time between this and
// the last shift has been exceeded.
const clusterDurationExceeded = event.ts - firstShiftTime > MAX_CLUSTER_DURATION;
const maxTimeDeltaSinceLastShiftExceeded = event.ts - lastShiftTime > MAX_SHIFT_TIME_DELTA;
// Next take a look at navigations. If between this and the last shift we have navigated,
// note it.
const currentShiftNavigation = Platform.ArrayUtilities.nearestIndexFromEnd(navigations, nav => nav.ts < event.ts);
const hasNavigated = lastShiftNavigation !== currentShiftNavigation && currentShiftNavigation !== null;
// If any of the above criteria are met or if we don't have any cluster yet we should
// start a new one.
if (clusterDurationExceeded || maxTimeDeltaSinceLastShiftExceeded || hasNavigated || !clusters.length) {
// The cluster starts #time should be the timestamp of the first layout shift in it.
const clusterStartTime = event.ts;
// If the last session window ended because the max delta time between shifts
// was exceeded set the endtime to MAX_SHIFT_TIME_DELTA microseconds after the
// last shift in the session.
const endTimeByMaxSessionDuration = clusterDurationExceeded ? firstShiftTime + MAX_CLUSTER_DURATION : Infinity;
// If the last session window ended because the max session duration was
// surpassed, set the endtime so that the window length = MAX_CLUSTER_DURATION;
const endTimeByMaxShiftGap = maxTimeDeltaSinceLastShiftExceeded ? lastShiftTime + MAX_SHIFT_TIME_DELTA : Infinity;
// If there was a navigation during the last window, close it at the time
// of the navigation.
const endTimeByNavigation = hasNavigated ? navigations[currentShiftNavigation].ts : Infinity;
// End the previous cluster at the time of the first of the criteria above that was met.
const previousClusterEndTime = Math.min(endTimeByMaxSessionDuration, endTimeByMaxShiftGap, endTimeByNavigation);
// If there is an existing cluster update its closing time.
if (clusters.length > 0) {
const currentCluster = clusters[clusters.length - 1];
updateTraceWindowMax(currentCluster.clusterWindow, Types.Timing.Micro(previousClusterEndTime));
}
// If this cluster happened after a navigation, set the navigationId to
// the current navigation. This lets us easily group clusters by
// navigation.
const navigationId = currentShiftNavigation === null ?
Types.Events.NO_NAVIGATION :
navigations[currentShiftNavigation].args.data?.navigationId;
// TODO: `navigationId` is `string | undefined`, but the undefined portion
// comes from `data.navigationId`. I don't think that is possible for this
// event type. Can we make this typing stronger? In the meantime, we allow
// `navigationId` to include undefined values.
clusters.push(Helpers.SyntheticEvents.SyntheticEventsManager
.registerSyntheticEvent({
name: 'SyntheticLayoutShiftCluster',
// Will be replaced by the worst layout shift in the next for loop.
rawSourceEvent: event,
events: [],
clusterWindow: traceWindowFromTime(clusterStartTime),
clusterCumulativeScore: 0,
scoreWindows: {
good: traceWindowFromTime(clusterStartTime),
},
navigationId,
// Set default Event so that this event is treated accordingly for the track appender.
ts: event.ts,
pid: event.pid,
tid: event.tid,
ph: Types.Events.Phase.COMPLETE,
cat: '',
dur: Types.Timing.Micro(-1), // This `cluster.dur` is updated below.
}));
firstShiftTime = clusterStartTime;
}
// Given the above we should have a cluster available, so pick the most
// recent one and append the shift, bump its score and window values accordingly.
const currentCluster = clusters[clusters.length - 1];
const timeFromNavigation = currentShiftNavigation !== null ?
Types.Timing.Micro(event.ts - navigations[currentShiftNavigation].ts) :
undefined;
currentCluster.clusterCumulativeScore += event.args.data ? event.args.data.weighted_score_delta : 0;
if (!event.args.data) {
continue;
}
const shift = Helpers.SyntheticEvents.SyntheticEventsManager.registerSyntheticEvent({
rawSourceEvent: event,
...event,
name: Types.Events.Name.SYNTHETIC_LAYOUT_SHIFT,
args: {
frame: event.args.frame,
data: {
...event.args.data,
rawEvent: event,
navigationId: currentCluster.navigationId ?? undefined,
},
},
parsedData: {
timeFromNavigation,
screenshots: findScreenshots(event.ts),
cumulativeWeightedScoreInWindow: currentCluster.clusterCumulativeScore,
// The score of the session window is temporarily set to 0 just
// to initialize it. Since we need to get the score of all shifts
// in the session window to determine its value, its definite
// value is set when stepping through the built clusters.
sessionWindowData: { cumulativeWindowScore: 0, id: clusters.length },
},
});
currentCluster.events.push(shift);
updateTraceWindowMax(currentCluster.clusterWindow, event.ts);
lastShiftTime = event.ts;
lastShiftNavigation = currentShiftNavigation;
}
// Now step through each cluster and set up the times at which the value
// goes from Good, to needs improvement, to Bad. Note that if there is a
// large jump we may go from Good to Bad without ever creating a Needs
// Improvement window at all.
for (const cluster of clusters) {
let weightedScore = 0;
let windowID = -1;
// If this is the last cluster update its window. The cluster duration is determined
// by the minimum between: time to next navigation, trace end time, time to maximum
// cluster duration and time to maximum gap between layout shifts.
if (cluster === clusters[clusters.length - 1]) {
const clusterEndByMaxDuration = MAX_CLUSTER_DURATION + cluster.clusterWindow.min;
const clusterEndByMaxGap = cluster.clusterWindow.max + MAX_SHIFT_TIME_DELTA;
const nextNavigationIndex = Platform.ArrayUtilities.nearestIndexFromBeginning(navigations, nav => nav.ts > cluster.clusterWindow.max);
const nextNavigationTime = nextNavigationIndex ? navigations[nextNavigationIndex].ts : Infinity;
const clusterEnd = Math.min(clusterEndByMaxDuration, clusterEndByMaxGap, traceBounds.max, nextNavigationTime);
updateTraceWindowMax(cluster.clusterWindow, Types.Timing.Micro(clusterEnd));
}
let largestScore = 0;
let worstShiftEvent = null;
for (const shift of cluster.events) {
weightedScore += shift.args.data ? shift.args.data.weighted_score_delta : 0;
windowID = shift.parsedData.sessionWindowData.id;
const ts = shift.ts;
// Update the the CLS score of this shift's session window now that
// we have it.
shift.parsedData.sessionWindowData.cumulativeWindowScore = cluster.clusterCumulativeScore;
if (weightedScore < LayoutShiftsThreshold.NEEDS_IMPROVEMENT) {
// Expand the Good window.
updateTraceWindowMax(cluster.scoreWindows.good, ts);
}
else if (weightedScore >= LayoutShiftsThreshold.NEEDS_IMPROVEMENT && weightedScore < LayoutShiftsThreshold.BAD) {
if (!cluster.scoreWindows.needsImprovement) {
// Close the Good window, and open the needs improvement window.
updateTraceWindowMax(cluster.scoreWindows.good, Types.Timing.Micro(ts - 1));
cluster.scoreWindows.needsImprovement = traceWindowFromTime(ts);
}
// Expand the needs improvement window.
updateTraceWindowMax(cluster.scoreWindows.needsImprovement, ts);
}
else if (weightedScore >= LayoutShiftsThreshold.BAD) {
if (!cluster.scoreWindows.bad) {
// We may jump from Good to Bad here, so update whichever window is open.
if (cluster.scoreWindows.needsImprovement) {
updateTraceWindowMax(cluster.scoreWindows.needsImprovement, Types.Timing.Micro(ts - 1));
}
else {
updateTraceWindowMax(cluster.scoreWindows.good, Types.Timing.Micro(ts - 1));
}
cluster.scoreWindows.bad = traceWindowFromTime(shift.ts);
}
// Expand the Bad window.
updateTraceWindowMax(cluster.scoreWindows.bad, ts);
}
// At this point the windows are set by the timestamps of the events, but the
// next cluster begins at the timestamp of its first event. As such we now
// need to expand the score window to the end of the cluster, and we do so
// by using the Bad widow if it's there, or the NI window, or finally the
// Good window.
if (cluster.scoreWindows.bad) {
updateTraceWindowMax(cluster.scoreWindows.bad, cluster.clusterWindow.max);
}
else if (cluster.scoreWindows.needsImprovement) {
updateTraceWindowMax(cluster.scoreWindows.needsImprovement, cluster.clusterWindow.max);
}
else {
updateTraceWindowMax(cluster.scoreWindows.good, cluster.clusterWindow.max);
}
// Find the worst layout shift of the cluster.
const score = shift.args.data?.weighted_score_delta;
if (score !== undefined && score > largestScore) {
largestScore = score;
worstShiftEvent = shift;
}
}
// Update the cluster's worst layout shift.
if (worstShiftEvent) {
cluster.worstShiftEvent = worstShiftEvent;
cluster.rawSourceEvent = worstShiftEvent;
}
// layout shifts are already sorted by time ascending.
// Capture the time range of the cluster.
cluster.ts = cluster.events[0].ts;
const lastShiftTimings = Helpers.Timing.eventTimingsMicroSeconds(cluster.events[cluster.events.length - 1]);
// Add MAX_SHIFT_TIME_DELTA, the section gap after the last layout shift. This marks the end of the cluster.
cluster.dur = Types.Timing.Micro((lastShiftTimings.endTime - cluster.events[0].ts) + MAX_SHIFT_TIME_DELTA);
if (weightedScore > sessionMaxScore) {
clsWindowID = windowID;
sessionMaxScore = weightedScore;
}
if (cluster.navigationId) {
const clustersForId = Platform.MapUtilities.getWithDefault(clustersByNavigationId, cluster.navigationId, () => {
return [];
});
clustersForId.push(cluster);
}
}
}
export function data() {
return {
clusters,
sessionMaxScore,
clsWindowID,
prePaintEvents,
layoutInvalidationEvents,
scheduleStyleInvalidationEvents,
styleRecalcInvalidationEvents: [],
renderFrameImplCreateChildFrameEvents,
domLoadingEvents,
layoutImageUnsizedEvents,
remoteFonts,
scoreRecords,
// TODO(crbug/41484172): change the type so no need to clone
backendNodeIds: [...backendNodeIds],
clustersByNavigationId: new Map(clustersByNavigationId),
paintImageEvents,
};
}
export function deps() {
return ['Screenshots', 'Meta'];
}
export function scoreClassificationForLayoutShift(score) {
let state = ScoreClassification.GOOD;
if (score >= LayoutShiftsThreshold.NEEDS_IMPROVEMENT) {
state = ScoreClassification.OK;
}
if (score >= LayoutShiftsThreshold.BAD) {
state = ScoreClassification.BAD;
}
return state;
}
// Based on https://web.dev/cls/
export var LayoutShiftsThreshold;
(function (LayoutShiftsThreshold) {
LayoutShiftsThreshold[LayoutShiftsThreshold["GOOD"] = 0] = "GOOD";
LayoutShiftsThreshold[LayoutShiftsThreshold["NEEDS_IMPROVEMENT"] = 0.1] = "NEEDS_IMPROVEMENT";
LayoutShiftsThreshold[LayoutShiftsThreshold["BAD"] = 0.25] = "BAD";
})(LayoutShiftsThreshold || (LayoutShiftsThreshold = {}));
//# sourceMappingURL=LayoutShiftsHandler.js.map