ui: add network batcher for 1.21
This commit is contained in:
parent
e4ae3c8ba4
commit
6ffc74e0d1
4 changed files with 594 additions and 280 deletions
|
|
@ -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 (
|
||||
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
|
||||
|
|
@ -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 = (
|
||||
<ul>
|
||||
{showTransferred && (
|
||||
<li>{`${formatBytes(
|
||||
r.encodedBodySize + headerSize
|
||||
)} transferred over network`}</li>
|
||||
<li>
|
||||
{`${formatBytes(
|
||||
r.encodedBodySize + headerSize,
|
||||
)} transferred over network`}
|
||||
</li>
|
||||
)}
|
||||
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
|
||||
<li>{`${resSizeStr}: ${formatBytes(r.decodedBodySize)} `}</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
@ -136,16 +157,11 @@ function renderStatus({
|
|||
}) {
|
||||
const displayedStatus = error ? (
|
||||
<Tooltip title={error}>
|
||||
<div
|
||||
style={{ width: 90 }}
|
||||
className={'overflow-hidden overflow-ellipsis'}
|
||||
>
|
||||
<div style={{ width: 90 }} className="overflow-hidden overflow-ellipsis">
|
||||
{error}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
status
|
||||
);
|
||||
) : (status);
|
||||
return (
|
||||
<>
|
||||
{cached ? (
|
||||
|
|
@ -156,17 +172,19 @@ function renderStatus({
|
|||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
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 (
|
||||
<NetworkPanelComp
|
||||
loadTime={loadTime}
|
||||
|
|
@ -226,8 +241,8 @@ function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
|||
resourceListNow={resourceListNow}
|
||||
player={player}
|
||||
startedAt={startedAt}
|
||||
websocketList={websocketList as WSMessage[]}
|
||||
websocketListNow={websocketListNow as WSMessage[]}
|
||||
websocketList={websocketList}
|
||||
websocketListNow={websocketListNow}
|
||||
getTabNum={getTabNum}
|
||||
getTabName={getTabName}
|
||||
showSingleTab={showSingleTab}
|
||||
|
|
@ -238,7 +253,7 @@ function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
|||
function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||
const { player, store } = React.useContext(MobilePlayerContext);
|
||||
const { uiPlayerStore, sessionStore } = useStore();
|
||||
const startedAt = sessionStore.current.startedAt;
|
||||
const { startedAt } = sessionStore.current;
|
||||
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
|
||||
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
|
||||
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
|
||||
|
|
@ -267,9 +282,7 @@ function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
|||
resourceListNow={resourceListNow}
|
||||
player={player}
|
||||
startedAt={startedAt}
|
||||
// @ts-ignore
|
||||
websocketList={websocketList}
|
||||
// @ts-ignore
|
||||
websocketListNow={websocketListNow}
|
||||
zoomEnabled={zoomEnabled}
|
||||
zoomStartTs={zoomStartTs}
|
||||
|
|
@ -278,12 +291,35 @@ function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
type WSMessage = Timed & {
|
||||
channelName: string;
|
||||
data: string;
|
||||
timestamp: number;
|
||||
dir: 'up' | 'down';
|
||||
messageType: string;
|
||||
const useInfiniteScroll = (loadMoreCallback: () => void, hasMore: boolean) => {
|
||||
const observerRef = useRef<IntersectionObserver>(null);
|
||||
const loadingRef = useRef<HTMLDivElement>(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<WSMessage>;
|
||||
websocketListNow: Array<WSMessage>;
|
||||
websocketList: Array<WsChannel>;
|
||||
websocketListNow: Array<WsChannel>;
|
||||
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<string, any>) => {
|
||||
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<HTMLInputElement>) =>
|
||||
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<string, any>) => (
|
||||
<Tooltip title={`${getTabName?.(r.tabId) ?? `Tab ${getTabNum?.(r.tabId) ?? 0}`}`} placement="left">
|
||||
<Tooltip
|
||||
title={`${getTabName?.(r.tabId) ?? `Tab ${getTabNum?.(r.tabId) ?? 0}`}`}
|
||||
placement="left"
|
||||
>
|
||||
<div className="bg-gray-light rounded-full min-w-5 min-h-5 w-5 h-5 flex items-center justify-center text-xs cursor-default">
|
||||
{getTabNum?.(r.tabId) ?? 0}
|
||||
</div>
|
||||
|
|
@ -579,7 +683,7 @@ export const NetworkPanelComp = observer(
|
|||
});
|
||||
}
|
||||
return cols;
|
||||
}, [showSingleTab]);
|
||||
}, [showSingleTab, activeTab, getTabName, getTabNum, isSpot]);
|
||||
|
||||
return (
|
||||
<BottomBlock
|
||||
|
|
@ -591,7 +695,7 @@ export const NetworkPanelComp = observer(
|
|||
<BottomBlock.Header onClose={onClose}>
|
||||
<div className="flex items-center">
|
||||
<span className="font-semibold color-gray-medium mr-4">
|
||||
Network
|
||||
{'Network'}
|
||||
</span>
|
||||
{isMobile ? null : (
|
||||
<Tabs
|
||||
|
|
@ -603,7 +707,7 @@ export const NetworkPanelComp = observer(
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div className="flex items-center gap-2">
|
||||
{!isMobile && !isSpot ? <TabSelector /> : null}
|
||||
<Input
|
||||
className="rounded-lg"
|
||||
|
|
@ -611,7 +715,7 @@ export const NetworkPanelComp = observer(
|
|||
name="filter"
|
||||
onChange={onFilterChange}
|
||||
width={280}
|
||||
value={filter}
|
||||
value={inputFilterValue}
|
||||
size="small"
|
||||
prefix={<SearchOutlined className="text-neutral-400" />}
|
||||
/>
|
||||
|
|
@ -619,7 +723,7 @@ export const NetworkPanelComp = observer(
|
|||
</BottomBlock.Header>
|
||||
<BottomBlock.Content>
|
||||
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<Form.Item name="show-errors-only" className="mb-0">
|
||||
<label
|
||||
style={{
|
||||
|
|
@ -636,21 +740,29 @@ export const NetworkPanelComp = observer(
|
|||
<span className="text-sm ms-2">4xx-5xx Only</span>
|
||||
</label>
|
||||
</Form.Item>
|
||||
|
||||
{isProcessing && (
|
||||
<span className="text-xs text-gray-500 ml-4">
|
||||
Processing data...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<InfoLine>
|
||||
<InfoLine.Point label={`${totalItems}`} value="requests" />
|
||||
<InfoLine.Point
|
||||
label={filteredList.length + ''}
|
||||
value=" requests"
|
||||
label={`${displayedItems.length}/${totalItems}`}
|
||||
value="displayed"
|
||||
display={displayedItems.length < totalItems}
|
||||
/>
|
||||
<InfoLine.Point
|
||||
label={formatBytes(transferredSize)}
|
||||
label={formatBytes(summaryStats.transferredSize)}
|
||||
value="transferred"
|
||||
display={transferredSize > 0}
|
||||
display={summaryStats.transferredSize > 0}
|
||||
/>
|
||||
<InfoLine.Point
|
||||
label={formatBytes(resourcesSize)}
|
||||
label={formatBytes(summaryStats.resourcesSize)}
|
||||
value="resources"
|
||||
display={resourcesSize > 0}
|
||||
display={summaryStats.resourcesSize > 0}
|
||||
/>
|
||||
<InfoLine.Point
|
||||
label={formatMs(domBuildingTime)}
|
||||
|
|
@ -673,50 +785,74 @@ export const NetworkPanelComp = observer(
|
|||
/>
|
||||
</InfoLine>
|
||||
</div>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center gap-2">
|
||||
<InfoCircleOutlined size={18} />
|
||||
No Data
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-2"></div>
|
||||
<p>Processing initial network data...</p>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={filteredList.length === 0}
|
||||
>
|
||||
{/*@ts-ignore*/}
|
||||
<TimeTable
|
||||
rows={filteredList}
|
||||
tableHeight={panelHeight - 102}
|
||||
referenceLines={referenceLines}
|
||||
renderPopup
|
||||
onRowClick={showDetailsModal}
|
||||
sortBy={'time'}
|
||||
sortAscending
|
||||
onJump={(row: any) => {
|
||||
devTools.update(INDEX_KEY, {
|
||||
index: filteredList.indexOf(row),
|
||||
});
|
||||
player.jump(row.time);
|
||||
}}
|
||||
activeIndex={activeIndex}
|
||||
>
|
||||
{tableCols}
|
||||
</TimeTable>
|
||||
{selectedWsChannel ? (
|
||||
<WSPanel
|
||||
socketMsgList={selectedWsChannel}
|
||||
onClose={() => setSelectedWsChannel(null)}
|
||||
/>
|
||||
) : null}
|
||||
</NoContent>
|
||||
</div>
|
||||
) : (
|
||||
<NoContent
|
||||
title={
|
||||
<div className="capitalize flex items-center gap-2">
|
||||
<InfoCircleOutlined size={18} />
|
||||
{'No Data'}
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={displayedItems.length === 0}
|
||||
>
|
||||
<div>
|
||||
<TimeTable
|
||||
rows={displayedItems}
|
||||
tableHeight={panelHeight - 102 - (hasMoreItems ? 30 : 0)}
|
||||
referenceLines={referenceLines}
|
||||
renderPopup
|
||||
onRowClick={showDetailsModal}
|
||||
sortBy="time"
|
||||
sortAscending
|
||||
onJump={(row) => {
|
||||
devTools.update(INDEX_KEY, {
|
||||
index: displayedItems.indexOf(row),
|
||||
});
|
||||
player.jump(row.time);
|
||||
}}
|
||||
activeIndex={activeIndex}
|
||||
>
|
||||
{tableCols}
|
||||
</TimeTable>
|
||||
|
||||
{hasMoreItems && (
|
||||
<div
|
||||
ref={loadingRef}
|
||||
className="flex justify-center items-center text-xs text-gray-500"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-2"></div>
|
||||
Loading more data ({totalItems - displayedItems.length}{' '}
|
||||
remaining)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedWsChannel ? (
|
||||
<WSPanel
|
||||
socketMsgList={selectedWsChannel}
|
||||
onClose={() => setSelectedWsChannel(null)}
|
||||
/>
|
||||
) : null}
|
||||
</NoContent>
|
||||
)}
|
||||
</BottomBlock.Content>
|
||||
</BottomBlock>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const WebNetworkPanel = observer(NetworkPanelCont);
|
||||
|
||||
const MobileNetworkPanel = observer(MobileNetworkPanelCont);
|
||||
|
||||
export { WebNetworkPanel, MobileNetworkPanel };
|
||||
175
frontend/app/components/shared/DevTools/NetworkPanel/utils.ts
Normal file
175
frontend/app/components/shared/DevTools/NetworkPanel/utils.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
export function mergeListsWithZoom<
|
||||
T extends Record<string, any>,
|
||||
Y extends Record<string, any>,
|
||||
Z extends Record<string, any>,
|
||||
>(
|
||||
arr1: T[],
|
||||
arr2: Y[],
|
||||
arr3: Z[],
|
||||
zoom?: { enabled: boolean; start: number; end: number },
|
||||
): Array<T | Y | Z> {
|
||||
// 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<T extends Record<string, any>>(
|
||||
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<string, any>,
|
||||
Y extends Record<string, any>,
|
||||
Z extends Record<string, any>,
|
||||
>(arr1: T[], arr2: Y[], arr3: Z[]): Array<T | Y | Z> {
|
||||
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<string, any>,
|
||||
Y extends Record<string, any>,
|
||||
Z extends Record<string, any>,
|
||||
>(
|
||||
arr1: T[],
|
||||
arr2: Y[],
|
||||
arr3: Z[],
|
||||
startIdx1: number,
|
||||
startIdx2: number,
|
||||
startIdx3: number,
|
||||
start: number,
|
||||
end: number,
|
||||
): Array<T | Y | Z> {
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue