useMetricsCorrelations.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import {useEffect, useMemo, useState} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import moment from 'moment';
  4. import type {MRI} from 'sentry/types';
  5. import {parsePeriodToHours} from 'sentry/utils/dates';
  6. import {getDateTimeParams} from 'sentry/utils/metrics';
  7. import type {
  8. MetricCorrelation,
  9. MetricMetaCodeLocation,
  10. SelectionRange,
  11. } from 'sentry/utils/metrics/types';
  12. import type {UseApiQueryOptions} from 'sentry/utils/queryClient';
  13. import {useApiQuery} from 'sentry/utils/queryClient';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import usePageFilters from 'sentry/utils/usePageFilters';
  16. type ApiResponse = {
  17. metrics: MetricMetaCodeLocation[];
  18. };
  19. type MetricCorrelationOpts = SelectionRange & {
  20. codeLocations?: boolean;
  21. metricSpans?: boolean;
  22. query?: string;
  23. };
  24. function useDateTimeParams(options: MetricCorrelationOpts) {
  25. const {selection} = usePageFilters();
  26. const {start, end} = options;
  27. return start || end
  28. ? {start, end, statsPeriod: undefined}
  29. : getDateTimeParams(selection.datetime);
  30. }
  31. function useMetricsCorrelations(
  32. mri: MRI | undefined,
  33. options: MetricCorrelationOpts,
  34. queryOptions: Partial<UseApiQueryOptions<ApiResponse>> = {}
  35. ) {
  36. const organization = useOrganization();
  37. const {selection} = usePageFilters();
  38. const dateTimeParams = useDateTimeParams(options);
  39. const minMaxParams =
  40. // remove non-numeric values
  41. options.min && options.max && !isNaN(options.min) && !isNaN(options.max)
  42. ? {min: options.min, max: options.max}
  43. : {};
  44. const queryInfo = useApiQuery<ApiResponse>(
  45. [
  46. `/organizations/${organization.slug}/ddm/meta/`,
  47. {
  48. query: {
  49. metric: mri,
  50. project: selection.projects,
  51. environment: selection.environments,
  52. codeLocations: options.codeLocations,
  53. metricSpans: options.metricSpans,
  54. query: options.query,
  55. ...dateTimeParams,
  56. ...minMaxParams,
  57. },
  58. },
  59. ],
  60. {
  61. enabled: !!mri,
  62. staleTime: Infinity,
  63. ...queryOptions,
  64. }
  65. );
  66. if (!queryInfo.data) {
  67. return queryInfo;
  68. }
  69. const data = sortCodeLocations(
  70. deduplicateCodeLocations(mapToNewResponseShape(queryInfo.data, mri))
  71. );
  72. return {...queryInfo, data};
  73. }
  74. export function useMetricSamples(
  75. mri: MRI | undefined,
  76. options: Omit<MetricCorrelationOpts, 'metricSpans'> = {}
  77. ) {
  78. const [isUsingFallback, setIsUseFallback] = useState(false);
  79. const dateTimeParams = useDateTimeParams(options);
  80. const mainQuery = useMetricsCorrelations(mri, {
  81. ...options,
  82. ...dateTimeParams,
  83. metricSpans: true,
  84. });
  85. const hasTimeParams =
  86. (!!dateTimeParams.end && !!dateTimeParams.start) || !!dateTimeParams.statsPeriod;
  87. const periodInHours =
  88. dateTimeParams.statsPeriod !== undefined
  89. ? parsePeriodToHours(dateTimeParams.statsPeriod)
  90. : undefined;
  91. const {startDate, endDate} = useMemo(() => {
  92. const end = periodInHours ? moment() : moment(dateTimeParams.end);
  93. end.set('milliseconds', 0); // trim milliseconds to de-duplicate requests
  94. return {
  95. startDate: end.clone().subtract(1, 'hour').set('milliseconds', 0).toISOString(),
  96. endDate: end.toISOString(),
  97. };
  98. }, [dateTimeParams.end, periodInHours]);
  99. const isFallbackEnabled =
  100. !!mri &&
  101. hasTimeParams &&
  102. (periodInHours ??
  103. moment(dateTimeParams.start).diff(moment(dateTimeParams.end), 'hours')) > 1;
  104. const fallbackQuery = useMetricsCorrelations(
  105. mri,
  106. {
  107. ...options,
  108. start: startDate,
  109. end: endDate,
  110. metricSpans: true,
  111. },
  112. {
  113. enabled: isFallbackEnabled,
  114. }
  115. );
  116. useEffect(() => {
  117. if (mainQuery.isLoading && isFallbackEnabled) {
  118. const timeout = setTimeout(() => {
  119. setIsUseFallback(true);
  120. Sentry.metrics.increment('ddm.correlated_samples.timeout');
  121. }, 15000);
  122. return () => clearTimeout(timeout);
  123. }
  124. if (!mainQuery.isError) {
  125. setIsUseFallback(false);
  126. }
  127. return () => {};
  128. }, [mainQuery.isLoading, isFallbackEnabled, mainQuery.isError]);
  129. const queryInfo = isUsingFallback ? fallbackQuery : mainQuery;
  130. if (!queryInfo.data) {
  131. return queryInfo;
  132. }
  133. const data = queryInfo.data.metrics
  134. .flatMap(m => m.metricSpans)
  135. .filter(correlation => !!correlation)
  136. .slice(0, 10) as MetricCorrelation[];
  137. return {...queryInfo, data};
  138. }
  139. export function useMetricCodeLocations(
  140. mri: MRI | undefined,
  141. options: Omit<MetricCorrelationOpts, 'codeLocations'> = {}
  142. ) {
  143. return useMetricsCorrelations(mri, {...options, codeLocations: true});
  144. }
  145. const mapToNewResponseShape = (
  146. data: ApiResponse & {metricSpans?: MetricCorrelation[]},
  147. mri: MRI | undefined
  148. ) => {
  149. // If the response is already in the new shape, do nothing
  150. if (data.metrics) {
  151. return data;
  152. }
  153. const newData = {...data};
  154. // @ts-expect-error codeLocations is defined in the old response shape
  155. newData.metrics = (data.codeLocations ?? [])?.map(codeLocation => {
  156. return {
  157. mri: codeLocation.mri,
  158. timestamp: codeLocation.timestamp,
  159. metricSpans: data.metricSpans,
  160. codeLocations: (codeLocation.frames ?? []).map(frame => {
  161. return {
  162. function: frame.function,
  163. module: frame.module,
  164. filename: frame.filename,
  165. absPath: frame.absPath,
  166. lineNo: frame.lineNo,
  167. preContext: frame.preContext,
  168. contextLine: frame.contextLine,
  169. postContext: frame.postContext,
  170. };
  171. }),
  172. };
  173. });
  174. if (!newData.metrics.length && data.metricSpans?.length && mri) {
  175. newData.metrics = data.metricSpans.map(metricSpan => {
  176. return {
  177. mri,
  178. // TODO(ddm): Api has inconsistent timestamp formats between codeLocations and metricSpans
  179. timestamp: new Date(metricSpan.timestamp).getTime(),
  180. metricSpans: data.metricSpans,
  181. codeLocations: [],
  182. };
  183. });
  184. }
  185. return newData;
  186. };
  187. const sortCodeLocations = (data: ApiResponse) => {
  188. const newData = {...data};
  189. newData.metrics = [...data.metrics].sort((a, b) => {
  190. return b.timestamp - a.timestamp;
  191. });
  192. return newData;
  193. };
  194. const deduplicateCodeLocations = (data: ApiResponse) => {
  195. const newData = {...data};
  196. newData.metrics = data.metrics.filter((element, index) => {
  197. return !data.metrics.slice(0, index).some(e => equalCodeLocations(e, element));
  198. });
  199. return newData;
  200. };
  201. const equalCodeLocations = (a: MetricMetaCodeLocation, b: MetricMetaCodeLocation) => {
  202. if (a.mri !== b.mri) {
  203. return false;
  204. }
  205. const aCodeLocation = JSON.stringify(a.codeLocations?.[0] ?? {});
  206. const bCodeLocation = JSON.stringify(b.codeLocations?.[0] ?? {});
  207. return aCodeLocation === bCodeLocation;
  208. };