useSpansQuery.tsx 7.0 KB

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