index.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import {PureComponent} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import {EventQuery} from 'sentry/actionCreators/events';
  5. import {Client} from 'sentry/api';
  6. import Pagination from 'sentry/components/pagination';
  7. import {t} from 'sentry/locale';
  8. import {Organization} from 'sentry/types';
  9. import {metric, trackAnalyticsEvent} from 'sentry/utils/analytics';
  10. import {TableData} from 'sentry/utils/discover/discoverQuery';
  11. import EventView, {
  12. isAPIPayloadSimilar,
  13. LocationQuery,
  14. } from 'sentry/utils/discover/eventView';
  15. import Measurements from 'sentry/utils/measurements/measurements';
  16. import parseLinkHeader from 'sentry/utils/parseLinkHeader';
  17. import {SPAN_OP_BREAKDOWN_FIELDS} from 'sentry/utils/performance/spanOperationBreakdowns/constants';
  18. import withApi from 'sentry/utils/withApi';
  19. import TableView from './tableView';
  20. type TableProps = {
  21. api: Client;
  22. confirmedQuery: boolean;
  23. eventView: EventView;
  24. location: Location;
  25. onChangeShowTags: () => void;
  26. organization: Organization;
  27. setError: (msg: string, code: number) => void;
  28. showTags: boolean;
  29. title: string;
  30. };
  31. type TableState = {
  32. error: null | string;
  33. isLoading: boolean;
  34. pageLinks: null | string;
  35. tableData: TableData | null | undefined;
  36. tableFetchID: symbol | undefined;
  37. };
  38. /**
  39. * `Table` is a container element that handles 2 things
  40. * 1. Fetch data from source
  41. * 2. Handle pagination of data
  42. *
  43. * It will pass the data it fetched to `TableView`, where the state of the
  44. * Table is maintained and controlled
  45. */
  46. class Table extends PureComponent<TableProps, TableState> {
  47. state: TableState = {
  48. isLoading: true,
  49. tableFetchID: undefined,
  50. error: null,
  51. pageLinks: null,
  52. tableData: null,
  53. };
  54. componentDidMount() {
  55. this.fetchData();
  56. }
  57. componentDidUpdate(prevProps: TableProps) {
  58. // Reload data if we aren't already loading, or if we've moved
  59. // from an invalid view state to a valid one.
  60. if (
  61. (!this.state.isLoading && this.shouldRefetchData(prevProps)) ||
  62. (prevProps.eventView.isValid() === false && this.props.eventView.isValid()) ||
  63. prevProps.confirmedQuery !== this.props.confirmedQuery
  64. ) {
  65. this.fetchData();
  66. }
  67. }
  68. shouldRefetchData = (prevProps: TableProps): boolean => {
  69. const thisAPIPayload = this.props.eventView.getEventsAPIPayload(this.props.location);
  70. const otherAPIPayload = prevProps.eventView.getEventsAPIPayload(prevProps.location);
  71. return !isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  72. };
  73. fetchData = () => {
  74. const {eventView, organization, location, setError, confirmedQuery} = this.props;
  75. if (!eventView.isValid() || !confirmedQuery) {
  76. return;
  77. }
  78. // note: If the eventView has no aggregates, the endpoint will automatically add the event id in
  79. // the API payload response
  80. const shouldUseEvents = organization.features.includes(
  81. 'discover-frontend-use-events-endpoint'
  82. );
  83. const url = shouldUseEvents
  84. ? `/organizations/${organization.slug}/events/`
  85. : `/organizations/${organization.slug}/eventsv2/`;
  86. const tableFetchID = Symbol('tableFetchID');
  87. const apiPayload = eventView.getEventsAPIPayload(location) as LocationQuery &
  88. EventQuery;
  89. apiPayload.referrer = 'api.discover.query-table';
  90. setError('', 200);
  91. this.setState({isLoading: true, tableFetchID});
  92. metric.mark({name: `discover-events-start-${apiPayload.query}`});
  93. this.props.api.clear();
  94. this.props.api
  95. .requestPromise(url, {
  96. method: 'GET',
  97. includeAllArgs: true,
  98. query: apiPayload,
  99. })
  100. .then(([data, _, resp]) => {
  101. // We want to measure this metric regardless of whether we use the result
  102. metric.measure({
  103. name: 'app.api.discover-query',
  104. start: `discover-events-start-${apiPayload.query}`,
  105. data: {
  106. status: resp && resp.status,
  107. },
  108. });
  109. if (this.state.tableFetchID !== tableFetchID) {
  110. // invariant: a different request was initiated after this request
  111. return;
  112. }
  113. const {fields, ...nonFieldsMeta} = data.meta ?? {};
  114. // events api uses a different response format so we need to construct tableData differently
  115. const tableData = shouldUseEvents
  116. ? {
  117. ...data,
  118. meta: {...fields, nonFieldsMeta},
  119. }
  120. : data;
  121. this.setState(prevState => ({
  122. isLoading: false,
  123. tableFetchID: undefined,
  124. error: null,
  125. pageLinks: resp ? resp.getResponseHeader('Link') : prevState.pageLinks,
  126. tableData,
  127. }));
  128. })
  129. .catch(err => {
  130. metric.measure({
  131. name: 'app.api.discover-query',
  132. start: `discover-events-start-${apiPayload.query}`,
  133. data: {
  134. status: err.status,
  135. },
  136. });
  137. const message = err?.responseJSON?.detail || t('An unknown error occurred.');
  138. this.setState({
  139. isLoading: false,
  140. tableFetchID: undefined,
  141. error: message,
  142. pageLinks: null,
  143. tableData: null,
  144. });
  145. trackAnalyticsEvent({
  146. eventKey: 'discover_search.failed',
  147. eventName: 'Discover Search: Failed',
  148. organization_id: this.props.organization.id,
  149. search_type: 'events',
  150. search_source: 'discover_search',
  151. error: message,
  152. });
  153. setError(message, err.status);
  154. });
  155. };
  156. render() {
  157. const {eventView} = this.props;
  158. const {pageLinks, tableData, isLoading, error} = this.state;
  159. const isFirstPage = pageLinks
  160. ? parseLinkHeader(pageLinks).previous.results === false
  161. : false;
  162. return (
  163. <Container>
  164. <Measurements>
  165. {({measurements}) => {
  166. const measurementKeys = Object.values(measurements).map(({key}) => key);
  167. return (
  168. <TableView
  169. {...this.props}
  170. isLoading={isLoading}
  171. isFirstPage={isFirstPage}
  172. error={error}
  173. eventView={eventView}
  174. tableData={tableData}
  175. measurementKeys={measurementKeys}
  176. spanOperationBreakdownKeys={SPAN_OP_BREAKDOWN_FIELDS}
  177. />
  178. );
  179. }}
  180. </Measurements>
  181. <Pagination pageLinks={pageLinks} />
  182. </Container>
  183. );
  184. }
  185. }
  186. export default withApi(Table);
  187. const Container = styled('div')`
  188. min-width: 0;
  189. `;