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>
299 lines
No EOL
14 KiB
Text
299 lines
No EOL
14 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 Helpers from '../helpers/helpers.js';
|
|
import * as Types from '../types/types.js';
|
|
import { data as metaHandlerData } from './MetaHandler.js';
|
|
import { ScoreClassification } from './PageLoadMetricsHandler.js';
|
|
// This handler serves two purposes. It generates a list of events that are
|
|
// used to show user clicks in the timeline. It is also used to gather
|
|
// EventTimings into Interactions, which we use to show interactions and
|
|
// highlight long interactions to the user, along with INP.
|
|
// We don't need to know which process / thread these events occurred in,
|
|
// because they are effectively global, so we just track all that we find.
|
|
const allEvents = [];
|
|
const beginCommitCompositorFrameEvents = [];
|
|
const parseMetaViewportEvents = [];
|
|
export const LONG_INTERACTION_THRESHOLD = Helpers.Timing.milliToMicro(Types.Timing.Milli(200));
|
|
const INP_GOOD_TIMING = LONG_INTERACTION_THRESHOLD;
|
|
const INP_MEDIUM_TIMING = Helpers.Timing.milliToMicro(Types.Timing.Milli(500));
|
|
let longestInteractionEvent = null;
|
|
const interactionEvents = [];
|
|
const interactionEventsWithNoNesting = [];
|
|
const eventTimingEndEventsById = new Map();
|
|
const eventTimingStartEventsForInteractions = [];
|
|
export function reset() {
|
|
allEvents.length = 0;
|
|
beginCommitCompositorFrameEvents.length = 0;
|
|
parseMetaViewportEvents.length = 0;
|
|
interactionEvents.length = 0;
|
|
eventTimingStartEventsForInteractions.length = 0;
|
|
eventTimingEndEventsById.clear();
|
|
interactionEventsWithNoNesting.length = 0;
|
|
longestInteractionEvent = null;
|
|
}
|
|
export function handleEvent(event) {
|
|
if (Types.Events.isBeginCommitCompositorFrame(event)) {
|
|
beginCommitCompositorFrameEvents.push(event);
|
|
return;
|
|
}
|
|
if (Types.Events.isParseMetaViewport(event)) {
|
|
parseMetaViewportEvents.push(event);
|
|
return;
|
|
}
|
|
if (!Types.Events.isEventTiming(event)) {
|
|
return;
|
|
}
|
|
if (Types.Events.isEventTimingEnd(event)) {
|
|
// Store the end event; for each start event that is an interaction, we need the matching end event to calculate the duration correctly.
|
|
eventTimingEndEventsById.set(event.id, event);
|
|
}
|
|
allEvents.push(event);
|
|
// From this point on we want to find events that represent interactions.
|
|
// These events are always start events - those are the ones that contain all
|
|
// the metadata about the interaction.
|
|
if (!event.args.data || !Types.Events.isEventTimingStart(event)) {
|
|
return;
|
|
}
|
|
const { duration, interactionId } = event.args.data;
|
|
// We exclude events for the sake of interactions if:
|
|
// 1. They have no duration.
|
|
// 2. They have no interactionId
|
|
// 3. They have an interactionId of 0: this indicates that it's not an
|
|
// interaction that we care about because it hasn't had its own interactionId
|
|
// set (0 is the default on the backend).
|
|
// See: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/responsiveness_metrics.cc;l=133;drc=40c209a9c365ebb9f16fb99dfe78c7fe768b9594
|
|
if (duration < 1 || interactionId === undefined || interactionId === 0) {
|
|
return;
|
|
}
|
|
// Store the start event. In the finalize() function we will pair this with
|
|
// its end event and create the synthetic interaction event.
|
|
eventTimingStartEventsForInteractions.push(event);
|
|
}
|
|
/**
|
|
* See https://web.dev/better-responsiveness-metric/#interaction-types for the
|
|
* table that defines these sets.
|
|
**/
|
|
const pointerEventTypes = new Set([
|
|
'pointerdown',
|
|
'touchstart',
|
|
'pointerup',
|
|
'touchend',
|
|
'mousedown',
|
|
'mouseup',
|
|
'click',
|
|
]);
|
|
const keyboardEventTypes = new Set([
|
|
'keydown',
|
|
'keypress',
|
|
'keyup',
|
|
]);
|
|
export function categoryOfInteraction(interaction) {
|
|
if (pointerEventTypes.has(interaction.type)) {
|
|
return 'POINTER';
|
|
}
|
|
if (keyboardEventTypes.has(interaction.type)) {
|
|
return 'KEYBOARD';
|
|
}
|
|
return 'OTHER';
|
|
}
|
|
/**
|
|
* We define a set of interactions as nested where:
|
|
* 1. Their end times align.
|
|
* 2. The longest interaction's start time is earlier than all other
|
|
* interactions with the same end time.
|
|
* 3. The interactions are of the same category [each interaction is either
|
|
* categorised as keyboard, or pointer.]
|
|
*
|
|
* =============A=[pointerup]=
|
|
* ====B=[pointerdown]=
|
|
* ===C=[pointerdown]==
|
|
* ===D=[pointerup]===
|
|
*
|
|
* In this example, B, C and D are all nested and therefore should not be
|
|
* returned from this function.
|
|
*
|
|
* However, in this example we would only consider B nested (under A) and D
|
|
* nested (under C). A and C both stay because they are of different types.
|
|
* ========A=[keydown]====
|
|
* =======B=[keyup]=====
|
|
* ====C=[pointerdown]=
|
|
* =D=[pointerup]=
|
|
**/
|
|
export function removeNestedInteractions(interactions) {
|
|
/**
|
|
* Because we nest events only that are in the same category, we store the
|
|
* longest event for a given end time by category.
|
|
**/
|
|
const earliestEventForEndTimePerCategory = {
|
|
POINTER: new Map(),
|
|
KEYBOARD: new Map(),
|
|
OTHER: new Map(),
|
|
};
|
|
function storeEventIfEarliestForCategoryAndEndTime(interaction) {
|
|
const category = categoryOfInteraction(interaction);
|
|
const earliestEventForEndTime = earliestEventForEndTimePerCategory[category];
|
|
const endTime = Types.Timing.Micro(interaction.ts + interaction.dur);
|
|
const earliestCurrentEvent = earliestEventForEndTime.get(endTime);
|
|
if (!earliestCurrentEvent) {
|
|
earliestEventForEndTime.set(endTime, interaction);
|
|
return;
|
|
}
|
|
if (interaction.ts < earliestCurrentEvent.ts) {
|
|
earliestEventForEndTime.set(endTime, interaction);
|
|
}
|
|
else if (interaction.ts === earliestCurrentEvent.ts &&
|
|
interaction.interactionId === earliestCurrentEvent.interactionId) {
|
|
// We have seen in traces that the same interaction can have multiple
|
|
// events (e.g. a 'click' and a 'pointerdown'). Often only one of these
|
|
// events will have an event handler bound to it which caused delay on
|
|
// the main thread, and the others will not. This leads to a situation
|
|
// where if we pick one of the events that had no event handler, its
|
|
// processing duration (processingEnd - processingStart) will be 0, but if we
|
|
// had picked the event that had the slow event handler, we would show
|
|
// correctly the main thread delay due to the event handler.
|
|
// So, if we find events with the same interactionId and the same
|
|
// begin/end times, we pick the one with the largest (processingEnd -
|
|
// processingStart) time in order to make sure we find the event with the
|
|
// worst main thread delay, as that is the one the user should care
|
|
// about.
|
|
const currentProcessingDuration = earliestCurrentEvent.processingEnd - earliestCurrentEvent.processingStart;
|
|
const newProcessingDuration = interaction.processingEnd - interaction.processingStart;
|
|
// Use the new interaction if it has a longer processing duration than the existing one.
|
|
if (newProcessingDuration > currentProcessingDuration) {
|
|
earliestEventForEndTime.set(endTime, interaction);
|
|
}
|
|
}
|
|
// Maximize the processing duration based on the "children" interactions.
|
|
// We pick the earliest start processing duration, and the latest end
|
|
// processing duration to avoid under-reporting.
|
|
if (interaction.processingStart < earliestCurrentEvent.processingStart) {
|
|
earliestCurrentEvent.processingStart = interaction.processingStart;
|
|
writeSyntheticTimespans(earliestCurrentEvent);
|
|
}
|
|
if (interaction.processingEnd > earliestCurrentEvent.processingEnd) {
|
|
earliestCurrentEvent.processingEnd = interaction.processingEnd;
|
|
writeSyntheticTimespans(earliestCurrentEvent);
|
|
}
|
|
}
|
|
for (const interaction of interactions) {
|
|
storeEventIfEarliestForCategoryAndEndTime(interaction);
|
|
}
|
|
// Combine all the events that we have kept from all the per-category event
|
|
// maps back into an array and sort them by timestamp.
|
|
const keptEvents = Object.values(earliestEventForEndTimePerCategory)
|
|
.flatMap(eventsByEndTime => Array.from(eventsByEndTime.values()));
|
|
keptEvents.sort((eventA, eventB) => {
|
|
return eventA.ts - eventB.ts;
|
|
});
|
|
return keptEvents;
|
|
}
|
|
function writeSyntheticTimespans(event) {
|
|
const startEvent = event.args.data.beginEvent;
|
|
const endEvent = event.args.data.endEvent;
|
|
event.inputDelay = Types.Timing.Micro(event.processingStart - startEvent.ts);
|
|
event.mainThreadHandling = Types.Timing.Micro(event.processingEnd - event.processingStart);
|
|
event.presentationDelay = Types.Timing.Micro(endEvent.ts - event.processingEnd);
|
|
}
|
|
export async function finalize() {
|
|
const { navigationsByFrameId } = metaHandlerData();
|
|
// For each interaction start event, find the async end event by the ID, and then create the Synthetic Interaction event.
|
|
for (const interactionStartEvent of eventTimingStartEventsForInteractions) {
|
|
const endEvent = eventTimingEndEventsById.get(interactionStartEvent.id);
|
|
if (!endEvent) {
|
|
// If we cannot find an end event, bail and drop this event.
|
|
continue;
|
|
}
|
|
const { type, interactionId, timeStamp, processingStart, processingEnd } = interactionStartEvent.args.data;
|
|
if (!type || !interactionId || !timeStamp || !processingStart || !processingEnd) {
|
|
// A valid interaction event that we care about has to have a type (e.g. pointerdown, keyup).
|
|
// We also need to ensure it has an interactionId and various timings. There are edge cases where these aren't included in the trace event.
|
|
continue;
|
|
}
|
|
// In the future we will add microsecond timestamps to the trace events…
|
|
// (See https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/window_performance.cc;l=900-901;drc=b503c262e425eae59ced4a80d59d176ed07152c7 )
|
|
// …but until then we can use the millisecond precision values that are in
|
|
// the trace event. To adjust them to be relative to the event.ts and the
|
|
// trace timestamps, for both processingStart and processingEnd we subtract
|
|
// the event timestamp (NOT event.ts, but the timeStamp millisecond value
|
|
// emitted in args.data), and then add that value to the event.ts. This
|
|
// will give us a processingStart and processingEnd time in microseconds
|
|
// that is relative to event.ts, and can be used when drawing boxes.
|
|
// There is some inaccuracy here as we are converting milliseconds to microseconds, but it is good enough until the backend emits more accurate numbers.
|
|
const processingStartRelativeToTraceTime = Types.Timing.Micro(Helpers.Timing.milliToMicro(processingStart) - Helpers.Timing.milliToMicro(timeStamp) +
|
|
interactionStartEvent.ts);
|
|
const processingEndRelativeToTraceTime = Types.Timing.Micro((Helpers.Timing.milliToMicro(processingEnd) - Helpers.Timing.milliToMicro(timeStamp)) +
|
|
interactionStartEvent.ts);
|
|
// Ultimate frameId fallback only needed for TSC, see comments in the type.
|
|
const frameId = interactionStartEvent.args.frame ?? interactionStartEvent.args.data.frame ?? '';
|
|
const navigation = Helpers.Trace.getNavigationForTraceEvent(interactionStartEvent, frameId, navigationsByFrameId);
|
|
const navigationId = navigation?.args.data?.navigationId;
|
|
const interactionEvent = Helpers.SyntheticEvents.SyntheticEventsManager.registerSyntheticEvent({
|
|
// Use the start event to define the common fields.
|
|
rawSourceEvent: interactionStartEvent,
|
|
cat: interactionStartEvent.cat,
|
|
name: interactionStartEvent.name,
|
|
pid: interactionStartEvent.pid,
|
|
tid: interactionStartEvent.tid,
|
|
ph: interactionStartEvent.ph,
|
|
processingStart: processingStartRelativeToTraceTime,
|
|
processingEnd: processingEndRelativeToTraceTime,
|
|
// These will be set in writeSyntheticTimespans()
|
|
inputDelay: Types.Timing.Micro(-1),
|
|
mainThreadHandling: Types.Timing.Micro(-1),
|
|
presentationDelay: Types.Timing.Micro(-1),
|
|
args: {
|
|
data: {
|
|
beginEvent: interactionStartEvent,
|
|
endEvent,
|
|
frame: frameId,
|
|
navigationId,
|
|
},
|
|
},
|
|
ts: interactionStartEvent.ts,
|
|
dur: Types.Timing.Micro(endEvent.ts - interactionStartEvent.ts),
|
|
type: interactionStartEvent.args.data.type,
|
|
interactionId: interactionStartEvent.args.data.interactionId,
|
|
});
|
|
writeSyntheticTimespans(interactionEvent);
|
|
interactionEvents.push(interactionEvent);
|
|
}
|
|
interactionEventsWithNoNesting.push(...removeNestedInteractions(interactionEvents));
|
|
// Pick the longest interactions from the set that were not nested, as we
|
|
// know those are the set of the largest interactions.
|
|
for (const interactionEvent of interactionEventsWithNoNesting) {
|
|
if (!longestInteractionEvent || longestInteractionEvent.dur < interactionEvent.dur) {
|
|
longestInteractionEvent = interactionEvent;
|
|
}
|
|
}
|
|
}
|
|
export function data() {
|
|
return {
|
|
allEvents,
|
|
beginCommitCompositorFrameEvents,
|
|
parseMetaViewportEvents,
|
|
interactionEvents,
|
|
interactionEventsWithNoNesting,
|
|
longestInteractionEvent,
|
|
interactionsOverThreshold: new Set(interactionEvents.filter(event => {
|
|
return event.dur > LONG_INTERACTION_THRESHOLD;
|
|
})),
|
|
};
|
|
}
|
|
export function deps() {
|
|
return ['Meta'];
|
|
}
|
|
/**
|
|
* Classifications sourced from
|
|
* https://web.dev/articles/inp#good-score
|
|
*/
|
|
export function scoreClassificationForInteractionToNextPaint(timing) {
|
|
if (timing <= INP_GOOD_TIMING) {
|
|
return ScoreClassification.GOOD;
|
|
}
|
|
if (timing <= INP_MEDIUM_TIMING) {
|
|
return ScoreClassification.OK;
|
|
}
|
|
return ScoreClassification.BAD;
|
|
}
|
|
//# sourceMappingURL=UserInteractionsHandler.js.map |