useSpansQuery.tsx 7.6 KB

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