Skip to content

Commit

Permalink
feat(replay): Let the a11y analysis run at any point as long as replay (
Browse files Browse the repository at this point in the history
#61012)

This deserves a video, but for now here's some screen shots

Related to: #55501


https://github.com/getsentry/sentry/assets/187460/a80c6e93-6904-427c-8949-93a41abf6fa6
  • Loading branch information
ryan953 authored Dec 4, 2023
1 parent 6b3c219 commit 694b980
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 37 deletions.
28 changes: 16 additions & 12 deletions static/app/utils/replays/hooks/useA11yData.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
import {useMemo} from 'react';

import {useReplayContext} from 'sentry/components/replays/replayContext';
import {useApiQuery} from 'sentry/utils/queryClient';
import {useQuery} from 'sentry/utils/queryClient';
import hydrateA11yFrame, {RawA11yResponse} from 'sentry/utils/replays/hydrateA11yFrame';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
import useProjects from 'sentry/utils/useProjects';

export default function useA11yData() {
const api = useApi();
const organization = useOrganization();
const {replay} = useReplayContext();
const {currentTime, replay} = useReplayContext();
const {projects} = useProjects();

const replayRecord = replay?.getReplay();
const startTimestampMs = replayRecord?.started_at.getTime();
const project = projects.find(p => p.id === replayRecord?.project_id);

const {data, ...rest} = useApiQuery<RawA11yResponse>(
[
const unixTimestamp = ((startTimestampMs || 0) + currentTime) / 1000;
const {data, ...rest} = useQuery<RawA11yResponse>({
queryKey: [
`/projects/${organization.slug}/${project?.slug}/replays/${replayRecord?.id}/accessibility-issues/`,
],
{
staleTime: 0,
enabled: Boolean(project) && Boolean(replayRecord),
}
);
queryFn: ({queryKey: [url]}) =>
api.requestPromise(String(url), {
method: 'GET',
query: {timestamp: unixTimestamp},
}),
staleTime: 0,
enabled: Boolean(project) && Boolean(replayRecord),
});

const hydrated = useMemo(
() => data?.data?.flatMap(record => hydrateA11yFrame(record, startTimestampMs ?? 0)),
[data?.data, startTimestampMs]
);
return {data: hydrated, ...rest};
return {data: hydrated, dataOffsetMs: currentTime, ...rest};
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const COLUMNS: {
label: t('Type'),
},
{field: 'element', label: t('Element')},
{field: '', label: ''},
];

export const COLUMN_COUNT = COLUMNS.length;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {useCallback, useState} from 'react';
import styled from '@emotion/styled';

import {Button} from 'sentry/components/button';
import {Flex} from 'sentry/components/profiling/flex';
import {useReplayContext} from 'sentry/components/replays/replayContext';
import {showPlayerTime} from 'sentry/components/replays/utils';
import Well from 'sentry/components/well';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import TimestampButton from 'sentry/views/replays/detail/timestampButton';

interface Props {
initialOffsetMs: number;
refetch: () => void;
}

export default function AccessibilityRefetchBanner({initialOffsetMs, refetch}: Props) {
const {currentTime, replay, setCurrentTime, isPlaying, togglePlayPause} =
useReplayContext();

const startTimestampMs = replay?.getReplay()?.started_at?.getTime() ?? 0;
const [lastOffsetMs, setLastOffsetMs] = useState(initialOffsetMs);

const handleClickRefetch = useCallback(() => {
togglePlayPause(false);
setLastOffsetMs(currentTime);
refetch();
}, [currentTime, refetch, togglePlayPause]);

const handleClickTimestamp = useCallback(() => {
setCurrentTime(lastOffsetMs);
}, [setCurrentTime, lastOffsetMs]);

const now = showPlayerTime(startTimestampMs + currentTime, startTimestampMs, false);

return (
<StyledWell>
<Flex
gap={space(1)}
justify="space-between"
align="center"
wrap="nowrap"
style={{overflow: 'auto'}}
>
<Flex gap={space(1)} wrap="nowrap" style={{whiteSpace: 'nowrap'}}>
{tct('Results as of [lastRuntime]', {
lastRuntime: (
<StyledTimestampButton
aria-label={t('See in replay')}
onClick={handleClickTimestamp}
startTimestampMs={startTimestampMs}
timestampMs={startTimestampMs + lastOffsetMs}
/>
),
})}
</Flex>
<Button
size="xs"
priority="primary"
onClick={handleClickRefetch}
disabled={currentTime === lastOffsetMs}
>
{isPlaying
? tct('Pause and Run validation for [now]', {now})
: tct('Run validation for [now]', {now})}
</Button>
</Flex>
</StyledWell>
);
}

const StyledWell = styled(Well)`
margin-bottom: 0;
border-radius: ${p => p.theme.borderRadiusTop};
`;

const StyledTimestampButton = styled(TimestampButton)`
align-self: center;
align-items: center;
`;
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import {ComponentProps, CSSProperties, forwardRef} from 'react';
import classNames from 'classnames';

import {Button} from 'sentry/components/button';
import {
Cell,
CodeHighlightCell,
Text,
} from 'sentry/components/replays/virtualizedGrid/bodyCell';
import {Tooltip} from 'sentry/components/tooltip';
import {IconFire, IconInfo, IconPlay, IconWarning} from 'sentry/icons';
import {t} from 'sentry/locale';
import {IconFire, IconInfo, IconWarning} from 'sentry/icons';
import type useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
import {Color} from 'sentry/utils/theme';
Expand Down Expand Up @@ -44,7 +42,6 @@ const AccessibilityTableCell = forwardRef<HTMLDivElement, Props>(
currentHoverTime,
currentTime,
onClickCell,
onClickTimestamp,
onMouseEnter,
onMouseLeave,
rowIndex,
Expand Down Expand Up @@ -125,20 +122,6 @@ const AccessibilityTableCell = forwardRef<HTMLDivElement, Props>(
</CodeHighlightCell>
</Cell>
),
() => (
<Cell {...columnProps}>
<Button
size="xs"
borderless
aria-label={t('See in replay')}
icon={<IconPlay size="xs" color={isSelected ? 'white' : 'black'} />}
onClick={e => {
e.stopPropagation();
onClickTimestamp(a11yIssue);
}}
/>
</Cell>
),
];

return renderFns[columnIndex]();
Expand Down
32 changes: 26 additions & 6 deletions static/app/views/replays/detail/accessibility/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useCallback, useMemo, useRef, useState} from 'react';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {AutoSizer, CellMeasurer, GridCellProps, MultiGrid} from 'react-virtualized';
import styled from '@emotion/styled';

import Placeholder from 'sentry/components/placeholder';
import JumpButtons from 'sentry/components/replays/jumpButtons';
Expand All @@ -18,6 +19,7 @@ import AccessibilityFilters from 'sentry/views/replays/detail/accessibility/acce
import AccessibilityHeaderCell, {
COLUMN_COUNT,
} from 'sentry/views/replays/detail/accessibility/accessibilityHeaderCell';
import AccessibilityRefetchBanner from 'sentry/views/replays/detail/accessibility/accessibilityRefetchBanner';
import AccessibilityTableCell from 'sentry/views/replays/detail/accessibility/accessibilityTableCell';
import AccessibilityDetails from 'sentry/views/replays/detail/accessibility/details';
import useAccessibilityFilters from 'sentry/views/replays/detail/accessibility/useAccessibilityFilters';
Expand All @@ -43,7 +45,13 @@ function AccessibilityList() {
const {currentTime, currentHoverTime} = useReplayContext();
const {onMouseEnter, onMouseLeave, onClickTimestamp} = useCrumbHandlers();

const {data: accessibilityData, isLoading} = useA11yData();
const {
dataOffsetMs,
data: accessibilityData,
isLoading,
isRefetching,
refetch,
} = useA11yData();

const [scrollToRow, setScrollToRow] = useState<undefined | number>(undefined);

Expand Down Expand Up @@ -92,6 +100,12 @@ function AccessibilityList() {
),
});

