metricsRequest.tsx 6.0 KB

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