Skip to content

Commit 364b48f

Browse files
authored
feat(replays): Add UI to display network request details (#46219)
New 'Network details' panel. Figma per [figma](https://www.figma.com/file/SVyNvm8p17xGfIC7r7l2YS/Specs%3A-Network-Request-Details?node-id=4%3A7443&t=VjNMXfMJQKHfj0ks-0) specs There are a few states so observe in the screen shots, and some followup tasks Note that: - You can only open the details panel for xhr & fetch network types - The data is a string that's run through the pii scrubber, we try to parse it with `JSON.parse`, but if that fails you'll just get a string | State | Img | | --- | --- | | Click a network row to open the Details. Notice that the row you clicked is Bold. The lower area with the data is scrollable, strings or objects will display | ![SCR-20230323-km4](https://user-images.githubusercontent.com/187460/227372507-b093de12-66df-4383-a46f-650a488c7821.png) | | The X button will close the panel | ![SCR-20230323-kmb](https://user-images.githubusercontent.com/187460/227372508-18954b0a-c593-4aa8-9eca-49a99f191685.png) | | Click+drag (don't click on the tabs or the X button) to make the panel larger/smaller | ![SCR-20230323-kmd](https://user-images.githubusercontent.com/187460/227372509-a3318199-01ef-486d-a00b-ece5008ca465.png) | Related to getsentry/sentry-javascript#7589 Fixes #46054
1 parent 8ee4e23 commit 364b48f

File tree

11 files changed

+411
-84
lines changed

11 files changed

+411
-84
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
"react-date-range": "^1.4.0",
142142
"react-dom": "17.0.2",
143143
"react-grid-layout": "^1.3.4",
144+
"react-inspector": "^6.0.1",
144145
"react-lazyload": "^2.3.0",
145146
"react-mentions": "4.4.2",
146147
"react-popper": "^2.3.0",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {ComponentPropsWithoutRef, useMemo} from 'react';
2+
import {
3+
chromeDark,
4+
chromeLight,
5+
ObjectInspector as OrigObjectInspector,
6+
} from 'react-inspector';
7+
8+
import ConfigStore from 'sentry/stores/configStore';
9+
10+
type Props = Omit<ComponentPropsWithoutRef<typeof OrigObjectInspector>, 'theme'>;
11+
12+
function ObjectInspector({data}: Props) {
13+
const isDark = ConfigStore.get('theme') === 'dark';
14+
15+
const INSPECTOR_THEME = useMemo(
16+
() => ({
17+
...(isDark ? chromeDark : chromeLight),
18+
BASE_BACKGROUND_COLOR: 'none',
19+
OBJECT_PREVIEW_OBJECT_MAX_PROPERTIES: 1,
20+
}),
21+
[isDark]
22+
);
23+
24+
return (
25+
<OrigObjectInspector
26+
data={data}
27+
// @ts-expect-error
28+
theme={INSPECTOR_THEME}
29+
/>
30+
);
31+
}
32+
33+
export default ObjectInspector;

static/app/views/replays/detail/layout/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import SideTabs from 'sentry/views/replays/detail/layout/sideTabs';
1616
import SplitPanel from 'sentry/views/replays/detail/layout/splitPanel';
1717

1818
const MIN_VIDEO_WIDTH = 325;
19-
const MIN_CONTENT_WIDTH = 325;
19+
const MIN_CONTENT_WIDTH = 340;
2020
const MIN_SIDEBAR_WIDTH = 325;
2121
const MIN_VIDEO_HEIGHT = 200;
2222
const MIN_CONTENT_HEIGHT = 180;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {DOMAttributes} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {IconGrabbable} from 'sentry/icons';
5+
import {space} from 'sentry/styles/space';
6+
7+
type Props = {
8+
isHeld: boolean;
9+
slideDirection: 'leftright' | 'updown';
10+
};
11+
12+
const SplitDivider = styled(
13+
({isHeld: _a, slideDirection: _b, ...props}: Props & DOMAttributes<HTMLDivElement>) => (
14+
<div {...props}>
15+
<IconGrabbable size="sm" />
16+
</div>
17+
)
18+
)<Props>`
19+
display: grid;
20+
place-items: center;
21+
height: 100%;
22+
width: 100%;
23+
24+
user-select: ${p => (p.isHeld ? 'none' : 'inherit')};
25+
background: ${p => (p.isHeld ? p.theme.hover : 'inherit')};
26+
27+
:hover {
28+
background: ${p => p.theme.hover};
29+
}
30+
31+
${p =>
32+
p.slideDirection === 'leftright'
33+
? `
34+
cursor: ew-resize;
35+
height: 100%;
36+
width: ${space(2)};
37+
`
38+
: `
39+
cursor: ns-resize;
40+
width: 100%;
41+
height: ${space(2)};
42+
43+
& > svg {
44+
transform: rotate(90deg);
45+
}
46+
`}
47+
`;
48+
49+
export default SplitDivider;

static/app/views/replays/detail/layout/splitPanel.tsx

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import {DOMAttributes, ReactNode, useCallback} from 'react';
1+
import {ReactNode, useCallback} from 'react';
22
import styled from '@emotion/styled';
33
import debounce from 'lodash/debounce';
44

5-
import {IconGrabbable} from 'sentry/icons';
6-
import {space} from 'sentry/styles/space';
75
import useSplitPanelTracking from 'sentry/utils/replays/hooks/useSplitPanelTracking';
86
import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
7+
import SplitDivider from 'sentry/views/replays/detail/layout/splitDivider';
98

109
type Side = {
1110
content: ReactNode;
@@ -87,7 +86,7 @@ function SplitPanel(props: Props) {
8786
size={sizePct}
8887
>
8988
<Panel>{a.content}</Panel>
90-
<Divider
89+
<SplitDivider
9190
isHeld={isHeld}
9291
onDoubleClick={onDoubleClick}
9392
onMouseDown={onMouseDown}
@@ -106,7 +105,7 @@ function SplitPanel(props: Props) {
106105
className={isHeld ? 'disable-iframe-pointer' : undefined}
107106
>
108107
<Panel>{a.content}</Panel>
109-
<Divider
108+
<SplitDivider
110109
isHeld={isHeld}
111110
onDoubleClick={onDoubleClick}
112111
onMouseDown={onMouseDown}
@@ -138,45 +137,4 @@ const Panel = styled('div')`
138137
overflow: hidden;
139138
`;
140139

141-
type DividerProps = {isHeld: boolean; slideDirection: 'leftright' | 'updown'};
142-
const Divider = styled(
143-
({
144-
isHeld: _a,
145-
slideDirection: _b,
146-
...props
147-
}: DividerProps & DOMAttributes<HTMLDivElement>) => (
148-
<div {...props}>
149-
<IconGrabbable size="sm" />
150-
</div>
151-
)
152-
)<DividerProps>`
153-
display: grid;
154-
place-items: center;
155-
height: 100%;
156-
width: 100%;
157-
158-
${p => (p.isHeld ? 'user-select: none;' : '')}
159-
160-
:hover {
161-
background: ${p => p.theme.hover};
162-
}
163-
164-
${p =>
165-
p.slideDirection === 'leftright'
166-
? `
167-
cursor: ew-resize;
168-
height: 100%;
169-
width: ${space(2)};
170-
`
171-
: `
172-
cursor: ns-resize;
173-
width: 100%;
174-
height: ${space(2)};
175-
176-
& > svg {
177-
transform: rotate(90deg);
178-
}
179-
`}
180-
`;
181-
182140
export default SplitPanel;

static/app/views/replays/detail/network/index.tsx

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import {useMemo, useRef} from 'react';
22
import {AutoSizer, CellMeasurer, GridCellProps, MultiGrid} from 'react-virtualized';
33
import styled from '@emotion/styled';
44

5+
import Feature from 'sentry/components/acl/feature';
56
import Placeholder from 'sentry/components/placeholder';
67
import {useReplayContext} from 'sentry/components/replays/replayContext';
78
import {t} from 'sentry/locale';
89
import {getPrevReplayEvent} from 'sentry/utils/replays/getReplayEvent';
910
import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
11+
import useOrganization from 'sentry/utils/useOrganization';
1012
import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
13+
import NetworkDetails from 'sentry/views/replays/detail/network/networkDetails';
1114
import NetworkFilters from 'sentry/views/replays/detail/network/networkFilters';
1215
import NetworkHeaderCell, {
1316
COLUMN_COUNT,
@@ -34,8 +37,14 @@ const cellMeasurer = {
3437
};
3538

3639
function NetworkList({networkSpans, startTimestampMs}: Props) {
40+
const organization = useOrganization();
3741
const {currentTime, currentHoverTime} = useReplayContext();
3842

43+
const initialRequestDetailsHeight = useMemo(
44+
() => Math.max(150, window.innerHeight * 0.25),
45+
[]
46+
);
47+
3948
const filterProps = useNetworkFilters({networkSpans: networkSpans || []});
4049
const {items: filteredItems, searchTerm, setSearchTerm} = filterProps;
4150
const clearSearchTerm = () => setSearchTerm('');
@@ -120,6 +129,7 @@ function NetworkList({networkSpans, startTimestampMs}: Props) {
120129
handleMouseLeave={handleMouseLeave}
121130
isCurrent={network.id === current?.id}
122131
isHovered={network.id === hovered?.id}
132+
rowIndex={rowIndex}
123133
sortConfig={sortConfig}
124134
span={network}
125135
startTimestampMs={startTimestampMs}
@@ -135,48 +145,62 @@ function NetworkList({networkSpans, startTimestampMs}: Props) {
135145
<FluidHeight>
136146
<NetworkFilters networkSpans={networkSpans} {...filterProps} />
137147
<NetworkTable>
138-
{networkSpans ? (
139-
<AutoSizer onResize={onWrapperResize}>
140-
{({width, height}) => (
141-
<MultiGrid
142-
ref={gridRef}
143-
cellRenderer={cellRenderer}
144-
columnCount={COLUMN_COUNT}
145-
columnWidth={getColumnWidth(width)}
146-
deferredMeasurementCache={cache}
147-
estimatedColumnSize={100}
148-
estimatedRowSize={BODY_HEIGHT}
149-
fixedRowCount={1}
150-
height={height}
151-
noContentRenderer={() => (
152-
<NoRowRenderer
153-
unfilteredItems={networkSpans}
154-
clearSearchTerm={clearSearchTerm}
155-
>
156-
{t('No network requests recorded')}
157-
</NoRowRenderer>
148+
<FluidHeight>
149+
{networkSpans ? (
150+
<OverflowHidden>
151+
<AutoSizer onResize={onWrapperResize}>
152+
{({height, width}) => (
153+
<MultiGrid
154+
ref={gridRef}
155+
cellRenderer={cellRenderer}
156+
columnCount={COLUMN_COUNT}
157+
columnWidth={getColumnWidth(width)}
158+
deferredMeasurementCache={cache}
159+
estimatedColumnSize={100}
160+
estimatedRowSize={BODY_HEIGHT}
161+
fixedRowCount={1}
162+
height={height}
163+
noContentRenderer={() => (
164+
<NoRowRenderer
165+
unfilteredItems={networkSpans}
166+
clearSearchTerm={clearSearchTerm}
167+
>
168+
{t('No network requests recorded')}
169+
</NoRowRenderer>
170+
)}
171+
onScrollbarPresenceChange={onScrollbarPresenceChange}
172+
overscanColumnCount={COLUMN_COUNT}
173+
overscanRowCount={5}
174+
rowCount={items.length + 1}
175+
rowHeight={({index}) => (index === 0 ? HEADER_HEIGHT : BODY_HEIGHT)}
176+
width={width}
177+
/>
158178
)}
159-
onScrollbarPresenceChange={onScrollbarPresenceChange}
160-
overscanColumnCount={COLUMN_COUNT}
161-
overscanRowCount={5}
162-
rowCount={items.length + 1}
163-
rowHeight={({index}) => (index === 0 ? HEADER_HEIGHT : BODY_HEIGHT)}
164-
width={width}
165-
/>
166-
)}
167-
</AutoSizer>
168-
) : (
169-
<Placeholder height="100%" />
170-
)}
179+
</AutoSizer>
180+
</OverflowHidden>
181+
) : (
182+
<Placeholder height="100%" />
183+
)}
184+
<Feature
185+
features={['session-replay-network-details']}
186+
organization={organization}
187+
renderDisabled={false}
188+
>
189+
<NetworkDetails initialHeight={initialRequestDetailsHeight} items={items} />
190+
</Feature>
191+
</FluidHeight>
171192
</NetworkTable>
172193
</FluidHeight>
173194
);
174195
}
175196

176-
const NetworkTable = styled('div')`
197+
const OverflowHidden = styled('div')`
177198
position: relative;
178199
height: 100%;
179200
overflow: hidden;
201+
`;
202+
203+
const NetworkTable = styled(OverflowHidden)`
180204
border: 1px solid ${p => p.theme.border};
181205
border-radius: ${p => p.theme.borderRadius};
182206
`;

0 commit comments

Comments
 (0)