diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index 2ad92eb8a..a4aa637e3 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -1,8 +1,16 @@ +/* eslint-disable i18next/no-literal-string */ import { ResourceType, Timed } from 'Player'; +import { WsChannel } from 'Player/web/messages'; import MobilePlayer from 'Player/mobile/IOSPlayer'; import WebPlayer from 'Player/web/WebPlayer'; import { observer } from 'mobx-react-lite'; -import React, { useMemo, useState } from 'react'; +import React, { + useMemo, + useState, + useEffect, + useCallback, + useRef, +} from 'react'; import { useModal } from 'App/components/Modal'; import { @@ -11,24 +19,26 @@ import { } from 'App/components/Session/playerContext'; import { formatMs } from 'App/date'; import { useStore } from 'App/mstore'; -import { formatBytes } from 'App/utils'; +import { formatBytes, debounceCall } from 'App/utils'; import { Icon, NoContent, Tabs } from 'UI'; import { Tooltip, Input, Switch, Form } from 'antd'; -import { SearchOutlined, InfoCircleOutlined } from '@ant-design/icons'; +import { + SearchOutlined, + InfoCircleOutlined, +} from '@ant-design/icons'; import FetchDetailsModal from 'Shared/FetchDetailsModal'; -import { WsChannel } from 'App/player/web/messages'; import BottomBlock from '../BottomBlock'; import InfoLine from '../BottomBlock/InfoLine'; import TabSelector from '../TabSelector'; import TimeTable from '../TimeTable'; import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; -import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'; import WSPanel from './WSPanel'; +import { mergeListsWithZoom, processInChunks } from './utils' +// Constants remain the same const INDEX_KEY = 'network'; - const ALL = 'ALL'; const XHR = 'xhr'; const JS = 'js'; @@ -37,6 +47,7 @@ const IMG = 'img'; const MEDIA = 'media'; const OTHER = 'other'; const WS = 'websocket'; +const GRAPHQL = 'graphql'; const TYPE_TO_TAB = { [ResourceType.XHR]: XHR, @@ -47,9 +58,10 @@ const TYPE_TO_TAB = { [ResourceType.MEDIA]: MEDIA, [ResourceType.WS]: WS, [ResourceType.OTHER]: OTHER, + [ResourceType.GRAPHQL]: GRAPHQL, }; -const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER, WS] as const; +const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER, WS, GRAPHQL] as const; export const NETWORK_TABS = TAP_KEYS.map((tab) => ({ text: tab === 'xhr' ? 'Fetch/XHR' : tab, key: tab, @@ -58,6 +70,9 @@ export const NETWORK_TABS = TAP_KEYS.map((tab) => ({ const DOM_LOADED_TIME_COLOR = 'teal'; const LOAD_TIME_COLOR = 'red'; +const BATCH_SIZE = 2500; +const INITIAL_LOAD_SIZE = 5000; + export function renderType(r: any) { return ( {r.type}}> @@ -75,12 +90,16 @@ export function renderName(r: any) { } function renderSize(r: any) { - if (r.responseBodySize) return formatBytes(r.responseBodySize); + const notCaptured = 'Not captured'; + const resSizeStr = 'Resource size' let triggerText; let content; - if (r.decodedBodySize == null || r.decodedBodySize === 0) { + if (r.responseBodySize) { + triggerText = formatBytes(r.responseBodySize); + content = undefined; + } else if (r.decodedBodySize == null || r.decodedBodySize === 0) { triggerText = 'x'; - content = 'Not captured'; + content = notCaptured; } else { const headerSize = r.headerSize || 0; const showTransferred = r.headerSize != null; @@ -89,11 +108,13 @@ function renderSize(r: any) { content = ( ); } @@ -136,16 +157,11 @@ function renderStatus({ }) { const displayedStatus = error ? ( -
+
{error}
- ) : ( - status - ); + ) : (status); return ( <> {cached ? ( @@ -156,17 +172,19 @@ function renderStatus({
) : ( - displayedStatus - )} + displayedStatus + )} ); } + +// Main component for Network Panel function NetworkPanelCont({ panelHeight }: { panelHeight: number }) { const { player, store } = React.useContext(PlayerContext); const { sessionStore, uiPlayerStore } = useStore(); - const startedAt = sessionStore.current.startedAt; + const { startedAt } = sessionStore.current; const { domContentLoadedTime, loadTime, @@ -177,43 +195,40 @@ function NetworkPanelCont({ panelHeight }: { panelHeight: number }) { } = store.get(); const tabsArr = Object.keys(tabStates); const tabValues = Object.values(tabStates); - const dataSource = uiPlayerStore.dataSource; + const { dataSource } = uiPlayerStore; const showSingleTab = dataSource === 'current'; - const { - fetchList = [], - resourceList = [], - fetchListNow = [], - resourceListNow = [], - websocketList = [], - websocketListNow = [], - } = React.useMemo(() => { - if (showSingleTab) { - return tabStates[currentTab] ?? {}; - } else { - const fetchList = tabValues.flatMap((tab) => tab.fetchList); - const resourceList = tabValues.flatMap((tab) => tab.resourceList); - const fetchListNow = tabValues - .flatMap((tab) => tab.fetchListNow) - .filter(Boolean); - const resourceListNow = tabValues - .flatMap((tab) => tab.resourceListNow) - .filter(Boolean); - const websocketList = tabValues.flatMap((tab) => tab.websocketList); - const websocketListNow = tabValues - .flatMap((tab) => tab.websocketListNow) - .filter(Boolean); - return { - fetchList, - resourceList, - fetchListNow, - resourceListNow, - websocketList, - websocketListNow, - }; - } - }, [currentTab, tabStates, dataSource, tabValues]); + + let fetchList = []; + let resourceList = []; + let fetchListNow = []; + let resourceListNow = []; + let websocketList = []; + let websocketListNow = []; + + if (showSingleTab) { + const state = tabStates[currentTab] ?? {}; + fetchList = state.fetchList ?? []; + resourceList = state.resourceList ?? []; + fetchListNow = state.fetchListNow ?? []; + resourceListNow = state.resourceListNow ?? []; + websocketList = state.websocketList ?? []; + websocketListNow = state.websocketListNow ?? []; + } else { + fetchList = tabValues.flatMap((tab) => tab.fetchList); + resourceList = tabValues.flatMap((tab) => tab.resourceList); + fetchListNow = tabValues.flatMap((tab) => tab.fetchListNow).filter(Boolean); + resourceListNow = tabValues + .flatMap((tab) => tab.resourceListNow) + .filter(Boolean); + websocketList = tabValues.flatMap((tab) => tab.websocketList); + websocketListNow = tabValues + .flatMap((tab) => tab.websocketListNow) + .filter(Boolean); + } + const getTabNum = (tab: string) => tabsArr.findIndex((t) => t === tab) + 1; - const getTabName = (tabId: string) => tabNames[tabId] + const getTabName = (tabId: string) => tabNames[tabId]; + return ( void, hasMore: boolean) => { + const observerRef = useRef(null); + const loadingRef = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting && hasMore) { + loadMoreCallback(); + } + }, + { threshold: 0.1 }, + ); + + if (loadingRef.current) { + observer.observe(loadingRef.current); + } + + // @ts-ignore + observerRef.current = observer; + + return () => { + if (observerRef.current) { + observerRef.current.disconnect(); + } + }; + }, [loadMoreCallback, hasMore, loadingRef]); + + return loadingRef; }; interface Props { @@ -300,8 +336,8 @@ interface Props { resourceList: Timed[]; fetchListNow: Timed[]; resourceListNow: Timed[]; - websocketList: Array; - websocketListNow: Array; + websocketList: Array; + websocketListNow: Array; player: WebPlayer | MobilePlayer; startedAt: number; isMobile?: boolean; @@ -346,112 +382,195 @@ export const NetworkPanelComp = observer( >(null); const { showModal } = useModal(); const [showOnlyErrors, setShowOnlyErrors] = useState(false); - const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isProcessing, setIsProcessing] = useState(false); + const [displayedItems, setDisplayedItems] = useState([]); + const [totalItems, setTotalItems] = useState(0); + const [summaryStats, setSummaryStats] = useState({ + resourcesSize: 0, + transferredSize: 0, + }); + + // Store original data in refs to avoid reprocessing + const originalListRef = useRef([]); + const socketListRef = useRef([]); + const { sessionStore: { devTools }, } = useStore(); - const filter = devTools[INDEX_KEY].filter; - const activeTab = devTools[INDEX_KEY].activeTab; + const { filter } = devTools[INDEX_KEY]; + const { activeTab } = devTools[INDEX_KEY]; const activeIndex = activeOutsideIndex ?? devTools[INDEX_KEY].index; + const [inputFilterValue, setInputFilterValue] = useState(filter); - const socketList = useMemo( - () => - websocketList.filter( - (ws, i, arr) => - arr.findIndex((it) => it.channelName === ws.channelName) === i - ), - [websocketList] + const debouncedFilter = useCallback( + debounceCall((filterValue) => { + devTools.update(INDEX_KEY, { filter: filterValue }); + }, 300), + [], ); - const list = useMemo( - () => - // TODO: better merge (with body size info) - do it in player - resourceList - .filter( - (res) => - !fetchList.some((ft) => { - // res.url !== ft.url doesn't work on relative URLs appearing within fetchList (to-fix in player) - if (res.name === ft.name) { - if (res.time === ft.time) return true; - if (res.url.includes(ft.url)) { - return ( - Math.abs(res.time - ft.time) < 350 || - Math.abs(res.timestamp - ft.timestamp) < 350 - ); - } - } - - if (res.name !== ft.name) { - return false; - } - if (Math.abs(res.time - ft.time) > 250) { - return false; - } // TODO: find good epsilons - if (Math.abs(res.duration - ft.duration) > 200) { - return false; - } - - return true; - }) - ) - .concat(fetchList) - .concat( - socketList.map((ws) => ({ - ...ws, - type: 'websocket', - method: 'ws', - url: ws.channelName, - name: ws.channelName, - status: '101', - duration: 0, - transferredBodySize: 0, - })) - ) - .filter((req) => - zoomEnabled - ? req.time >= zoomStartTs! && req.time <= zoomEndTs! - : true - ) - .sort((a, b) => a.time - b.time), - [resourceList.length, fetchList.length, socketList] - ); - - let filteredList = useMemo(() => { - if (!showOnlyErrors) { - return list; - } - return list.filter( - (it) => parseInt(it.status) >= 400 || !it.success || it.error + // Process socket lists once + useEffect(() => { + const uniqueSocketList = websocketList.filter( + (ws, i, arr) => + arr.findIndex((it) => it.channelName === ws.channelName) === i, ); - }, [showOnlyErrors, list]); - filteredList = useRegExListFilterMemo( - filteredList, - (it) => [it.status, it.name, it.type, it.method], - filter - ); - filteredList = useTabListFilterMemo( - filteredList, - (it) => TYPE_TO_TAB[it.type], - ALL, - activeTab - ); + socketListRef.current = uniqueSocketList; + }, [websocketList.length]); - const onTabClick = (activeTab: (typeof TAP_KEYS)[number]) => + // Initial data processing - do this only once when data changes + useEffect(() => { + setIsLoading(true); + + // Heaviest operation here, will create a final merged network list + const processData = async () => { + const fetchUrls = new Set( + fetchList.map((ft) => { + return `${ft.name}-${Math.floor(ft.time / 100)}-${Math.floor(ft.duration / 100)}`; + }), + ); + + // We want to get resources that aren't in fetch list + const filteredResources = await processInChunks(resourceList, (chunk) => + chunk.filter((res: any) => { + const key = `${res.name}-${Math.floor(res.time / 100)}-${Math.floor(res.duration / 100)}`; + return !fetchUrls.has(key); + }), + BATCH_SIZE, + 25, + ); + + const processedSockets = socketListRef.current.map((ws: any) => ({ + ...ws, + type: 'websocket', + method: 'ws', + url: ws.channelName, + name: ws.channelName, + status: '101', + duration: 0, + transferredBodySize: 0, + })); + + const mergedList: Timed[] = mergeListsWithZoom( + filteredResources as Timed[], + fetchList, + processedSockets as Timed[], + { enabled: Boolean(zoomEnabled), start: zoomStartTs ?? 0, end: zoomEndTs ?? 0 } + ) + + originalListRef.current = mergedList; + setTotalItems(mergedList.length); + + calculateResourceStats(resourceList); + + // Only display initial chunk + setDisplayedItems(mergedList.slice(0, INITIAL_LOAD_SIZE)); + setIsLoading(false); + }; + + void processData(); + }, [ + resourceList.length, + fetchList.length, + socketListRef.current.length, + zoomEnabled, + zoomStartTs, + zoomEndTs, + ]); + + const calculateResourceStats = (resourceList: Record) => { + setTimeout(() => { + let resourcesSize = 0 + let transferredSize = 0 + resourceList.forEach(({ decodedBodySize, headerSize, encodedBodySize }: any) => { + resourcesSize += decodedBodySize || 0 + transferredSize += (headerSize || 0) + (encodedBodySize || 0) + }) + + setSummaryStats({ + resourcesSize, + transferredSize, + }); + }, 0); + } + + useEffect(() => { + if (originalListRef.current.length === 0) return; + setIsProcessing(true); + const applyFilters = async () => { + let filteredItems: any[] = originalListRef.current; + + filteredItems = await processInChunks(filteredItems, (chunk) => + chunk.filter( + (it) => { + let valid = true; + if (showOnlyErrors) { + valid = parseInt(it.status) >= 400 || !it.success || it.error + } + if (filter) { + try { + const regex = new RegExp(filter, 'i'); + valid = valid && regex.test(it.status) || regex.test(it.name) || regex.test(it.type) || regex.test(it.method); + } catch (e) { + valid = valid && String(it.status).includes(filter) || it.name.includes(filter) || it.type.includes(filter) || (it.method && it.method.includes(filter)); + } + } + if (activeTab !== ALL) { + valid = valid && TYPE_TO_TAB[it.type] === activeTab; + } + + return valid; + }, + ), + ); + + // Update displayed items + setDisplayedItems(filteredItems.slice(0, INITIAL_LOAD_SIZE)); + setTotalItems(filteredItems.length); + setIsProcessing(false); + }; + + void applyFilters(); + }, [filter, activeTab, showOnlyErrors]); + + const loadMoreItems = useCallback(() => { + if (isProcessing) return; + + setIsProcessing(true); + setTimeout(() => { + setDisplayedItems((prevItems) => { + const currentLength = prevItems.length; + const newItems = originalListRef.current.slice( + currentLength, + currentLength + BATCH_SIZE, + ); + return [...prevItems, ...newItems]; + }); + setIsProcessing(false); + }, 10); + }, [isProcessing]); + + const hasMoreItems = displayedItems.length < totalItems; + const loadingRef = useInfiniteScroll(loadMoreItems, hasMoreItems); + + const onTabClick = (activeTab) => { devTools.update(INDEX_KEY, { activeTab }); - const onFilterChange = ({ - target: { value }, - }: React.ChangeEvent) => - devTools.update(INDEX_KEY, { filter: value }); + }; + + const onFilterChange = ({ target: { value } }) => { + setInputFilterValue(value) + debouncedFilter(value); + }; - // AutoScroll const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll( - filteredList, + displayedItems, getLastItemTime(fetchListNow, resourceListNow), activeIndex, - (index) => devTools.update(INDEX_KEY, { index }) + (index) => devTools.update(INDEX_KEY, { index }), ); - const onMouseEnter = stopAutoscroll; + const onMouseEnter = () => stopAutoscroll; const onMouseLeave = () => { if (isDetailsModalActive) { return; @@ -459,24 +578,6 @@ export const NetworkPanelComp = observer( timeoutStartAutoscroll(); }; - const resourcesSize = useMemo( - () => - resourceList.reduce( - (sum, { decodedBodySize }) => sum + (decodedBodySize || 0), - 0 - ), - [resourceList.length] - ); - const transferredSize = useMemo( - () => - resourceList.reduce( - (sum, { headerSize, encodedBodySize }) => - sum + (headerSize || 0) + (encodedBodySize || 0), - 0 - ), - [resourceList.length] - ); - const referenceLines = useMemo(() => { const arr = []; @@ -499,7 +600,7 @@ export const NetworkPanelComp = observer( const showDetailsModal = (item: any) => { if (item.type === 'websocket') { const socketMsgList = websocketList.filter( - (ws) => ws.channelName === item.channelName + (ws) => ws.channelName === item.channelName, ); return setSelectedWsChannel(socketMsgList); @@ -510,7 +611,7 @@ export const NetworkPanelComp = observer( isSpot={isSpot} time={item.time + startedAt} resource={item} - rows={filteredList} + rows={displayedItems} fetchPresented={fetchList.length > 0} />, { @@ -520,14 +621,14 @@ export const NetworkPanelComp = observer( setIsDetailsModalActive(false); timeoutStartAutoscroll(); }, - } + }, ); - devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) }); + devTools.update(INDEX_KEY, { index: displayedItems.indexOf(item) }); stopAutoscroll(); }; - const tableCols = React.useMemo(() => { - const cols: any[] = [ + const tableCols = useMemo(() => { + const cols = [ { label: 'Status', dataKey: 'status', @@ -570,7 +671,10 @@ export const NetworkPanelComp = observer( label: 'Source', width: 64, render: (r: Record) => ( - +
{getTabNum?.(r.tabId) ?? 0}
@@ -579,7 +683,7 @@ export const NetworkPanelComp = observer( }); } return cols; - }, [showSingleTab]); + }, [showSingleTab, activeTab, getTabName, getTabNum, isSpot]); return (
- Network + {'Network'} {isMobile ? null : ( )}
-
+
{!isMobile && !isSpot ? : null} } /> @@ -619,7 +723,7 @@ export const NetworkPanelComp = observer(
-
+
+ + {isProcessing && ( + + Processing data... + + )}
+ 0} + display={summaryStats.transferredSize > 0} /> 0} + display={summaryStats.resourcesSize > 0} />
- - - No Data + + {isLoading ? ( +
+
+
+

Processing initial network data...

- } - size="small" - show={filteredList.length === 0} - > - {/*@ts-ignore*/} - { - devTools.update(INDEX_KEY, { - index: filteredList.indexOf(row), - }); - player.jump(row.time); - }} - activeIndex={activeIndex} - > - {tableCols} - - {selectedWsChannel ? ( - setSelectedWsChannel(null)} - /> - ) : null} - +
+ ) : ( + + + {'No Data'} +
+ } + size="small" + show={displayedItems.length === 0} + > +
+ { + devTools.update(INDEX_KEY, { + index: displayedItems.indexOf(row), + }); + player.jump(row.time); + }} + activeIndex={activeIndex} + > + {tableCols} + + + {hasMoreItems && ( +
+
+
+ Loading more data ({totalItems - displayedItems.length}{' '} + remaining) +
+
+ )} +
+ + {selectedWsChannel ? ( + setSelectedWsChannel(null)} + /> + ) : null} + + )}
); - } + }, ); const WebNetworkPanel = observer(NetworkPanelCont); - const MobileNetworkPanel = observer(MobileNetworkPanelCont); -export { WebNetworkPanel, MobileNetworkPanel }; +export { WebNetworkPanel, MobileNetworkPanel }; \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/utils.ts b/frontend/app/components/shared/DevTools/NetworkPanel/utils.ts new file mode 100644 index 000000000..e189ebd6a --- /dev/null +++ b/frontend/app/components/shared/DevTools/NetworkPanel/utils.ts @@ -0,0 +1,175 @@ +export function mergeListsWithZoom< + T extends Record, + Y extends Record, + Z extends Record, +>( + arr1: T[], + arr2: Y[], + arr3: Z[], + zoom?: { enabled: boolean; start: number; end: number }, +): Array { + // Early return for empty arrays + if (arr1.length === 0 && arr2.length === 0 && arr3.length === 0) { + return []; + } + + if (!zoom?.enabled) { + return mergeThreeSortedArrays(arr1, arr2, arr3); + } + + // Binary search for start indexes (faster than linear search for large arrays) + const index1 = binarySearchStartIndex(arr1, zoom.start); + const index2 = binarySearchStartIndex(arr2, zoom.start); + const index3 = binarySearchStartIndex(arr3, zoom.start); + + // Merge arrays within zoom range + return mergeThreeSortedArraysWithinRange( + arr1, + arr2, + arr3, + index1, + index2, + index3, + zoom.start, + zoom.end, + ); +} + +function binarySearchStartIndex>( + arr: T[], + threshold: number, +): number { + if (arr.length === 0) return 0; + + let low = 0; + let high = arr.length - 1; + + // Handle edge cases first for better performance + if (arr[high].time < threshold) return arr.length; + if (arr[low].time >= threshold) return 0; + + while (low <= high) { + const mid = Math.floor((low + high) / 2); + + if (arr[mid].time < threshold) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + return low; +} + +function mergeThreeSortedArrays< + T extends Record, + Y extends Record, + Z extends Record, +>(arr1: T[], arr2: Y[], arr3: Z[]): Array { + const totalLength = arr1.length + arr2.length + arr3.length; + const result = new Array(totalLength); + + let i = 0, + j = 0, + k = 0, + index = 0; + + while (i < arr1.length || j < arr2.length || k < arr3.length) { + const val1 = i < arr1.length ? arr1[i].time : Infinity; + const val2 = j < arr2.length ? arr2[j].time : Infinity; + const val3 = k < arr3.length ? arr3[k].time : Infinity; + + if (val1 <= val2 && val1 <= val3) { + result[index++] = arr1[i++]; + } else if (val2 <= val1 && val2 <= val3) { + result[index++] = arr2[j++]; + } else { + result[index++] = arr3[k++]; + } + } + + return result; +} + +function mergeThreeSortedArraysWithinRange< + T extends Record, + Y extends Record, + Z extends Record, +>( + arr1: T[], + arr2: Y[], + arr3: Z[], + startIdx1: number, + startIdx2: number, + startIdx3: number, + start: number, + end: number, +): Array { + // we don't know beforehand how many items will be there + const result = []; + + let i = startIdx1; + let j = startIdx2; + let k = startIdx3; + + while (i < arr1.length || j < arr2.length || k < arr3.length) { + const val1 = i < arr1.length ? arr1[i].time : Infinity; + const val2 = j < arr2.length ? arr2[j].time : Infinity; + const val3 = k < arr3.length ? arr3[k].time : Infinity; + + // Early termination: if all remaining values exceed end time + if (Math.min(val1, val2, val3) > end) { + break; + } + + if (val1 <= val2 && val1 <= val3) { + if (val1 <= end) { + result.push(arr1[i]); + } + i++; + } else if (val2 <= val1 && val2 <= val3) { + if (val2 <= end) { + result.push(arr2[j]); + } + j++; + } else { + if (val3 <= end) { + result.push(arr3[k]); + } + k++; + } + } + + return result; +} + +export function processInChunks( + items: any[], + processFn: (item: any) => any, + chunkSize = 1000, + overscan = 0, +) { + return new Promise((resolve) => { + if (items.length === 0) { + resolve([]); + return; + } + + let result: any[] = []; + let index = 0; + + const processNextChunk = () => { + const chunk = items.slice(index, index + chunkSize + overscan); + result = result.concat(processFn(chunk)); + index += chunkSize; + + if (index < items.length) { + setTimeout(processNextChunk, 0); + } else { + resolve(result); + } + }; + + processNextChunk(); + }); +} diff --git a/frontend/app/utils/index.ts b/frontend/app/utils/index.ts index 03419fa72..a853e2f70 100644 --- a/frontend/app/utils/index.ts +++ b/frontend/app/utils/index.ts @@ -29,6 +29,15 @@ export function debounce(callback, wait, context = this) { }; } +export function debounceCall(func, wait) { + let timeout; + return function (...args) { + const context = this; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(context, args), wait); + }; +} + /* eslint-disable no-mixed-operators */ export function randomInt(a, b) { const min = (b ? a : 0) - 0.5; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f1b91f871..140989c55 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2952,67 +2952,61 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:^5.21.1": - version: 5.30.0 - resolution: "@sentry/browser@npm:5.30.0" +"@sentry-internal/browser-utils@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/browser-utils@npm:8.55.0" dependencies: - "@sentry/core": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/4787cc3ea90600b36b548a8403afb30f13e1e562dd426871879d824536c16005d0734b7406498f1a6dd4fa7e0a49808e17a1c2c24750430ba7f86f909a9eb95a + "@sentry/core": "npm:8.55.0" + checksum: 10c1/67fdc5ec9c8bc6c8eeda4598332a7937a8c7d6cc1cadb05a886323f3d13c25def7b9258ad4b834919dea5d612010de8900f5cf738e9a577a711c839f285557d7 languageName: node linkType: hard -"@sentry/core@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/core@npm:5.30.0" +"@sentry-internal/feedback@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/feedback@npm:8.55.0" dependencies: - "@sentry/hub": "npm:5.30.0" - "@sentry/minimal": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/5c6dcdccc48a9d6957af7745226eacd3d4926574593e852ccbad0fbaa71355879b9c4707c194e3d9b1ef389d98171a3d85d2c75636a5c6d1cc3c9950cd06334a + "@sentry/core": "npm:8.55.0" + checksum: 10c1/3f6fd3b8c2305b457a5c729c92b2a2335200e5ee0d5a210b513246e00ecda6d2a28940871ed88eee7f7bd8465571388698a7b789c8e0f3d5832ff3a0b040b514 languageName: node linkType: hard -"@sentry/hub@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/hub@npm:5.30.0" +"@sentry-internal/replay-canvas@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/replay-canvas@npm:8.55.0" dependencies: - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/28b86742c72427b5831ee3077c377d1f305d2eb080f7dc977e81b8f29e8eb0dfa07f129c1f5cda29bda9238fe50e292ab719119c4c5a5b7ef580a24bcb6356a3 + "@sentry-internal/replay": "npm:8.55.0" + "@sentry/core": "npm:8.55.0" + checksum: 10c1/48511881330193d754e01b842e3b2b931641d0954bac8a8f01503ff3d2aedc9f1779049be0a7a56ba35583769f0566381853c7656888c42f9f59224c6520e593 languageName: node linkType: hard -"@sentry/minimal@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/minimal@npm:5.30.0" +"@sentry-internal/replay@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/replay@npm:8.55.0" dependencies: - "@sentry/hub": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/d28ad14e43d3c5c06783288ace1fcf1474437070f04d1476b04d0288656351d9a6285cc66d346e8d84a3e73cf895944c06fa7c82bad93415831e4449e11f2d89 + "@sentry-internal/browser-utils": "npm:8.55.0" + "@sentry/core": "npm:8.55.0" + checksum: 10c1/d60b4261df037b4c82dafc6b25695b2be32f95a45cd25fc43c659d65644325238f7152f6222cd5d4f3f52407c3f5ad67ea30b38fea27c9422536f8aaba6b0048 languageName: node linkType: hard -"@sentry/types@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/types@npm:5.30.0" - checksum: 10c1/07fe7f04f6aae13f037761fe56a20e06fa4a768bf024fb81970d3087ab9ab5b45bd85b9081945ef5019d93b7de742918374a0e7b70a992dbb29a5078982ddfd9 +"@sentry/browser@npm:^8.34.0": + version: 8.55.0 + resolution: "@sentry/browser@npm:8.55.0" + dependencies: + "@sentry-internal/browser-utils": "npm:8.55.0" + "@sentry-internal/feedback": "npm:8.55.0" + "@sentry-internal/replay": "npm:8.55.0" + "@sentry-internal/replay-canvas": "npm:8.55.0" + "@sentry/core": "npm:8.55.0" + checksum: 10c1/3baf51a0b401bb63b345df480773d49b713dd557e15baf2c98f089612c9497aca6f2c7849b1c4d6ded6229d1de495e3305a0438145333de26c6ba190d261c039 languageName: node linkType: hard -"@sentry/utils@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/utils@npm:5.30.0" - dependencies: - "@sentry/types": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/311ad0be0e40af9f4ab7be2dfb8a782a779fa56700a0662f49ebcbff0dbbe4ea5dff690ad2c0ed4ecb6a6721a3066186b3c8f677fa302c5b606f86dfaa654de3 +"@sentry/core@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry/core@npm:8.55.0" + checksum: 10c1/fbb71058626214674c4b103160fea859ce1fcc83b26533920b2c4fc7d5169bde178b08cd46dad29fabaf616fa465db4274356c500a37f33888bdb8d10fda3d55 languageName: node linkType: hard @@ -11574,7 +11568,7 @@ __metadata: "@jest/globals": "npm:^29.7.0" "@medv/finder": "npm:^3.1.0" "@openreplay/sourcemap-uploader": "npm:^3.0.10" - "@sentry/browser": "npm:^5.21.1" + "@sentry/browser": "npm:^8.34.0" "@svg-maps/world": "npm:^1.0.1" "@tanstack/react-query": "npm:^5.56.2" "@trivago/prettier-plugin-sort-imports": "npm:^4.3.0" @@ -15762,7 +15756,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1, tslib@npm:^1.9.3": +"tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10c1/24ee51ea8fb127ca8ad30a25fdac22c5bb11a2b043781757ddde0daf2e03126e1e13e88ab1748d9c50f786a648b5b038e70782063fd15c3ad07ebec039df8f6f