useSortedTimeSeries.tsx 6.3 KB

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