useSortedTimeSeries.tsx 6.9 KB

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