useMetricsQuery.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import {useMemo} from 'react';
  2. import type {PageFilters} from 'sentry/types/core';
  3. import {getDateTimeParams, getMetricsInterval} from 'sentry/utils/metrics';
  4. import {getUseCaseFromMRI, MRIToField, parseMRI} from 'sentry/utils/metrics/mri';
  5. import {useVirtualMetricsContext} from 'sentry/utils/metrics/virtualMetricsContext';
  6. import {useApiQuery} from 'sentry/utils/queryClient';
  7. import useOrganization from 'sentry/utils/useOrganization';
  8. import type {
  9. MetricAggregation,
  10. MetricsDataIntervalLadder,
  11. MetricsQueryApiResponse,
  12. MRI,
  13. } from '../../types/metrics';
  14. import {parsePeriodToHours} from '../duration/parsePeriodToHours';
  15. export function createMqlQuery({
  16. field,
  17. query,
  18. groupBy = [],
  19. }: {
  20. field: string;
  21. groupBy?: string[];
  22. query?: string;
  23. }) {
  24. let mql = field;
  25. if (query) {
  26. mql = `${mql}{${query}}`;
  27. }
  28. if (groupBy.length) {
  29. mql = `${mql} by (${groupBy.join(',')})`;
  30. }
  31. return mql;
  32. }
  33. export interface MetricsQueryApiRequestQuery {
  34. aggregation: MetricAggregation;
  35. mri: MRI;
  36. name: string;
  37. alias?: string;
  38. // Conditions are used to identify virtual metrics
  39. condition?: number;
  40. groupBy?: string[];
  41. isQueryOnly?: boolean;
  42. limit?: number;
  43. orderBy?: 'asc' | 'desc';
  44. query?: string;
  45. }
  46. export interface MetricsQueryApiRequestFormula {
  47. formula: string;
  48. name: string;
  49. alias?: string;
  50. limit?: number;
  51. orderBy?: 'asc' | 'desc';
  52. }
  53. export type MetricsQueryApiQueryParams =
  54. | MetricsQueryApiRequestQuery
  55. | MetricsQueryApiRequestFormula;
  56. const getQueryInterval = (
  57. query: MetricsQueryApiRequestQuery,
  58. datetime: PageFilters['datetime'],
  59. intervalLadder?: MetricsDataIntervalLadder
  60. ) => {
  61. const useCase = getUseCaseFromMRI(query.mri) ?? 'custom';
  62. return getMetricsInterval(datetime, useCase, intervalLadder);
  63. };
  64. export function isMetricFormula(
  65. queryEntry: MetricsQueryApiQueryParams
  66. ): queryEntry is MetricsQueryApiRequestFormula {
  67. return 'formula' in queryEntry;
  68. }
  69. export function getMetricsQueryApiRequestPayload(
  70. queries: (MetricsQueryApiRequestQuery | MetricsQueryApiRequestFormula)[],
  71. {
  72. projects,
  73. environments,
  74. datetime,
  75. }: {
  76. datetime: PageFilters['datetime'];
  77. environments: PageFilters['environments'];
  78. projects: (number | string)[];
  79. },
  80. {
  81. intervalLadder,
  82. interval: intervalParam,
  83. includeSeries = true,
  84. }: {
  85. includeSeries?: boolean;
  86. interval?: string;
  87. intervalLadder?: MetricsDataIntervalLadder;
  88. } = {}
  89. ) {
  90. // We want to use the largest interval from all queries so none fails
  91. // In the future the endpoint should handle this
  92. const interval =
  93. intervalParam ??
  94. queries
  95. .map(query =>
  96. !isMetricFormula(query) ? getQueryInterval(query, datetime, intervalLadder) : '1m'
  97. )
  98. .reduce(
  99. (acc, curr) => (parsePeriodToHours(curr) > parsePeriodToHours(acc) ? curr : acc),
  100. '1m'
  101. );
  102. const requestQueries: {mql: string; name: string}[] = [];
  103. const requestFormulas: {
  104. mql: string;
  105. limit?: number;
  106. name?: string;
  107. order?: 'asc' | 'desc';
  108. }[] = [];
  109. queries.forEach((query, index) => {
  110. if (isMetricFormula(query)) {
  111. requestFormulas.push({
  112. mql: query.formula,
  113. limit: query.limit,
  114. order: query.orderBy,
  115. });
  116. return;
  117. }
  118. const {
  119. mri,
  120. aggregation,
  121. groupBy,
  122. limit,
  123. orderBy,
  124. query: queryParam,
  125. name: nameParam,
  126. isQueryOnly,
  127. } = query;
  128. const name = nameParam || `query_${index + 1}`;
  129. const hasGroupBy = groupBy && groupBy.length > 0;
  130. requestQueries.push({
  131. name,
  132. mql: createMqlQuery({
  133. field: MRIToField(mri, aggregation),
  134. query: queryParam,
  135. groupBy,
  136. }),
  137. });
  138. if (!isQueryOnly) {
  139. requestFormulas.push({
  140. mql: `$${name}`,
  141. limit,
  142. order: hasGroupBy ? orderBy : undefined,
  143. });
  144. }
  145. });
  146. return {
  147. query: {
  148. ...getDateTimeParams(datetime),
  149. project: projects,
  150. environment: environments,
  151. interval,
  152. includeSeries,
  153. },
  154. body: {
  155. queries: requestQueries,
  156. formulas: requestFormulas,
  157. },
  158. };
  159. }
  160. export function useMetricsQuery(
  161. queries: MetricsQueryApiQueryParams[],
  162. {
  163. projects,
  164. environments,
  165. datetime,
  166. }: {
  167. datetime: PageFilters['datetime'];
  168. environments: PageFilters['environments'];
  169. projects: (number | string)[];
  170. },
  171. overrides: {
  172. includeSeries?: boolean;
  173. interval?: string;
  174. intervalLadder?: MetricsDataIntervalLadder;
  175. } = {},
  176. enableRefetch = true
  177. ) {
  178. const organization = useOrganization();
  179. const {resolveVirtualMRI, isLoading} = useVirtualMetricsContext();
  180. const resolvedQueries = useMemo(
  181. () =>
  182. queries.map(query => {
  183. if (isMetricFormula(query)) {
  184. return query;
  185. }
  186. const {type} = parseMRI(query.mri);
  187. if (type !== 'v' || !query.condition) {
  188. return query;
  189. }
  190. const {mri, aggregation} = resolveVirtualMRI(
  191. query.mri,
  192. query.condition,
  193. query.aggregation
  194. );
  195. return {...query, mri, aggregation};
  196. }),
  197. [queries, resolveVirtualMRI]
  198. );
  199. const {query: queryToSend, body} = useMemo(
  200. () =>
  201. getMetricsQueryApiRequestPayload(
  202. resolvedQueries,
  203. {datetime, projects, environments},
  204. {...overrides}
  205. ),
  206. [resolvedQueries, datetime, projects, environments, overrides]
  207. );
  208. return useApiQuery<MetricsQueryApiResponse>(
  209. [
  210. `/organizations/${organization.slug}/metrics/query/`,
  211. {query: queryToSend, data: body, method: 'POST'},
  212. ],
  213. {
  214. retry: 0,
  215. staleTime: 0,
  216. refetchOnReconnect: enableRefetch,
  217. refetchOnWindowFocus: enableRefetch,
  218. refetchInterval: false,
  219. enabled: !isLoading,
  220. }
  221. );
  222. }