useSpansQuery.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import moment from 'moment-timezone';
  2. import type {TableData} from 'sentry/utils/discover/discoverQuery';
  3. import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
  4. import type {EventsMetaType, MetaType} from 'sentry/utils/discover/eventView';
  5. import type EventView from 'sentry/utils/discover/eventView';
  6. import {encodeSort} from 'sentry/utils/discover/eventView';
  7. import type {DiscoverQueryProps} from 'sentry/utils/discover/genericDiscoverQuery';
  8. import {useGenericDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
  9. import {useLocation} from 'sentry/utils/useLocation';
  10. import useOrganization from 'sentry/utils/useOrganization';
  11. import usePageFilters from 'sentry/utils/usePageFilters';
  12. import {
  13. getRetryDelay,
  14. shouldRetryHandler,
  15. } from 'sentry/views/insights/common/utils/retryHandlers';
  16. import {TrackResponse} from 'sentry/views/insights/common/utils/trackResponse';
  17. export const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ';
  18. export function useSpansQuery<T = any[]>({
  19. eventView,
  20. initialData,
  21. limit,
  22. enabled,
  23. referrer = 'use-spans-query',
  24. allowAggregateConditions,
  25. cursor,
  26. }: {
  27. allowAggregateConditions?: boolean;
  28. cursor?: string;
  29. enabled?: boolean;
  30. eventView?: EventView;
  31. initialData?: T;
  32. limit?: number;
  33. referrer?: string;
  34. }) {
  35. const isTimeseriesQuery = (eventView?.yAxis?.length ?? 0) > 0;
  36. const queryFunction = isTimeseriesQuery
  37. ? useWrappedDiscoverTimeseriesQuery
  38. : useWrappedDiscoverQuery;
  39. const {isReady: pageFiltersReady} = usePageFilters();
  40. if (eventView) {
  41. const newEventView = eventView.clone();
  42. const response = queryFunction<T>({
  43. eventView: newEventView,
  44. initialData,
  45. limit,
  46. // We always want to wait until the pageFilters are ready to prevent clobbering requests
  47. enabled: (enabled || enabled === undefined) && pageFiltersReady,
  48. referrer,
  49. cursor,
  50. allowAggregateConditions,
  51. });
  52. TrackResponse(eventView, response);
  53. return response;
  54. }
  55. throw new Error('eventView argument must be defined when Starfish useDiscover is true');
  56. }
  57. export function useWrappedDiscoverTimeseriesQuery<T>({
  58. eventView,
  59. enabled,
  60. initialData,
  61. referrer,
  62. cursor,
  63. overriddenRoute,
  64. }: {
  65. eventView: EventView;
  66. cursor?: string;
  67. enabled?: boolean;
  68. initialData?: any;
  69. overriddenRoute?: string;
  70. referrer?: string;
  71. }) {
  72. const location = useLocation();
  73. const organization = useOrganization();
  74. const {isReady: pageFiltersReady} = usePageFilters();
  75. const result = useGenericDiscoverQuery<
  76. {
  77. data: any[];
  78. meta: MetaType;
  79. },
  80. DiscoverQueryProps
  81. >({
  82. route: overriddenRoute ?? 'events-stats',
  83. eventView,
  84. location,
  85. orgSlug: organization.slug,
  86. getRequestPayload: () => ({
  87. ...eventView.getEventsAPIPayload(location),
  88. yAxis: eventView.yAxis,
  89. topEvents: eventView.topEvents,
  90. excludeOther: 0,
  91. partial: 1,
  92. orderby: eventView.sorts?.[0] ? encodeSort(eventView.sorts?.[0]) : undefined,
  93. interval: eventView.interval,
  94. cursor,
  95. }),
  96. options: {
  97. enabled: enabled && pageFiltersReady,
  98. refetchOnWindowFocus: false,
  99. retry: shouldRetryHandler,
  100. retryDelay: getRetryDelay,
  101. staleTime: Infinity,
  102. },
  103. referrer,
  104. });
  105. const isFetchingOrLoading = result.isPending || result.isFetching;
  106. const defaultData = initialData ?? undefined;
  107. const data: T = isFetchingOrLoading
  108. ? defaultData
  109. : processDiscoverTimeseriesResult(result.data, eventView);
  110. const pageLinks = result.response?.getResponseHeader('Link') ?? undefined;
  111. return {
  112. ...result,
  113. pageLinks,
  114. data,
  115. meta: result.data?.meta,
  116. };
  117. }
  118. export function useWrappedDiscoverQuery<T>({
  119. eventView,
  120. initialData,
  121. enabled,
  122. referrer,
  123. limit,
  124. cursor,
  125. noPagination,
  126. allowAggregateConditions,
  127. }: {
  128. eventView: EventView;
  129. allowAggregateConditions?: boolean;
  130. cursor?: string;
  131. enabled?: boolean;
  132. initialData?: T;
  133. limit?: number;
  134. noPagination?: boolean;
  135. referrer?: string;
  136. }) {
  137. const location = useLocation();
  138. const organization = useOrganization();
  139. const {isReady: pageFiltersReady} = usePageFilters();
  140. const queryExtras: Record<string, string> = {};
  141. if (allowAggregateConditions !== undefined) {
  142. queryExtras.allowAggregateConditions = allowAggregateConditions ? '1' : '0';
  143. }
  144. const result = useDiscoverQuery({
  145. eventView,
  146. orgSlug: organization.slug,
  147. location,
  148. referrer,
  149. cursor,
  150. limit,
  151. options: {
  152. enabled: enabled && pageFiltersReady,
  153. refetchOnWindowFocus: false,
  154. retry: shouldRetryHandler,
  155. retryDelay: getRetryDelay,
  156. staleTime: Infinity,
  157. },
  158. queryExtras,
  159. noPagination,
  160. });
  161. // TODO: useDiscoverQuery incorrectly states that it returns MetaType, but it
  162. // does not!
  163. const meta = result.data?.meta as EventsMetaType | undefined;
  164. const data =
  165. result.isPending && initialData ? initialData : (result.data?.data as T | undefined);
  166. return {
  167. ...result,
  168. data,
  169. meta,
  170. };
  171. }
  172. type Interval = {interval: string; group?: string};
  173. function processDiscoverTimeseriesResult(
  174. result: TableData | undefined,
  175. eventView: EventView
  176. ) {
  177. if (!result) {
  178. return undefined;
  179. }
  180. if (!eventView.yAxis) {
  181. return [];
  182. }
  183. const firstYAxis =
  184. typeof eventView.yAxis === 'string' ? eventView.yAxis : eventView.yAxis[0];
  185. if (result.data) {
  186. // Result data only returned one series. This means there was only only one yAxis requested, and no sub-series. Iterate the data, and return the result
  187. return processSingleDiscoverTimeseriesResult(result, firstYAxis).map(data => ({
  188. interval: moment(parseInt(data.interval, 10) * 1000).format(DATE_FORMAT),
  189. [firstYAxis]: data[firstYAxis],
  190. group: data.group,
  191. }));
  192. }
  193. let intervals = [] as Interval[];
  194. // Result data had more than one series, grouped by a key. This means either multiple yAxes were requested _or_ a top-N query was set. Iterate the keys, and construct a series for each one.
  195. Object.keys(result).forEach(key => {
  196. // Each key has just one timeseries. Either this is a simple multi-axis query, or a top-N query with just one axis
  197. if (result[key].data) {
  198. intervals = mergeIntervals(
  199. intervals,
  200. processSingleDiscoverTimeseriesResult(result[key], key)
  201. );
  202. } else {
  203. // Each key has more than one timeseries. This is a multi-axis top-N query. Iterate each series, but this time set both the key _and_ the group
  204. Object.keys(result[key]).forEach(innerKey => {
  205. if (innerKey !== 'order') {
  206. // `order` is a special value, each series has it in a multi-series query
  207. intervals = mergeIntervals(
  208. intervals,
  209. processSingleDiscoverTimeseriesResult(result[key][innerKey], innerKey, key)
  210. );
  211. }
  212. });
  213. }
  214. });
  215. const processed = intervals.map(interval => ({
  216. ...interval,
  217. interval: moment(parseInt(interval.interval, 10) * 1000).format(DATE_FORMAT),
  218. }));
  219. return processed;
  220. }
  221. function processSingleDiscoverTimeseriesResult(result, key: string, group?: string) {
  222. const intervals = [] as Interval[];
  223. result.data.forEach(([timestamp, [{count: value}]]) => {
  224. const existingInterval = intervals.find(
  225. interval =>
  226. interval.interval === timestamp && (group ? interval.group === group : true)
  227. );
  228. if (existingInterval) {
  229. existingInterval[key] = value;
  230. return;
  231. }
  232. intervals.push({
  233. interval: timestamp,
  234. [key]: value,
  235. group,
  236. });
  237. });
  238. return intervals;
  239. }
  240. function mergeIntervals(first: Interval[], second: Interval[]) {
  241. const target: Interval[] = JSON.parse(JSON.stringify(first));
  242. second.forEach(({interval: timestamp, group, ...rest}) => {
  243. const existingInterval = target.find(
  244. interval =>
  245. interval.interval === timestamp && (group ? interval.group === group : true)
  246. );
  247. if (existingInterval) {
  248. Object.keys(rest).forEach(key => {
  249. existingInterval[key] = rest[key];
  250. });
  251. return;
  252. }
  253. target.push({interval: timestamp, group, ...rest});
  254. });
  255. return target;
  256. }