groupReplays.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import {Fragment, useCallback, useEffect} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import {Button} from 'sentry/components/button';
  5. import * as Layout from 'sentry/components/layouts/thirds';
  6. import Placeholder from 'sentry/components/placeholder';
  7. import {StaticReplayPreferences} from 'sentry/components/replays/preferences/replayPreferences';
  8. import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
  9. import {IconPlay, IconUser} from 'sentry/icons';
  10. import {t, tn} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import type {Group} from 'sentry/types/group';
  13. import type {Organization} from 'sentry/types/organization';
  14. import {trackAnalytics} from 'sentry/utils/analytics';
  15. import {browserHistory} from 'sentry/utils/browserHistory';
  16. import type EventView from 'sentry/utils/discover/eventView';
  17. import useReplayCountForIssues from 'sentry/utils/replayCount/useReplayCountForIssues';
  18. import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
  19. import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
  20. import {useLocation} from 'sentry/utils/useLocation';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. import useUrlParams from 'sentry/utils/useUrlParams';
  23. import useAllMobileProj from 'sentry/views/replays/detail/useAllMobileProj';
  24. import ReplayTable from 'sentry/views/replays/replayTable';
  25. import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
  26. import type {ReplayListLocationQuery, ReplayListRecord} from 'sentry/views/replays/types';
  27. import {ReplayClipPreviewWrapper} from './replayClipPreviewWrapper';
  28. import useReplaysFromIssue from './useReplaysFromIssue';
  29. type Props = {
  30. group: Group;
  31. };
  32. const VISIBLE_COLUMNS = [
  33. ReplayColumn.REPLAY,
  34. ReplayColumn.OS,
  35. ReplayColumn.BROWSER,
  36. ReplayColumn.DURATION,
  37. ReplayColumn.COUNT_ERRORS,
  38. ReplayColumn.ACTIVITY,
  39. ];
  40. const VISIBLE_COLUMNS_MOBILE = [
  41. ReplayColumn.REPLAY,
  42. ReplayColumn.OS,
  43. ReplayColumn.DURATION,
  44. ReplayColumn.COUNT_ERRORS,
  45. ReplayColumn.ACTIVITY,
  46. ];
  47. const visibleColumns = (allMobileProj: boolean) =>
  48. allMobileProj ? VISIBLE_COLUMNS_MOBILE : VISIBLE_COLUMNS;
  49. function GroupReplays({group}: Props) {
  50. const organization = useOrganization();
  51. const location = useLocation<ReplayListLocationQuery>();
  52. const {eventView, fetchError, isFetching, pageLinks} = useReplaysFromIssue({
  53. group,
  54. location,
  55. organization,
  56. });
  57. const {allMobileProj} = useAllMobileProj();
  58. useEffect(() => {
  59. trackAnalytics('replay.render-issues-group-list', {
  60. project_id: group.project.id,
  61. platform: group.project.platform,
  62. organization,
  63. });
  64. // we only want to fire this event once
  65. // eslint-disable-next-line react-hooks/exhaustive-deps
  66. }, []);
  67. if (!eventView) {
  68. // Shown on load and no replay data available
  69. return (
  70. <StyledLayoutPage withPadding>
  71. <ReplayCountHeader>
  72. <IconUser size="sm" />
  73. {isFetching ? (
  74. <Placeholder height="18px" width="400px" />
  75. ) : (
  76. t('No replay data available.')
  77. )}
  78. </ReplayCountHeader>
  79. <ReplayTable
  80. fetchError={fetchError}
  81. isFetching={isFetching}
  82. replays={[]}
  83. sort={undefined}
  84. visibleColumns={visibleColumns(allMobileProj)}
  85. showDropdownFilters={false}
  86. />
  87. </StyledLayoutPage>
  88. );
  89. }
  90. return (
  91. <GroupReplaysTable
  92. eventView={eventView}
  93. organization={organization}
  94. pageLinks={pageLinks}
  95. visibleColumns={visibleColumns(allMobileProj)}
  96. group={group}
  97. />
  98. );
  99. }
  100. function GroupReplaysTableInner({
  101. children,
  102. organization,
  103. group,
  104. replaySlug,
  105. setSelectedReplayIndex,
  106. selectedReplayIndex,
  107. overlayContent,
  108. replays,
  109. pageLinks,
  110. }: {
  111. children: React.ReactNode;
  112. group: Group;
  113. organization: Organization;
  114. pageLinks: string | null;
  115. replaySlug: string;
  116. replays: ReplayListRecord[] | undefined;
  117. selectedReplayIndex: number;
  118. setSelectedReplayIndex: (index: number) => void;
  119. overlayContent?: React.ReactNode;
  120. }) {
  121. const orgSlug = organization.slug;
  122. const {fetching, replay} = useReplayReader({
  123. orgSlug,
  124. replaySlug,
  125. group,
  126. });
  127. const {allMobileProj} = useAllMobileProj();
  128. return (
  129. <ReplayContextProvider
  130. analyticsContext="replay_tab"
  131. isFetching={fetching}
  132. prefsStrategy={StaticReplayPreferences}
  133. replay={replay}
  134. autoStart
  135. >
  136. <ReplayClipPreviewWrapper
  137. orgSlug={orgSlug}
  138. replaySlug={replaySlug}
  139. group={group}
  140. pageLinks={pageLinks}
  141. selectedReplayIndex={selectedReplayIndex}
  142. setSelectedReplayIndex={setSelectedReplayIndex}
  143. visibleColumns={[ReplayColumn.PLAY_PAUSE, ...visibleColumns(allMobileProj)]}
  144. overlayContent={overlayContent}
  145. replays={replays}
  146. />
  147. {children}
  148. </ReplayContextProvider>
  149. );
  150. }
  151. const locationForFetching = {query: {}} as Location<ReplayListLocationQuery>;
  152. function GroupReplaysTable({
  153. eventView,
  154. organization,
  155. group,
  156. }: {
  157. eventView: EventView;
  158. group: Group;
  159. organization: Organization;
  160. pageLinks: string | null;
  161. visibleColumns: ReplayColumn[];
  162. }) {
  163. const location = useLocation();
  164. const urlParams = useUrlParams();
  165. const {getReplayCountForIssue} = useReplayCountForIssues({
  166. statsPeriod: '90d',
  167. });
  168. const replayListData = useReplayList({
  169. eventView,
  170. location: locationForFetching,
  171. organization,
  172. queryReferrer: 'issueReplays',
  173. });
  174. const {replays} = replayListData;
  175. const {allMobileProj} = useAllMobileProj();
  176. const rawReplayIndex = urlParams.getParamValue('selected_replay_index');
  177. const selectedReplayIndex = parseInt(
  178. typeof rawReplayIndex === 'string' ? rawReplayIndex : '0',
  179. 10
  180. );
  181. const setSelectedReplayIndex = useCallback(
  182. (index: number) => {
  183. browserHistory.replace({
  184. pathname: location.pathname,
  185. query: {...location.query, selected_replay_index: index},
  186. });
  187. },
  188. [location]
  189. );
  190. const selectedReplay = replays?.[selectedReplayIndex];
  191. const replayCount = getReplayCountForIssue(group.id, group.issueCategory);
  192. const nextReplay = replays?.[selectedReplayIndex + 1];
  193. const nextReplayText = nextReplay?.id
  194. ? `${nextReplay.user.display_name || t('Anonymous User')}`
  195. : undefined;
  196. const overlayContent =
  197. nextReplayText && replayCount && replayCount > 1 ? (
  198. <Fragment>
  199. <UpNext>{t('Up Next')}</UpNext>
  200. <OverlayText>{nextReplayText}</OverlayText>
  201. <Button
  202. onClick={() => {
  203. setSelectedReplayIndex(selectedReplayIndex + 1);
  204. }}
  205. icon={<IconPlay size="md" />}
  206. analyticsEventKey="issue_details.replay_tab.play_next_replay"
  207. analyticsEventName="Issue Details: Replay Tab Clicked Play Next Replay"
  208. >
  209. {t('Play Now')}
  210. </Button>
  211. </Fragment>
  212. ) : undefined;
  213. const replayTable = (
  214. <ReplayTable
  215. sort={undefined}
  216. visibleColumns={[
  217. ...(selectedReplay ? [ReplayColumn.PLAY_PAUSE] : []),
  218. ...visibleColumns(allMobileProj),
  219. ]}
  220. showDropdownFilters={false}
  221. onClickPlay={setSelectedReplayIndex}
  222. fetchError={replayListData.fetchError}
  223. isFetching={replayListData.isFetching}
  224. replays={replays}
  225. />
  226. );
  227. const inner = selectedReplay ? (
  228. <GroupReplaysTableInner
  229. // Use key to force unmount/remount of component to reset the context and replay iframe
  230. key={selectedReplay.id}
  231. setSelectedReplayIndex={setSelectedReplayIndex}
  232. selectedReplayIndex={selectedReplayIndex}
  233. overlayContent={overlayContent}
  234. organization={organization}
  235. group={group}
  236. replaySlug={selectedReplay.id}
  237. pageLinks={replayListData.pageLinks}
  238. replays={replays}
  239. >
  240. {replayTable}
  241. </GroupReplaysTableInner>
  242. ) : (
  243. replayTable
  244. );
  245. return (
  246. <StyledLayoutPage withPadding>
  247. <ReplayCountHeader>
  248. <IconUser size="sm" />
  249. {t(
  250. 'There are %s for this issue across %s.',
  251. tn('%s replay', '%s replays', replayCount ?? 0),
  252. tn('%s event', '%s events', group.count)
  253. )}
  254. </ReplayCountHeader>
  255. {inner}
  256. </StyledLayoutPage>
  257. );
  258. }
  259. const StyledLayoutPage = styled(Layout.Page)`
  260. box-shadow: 0px 0px 1px ${p => p.theme.gray200};
  261. background-color: ${p => p.theme.background};
  262. gap: ${space(2)};
  263. `;
  264. const ReplayCountHeader = styled('div')`
  265. display: flex;
  266. align-items: center;
  267. gap: ${space(1)};
  268. `;
  269. const OverlayText = styled('div')`
  270. font-size: ${p => p.theme.fontSizeExtraLarge};
  271. `;
  272. const UpNext = styled('div')`
  273. line-height: 0;
  274. `;
  275. export default GroupReplays;