details.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import {Fragment, useEffect} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import Alert from 'sentry/components/alert';
  4. import {Flex} from 'sentry/components/container/flex';
  5. import DetailedError from 'sentry/components/errors/detailedError';
  6. import NotFound from 'sentry/components/errors/notFound';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import List from 'sentry/components/list';
  9. import ListItem from 'sentry/components/list/listItem';
  10. import {LocalStorageReplayPreferences} from 'sentry/components/replays/preferences/replayPreferences';
  11. import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
  12. import {IconDelete} from 'sentry/icons';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import {decodeScalar} from 'sentry/utils/queryString';
  16. import type {TimeOffsetLocationQueryParams} from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs';
  17. import useInitialTimeOffsetMs from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs';
  18. import useLogReplayDataLoaded from 'sentry/utils/replays/hooks/useLogReplayDataLoaded';
  19. import useMarkReplayViewed from 'sentry/utils/replays/hooks/useMarkReplayViewed';
  20. import useReplayPageview from 'sentry/utils/replays/hooks/useReplayPageview';
  21. import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
  22. import {ReplayPreferencesContextProvider} from 'sentry/utils/replays/playback/providers/useReplayPrefs';
  23. import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
  24. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  25. import {useLocation} from 'sentry/utils/useLocation';
  26. import useOrganization from 'sentry/utils/useOrganization';
  27. import {useUser} from 'sentry/utils/useUser';
  28. import ReplaysLayout from 'sentry/views/replays/detail/layout';
  29. import Page from 'sentry/views/replays/detail/page';
  30. import ReplayTransactionContext from 'sentry/views/replays/detail/trace/replayTransactionContext';
  31. type Props = RouteComponentProps<
  32. {replaySlug: string},
  33. {},
  34. any,
  35. TimeOffsetLocationQueryParams
  36. >;
  37. function ReplayDetails({params: {replaySlug}}: Props) {
  38. const user = useUser();
  39. const location = useLocation();
  40. const organization = useOrganization();
  41. const {slug: orgSlug} = organization;
  42. // TODO: replayId is known ahead of time and useReplayData is parsing it from the replaySlug
  43. // once we fix the route params and links we should fix this to accept replayId and stop returning it
  44. const {
  45. errors,
  46. fetchError,
  47. fetching,
  48. onRetry,
  49. projectSlug,
  50. replay,
  51. replayId,
  52. replayRecord,
  53. } = useReplayReader({
  54. replaySlug,
  55. orgSlug,
  56. });
  57. const replayErrors = errors.filter(e => e.title !== 'User Feedback');
  58. const isVideoReplay = replay?.isVideoReplay();
  59. useReplayPageview('replay.details-time-spent');
  60. useRouteAnalyticsEventNames('replay_details.viewed', 'Replay Details: Viewed');
  61. useRouteAnalyticsParams({
  62. organization,
  63. referrer: decodeScalar(location.query.referrer),
  64. user_email: user.email,
  65. tab: location.query.t_main,
  66. mobile: isVideoReplay,
  67. });
  68. useLogReplayDataLoaded({fetchError, fetching, projectSlug, replay});
  69. const {mutate: markAsViewed} = useMarkReplayViewed();
  70. useEffect(() => {
  71. if (
  72. !fetchError &&
  73. replayRecord &&
  74. !replayRecord.has_viewed &&
  75. projectSlug &&
  76. !fetching &&
  77. replayId
  78. ) {
  79. markAsViewed({projectSlug, replayId});
  80. }
  81. }, [
  82. fetchError,
  83. fetching,
  84. markAsViewed,
  85. organization,
  86. projectSlug,
  87. replayId,
  88. replayRecord,
  89. ]);
  90. const initialTimeOffsetMs = useInitialTimeOffsetMs({
  91. orgSlug,
  92. projectSlug,
  93. replayId,
  94. replayStartTimestampMs: replayRecord?.started_at?.getTime(),
  95. });
  96. const rrwebFrames = replay?.getRRWebFrames();
  97. // The replay data takes a while to load in, which causes `isVideoReplay`
  98. // to return an early `false`, which used to cause UI jumping.
  99. // One way to check whether it's finished loading is by checking the length
  100. // of the rrweb frames, which should always be > 1 for any given replay.
  101. // By default, the 1 frame is replay.end
  102. const isLoading = !rrwebFrames || (rrwebFrames && rrwebFrames.length <= 1);
  103. if (replayRecord?.is_archived) {
  104. return (
  105. <Page
  106. orgSlug={orgSlug}
  107. replayRecord={replayRecord}
  108. projectSlug={projectSlug}
  109. replayErrors={replayErrors}
  110. >
  111. <Layout.Page>
  112. <Alert system type="warning" data-test-id="replay-deleted">
  113. <Flex gap={space(0.5)}>
  114. <IconDelete color="gray500" size="sm" />
  115. {t('This replay has been deleted.')}
  116. </Flex>
  117. </Alert>
  118. </Layout.Page>
  119. </Page>
  120. );
  121. }
  122. if (fetchError) {
  123. if (fetchError.status === 404) {
  124. return (
  125. <Page
  126. orgSlug={orgSlug}
  127. replayRecord={replayRecord}
  128. projectSlug={projectSlug}
  129. replayErrors={replayErrors}
  130. >
  131. <Layout.Page withPadding>
  132. <NotFound />
  133. </Layout.Page>
  134. </Page>
  135. );
  136. }
  137. const reasons = [
  138. t('The replay is still processing'),
  139. t('The replay has been deleted by a member in your organization'),
  140. t('There is an internal systems error'),
  141. ];
  142. return (
  143. <Page
  144. orgSlug={orgSlug}
  145. replayRecord={replayRecord}
  146. projectSlug={projectSlug}
  147. replayErrors={replayErrors}
  148. >
  149. <Layout.Page>
  150. <DetailedError
  151. onRetry={onRetry}
  152. hideSupportLinks
  153. heading={t('There was an error while fetching this Replay')}
  154. message={
  155. <Fragment>
  156. <p>{t('This could be due to these reasons:')}</p>
  157. <List symbol="bullet">
  158. {reasons.map((reason, i) => (
  159. <ListItem key={i}>{reason}</ListItem>
  160. ))}
  161. </List>
  162. </Fragment>
  163. }
  164. />
  165. </Layout.Page>
  166. </Page>
  167. );
  168. }
  169. return (
  170. <ReplayPreferencesContextProvider prefsStrategy={LocalStorageReplayPreferences}>
  171. <ReplayContextProvider
  172. analyticsContext="replay_details"
  173. initialTimeOffsetMs={initialTimeOffsetMs}
  174. isFetching={fetching}
  175. replay={replay}
  176. >
  177. <ReplayTransactionContext replayRecord={replayRecord}>
  178. <Page
  179. isVideoReplay={isVideoReplay}
  180. orgSlug={orgSlug}
  181. replayRecord={replayRecord}
  182. projectSlug={projectSlug}
  183. replayErrors={replayErrors}
  184. isLoading={isLoading}
  185. >
  186. <ReplaysLayout
  187. isVideoReplay={isVideoReplay}
  188. replayRecord={replayRecord}
  189. isLoading={isLoading}
  190. />
  191. </Page>
  192. </ReplayTransactionContext>
  193. </ReplayContextProvider>
  194. </ReplayPreferencesContextProvider>
  195. );
  196. }
  197. export default ReplayDetails;