eventsRequest.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import * as React from 'react';
  2. import isEqual from 'lodash/isEqual';
  3. import omitBy from 'lodash/omitBy';
  4. import {doEventsRequest} from 'app/actionCreators/events';
  5. import {addErrorMessage} from 'app/actionCreators/indicator';
  6. import {Client} from 'app/api';
  7. import LoadingPanel from 'app/components/charts/loadingPanel';
  8. import {canIncludePreviousPeriod, isMultiSeriesStats} from 'app/components/charts/utils';
  9. import {t} from 'app/locale';
  10. import {
  11. DateString,
  12. EventsStats,
  13. EventsStatsData,
  14. MultiSeriesEventsStats,
  15. OrganizationSummary,
  16. } from 'app/types';
  17. import {Series, SeriesDataUnit} from 'app/types/echarts';
  18. export type TimeSeriesData = {
  19. // timeseries data
  20. timeseriesData?: Series[];
  21. allTimeseriesData?: EventsStatsData;
  22. originalTimeseriesData?: EventsStatsData;
  23. timeseriesTotals?: {count: number};
  24. originalPreviousTimeseriesData?: EventsStatsData | null;
  25. previousTimeseriesData?: Series | null;
  26. timeAggregatedData?: Series | {};
  27. };
  28. type LoadingStatus = {
  29. loading: boolean;
  30. reloading: boolean;
  31. /**
  32. * Whether there was an error retrieving data
  33. */
  34. errored: boolean;
  35. };
  36. // Chart format for multiple series.
  37. type MultiSeriesResults = Series[];
  38. type RenderProps = LoadingStatus & TimeSeriesData & {results?: MultiSeriesResults};
  39. type DefaultProps = {
  40. /**
  41. * Relative time period for query.
  42. *
  43. * Use `start` and `end` for absolute dates.
  44. *
  45. * e.g. 24h, 7d, 30d
  46. */
  47. period?: string;
  48. /**
  49. * Absolute start date for query
  50. */
  51. start?: DateString;
  52. /**
  53. * Absolute end date for query
  54. */
  55. end?: DateString;
  56. /**
  57. * Interval to group results in
  58. *
  59. * e.g. 1d, 1h, 1m, 1s
  60. */
  61. interval: string;
  62. /**
  63. * number of rows to return
  64. */
  65. limit: number;
  66. /**
  67. * The query string to search events by
  68. */
  69. query: string;
  70. /**
  71. * Include data for previous period
  72. */
  73. includePrevious: boolean;
  74. /**
  75. * Transform the response data to be something ingestible by charts
  76. */
  77. includeTransformedData: boolean;
  78. };
  79. type EventsRequestPartialProps = {
  80. /**
  81. * API client instance
  82. */
  83. api: Client;
  84. organization: OrganizationSummary;
  85. /**
  86. * List of project ids to query
  87. */
  88. project?: Readonly<number[]>;
  89. /**
  90. * List of environments to query
  91. */
  92. environment?: Readonly<string[]>;
  93. /**
  94. * List of team ids to query
  95. */
  96. team?: Readonly<string | string[]>;
  97. /**
  98. * List of fields to group with when doing a topEvents request.
  99. */
  100. field?: string[];
  101. /**
  102. * Initial loading state
  103. */
  104. loading?: boolean;
  105. /**
  106. * Should loading be shown.
  107. */
  108. showLoading?: boolean;
  109. /**
  110. * The yAxis being plotted. If multiple yAxis are requested,
  111. * the child render function will be called with `results`
  112. */
  113. yAxis?: string | string[];
  114. /**
  115. * Name used for display current series data set tooltip
  116. */
  117. currentSeriesName?: string;
  118. previousSeriesName?: string;
  119. children: (renderProps: RenderProps) => React.ReactNode;
  120. /**
  121. * The number of top results to get. When set a multi-series result will be returned
  122. * in the `results` child render function.
  123. */
  124. topEvents?: number;
  125. /**
  126. * How to order results when getting top events.
  127. */
  128. orderby?: string;
  129. /**
  130. * Discover needs confirmation to run >30 day >10 project queries,
  131. * optional and when not passed confirmation is not required.
  132. */
  133. confirmedQuery?: boolean;
  134. /**
  135. * Is query out of retention
  136. */
  137. expired?: boolean;
  138. /**
  139. * Query name used for displaying error toast if it is out of retention
  140. */
  141. name?: string;
  142. /**
  143. * Whether or not to include the last partial bucket. This happens for example when the
  144. * current time is 11:26 and the last bucket ranges from 11:25-11:30. This means that
  145. * the last bucket contains 1 minute worth of data while the rest contains 5 minutes.
  146. *
  147. * This flag indicates whether or not this last bucket should be included in the result.
  148. */
  149. partial: boolean;
  150. /**
  151. * Hide error toast (used for pages which also query eventsV2)
  152. */
  153. hideError?: boolean;
  154. };
  155. type TimeAggregationProps =
  156. | {includeTimeAggregation: true; timeAggregationSeriesName: string}
  157. | {includeTimeAggregation?: false; timeAggregationSeriesName?: undefined};
  158. type EventsRequestProps = DefaultProps & TimeAggregationProps & EventsRequestPartialProps;
  159. type EventsRequestState = {
  160. reloading: boolean;
  161. errored: boolean;
  162. timeseriesData: null | EventsStats | MultiSeriesEventsStats;
  163. fetchedWithPrevious: boolean;
  164. };
  165. const propNamesToIgnore = ['api', 'children', 'organization', 'loading'];
  166. const omitIgnoredProps = (props: EventsRequestProps) =>
  167. omitBy(props, (_value, key) => propNamesToIgnore.includes(key));
  168. class EventsRequest extends React.PureComponent<EventsRequestProps, EventsRequestState> {
  169. static defaultProps: DefaultProps = {
  170. period: undefined,
  171. start: null,
  172. end: null,
  173. interval: '1d',
  174. limit: 15,
  175. query: '',
  176. includePrevious: true,
  177. includeTransformedData: true,
  178. };
  179. state: EventsRequestState = {
  180. reloading: !!this.props.loading,
  181. errored: false,
  182. timeseriesData: null,
  183. fetchedWithPrevious: false,
  184. };
  185. componentDidMount() {
  186. this.fetchData();
  187. }
  188. componentDidUpdate(prevProps: EventsRequestProps) {
  189. if (isEqual(omitIgnoredProps(prevProps), omitIgnoredProps(this.props))) {
  190. return;
  191. }
  192. this.fetchData();
  193. }
  194. componentWillUnmount() {
  195. this.unmounting = true;
  196. }
  197. private unmounting: boolean = false;
  198. fetchData = async () => {
  199. const {api, confirmedQuery, expired, name, hideError, ...props} = this.props;
  200. let timeseriesData: EventsStats | MultiSeriesEventsStats | null = null;
  201. if (confirmedQuery === false) {
  202. return;
  203. }
  204. this.setState(state => ({
  205. reloading: state.timeseriesData !== null,
  206. errored: false,
  207. }));
  208. if (expired) {
  209. addErrorMessage(
  210. t('%s has an invalid date range. Please try a more recent date range.', name),
  211. {append: true}
  212. );
  213. this.setState({
  214. errored: true,
  215. });
  216. } else {
  217. try {
  218. api.clear();
  219. timeseriesData = await doEventsRequest(api, props);
  220. } catch (resp) {
  221. if (!hideError) {
  222. if (resp && resp.responseJSON && resp.responseJSON.detail) {
  223. addErrorMessage(resp.responseJSON.detail);
  224. } else {
  225. addErrorMessage(t('Error loading chart data'));
  226. }
  227. }
  228. this.setState({
  229. errored: true,
  230. });
  231. }
  232. }
  233. if (this.unmounting) {
  234. return;
  235. }
  236. this.setState({
  237. reloading: false,
  238. timeseriesData,
  239. fetchedWithPrevious: props.includePrevious,
  240. });
  241. };
  242. /**
  243. * Retrieves data set for the current period (since data can potentially
  244. * contain previous period's data), as well as the previous period if
  245. * possible.
  246. *
  247. * Returns `null` if data does not exist
  248. */
  249. getData = (
  250. data: EventsStatsData
  251. ): {previous: EventsStatsData | null; current: EventsStatsData} => {
  252. const {fetchedWithPrevious} = this.state;
  253. const {period, includePrevious} = this.props;
  254. const hasPreviousPeriod =
  255. fetchedWithPrevious || canIncludePreviousPeriod(includePrevious, period);
  256. // Take the floor just in case, but data should always be divisible by 2
  257. const dataMiddleIndex = Math.floor(data.length / 2);
  258. return {
  259. current: hasPreviousPeriod ? data.slice(dataMiddleIndex) : data,
  260. previous: hasPreviousPeriod ? data.slice(0, dataMiddleIndex) : null,
  261. };
  262. };
  263. // This aggregates all values per `timestamp`
  264. calculateTotalsPerTimestamp(
  265. data: EventsStatsData,
  266. getName: (
  267. timestamp: number,
  268. countArray: {count: number}[],
  269. i: number
  270. ) => number = timestamp => timestamp * 1000
  271. ): SeriesDataUnit[] {
  272. return data.map(([timestamp, countArray], i) => ({
  273. name: getName(timestamp, countArray, i),
  274. value: countArray.reduce((acc, {count}) => acc + count, 0),
  275. }));
  276. }
  277. /**
  278. * Get previous period data, but transform timestamps so that data fits unto
  279. * the current period's data axis
  280. */
  281. transformPreviousPeriodData(
  282. current: EventsStatsData,
  283. previous: EventsStatsData | null
  284. ): Series | null {
  285. // Need the current period data array so we can take the timestamp
  286. // so we can be sure the data lines up
  287. if (!previous) {
  288. return null;
  289. }
  290. return {
  291. seriesName: this.props.previousSeriesName ?? 'Previous',
  292. data: this.calculateTotalsPerTimestamp(
  293. previous,
  294. (_timestamp, _countArray, i) => current[i][0] * 1000
  295. ),
  296. };
  297. }
  298. /**
  299. * Aggregate all counts for each time stamp
  300. */
  301. transformAggregatedTimeseries(data: EventsStatsData, seriesName: string = ''): Series {
  302. return {
  303. seriesName,
  304. data: this.calculateTotalsPerTimestamp(data),
  305. };
  306. }
  307. /**
  308. * Transforms query response into timeseries data to be used in a chart
  309. */
  310. transformTimeseriesData(data: EventsStatsData, seriesName?: string): Series[] {
  311. return [
  312. {
  313. seriesName: seriesName || 'Current',
  314. data: data.map(([timestamp, countsForTimestamp]) => ({
  315. name: timestamp * 1000,
  316. value: countsForTimestamp.reduce((acc, {count}) => acc + count, 0),
  317. })),
  318. },
  319. ];
  320. }
  321. processData(response: EventsStats | null) {
  322. if (!response) {
  323. return {};
  324. }
  325. const {data, totals} = response;
  326. const {
  327. includeTransformedData,
  328. includeTimeAggregation,
  329. timeAggregationSeriesName,
  330. } = this.props;
  331. const {current, previous} = this.getData(data);
  332. const transformedData = includeTransformedData
  333. ? this.transformTimeseriesData(current, this.props.currentSeriesName)
  334. : [];
  335. const previousData = includeTransformedData
  336. ? this.transformPreviousPeriodData(current, previous)
  337. : null;
  338. const timeAggregatedData = includeTimeAggregation
  339. ? this.transformAggregatedTimeseries(current, timeAggregationSeriesName || '')
  340. : {};
  341. return {
  342. data: transformedData,
  343. allData: data,
  344. originalData: current,
  345. totals,
  346. originalPreviousData: previous,
  347. previousData,
  348. timeAggregatedData,
  349. };
  350. }
  351. render() {
  352. const {children, showLoading, ...props} = this.props;
  353. const {timeseriesData, reloading, errored} = this.state;
  354. // Is "loading" if data is null
  355. const loading = this.props.loading || timeseriesData === null;
  356. if (showLoading && loading) {
  357. return <LoadingPanel data-test-id="events-request-loading" />;
  358. }
  359. if (isMultiSeriesStats(timeseriesData)) {
  360. // Convert multi-series results into chartable series. Multi series results
  361. // are created when multiple yAxis are used or a topEvents request is made.
  362. // Convert the timeseries data into a multi-series result set.
  363. // As the server will have replied with a map like:
  364. // {[titleString: string]: EventsStats}
  365. const results: MultiSeriesResults = Object.keys(timeseriesData)
  366. .map((seriesName: string): [number, Series] => {
  367. const seriesData: EventsStats = timeseriesData[seriesName];
  368. const transformed = this.transformTimeseriesData(
  369. seriesData.data,
  370. seriesName
  371. )[0];
  372. return [seriesData.order || 0, transformed];
  373. })
  374. .sort((a, b) => a[0] - b[0])
  375. .map(item => item[1]);
  376. return children({
  377. loading,
  378. reloading,
  379. errored,
  380. results,
  381. // sometimes we want to reference props that were given to EventsRequest
  382. ...props,
  383. });
  384. }
  385. const {
  386. data: transformedTimeseriesData,
  387. allData: allTimeseriesData,
  388. originalData: originalTimeseriesData,
  389. totals: timeseriesTotals,
  390. originalPreviousData: originalPreviousTimeseriesData,
  391. previousData: previousTimeseriesData,
  392. timeAggregatedData,
  393. } = this.processData(timeseriesData);
  394. return children({
  395. loading,
  396. reloading,
  397. errored,
  398. // timeseries data
  399. timeseriesData: transformedTimeseriesData,
  400. allTimeseriesData,
  401. originalTimeseriesData,
  402. timeseriesTotals,
  403. originalPreviousTimeseriesData,
  404. previousTimeseriesData,
  405. timeAggregatedData,
  406. // sometimes we want to reference props that were given to EventsRequest
  407. ...props,
  408. });
  409. }
  410. }
  411. export default EventsRequest;