import {Fragment, useCallback, useEffect} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import type {Location} from 'history'; import {Button} from 'sentry/components/button'; import * as Layout from 'sentry/components/layouts/thirds'; import Placeholder from 'sentry/components/placeholder'; import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext'; import {IconPlay, IconUser} from 'sentry/icons'; import {t, tn} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Group} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; import {browserHistory} from 'sentry/utils/browserHistory'; import type EventView from 'sentry/utils/discover/eventView'; import useReplayCountForIssues from 'sentry/utils/replayCount/useReplayCountForIssues'; import useReplayList from 'sentry/utils/replays/hooks/useReplayList'; import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import useUrlParams from 'sentry/utils/useUrlParams'; import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils'; import useAllMobileProj from 'sentry/views/replays/detail/useAllMobileProj'; import ReplayTable from 'sentry/views/replays/replayTable'; import {ReplayColumn} from 'sentry/views/replays/replayTable/types'; import type {ReplayListLocationQuery, ReplayListRecord} from 'sentry/views/replays/types'; import {ReplayClipPreviewWrapper} from './replayClipPreviewWrapper'; import useReplaysFromIssue from './useReplaysFromIssue'; type Props = { group: Group; }; const VISIBLE_COLUMNS = [ ReplayColumn.REPLAY, ReplayColumn.OS, ReplayColumn.BROWSER, ReplayColumn.DURATION, ReplayColumn.COUNT_ERRORS, ReplayColumn.ACTIVITY, ]; const VISIBLE_COLUMNS_MOBILE = [ ReplayColumn.REPLAY, ReplayColumn.OS, ReplayColumn.DURATION, ReplayColumn.COUNT_ERRORS, ReplayColumn.ACTIVITY, ]; const visibleColumns = (allMobileProj: boolean) => allMobileProj ? VISIBLE_COLUMNS_MOBILE : VISIBLE_COLUMNS; function GroupReplays({group}: Props) { const organization = useOrganization(); const location = useLocation(); const hasStreamlinedUI = useHasStreamlinedUI(); const {eventView, fetchError, isFetching, pageLinks} = useReplaysFromIssue({ group, location, organization, }); const {allMobileProj} = useAllMobileProj(); useEffect(() => { trackAnalytics('replay.render-issues-group-list', { project_id: group.project.id, platform: group.project.platform, organization, }); // we only want to fire this event once // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (!eventView) { // Shown on load and no replay data available return ( {isFetching ? ( ) : ( t('No replay data available.') )} ); } return ( ); } function GroupReplaysTableInner({ children, organization, group, replaySlug, setSelectedReplayIndex, selectedReplayIndex, overlayContent, replays, pageLinks, }: { children: React.ReactNode; group: Group; organization: Organization; pageLinks: string | null; replaySlug: string; replays: ReplayListRecord[] | undefined; selectedReplayIndex: number; setSelectedReplayIndex: (index: number) => void; overlayContent?: React.ReactNode; }) { const orgSlug = organization.slug; const {fetching, replay} = useReplayReader({ orgSlug, replaySlug, group, }); const {allMobileProj} = useAllMobileProj(); return ( {children} ); } const locationForFetching = {query: {}} as Location; function GroupReplaysTable({ eventView, organization, group, }: { eventView: EventView; group: Group; organization: Organization; pageLinks: string | null; visibleColumns: ReplayColumn[]; }) { const location = useLocation(); const urlParams = useUrlParams(); const {getReplayCountForIssue} = useReplayCountForIssues({ statsPeriod: '90d', }); const hasStreamlinedUI = useHasStreamlinedUI(); const replayListData = useReplayList({ eventView, location: locationForFetching, organization, queryReferrer: 'issueReplays', }); const {replays} = replayListData; const {allMobileProj} = useAllMobileProj(); const rawReplayIndex = urlParams.getParamValue('selected_replay_index'); const selectedReplayIndex = parseInt( typeof rawReplayIndex === 'string' ? rawReplayIndex : '0', 10 ); const setSelectedReplayIndex = useCallback( (index: number) => { browserHistory.replace({ pathname: location.pathname, query: {...location.query, selected_replay_index: index}, }); }, [location] ); const selectedReplay = replays?.[selectedReplayIndex]; const replayCount = getReplayCountForIssue(group.id, group.issueCategory); const nextReplay = replays?.[selectedReplayIndex + 1]; const nextReplayText = nextReplay?.id ? `${nextReplay.user.display_name || t('Anonymous User')}` : undefined; const overlayContent = nextReplayText && replayCount && replayCount > 1 ? ( {t('Up Next')} {nextReplayText} ) : undefined; const replayTable = ( ); const inner = selectedReplay ? ( {replayTable} ) : ( replayTable ); return ( {t( 'There are %s for this issue across %s.', tn('%s replay', '%s replays', replayCount ?? 0), tn('%s event', '%s events', group.count) )} {inner} ); } const StyledLayoutPage = styled(Layout.Page)<{hasStreamlinedUI?: boolean}>` background-color: ${p => p.theme.background}; gap: ${space(2)}; ${p => p.hasStreamlinedUI && css` border: 1px solid ${p.theme.border}; border-radius: ${p.theme.borderRadius}; padding: ${space(3)} ${space(2)}; `} `; const ReplayCountHeader = styled('div')` display: flex; align-items: center; gap: ${space(1)}; `; const OverlayText = styled('div')` font-size: ${p => p.theme.fontSizeExtraLarge}; `; const UpNext = styled('div')` line-height: 0; `; export default GroupReplays;