From 9a04cc284e0a7a4d5a3437770b828aea41f49b68 Mon Sep 17 00:00:00 2001 From: jerryzhou196 Date: Tue, 2 Dec 2025 11:32:51 -0500 Subject: [PATCH 01/22] ref(replays): update placeholder dimensions and layout adjustments - Adjusted the dimensions of placeholders in various components for consistency. - Updated padding in the header for improved layout. - Refactored ReplayDetailsUserBadge to utilize a new layout structure and link query handling. - Added Flex container to enhance loading state presentation in ReplayDetailsMetadata. These changes aim to enhance the UI and maintain a cohesive design across replay components. --- .../replays/header/replayViewers.tsx | 2 +- .../header/replayDetailsHeaderActions.tsx | 2 +- .../detail/header/replayDetailsMetadata.tsx | 7 +- .../header/replayDetailsPageBreadcrumbs.tsx | 1 + .../detail/header/replayDetailsUserBadge.tsx | 153 ++++++------------ static/app/views/replays/details.tsx | 2 +- 6 files changed, 60 insertions(+), 107 deletions(-) diff --git a/static/app/components/replays/header/replayViewers.tsx b/static/app/components/replays/header/replayViewers.tsx index 7957949cbd8d3e..906ef3c9ca0652 100644 --- a/static/app/components/replays/header/replayViewers.tsx +++ b/static/app/components/replays/header/replayViewers.tsx @@ -29,7 +29,7 @@ export default function ReplayViewers({projectId, replayId}: Props) { }); return isPending || isError ? ( - + ) : ( ); diff --git a/static/app/views/replays/detail/header/replayDetailsHeaderActions.tsx b/static/app/views/replays/detail/header/replayDetailsHeaderActions.tsx index 5753a753a77782..ffd2e5cc15346a 100644 --- a/static/app/views/replays/detail/header/replayDetailsHeaderActions.tsx +++ b/static/app/views/replays/detail/header/replayDetailsHeaderActions.tsx @@ -20,7 +20,7 @@ export default function ReplayDetailsHeaderActions({readerResult}: Props) { renderArchived={() => null} renderError={() => null} renderThrottled={() => null} - renderLoading={() => } + renderLoading={() => } renderMissing={() => null} renderProcessingError={({replayRecord, projectSlug}) => ( diff --git a/static/app/views/replays/detail/header/replayDetailsMetadata.tsx b/static/app/views/replays/detail/header/replayDetailsMetadata.tsx index 747fb52e1f68f9..17b5db40f3aada 100644 --- a/static/app/views/replays/detail/header/replayDetailsMetadata.tsx +++ b/static/app/views/replays/detail/header/replayDetailsMetadata.tsx @@ -1,3 +1,4 @@ +import {Flex} from 'sentry/components/core/layout'; import Placeholder from 'sentry/components/placeholder'; import ReplayMetaData from 'sentry/components/replays/header/replayMetaData'; import ReplayLoadingState from 'sentry/components/replays/player/replayLoadingState'; @@ -14,7 +15,11 @@ export default function ReplayDetailsMetadata({readerResult}: Props) { renderArchived={() => null} renderError={() => null} renderThrottled={() => null} - renderLoading={() => } + renderLoading={() => ( + + + + )} renderMissing={() => null} renderProcessingError={() => null} > diff --git a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx index e5a41c96b025d2..1670c00b9cc84a 100644 --- a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx +++ b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx @@ -173,4 +173,5 @@ export default function ReplayDetailsPageBreadcrumbs({readerResult}: Props) { const StyledBreadcrumbs = styled(Breadcrumbs)` padding: 0; + height: 34px; `; diff --git a/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx b/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx index 537a6ce3604aab..c41039aba515ed 100644 --- a/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx +++ b/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx @@ -1,37 +1,25 @@ import styled from '@emotion/styled'; -import {Flex} from '@sentry/scraps/layout'; -import {Text} from '@sentry/scraps/text'; -import {Tooltip} from '@sentry/scraps/tooltip'; - import {Button} from 'sentry/components/core/button'; -import {Link} from 'sentry/components/core/link'; -import UserBadge from 'sentry/components/idBadge/userBadge'; -import * as Layout from 'sentry/components/layouts/thirds'; +import {Flex} from 'sentry/components/core/layout'; import Placeholder from 'sentry/components/placeholder'; import ReplayLoadingState from 'sentry/components/replays/player/replayLoadingState'; -import { - getLiveDurationMs, - getReplayExpiresAtMs, - LIVE_TOOLTIP_MESSAGE, - LiveIndicator, - useLiveRefresh, -} from 'sentry/components/replays/replayLiveIndicator'; -import TimeSince from 'sentry/components/timeSince'; -import {IconCalendar, IconRefresh} from 'sentry/icons'; +import {useLiveRefresh} from 'sentry/components/replays/replayLiveIndicator'; +import {ReplaySessionColumn} from 'sentry/components/replays/table/replayTableColumns'; +import {IconRefresh} from 'sentry/icons'; import {t} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; import type useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; +import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import {makeReplaysPathname} from 'sentry/views/replays/pathnames'; interface Props { readerResult: ReturnType; } - export default function ReplayDetailsUserBadge({readerResult}: Props) { const organization = useOrganization(); const replayRecord = readerResult.replayRecord; + const replayId = replayRecord?.id; const {shouldShowRefreshButton, doRefresh} = useLiveRefresh({replay: replayRecord}); // Generate search query based on available user data @@ -50,84 +38,48 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) { } return null; }; - const searchQuery = getUserSearchQuery(); - const userDisplayName = replayRecord?.user.display_name || t('Anonymous User'); - const isReplayExpired = - Date.now() > getReplayExpiresAtMs(replayRecord?.started_at ?? null); - - const showLiveIndicator = - !isReplayExpired && replayRecord && getLiveDurationMs(replayRecord.finished_at) > 0; + const location = useLocation(); + const linkQuery = searchQuery + ? { + pathname: makeReplaysPathname({ + path: '/', + organization, + }), + query: { + query: searchQuery, + }, + } + : { + pathname: makeReplaysPathname({ + path: `/${replayId}/`, + organization, + }), + query: { + ...location.query, + }, + }; const badge = replayRecord ? ( - - - {searchQuery ? ( - - {userDisplayName} - - ) : ( - userDisplayName - )} - - {replayRecord.started_at ? ( - - - - {showLiveIndicator ? ( - - - - {t('LIVE')} - - - - - ) : null} - - - ) : null} - - } - user={{ - name: replayRecord.user.display_name || '', - email: replayRecord.user.email || '', - username: replayRecord.user.username || '', - ip_address: replayRecord.user.ip || '', - id: replayRecord.user.id || '', - }} - hideEmail - /> + + + + ) : null; return ( @@ -137,7 +89,7 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) { renderError={() => null} renderThrottled={() => null} renderLoading={() => - replayRecord ? badge : + replayRecord ? badge : } renderMissing={() => null} renderProcessingError={() => badge} @@ -147,16 +99,11 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) { ); } -const TimeContainer = styled('div')` - display: flex; - gap: ${space(1)}; - align-items: center; - color: ${p => p.theme.subText}; - font-size: ${p => p.theme.fontSize.md}; - line-height: 1.4; +// column components expect to be stored in a relative container +const ColumnWrapper = styled(Flex)` + position: relative; `; -const DisplayHeader = styled('div')` - display: flex; - flex-direction: column; +const StyledReplaySessionColumn = styled(ReplaySessionColumn.Component)` + flex: 0; `; diff --git a/static/app/views/replays/details.tsx b/static/app/views/replays/details.tsx index ba850b00c644cf..041689aed13f45 100644 --- a/static/app/views/replays/details.tsx +++ b/static/app/views/replays/details.tsx @@ -125,7 +125,7 @@ const Header = styled(Layout.Header)` padding-bottom: ${space(1.5)}; @media (min-width: ${p => p.theme.breakpoints.md}) { gap: ${space(1)} ${space(3)}; - padding: ${space(2)} ${space(2)} ${space(1.5)} ${space(2)}; + padding: ${space(2)} ${space(1)} ${space(0.5)} ${space(2)}; } `; From e1af41f19011832a85d68fc57d3a4c40f40e679f Mon Sep 17 00:00:00 2001 From: jerryzhou196 Date: Tue, 2 Dec 2025 11:45:15 -0500 Subject: [PATCH 02/22] refactor(replay): update replay components to use linkQuery for navigation Modified ReplayPreviewPlayer and ReplayTable components to utilize linkQuery for constructing navigation paths. This change enhances the clarity of the code and ensures consistent handling of query parameters across replay-related components. --- static/app/components/replays/table/replayTableColumns.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/app/components/replays/table/replayTableColumns.tsx b/static/app/components/replays/table/replayTableColumns.tsx index a2992f9ecf27cf..4f0a33efeb7d1e 100644 --- a/static/app/components/replays/table/replayTableColumns.tsx +++ b/static/app/components/replays/table/replayTableColumns.tsx @@ -52,6 +52,7 @@ interface HeaderProps { interface CellProps { columnIndex: number; + linkQuery: string | LocationDescriptor; replay: ListRecord; rowIndex: number; showDropdownFilters: boolean; From 76554db38c20a45ed8adef293f2f66e86c421d2a Mon Sep 17 00:00:00 2001 From: jerryzhou196 Date: Tue, 2 Dec 2025 14:59:36 -0500 Subject: [PATCH 03/22] fix(replayDetailsUserBadge): Update linkQuery prop to 'to' for improved clarity --- .../app/views/replays/detail/header/replayDetailsUserBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx b/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx index c41039aba515ed..41b3d1d47fd4c8 100644 --- a/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx +++ b/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx @@ -68,7 +68,7 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) { rowIndex={0} columnIndex={0} showDropdownFilters={false} - linkQuery={linkQuery} + to={linkQuery} /> - + ) : null; return ( @@ -100,11 +99,84 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) { ); } -// column components expect to be stored in a relative container -const ColumnWrapper = styled(Flex)` - position: relative; -`; +/** + * Modified that is only used in header of Replay Details + */ +function ReplayBadge({replay}: {replay: ReplayRecord}) { + const [prefs] = useReplayPrefs(); + const timestampType = prefs.timestampType; + + const {isLive} = useLiveBadge({ + startedAt: replay.started_at, + finishedAt: replay.finished_at, + }); + + if (replay.is_archived) { + return ( + + + + + + + + {t('Deleted Replay')} + + + + ); + } -const StyledReplaySessionColumn = styled(ReplaySessionColumn.Component)` - flex: 0; -`; + invariant( + replay.started_at, + 'For TypeScript: replay.started_at is implied because replay.is_archived is false' + ); + + return ( + + + + + + {/* We use div here because the Text component has width 100% and will take up the + full width of the container, causing a gap between the text and the badge */} +
+ + {replay.user.display_name || t('Anonymous User')} + +
+ {isLive ? : null} +
+ + + + + + + {timestampType === 'absolute' ? ( + + ) : ( + + )} + + + + + + {replay.duration.humanize()} + + + +
+
+ ); +} From aa5810dc45d4a2a19a7a698b48c5acee8f517e21 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 5 Jan 2026 14:29:10 -0500 Subject: [PATCH 11/22] Revert "add referrer as prop" This reverts commit bf2f055164a4342c7d8effaa8f4cf79663d9fbbb. --- .../events/eventReplay/replayPreviewPlayer.tsx | 1 - static/app/components/replays/table/replayTable.tsx | 4 ---- .../app/components/replays/table/replayTableColumns.tsx | 6 ++---- static/app/utils/analytics/replayAnalyticsEvents.tsx | 9 +-------- .../app/views/issueDetails/groupReplays/groupReplays.tsx | 2 -- .../transactionReplays/transactionReplays.tsx | 1 - static/app/views/replays/list/replayIndexTable.tsx | 1 - 7 files changed, 3 insertions(+), 21 deletions(-) diff --git a/static/app/components/events/eventReplay/replayPreviewPlayer.tsx b/static/app/components/events/eventReplay/replayPreviewPlayer.tsx index 01e3700a59a92e..62c1185983a7f1 100644 --- a/static/app/components/events/eventReplay/replayPreviewPlayer.tsx +++ b/static/app/components/events/eventReplay/replayPreviewPlayer.tsx @@ -101,7 +101,6 @@ export default function ReplayPreviewPlayer({ )} ( { + Component: ({replay, to, className}) => { const routes = useRoutes(); const referrer = getRouteStringFromRoutes(routes); @@ -529,7 +527,7 @@ export const ReplaySessionColumn: ReplayTableColumn = { platform: project?.platform, organization, referrer, - referrer_table: referrerTable, + referrer_table: referrer === '/explore/replays/:replaySlug/' ? 'details' : 'main', }); return ( diff --git a/static/app/utils/analytics/replayAnalyticsEvents.tsx b/static/app/utils/analytics/replayAnalyticsEvents.tsx index 4918ce48e1209c..7b25e1dd658240 100644 --- a/static/app/utils/analytics/replayAnalyticsEvents.tsx +++ b/static/app/utils/analytics/replayAnalyticsEvents.tsx @@ -90,14 +90,7 @@ export type ReplayEventParameters = { platform: string | undefined; project_id: string | undefined; referrer: string; - referrer_table?: - | 'main' - | 'selector-widget' - | 'header' - | 'playlist' - | 'preview' - | 'transactions' - | 'issues'; + referrer_table?: 'main' | 'selector-widget' | 'details'; }; 'replay.list-paginated': { direction: 'next' | 'prev'; diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.tsx index c3a44e731b8cbb..7be23ecc6881f1 100644 --- a/static/app/views/issueDetails/groupReplays/groupReplays.tsx +++ b/static/app/views/issueDetails/groupReplays/groupReplays.tsx @@ -133,7 +133,6 @@ function GroupReplaysContent({group}: Props) { ) : ( Date: Mon, 5 Jan 2026 14:30:43 -0500 Subject: [PATCH 12/22] revert change to referrer_table --- static/app/components/replays/table/replayTableColumns.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/replays/table/replayTableColumns.tsx b/static/app/components/replays/table/replayTableColumns.tsx index 441eb8d811e513..a2992f9ecf27cf 100644 --- a/static/app/components/replays/table/replayTableColumns.tsx +++ b/static/app/components/replays/table/replayTableColumns.tsx @@ -527,7 +527,7 @@ export const ReplaySessionColumn: ReplayTableColumn = { platform: project?.platform, organization, referrer, - referrer_table: referrer === '/explore/replays/:replaySlug/' ? 'details' : 'main', + referrer_table: 'main', }); return ( From 3fc83da92c829ad7c66e97c6dfbd6edb470f5ec7 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 5 Jan 2026 15:05:52 -0500 Subject: [PATCH 13/22] move refresh button --- .../detail/header/replayDetailsUserBadge.tsx | 113 ++++++++++-------- 1 file changed, 61 insertions(+), 52 deletions(-) diff --git a/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx b/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx index 5e50729eca33b3..7a4891ada546a5 100644 --- a/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx +++ b/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx @@ -1,4 +1,3 @@ -import styled from '@emotion/styled'; import invariant from 'invariant'; import {Flex} from '@sentry/scraps/layout'; @@ -6,6 +5,7 @@ import {Flex} from '@sentry/scraps/layout'; import {UserAvatar} from 'sentry/components/core/avatar/userAvatar'; import {Button} from 'sentry/components/core/button'; import {Grid} from 'sentry/components/core/layout'; +import {Link} from 'sentry/components/core/link'; import {Text} from 'sentry/components/core/text'; import {DateTime} from 'sentry/components/dateTime'; import Placeholder from 'sentry/components/placeholder'; @@ -30,55 +30,11 @@ interface Props { readerResult: ReturnType; } export default function ReplayDetailsUserBadge({readerResult}: Props) { - const organization = useOrganization(); const replayRecord = readerResult.replayRecord; - const {shouldShowRefreshButton, doRefresh} = useLiveRefresh({replay: replayRecord}); - - // Generate search query based on available user data - const getUserSearchQuery = () => { - if (!replayRecord?.user) { - return null; - } - - const user = replayRecord.user; - // Prefer email over id for search query - if (user.email) { - return `user.email:"${user.email}"`; - } - if (user.id) { - return `user.id:"${user.id}"`; - } - return null; - }; - const searchQuery = getUserSearchQuery(); - - const linkQuery = searchQuery - ? { - pathname: makeReplaysPathname({ - path: '/', - organization, - }), - query: { - query: searchQuery, - }, - } - : null; const badge = replayRecord ? ( - ) : null; @@ -103,8 +59,10 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) { * Modified that is only used in header of Replay Details */ function ReplayBadge({replay}: {replay: ReplayRecord}) { + const organization = useOrganization(); const [prefs] = useReplayPrefs(); const timestampType = prefs.timestampType; + const {shouldShowRefreshButton, doRefresh} = useLiveRefresh({replay}); const {isLive} = useLiveBadge({ startedAt: replay.started_at, @@ -115,7 +73,7 @@ function ReplayBadge({replay}: {replay: ReplayRecord}) { return ( - + @@ -132,6 +90,27 @@ function ReplayBadge({replay}: {replay: ReplayRecord}) { 'For TypeScript: replay.started_at is implied because replay.is_archived is false' ); + // Generate search query based on available user data + const searchQuery = getUserSearchQuery({user: replay.user}); + + const replaysIndexUrl = searchQuery + ? { + pathname: makeReplaysPathname({ + path: '/', + organization, + }), + query: { + query: searchQuery, + }, + } + : null; + + const replaysIndexLinkText = ( + + {replay.user.display_name || t('Anonymous User')} + + ); + return ( - + {/* We use div here because the Text component has width 100% and will take up the full width of the container, causing a gap between the text and the badge */} -
- - {replay.user.display_name || t('Anonymous User')} - -
+ {replaysIndexUrl ? ( + {replaysIndexLinkText} + ) : ( +
{replaysIndexLinkText}
+ )} + {isLive ? : null} + {shouldShowRefreshButton ? ( + + ) : null}
@@ -180,3 +174,18 @@ function ReplayBadge({replay}: {replay: ReplayRecord}) { ); } + +function getUserSearchQuery({user}: {user: ReplayRecord['user']}) { + if (!user) { + return null; + } + + // Prefer email over id for search query + if (user.email) { + return `user.email:"${user.email}"`; + } + if (user.id) { + return `user.id:"${user.id}"`; + } + return null; +} From f1d3a71268194cb40c90fd00e3f7c08fe704bb2f Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 5 Jan 2026 15:08:05 -0500 Subject: [PATCH 14/22] revert change to referrer_table --- static/app/utils/analytics/replayAnalyticsEvents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/utils/analytics/replayAnalyticsEvents.tsx b/static/app/utils/analytics/replayAnalyticsEvents.tsx index 7b25e1dd658240..b0f2658351033e 100644 --- a/static/app/utils/analytics/replayAnalyticsEvents.tsx +++ b/static/app/utils/analytics/replayAnalyticsEvents.tsx @@ -90,7 +90,7 @@ export type ReplayEventParameters = { platform: string | undefined; project_id: string | undefined; referrer: string; - referrer_table?: 'main' | 'selector-widget' | 'details'; + referrer_table?: 'main' | 'selector-widget'; }; 'replay.list-paginated': { direction: 'next' | 'prev'; From ef7332c43e230d59df0e27e20106bf2c89e80750 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 5 Jan 2026 15:51:30 -0500 Subject: [PATCH 15/22] clean up css + live --- static/app/components/replays/replayBadge.tsx | 4 ---- static/app/components/replays/replayLiveIndicator.tsx | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/static/app/components/replays/replayBadge.tsx b/static/app/components/replays/replayBadge.tsx index b3f07acc56362f..52892319fcfd52 100644 --- a/static/app/components/replays/replayBadge.tsx +++ b/static/app/components/replays/replayBadge.tsx @@ -113,7 +113,3 @@ export default function ReplayBadge({replay}: Props) { const Wrapper = styled(Grid)` white-space: nowrap; `; - -const StyledText = styled(Text)` - position: relative; -`; diff --git a/static/app/components/replays/replayLiveIndicator.tsx b/static/app/components/replays/replayLiveIndicator.tsx index e592b2153c5599..4a817d92a0f081 100644 --- a/static/app/components/replays/replayLiveIndicator.tsx +++ b/static/app/components/replays/replayLiveIndicator.tsx @@ -72,12 +72,12 @@ const LiveIndicator = styled('div')` export function LiveBadge() { return ( - - {t('LIVE')} - + + {t('LIVE')} + ); } From b85a0360b93ea214b531c21bfc792241884d827e Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 5 Jan 2026 15:53:29 -0500 Subject: [PATCH 16/22] update comment --- static/app/components/replays/replayBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/replays/replayBadge.tsx b/static/app/components/replays/replayBadge.tsx index 52892319fcfd52..f562681c69b23b 100644 --- a/static/app/components/replays/replayBadge.tsx +++ b/static/app/components/replays/replayBadge.tsx @@ -74,7 +74,7 @@ export default function ReplayBadge({replay}: Props) { - {/* We use div here because the Text component doesn't have display: block */} + {/* We use div here because the Text component has 100% width and will push live indicator to the far right */}
{replay.user.display_name || t('Anonymous User')} From 661227fb39246f880c29a53996ba54b240bec9d6 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 5 Jan 2026 16:38:28 -0500 Subject: [PATCH 17/22] remove fixed height, cleanups, remove ui feature flag, not needed --- static/app/views/replays/details.tsx | 48 +++++++--------------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/static/app/views/replays/details.tsx b/static/app/views/replays/details.tsx index 041689aed13f45..ba17406eedad32 100644 --- a/static/app/views/replays/details.tsx +++ b/static/app/views/replays/details.tsx @@ -12,7 +12,6 @@ import { } from 'sentry/components/replays/replayAccess'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {t} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; import {decodeScalar} from 'sentry/utils/queryString'; import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import useReplayPageview from 'sentry/utils/replays/hooks/useReplayPageview'; @@ -35,7 +34,9 @@ export default function ReplayDetails() { - {t('Replay Details')} + + {t('Replay Details')} + @@ -79,30 +80,20 @@ function ReplayDetailsContent() { ? `${replayRecord.user.display_name ?? t('Anonymous User')} — Session Replay — ${orgSlug}` : `Session Replay — ${orgSlug}`; - const content = organization.features.includes('replay-details-new-ui') ? ( + const content = ( - + - - + + - + - ) : ( - -
- - - - -
- -
); return ( @@ -120,28 +111,13 @@ function ReplayDetailsContent() { ); } -const Header = styled(Layout.Header)` - gap: ${space(1)}; - padding-bottom: ${space(1.5)}; - @media (min-width: ${p => p.theme.breakpoints.md}) { - gap: ${space(1)} ${space(3)}; - padding: ${space(2)} ${space(1)} ${space(0.5)} ${space(2)}; - } -`; - -const NewTopHeader = styled('div')` - padding-left: ${p => p.theme.space.lg}; - padding-right: ${p => p.theme.space.lg}; +const TopHeader = styled(Flex)` + padding: ${p => p.theme.space.sm} ${p => p.theme.space.lg}; border-bottom: 1px solid ${p => p.theme.innerBorder}; - display: flex; - align-items: center; - justify-content: space-between; - gap: ${space(1)}; - flex-flow: row wrap; - height: 44px; + flex-wrap: wrap; `; -const NewBottonHeader = styled(Flex)` +const BottonHeader = styled(Flex)` padding: ${p => p.theme.space.md} ${p => p.theme.space.lg}; border-bottom: 1px solid ${p => p.theme.innerBorder}; `; From 0ea717e83a1293ae811ae16ca36e61433803a5f9 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 5 Jan 2026 16:42:02 -0500 Subject: [PATCH 18/22] remove exports --- static/app/components/replays/replayLiveIndicator.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/components/replays/replayLiveIndicator.tsx b/static/app/components/replays/replayLiveIndicator.tsx index 4a817d92a0f081..a64274b446f6e7 100644 --- a/static/app/components/replays/replayLiveIndicator.tsx +++ b/static/app/components/replays/replayLiveIndicator.tsx @@ -18,12 +18,12 @@ import type {ReplayRecord} from 'sentry/views/replays/types'; const LIVE_TOOLTIP_MESSAGE = t('This replay is in progress.'); -export function getReplayExpiresAtMs(startedAt: ReplayRecord['started_at']): number { +function getReplayExpiresAtMs(startedAt: ReplayRecord['started_at']): number { const ONE_HOUR_MS = 3_600_000; return startedAt ? startedAt.getTime() + ONE_HOUR_MS : 0; } -export function getLiveDurationMs(finishedAt: ReplayRecord['finished_at']): number { +function getLiveDurationMs(finishedAt: ReplayRecord['finished_at']): number { if (!finishedAt) { return 0; } From a1af84502f88286a2c743470a7873d997f21af5d Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 5 Jan 2026 18:11:53 -0500 Subject: [PATCH 19/22] fix tests --- .../replays/detail/header/replayDetailsUserBadge.spec.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/app/views/replays/detail/header/replayDetailsUserBadge.spec.tsx b/static/app/views/replays/detail/header/replayDetailsUserBadge.spec.tsx index 4a570cdfac9950..5d3a16e11f38d7 100644 --- a/static/app/views/replays/detail/header/replayDetailsUserBadge.spec.tsx +++ b/static/app/views/replays/detail/header/replayDetailsUserBadge.spec.tsx @@ -80,7 +80,7 @@ describe('replayDetailsUserBadge', () => { ); render(); - expect(screen.queryByTestId('refresh-button')).not.toBeVisible(); + expect(screen.queryByTestId('refresh-button')).not.toBeInTheDocument(); }); it('should show refresh button when replay record is outdated', async () => { @@ -140,7 +140,7 @@ describe('replayDetailsUserBadge', () => { render(, {organization}); - expect(screen.queryByTestId('refresh-button')).not.toBeVisible(); + expect(screen.queryByTestId('refresh-button')).not.toBeInTheDocument(); const updatedReplayRecord = replayRecordFixture({ started_at: STARTED_AT, @@ -386,7 +386,7 @@ describe('replayDetailsUserBadge', () => { ) ); - expect(screen.queryByTestId('refresh-button')).not.toBeVisible(); + expect(screen.queryByTestId('refresh-button')).not.toBeInTheDocument(); const updatedReplayRecord = replayRecordFixture({ started_at: STARTED_AT, From 6791b011c7bf4e28d6be6e14e19d5d5933f73085 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 6 Jan 2026 10:49:16 -0500 Subject: [PATCH 20/22] design changes after chat w/ jesse --- .../replays/replayLiveIndicator.tsx | 2 +- .../header/replayDetailsPageBreadcrumbs.tsx | 47 ++++++++++++++----- .../detail/header/replayDetailsUserBadge.tsx | 32 +------------ 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/static/app/components/replays/replayLiveIndicator.tsx b/static/app/components/replays/replayLiveIndicator.tsx index a64274b446f6e7..5cf683b4b58313 100644 --- a/static/app/components/replays/replayLiveIndicator.tsx +++ b/static/app/components/replays/replayLiveIndicator.tsx @@ -76,7 +76,7 @@ export function LiveBadge() { - {t('LIVE')} + {t('Live')} ); diff --git a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx index 1670c00b9cc84a..7b2cfc514552c4 100644 --- a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx +++ b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx @@ -1,6 +1,8 @@ import {useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; +import {Text} from '@sentry/scraps/text'; + import {Breadcrumbs} from 'sentry/components/breadcrumbs'; import {Button} from 'sentry/components/core/button'; import {LinkButton} from 'sentry/components/core/button/linkButton'; @@ -8,7 +10,8 @@ import {Flex} from 'sentry/components/core/layout'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import Placeholder from 'sentry/components/placeholder'; import {useReplayContext} from 'sentry/components/replays/replayContext'; -import {IconChevron, IconCopy} from 'sentry/icons'; +import {useLiveRefresh} from 'sentry/components/replays/replayLiveIndicator'; +import {IconChevron, IconCopy, IconRefresh} from 'sentry/icons'; import {t} from 'sentry/locale'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -36,6 +39,9 @@ export default function ReplayDetailsPageBreadcrumbs({readerResult}: Props) { const {currentTime} = useReplayContext(); const {replays, currentReplayIndex} = useReplayPlaylist(); + const {shouldShowRefreshButton, doRefresh} = useLiveRefresh({ + replay: replayRecord ?? undefined, + }); // We use a ref to store the initial location so that we can use it to navigate to the previous and next replays // without dirtying the URL with the URL params from the tabs navigation. @@ -145,20 +151,35 @@ export default function ReplayDetailsPageBreadcrumbs({readerResult}: Props) { > {getShortEventId(replayRecord?.id)}
+ {isHovered && ( + + ) : null}
) : ( diff --git a/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx b/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx index 7a4891ada546a5..6458f7dbbb1af4 100644 --- a/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx +++ b/static/app/views/replays/detail/header/replayDetailsUserBadge.tsx @@ -3,20 +3,14 @@ import invariant from 'invariant'; import {Flex} from '@sentry/scraps/layout'; import {UserAvatar} from 'sentry/components/core/avatar/userAvatar'; -import {Button} from 'sentry/components/core/button'; import {Grid} from 'sentry/components/core/layout'; import {Link} from 'sentry/components/core/link'; import {Text} from 'sentry/components/core/text'; import {DateTime} from 'sentry/components/dateTime'; import Placeholder from 'sentry/components/placeholder'; import ReplayLoadingState from 'sentry/components/replays/player/replayLoadingState'; -import { - LiveBadge, - useLiveBadge, - useLiveRefresh, -} from 'sentry/components/replays/replayLiveIndicator'; +import {LiveBadge, useLiveBadge} from 'sentry/components/replays/replayLiveIndicator'; import TimeSince from 'sentry/components/timeSince'; -import {IconRefresh, IconTimer} from 'sentry/icons'; import {IconCalendar} from 'sentry/icons/iconCalendar'; import {IconDelete} from 'sentry/icons/iconDelete'; import {t} from 'sentry/locale'; @@ -62,7 +56,6 @@ function ReplayBadge({replay}: {replay: ReplayRecord}) { const organization = useOrganization(); const [prefs] = useReplayPrefs(); const timestampType = prefs.timestampType; - const {shouldShowRefreshButton, doRefresh} = useLiveRefresh({replay}); const {isLive} = useLiveBadge({ startedAt: replay.started_at, @@ -133,22 +126,6 @@ function ReplayBadge({replay}: {replay: ReplayRecord}) { ) : (
{replaysIndexLinkText}
)} - - {isLive ? : null} - {shouldShowRefreshButton ? ( - - ) : null}
@@ -163,12 +140,7 @@ function ReplayBadge({replay}: {replay: ReplayRecord}) { )} - - - - {replay.duration.humanize()} - - + {isLive ? : null}
From 3075c5e71500449dca19ebe09b6b6ee89cc6bef2 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 6 Jan 2026 11:11:37 -0500 Subject: [PATCH 21/22] accent + font size --- .../replays/detail/header/replayDetailsPageBreadcrumbs.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx index 7b2cfc514552c4..61ce009ef38053 100644 --- a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx +++ b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx @@ -173,9 +173,9 @@ export default function ReplayDetailsPageBreadcrumbs({readerResult}: Props) { size="zero" priority="link" onClick={doRefresh} - icon={} + icon={} > - + {t('Update')} From 6aac250d2735a7befe1a877db69e10e85af5b604 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 6 Jan 2026 13:06:11 -0500 Subject: [PATCH 22/22] update tests --- .../replays/replayLiveIndicator.spec.tsx | 225 +++++++++++++ .../header/replayDetailsUserBadge.spec.tsx | 295 +----------------- 2 files changed, 231 insertions(+), 289 deletions(-) create mode 100644 static/app/components/replays/replayLiveIndicator.spec.tsx diff --git a/static/app/components/replays/replayLiveIndicator.spec.tsx b/static/app/components/replays/replayLiveIndicator.spec.tsx new file mode 100644 index 00000000000000..25e21426dd50d6 --- /dev/null +++ b/static/app/components/replays/replayLiveIndicator.spec.tsx @@ -0,0 +1,225 @@ +import {act} from 'react'; +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; + +import {makeTestQueryClient} from 'sentry-test/queryClient'; +import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary'; + +import {QueryClientProvider} from 'sentry/utils/queryClient'; +import {OrganizationContext} from 'sentry/views/organizationContext'; + +import {useLiveBadge, useLiveRefresh} from './replayLiveIndicator'; + +jest.mock('sentry/views/replays/detail/ai/replaySummaryContext', () => ({ + useReplaySummaryContext: () => ({ + startSummaryRequest: jest.fn(), + }), +})); + +jest.mock('sentry/utils/replays/hooks/useReplayProjectSlug', () => ({ + useReplayProjectSlug: () => 'test-project', +})); + +jest.useFakeTimers(); + +describe('useLiveBadge', () => { + it('should return isLive=true when replay finished within 5 minutes', () => { + const now = Date.now(); + const startedAt = new Date(now - 60_000); // 1 minute ago + const finishedAt = new Date(now); // just now + + const {result} = renderHook(() => + useLiveBadge({ + startedAt, + finishedAt, + }) + ); + + expect(result.current.isLive).toBe(true); + }); + + it('should return isLive=false when replay finished more than 5 minutes ago', () => { + const now = Date.now(); + const startedAt = new Date(now - 10 * 60_000); // 10 minutes ago + const finishedAt = new Date(now - 6 * 60_000); // 6 minutes ago (more than 5 min threshold) + + const {result} = renderHook(() => + useLiveBadge({ + startedAt, + finishedAt, + }) + ); + + expect(result.current.isLive).toBe(false); + }); + + it('should return isLive=false when replay has expired (started more than 1 hour ago)', () => { + const now = Date.now(); + const startedAt = new Date(now - 2 * 60 * 60_000); // 2 hours ago + const finishedAt = new Date(now); // just now + + const {result} = renderHook(() => + useLiveBadge({ + startedAt, + finishedAt, + }) + ); + + expect(result.current.isLive).toBe(false); + }); + + it('should transition from isLive=true to isLive=false after 5 minutes', async () => { + const now = Date.now(); + const startedAt = new Date(now - 60_000); // 1 minute ago + const finishedAt = new Date(now); // just now + + const {result} = renderHook(() => + useLiveBadge({ + startedAt, + finishedAt, + }) + ); + + expect(result.current.isLive).toBe(true); + + // Advance time by 5 minutes + 1ms + await act(async () => { + await jest.advanceTimersByTimeAsync(5 * 60 * 1000 + 1); + }); + + expect(result.current.isLive).toBe(false); + }); + + it('should return isLive=false when finishedAt is null', () => { + const now = Date.now(); + const startedAt = new Date(now - 60_000); + + const {result} = renderHook(() => + useLiveBadge({ + startedAt, + finishedAt: null, + }) + ); + + expect(result.current.isLive).toBe(false); + }); +}); + +describe('useLiveRefresh', () => { + const organization = OrganizationFixture(); + + function createWrapper() { + const queryClient = makeTestQueryClient(); + return function Wrapper({children}: {children: React.ReactNode}) { + return ( + + {children} + + ); + }; + } + + beforeEach(() => { + MockApiClient.clearMockResponses(); + }); + + it('should not show refresh button when replay is undefined', () => { + const {result} = renderHook(() => useLiveRefresh({replay: undefined}), { + wrapper: createWrapper(), + }); + + expect(result.current.shouldShowRefreshButton).toBe(false); + }); + + it('should not show refresh button initially when polled segments equals current segments', () => { + const replay = ReplayRecordFixture({ + count_segments: 5, + }); + + // Mock the polling endpoint to return same segment count + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/replays/${replay.id}/`, + body: {data: replay}, + }); + + const {result} = renderHook(() => useLiveRefresh({replay}), { + wrapper: createWrapper(), + }); + + // Initial state - no refresh button since polled and current are equal + expect(result.current.shouldShowRefreshButton).toBe(false); + }); + + it('should show refresh button when polled segments is greater than current segments', async () => { + const now = Date.now(); + const replay = ReplayRecordFixture({ + started_at: new Date(now - 60_000), // 1 minute ago (not expired) + count_segments: 5, + }); + + const updatedReplay = ReplayRecordFixture({ + ...replay, + count_segments: 10, + }); + + // Mock the polling endpoint to return updated segment count + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/replays/${replay.id}/`, + body: {data: updatedReplay}, + }); + + const {result} = renderHook(() => useLiveRefresh({replay}), { + wrapper: createWrapper(), + }); + + // Wait for the API call to complete and state to update + await waitFor(() => { + expect(result.current.shouldShowRefreshButton).toBe(true); + }); + }); + + it('should not poll when replay has expired (started more than 1 hour ago)', async () => { + const now = Date.now(); + const replay = ReplayRecordFixture({ + started_at: new Date(now - 2 * 60 * 60_000), // 2 hours ago (expired) + count_segments: 5, + }); + + const replayEndpoint = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/replays/${replay.id}/`, + body: {data: {...replay, count_segments: 10}}, + }); + + renderHook(() => useLiveRefresh({replay}), { + wrapper: createWrapper(), + }); + + // Advance time past polling interval + await act(async () => { + await jest.advanceTimersByTimeAsync(30_000 + 1); + }); + + // Polling should not happen for expired replays + expect(replayEndpoint).not.toHaveBeenCalled(); + }); + + it('should provide a doRefresh function that can be called', () => { + const now = Date.now(); + const replay = ReplayRecordFixture({ + started_at: new Date(now - 60_000), + count_segments: 5, + }); + + const updateMock = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/replays/${replay.id}/`, + body: {data: replay}, + }); + + const {result} = renderHook(() => useLiveRefresh({replay}), { + wrapper: createWrapper(), + }); + + result.current.doRefresh(); + expect(updateMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/static/app/views/replays/detail/header/replayDetailsUserBadge.spec.tsx b/static/app/views/replays/detail/header/replayDetailsUserBadge.spec.tsx index 5d3a16e11f38d7..c200bc03acb906 100644 --- a/static/app/views/replays/detail/header/replayDetailsUserBadge.spec.tsx +++ b/static/app/views/replays/detail/header/replayDetailsUserBadge.spec.tsx @@ -1,10 +1,7 @@ import {act, type ReactNode} from 'react'; import {duration} from 'moment-timezone'; import {OrganizationFixture} from 'sentry-fixture/organization'; -import { - ReplayConsoleEventFixture, - ReplayNavigateEventFixture, -} from 'sentry-fixture/replay/helpers'; +import {ReplayNavigateEventFixture} from 'sentry-fixture/replay/helpers'; import {RRWebInitFrameEventsFixture} from 'sentry-fixture/replay/rrweb'; import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; @@ -22,11 +19,8 @@ const {organization, project} = initializeOrg({ organization: OrganizationFixture({}), }); -const mockInvalidateQueries = jest.fn(); - function wrapper({children}: {children?: ReactNode}) { const queryClient = makeTestQueryClient(); - queryClient.invalidateQueries = mockInvalidateQueries; return ( {children} @@ -45,138 +39,6 @@ jest.useFakeTimers(); describe('replayDetailsUserBadge', () => { beforeEach(() => { MockApiClient.clearMockResponses(); - mockInvalidateQueries.mockClear(); - }); - it('should not show refresh button on initial render', async () => { - const replayRecord = replayRecordFixture({count_segments: 100}); - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/replays/${replayRecord.id}/`, - body: {data: replayRecord}, - }); - - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/replays-events-meta/`, - body: { - data: [], - }, - headers: { - Link: [ - '; rel="previous"; results="false"; cursor="0:1:0"', - '; rel="next"; results="false"; cursor="0:1:0"', - ].join(','), - }, - }); - - const {result} = renderHook(useLoadReplayReader, { - wrapper, - initialProps: { - orgSlug: organization.slug, - replaySlug: `${project.slug}:${replayRecord.id}`, - }, - }); - - await waitFor(() => - expect(result.current.replayRecord?.count_segments).toBeDefined() - ); - - render(); - expect(screen.queryByTestId('refresh-button')).not.toBeInTheDocument(); - }); - - it('should show refresh button when replay record is outdated', async () => { - const now = Date.now(); - const STARTED_AT = new Date(now - 10 * 1000); - const FINISHED_AT_FIVE_SECONDS = new Date(now - 5 * 1000); - const FINISHED_AT_TEN_SECONDS = new Date(now); - - const replayRecord = replayRecordFixture({ - started_at: STARTED_AT, - finished_at: FINISHED_AT_FIVE_SECONDS, - duration: duration(5, 'seconds'), - count_errors: 0, - count_segments: 1, - error_ids: [], - }); - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/replays/${replayRecord.id}/`, - body: {data: replayRecord}, - }); - - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/replays-events-meta/`, - body: { - data: [], - }, - headers: { - Link: [ - '; rel="previous"; results="false"; cursor="0:1:0"', - '; rel="next"; results="false"; cursor="0:1:0"', - ].join(','), - }, - }); - - MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/replays/${replayRecord.id}/recording-segments/`, - body: [ - RRWebInitFrameEventsFixture({ - timestamp: STARTED_AT, - }), - ReplayConsoleEventFixture({timestamp: STARTED_AT}), - ], - match: [(_url, options) => options.query?.cursor === '0:0:0'], - }); - - const {result} = renderHook(useLoadReplayReader, { - wrapper, - initialProps: { - orgSlug: organization.slug, - replaySlug: `${project.slug}:${replayRecord.id}`, - }, - }); - - await waitFor(() => - expect(result.current.replayRecord?.count_segments).toBeDefined() - ); - - render(, {organization}); - - expect(screen.queryByTestId('refresh-button')).not.toBeInTheDocument(); - - const updatedReplayRecord = replayRecordFixture({ - started_at: STARTED_AT, - finished_at: FINISHED_AT_TEN_SECONDS, - duration: duration(10, 'seconds'), - count_errors: 0, - count_segments: 2, - error_ids: [], - }); - - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/replays/${updatedReplayRecord.id}/`, - body: {data: updatedReplayRecord}, - }); - - MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/replays/${updatedReplayRecord.id}/recording-segments/`, - body: [ - RRWebInitFrameEventsFixture({ - timestamp: STARTED_AT, - }), - ReplayConsoleEventFixture({timestamp: STARTED_AT}), - ReplayNavigateEventFixture({ - startTimestamp: STARTED_AT, - endTimestamp: FINISHED_AT_TEN_SECONDS, - }), - ], - match: [(_url, options) => options.query?.cursor === '0:0:0'], - }); - - await act(async () => { - // advance to next polling interval - await jest.advanceTimersByTimeAsync(1000 * 30 + 1); - }); - - expect(screen.queryByTestId('refresh-button')).toBeVisible(); }); it('should show LIVE badge when last received segment is within 5 minutes', async () => { @@ -190,7 +52,7 @@ describe('replayDetailsUserBadge', () => { count_segments: 1, error_ids: [], }); - const replayRecordEndpoint = MockApiClient.addMockResponse({ + MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/replays/${replayRecord.id}/`, body: {data: replayRecord}, }); @@ -232,8 +94,7 @@ describe('replayDetailsUserBadge', () => { render(, {organization}); - await waitFor(() => expect(replayRecordEndpoint).toHaveBeenCalledTimes(2)); - expect(screen.queryByTestId('live-badge')).toBeVisible(); + expect(screen.getByTestId('live-badge')).toBeVisible(); }); it('should hide LIVE badge when last received segment is more than 5 minutes ago', async () => { @@ -249,7 +110,7 @@ describe('replayDetailsUserBadge', () => { count_segments: 1, error_ids: [], }); - const replayRecordEndpoint = MockApiClient.addMockResponse({ + MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/replays/${replayRecord.id}/`, body: {data: replayRecord}, }); @@ -289,162 +150,18 @@ describe('replayDetailsUserBadge', () => { }, }); - // useLoadReplayReader calls this endpoint - await waitFor(() => expect(replayRecordEndpoint).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(result.current.replayRecord?.count_segments).toBeDefined() ); render(, {organization}); - // usePollReplayRecord calls this endpoint - await waitFor(() => expect(replayRecordEndpoint).toHaveBeenCalledTimes(2)); - expect(screen.queryByTestId('live-badge')).toBeVisible(); + // Live badge should be visible initially + expect(screen.getByTestId('live-badge')).toBeVisible(); // let 5 minutes and 1/1000 second pass await act(async () => jest.advanceTimersByTimeAsync(5 * 60 * 1000 + 1)); expect(screen.queryByTestId('live-badge')).not.toBeInTheDocument(); }); - - it('should cause useLoadReplayReader to refetch when the refresh button is pressed', async () => { - const queryClient = makeTestQueryClient(); - queryClient.invalidateQueries = mockInvalidateQueries; - - function sharedQueryClientWrapper({children}: {children?: ReactNode}) { - return ( - - {children} - - ); - } - const now = Date.now(); - const STARTED_AT = new Date(now - 10 * 1000); - const FINISHED_AT_FIVE_SECONDS = new Date(now - 5 * 1000); - const FINISHED_AT_TEN_SECONDS = new Date(now); - - const replayRecord = replayRecordFixture({ - started_at: STARTED_AT, - finished_at: FINISHED_AT_FIVE_SECONDS, - duration: duration(5, 'seconds'), - count_errors: 0, - count_segments: 1, - error_ids: [], - }); - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/replays/${replayRecord.id}/`, - body: {data: replayRecord}, - }); - - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/replays-events-meta/`, - body: { - data: [], - }, - headers: { - Link: [ - '; rel="previous"; results="false"; cursor="0:1:0"', - '; rel="next"; results="false"; cursor="0:1:0"', - ].join(','), - }, - }); - - MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/replays/${replayRecord.id}/recording-segments/`, - body: [ - RRWebInitFrameEventsFixture({ - timestamp: STARTED_AT, - }), - ReplayConsoleEventFixture({timestamp: STARTED_AT}), - ], - match: [(_url, options) => options.query?.cursor === '0:0:0'], - }); - - const {result} = await act(() => - renderHook(useLoadReplayReader, { - wrapper: sharedQueryClientWrapper, - initialProps: { - orgSlug: organization.slug, - replaySlug: `${project.slug}:${replayRecord.id}`, - }, - }) - ); - - await waitFor(() => - expect(result.current.replayRecord?.count_segments).toBeDefined() - ); - - act(() => - render( - sharedQueryClientWrapper({ - children: , - }), - { - organization, - } - ) - ); - - expect(screen.queryByTestId('refresh-button')).not.toBeInTheDocument(); - - const updatedReplayRecord = replayRecordFixture({ - started_at: STARTED_AT, - finished_at: FINISHED_AT_TEN_SECONDS, - duration: duration(10, 'seconds'), - count_errors: 0, - count_segments: 2, - error_ids: [], - }); - - const updatedReplayRecordEndpoint = MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/replays/${updatedReplayRecord.id}/`, - body: {data: updatedReplayRecord}, - }); - - MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/replays/${updatedReplayRecord.id}/recording-segments/`, - body: [ - RRWebInitFrameEventsFixture({ - timestamp: STARTED_AT, - }), - ReplayConsoleEventFixture({timestamp: STARTED_AT}), - ReplayNavigateEventFixture({ - startTimestamp: STARTED_AT, - endTimestamp: FINISHED_AT_TEN_SECONDS, - }), - ], - match: [(_url, options) => options.query?.cursor === '0:0:0'], - }); - - MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/replays-events-meta/`, - body: { - data: [], - }, - headers: { - Link: [ - '; rel="previous"; results="false"; cursor="0:1:0"', - '; rel="next"; results="false"; cursor="0:1:0"', - ].join(','), - }, - }); - - await act(async () => { - // advance to next polling interval - await jest.advanceTimersByTimeAsync(1000 * 30 + 1); - }); - - await waitFor(() => expect(updatedReplayRecordEndpoint).toHaveBeenCalledTimes(1)); - - act(() => { - screen.queryByTestId('refresh-button')?.click(); - }); - - expect(updatedReplayRecordEndpoint).toHaveBeenCalledTimes(2); - - await waitFor(() => - expect(result.current.replayRecord?.finished_at).toEqual(FINISHED_AT_TEN_SECONDS) - ); - }); });