replayPreview.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import {ComponentProps, Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Alert} from 'sentry/components/alert';
  4. import {LinkButton} from 'sentry/components/button';
  5. import ExternalLink from 'sentry/components/links/externalLink';
  6. import Link from 'sentry/components/links/link';
  7. import List from 'sentry/components/list';
  8. import ListItem from 'sentry/components/list/listItem';
  9. import Placeholder from 'sentry/components/placeholder';
  10. import {Flex} from 'sentry/components/profiling/flex';
  11. import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
  12. import ReplayPlayer from 'sentry/components/replays/replayPlayer';
  13. import ReplayProcessingError from 'sentry/components/replays/replayProcessingError';
  14. import {IconDelete, IconPlay} from 'sentry/icons';
  15. import {t, tct} from 'sentry/locale';
  16. import {space} from 'sentry/styles/space';
  17. import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
  18. import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
  19. import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
  20. import RequestError from 'sentry/utils/requestError/requestError';
  21. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  22. import {useRoutes} from 'sentry/utils/useRoutes';
  23. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  24. import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
  25. import {ReplayRecord} from 'sentry/views/replays/types';
  26. type Props = {
  27. eventTimestampMs: number;
  28. orgSlug: string;
  29. replaySlug: string;
  30. buttonProps?: Partial<ComponentProps<typeof LinkButton>>;
  31. focusTab?: TabKey;
  32. };
  33. function getReplayAnalyticsStatus({
  34. fetchError,
  35. replayRecord,
  36. }: {
  37. fetchError?: RequestError;
  38. replayRecord?: ReplayRecord;
  39. }) {
  40. if (fetchError) {
  41. return 'error';
  42. }
  43. if (replayRecord?.is_archived) {
  44. return 'archived';
  45. }
  46. if (replayRecord) {
  47. return 'success';
  48. }
  49. return 'none';
  50. }
  51. function ReplayPreview({
  52. buttonProps,
  53. eventTimestampMs,
  54. focusTab,
  55. orgSlug,
  56. replaySlug,
  57. }: Props) {
  58. const routes = useRoutes();
  59. const {fetching, replay, replayRecord, fetchError, replayId} = useReplayReader({
  60. orgSlug,
  61. replaySlug,
  62. });
  63. const startTimestampMs = replayRecord?.started_at?.getTime() ?? 0;
  64. const initialTimeOffsetMs = useMemo(() => {
  65. if (eventTimestampMs && startTimestampMs) {
  66. return Math.abs(eventTimestampMs - startTimestampMs);
  67. }
  68. return 0;
  69. }, [eventTimestampMs, startTimestampMs]);
  70. useRouteAnalyticsParams({
  71. event_replay_status: getReplayAnalyticsStatus({fetchError, replayRecord}),
  72. });
  73. if (replayRecord?.is_archived) {
  74. return (
  75. <Alert type="warning" data-test-id="replay-error">
  76. <Flex gap={space(0.5)}>
  77. <IconDelete color="gray500" size="sm" />
  78. {t('The replay for this event has been deleted.')}
  79. </Flex>
  80. </Alert>
  81. );
  82. }
  83. if (fetchError) {
  84. const reasons = [
  85. t('The replay is still processing'),
  86. tct(
  87. 'The replay was rate-limited and could not be accepted. [link:View the stats page] for more information.',
  88. {
  89. link: <Link to={`/organizations/${orgSlug}/stats/?dataCategory=replays`} />,
  90. }
  91. ),
  92. t('The replay has been deleted by a member in your organization.'),
  93. t('There were network errors and the replay was not saved.'),
  94. tct('[link:Read the docs] to understand why.', {
  95. link: (
  96. <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/#error-linking" />
  97. ),
  98. }),
  99. ];
  100. return (
  101. <Alert
  102. type="info"
  103. showIcon
  104. data-test-id="replay-error"
  105. trailingItems={
  106. <LinkButton
  107. external
  108. href="https://docs.sentry.io/platforms/javascript/session-replay/#error-linking"
  109. size="xs"
  110. >
  111. {t('Read Docs')}
  112. </LinkButton>
  113. }
  114. >
  115. <p>
  116. {t(
  117. 'The replay for this event cannot be found. This could be due to these reasons:'
  118. )}
  119. </p>
  120. <List symbol="bullet">
  121. {reasons.map((reason, i) => (
  122. <ListItem key={i}>{reason}</ListItem>
  123. ))}
  124. </List>
  125. </Alert>
  126. );
  127. }
  128. if (fetching || !replayRecord) {
  129. return (
  130. <StyledPlaceholder
  131. testId="replay-loading-placeholder"
  132. height="400px"
  133. width="100%"
  134. />
  135. );
  136. }
  137. const fullReplayUrl = {
  138. pathname: normalizeUrl(`/organizations/${orgSlug}/replays/${replayId}/`),
  139. query: {
  140. referrer: getRouteStringFromRoutes(routes),
  141. t_main: focusTab ?? TabKey.ERRORS,
  142. t: initialTimeOffsetMs / 1000,
  143. },
  144. };
  145. return (
  146. <ReplayContextProvider
  147. isFetching={fetching}
  148. replay={replay}
  149. initialTimeOffsetMs={{offsetMs: initialTimeOffsetMs}}
  150. >
  151. <PlayerContainer data-test-id="player-container">
  152. {replay?.hasProcessingErrors() ? (
  153. <ReplayProcessingError processingErrors={replay.processingErrors()} />
  154. ) : (
  155. <Fragment>
  156. <StaticPanel>
  157. <ReplayPlayer isPreview />
  158. </StaticPanel>
  159. <CTAOverlay>
  160. <LinkButton
  161. {...buttonProps}
  162. icon={<IconPlay />}
  163. priority="primary"
  164. to={fullReplayUrl}
  165. >
  166. {t('Open Replay')}
  167. </LinkButton>
  168. </CTAOverlay>
  169. </Fragment>
  170. )}
  171. </PlayerContainer>
  172. </ReplayContextProvider>
  173. );
  174. }
  175. const PlayerContainer = styled(FluidHeight)`
  176. position: relative;
  177. background: ${p => p.theme.background};
  178. gap: ${space(1)};
  179. max-height: 448px;
  180. `;
  181. const StaticPanel = styled(FluidHeight)`
  182. border: 1px solid ${p => p.theme.border};
  183. border-radius: ${p => p.theme.borderRadius};
  184. `;
  185. const CTAOverlay = styled('div')`
  186. position: absolute;
  187. width: 100%;
  188. height: 100%;
  189. display: flex;
  190. justify-content: center;
  191. align-items: center;
  192. background: rgba(255, 255, 255, 0.5);
  193. `;
  194. const StyledPlaceholder = styled(Placeholder)`
  195. margin-bottom: ${space(2)};
  196. `;
  197. export default ReplayPreview;