Skip to content

Commit

Permalink
feat(replays): add initial replay cards to replay list (#53323)
Browse files Browse the repository at this point in the history
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">
  • Loading branch information
michellewzhang authored Jul 21, 2023
1 parent 5f94dd6 commit 82a85ca
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 6 deletions.
2 changes: 2 additions & 0 deletions static/app/views/replays/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {t} from 'sentry/locale';
import useReplayPageview from 'sentry/utils/replays/hooks/useReplayPageview';
import useOrganization from 'sentry/utils/useOrganization';
import ReplaysFilters from 'sentry/views/replays/list/filters';
import ReplaysErroneousDeadRageCards from 'sentry/views/replays/list/replaysErroneousDeadRageCards';
import ReplaysList from 'sentry/views/replays/list/replaysList';

function ReplaysListContainer() {
Expand All @@ -30,6 +31,7 @@ function ReplaysListContainer() {
<PageFiltersContainer>
<Layout.Body>
<Layout.Main fullWidth>
<ReplaysErroneousDeadRageCards />
<ReplaysFilters />
<ReplaysList />
</Layout.Main>
Expand Down
144 changes: 144 additions & 0 deletions static/app/views/replays/list/replaysErroneousDeadRageCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import styled from '@emotion/styled';
import {Location} from 'history';

import {space} from 'sentry/styles/space';
import type {Organization} from 'sentry/types';
import EventView from 'sentry/utils/discover/eventView';
import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
import {useHaveSelectedProjectsSentAnyReplayEvents} from 'sentry/utils/replays/hooks/useReplayOnboarding';
import useOrganization from 'sentry/utils/useOrganization';
import ReplayTable from 'sentry/views/replays/replayTable';
import {ReplayColumn} from 'sentry/views/replays/replayTable/types';

function ReplaysErroneousDeadRageCards() {
const location: Location = {
pathname: '',
search: '',
query: {},
hash: '',
state: '',
action: 'PUSH',
key: '',
};
const organization = useOrganization();

const eventViewErrors = EventView.fromNewQueryWithLocation(
{
name: '',
version: 2,
fields: [
'activity',
'duration',
'count_errors',
'id',
'project_id',
'user',
'finished_at',
'is_archived',
'started_at',
],
range: '7d',
projects: [],
query: '',
orderby: '-count_errors',
},
location
);

const eventViewDeadRage = EventView.fromNewQueryWithLocation(
{
name: '',
version: 2,
fields: [
'activity',
'duration',
'count_dead_clicks',
'count_rage_clicks',
'id',
'project_id',
'user',
'finished_at',
'is_archived',
'started_at',
],
range: '7d',
projects: [],
query: '',
orderby: '-count_dead_clicks',
},
location
);

const hasSessionReplay = organization.features.includes('session-replay');
const hasDeadRageCards = organization.features.includes('replay-error-click-cards');
const {hasSentOneReplay, fetching} = useHaveSelectedProjectsSentAnyReplayEvents();

const deadRageCols = [
ReplayColumn.MOST_DEAD_CLICKS,
ReplayColumn.COUNT_DEAD_CLICKS,
ReplayColumn.COUNT_RAGE_CLICKS,
];

const errorCols = [
ReplayColumn.MOST_ERRONEOUS_REPLAYS,
ReplayColumn.DURATION,
ReplayColumn.COUNT_ERRORS,
ReplayColumn.ACTIVITY,
];

return hasSessionReplay && !fetching && hasSentOneReplay ? (
hasDeadRageCards ? (
<SplitCardContainer>
<CardTable
eventView={eventViewErrors}
location={location}
organization={organization}
visibleColumns={errorCols}
/>
<CardTable
eventView={eventViewDeadRage}
location={location}
organization={organization}
visibleColumns={deadRageCols}
/>
</SplitCardContainer>
) : null
) : null;
}

function CardTable({
eventView,
location,
organization,
visibleColumns,
}: {
eventView: EventView;
location: Location;
organization: Organization;
visibleColumns: ReplayColumn[];
}) {
const {replays, isFetching, fetchError} = useReplayList({
eventView,
location,
organization,
});

return (
<ReplayTable
fetchError={fetchError}
isFetching={isFetching}
replays={replays?.slice(0, 3)}
sort={undefined}
visibleColumns={visibleColumns}
saveLocation
/>
);
}

const SplitCardContainer = styled('div')`
display: grid;
grid-template-columns: 1fr 1fr;
gap: ${space(2)};
`;

export default ReplaysErroneousDeadRageCards;
10 changes: 8 additions & 2 deletions static/app/views/replays/replayTable/headerCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function HeaderCell({column, sort}: Props) {
<SortableHeader
sort={sort}
fieldName="count_dead_clicks"
label={t('Dead Clicks')}
label={t('Dead clicks')}
tooltip={t(
'A dead click is a user click that does not result in any page activity after 7 seconds.'
)}
Expand All @@ -45,7 +45,7 @@ function HeaderCell({column, sort}: Props) {
<SortableHeader
sort={sort}
fieldName="count_rage_clicks"
label={t('Rage Clicks')}
label={t('Rage clicks')}
tooltip={t(
'A rage click is 5 or more clicks on a dead element, which exhibits no page activity after 7 seconds.'
)}
Expand All @@ -61,6 +61,12 @@ function HeaderCell({column, sort}: Props) {
case ReplayColumn.REPLAY:
return <SortableHeader sort={sort} fieldName="started_at" label={t('Replay')} />;

case ReplayColumn.MOST_ERRONEOUS_REPLAYS:
return <SortableHeader label={t('Most erroneous replays')} />;

case ReplayColumn.MOST_DEAD_CLICKS:
return <SortableHeader label={t('Most dead clicks')} />;

case ReplayColumn.SLOWEST_TRANSACTION:
return (
<SortableHeader
Expand Down
52 changes: 50 additions & 2 deletions static/app/views/replays/replayTable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Fragment, ReactNode} from 'react';
import styled from '@emotion/styled';
import {Location} from 'history';

import {Alert} from 'sentry/components/alert';
import PanelTable from 'sentry/components/panels/panelTable';
Expand Down Expand Up @@ -33,6 +34,7 @@ type Props = {
sort: Sort | undefined;
visibleColumns: ReplayColumn[];
emptyMessage?: ReactNode;
saveLocation?: boolean;
};

function ReplayTable({
Expand All @@ -42,11 +44,24 @@ function ReplayTable({
sort,
visibleColumns,
emptyMessage,
saveLocation,
}: Props) {
const routes = useRoutes();
const location = useLocation();
const newLocation = useLocation();
const organization = useOrganization();

const location: Location = saveLocation
? {
pathname: '',
search: '',
query: {},
hash: '',
state: '',
action: 'PUSH',
key: '',
}
: newLocation;

const tableHeaders = visibleColumns
.filter(Boolean)
.map(column => <HeaderCell key={column} column={column} sort={sort} />);
Expand Down Expand Up @@ -117,6 +132,7 @@ function ReplayTable({
eventView={eventView}
organization={organization}
referrer={referrer}
showUrl
/>
);

Expand All @@ -129,6 +145,30 @@ function ReplayTable({
/>
);

case ReplayColumn.MOST_DEAD_CLICKS:
return (
<ReplayCell
key="mostDeadClicks"
replay={replay}
organization={organization}
referrer={referrer}
showUrl={false}
eventView={eventView}
/>
);

case ReplayColumn.MOST_ERRONEOUS_REPLAYS:
return (
<ReplayCell
key="mostErroneousReplays"
replay={replay}
organization={organization}
referrer={referrer}
showUrl={false}
eventView={eventView}
/>
);

default:
return null;
}
Expand All @@ -140,13 +180,21 @@ function ReplayTable({
);
}

const flexibleColumns = [
ReplayColumn.REPLAY,
ReplayColumn.MOST_DEAD_CLICKS,
ReplayColumn.MOST_ERRONEOUS_REPLAYS,
];

const StyledPanelTable = styled(PanelTable)<{
visibleColumns: ReplayColumn[];
}>`
grid-template-columns: ${p =>
p.visibleColumns
.filter(Boolean)
.map(column => (column === 'replay' ? 'minmax(100px, 1fr)' : 'max-content'))
.map(column =>
flexibleColumns.includes(column) ? 'minmax(100px, 1fr)' : 'max-content'
)
.join(' ')};
`;

Expand Down
10 changes: 8 additions & 2 deletions static/app/views/replays/replayTable/tableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@ export function ReplayCell({
organization,
referrer,
replay,
}: Props & {eventView: EventView; organization: Organization; referrer: string}) {
showUrl,
}: Props & {
eventView: EventView;
organization: Organization;
referrer: string;
showUrl: boolean;
}) {
const {projects} = useProjects();
const project = projects.find(p => p.id === replay.project_id);

Expand Down Expand Up @@ -90,7 +96,7 @@ export function ReplayCell({

const subText = (
<Cols>
<StringWalker urls={replay.urls} />
{showUrl ? <StringWalker urls={replay.urls} /> : undefined}
<Row gap={1}>
<Row gap={0.5}>
{project ? <Avatar size={12} project={project} /> : null}
Expand Down
2 changes: 2 additions & 0 deletions static/app/views/replays/replayTable/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export enum ReplayColumn {
COUNT_ERRORS = 'countErrors',
COUNT_RAGE_CLICKS = 'countRageClicks',
DURATION = 'duration',
MOST_ERRONEOUS_REPLAYS = 'mostErroneousReplays',
MOST_DEAD_CLICKS = 'mostDeadClicks',
OS = 'os',
REPLAY = 'replay',
SLOWEST_TRANSACTION = 'slowestTransaction',
Expand Down

0 comments on commit 82a85ca

Please sign in to comment.