index.tsx 7.6 KB

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