Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
e336a81
feat(replays): Enhance replay details navigation with start and end t…
jerryzhou196 Nov 3, 2025
76b424d
ref(replays): Simplify ReplayDetailsUserBadge component. Removed unus…
jerryzhou196 Nov 3, 2025
81f8b76
ref(replays): Update ReplayDetails and Breadcrumbs for improved navig…
jerryzhou196 Nov 3, 2025
0481f7e
ref(replays): Update replay navigation icons and structure
jerryzhou196 Nov 3, 2025
b92a135
ref(replays): Simplify replay time handling in ReplayTable and relate…
jerryzhou196 Nov 3, 2025
dda816f
Merge branch 'master' into replay-carousal
jerryzhou196 Nov 4, 2025
411821b
Merge branch 'master' into replay-carousal
jerryzhou196 Nov 5, 2025
ba35080
fix(replay-table): Enhance sorting functionality and update query par…
jerryzhou196 Nov 5, 2025
24c97a4
Merge branch 'master' into replay-carousal
jerryzhou196 Nov 5, 2025
d2ecfbf
fix(replay): fix typo in DEFAULT_SORT
jerryzhou196 Nov 5, 2025
8f60afe
Merge remote-tracking branch 'origin/master' into replay-carousal
jerryzhou196 Nov 6, 2025
7a46ed8
refactor(replayTable): Rename 'start' to 'end' for replay timestamps
jerryzhou196 Nov 6, 2025
0b6ba8d
fix(replayTable): Update comment for sort query string handling
jerryzhou196 Nov 6, 2025
8383216
feat(replayDetails): remove location.query
jerryzhou196 Nov 6, 2025
d70cc2d
refactor(replayTable): Remove unused end timestamp and update query h…
jerryzhou196 Nov 6, 2025
6277a38
added left margin to buttons
jerryzhou196 Nov 7, 2025
f5f3700
address Billy's code review
jerryzhou196 Nov 7, 2025
40be3b2
feat(replayTable): Integrate EventView into ReplayTable and related c…
jerryzhou196 Nov 7, 2025
d2f35ac
fix(replayDetails): Correct nextReplay calculation to handle edge cas…
jerryzhou196 Nov 7, 2025
00a8a90
feat(replayTable): normalize eventView handling in ReplayTable and co…
jerryzhou196 Nov 7, 2025
3db7bba
feat(replayTable): Enhance eventView query handling with stats period…
jerryzhou196 Nov 7, 2025
ab71b4c
Merge branch 'master' into replay-carousal
jerryzhou196 Nov 7, 2025
1ab7a54
removed changes for issueDetail Column view
jerryzhou196 Nov 7, 2025
8d8da21
undo replayController
jerryzhou196 Nov 7, 2025
c5ae428
Merge branch 'master' into replay-carousal
jerryzhou196 Nov 7, 2025
40b2262
remove export
jerryzhou196 Nov 7, 2025
a2646bb
fix(replayTable): Refactor query generation to include playlist start…
jerryzhou196 Nov 7, 2025
3c2e433
feat(replayDetails): move hover state to only be in copy button for r…
jerryzhou196 Nov 7, 2025
525f5d8
fix(replayTable): move sort fowarding back to index page
jerryzhou196 Nov 7, 2025
afab360
fix(replayTable): move sort fowarding back to index page
jerryzhou196 Nov 8, 2025
1682740
feat(replay): conditionally render if playlIstStart and playlistEnd a…
jerryzhou196 Nov 8, 2025
c0c6c01
fix(replay): refactor query handling in ReplaySessionColumn and Repla…
jerryzhou196 Nov 8, 2025
cc6b7d0
fix(replayDetails): conditionally render playlistView buttons
jerryzhou196 Nov 8, 2025
f5347d6
fix(replaySessionColumn): enhance query handling to include cursor fr…
jerryzhou196 Nov 8, 2025
89e9577
fix(groupReplays.spec): update expected query, replacing statsPeriod …
jerryzhou196 Nov 8, 2025
e4d9d75
Merge branch 'master' into replay-carousal
jerryzhou196 Nov 8, 2025
6561e90
chore(replay): Update icon for next breadcrumb button from IconNext t…
jerryzhou196 Nov 8, 2025
0d4f502
Merge remote-tracking branch 'origin/master' into replay-carousal
jerryzhou196 Nov 8, 2025
4bfb035
fix(transactionReplays): update expected query to include playlistSta…
jerryzhou196 Nov 9, 2025
c09d6e6
feat(replay): introduce useReplayPlaylist hook for managing replay pl…
jerryzhou196 Nov 9, 2025
21ed4b8
refactor(replayDetails): streamline breadcrumb component by moving re…
jerryzhou196 Nov 9, 2025
cb30e23
refactor(useReplayPlaylist): simplify query options by removing unnec…
jerryzhou196 Nov 9, 2025
3a20fb7
feat(replay): integrate ReplayPlaylistProvider into ReplayDetails for…
jerryzhou196 Nov 9, 2025
47e4fe3
fix(replay): fix query property in transactions
jerryzhou196 Nov 10, 2025
cb5cbfd
Merge branch 'master' into fix-fetch-replay-list-hook
jerryzhou196 Nov 10, 2025
a664264
Merge branch 'fix-fetch-replay-list-hook' into replay-carousal
jerryzhou196 Nov 10, 2025
dd12572
refactor(replay): update replay query handling and remove unused useR…
jerryzhou196 Nov 10, 2025
30011c5
refactor(replay): update field mapping in ReplaySessionColumn for imp…
jerryzhou196 Nov 10, 2025
353f170
fix(replay): Add enabled option to useReplayList and adjust fetching …
jerryzhou196 Nov 10, 2025
ee88392
fix(replay): streamline saved query creation in useReplaysFromTransac…
jerryzhou196 Nov 10, 2025
108f0d5
Merge branch 'fix-fetch-replay-list-hook' into replay-carousal
jerryzhou196 Nov 10, 2025
5bf9e79
Merge branch 'master' into replay-carousal
jerryzhou196 Nov 10, 2025
a770c0c
Merge branch 'master' into fix-fetch-replay-list-hook
jerryzhou196 Nov 10, 2025
649918f
fix(replay): set default value for enabled option in useReplayList hook
jerryzhou196 Nov 10, 2025
aac8f09
refactor(transactionReplays): simplify empty state test by removing r…
jerryzhou196 Nov 10, 2025
5218eb6
Merge branch 'master' into fix-fetch-replay-list-hook
jerryzhou196 Nov 10, 2025
fb7722f
Merge branch 'master' into fix-fetch-replay-list-hook
jerryzhou196 Nov 10, 2025
ab5c5d2
Merge branch 'fix-fetch-replay-list-hook' into replay-carousal
jerryzhou196 Nov 10, 2025
72614f1
move replayPrev and replayNext icon to left of icons
jerryzhou196 Nov 11, 2025
25cbaaa
Merge branch 'master' into fix-fetch-replay-list-hook
jerryzhou196 Nov 11, 2025
f9aabfa
fix(replays): Ensure replay list fetching is correctly enabled
jerryzhou196 Nov 11, 2025
1f8ba92
fix(replays): Simplify enabled flag condition for fetching replays
jerryzhou196 Nov 11, 2025
b714401
Merge branch 'master' into fix-fetch-replay-list-hook
jerryzhou196 Nov 11, 2025
dab5a78
Merge branch 'master' into replay-carousal
jerryzhou196 Nov 11, 2025
4c03a95
Merge branch 'master' into replay-carousal
jerryzhou196 Nov 11, 2025
01ab6c3
fix(replays): Update query parameters in tests and enable replay list…
jerryzhou196 Nov 11, 2025
7deb031
fix(replays): Update replay list fetching condition
jerryzhou196 Nov 12, 2025
18a2e93
refactor(replays): Enhance replay query handling and type definitions
jerryzhou196 Nov 12, 2025
92e9e4a
Merge branch master into replay-carousal
jerryzhou196 Nov 12, 2025
4860a30
fixed tests
jerryzhou196 Nov 12, 2025
3c093c9
ref(replays): Update sorting logic to use shared constant for replay …
jerryzhou196 Nov 12, 2025
a567e2c
ref(replays): Refactor query handling in ReplayDetailsProviders
jerryzhou196 Nov 12, 2025
989b379
ref(location): Change DefaultQuery type to local scope in useLocation…
jerryzhou196 Nov 12, 2025
164a2b2
ref(replays): Simplify query object construction in ReplayDetailsProv…
jerryzhou196 Nov 12, 2025
0fc2f4a
ref(replays): Use useRef for initial location in ReplayDetailsPageBre…
jerryzhou196 Nov 13, 2025
6efdbb3
ref(replays): Refactor ReplayDetailsPageBreadcrumbs for improved styling
jerryzhou196 Nov 13, 2025
6f21136
ref(replays): Update queryReferrer in ReplayDetailsProviders for cons…
jerryzhou196 Nov 13, 2025
47d5c55
ref(replays): Enhance query handling in ReplaySessionColumn and Repla…
jerryzhou196 Nov 13, 2025
0e36592
ref(replays): Simplify sorting logic in ReplayDetailsProviders
jerryzhou196 Nov 13, 2025
5c130e6
ref(replays): Rename StyledDiv to ShortId for clarity in ReplayDetail…
jerryzhou196 Nov 13, 2025
302d359
ref(replays): Adjust sorting logic in ReplayDetailsProviders for clarity
jerryzhou196 Nov 13, 2025
d42da0d
ref(replays): Improve sorting logic in ReplayDetailsProviders for bet…
jerryzhou196 Nov 13, 2025
d1aa45a
ref(replays): Enhance parseStatsPeriod function to support UTC option
jerryzhou196 Nov 13, 2025
e03e05d
ref(replays): Clarify comment in ReplaySessionColumn regarding query …
jerryzhou196 Nov 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions static/app/components/replays/replayController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {useReplayContext} from 'sentry/components/replays/replayContext';
import {ReplayFullscreenButton} from 'sentry/components/replays/replayFullscreenButton';
import ReplayPlayPauseButton from 'sentry/components/replays/replayPlayPauseButton';
import TimeAndScrubberGrid from 'sentry/components/replays/timeAndScrubberGrid';
import {IconNext, IconRewind10} from 'sentry/icons';
import {IconChevron, IconRewind10} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {getNextReplayFrame} from 'sentry/utils/replays/getReplayEvent';
Expand Down Expand Up @@ -46,7 +46,7 @@ function ReplayPlayPauseBar({isLoading}: {isLoading?: boolean}) {
disabled={isLoading}
size="sm"
title={t('Next breadcrumb')}
icon={<IconNext size="sm" />}
icon={<IconChevron direction="right" />}
onClick={() => {
if (!replay) {
return;
Expand Down
13 changes: 13 additions & 0 deletions static/app/components/replays/table/replayTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {RefObject} from 'react';
import {useMemo} from 'react';
import styled from '@emotion/styled';

import {Alert} from 'sentry/components/core/alert';
Expand Down Expand Up @@ -42,6 +43,17 @@ export default function ReplayTable({
const gridTemplateColumns = columns.map(col => col.width ?? 'max-content').join(' ');
const hasInteractiveColumn = columns.some(col => col.interactive);

const start = useMemo(() => {
const earliestReplayStartedAt = replays.reduce(
(acc: number, replay) => Math.min(replay.started_at?.getTime() ?? 0, acc),
Infinity
);

return replays.length > 0
? new Date(earliestReplayStartedAt).toISOString()
: undefined;
}, [replays]);

if (isPending) {
return (
<StyledSimpleTable
Expand Down Expand Up @@ -112,6 +124,7 @@ export default function ReplayTable({
<column.Component
columnIndex={columnIndex}
replay={replay}
start={start ?? ''}
rowIndex={rowIndex}
showDropdownFilters={showDropdownFilters}
/>
Expand Down
28 changes: 19 additions & 9 deletions static/app/components/replays/table/replayTableColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {IconPlay} from 'sentry/icons/iconPlay';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {trackAnalytics} from 'sentry/utils/analytics';
import EventView from 'sentry/utils/discover/eventView';
import EventView, {encodeSort} from 'sentry/utils/discover/eventView';
import {spanOperationRelativeBreakdownRenderer} from 'sentry/utils/discover/fieldRenderers';
import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
import {useListItemCheckboxContext} from 'sentry/utils/list/useListItemCheckboxState';
Expand All @@ -43,6 +43,8 @@ import type {
ReplayRecordNestedFieldName,
} from 'sentry/views/replays/types';

import {DEFAULT_SORT} from './useReplayTableSort';

type ListRecord = ReplayListRecord | ReplayListRecordWithTx;

interface HeaderProps {
Expand All @@ -56,6 +58,7 @@ interface CellProps {
replay: ListRecord;
rowIndex: number;
showDropdownFilters: boolean;
start: string;
}

export interface ReplayTableColumn {
Expand Down Expand Up @@ -505,7 +508,7 @@ export const ReplaySessionColumn: ReplayTableColumn = {
interactive: true,
sortKey: 'started_at',
width: 'minmax(150px, 1fr)',
Component: ({replay}) => {
Component: ({replay, start}) => {
const routes = useRoutes();
const location = useLocation();
const organization = useOrganization();
Expand All @@ -522,18 +525,25 @@ export const ReplaySessionColumn: ReplayTableColumn = {

const referrer = getRouteStringFromRoutes(routes);
const eventView = EventView.fromLocation(location);

// This is a fix to get the 'sort' query string to work without explicitly dirtying the URL with fields params
const sort = location.query.sort ?? encodeSort(DEFAULT_SORT);
const replayDetailsPathname = makeReplaysPathname({
path: `/${replay.id}/`,
organization,
});

const detailsTab = () => ({
pathname: replayDetailsPathname,
query: {
referrer,
...eventView.generateQueryStringObject(),
},
});
const detailsTab = () => {
return {
pathname: replayDetailsPathname,
query: {
referrer,
...eventView.generateQueryStringObject(),
playlistStart: start,
sort,
},
};
};
const trackNavigationEvent = () =>
trackAnalytics('replay.list-navigate-to-details', {
project_id: project?.id,
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/replays/table/useReplayTableSort.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Props {
queryParamKey?: string;
}

const DEFAULT_SORT = {field: 'started_at', kind: 'asc'} as const;
export const DEFAULT_SORT = {field: 'started_at', kind: 'desc'} as const;

export default function useReplayTableSort({
defaultSort = DEFAULT_SORT,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {useState} from 'react';
import styled from '@emotion/styled';

import {Breadcrumbs} from 'sentry/components/breadcrumbs';
import {Button} from 'sentry/components/core/button';
import {Button, ButtonBar} from 'sentry/components/core/button';
import {LinkButton} from 'sentry/components/core/button/linkButton';
import {Flex} from 'sentry/components/core/layout';
import {Tooltip} from 'sentry/components/core/tooltip';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import Placeholder from 'sentry/components/placeholder';
import {useReplayContext} from 'sentry/components/replays/replayContext';
import {IconCopy} from 'sentry/icons';
import {IconCopy, IconNext, IconPrevious} from 'sentry/icons';
import {t} from 'sentry/locale';
import {defined} from 'sentry/utils';
import EventView from 'sentry/utils/discover/eventView';
Expand All @@ -19,20 +19,25 @@ import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import useProjectFromId from 'sentry/utils/useProjectFromId';
import {makeReplaysPathname} from 'sentry/views/replays/pathnames';
import type {ReplayListRecord} from 'sentry/views/replays/types';

interface Props {
nextReplay: ReplayListRecord | undefined;
previousReplay: ReplayListRecord | undefined;
readerResult: ReturnType<typeof useLoadReplayReader>;
}

export default function ReplayDetailsPageBreadcrumbs({readerResult}: Props) {
export default function ReplayDetailsPageBreadcrumbs({
readerResult,
nextReplay,
previousReplay,
}: Props) {
const replayRecord = readerResult.replayRecord;
const organization = useOrganization();
const location = useLocation();
const eventView = EventView.fromLocation(location);
const project = useProjectFromId({project_id: replayRecord?.project_id ?? undefined});
const [isHovered, setIsHovered] = useState(false);
const {currentTime} = useReplayContext();

// Create URL with current timestamp for copying
const replayUrlWithTimestamp = replayRecord
? (() => {
Expand Down Expand Up @@ -74,12 +79,7 @@ export default function ReplayDetailsPageBreadcrumbs({readerResult}: Props) {

const replayCrumb = {
label: replayRecord ? (
<Flex
align="center"
gap="xs"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Flex align="center" gap="xs">
<div
onClick={() =>
copy(replayUrlWithTimestamp, {
Expand All @@ -89,21 +89,43 @@ export default function ReplayDetailsPageBreadcrumbs({readerResult}: Props) {
>
{getShortEventId(replayRecord?.id)}
</div>
{isHovered && (
<Tooltip title={t('Copy link to replay at current timestamp')}>
<Button
aria-label={t('Copy link to replay at current timestamp')}
onClick={() =>
copy(replayUrlWithTimestamp, {
successMessage: t('Copied replay link to clipboard'),
})
}
size="zero"
borderless
icon={<IconCopy size="xs" color="subText" />}
<Tooltip title={t('Copy link to replay at current timestamp')}>
<Button
aria-label={t('Copy link to replay at current timestamp')}
onClick={() =>
copy(replayUrlWithTimestamp, {
successMessage: t('Copied replay link to clipboard'),
})
}
size="zero"
borderless
icon={<IconCopy size="xs" color="subText" />}
/>
</Tooltip>
<Flex>
<ButtonBar merged gap="0">
<LinkButton
size="xs"
icon={<IconPrevious />}
disabled={!previousReplay}
to={{
pathname: previousReplay
? `/explore/replays/${previousReplay.id}/`
: undefined,
query: {...location.query},
}}
/>
<LinkButton
size="xs"
icon={<IconNext />}
disabled={!nextReplay}
to={{
pathname: nextReplay ? `/explore/replays/${nextReplay.id}/` : undefined,
query: {...location.query},
}}
/>
</Tooltip>
)}
</ButtonBar>
</Flex>
</Flex>
) : (
<Placeholder width="100%" height="16px" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export default function ReplayDetailsUserBadge({readerResult}: Props) {
renderError={() => null}
renderThrottled={() => null}
renderLoading={() =>
replayRecord ? badge : <Placeholder width="30%" height="45px" />
replayRecord ? badge : <Placeholder width="30%" height="66px" />
}
renderMissing={() => null}
renderProcessingError={() => badge}
Expand Down
60 changes: 57 additions & 3 deletions static/app/views/replays/details.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Fragment} from 'react';
import {Fragment, useMemo} from 'react';
import styled from '@emotion/styled';
import invariant from 'invariant';

Expand All @@ -8,11 +8,16 @@ import * as Layout from 'sentry/components/layouts/thirds';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {decodeScalar} from 'sentry/utils/queryString';
import {useApiQuery} from 'sentry/utils/queryClient';
import {decodeList, decodeScalar} from 'sentry/utils/queryString';
import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader';
import useReplayListQueryKey from 'sentry/utils/replays/hooks/useReplayListQueryKey';
import useReplayPageview from 'sentry/utils/replays/hooks/useReplayPageview';
import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils';
import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
import useLocationQuery from 'sentry/utils/url/useLocationQuery';
import useUrlParams from 'sentry/utils/url/useUrlParams';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
Expand All @@ -23,6 +28,7 @@ import ReplayDetailsMetadata from 'sentry/views/replays/detail/header/replayDeta
import ReplayDetailsPageBreadcrumbs from 'sentry/views/replays/detail/header/replayDetailsPageBreadcrumbs';
import ReplayDetailsUserBadge from 'sentry/views/replays/detail/header/replayDetailsUserBadge';
import ReplayDetailsPage from 'sentry/views/replays/detail/page';
import type {ReplayListRecord} from 'sentry/views/replays/types';

export default function ReplayDetails() {
const user = useUser();
Expand All @@ -41,6 +47,50 @@ export default function ReplayDetails() {
});
const {replay, replayRecord} = readerResult;

const {playlistStart, ...query} = useLocationQuery({
fields: {
cursor: decodeScalar,
environment: decodeList,
project: decodeList,
sort: decodeScalar,
query: decodeScalar,
playlistStart: decodeScalar,
statsPeriod: decodeScalar,
utc: decodeScalar,
},
});
const {getParamValue} = useUrlParams('sort');
const sortQuery = getParamValue();

const queryKey = useReplayListQueryKey({
options: {query: {...query, start: playlistStart, sort: sortQuery}},
organization,
queryReferrer: 'replayList',
});
const {data} = useApiQuery<{
data: ReplayListRecord[];
enabled: true;
}>(queryKey, {staleTime: 0});

const replays = useMemo(() => data?.data?.map(mapResponseToReplayRecord) ?? [], [data]);

const currentReplayIndex = useMemo(
() => replays.findIndex(r => r.id === replayRecord?.id),
[replays, replayRecord]
);

const nextReplay = useMemo(
() =>
currentReplayIndex < replays.length - 1
? replays[currentReplayIndex + 1]
: undefined,
[replays, currentReplayIndex]
);
const previousReplay = useMemo(
() => (currentReplayIndex > 0 ? replays[currentReplayIndex - 1] : undefined),
[replays, currentReplayIndex]
);

useReplayPageview('replay.details-time-spent');
useRouteAnalyticsEventNames('replay_details.viewed', 'Replay Details: Viewed');
useRouteAnalyticsParams({
Expand All @@ -58,7 +108,11 @@ export default function ReplayDetails() {
const content = (
<Fragment>
<Header>
<ReplayDetailsPageBreadcrumbs readerResult={readerResult} />
<ReplayDetailsPageBreadcrumbs
readerResult={readerResult}
nextReplay={nextReplay}
previousReplay={previousReplay}
/>
<ReplayDetailsHeaderActions readerResult={readerResult} />
<ReplayDetailsUserBadge readerResult={readerResult} />
<ReplayDetailsMetadata readerResult={readerResult} />
Expand Down
Loading