details.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import {Fragment} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import Alert from 'sentry/components/alert';
  4. import DetailedError from 'sentry/components/errors/detailedError';
  5. import NotFound from 'sentry/components/errors/notFound';
  6. import * as Layout from 'sentry/components/layouts/thirds';
  7. import List from 'sentry/components/list';
  8. import ListItem from 'sentry/components/list/listItem';
  9. import {Flex} from 'sentry/components/profiling/flex';
  10. import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
  11. import {IconDelete} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import ConfigStore from 'sentry/stores/configStore';
  14. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  15. import {space} from 'sentry/styles/space';
  16. import {decodeScalar} from 'sentry/utils/queryString';
  17. import useInitialTimeOffsetMs, {
  18. TimeOffsetLocationQueryParams,
  19. } from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs';
  20. import useLogReplayDataLoaded from 'sentry/utils/replays/hooks/useLogReplayDataLoaded';
  21. import useReplayLayout from 'sentry/utils/replays/hooks/useReplayLayout';
  22. import useReplayPageview from 'sentry/utils/replays/hooks/useReplayPageview';
  23. import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
  24. import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
  25. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  26. import {useLocation} from 'sentry/utils/useLocation';
  27. import useOrganization from 'sentry/utils/useOrganization';
  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. import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
  32. type Props = RouteComponentProps<
  33. {replaySlug: string},
  34. {},
  35. any,
  36. TimeOffsetLocationQueryParams
  37. >;
  38. function ReplayDetails({params: {replaySlug}}: Props) {
  39. const config = useLegacyStore(ConfigStore);
  40. const location = useLocation();
  41. const organization = useOrganization();
  42. useReplayPageview('replay.details-time-spent');
  43. useRouteAnalyticsEventNames('replay_details.viewed', 'Replay Details: Viewed');
  44. useRouteAnalyticsParams({
  45. organization,
  46. referrer: decodeScalar(location.query.referrer),
  47. user_email: config.user.email,
  48. tab: location.query.t_main,
  49. });
  50. const {slug: orgSlug} = organization;
  51. // TODO: replayId is known ahead of time and useReplayData is parsing it from the replaySlug
  52. // once we fix the route params and links we should fix this to accept replayId and stop returning it
  53. const {
  54. errors: replayErrors,
  55. fetchError,
  56. fetching,
  57. onRetry,
  58. projectSlug,
  59. replay,
  60. replayId,
  61. replayRecord,
  62. } = useReplayReader({
  63. replaySlug,
  64. orgSlug,
  65. });
  66. useLogReplayDataLoaded({fetchError, fetching, projectSlug, replay});
  67. const initialTimeOffsetMs = useInitialTimeOffsetMs({
  68. orgSlug,
  69. projectSlug,
  70. replayId,
  71. replayStartTimestampMs: replayRecord?.started_at?.getTime(),
  72. });
  73. if (replayRecord?.is_archived) {
  74. return (
  75. <Page
  76. orgSlug={orgSlug}
  77. replayRecord={replayRecord}
  78. projectSlug={projectSlug}
  79. replayErrors={replayErrors}
  80. >
  81. <Layout.Page>
  82. <Alert system type="warning" data-test-id="replay-deleted">
  83. <Flex gap={space(0.5)}>
  84. <IconDelete color="gray500" size="sm" />
  85. {t('This replay has been deleted.')}
  86. </Flex>
  87. </Alert>
  88. </Layout.Page>
  89. </Page>
  90. );
  91. }
  92. if (fetchError) {
  93. if (fetchError.statusText === 'Not Found') {
  94. return (
  95. <Page
  96. orgSlug={orgSlug}
  97. replayRecord={replayRecord}
  98. projectSlug={projectSlug}
  99. replayErrors={replayErrors}
  100. >
  101. <Layout.Page withPadding>
  102. <NotFound />
  103. </Layout.Page>
  104. </Page>
  105. );
  106. }
  107. const reasons = [
  108. t('The replay is still processing'),
  109. t('The replay has been deleted by a member in your organization'),
  110. t('There is an internal systems error'),
  111. ];
  112. return (
  113. <Page
  114. orgSlug={orgSlug}
  115. replayRecord={replayRecord}
  116. projectSlug={projectSlug}
  117. replayErrors={replayErrors}
  118. >
  119. <Layout.Page>
  120. <DetailedError
  121. onRetry={onRetry}
  122. hideSupportLinks
  123. heading={t('There was an error while fetching this Replay')}
  124. message={
  125. <Fragment>
  126. <p>{t('This could be due to these reasons:')}</p>
  127. <List symbol="bullet">
  128. {reasons.map((reason, i) => (
  129. <ListItem key={i}>{reason}</ListItem>
  130. ))}
  131. </List>
  132. </Fragment>
  133. }
  134. />
  135. </Layout.Page>
  136. </Page>
  137. );
  138. }
  139. if (!fetching && replay && replay.getRRWebFrames().length < 2) {
  140. return (
  141. <Page
  142. orgSlug={orgSlug}
  143. replayRecord={replayRecord}
  144. projectSlug={projectSlug}
  145. replayErrors={replayErrors}
  146. >
  147. <DetailedError
  148. hideSupportLinks
  149. heading={t('Error loading replay')}
  150. message={
  151. <Fragment>
  152. <p>
  153. {t(
  154. 'Expected two or more replay events. This Replay may not have captured any user actions.'
  155. )}
  156. </p>
  157. <p>
  158. {t(
  159. 'Or there may be an issue loading the actions from the server, click to try loading the Replay again.'
  160. )}
  161. </p>
  162. </Fragment>
  163. }
  164. />
  165. </Page>
  166. );
  167. }
  168. return (
  169. <ReplayContextProvider
  170. isFetching={fetching}
  171. replay={replay}
  172. initialTimeOffsetMs={initialTimeOffsetMs}
  173. >
  174. <ReplayTransactionContext replayRecord={replayRecord}>
  175. <DetailsInsideContext
  176. orgSlug={orgSlug}
  177. replayRecord={replayRecord}
  178. projectSlug={projectSlug}
  179. replayErrors={replayErrors}
  180. />
  181. </ReplayTransactionContext>
  182. </ReplayContextProvider>
  183. );
  184. }
  185. function DetailsInsideContext({
  186. orgSlug,
  187. replayRecord,
  188. projectSlug,
  189. replayErrors,
  190. }: {
  191. orgSlug: string;
  192. projectSlug: string | null;
  193. replayErrors: ReplayError[];
  194. replayRecord: ReplayRecord | undefined;
  195. }) {
  196. const {getLayout} = useReplayLayout();
  197. return (
  198. <Page
  199. orgSlug={orgSlug}
  200. replayRecord={replayRecord}
  201. projectSlug={projectSlug}
  202. replayErrors={replayErrors}
  203. >
  204. <ReplaysLayout layout={getLayout()} />
  205. </Page>
  206. );
  207. }
  208. export default ReplayDetails;