useEffect(() => {
if (isRefetching) {
onCloseDetailsSplit();
}
}, [isRefetching, onCloseDetailsSplit]);

const {
handleClick: onClickToJump,
onSectionRendered,
Expand Down Expand Up @@ -147,16 +161,17 @@ function AccessibilityList() {

return (
<FluidHeight>
<FilterLoadingIndicator isLoading={isLoading}>
<FilterLoadingIndicator isLoading={isLoading || isRefetching}>
<AccessibilityFilters accessibilityData={accessibilityData} {...filterProps} />
</FilterLoadingIndicator>
<GridTable ref={containerRef} data-test-id="replay-details-accessibility-tab">
<AccessibilityRefetchBanner initialOffsetMs={dataOffsetMs} refetch={refetch} />
<StyledGridTable ref={containerRef} data-test-id="replay-details-accessibility-tab">
<SplitPanel
style={{
gridTemplateRows: splitSize !== undefined ? `1fr auto ${splitSize}px` : '1fr',
}}
>
{accessibilityData ? (
{accessibilityData && !isRefetching ? (
<OverflowHidden>
<AutoSizer onResize={onWrapperResize}>
{({height, width}) => (
Expand Down Expand Up @@ -211,9 +226,14 @@ function AccessibilityList() {
onClose={onCloseDetailsSplit}
/>
</SplitPanel>
</GridTable>
</StyledGridTable>
</FluidHeight>
);
}

const StyledGridTable = styled(GridTable)`
border-radius: ${p => p.theme.borderRadiusBottom};
border-top: none;
`;

export default AccessibilityList;

0 comments on commit 694b980

Please sign in to comment.