metricsRequest.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import * as React from 'react';
  2. import isEqual from 'lodash/isEqual';
  3. import omitBy from 'lodash/omitBy';
  4. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  5. import {doMetricsRequest, DoMetricsRequestOptions} from 'sentry/actionCreators/metrics';
  6. import {Client, ResponseMeta} from 'sentry/api';
  7. import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
  8. import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
  9. import {t} from 'sentry/locale';
  10. import {MetricsApiResponse} from 'sentry/types';
  11. import {getPeriod} from 'sentry/utils/getPeriod';
  12. import {TableData} from '../discover/discoverQuery';
  13. import {transformMetricsResponseToTable} from './transformMetricsResponseToTable';
  14. const propNamesToIgnore = ['api', 'children'];
  15. const omitIgnoredProps = (props: Props) =>
  16. omitBy(props, (_value, key) => propNamesToIgnore.includes(key));
  17. export type MetricsRequestRenderProps = {
  18. error: string | null;
  19. errored: boolean;
  20. isLoading: boolean;
  21. loading: boolean;
  22. pageLinks: string | null;
  23. reloading: boolean;
  24. response: MetricsApiResponse | null;
  25. responsePrevious: MetricsApiResponse | null;
  26. tableData?: TableData;
  27. };
  28. type DefaultProps = {
  29. /**
  30. * Include data for previous period
  31. */
  32. includePrevious: boolean;
  33. /**
  34. * Transform the response data to be something ingestible by GridEditable tables
  35. */
  36. includeTabularData?: boolean;
  37. /**
  38. * If true, no request will be made
  39. */
  40. isDisabled?: boolean;
  41. };
  42. type Props = DefaultProps &
  43. Omit<
  44. DoMetricsRequestOptions,
  45. 'includeAllArgs' | 'statsPeriodStart' | 'statsPeriodEnd'
  46. > & {
  47. api: Client;
  48. children?: (renderProps: MetricsRequestRenderProps) => React.ReactNode;
  49. };
  50. type State = {
  51. error: string | null;
  52. errored: boolean;
  53. pageLinks: string | null;
  54. reloading: boolean;
  55. response: MetricsApiResponse | null;
  56. responsePrevious: MetricsApiResponse | null;
  57. };
  58. class MetricsRequest extends React.Component<Props, State> {
  59. static defaultProps: DefaultProps = {
  60. includePrevious: false,
  61. includeTabularData: false,
  62. isDisabled: false,
  63. };
  64. state: State = {
  65. reloading: false,
  66. errored: false,
  67. error: null,
  68. response: null,
  69. responsePrevious: null,
  70. pageLinks: null,
  71. };
  72. componentDidMount() {
  73. this.fetchData();
  74. }
  75. componentDidUpdate(prevProps: Props) {
  76. if (isEqual(omitIgnoredProps(prevProps), omitIgnoredProps(this.props))) {
  77. return;
  78. }
  79. this.fetchData();
  80. }
  81. componentWillUnmount() {
  82. this.unmounting = true;
  83. }
  84. private unmounting: boolean = false;
  85. getQueryParams({previousPeriod = false} = {}) {
  86. const {
  87. project,
  88. environment,
  89. field,
  90. query,
  91. groupBy,
  92. orderBy,
  93. limit,
  94. interval,
  95. cursor,
  96. statsPeriod,
  97. start,
  98. end,
  99. orgSlug,
  100. } = this.props;
  101. const commonQuery = {
  102. field,
  103. cursor,
  104. environment,
  105. groupBy,
  106. interval,
  107. query,
  108. limit,
  109. project,
  110. orderBy,
  111. orgSlug,
  112. };
  113. if (!previousPeriod) {
  114. return {
  115. ...commonQuery,
  116. statsPeriod,
  117. start,
  118. end,
  119. };
  120. }
  121. const doubledStatsPeriod = getPeriod(
  122. {period: statsPeriod, start: undefined, end: undefined},
  123. {shouldDoublePeriod: true}
  124. ).statsPeriod;
  125. return {
  126. ...commonQuery,
  127. statsPeriodStart: doubledStatsPeriod,
  128. statsPeriodEnd: statsPeriod ?? DEFAULT_STATS_PERIOD,
  129. };
  130. }
  131. fetchData = async () => {
  132. const {api, isDisabled, start, end, statsPeriod, includePrevious} = this.props;
  133. if (isDisabled) {
  134. return;
  135. }
  136. this.setState(state => ({
  137. reloading: state.response !== null,
  138. errored: false,
  139. error: null,
  140. pageLinks: null,
  141. }));
  142. const promises = [
  143. doMetricsRequest(api, {includeAllArgs: true, ...this.getQueryParams()}),
  144. ];
  145. // TODO(metrics): this could be merged into one request by doubling the statsPeriod and then splitting the response in half
  146. if (shouldFetchPreviousPeriod({start, end, period: statsPeriod, includePrevious})) {
  147. promises.push(doMetricsRequest(api, this.getQueryParams({previousPeriod: true})));
  148. }
  149. try {
  150. const [[response, _, responseMeta], responsePrevious] = (await Promise.all(
  151. promises
  152. )) as [
  153. [MetricsApiResponse, string | undefined, ResponseMeta | undefined],
  154. MetricsApiResponse | undefined
  155. ];
  156. if (this.unmounting) {
  157. return;
  158. }
  159. this.setState({
  160. reloading: false,
  161. response,
  162. responsePrevious: responsePrevious ?? null,
  163. pageLinks: responseMeta?.getResponseHeader('Link') ?? null,
  164. });
  165. } catch (error) {
  166. addErrorMessage(error.responseJSON?.detail ?? t('Error loading metrics data'));
  167. this.setState({
  168. reloading: false,
  169. errored: true,
  170. error: error.responseJSON?.detail ?? null,
  171. pageLinks: null,
  172. });
  173. }
  174. };
  175. render() {
  176. const {reloading, errored, error, response, responsePrevious, pageLinks} = this.state;
  177. const {children, isDisabled, includeTabularData} = this.props;
  178. const loading = response === null && !isDisabled && !error;
  179. return children?.({
  180. loading,
  181. reloading,
  182. isLoading: loading || reloading, // some components downstream are used to loading/reloading or isLoading that combines both (EventsRequest vs DiscoverQuery)
  183. errored,
  184. error,
  185. response,
  186. responsePrevious,
  187. pageLinks,
  188. tableData: includeTabularData
  189. ? transformMetricsResponseToTable({response})
  190. : undefined,
  191. });
  192. }
  193. }
  194. export default MetricsRequest;