summaryrefslogtreecommitdiff
path: root/frontend-old/node_modules/web-vitals/src/attribution
diff options
context:
space:
mode:
authoraltaf-creator <dev@altafcreator.com>2025-11-09 11:15:19 +0800
committeraltaf-creator <dev@altafcreator.com>2025-11-09 11:15:19 +0800
commit8eff962cab608341a6f2fedc640a0e32d96f26e2 (patch)
tree05534d1a720ddc3691d346c69b4972555820a061 /frontend-old/node_modules/web-vitals/src/attribution
pain
Diffstat (limited to 'frontend-old/node_modules/web-vitals/src/attribution')
-rw-r--r--frontend-old/node_modules/web-vitals/src/attribution/deprecated.ts26
-rw-r--r--frontend-old/node_modules/web-vitals/src/attribution/index.ts30
-rw-r--r--frontend-old/node_modules/web-vitals/src/attribution/onCLS.ts93
-rw-r--r--frontend-old/node_modules/web-vitals/src/attribution/onFCP.ts76
-rw-r--r--frontend-old/node_modules/web-vitals/src/attribution/onFID.ts62
-rw-r--r--frontend-old/node_modules/web-vitals/src/attribution/onINP.ts338
-rw-r--r--frontend-old/node_modules/web-vitals/src/attribution/onLCP.ts113
-rw-r--r--frontend-old/node_modules/web-vitals/src/attribution/onTTFB.ts107
8 files changed, 845 insertions, 0 deletions
diff --git a/frontend-old/node_modules/web-vitals/src/attribution/deprecated.ts b/frontend-old/node_modules/web-vitals/src/attribution/deprecated.ts
new file mode 100644
index 0000000..826ebfc
--- /dev/null
+++ b/frontend-old/node_modules/web-vitals/src/attribution/deprecated.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {
+ /**
+ * @deprecated Use `onINP()` instead.
+ */
+ onFID,
+} from './onFID.js';
+
+export {FIDThresholds} from '../onFID.js';
+
+export * from '../types.js';
diff --git a/frontend-old/node_modules/web-vitals/src/attribution/index.ts b/frontend-old/node_modules/web-vitals/src/attribution/index.ts
new file mode 100644
index 0000000..c036282
--- /dev/null
+++ b/frontend-old/node_modules/web-vitals/src/attribution/index.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {onCLS} from './onCLS.js';
+export {onFCP} from './onFCP.js';
+export {onINP} from './onINP.js';
+export {onLCP} from './onLCP.js';
+export {onTTFB} from './onTTFB.js';
+
+export {CLSThresholds} from '../onCLS.js';
+export {FCPThresholds} from '../onFCP.js';
+export {INPThresholds} from '../onINP.js';
+export {LCPThresholds} from '../onLCP.js';
+export {TTFBThresholds} from '../onTTFB.js';
+
+export * from './deprecated.js';
+export * from '../types.js';
diff --git a/frontend-old/node_modules/web-vitals/src/attribution/onCLS.ts b/frontend-old/node_modules/web-vitals/src/attribution/onCLS.ts
new file mode 100644
index 0000000..52e2bee
--- /dev/null
+++ b/frontend-old/node_modules/web-vitals/src/attribution/onCLS.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {getLoadState} from '../lib/getLoadState.js';
+import {getSelector} from '../lib/getSelector.js';
+import {onCLS as unattributedOnCLS} from '../onCLS.js';
+import {
+ CLSAttribution,
+ CLSMetric,
+ CLSMetricWithAttribution,
+ ReportOpts,
+} from '../types.js';
+
+const getLargestLayoutShiftEntry = (entries: LayoutShift[]) => {
+ return entries.reduce((a, b) => (a && a.value > b.value ? a : b));
+};
+
+const getLargestLayoutShiftSource = (sources: LayoutShiftAttribution[]) => {
+ return sources.find((s) => s.node && s.node.nodeType === 1) || sources[0];
+};
+
+const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => {
+ // Use an empty object if no other attribution has been set.
+ let attribution: CLSAttribution = {};
+
+ if (metric.entries.length) {
+ const largestEntry = getLargestLayoutShiftEntry(metric.entries);
+ if (largestEntry && largestEntry.sources && largestEntry.sources.length) {
+ const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
+ if (largestSource) {
+ attribution = {
+ largestShiftTarget: getSelector(largestSource.node),
+ largestShiftTime: largestEntry.startTime,
+ largestShiftValue: largestEntry.value,
+ largestShiftSource: largestSource,
+ largestShiftEntry: largestEntry,
+ loadState: getLoadState(largestEntry.startTime),
+ };
+ }
+ }
+ }
+
+ // Use Object.assign to set property to keep tsc happy.
+ const metricWithAttribution: CLSMetricWithAttribution = Object.assign(
+ metric,
+ {attribution},
+ );
+ return metricWithAttribution;
+};
+
+/**
+ * Calculates the [CLS](https://web.dev/articles/cls) value for the current page and
+ * calls the `callback` function once the value is ready to be reported, along
+ * with all `layout-shift` performance entries that were used in the metric
+ * value calculation. The reported value is a `double` (corresponding to a
+ * [layout shift score](https://web.dev/articles/cls#layout_shift_score)).
+ *
+ * If the `reportAllChanges` configuration option is set to `true`, the
+ * `callback` function will be called as soon as the value is initially
+ * determined as well as any time the value changes throughout the page
+ * lifespan.
+ *
+ * _**Important:** CLS should be continually monitored for changes throughout
+ * the entire lifespan of a page—including if the user returns to the page after
+ * it's been hidden/backgrounded. However, since browsers often [will not fire
+ * additional callbacks once the user has backgrounded a
+ * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
+ * `callback` is always called when the page's visibility state changes to
+ * hidden. As a result, the `callback` function might be called multiple times
+ * during the same page load._
+ */
+export const onCLS = (
+ onReport: (metric: CLSMetricWithAttribution) => void,
+ opts?: ReportOpts,
+) => {
+ unattributedOnCLS((metric: CLSMetric) => {
+ const metricWithAttribution = attributeCLS(metric);
+ onReport(metricWithAttribution);
+ }, opts);
+};
diff --git a/frontend-old/node_modules/web-vitals/src/attribution/onFCP.ts b/frontend-old/node_modules/web-vitals/src/attribution/onFCP.ts
new file mode 100644
index 0000000..079bf91
--- /dev/null
+++ b/frontend-old/node_modules/web-vitals/src/attribution/onFCP.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {getBFCacheRestoreTime} from '../lib/bfcache.js';
+import {getLoadState} from '../lib/getLoadState.js';
+import {getNavigationEntry} from '../lib/getNavigationEntry.js';
+import {onFCP as unattributedOnFCP} from '../onFCP.js';
+import {
+ FCPAttribution,
+ FCPMetric,
+ FCPMetricWithAttribution,
+ ReportOpts,
+} from '../types.js';
+
+const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => {
+ // Use a default object if no other attribution has been set.
+ let attribution: FCPAttribution = {
+ timeToFirstByte: 0,
+ firstByteToFCP: metric.value,
+ loadState: getLoadState(getBFCacheRestoreTime()),
+ };
+
+ if (metric.entries.length) {
+ const navigationEntry = getNavigationEntry();
+ const fcpEntry = metric.entries[metric.entries.length - 1];
+
+ if (navigationEntry) {
+ const activationStart = navigationEntry.activationStart || 0;
+ const ttfb = Math.max(0, navigationEntry.responseStart - activationStart);
+
+ attribution = {
+ timeToFirstByte: ttfb,
+ firstByteToFCP: metric.value - ttfb,
+ loadState: getLoadState(metric.entries[0].startTime),
+ navigationEntry,
+ fcpEntry,
+ };
+ }
+ }
+
+ // Use Object.assign to set property to keep tsc happy.
+ const metricWithAttribution: FCPMetricWithAttribution = Object.assign(
+ metric,
+ {attribution},
+ );
+ return metricWithAttribution;
+};
+
+/**
+ * Calculates the [FCP](https://web.dev/articles/fcp) value for the current page and
+ * calls the `callback` function once the value is ready, along with the
+ * relevant `paint` performance entry used to determine the value. The reported
+ * value is a `DOMHighResTimeStamp`.
+ */
+export const onFCP = (
+ onReport: (metric: FCPMetricWithAttribution) => void,
+ opts?: ReportOpts,
+) => {
+ unattributedOnFCP((metric: FCPMetric) => {
+ const metricWithAttribution = attributeFCP(metric);
+ onReport(metricWithAttribution);
+ }, opts);
+};
diff --git a/frontend-old/node_modules/web-vitals/src/attribution/onFID.ts b/frontend-old/node_modules/web-vitals/src/attribution/onFID.ts
new file mode 100644
index 0000000..ed56142
--- /dev/null
+++ b/frontend-old/node_modules/web-vitals/src/attribution/onFID.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {getLoadState} from '../lib/getLoadState.js';
+import {getSelector} from '../lib/getSelector.js';
+import {onFID as unattributedOnFID} from '../onFID.js';
+import {
+ FIDAttribution,
+ FIDMetric,
+ FIDMetricWithAttribution,
+ ReportOpts,
+} from '../types.js';
+
+const attributeFID = (metric: FIDMetric): FIDMetricWithAttribution => {
+ const fidEntry = metric.entries[0];
+ const attribution: FIDAttribution = {
+ eventTarget: getSelector(fidEntry.target),
+ eventType: fidEntry.name,
+ eventTime: fidEntry.startTime,
+ eventEntry: fidEntry,
+ loadState: getLoadState(fidEntry.startTime),
+ };
+
+ // Use Object.assign to set property to keep tsc happy.
+ const metricWithAttribution: FIDMetricWithAttribution = Object.assign(
+ metric,
+ {attribution},
+ );
+ return metricWithAttribution;
+};
+
+/**
+ * Calculates the [FID](https://web.dev/articles/fid) value for the current page and
+ * calls the `callback` function once the value is ready, along with the
+ * relevant `first-input` performance entry used to determine the value. The
+ * reported value is a `DOMHighResTimeStamp`.
+ *
+ * _**Important:** since FID is only reported after the user interacts with the
+ * page, it's possible that it will not be reported for some page loads._
+ */
+export const onFID = (
+ onReport: (metric: FIDMetricWithAttribution) => void,
+ opts?: ReportOpts,
+) => {
+ unattributedOnFID((metric: FIDMetric) => {
+ const metricWithAttribution = attributeFID(metric);
+ onReport(metricWithAttribution);
+ }, opts);
+};
diff --git a/frontend-old/node_modules/web-vitals/src/attribution/onINP.ts b/frontend-old/node_modules/web-vitals/src/attribution/onINP.ts
new file mode 100644
index 0000000..0986765
--- /dev/null
+++ b/frontend-old/node_modules/web-vitals/src/attribution/onINP.ts
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {getLoadState} from '../lib/getLoadState.js';
+import {getSelector} from '../lib/getSelector.js';
+import {
+ longestInteractionList,
+ entryPreProcessingCallbacks,
+ longestInteractionMap,
+} from '../lib/interactions.js';
+import {observe} from '../lib/observe.js';
+import {whenIdle} from '../lib/whenIdle.js';
+import {onINP as unattributedOnINP} from '../onINP.js';
+import {
+ INPAttribution,
+ INPMetric,
+ INPMetricWithAttribution,
+ ReportOpts,
+} from '../types.js';
+
+interface pendingEntriesGroup {
+ startTime: DOMHighResTimeStamp;
+ processingStart: DOMHighResTimeStamp;
+ processingEnd: DOMHighResTimeStamp;
+ renderTime: DOMHighResTimeStamp;
+ entries: PerformanceEventTiming[];
+}
+
+// The maximum number of previous frames for which data is kept.
+// Storing data about previous frames is necessary to handle cases where event
+// and LoAF entries are dispatched out of order, and so a buffer of previous
+// frame data is needed to determine various bits of INP attribution once all
+// the frame-related data has come in.
+// In most cases this out-of-order data is only off by a frame or two, so
+// keeping the most recent 50 should be more than sufficient.
+const MAX_PREVIOUS_FRAMES = 50;
+
+// A PerformanceObserver, observing new `long-animation-frame` entries.
+// If this variable is defined it means the browser supports LoAF.
+let loafObserver: PerformanceObserver | undefined;
+
+// A list of LoAF entries that have been dispatched and could potentially
+// intersect with the INP candidate interaction. Note that periodically this
+// list is cleaned up and entries that are known to not match INP are removed.
+let pendingLoAFs: PerformanceLongAnimationFrameTiming[] = [];
+
+// An array of groups of all the event timing entries that occurred within a
+// particular frame. Note that periodically this array is cleaned up and entries
+// that are known to not match INP are removed.
+let pendingEntriesGroups: pendingEntriesGroup[] = [];
+
+// The `processingEnd` time of most recently-processed event, chronologically.
+let latestProcessingEnd: number = 0;
+
+// A WeakMap to look up the event-timing-entries group of a given entry.
+// Note that this only maps from "important" entries: either the first input or
+// those with an `interactionId`.
+const entryToEntriesGroupMap: WeakMap<
+ PerformanceEventTiming,
+ pendingEntriesGroup
+> = new WeakMap();
+
+// A mapping of interactionIds to the target Node.
+export const interactionTargetMap: Map<number, Node> = new Map();
+
+// A reference to the idle task used to clean up entries from the above
+// variables. If the value is -1 it means no task is queue, and if it's
+// greater than -1 the value corresponds to the idle callback handle.
+let idleHandle: number = -1;
+
+/**
+ * Adds new LoAF entries to the `pendingLoAFs` list.
+ */
+const handleLoAFEntries = (entries: PerformanceLongAnimationFrameTiming[]) => {
+ pendingLoAFs = pendingLoAFs.concat(entries);
+ queueCleanup();
+};
+
+// Get a reference to the interaction target element in case it's removed
+// from the DOM later.
+const saveInteractionTarget = (entry: PerformanceEventTiming) => {
+ if (
+ entry.interactionId &&
+ entry.target &&
+ !interactionTargetMap.has(entry.interactionId)
+ ) {
+ interactionTargetMap.set(entry.interactionId, entry.target);
+ }
+};
+
+/**
+ * Groups entries that were presented within the same animation frame by
+ * a common `renderTime`. This function works by referencing
+ * `pendingEntriesGroups` and using an existing render time if one is found
+ * (otherwise creating a new one). This function also adds all interaction
+ * entries to an `entryToRenderTimeMap` WeakMap so that the "grouped" entries
+ * can be looked up later.
+ */
+const groupEntriesByRenderTime = (entry: PerformanceEventTiming) => {
+ const renderTime = entry.startTime + entry.duration;
+ let group;
+
+ latestProcessingEnd = Math.max(latestProcessingEnd, entry.processingEnd);
+
+ // Iterate over all previous render times in reverse order to find a match.
+ // Go in reverse since the most likely match will be at the end.
+ for (let i = pendingEntriesGroups.length - 1; i >= 0; i--) {
+ const potentialGroup = pendingEntriesGroups[i];
+
+ // If a group's render time is within 8ms of the entry's render time,
+ // assume they were part of the same frame and add it to the group.
+ if (Math.abs(renderTime - potentialGroup.renderTime) <= 8) {
+ group = potentialGroup;
+ group.startTime = Math.min(entry.startTime, group.startTime);
+ group.processingStart = Math.min(
+ entry.processingStart,
+ group.processingStart,
+ );
+ group.processingEnd = Math.max(entry.processingEnd, group.processingEnd);
+ group.entries.push(entry);
+
+ break;
+ }
+ }
+
+ // If there was no matching group, assume this is a new frame.
+ if (!group) {
+ group = {
+ startTime: entry.startTime,
+ processingStart: entry.processingStart,
+ processingEnd: entry.processingEnd,
+ renderTime,
+ entries: [entry],
+ };
+
+ pendingEntriesGroups.push(group);
+ }
+
+ // Store the grouped render time for this entry for reference later.
+ if (entry.interactionId || entry.entryType === 'first-input') {
+ entryToEntriesGroupMap.set(entry, group);
+ }
+
+ queueCleanup();
+};
+
+const queueCleanup = () => {
+ // Queue cleanup of entries that are not part of any INP candidates.
+ if (idleHandle < 0) {
+ idleHandle = whenIdle(cleanupEntries);
+ }
+};
+
+const cleanupEntries = () => {
+ // Delete any stored interaction target elements if they're not part of one
+ // of the 10 longest interactions.
+ if (interactionTargetMap.size > 10) {
+ interactionTargetMap.forEach((_, key) => {
+ if (!longestInteractionMap.has(key)) {
+ interactionTargetMap.delete(key);
+ }
+ });
+ }
+
+ // Keep all render times that are part of a pending INP candidate or
+ // that occurred within the 50 most recently-dispatched groups of events.
+ const longestInteractionGroups = longestInteractionList.map((i) => {
+ return entryToEntriesGroupMap.get(i.entries[0]);
+ });
+ const minIndex = pendingEntriesGroups.length - MAX_PREVIOUS_FRAMES;
+ pendingEntriesGroups = pendingEntriesGroups.filter((group, index) => {
+ if (index >= minIndex) return true;
+ return longestInteractionGroups.includes(group);
+ });
+
+ // Keep all pending LoAF entries that either:
+ // 1) intersect with entries in the newly cleaned up `pendingEntriesGroups`
+ // 2) occur after the most recently-processed event entry (for up to MAX_PREVIOUS_FRAMES)
+ const loafsToKeep: Set<PerformanceLongAnimationFrameTiming> = new Set();
+ for (let i = 0; i < pendingEntriesGroups.length; i++) {
+ const group = pendingEntriesGroups[i];
+ getIntersectingLoAFs(group.startTime, group.processingEnd).forEach(
+ (loaf) => {
+ loafsToKeep.add(loaf);
+ },
+ );
+ }
+ const prevFrameIndexCutoff = pendingLoAFs.length - 1 - MAX_PREVIOUS_FRAMES;
+ // Filter `pendingLoAFs` to preserve LoAF order.
+ pendingLoAFs = pendingLoAFs.filter((loaf, index) => {
+ if (loaf.startTime > latestProcessingEnd && index > prevFrameIndexCutoff) {
+ return true;
+ }
+
+ return loafsToKeep.has(loaf);
+ });
+
+ // Reset the idle callback handle so it can be queued again.
+ idleHandle = -1;
+};
+
+entryPreProcessingCallbacks.push(
+ saveInteractionTarget,
+ groupEntriesByRenderTime,
+);
+
+const getIntersectingLoAFs = (
+ start: DOMHighResTimeStamp,
+ end: DOMHighResTimeStamp,
+) => {
+ const intersectingLoAFs = [];
+
+ for (let i = 0, loaf; (loaf = pendingLoAFs[i]); i++) {
+ // If the LoAF ends before the given start time, ignore it.
+ if (loaf.startTime + loaf.duration < start) continue;
+
+ // If the LoAF starts after the given end time, ignore it and all
+ // subsequent pending LoAFs (because they're in time order).
+ if (loaf.startTime > end) break;
+
+ // Still here? If so this LoAF intersects with the interaction.
+ intersectingLoAFs.push(loaf);
+ }
+ return intersectingLoAFs;
+};
+
+const attributeINP = (metric: INPMetric): INPMetricWithAttribution => {
+ const firstEntry = metric.entries[0];
+ const group = entryToEntriesGroupMap.get(firstEntry)!;
+
+ const processingStart = firstEntry.processingStart;
+ const processingEnd = group.processingEnd;
+
+ // Sort the entries in processing time order.
+ const processedEventEntries = group.entries.sort((a, b) => {
+ return a.processingStart - b.processingStart;
+ });
+
+ const longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[] =
+ getIntersectingLoAFs(firstEntry.startTime, processingEnd);
+
+ // The first interaction entry may not have a target defined, so use the
+ // first one found in the entry list.
+ // TODO: when the following bug is fixed just use `firstInteractionEntry`.
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=1367329
+ // As a fallback, also check the interactionTargetMap (to account for
+ // cases where the element is removed from the DOM before reporting happens).
+ const firstEntryWithTarget = metric.entries.find((entry) => entry.target);
+ const interactionTargetElement =
+ (firstEntryWithTarget && firstEntryWithTarget.target) ||
+ interactionTargetMap.get(firstEntry.interactionId);
+
+ // Since entry durations are rounded to the nearest 8ms, we need to clamp
+ // the `nextPaintTime` value to be higher than the `processingEnd` or
+ // end time of any LoAF entry.
+ const nextPaintTimeCandidates = [
+ firstEntry.startTime + firstEntry.duration,
+ processingEnd,
+ ].concat(
+ longAnimationFrameEntries.map((loaf) => loaf.startTime + loaf.duration),
+ );
+
+ const nextPaintTime = Math.max.apply(Math, nextPaintTimeCandidates);
+
+ const attribution: INPAttribution = {
+ interactionTarget: getSelector(interactionTargetElement),
+ interactionTargetElement: interactionTargetElement,
+ interactionType: firstEntry.name.startsWith('key') ? 'keyboard' : 'pointer',
+ interactionTime: firstEntry.startTime,
+ nextPaintTime: nextPaintTime,
+ processedEventEntries: processedEventEntries,
+ longAnimationFrameEntries: longAnimationFrameEntries,
+ inputDelay: processingStart - firstEntry.startTime,
+ processingDuration: processingEnd - processingStart,
+ presentationDelay: Math.max(nextPaintTime - processingEnd, 0),
+ loadState: getLoadState(firstEntry.startTime),
+ };
+
+ // Use Object.assign to set property to keep tsc happy.
+ const metricWithAttribution: INPMetricWithAttribution = Object.assign(
+ metric,
+ {attribution},
+ );
+ return metricWithAttribution;
+};
+
+/**
+ * Calculates the [INP](https://web.dev/articles/inp) value for the current
+ * page and calls the `callback` function once the value is ready, along with
+ * the `event` performance entries reported for that interaction. The reported
+ * value is a `DOMHighResTimeStamp`.
+ *
+ * A custom `durationThreshold` configuration option can optionally be passed to
+ * control what `event-timing` entries are considered for INP reporting. The
+ * default threshold is `40`, which means INP scores of less than 40 are
+ * reported as 0. Note that this will not affect your 75th percentile INP value
+ * unless that value is also less than 40 (well below the recommended
+ * [good](https://web.dev/articles/inp#what_is_a_good_inp_score) threshold).
+ *
+ * If the `reportAllChanges` configuration option is set to `true`, the
+ * `callback` function will be called as soon as the value is initially
+ * determined as well as any time the value changes throughout the page
+ * lifespan.
+ *
+ * _**Important:** INP should be continually monitored for changes throughout
+ * the entire lifespan of a page—including if the user returns to the page after
+ * it's been hidden/backgrounded. However, since browsers often [will not fire
+ * additional callbacks once the user has backgrounded a
+ * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
+ * `callback` is always called when the page's visibility state changes to
+ * hidden. As a result, the `callback` function might be called multiple times
+ * during the same page load._
+ */
+export const onINP = (
+ onReport: (metric: INPMetricWithAttribution) => void,
+ opts?: ReportOpts,
+) => {
+ if (!loafObserver) {
+ loafObserver = observe('long-animation-frame', handleLoAFEntries);
+ }
+ unattributedOnINP((metric: INPMetric) => {
+ const metricWithAttribution = attributeINP(metric);
+ onReport(metricWithAttribution);
+ }, opts);
+};
diff --git a/frontend-old/node_modules/web-vitals/src/attribution/onLCP.ts b/frontend-old/node_modules/web-vitals/src/attribution/onLCP.ts
new file mode 100644
index 0000000..285f319
--- /dev/null
+++ b/frontend-old/node_modules/web-vitals/src/attribution/onLCP.ts
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {getNavigationEntry} from '../lib/getNavigationEntry.js';
+import {getSelector} from '../lib/getSelector.js';
+import {onLCP as unattributedOnLCP} from '../onLCP.js';
+import {
+ LCPAttribution,
+ LCPMetric,
+ LCPMetricWithAttribution,
+ ReportOpts,
+} from '../types.js';
+
+const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => {
+ // Use a default object if no other attribution has been set.
+ let attribution: LCPAttribution = {
+ timeToFirstByte: 0,
+ resourceLoadDelay: 0,
+ resourceLoadDuration: 0,
+ elementRenderDelay: metric.value,
+ };
+
+ if (metric.entries.length) {
+ const navigationEntry = getNavigationEntry();
+ if (navigationEntry) {
+ const activationStart = navigationEntry.activationStart || 0;
+ const lcpEntry = metric.entries[metric.entries.length - 1];
+ const lcpResourceEntry =
+ lcpEntry.url &&
+ performance
+ .getEntriesByType('resource')
+ .filter((e) => e.name === lcpEntry.url)[0];
+
+ const ttfb = Math.max(0, navigationEntry.responseStart - activationStart);
+
+ const lcpRequestStart = Math.max(
+ ttfb,
+ // Prefer `requestStart` (if TOA is set), otherwise use `startTime`.
+ lcpResourceEntry
+ ? (lcpResourceEntry.requestStart || lcpResourceEntry.startTime) -
+ activationStart
+ : 0,
+ );
+ const lcpResponseEnd = Math.max(
+ lcpRequestStart,
+ lcpResourceEntry ? lcpResourceEntry.responseEnd - activationStart : 0,
+ );
+ const lcpRenderTime = Math.max(
+ lcpResponseEnd,
+ lcpEntry.startTime - activationStart,
+ );
+
+ attribution = {
+ element: getSelector(lcpEntry.element),
+ timeToFirstByte: ttfb,
+ resourceLoadDelay: lcpRequestStart - ttfb,
+ resourceLoadDuration: lcpResponseEnd - lcpRequestStart,
+ elementRenderDelay: lcpRenderTime - lcpResponseEnd,
+ navigationEntry,
+ lcpEntry,
+ };
+
+ // Only attribution the URL and resource entry if they exist.
+ if (lcpEntry.url) {
+ attribution.url = lcpEntry.url;
+ }
+ if (lcpResourceEntry) {
+ attribution.lcpResourceEntry = lcpResourceEntry;
+ }
+ }
+ }
+
+ // Use Object.assign to set property to keep tsc happy.
+ const metricWithAttribution: LCPMetricWithAttribution = Object.assign(
+ metric,
+ {attribution},
+ );
+ return metricWithAttribution;
+};
+
+/**
+ * Calculates the [LCP](https://web.dev/articles/lcp) value for the current page and
+ * calls the `callback` function once the value is ready (along with the
+ * relevant `largest-contentful-paint` performance entry used to determine the
+ * value). The reported value is a `DOMHighResTimeStamp`.
+ *
+ * If the `reportAllChanges` configuration option is set to `true`, the
+ * `callback` function will be called any time a new `largest-contentful-paint`
+ * performance entry is dispatched, or once the final value of the metric has
+ * been determined.
+ */
+export const onLCP = (
+ onReport: (metric: LCPMetricWithAttribution) => void,
+ opts?: ReportOpts,
+) => {
+ unattributedOnLCP((metric: LCPMetric) => {
+ const metricWithAttribution = attributeLCP(metric);
+ onReport(metricWithAttribution);
+ }, opts);
+};
diff --git a/frontend-old/node_modules/web-vitals/src/attribution/onTTFB.ts b/frontend-old/node_modules/web-vitals/src/attribution/onTTFB.ts
new file mode 100644
index 0000000..1cfd74b
--- /dev/null
+++ b/frontend-old/node_modules/web-vitals/src/attribution/onTTFB.ts
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {onTTFB as unattributedOnTTFB} from '../onTTFB.js';
+import {
+ TTFBMetric,
+ TTFBMetricWithAttribution,
+ ReportOpts,
+ TTFBAttribution,
+} from '../types.js';
+
+const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => {
+ // Use a default object if no other attribution has been set.
+ let attribution: TTFBAttribution = {
+ waitingDuration: 0,
+ cacheDuration: 0,
+ dnsDuration: 0,
+ connectionDuration: 0,
+ requestDuration: 0,
+ };
+
+ if (metric.entries.length) {
+ const navigationEntry = metric.entries[0];
+ const activationStart = navigationEntry.activationStart || 0;
+
+ // Measure from workerStart or fetchStart so any service worker startup
+ // time is included in cacheDuration (which also includes other sw time
+ // anyway, that cannot be accurately split out cross-browser).
+ const waitEnd = Math.max(
+ (navigationEntry.workerStart || navigationEntry.fetchStart) -
+ activationStart,
+ 0,
+ );
+ const dnsStart = Math.max(
+ navigationEntry.domainLookupStart - activationStart,
+ 0,
+ );
+ const connectStart = Math.max(
+ navigationEntry.connectStart - activationStart,
+ 0,
+ );
+ const connectEnd = Math.max(
+ navigationEntry.connectEnd - activationStart,
+ 0,
+ );
+
+ attribution = {
+ waitingDuration: waitEnd,
+ cacheDuration: dnsStart - waitEnd,
+ // dnsEnd usually equals connectStart but use connectStart over dnsEnd
+ // for dnsDuration in case there ever is a gap.
+ dnsDuration: connectStart - dnsStart,
+ connectionDuration: connectEnd - connectStart,
+ // There is often a gap between connectEnd and requestStart. Attribute
+ // that to requestDuration so connectionDuration remains 0 for
+ // service worker controlled requests were connectStart and connectEnd
+ // are the same.
+ requestDuration: metric.value - connectEnd,
+ navigationEntry: navigationEntry,
+ };
+ }
+
+ // Use Object.assign to set property to keep tsc happy.
+ const metricWithAttribution: TTFBMetricWithAttribution = Object.assign(
+ metric,
+ {attribution},
+ );
+ return metricWithAttribution;
+};
+
+/**
+ * Calculates the [TTFB](https://web.dev/articles/ttfb) value for the
+ * current page and calls the `callback` function once the page has loaded,
+ * along with the relevant `navigation` performance entry used to determine the
+ * value. The reported value is a `DOMHighResTimeStamp`.
+ *
+ * Note, this function waits until after the page is loaded to call `callback`
+ * in order to ensure all properties of the `navigation` entry are populated.
+ * This is useful if you want to report on other metrics exposed by the
+ * [Navigation Timing API](https://w3c.github.io/navigation-timing/). For
+ * example, the TTFB metric starts from the page's [time
+ * origin](https://www.w3.org/TR/hr-time-2/#sec-time-origin), which means it
+ * includes time spent on DNS lookup, connection negotiation, network latency,
+ * and server processing time.
+ */
+export const onTTFB = (
+ onReport: (metric: TTFBMetricWithAttribution) => void,
+ opts?: ReportOpts,
+) => {
+ unattributedOnTTFB((metric: TTFBMetric) => {
+ const metricWithAttribution = attributeTTFB(metric);
+ onReport(metricWithAttribution);
+ }, opts);
+};