openreplay/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx
Delirium 35461acaf3
[WIP] Mobile replayer (#1452)
* fix(ui): fix up mobile recordings display

* fix(ui): some messages

* fix(ui): some messages

* fix(player): fix msg generation for ios messages

* feat(player): add generic mmanager interface for ios player impl

* feat(player): mobile player and message manager; touch manager; videoplayer

* feat(player): add iphone shells, add log panel,

* feat(player): detect ios sessions and inject correct player

* feat(player): move screen mapper to utils

* feat(player): events panel for mobile, map shell sizes to device type data,

* feat(player): added network tab to mobile player; unify network message (ios and web)

* feat(player): resize canvas up to phone screen size, fix capitalize util

* feat(player): some general changes to support mobile events and network entries

* feat(player): remove swipes from timeline

* feat(player): more stuff for list walker

* fix(ui): performance tab, mobile project typings and form

* fix(ui):some ui touches for ios replayer shell

* fix(ui): more fixes for ui, new onboarding screen for mobile projects

* feat(ui): mobile overview panel (xray)

* feat(ui): fixes for phone shell and tap events

* fix(tracker): change phone shells and sizes

* fix(tracker): fix border on replay screen

* feat(ui): use crashes from db to show in session

* feat(ui): use event name for xray

* feat(ui): some overall ui fixes

* feat(ui): IOS -> iOS

* feat(ui): change tags to ant d

* fix(ui): fast fix

* fix(ui): fix for capitalizer

* fix(ui): fix for browser display

* fix(ui): fix for note popup

* fix(ui): change exceptions display

* fix(ui): add click rage to ios xray

* fix(ui): some icons and resizing

* fix(ui): fix ios context menu overlay, fix console logs creation for ios

* feat(ui): added icons

* feat(ui): performance warnings

* feat(ui): performance warnings

* feat(ui): different styles

* feat(ui): rm debug true

* feat(ui): fix warnings display

* feat(ui): some styles for animation

* feat(ui): add some animations to warnings

* feat(ui): move perf warnings to performance graph

* feat(ui): hide/show warns dynamically

* feat(ui): new mobile touch animation

* fix(tracker): update msg for ios

* fix(ui): taprage fixes

* fix(ui): regenerate icons and messages

* fix(ui): fix msgs

* fix(backend): fix events gen

* fix(backend): fix userid msg
2023-10-27 12:12:09 +02:00

525 lines
No EOL
14 KiB
TypeScript

import WebPlayer from 'Player/web/WebPlayer';
import MobilePlayer from 'Player/mobile/IOSPlayer';
import React, { useMemo, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { Duration } from 'luxon';
import { Tooltip, Tabs, Input, NoContent, Icon, Toggler } from 'UI';
import { ResourceType, Timed } from 'Player';
import { formatBytes } from 'App/utils';
import { formatMs } from 'App/date';
import { useModal } from 'App/components/Modal';
import FetchDetailsModal from 'Shared/FetchDetailsModal';
import { MobilePlayerContext, PlayerContext } from 'App/components/Session/playerContext';
import { useStore } from 'App/mstore';
import { connect } from 'react-redux';
import TimeTable from '../TimeTable';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
const INDEX_KEY = 'network';
const ALL = 'ALL';
const XHR = 'xhr';
const JS = 'js';
const CSS = 'css';
const IMG = 'img';
const MEDIA = 'media';
const OTHER = 'other';
const TYPE_TO_TAB = {
[ResourceType.XHR]: XHR,
[ResourceType.FETCH]: XHR,
[ResourceType.SCRIPT]: JS,
[ResourceType.CSS]: CSS,
[ResourceType.IMG]: IMG,
[ResourceType.MEDIA]: MEDIA,
[ResourceType.OTHER]: OTHER,
};
const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER] as const;
const TABS = TAP_KEYS.map((tab) => ({
text: tab === 'xhr' ? 'Fetch/XHR' : tab,
key: tab,
}));
const DOM_LOADED_TIME_COLOR = 'teal';
const LOAD_TIME_COLOR = 'red';
function compare(a: any, b: any, key: string) {
if (a[key] > b[key]) return 1;
if (a[key] < b[key]) return -1;
return 0;
}
export function renderType(r: any) {
return (
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
<div>{r.type}</div>
</Tooltip>
);
}
export function renderName(r: any) {
return (
<Tooltip style={{ width: '100%' }} title={<div>{r.url}</div>}>
<div>{r.name}</div>
</Tooltip>
);
}
export function renderStart(r: any) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
</div>
);
}
function renderSize(r: any) {
if (r.responseBodySize) return formatBytes(r.responseBodySize);
let triggerText;
let content;
if (r.decodedBodySize == null || r.decodedBodySize === 0) {
triggerText = 'x';
content = 'Not captured';
} else {
const headerSize = r.headerSize || 0;
const showTransferred = r.headerSize != null;
triggerText = formatBytes(r.decodedBodySize);
content = (
<ul>
{showTransferred && (
<li>{`${formatBytes(r.encodedBodySize + headerSize)} transferred over network`}</li>
)}
<li>{`Resource size: ${formatBytes(r.decodedBodySize)} `}</li>
</ul>
);
}
return (
<Tooltip style={{ width: '100%' }} title={content}>
<div>{triggerText}</div>
</Tooltip>
);
}
export function renderDuration(r: any) {
if (!r.success) return 'x';
const text = `${Math.floor(r.duration)}ms`;
if (!r.isRed && !r.isYellow) return text;
let tooltipText;
let className = 'w-full h-full flex items-center ';
if (r.isYellow) {
tooltipText = 'Slower than average';
className += 'warn color-orange';
} else {
tooltipText = 'Much slower than average';
className += 'error color-red';
}
return (
<Tooltip style={{ width: '100%' }} title={tooltipText}>
<div> {text} </div>
</Tooltip>
);
}
function renderStatus({ status, cached }: { status: string; cached: boolean }) {
return (
<>
{cached ? (
<Tooltip title={'Served from cache'}>
<div className="flex items-center">
<span className="mr-1">{status}</span>
<Icon name="wifi" size={16} />
</div>
</Tooltip>
) : (
status
)}
</>
);
}
function NetworkPanelCont({ startedAt }: { startedAt: number }) {
const { player, store } = React.useContext(PlayerContext);
const { domContentLoadedTime, loadTime, domBuildingTime, tabStates, currentTab } = store.get();
const {
fetchList = [],
resourceList = [],
fetchListNow = [],
resourceListNow = [],
} = tabStates[currentTab];
return (
<NetworkPanelComp
loadTime={loadTime}
domBuildingTime={domBuildingTime}
domContentLoadedTime={domContentLoadedTime}
fetchList={fetchList}
resourceList={resourceList}
fetchListNow={fetchListNow}
resourceListNow={resourceListNow}
player={player}
startedAt={startedAt}
/>
);
}
function MobileNetworkPanelCont({ startedAt }: { startedAt: number }) {
const { player, store } = React.useContext(MobilePlayerContext);
const domContentLoadedTime = undefined;
const loadTime = undefined;
const domBuildingTime = undefined;
const {
fetchList = [],
resourceList = [],
fetchListNow = [],
resourceListNow = [],
} = store.get();
return (
<NetworkPanelComp
isMobile
loadTime={loadTime}
domBuildingTime={domBuildingTime}
domContentLoadedTime={domContentLoadedTime}
fetchList={fetchList}
resourceList={resourceList}
fetchListNow={fetchListNow}
resourceListNow={resourceListNow}
player={player}
startedAt={startedAt}
/>
);
}
interface Props {
domContentLoadedTime?: {
time: number;
value: number;
};
loadTime?: {
time: number;
value: number;
};
domBuildingTime?: number;
fetchList: Timed[];
resourceList: Timed[];
fetchListNow: Timed[];
resourceListNow: Timed[];
player: WebPlayer | MobilePlayer;
startedAt: number;
isMobile?: boolean;
}
const NetworkPanelComp = observer(({
loadTime,
domBuildingTime,
domContentLoadedTime,
fetchList,
resourceList,
fetchListNow,
resourceListNow,
player,
startedAt,
isMobile
}: Props) => {
const { showModal } = useModal();
const [sortBy, setSortBy] = useState('time');
const [sortAscending, setSortAscending] = useState(true);
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
const {
sessionStore: { devTools },
} = useStore();
const filter = devTools[INDEX_KEY].filter;
const activeTab = devTools[INDEX_KEY].activeTab;
const activeIndex = devTools[INDEX_KEY].index;
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)
.sort((a, b) => a.time - b.time),
[resourceList.length, fetchList.length]
);
let filteredList = useMemo(() => {
if (!showOnlyErrors) {
return list;
}
return list.filter((it) => parseInt(it.status) >= 400 || !it.success);
}, [showOnlyErrors, list]);
filteredList = useRegExListFilterMemo(
filteredList,
(it) => [it.status, it.name, it.type],
filter
);
filteredList = useTabListFilterMemo(filteredList, (it) => TYPE_TO_TAB[it.type], ALL, activeTab);
const onTabClick = (activeTab: (typeof TAP_KEYS)[number]) =>
devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) =>
devTools.update(INDEX_KEY, { filter: value });
// AutoScroll
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
filteredList,
getLastItemTime(fetchListNow, resourceListNow),
activeIndex,
(index) => devTools.update(INDEX_KEY, { index })
);
const onMouseEnter = stopAutoscroll;
const onMouseLeave = () => {
if (isDetailsModalActive) {
return;
}
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 = [];
if (domContentLoadedTime != null) {
arr.push({
time: domContentLoadedTime.time,
color: DOM_LOADED_TIME_COLOR,
});
}
if (loadTime != null) {
arr.push({
time: loadTime.time,
color: LOAD_TIME_COLOR,
});
}
return arr;
}, [domContentLoadedTime, loadTime]);
const showDetailsModal = (item: any) => {
setIsDetailsModalActive(true);
showModal(
<FetchDetailsModal
time={item.time + startedAt}
resource={item}
rows={filteredList}
fetchPresented={fetchList.length > 0}
/>,
{
right: true,
width: 500,
onClose: () => {
setIsDetailsModalActive(false);
timeoutStartAutoscroll();
},
}
);
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
stopAutoscroll();
};
return (
<React.Fragment>
<BottomBlock
style={{ height: '300px' }}
className="border"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Network</span>
{isMobile ? null :
<Tabs
className="uppercase"
tabs={TABS}
active={activeTab}
onClick={onTabClick}
border={false}
/>
}
</div>
<Input
className="input-small"
placeholder="Filter by name, type or value"
icon="search"
name="filter"
onChange={onFilterChange}
height={28}
width={230}
value={filter}
/>
</BottomBlock.Header>
<BottomBlock.Content>
<div className="flex items-center justify-between px-4">
<div>
<Toggler
checked={showOnlyErrors}
name="test"
onChange={() => setShowOnlyErrors(!showOnlyErrors)}
label="4xx-5xx Only"
/>
</div>
<InfoLine>
<InfoLine.Point label={filteredList.length + ''} value=" requests" />
<InfoLine.Point
label={formatBytes(transferredSize)}
value="transferred"
display={transferredSize > 0}
/>
<InfoLine.Point
label={formatBytes(resourcesSize)}
value="resources"
display={resourcesSize > 0}
/>
<InfoLine.Point
label={formatMs(domBuildingTime)}
value="DOM Building Time"
display={domBuildingTime != null}
/>
<InfoLine.Point
label={domContentLoadedTime && formatMs(domContentLoadedTime.value)}
value="DOMContentLoaded"
display={domContentLoadedTime != null}
dotColor={DOM_LOADED_TIME_COLOR}
/>
<InfoLine.Point
label={loadTime && formatMs(loadTime.value)}
value="Load"
display={loadTime != null}
dotColor={LOAD_TIME_COLOR}
/>
</InfoLine>
</div>
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
No Data
</div>
}
size="small"
show={filteredList.length === 0}
>
<TimeTable
rows={filteredList}
referenceLines={referenceLines}
renderPopup
onRowClick={showDetailsModal}
sortBy={sortBy}
sortAscending={sortAscending}
onJump={(row: any) => {
devTools.update(INDEX_KEY, { index: filteredList.indexOf(row) });
player.jump(row.time);
}}
activeIndex={activeIndex}
>
{[
// {
// label: 'Start',
// width: 120,
// render: renderStart,
// },
{
label: 'Status',
dataKey: 'status',
width: 90,
render: renderStatus,
},
{
label: 'Type',
dataKey: 'type',
width: 90,
render: renderType,
},
{
label: 'Name',
width: 240,
dataKey: 'name',
render: renderName,
},
{
label: 'Size',
width: 80,
dataKey: 'decodedBodySize',
render: renderSize,
hidden: activeTab === XHR,
},
{
label: 'Duration',
width: 80,
dataKey: 'duration',
render: renderDuration,
},
]}
</TimeTable>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
</React.Fragment>
);
})
const WebNetworkPanel = connect((state: any) => ({
startedAt: state.getIn(['sessions', 'current']).startedAt,
}))(observer(NetworkPanelCont));
const MobileNetworkPanel = connect((state: any) => ({
startedAt: state.getIn(['sessions', 'current']).startedAt,
}))(observer(MobileNetworkPanelCont));
export {
WebNetworkPanel,
MobileNetworkPanel
}