index.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import {Fragment, useEffect} from 'react';
  2. import {Location} from 'history';
  3. import Feature from 'sentry/components/acl/feature';
  4. import {Alert} from 'sentry/components/alert';
  5. import * as Layout from 'sentry/components/layouts/thirds';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import {t} from 'sentry/locale';
  8. import type {Organization, Project} from 'sentry/types';
  9. import EventView from 'sentry/utils/discover/eventView';
  10. import {
  11. SPAN_OP_BREAKDOWN_FIELDS,
  12. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  13. } from 'sentry/utils/discover/fields';
  14. import {decodeScalar} from 'sentry/utils/queryString';
  15. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  16. import withOrganization from 'sentry/utils/withOrganization';
  17. import withProjects from 'sentry/utils/withProjects';
  18. import type {ReplayListLocationQuery} from 'sentry/views/replays/types';
  19. import {decodeFilterFromLocation, SpanOperationBreakdownFilter} from '../filter';
  20. import PageLayout, {ChildProps} from '../pageLayout';
  21. import Tab from '../tabs';
  22. import {decodeEventsDisplayFilterFromLocation} from '../transactionEvents/utils';
  23. import ReplaysContent from './content';
  24. import useReplaysFromTransaction from './useReplaysFromTransaction';
  25. type Props = {
  26. location: Location<ReplayListLocationQuery>;
  27. organization: Organization;
  28. projects: Project[];
  29. };
  30. function renderNoAccess() {
  31. return (
  32. <Layout.Page withPadding>
  33. <Alert type="warning">{t("You don't have access to this feature")}</Alert>
  34. </Layout.Page>
  35. );
  36. }
  37. function TransactionReplays(props: Props) {
  38. const {location, organization, projects} = props;
  39. return (
  40. <Feature
  41. features={['session-replay-ui']}
  42. organization={organization}
  43. renderDisabled={renderNoAccess}
  44. >
  45. <PageLayout
  46. location={location}
  47. organization={organization}
  48. projects={projects}
  49. tab={Tab.Replays}
  50. getDocumentTitle={getDocumentTitle}
  51. generateEventView={generateEventView}
  52. childComponent={ReplaysContentWrapper}
  53. />
  54. </Feature>
  55. );
  56. }
  57. function ReplaysContentWrapper({
  58. eventView: eventsWithReplaysView,
  59. location,
  60. organization,
  61. setError,
  62. }: ChildProps) {
  63. const eventsDisplayFilterName = decodeEventsDisplayFilterFromLocation(location);
  64. const spanOperationBreakdownFilter = decodeFilterFromLocation(location);
  65. const {eventView, replays, pageLinks, isFetching, fetchError} =
  66. useReplaysFromTransaction({
  67. eventsWithReplaysView,
  68. location,
  69. organization,
  70. eventsDisplayFilterName,
  71. spanOperationBreakdownFilter,
  72. });
  73. useEffect(() => {
  74. setError(fetchError?.message);
  75. }, [setError, fetchError]);
  76. if (isFetching || !eventView) {
  77. return (
  78. <Layout.Main fullWidth>
  79. <LoadingIndicator />
  80. </Layout.Main>
  81. );
  82. }
  83. return replays ? (
  84. <ReplaysContent
  85. eventView={eventView}
  86. isFetching={isFetching}
  87. location={location}
  88. organization={organization}
  89. pageLinks={pageLinks}
  90. replays={replays}
  91. eventsDisplayFilterName={eventsDisplayFilterName}
  92. spanOperationBreakdownFilter={spanOperationBreakdownFilter}
  93. />
  94. ) : (
  95. <Fragment>{null}</Fragment>
  96. );
  97. }
  98. function getDocumentTitle(transactionName: string): string {
  99. const hasTransactionName =
  100. typeof transactionName === 'string' && String(transactionName).trim().length > 0;
  101. if (hasTransactionName) {
  102. return [String(transactionName).trim(), t('Replays')].join(' \u2014 ');
  103. }
  104. return [t('Summary'), t('Replays')].join(' \u2014 ');
  105. }
  106. function generateEventView({
  107. location,
  108. transactionName,
  109. }: {
  110. location: Location;
  111. transactionName: string;
  112. }) {
  113. const fields = ['replayId', 'count()', 'transaction.duration', 'trace', 'timestamp'];
  114. const breakdown = decodeFilterFromLocation(location);
  115. if (breakdown !== SpanOperationBreakdownFilter.None) {
  116. fields.push(`spans.${breakdown}`);
  117. } else {
  118. fields.push(...SPAN_OP_BREAKDOWN_FIELDS, SPAN_OP_RELATIVE_BREAKDOWN_FIELD);
  119. }
  120. const query = decodeScalar(location.query.query, '');
  121. const conditions = new MutableSearch(query);
  122. conditions.setFilterValues('event.type', ['transaction']);
  123. conditions.addFilterValues('transaction', [transactionName]);
  124. conditions.addFilterValues('!replayId', ['']);
  125. return EventView.fromNewQueryWithLocation(
  126. {
  127. id: '',
  128. name: `Replay events within a transaction`,
  129. version: 2,
  130. fields,
  131. query: conditions.formatString(),
  132. projects: [],
  133. orderby: decodeScalar(location.query.sort, '-timestamp'),
  134. },
  135. location
  136. );
  137. }
  138. export default withProjects(withOrganization(TransactionReplays));