Skip to content

Commit 41b1259

Browse files
authored
feat(replays) - add playlist support from Issue Details and Insights (#103007)
Depends on #102583 Issue Details: ![issue_details_to_replays](https://github.com/user-attachments/assets/8d474998-7bbc-4ad2-a971-55343fa0297b) Performance: ![performance_replay_carousal](https://github.com/user-attachments/assets/9e6c3ba2-0872-44f5-8e0c-a832da3dac12)
1 parent 5a6903b commit 41b1259

File tree

12 files changed

+107
-59
lines changed

12 files changed

+107
-59
lines changed

static/app/components/events/eventReplay/replayPreviewPlayer.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {ComponentProps} from 'react';
22
import {useEffect, useRef, useState} from 'react';
33
import styled from '@emotion/styled';
4+
import type {Query} from 'history';
45

56
import {Alert} from 'sentry/components/core/alert';
67
import {Button} from 'sentry/components/core/button';
@@ -38,6 +39,7 @@ import {makeReplaysPathname} from 'sentry/views/replays/pathnames';
3839
import type {ReplayListRecord, ReplayRecord} from 'sentry/views/replays/types';
3940

4041
export default function ReplayPreviewPlayer({
42+
query,
4143
errorBeforeReplayStart,
4244
replayId,
4345
fullReplayButtonProps,
@@ -56,6 +58,7 @@ export default function ReplayPreviewPlayer({
5658
handleForwardClick?: () => void;
5759
overlayContent?: React.ReactNode;
5860
playPausePriority?: ComponentProps<typeof ReplayPlayPauseButton>['priority'];
61+
query?: Query;
5962
showNextAndPrevious?: boolean;
6063
}) {
6164
const routes = useRoutes();
@@ -100,6 +103,7 @@ export default function ReplayPreviewPlayer({
100103
)}
101104
<HeaderWrapper>
102105
<ReplaySessionColumn.Component
106+
query={query}
103107
replay={replayRecord as ReplayListRecord}
104108
rowIndex={0}
105109
columnIndex={0}
@@ -117,6 +121,7 @@ export default function ReplayPreviewPlayer({
117121
t_main: fromFeedback ? TabKey.BREADCRUMBS : TabKey.ERRORS,
118122
t: (currentTime + startOffsetMs) / 1000,
119123
groupId,
124+
...query,
120125
},
121126
}}
122127
{...fullReplayButtonProps}

static/app/components/replays/table/replayTable.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {RefObject} from 'react';
22
import styled from '@emotion/styled';
3+
import type {Query} from 'history';
34

45
import {Alert} from 'sentry/components/core/alert';
56
import InteractionStateLayer from 'sentry/components/core/interactionStateLayer';
@@ -26,10 +27,12 @@ type Props = SortProps & {
2627
isPending: boolean;
2728
replays: ReplayListRecord[];
2829
showDropdownFilters: boolean;
30+
query?: Query;
2931
ref?: RefObject<HTMLDivElement | null>;
3032
};
3133

3234
export default function ReplayTable({
35+
query,
3336
columns,
3437
error,
3538
isPending,
@@ -110,6 +113,7 @@ export default function ReplayTable({
110113
{columns.map((column, columnIndex) => (
111114
<RowCell key={`${replay.id}-${columnIndex}-${column.sortKey}`}>
112115
<column.Component
116+
query={query}
113117
columnIndex={columnIndex}
114118
replay={replay}
115119
rowIndex={rowIndex}

static/app/components/replays/table/replayTableColumns.tsx

Lines changed: 18 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import NumericDropdownFilter from 'sentry/components/replays/table/filters/numer
1818
import OSBrowserDropdownFilter from 'sentry/components/replays/table/filters/osBrowserDropdownFilter';
1919
import ScoreBar from 'sentry/components/scoreBar';
2020
import {SimpleTable} from 'sentry/components/tables/simpleTable';
21-
import {parseStatsPeriod} from 'sentry/components/timeRangeSelector/utils';
2221
import {IconNot} from 'sentry/icons';
2322
import {IconCursorArrow} from 'sentry/icons/iconCursorArrow';
2423
import {IconFire} from 'sentry/icons/iconFire';
@@ -27,7 +26,6 @@ import {IconPlay} from 'sentry/icons/iconPlay';
2726
import {t, tct} from 'sentry/locale';
2827
import {space} from 'sentry/styles/space';
2928
import {trackAnalytics} from 'sentry/utils/analytics';
30-
import EventView from 'sentry/utils/discover/eventView';
3129
import {spanOperationRelativeBreakdownRenderer} from 'sentry/utils/discover/fieldRenderers';
3230
import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
3331
import {useListItemCheckboxContext} from 'sentry/utils/list/useListItemCheckboxState';
@@ -58,6 +56,7 @@ interface CellProps {
5856
replay: ListRecord;
5957
rowIndex: number;
6058
showDropdownFilters: boolean;
59+
query?: Query;
6160
}
6261

6362
export interface ReplayTableColumn {
@@ -316,10 +315,15 @@ export const ReplayDetailsLinkColumn: ReplayTableColumn = {
316315
Header: '',
317316
interactive: true,
318317
sortKey: undefined,
319-
Component: ({replay}) => {
318+
Component: ({replay, query}) => {
320319
const organization = useOrganization();
321320
return (
322-
<DetailsLink to={makeReplaysPathname({path: `/${replay.id}/`, organization})}>
321+
<DetailsLink
322+
to={{
323+
pathname: makeReplaysPathname({path: `/${replay.id}/`, organization}),
324+
query,
325+
}}
326+
>
323327
<Tooltip title={t('See Full Replay')}>
324328
<IconOpen />
325329
</Tooltip>
@@ -507,9 +511,10 @@ export const ReplaySessionColumn: ReplayTableColumn = {
507511
interactive: true,
508512
sortKey: 'started_at',
509513
width: 'minmax(150px, 1fr)',
510-
Component: ({replay}) => {
514+
Component: ({replay, query}) => {
511515
const routes = useRoutes();
512-
const location = useLocation();
516+
const referrer = getRouteStringFromRoutes(routes);
517+
513518
const organization = useOrganization();
514519
const project = useProjectFromId({project_id: replay.project_id ?? undefined});
515520

@@ -522,50 +527,6 @@ export const ReplaySessionColumn: ReplayTableColumn = {
522527
'For TypeScript: replay.started_at is implied because replay.is_archived is false'
523528
);
524529

525-
const referrer = getRouteStringFromRoutes(routes);
526-
const eventView = EventView.fromLocation(location);
527-
528-
const {statsPeriod, start, end, ...eventViewQuery} =
529-
eventView.generateQueryStringObject();
530-
531-
const detailsTabQuery: Query = {
532-
referrer,
533-
...eventViewQuery,
534-
};
535-
536-
if (typeof statsPeriod === 'string') {
537-
const {start: playlistStart, end: playlistEnd} = parseStatsPeriod(
538-
statsPeriod,
539-
undefined,
540-
true
541-
);
542-
detailsTabQuery.playlistStart = playlistStart;
543-
detailsTabQuery.playlistEnd = playlistEnd;
544-
} else if (start && end) {
545-
detailsTabQuery.playlistStart = start;
546-
detailsTabQuery.playlistEnd = end;
547-
}
548-
549-
// Because the sort and cursor field is only generated in EventView conditionally and we
550-
// want to avoid dirtying the URL with fields, we manually add them to the query here.
551-
if (location.query.sort) {
552-
detailsTabQuery.playlistSort = location.query.sort;
553-
}
554-
if (location.query.cursor) {
555-
detailsTabQuery.cursor = location.query.cursor;
556-
}
557-
558-
const replayDetailsPathname = makeReplaysPathname({
559-
path: `/${replay.id}/`,
560-
organization,
561-
});
562-
563-
const detailsTab = () => {
564-
return {
565-
pathname: replayDetailsPathname,
566-
query: detailsTabQuery,
567-
};
568-
};
569530
const trackNavigationEvent = () =>
570531
trackAnalytics('replay.list-navigate-to-details', {
571532
project_id: project?.id,
@@ -576,7 +537,13 @@ export const ReplaySessionColumn: ReplayTableColumn = {
576537
});
577538

578539
return (
579-
<CellLink to={detailsTab()} onClick={trackNavigationEvent}>
540+
<CellLink
541+
to={{
542+
pathname: makeReplaysPathname({path: `/${replay.id}/`, organization}),
543+
query,
544+
}}
545+
onClick={trackNavigationEvent}
546+
>
580547
<ReplayBadge replay={replay} />
581548
</CellLink>
582549
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type {Query} from 'history';
2+
3+
import {parseStatsPeriod} from 'sentry/components/timeRangeSelector/utils';
4+
import EventView from 'sentry/utils/discover/eventView';
5+
import {useLocation} from 'sentry/utils/useLocation';
6+
import type {ReplayListQueryReferrer} from 'sentry/views/replays/types';
7+
8+
export function usePlaylistQuery(
9+
referrer: ReplayListQueryReferrer,
10+
eventView?: EventView
11+
): Query {
12+
const location = useLocation();
13+
if (!eventView) {
14+
eventView = EventView.fromLocation(location);
15+
}
16+
17+
const {statsPeriod, start, end, query, project, environment} =
18+
eventView.generateQueryStringObject();
19+
20+
const eventViewQuery: Query = {
21+
query,
22+
referrer,
23+
project,
24+
environment,
25+
};
26+
27+
if (typeof statsPeriod === 'string') {
28+
const {start: playlistStart, end: playlistEnd} = parseStatsPeriod(
29+
statsPeriod,
30+
undefined,
31+
true
32+
);
33+
eventViewQuery.playlistStart = playlistStart;
34+
eventViewQuery.playlistEnd = playlistEnd;
35+
} else if (start && end) {
36+
eventViewQuery.playlistStart = start;
37+
eventViewQuery.playlistEnd = end;
38+
}
39+
40+
// Because the sort and cursor field is only generated in EventView conditionally and we
41+
// want to avoid dirtying the URL with fields, we manually add them to the query here.
42+
if (location.query.sort) {
43+
eventViewQuery.playlistSort = location.query.sort;
44+
}
45+
if (location.query.cursor) {
46+
eventViewQuery.cursor = location.query.cursor;
47+
}
48+
eventViewQuery.referrer = referrer;
49+
return eventViewQuery;
50+
}

static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ describe('GroupReplays', () => {
414414
expect(await screen.findAllByText('testDisplayName')).toHaveLength(2);
415415

416416
const expectedQuery =
417-
'playlistEnd=2022-09-28T23%3A29%3A13&playlistStart=2022-09-14T23%3A29%3A13&query=&referrer=%2Forganizations%2F%3AorgId%2Fissues%2F%3AgroupId%2Freplays%2F&yAxis=count%28%29';
417+
'playlistEnd=2022-09-28T23%3A29%3A13&playlistStart=2022-06-30T23%3A29%3A13&query=id%3A%5B346789a703f6454384f1de473b8b9fcc%2Cb05dae9b6be54d21a4d5ad9f8f02b780%5D&referrer=issueReplays';
418418

419419
// Expect the first row to have the correct href
420420
expect(

static/app/views/issueDetails/groupReplays/groupReplays.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Fragment, useEffect, useMemo} from 'react';
22
import {css} from '@emotion/react';
33
import styled from '@emotion/styled';
4-
import type {Location} from 'history';
4+
import type {Location, Query} from 'history';
55

66
import {Button} from 'sentry/components/core/button';
77
import * as Layout from 'sentry/components/layouts/thirds';
@@ -22,6 +22,7 @@ import {
2222
ReplayPlayPauseColumn,
2323
ReplaySessionColumn,
2424
} from 'sentry/components/replays/table/replayTableColumns';
25+
import {usePlaylistQuery} from 'sentry/components/replays/usePlaylistQuery';
2526
import {replayMobilePlatforms} from 'sentry/data/platformCategories';
2627
import {IconPlay, IconUser} from 'sentry/icons';
2728
import {t, tn} from 'sentry/locale';
@@ -165,6 +166,7 @@ export default function GroupReplays({group}: Props) {
165166

166167
function SelectedReplayWrapper({
167168
children,
169+
query,
168170
group,
169171
replaySlug,
170172
overlayContent,
@@ -175,6 +177,7 @@ function SelectedReplayWrapper({
175177
overlayContent: React.ReactNode;
176178
replaySlug: string;
177179
replays: ReplayListRecord[] | undefined;
180+
query?: Query;
178181
}) {
179182
const organization = useOrganization();
180183
const readerResult = useLoadReplayReader({
@@ -195,6 +198,7 @@ function SelectedReplayWrapper({
195198
autoStart
196199
>
197200
<GroupReplaysPlayer
201+
query={query}
198202
replayReaderResult={readerResult}
199203
overlayContent={overlayContent}
200204
handleForwardClick={
@@ -242,9 +246,11 @@ function GroupReplaysTable({
242246
});
243247
const {replays} = replayListData;
244248
const selectedReplay = replays?.[selectedReplayIndex];
249+
const playlistQuery = usePlaylistQuery('issueReplays', eventView);
245250

246251
const replayTable = (
247252
<ReplayTable
253+
query={playlistQuery}
248254
columns={[
249255
...(selectedReplay ? [ReplayPlayPauseColumn] : []),
250256
...(allMobileProj ? VISIBLE_COLUMNS_MOBILE : VISIBLE_COLUMNS),
@@ -265,6 +271,7 @@ function GroupReplaysTable({
265271
group={group}
266272
replaySlug={selectedReplay.id}
267273
replays={replays}
274+
query={playlistQuery}
268275
>
269276
{replayTable}
270277
</SelectedReplayWrapper>

static/app/views/issueDetails/groupReplays/groupReplaysPlayer.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import styled from '@emotion/styled';
2+
import type {Query} from 'history';
23

34
import NegativeSpaceContainer from 'sentry/components/container/negativeSpaceContainer';
45
import {REPLAY_LOADING_HEIGHT_LARGE} from 'sentry/components/events/eventReplay/constants';
@@ -21,10 +22,12 @@ interface Props {
2122
handleForwardClick: undefined | (() => void);
2223
overlayContent: React.ReactNode;
2324
replayReaderResult: ReturnType<typeof useLoadReplayReader>;
25+
query?: Query;
2426
}
2527

2628
export default function GroupReplaysPlayer({
2729
analyticsContext,
30+
query,
2831
handleForwardClick,
2932
handleBackClick,
3033
overlayContent,
@@ -65,6 +68,7 @@ export default function GroupReplaysPlayer({
6568
<ReplayReaderProvider replay={replay}>
6669
<ReplayPlayerStateContextProvider>
6770
<ReplayPreviewPlayer
71+
query={query}
6872
errorBeforeReplayStart={replay.getErrorBeforeReplayStart()}
6973
replayId={replayReaderResult.replayId}
7074
replayRecord={replayReaderResult.replayRecord!}

static/app/views/performance/transactionSummary/transactionReplays/index.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ describe('TransactionReplays', () => {
207207
expect(screen.getAllByText('testDisplayName')).toHaveLength(2);
208208

209209
const expectedQuery =
210-
'playlistEnd=2022-09-28T23%3A29%3A13&playlistStart=2022-09-14T23%3A29%3A13&project=1&query=test&referrer=replays%2F&yAxis=count%28%29';
210+
'playlistEnd=2022-09-28T23%3A29%3A13&playlistStart=2022-09-14T23%3A29%3A13&query=test&referrer=transactionReplays';
211211
// Expect the first row to have the correct href
212212
expect(
213213
screen.getByRole('link', {

static/app/views/performance/transactionSummary/transactionReplays/transactionReplays.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ReplaySessionColumn,
1515
ReplaySlowestTransactionColumn,
1616
} from 'sentry/components/replays/table/replayTableColumns';
17+
import {usePlaylistQuery} from 'sentry/components/replays/usePlaylistQuery';
1718
import type {Organization} from 'sentry/types/organization';
1819
import EventView from 'sentry/utils/discover/eventView';
1920
import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
@@ -89,6 +90,7 @@ function ReplaysContent({
8990
if (!eventView.query) {
9091
eventView.query = String(location.query.query ?? '');
9192
}
93+
const playlistQuery = usePlaylistQuery('transactionReplays', eventView);
9294

9395
const newLocation = useMemo(
9496
() => ({query: {}}) as Location<ReplayListLocationQuery>,
@@ -121,6 +123,7 @@ function ReplaysContent({
121123
return (
122124
<Layout.Main width="full">
123125
<ReplayTable
126+
query={playlistQuery}
124127
columns={[
125128
ReplaySessionColumn,
126129
...(hasRoomForColumns ? [ReplaySlowestTransactionColumn] : []),

0 commit comments

Comments
 (0)