transactionReplays.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import {Fragment, useEffect, useMemo} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import {Location} from 'history';
  4. import * as Layout from 'sentry/components/layouts/thirds';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import {t} from 'sentry/locale';
  7. import type {Organization} from 'sentry/types';
  8. import EventView from 'sentry/utils/discover/eventView';
  9. import {
  10. SPAN_OP_BREAKDOWN_FIELDS,
  11. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  12. } from 'sentry/utils/discover/fields';
  13. import useReplayList from 'sentry/utils/replays/hooks/useReplayList';
  14. import {useLocation} from 'sentry/utils/useLocation';
  15. import useMedia from 'sentry/utils/useMedia';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import useProjects from 'sentry/utils/useProjects';
  18. import PageLayout, {
  19. ChildProps,
  20. } from 'sentry/views/performance/transactionSummary/pageLayout';
  21. import Tab from 'sentry/views/performance/transactionSummary/tabs';
  22. import ReplayTable from 'sentry/views/replays/replayTable';
  23. import {ReplayColumn} from 'sentry/views/replays/replayTable/types';
  24. import type {ReplayListLocationQuery} from 'sentry/views/replays/types';
  25. import useReplaysFromTransaction, {EventSpanData} from './useReplaysFromTransaction';
  26. import useReplaysWithTxData from './useReplaysWithTxData';
  27. function TransactionReplays() {
  28. const location = useLocation<ReplayListLocationQuery>();
  29. const organization = useOrganization();
  30. const {projects} = useProjects();
  31. return (
  32. <PageLayout
  33. location={{
  34. ...location,
  35. query: {
  36. ...location.query,
  37. statsPeriod: '14d',
  38. },
  39. }}
  40. organization={organization}
  41. projects={projects}
  42. tab={Tab.REPLAYS}
  43. getDocumentTitle={getDocumentTitle}
  44. generateEventView={generateEventView}
  45. childComponent={ReplaysContentWrapper}
  46. />
  47. );
  48. }
  49. function getDocumentTitle(transactionName: string): string {
  50. const hasTransactionName =
  51. typeof transactionName === 'string' && String(transactionName).trim().length > 0;
  52. if (hasTransactionName) {
  53. return [String(transactionName).trim(), t('Replays')].join(' \u2014 ');
  54. }
  55. return [t('Summary'), t('Replays')].join(' \u2014 ');
  56. }
  57. function generateEventView({
  58. location,
  59. transactionName,
  60. }: {
  61. location: Location;
  62. transactionName: string;
  63. }) {
  64. const fields = [
  65. 'replayId',
  66. 'count()',
  67. 'transaction.duration',
  68. 'trace',
  69. 'timestamp',
  70. ...SPAN_OP_BREAKDOWN_FIELDS,
  71. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  72. ];
  73. return EventView.fromSavedQuery({
  74. id: '',
  75. name: `Replay events within a transaction`,
  76. version: 2,
  77. fields,
  78. query: `event.type:transaction transaction:"${transactionName}" !replayId:""`,
  79. projects: [Number(location.query.project)],
  80. });
  81. }
  82. function ReplaysContentWrapper({
  83. eventView: replayIdsEventView,
  84. location,
  85. organization,
  86. setError,
  87. }: ChildProps) {
  88. const {data, fetchError, isFetching, pageLinks} = useReplaysFromTransaction({
  89. replayIdsEventView,
  90. location,
  91. organization,
  92. });
  93. useEffect(() => {
  94. setError(fetchError?.message ?? fetchError);
  95. }, [setError, fetchError]);
  96. if (!data) {
  97. return isFetching ? (
  98. <Layout.Main fullWidth>
  99. <LoadingIndicator />
  100. </Layout.Main>
  101. ) : (
  102. <Fragment>{null}</Fragment>
  103. );
  104. }
  105. const {events, replayRecordsEventView} = data;
  106. return (
  107. <ReplaysContent
  108. eventView={replayRecordsEventView}
  109. events={events}
  110. organization={organization}
  111. pageLinks={pageLinks}
  112. />
  113. );
  114. }
  115. function ReplaysContent({
  116. eventView,
  117. events,
  118. organization,
  119. }: {
  120. eventView: EventView;
  121. events: EventSpanData[];
  122. organization: Organization;
  123. pageLinks: string | null;
  124. }) {
  125. const location = useMemo(() => ({query: {}}) as Location<ReplayListLocationQuery>, []);
  126. const theme = useTheme();
  127. const hasRoomForColumns = useMedia(`(min-width: ${theme.breakpoints.small})`);
  128. const {replays, isFetching, fetchError} = useReplayList({
  129. eventView,
  130. location,
  131. organization,
  132. });
  133. const replaysWithTx = useReplaysWithTxData({
  134. replays,
  135. events,
  136. });
  137. return (
  138. <Layout.Main fullWidth>
  139. <ReplayTable
  140. fetchError={fetchError}
  141. isFetching={isFetching}
  142. replays={replaysWithTx}
  143. sort={undefined}
  144. visibleColumns={[
  145. ReplayColumn.REPLAY,
  146. ...(hasRoomForColumns ? [ReplayColumn.SLOWEST_TRANSACTION] : []),
  147. ReplayColumn.OS,
  148. ReplayColumn.BROWSER,
  149. ReplayColumn.DURATION,
  150. ReplayColumn.COUNT_ERRORS,
  151. ReplayColumn.ACTIVITY,
  152. ]}
  153. showDropdownFilters={false}
  154. />
  155. </Layout.Main>
  156. );
  157. }
  158. export default TransactionReplays;