useSortedTimeSeries.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import {useMemo} from 'react';
  2. import type {Series} from 'sentry/types/echarts';
  3. import type {
  4. Confidence,
  5. EventsStats,
  6. GroupedMultiSeriesEventsStats,
  7. MultiSeriesEventsStats,
  8. } from 'sentry/types/organization';
  9. import {defined} from 'sentry/utils';
  10. import {encodeSort} from 'sentry/utils/discover/eventView';
  11. import {DURATION_UNITS, SIZE_UNITS} from 'sentry/utils/discover/fieldRenderers';
  12. import {getAggregateAlias} from 'sentry/utils/discover/fields';
  13. import {
  14. type DiscoverQueryProps,
  15. useGenericDiscoverQuery,
  16. } from 'sentry/utils/discover/genericDiscoverQuery';
  17. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  18. import type {MutableSearch} from 'sentry/utils/tokenizeSearch';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import usePageFilters from 'sentry/utils/usePageFilters';
  22. import {determineSeriesConfidence} from 'sentry/views/alerts/rules/metric/utils/determineSeriesConfidence';
  23. import {getSeriesEventView} from 'sentry/views/insights/common/queries/getSeriesEventView';
  24. import type {SpanFunctions, SpanIndexedField} from 'sentry/views/insights/types';
  25. import {
  26. isEventsStats,
  27. isMultiSeriesEventsStats,
  28. } from '../../../dashboards/utils/isEventsStats';
  29. import {getRetryDelay, shouldRetryHandler} from '../utils/retryHandlers';
  30. type SeriesMap = {
  31. [seriesName: string]: Series[];
  32. };
  33. interface Options<Fields> {
  34. enabled?: boolean;
  35. fields?: string[];
  36. interval?: string;
  37. orderby?: string | string[];
  38. overriddenRoute?: string;
  39. referrer?: string;
  40. search?: MutableSearch;
  41. topEvents?: number;
  42. yAxis?: Fields;
  43. }
  44. export const useSortedTimeSeries = <
  45. Fields extends SpanIndexedField[] | SpanFunctions[] | string[],
  46. >(
  47. options: Options<Fields> = {},
  48. referrer: string,
  49. dataset?: DiscoverDatasets
  50. ) => {
  51. const location = useLocation();
  52. const organization = useOrganization();
  53. const {
  54. search,
  55. yAxis = [],
  56. interval,
  57. topEvents,
  58. fields,
  59. orderby,
  60. overriddenRoute,
  61. enabled,
  62. } = options;
  63. const pageFilters = usePageFilters();
  64. const eventView = getSeriesEventView(
  65. search,
  66. fields,
  67. pageFilters.selection,
  68. yAxis,
  69. topEvents,
  70. dataset ?? DiscoverDatasets.SPANS_INDEXED,
  71. orderby
  72. );
  73. if (interval) {
  74. eventView.interval = interval;
  75. }
  76. const result = useGenericDiscoverQuery<
  77. MultiSeriesEventsStats | GroupedMultiSeriesEventsStats,
  78. DiscoverQueryProps
  79. >({
  80. route: overriddenRoute ?? 'events-stats',
  81. eventView,
  82. location,
  83. orgSlug: organization.slug,
  84. getRequestPayload: () => ({
  85. ...eventView.getEventsAPIPayload(location),
  86. yAxis: eventView.yAxis,
  87. topEvents: eventView.topEvents,
  88. excludeOther: 0,
  89. partial: 1,
  90. orderby: eventView.sorts?.[0] ? encodeSort(eventView.sorts?.[0]) : undefined,
  91. interval: eventView.interval,
  92. }),
  93. options: {
  94. enabled: enabled && pageFilters.isReady,
  95. refetchOnWindowFocus: false,
  96. retry: shouldRetryHandler,
  97. retryDelay: getRetryDelay,
  98. staleTime: Infinity,
  99. },
  100. referrer,
  101. });
  102. const isFetchingOrLoading = result.isPending || result.isFetching;
  103. const data: SeriesMap = useMemo(() => {
  104. return isFetchingOrLoading ? {} : transformToSeriesMap(result.data, yAxis);
  105. }, [isFetchingOrLoading, result.data, yAxis]);
  106. const pageLinks = result.response?.getResponseHeader('Link') ?? undefined;
  107. return {
  108. ...result,
  109. pageLinks,
  110. data,
  111. meta: result.data?.meta,
  112. };
  113. };
  114. export function transformToSeriesMap(
  115. result: MultiSeriesEventsStats | GroupedMultiSeriesEventsStats | undefined,
  116. yAxis: string[]
  117. ): SeriesMap {
  118. if (!result) {
  119. return {};
  120. }
  121. // Single series, applies to single axis queries
  122. const firstYAxis = yAxis[0] || '';
  123. if (isEventsStats(result)) {
  124. const [, series] = processSingleEventStats(firstYAxis, result);
  125. return {
  126. [firstYAxis]: [series],
  127. };
  128. }
  129. // Multiple series, applies to multi axis or topN events queries
  130. const hasMultipleYAxes = yAxis.length > 1;
  131. if (isMultiSeriesEventsStats(result)) {
  132. const processedResults: Array<[number, Series]> = Object.keys(result).map(
  133. seriesName => processSingleEventStats(seriesName, result[seriesName]!)
  134. );
  135. if (!hasMultipleYAxes) {
  136. return {
  137. [firstYAxis]: processedResults
  138. .sort(([a], [b]) => a - b)
  139. .map(([, series]) => series),
  140. };
  141. }
  142. return processedResults
  143. .sort(([a], [b]) => a - b)
  144. .reduce((acc, [, series]) => {
  145. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  146. acc[series.seriesName] = [series];
  147. return acc;
  148. }, {});
  149. }
  150. // Grouped multi series, applies to topN events queries with multiple y-axes
  151. // First, we process the grouped multi series into a list of [seriesName, order, {[aggFunctionAlias]: EventsStats}]
  152. // to enable sorting.
  153. const processedResults: Array<[string, number, MultiSeriesEventsStats]> = [];
  154. Object.keys(result).forEach(groupName => {
  155. const {order: groupOrder, ...groupData} = result[groupName]!;
  156. processedResults.push([
  157. groupName,
  158. groupOrder || 0,
  159. groupData as MultiSeriesEventsStats,
  160. ]);
  161. });
  162. return processedResults
  163. .sort(([, orderA], [, orderB]) => orderA - orderB)
  164. .reduce((acc, [seriesName, , groupData]) => {
  165. Object.keys(groupData).forEach(aggFunctionAlias => {
  166. const [, series] = processSingleEventStats(
  167. seriesName,
  168. groupData[aggFunctionAlias]!
  169. );
  170. if (!acc[aggFunctionAlias]) {
  171. acc[aggFunctionAlias] = [series];
  172. } else {
  173. acc[aggFunctionAlias].push(series);
  174. }
  175. });
  176. return acc;
  177. }, {} as SeriesMap);
  178. }
  179. function processSingleEventStats(
  180. seriesName: string,
  181. seriesData: EventsStats
  182. ): [number, Series] {
  183. let scale = 1;
  184. if (seriesName) {
  185. const unit = seriesData.meta?.units?.[getAggregateAlias(seriesName)];
  186. // Scale series values to milliseconds or bytes depending on units from meta
  187. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  188. scale = (unit && (DURATION_UNITS[unit] ?? SIZE_UNITS[unit])) ?? 1;
  189. }
  190. const processedData: Series = {
  191. seriesName: seriesName || '(empty string)',
  192. data: seriesData.data.map(([timestamp, countsForTimestamp]) => ({
  193. name: timestamp * 1000,
  194. value: countsForTimestamp.reduce((acc, {count}) => acc + count, 0) * scale,
  195. })),
  196. };
  197. const confidence: Confidence = determineSeriesConfidence(seriesData);
  198. if (defined(confidence)) {
  199. processedData.confidence = confidence;
  200. }
  201. return [seriesData.order || 0, processedData];
  202. }