Skip to content

Commit acb068a

Browse files
authored
feat(issue-details): Show static replay when error is not within the replay (#64827)
This covers an edge case when the error event is not within the bounds of the replay.
1 parent 270094f commit acb068a

File tree

4 files changed

+139
-88
lines changed

4 files changed

+139
-88
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {Alert} from 'sentry/components/alert';
66
import {LinkButton} from 'sentry/components/button';
77
import ButtonBar from 'sentry/components/buttonBar';
88
import ErrorBoundary from 'sentry/components/errorBoundary';
9+
import {StaticReplayPreview} from 'sentry/components/events/eventReplay/staticReplayPreview';
910
import Panel from 'sentry/components/panels/panel';
1011
import Placeholder from 'sentry/components/placeholder';
1112
import {Flex} from 'sentry/components/profiling/flex';
@@ -191,6 +192,19 @@ function ReplayClipPreview({
191192
);
192193
}
193194

195+
if (replay.getDurationMs() <= 0) {
196+
return (
197+
<StaticReplayPreview
198+
analyticsContext={analyticsContext}
199+
isFetching={false}
200+
replay={replay}
201+
replayId={replayId}
202+
fullReplayButtonProps={fullReplayButtonProps}
203+
initialTimeOffsetMs={0}
204+
/>
205+
);
206+
}
207+
194208
return (
195209
<ReplayContextProvider
196210
isFetching={fetching}

static/app/components/events/eventReplay/replayPreview.spec.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import {render as baseRender, screen} from 'sentry-test/reactTestingLibrary';
99
import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
1010
import ReplayReader from 'sentry/utils/replays/replayReader';
1111
import type RequestError from 'sentry/utils/requestError/requestError';
12-
import {OrganizationContext} from 'sentry/views/organizationContext';
13-
import {RouteContext} from 'sentry/views/routeContext';
1412

1513
import ReplayPreview from './replayPreview';
1614

@@ -65,7 +63,7 @@ mockUseReplayReader.mockImplementation(() => {
6563
});
6664

6765
const render: typeof baseRender = children => {
68-
const {router, routerContext} = initializeOrg({
66+
const {routerContext} = initializeOrg({
6967
router: {
7068
routes: [
7169
{path: '/'},
@@ -79,21 +77,10 @@ const render: typeof baseRender = children => {
7977
},
8078
});
8179

82-
return baseRender(
83-
<RouteContext.Provider
84-
value={{
85-
router,
86-
location: router.location,
87-
params: router.params,
88-
routes: router.routes,
89-
}}
90-
>
91-
<OrganizationContext.Provider value={OrganizationFixture()}>
92-
{children}
93-
</OrganizationContext.Provider>
94-
</RouteContext.Provider>,
95-
{context: routerContext}
96-
);
80+
return baseRender(children, {
81+
context: routerContext,
82+
organization: OrganizationFixture({slug: mockOrgSlug}),
83+
});
9784
};
9885

9986
const defaultProps = {

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

Lines changed: 12 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
11
import type {ComponentProps} from 'react';
2-
import {Fragment, useMemo} from 'react';
2+
import {useMemo} from 'react';
33
import styled from '@emotion/styled';
44

55
import {Alert} from 'sentry/components/alert';
6-
import {LinkButton} from 'sentry/components/button';
6+
import type {LinkButton} from 'sentry/components/button';
7+
import {StaticReplayPreview} from 'sentry/components/events/eventReplay/staticReplayPreview';
78
import Placeholder from 'sentry/components/placeholder';
89
import {Flex} from 'sentry/components/profiling/flex';
910
import MissingReplayAlert from 'sentry/components/replays/alerts/missingReplayAlert';
10-
import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
11-
import ReplayPlayer from 'sentry/components/replays/replayPlayer';
12-
import ReplayProcessingError from 'sentry/components/replays/replayProcessingError';
13-
import {IconDelete, IconPlay} from 'sentry/icons';
11+
import {IconDelete} from 'sentry/icons';
1412
import {t} from 'sentry/locale';
1513
import {space} from 'sentry/styles/space';
16-
import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
17-
import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
14+
import type {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
1815
import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
1916
import type RequestError from 'sentry/utils/requestError/requestError';
2017
import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
21-
import {useRoutes} from 'sentry/utils/useRoutes';
22-
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
23-
import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
2418
import type {ReplayRecord} from 'sentry/views/replays/types';
2519

2620
type Props = {
@@ -62,7 +56,6 @@ function ReplayPreview({
6256
orgSlug,
6357
replaySlug,
6458
}: Props) {
65-
const routes = useRoutes();
6659
const {fetching, replay, replayRecord, fetchError, replayId} = useReplayReader({
6760
orgSlug,
6861
replaySlug,
@@ -106,70 +99,19 @@ function ReplayPreview({
10699
);
107100
}
108101

109-
const fullReplayUrl = {
110-
pathname: normalizeUrl(`/organizations/${orgSlug}/replays/${replayId}/`),
111-
query: {
112-
referrer: getRouteStringFromRoutes(routes),
113-
t_main: focusTab ?? TabKey.ERRORS,
114-
t: initialTimeOffsetMs / 1000,
115-
},
116-
};
117-
118102
return (
119-
<ReplayContextProvider
103+
<StaticReplayPreview
104+
focusTab={focusTab}
120105
isFetching={fetching}
121-
replay={replay}
122-
initialTimeOffsetMs={{offsetMs: initialTimeOffsetMs}}
123106
analyticsContext={analyticsContext}
124-
>
125-
<PlayerContainer data-test-id="player-container">
126-
{replay?.hasProcessingErrors() ? (
127-
<ReplayProcessingError processingErrors={replay.processingErrors()} />
128-
) : (
129-
<Fragment>
130-
<StaticPanel>
131-
<ReplayPlayer isPreview />
132-
</StaticPanel>
133-
134-
<CTAOverlay>
135-
<LinkButton
136-
{...fullReplayButtonProps}
137-
icon={<IconPlay />}
138-
priority="primary"
139-
to={fullReplayUrl}
140-
>
141-
{t('Open Replay')}
142-
</LinkButton>
143-
</CTAOverlay>
144-
</Fragment>
145-
)}
146-
</PlayerContainer>
147-
</ReplayContextProvider>
107+
replay={replay}
108+
replayId={replayId}
109+
fullReplayButtonProps={fullReplayButtonProps}
110+
initialTimeOffsetMs={initialTimeOffsetMs}
111+
/>
148112
);
149113
}
150114

151-
const PlayerContainer = styled(FluidHeight)`
152-
position: relative;
153-
background: ${p => p.theme.background};
154-
gap: ${space(1)};
155-
max-height: 448px;
156-
`;
157-
158-
const StaticPanel = styled(FluidHeight)`
159-
border: 1px solid ${p => p.theme.border};
160-
border-radius: ${p => p.theme.borderRadius};
161-
`;
162-
163-
const CTAOverlay = styled('div')`
164-
position: absolute;
165-
width: 100%;
166-
height: 100%;
167-
display: flex;
168-
justify-content: center;
169-
align-items: center;
170-
background: rgba(255, 255, 255, 0.5);
171-
`;
172-
173115
const StyledPlaceholder = styled(Placeholder)`
174116
margin-bottom: ${space(2)};
175117
`;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {type ComponentProps, Fragment, useMemo} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {LinkButton} from 'sentry/components/button';
5+
import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
6+
import ReplayPlayer from 'sentry/components/replays/replayPlayer';
7+
import ReplayProcessingError from 'sentry/components/replays/replayProcessingError';
8+
import {IconPlay} from 'sentry/icons';
9+
import {t} from 'sentry/locale';
10+
import {space} from 'sentry/styles/space';
11+
import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
12+
import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
13+
import type ReplayReader from 'sentry/utils/replays/replayReader';
14+
import useOrganization from 'sentry/utils/useOrganization';
15+
import {useRoutes} from 'sentry/utils/useRoutes';
16+
import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
17+
18+
type StaticReplayPreviewProps = {
19+
analyticsContext: string;
20+
initialTimeOffsetMs: number;
21+
isFetching: boolean;
22+
replay: ReplayReader | null;
23+
replayId: string;
24+
focusTab?: TabKey;
25+
fullReplayButtonProps?: Partial<ComponentProps<typeof LinkButton>>;
26+
};
27+
28+
export function StaticReplayPreview({
29+
analyticsContext,
30+
initialTimeOffsetMs,
31+
isFetching,
32+
focusTab,
33+
replayId,
34+
fullReplayButtonProps,
35+
replay,
36+
}: StaticReplayPreviewProps) {
37+
const organization = useOrganization();
38+
const routes = useRoutes();
39+
const fullReplayUrl = {
40+
pathname: `/organizations/${organization.slug}/replays/${replayId}/`,
41+
query: {
42+
referrer: getRouteStringFromRoutes(routes),
43+
t_main: focusTab ?? TabKey.ERRORS,
44+
t: initialTimeOffsetMs / 1000,
45+
},
46+
};
47+
48+
const offset = useMemo(
49+
() => ({
50+
offsetMs: initialTimeOffsetMs,
51+
}),
52+
[initialTimeOffsetMs]
53+
);
54+
55+
return (
56+
<ReplayContextProvider
57+
isFetching={isFetching}
58+
replay={replay}
59+
initialTimeOffsetMs={offset}
60+
analyticsContext={analyticsContext}
61+
>
62+
<PlayerContainer data-test-id="player-container">
63+
{replay?.hasProcessingErrors() ? (
64+
<ReplayProcessingError processingErrors={replay.processingErrors()} />
65+
) : (
66+
<Fragment>
67+
<StaticPanel>
68+
<ReplayPlayer isPreview />
69+
</StaticPanel>
70+
71+
<CTAOverlay>
72+
<LinkButton
73+
{...fullReplayButtonProps}
74+
icon={<IconPlay />}
75+
priority="primary"
76+
to={fullReplayUrl}
77+
>
78+
{t('Open Replay')}
79+
</LinkButton>
80+
</CTAOverlay>
81+
</Fragment>
82+
)}
83+
</PlayerContainer>
84+
</ReplayContextProvider>
85+
);
86+
}
87+
88+
const PlayerContainer = styled(FluidHeight)`
89+
position: relative;
90+
background: ${p => p.theme.background};
91+
gap: ${space(1)};
92+
max-height: 448px;
93+
`;
94+
95+
const StaticPanel = styled(FluidHeight)`
96+
border: 1px solid ${p => p.theme.border};
97+
border-radius: ${p => p.theme.borderRadius};
98+
`;
99+
100+
const CTAOverlay = styled('div')`
101+
position: absolute;
102+
width: 100%;
103+
height: 100%;
104+
display: flex;
105+
justify-content: center;
106+
align-items: center;
107+
background: rgba(255, 255, 255, 0.5);
108+
`;

0 commit comments

Comments
 (0)