useSpansQuery.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import moment from 'moment';
  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/starfish/utils/retryHandlers';
  16. import {TrackResponse} from 'sentry/views/starfish/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. }: {
  61. eventView: EventView;
  62. cursor?: string;
  63. enabled?: boolean;
  64. initialData?: any;
  65. referrer?: string;
  66. }) {
  67. const location = useLocation();
  68. const organization = useOrganization();
  69. const {isReady: pageFiltersReady} = usePageFilters();
  70. const result = useGenericDiscoverQuery<
  71. {
  72. data: any[];
  73. meta: MetaType;
  74. },
  75. DiscoverQueryProps
  76. >({
  77. route: 'events-stats',
  78. eventView,
  79. location,
  80. orgSlug: organization.slug,
  81. getRequestPayload: () => ({
  82. ...eventView.getEventsAPIPayload(location),
  83. yAxis: eventView.yAxis,
  84. topEvents: eventView.topEvents,
  85. excludeOther: 0,
  86. partial: 1,
  87. orderby: eventView.sorts?.[0] ? encodeSort(eventView.sorts?.[0]) : undefined,
  88. interval: eventView.interval,
  89. cursor,
  90. }),
  91. options: {
  92. enabled: enabled && pageFiltersReady,
  93. refetchOnWindowFocus: false,
  94. retry: shouldRetryHandler,
  95. retryDelay: getRetryDelay,
  96. staleTime: Infinity,
  97. },
  98. referrer,
  99. });
  100. const isFetchingOrLoading = result.isLoading || result.isFetching;
  101. const defaultData = initialData ?? undefined;
  102. const data: T = isFetchingOrLoading
  103. ? defaultData
  104. : processDiscoverTimeseriesResult(result.data, eventView);
  105. const pageLinks = result.response?.getResponseHeader('Link') ?? undefined;
  106. return {
  107. ...result,
  108. pageLinks,
  109. data,
  110. meta: result.data?.meta,
  111. };
  112. }
  113. export function useWrappedDiscoverQuery<T>({
  114. eventView,
  115. initialData,
  116. enabled,
  117. referrer,
  118. limit,
  119. cursor,
  120. }: {
  121. eventView: EventView;
  122. cursor?: string;
  123. enabled?: boolean;
  124. initialData?: T;
  125. limit?: number;
  126. referrer?: string;
  127. }) {
  128. const location = useLocation();
  129. const organization = useOrganization();
  130. const {isReady: pageFiltersReady} = usePageFilters();
  131. const result = useDiscoverQuery({
  132. eventView,
  133. orgSlug: organization.slug,
  134. location,
  135. referrer,
  136. cursor,
  137. limit,
  138. options: {
  139. enabled: enabled && pageFiltersReady,
  140. refetchOnWindowFocus: false,
  141. retry: shouldRetryHandler,
  142. retryDelay: getRetryDelay,
  143. staleTime: Infinity,
  144. },
  145. });
  146. // TODO: useDiscoverQuery incorrectly states that it returns MetaType, but it
  147. // does not!
  148. const meta = result.data?.meta as EventsMetaType | undefined;
  149. const data =
  150. result.isLoading && initialData ? initialData : (result.data?.data as T | undefined);
  151. return {
  152. ...result,
  153. data,
  154. meta,
  155. };
  156. }
  157. type Interval = {interval: string; group?: string};
  158. function processDiscoverTimeseriesResult(
  159. result: TableData | undefined,
  160. eventView: EventView
  161. ) {
  162. if (!result) {
  163. return undefined;
  164. }
  165. if (!eventView.yAxis) {
  166. return [];
  167. }
  168. let intervals = [] as Interval[];
  169. const singleYAxis =
  170. eventView.yAxis &&
  171. (typeof eventView.yAxis === 'string' || eventView.yAxis.length === 1);
  172. const firstYAxis =
  173. typeof eventView.yAxis === 'string' ? eventView.yAxis : eventView.yAxis[0];
  174. if (result.data) {
  175. const timeSeriesResult: Interval[] = processSingleDiscoverTimeseriesResult(
  176. result,
  177. singleYAxis ? firstYAxis : 'count'
  178. ).map(data => ({
  179. interval: moment(parseInt(data.interval, 10) * 1000).format(DATE_FORMAT),
  180. [firstYAxis]: data[firstYAxis],
  181. group: data.group,
  182. }));
  183. return timeSeriesResult;
  184. }
  185. Object.keys(result).forEach(key => {
  186. if (result[key].data) {
  187. intervals = mergeIntervals(
  188. intervals,
  189. processSingleDiscoverTimeseriesResult(result[key], singleYAxis ? firstYAxis : key)
  190. );
  191. } else {
  192. Object.keys(result[key]).forEach(innerKey => {
  193. if (innerKey !== 'order') {
  194. intervals = mergeIntervals(
  195. intervals,
  196. processSingleDiscoverTimeseriesResult(result[key][innerKey], innerKey, key)
  197. );
  198. }
  199. });
  200. }
  201. });
  202. const processed = intervals.map(interval => ({
  203. ...interval,
  204. interval: moment(parseInt(interval.interval, 10) * 1000).format(DATE_FORMAT),
  205. }));
  206. return processed;
  207. }
  208. function processSingleDiscoverTimeseriesResult(result, key: string, group?: string) {
  209. const intervals = [] as Interval[];
  210. result.data.forEach(([timestamp, [{count: value}]]) => {
  211. const existingInterval = intervals.find(
  212. interval =>
  213. interval.interval === timestamp && (group ? interval.group === group : true)
  214. );
  215. if (existingInterval) {
  216. existingInterval[key] = value;
  217. return;
  218. }
  219. intervals.push({
  220. interval: timestamp,
  221. [key]: value,
  222. group,
  223. });
  224. });
  225. return intervals;
  226. }
  227. function mergeIntervals(first: Interval[], second: Interval[]) {
  228. const target: Interval[] = JSON.parse(JSON.stringify(first));
  229. second.forEach(({interval: timestamp, group, ...rest}) => {
  230. const existingInterval = target.find(
  231. interval =>
  232. interval.interval === timestamp && (group ? interval.group === group : true)
  233. );
  234. if (existingInterval) {
  235. Object.keys(rest).forEach(key => {
  236. existingInterval[key] = rest[key];
  237. });
  238. return;
  239. }
  240. target.push({interval: timestamp, group, ...rest});
  241. });
  242. return target;
  243. }