import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import Avatar from 'sentry/components/avatar'; import UserBadge from 'sentry/components/idBadge/userBadge'; import Link from 'sentry/components/links/link'; import ContextIcon from 'sentry/components/replays/contextIcon'; import {formatTime} from 'sentry/components/replays/utils'; import StringWalker from 'sentry/components/replays/walker/stringWalker'; import ScoreBar from 'sentry/components/scoreBar'; import TimeSince from 'sentry/components/timeSince'; import {CHART_PALETTE} from 'sentry/constants/chartPalette'; import {IconCalendar, IconDelete, IconFire} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space, ValidSize} from 'sentry/styles/space'; import type {Organization} from 'sentry/types'; import {trackAnalytics} from 'sentry/utils/analytics'; import EventView from 'sentry/utils/discover/eventView'; import {spanOperationRelativeBreakdownRenderer} from 'sentry/utils/discover/fieldRenderers'; import {getShortEventId} from 'sentry/utils/events'; import {useLocation} from 'sentry/utils/useLocation'; import useMedia from 'sentry/utils/useMedia'; import useProjects from 'sentry/utils/useProjects'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData'; import type {ReplayListRecord} from 'sentry/views/replays/types'; type Props = { replay: ReplayListRecord | ReplayListRecordWithTx; }; function getUserBadgeUser(replay: Props['replay']) { return replay.is_archived ? { username: '', email: '', id: '', ip_address: '', name: '', } : { username: replay.user?.display_name || '', email: replay.user?.email || '', id: replay.user?.id || '', ip_address: replay.user?.ip || '', name: replay.user?.username || '', }; } export function ReplayCell({ eventView, organization, referrer, replay, showUrl, }: Props & { eventView: EventView; organization: Organization; referrer: string; showUrl: boolean; }) { const {projects} = useProjects(); const project = projects.find(p => p.id === replay.project_id); const replayDetails = { pathname: normalizeUrl(`/organizations/${organization.slug}/replays/${replay.id}/`), query: { referrer, ...eventView.generateQueryStringObject(), }, }; const trackNavigationEvent = () => trackAnalytics('replay.list-navigate-to-details', { project_id: project?.id, platform: project?.platform, organization, referrer, }); if (replay.is_archived) { return (
{t('Deleted Replay')} {project ? : null} {getShortEventId(replay.id)}
); } const subText = ( {showUrl ? : undefined} {project ? : null} {getShortEventId(replay.id)} ); return ( {replay.user.display_name || t('Unknown User')} ) } user={getUserBadgeUser(replay)} // this is the subheading for the avatar, so displayEmail in this case is a misnomer displayEmail={subText} /> ); } const StyledIconDelete = styled(IconDelete)` margin: ${space(0.25)}; `; // Need to be full width for StringWalker to take up full width and truncate properly const UserBadgeFullWidth = styled(UserBadge)` width: 100%; `; const Cols = styled('div')` display: flex; flex-direction: column; gap: ${space(0.5)}; width: 100%; `; const Row = styled('div')<{gap: ValidSize; minWidth?: number}>` display: flex; gap: ${p => space(p.gap)}; align-items: center; ${p => (p.minWidth ? `min-width: ${p.minWidth}px;` : '')} `; const MainLink = styled(Link)` font-size: ${p => p.theme.fontSizeLarge}; `; export function TransactionCell({ organization, replay, }: Props & {organization: Organization}) { const location = useLocation(); if (replay.is_archived) { return ; } const hasTxEvent = 'txEvent' in replay; const txDuration = hasTxEvent ? replay.txEvent?.['transaction.duration'] : undefined; return hasTxEvent ? ( {txDuration ?
{txDuration}ms
: null} {spanOperationRelativeBreakdownRenderer( replay.txEvent, {organization, location}, {enableOnClick: false} )}
) : null; } export function OSCell({replay}: Props) { const {name, version} = replay.os ?? {}; const theme = useTheme(); const hasRoomForColumns = useMedia(`(min-width: ${theme.breakpoints.large})`); if (replay.is_archived) { return ; } return ( ); } export function BrowserCell({replay}: Props) { const {name, version} = replay.browser ?? {}; const theme = useTheme(); const hasRoomForColumns = useMedia(`(min-width: ${theme.breakpoints.large})`); if (replay.is_archived) { return ; } return ( ); } export function DurationCell({replay}: Props) { if (replay.is_archived) { return ; } return ( ); } export function RageClickCountCell({replay}: Props) { if (replay.is_archived) { return ; } return ( {replay.count_rage_clicks ? ( {replay.count_rage_clicks} ) : ( 0 )} ); } export function DeadClickCountCell({replay}: Props) { if (replay.is_archived) { return ; } return ( {replay.count_dead_clicks ? ( {replay.count_dead_clicks} ) : ( 0 )} ); } export function ErrorCountCell({replay}: Props) { if (replay.is_archived) { return ; } return ( {replay.count_errors ? ( {replay.count_errors} ) : ( 0 )} ); } export function ActivityCell({replay}: Props) { if (replay.is_archived) { return ; } const scoreBarPalette = new Array(10).fill([CHART_PALETTE[0][0]]); return ( ); } const Item = styled('div')<{isArchived?: boolean}>` display: flex; align-items: center; gap: ${space(1)}; padding: ${space(1.5)}; ${p => (p.isArchived ? 'opacity: 0.5;' : '')}; `; const Count = styled('span')` font-variant-numeric: tabular-nums; `; const ErrorCount = styled(Count)` display: flex; align-items: center; gap: ${space(0.5)}; color: ${p => p.theme.red400}; `; const Time = styled('span')` font-variant-numeric: tabular-nums; `; const SpanOperationBreakdown = styled('div')` width: 100%; display: flex; flex-direction: column; gap: ${space(0.5)}; color: ${p => p.theme.gray500}; font-size: ${p => p.theme.fontSizeMedium}; text-align: right; `;