Skip to content

Commit c037c27

Browse files
michellewzhangarmenzg
authored andcommitted
feat(replays): add initial replay cards to replay list (#53323)
closes #53086 created two experimental tables on the replay list page: 1. top 3 most erroneous replays within the last 7 days 2. top 3 most dead click replays within the last 7 days (can change these time periods, I just thought 1 day was too little time for a useful sample size since a lot of numbers showed 0) notes: - these tables are static when compared to the big replay list table, so pagination doesn't affect them (see below for screenshots showing this) - in theory, rage clicks always < dead clicks, which is why I chose to sort by dead clicks page 1: <img width="1250" alt="SCR-20230721-jybf" src="https://github.com/getsentry/sentry/assets/56095982/1c2f6981-e504-4d90-9f88-4c4dfc4523f5"> page 2 (same tables, different replays on big list): <img width="1238" alt="SCR-20230721-jxge" src="https://github.com/getsentry/sentry/assets/56095982/d4b231e3-8786-4144-8122-cc57af0ef3ca">
1 parent 20b9b2b commit c037c27

File tree

6 files changed

+214
-6
lines changed

6 files changed

+214
-6
lines changed

static/app/views/replays/list.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {t} from 'sentry/locale';
66
import useReplayPageview from 'sentry/utils/replays/hooks/useReplayPageview';
77
import useOrganization from 'sentry/utils/useOrganization';
88
import ReplaysFilters from 'sentry/views/replays/list/filters';
9+
import ReplaysErroneousDeadRageCards from 'sentry/views/replays/list/replaysErroneousDeadRageCards';
910
import ReplaysList from 'sentry/views/replays/list/replaysList';
1011

1112
function ReplaysListContainer() {
@@ -30,6 +31,7 @@ function ReplaysListContainer() {
3031
<PageFiltersContainer>
3132
<Layout.Body>
3233
<Layout.Main fullWidth>
34+
<ReplaysErroneousDeadRageCards />
3335
<ReplaysFilters />
3436
<ReplaysList />
3537
</Layout.Main>
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import styled from '@emotion/styled';
2+
import {Location} from 'history';
3+
4+
import {space} from 'sentry/styles/space';
5+
import type {Organization} from 'sentry/types';
6+
import EventView from 'sentry/utils/discover/eventView';
7+
import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
8+
import {useHaveSelectedProjectsSentAnyReplayEvents} from 'sentry/utils/replays/hooks/useReplayOnboarding';
9+
import useOrganization from 'sentry/utils/useOrganization';
10+
import ReplayTable from 'sentry/views/replays/replayTable';
11+
import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
12+
13+
function ReplaysErroneousDeadRageCards() {
14+
const location: Location = {
15+
pathname: '',
16+
search: '',
17+
query: {},
18+
hash: '',
19+
state: '',
20+
action: 'PUSH',
21+
key: '',
22+
};
23+
const organization = useOrganization();
24+
25+
const eventViewErrors = EventView.fromNewQueryWithLocation(
26+
{
27+
name: '',
28+
version: 2,
29+
fields: [
30+
'activity',
31+
'duration',
32+
'count_errors',
33+
'id',
34+
'project_id',
35+
'user',
36+
'finished_at',
37+
'is_archived',
38+
'started_at',
39+
],
40+
range: '7d',
41+
projects: [],
42+
query: '',
43+
orderby: '-count_errors',
44+
},
45+
location
46+
);
47+
48+
const eventViewDeadRage = EventView.fromNewQueryWithLocation(
49+
{
50+
name: '',
51+
version: 2,
52+
fields: [
53+
'activity',
54+
'duration',
55+
'count_dead_clicks',
56+
'count_rage_clicks',
57+
'id',
58+
'project_id',
59+
'user',
60+
'finished_at',
61+
'is_archived',
62+
'started_at',
63+
],
64+
range: '7d',
65+
projects: [],
66+
query: '',
67+
orderby: '-count_dead_clicks',
68+
},
69+
location
70+
);
71+
72+
const hasSessionReplay = organization.features.includes('session-replay');
73+
const hasDeadRageCards = organization.features.includes('replay-error-click-cards');
74+
const {hasSentOneReplay, fetching} = useHaveSelectedProjectsSentAnyReplayEvents();
75+
76+
const deadRageCols = [
77+
ReplayColumn.MOST_DEAD_CLICKS,
78+
ReplayColumn.COUNT_DEAD_CLICKS,
79+
ReplayColumn.COUNT_RAGE_CLICKS,
80+
];
81+
82+
const errorCols = [
83+
ReplayColumn.MOST_ERRONEOUS_REPLAYS,
84+
ReplayColumn.DURATION,
85+
ReplayColumn.COUNT_ERRORS,
86+
ReplayColumn.ACTIVITY,
87+
];
88+
89+
return hasSessionReplay && !fetching && hasSentOneReplay ? (
90+
hasDeadRageCards ? (
91+
<SplitCardContainer>
92+
<CardTable
93+
eventView={eventViewErrors}
94+
location={location}
95+
organization={organization}
96+
visibleColumns={errorCols}
97+
/>
98+
<CardTable
99+
eventView={eventViewDeadRage}
100+
location={location}
101+
organization={organization}
102+
visibleColumns={deadRageCols}
103+
/>
104+
</SplitCardContainer>
105+
) : null
106+
) : null;
107+
}
108+
109+
function CardTable({
110+
eventView,
111+
location,
112+
organization,
113+
visibleColumns,
114+
}: {
115+
eventView: EventView;
116+
location: Location;
117+
organization: Organization;
118+
visibleColumns: ReplayColumn[];
119+
}) {
120+
const {replays, isFetching, fetchError} = useReplayList({
121+
eventView,
122+
location,
123+
organization,
124+
});
125+
126+
return (
127+
<ReplayTable
128+
fetchError={fetchError}
129+
isFetching={isFetching}
130+
replays={replays?.slice(0, 3)}
131+
sort={undefined}
132+
visibleColumns={visibleColumns}
133+
saveLocation
134+
/>
135+
);
136+
}
137+
138+
const SplitCardContainer = styled('div')`
139+
display: grid;
140+
grid-template-columns: 1fr 1fr;
141+
gap: ${space(2)};
142+
`;
143+
144+
export default ReplaysErroneousDeadRageCards;

static/app/views/replays/replayTable/headerCell.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function HeaderCell({column, sort}: Props) {
3030
<SortableHeader
3131
sort={sort}
3232
fieldName="count_dead_clicks"
33-
label={t('Dead Clicks')}
33+
label={t('Dead clicks')}
3434
tooltip={t(
3535
'A dead click is a user click that does not result in any page activity after 7 seconds.'
3636
)}
@@ -45,7 +45,7 @@ function HeaderCell({column, sort}: Props) {
4545
<SortableHeader
4646
sort={sort}
4747
fieldName="count_rage_clicks"
48-
label={t('Rage Clicks')}
48+
label={t('Rage clicks')}
4949
tooltip={t(
5050
'A rage click is 5 or more clicks on a dead element, which exhibits no page activity after 7 seconds.'
5151
)}
@@ -61,6 +61,12 @@ function HeaderCell({column, sort}: Props) {
6161
case ReplayColumn.REPLAY:
6262
return <SortableHeader sort={sort} fieldName="started_at" label={t('Replay')} />;
6363

64+
case ReplayColumn.MOST_ERRONEOUS_REPLAYS:
65+
return <SortableHeader label={t('Most erroneous replays')} />;
66+
67+
case ReplayColumn.MOST_DEAD_CLICKS:
68+
return <SortableHeader label={t('Most dead clicks')} />;
69+
6470
case ReplayColumn.SLOWEST_TRANSACTION:
6571
return (
6672
<SortableHeader

static/app/views/replays/replayTable/index.tsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Fragment, ReactNode} from 'react';
22
import styled from '@emotion/styled';
3+
import {Location} from 'history';
34

45
import {Alert} from 'sentry/components/alert';
56
import PanelTable from 'sentry/components/panels/panelTable';
@@ -33,6 +34,7 @@ type Props = {
3334
sort: Sort | undefined;
3435
visibleColumns: ReplayColumn[];
3536
emptyMessage?: ReactNode;
37+
saveLocation?: boolean;
3638
};
3739

3840
function ReplayTable({
@@ -42,11 +44,24 @@ function ReplayTable({
4244
sort,
4345
visibleColumns,
4446
emptyMessage,
47+
saveLocation,
4548
}: Props) {
4649
const routes = useRoutes();
47-
const location = useLocation();
50+
const newLocation = useLocation();
4851
const organization = useOrganization();
4952

53+
const location: Location = saveLocation
54+
? {
55+
pathname: '',
56+
search: '',
57+
query: {},
58+
hash: '',
59+
state: '',
60+
action: 'PUSH',
61+
key: '',
62+
}
63+
: newLocation;
64+
5065
const tableHeaders = visibleColumns
5166
.filter(Boolean)
5267
.map(column => <HeaderCell key={column} column={column} sort={sort} />);
@@ -117,6 +132,7 @@ function ReplayTable({
117132
eventView={eventView}
118133
organization={organization}
119134
referrer={referrer}
135+
showUrl
120136
/>
121137
);
122138

@@ -129,6 +145,30 @@ function ReplayTable({
129145
/>
130146
);
131147

148+
case ReplayColumn.MOST_DEAD_CLICKS:
149+
return (
150+
<ReplayCell
151+
key="mostDeadClicks"
152+
replay={replay}
153+
organization={organization}
154+
referrer={referrer}
155+
showUrl={false}
156+
eventView={eventView}
157+
/>
158+
);
159+
160+
case ReplayColumn.MOST_ERRONEOUS_REPLAYS:
161+
return (
162+
<ReplayCell
163+
key="mostErroneousReplays"
164+
replay={replay}
165+
organization={organization}
166+
referrer={referrer}
167+
showUrl={false}
168+
eventView={eventView}
169+
/>
170+
);
171+
132172
default:
133173
return null;
134174
}
@@ -140,13 +180,21 @@ function ReplayTable({
140180
);
141181
}
142182

183+
const flexibleColumns = [
184+
ReplayColumn.REPLAY,
185+
ReplayColumn.MOST_DEAD_CLICKS,
186+
ReplayColumn.MOST_ERRONEOUS_REPLAYS,
187+
];
188+
143189
const StyledPanelTable = styled(PanelTable)<{
144190
visibleColumns: ReplayColumn[];
145191
}>`
146192
grid-template-columns: ${p =>
147193
p.visibleColumns
148194
.filter(Boolean)
149-
.map(column => (column === 'replay' ? 'minmax(100px, 1fr)' : 'max-content'))
195+
.map(column =>
196+
flexibleColumns.includes(column) ? 'minmax(100px, 1fr)' : 'max-content'
197+
)
150198
.join(' ')};
151199
`;
152200

static/app/views/replays/replayTable/tableCell.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,13 @@ export function ReplayCell({
5151
organization,
5252
referrer,
5353
replay,
54-
}: Props & {eventView: EventView; organization: Organization; referrer: string}) {
54+
showUrl,
55+
}: Props & {
56+
eventView: EventView;
57+
organization: Organization;
58+
referrer: string;
59+
showUrl: boolean;
60+
}) {
5561
const {projects} = useProjects();
5662
const project = projects.find(p => p.id === replay.project_id);
5763

@@ -90,7 +96,7 @@ export function ReplayCell({
9096

9197
const subText = (
9298
<Cols>
93-
<StringWalker urls={replay.urls} />
99+
{showUrl ? <StringWalker urls={replay.urls} /> : undefined}
94100
<Row gap={1}>
95101
<Row gap={0.5}>
96102
{project ? <Avatar size={12} project={project} /> : null}

static/app/views/replays/replayTable/types.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export enum ReplayColumn {
55
COUNT_ERRORS = 'countErrors',
66
COUNT_RAGE_CLICKS = 'countRageClicks',
77
DURATION = 'duration',
8+
MOST_ERRONEOUS_REPLAYS = 'mostErroneousReplays',
9+
MOST_DEAD_CLICKS = 'mostDeadClicks',
810
OS = 'os',
911
REPLAY = 'replay',
1012
SLOWEST_TRANSACTION = 'slowestTransaction',

0 commit comments

Comments
 (0)