utils.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. import {useMemo} from 'react';
  2. import {getEquationSymbol} from 'sentry/components/metrics/equationSymbol';
  3. import {getQuerySymbol} from 'sentry/components/metrics/querySymbol';
  4. import type {MetricAggregation, MRI} from 'sentry/types/metrics';
  5. import {getDefaultAggregation, unescapeMetricsFormula} from 'sentry/utils/metrics';
  6. import {NO_QUERY_ID} from 'sentry/utils/metrics/constants';
  7. import {
  8. formatMRIField,
  9. isExtractedCustomMetric,
  10. MRIToField,
  11. parseField,
  12. } from 'sentry/utils/metrics/mri';
  13. import {MetricDisplayType, MetricExpressionType} from 'sentry/utils/metrics/types';
  14. import type {MetricsQueryApiQueryParams} from 'sentry/utils/metrics/useMetricsQuery';
  15. import {SPAN_DURATION_MRI} from 'sentry/utils/metrics/useMetricsTags';
  16. import type {
  17. DashboardMetricsEquation,
  18. DashboardMetricsExpression,
  19. DashboardMetricsQuery,
  20. } from 'sentry/views/dashboards/metrics/types';
  21. import {
  22. type DashboardFilters,
  23. DisplayType,
  24. type Widget,
  25. type WidgetQuery,
  26. WidgetType,
  27. } from 'sentry/views/dashboards/types';
  28. import {getUniqueQueryIdGenerator} from 'sentry/views/metrics/utils/uniqueQueryId';
  29. function extendQuery(query = '', dashboardFilters?: DashboardFilters) {
  30. if (!dashboardFilters?.release?.length) {
  31. return query;
  32. }
  33. const releaseQuery = getReleaseQuery(dashboardFilters);
  34. return `${query} ${releaseQuery}`.trim();
  35. }
  36. function getReleaseQuery(dashboardFilters: DashboardFilters) {
  37. const {release} = dashboardFilters;
  38. if (!release?.length) {
  39. return '';
  40. }
  41. if (release.length === 1) {
  42. return `release:${release[0]}`;
  43. }
  44. return `release:[${release.join(',')}]`;
  45. }
  46. export function isMetricsEquation(
  47. query: DashboardMetricsExpression
  48. ): query is DashboardMetricsEquation {
  49. return query.type === MetricExpressionType.EQUATION;
  50. }
  51. function getExpressionIdFromWidgetQuery(query: WidgetQuery): number {
  52. let id = query.name && Number(query.name);
  53. if (typeof id !== 'number' || Number.isNaN(id) || id < 0 || !Number.isInteger(id)) {
  54. id = NO_QUERY_ID;
  55. }
  56. return id;
  57. }
  58. function fillMissingExpressionIds(
  59. expressions: (DashboardMetricsExpression | null)[],
  60. indizesWithoutId: number[],
  61. usedIds: Set<number>
  62. ): (DashboardMetricsExpression | null)[] {
  63. if (indizesWithoutId.length > 0) {
  64. const generateId = getUniqueQueryIdGenerator(usedIds);
  65. for (const index of indizesWithoutId) {
  66. const expression = expressions[index];
  67. if (!expression) {
  68. continue;
  69. }
  70. expression.id = generateId.next().value;
  71. }
  72. }
  73. return expressions;
  74. }
  75. export function getMetricQueries(
  76. widget: Widget,
  77. dashboardFilters: DashboardFilters | undefined,
  78. getVirtualMRIQuery: (
  79. mri: MRI,
  80. aggregation: MetricAggregation
  81. ) => {
  82. aggregation: MetricAggregation;
  83. conditionId: number;
  84. mri: MRI;
  85. } | null
  86. ): DashboardMetricsQuery[] {
  87. const usedIds = new Set<number>();
  88. const indizesWithoutId: number[] = [];
  89. const queries = widget.queries.map((query, index): DashboardMetricsQuery | null => {
  90. if (query.aggregates[0].startsWith('equation|')) {
  91. return null;
  92. }
  93. const id = getExpressionIdFromWidgetQuery(query);
  94. if (id === NO_QUERY_ID) {
  95. indizesWithoutId.push(index);
  96. } else {
  97. usedIds.add(id);
  98. }
  99. const parsed = parseField(query.aggregates[0]);
  100. if (!parsed) {
  101. return null;
  102. }
  103. let mri = parsed.mri;
  104. let condition: number | undefined = undefined;
  105. let aggregation = parsed.aggregation;
  106. if (isExtractedCustomMetric({mri})) {
  107. const resolved = getVirtualMRIQuery(mri, aggregation);
  108. if (resolved) {
  109. aggregation = resolved.aggregation;
  110. mri = resolved.mri;
  111. condition = resolved.conditionId;
  112. }
  113. }
  114. const orderBy = query.orderby ? query.orderby : undefined;
  115. return {
  116. id: id,
  117. type: MetricExpressionType.QUERY,
  118. condition: condition,
  119. mri: mri,
  120. aggregation: aggregation,
  121. query: extendQuery(query.conditions, dashboardFilters),
  122. groupBy: query.columns,
  123. orderBy: orderBy === 'asc' || orderBy === 'desc' ? orderBy : undefined,
  124. isHidden: !!query.isHidden,
  125. alias: query.fieldAliases?.[0],
  126. };
  127. });
  128. return fillMissingExpressionIds(queries, indizesWithoutId, usedIds).filter(
  129. (query): query is DashboardMetricsQuery => query !== null
  130. );
  131. }
  132. export function getMetricEquations(widget: Widget): DashboardMetricsEquation[] {
  133. const usedIds = new Set<number>();
  134. const indicesWithoutId: number[] = [];
  135. const equations = widget.queries.map(
  136. (query, index): DashboardMetricsEquation | null => {
  137. if (!query.aggregates[0].startsWith('equation|')) {
  138. return null;
  139. }
  140. const id = getExpressionIdFromWidgetQuery(query);
  141. if (id === NO_QUERY_ID) {
  142. indicesWithoutId.push(index);
  143. } else {
  144. usedIds.add(id);
  145. }
  146. return {
  147. id: id,
  148. type: MetricExpressionType.EQUATION,
  149. formula: query.aggregates[0].slice(9),
  150. isHidden: !!query.isHidden,
  151. alias: query.fieldAliases?.[0],
  152. } satisfies DashboardMetricsEquation;
  153. }
  154. );
  155. return fillMissingExpressionIds(equations, indicesWithoutId, usedIds).filter(
  156. (query): query is DashboardMetricsEquation => query !== null
  157. );
  158. }
  159. export function getMetricExpressions(
  160. widget: Widget,
  161. dashboardFilters: DashboardFilters | undefined,
  162. getVirtualMRIQuery: (
  163. mri: MRI,
  164. aggregation: MetricAggregation
  165. ) => {
  166. aggregation: MetricAggregation;
  167. conditionId: number;
  168. mri: MRI;
  169. } | null
  170. ): DashboardMetricsExpression[] {
  171. return [
  172. ...getMetricQueries(widget, dashboardFilters, getVirtualMRIQuery),
  173. ...getMetricEquations(widget),
  174. ];
  175. }
  176. export function useGenerateExpressionId(expressions: DashboardMetricsExpression[]) {
  177. return useMemo(() => {
  178. const usedIds = new Set<number>(expressions.map(e => e.id));
  179. return () => getUniqueQueryIdGenerator(usedIds).next().value;
  180. }, [expressions]);
  181. }
  182. export function expressionsToApiQueries(
  183. expressions: DashboardMetricsExpression[],
  184. metricsNewInputs: boolean
  185. ): MetricsQueryApiQueryParams[] {
  186. return expressions
  187. .filter(e => !(e.type === MetricExpressionType.EQUATION && e.isHidden))
  188. .map(e =>
  189. isMetricsEquation(e)
  190. ? {
  191. alias: e.alias,
  192. formula: e.formula,
  193. name: getEquationSymbol(e.id, metricsNewInputs),
  194. }
  195. : {...e, name: getQuerySymbol(e.id, metricsNewInputs), isQueryOnly: e.isHidden}
  196. );
  197. }
  198. export function toMetricDisplayType(displayType: unknown): MetricDisplayType {
  199. if (Object.values(MetricDisplayType).includes(displayType as MetricDisplayType)) {
  200. return displayType as MetricDisplayType;
  201. }
  202. return MetricDisplayType.LINE;
  203. }
  204. function getWidgetQuery(metricsQuery: DashboardMetricsQuery): WidgetQuery {
  205. const field = MRIToField(metricsQuery.mri, metricsQuery.aggregation);
  206. return {
  207. name: `${metricsQuery.id}`,
  208. aggregates: [field],
  209. columns: metricsQuery.groupBy ?? [],
  210. fields: [field],
  211. conditions: metricsQuery.query ?? '',
  212. orderby: metricsQuery.orderBy ?? '',
  213. isHidden: metricsQuery.isHidden,
  214. fieldAliases: metricsQuery.alias ? [metricsQuery.alias] : [],
  215. };
  216. }
  217. function getWidgetEquation(metricsEquation: DashboardMetricsEquation): WidgetQuery {
  218. return {
  219. name: `${metricsEquation.id}`,
  220. aggregates: [`equation|${metricsEquation.formula}`],
  221. columns: [],
  222. fields: [`equation|${metricsEquation.formula}`],
  223. isHidden: metricsEquation.isHidden,
  224. fieldAliases: metricsEquation.alias ? [metricsEquation.alias] : [],
  225. // Not used for equations
  226. conditions: '',
  227. orderby: '',
  228. };
  229. }
  230. export function expressionsToWidget(
  231. expressions: DashboardMetricsExpression[],
  232. title: string,
  233. displayType: DisplayType,
  234. interval = '5m'
  235. ): Widget {
  236. return {
  237. title,
  238. interval,
  239. displayType: displayType,
  240. widgetType: WidgetType.METRICS,
  241. limit: 10,
  242. queries: expressions.map(e => {
  243. if (isMetricsEquation(e)) {
  244. return getWidgetEquation(e);
  245. }
  246. return getWidgetQuery(e);
  247. }),
  248. };
  249. }
  250. export function getMetricWidgetTitle(queries: DashboardMetricsExpression[]) {
  251. return queries.map(getMetricQueryName).join(', ');
  252. }
  253. export function getMetricQueryName(query: DashboardMetricsExpression): string {
  254. return (
  255. query.alias ??
  256. (isMetricsEquation(query)
  257. ? unescapeMetricsFormula(query.formula)
  258. : formatMRIField(MRIToField(query.mri, query.aggregation)))
  259. );
  260. }
  261. export function defaultMetricWidget(): Widget {
  262. return expressionsToWidget(
  263. [
  264. {
  265. id: 0,
  266. type: MetricExpressionType.QUERY,
  267. mri: SPAN_DURATION_MRI,
  268. aggregation: getDefaultAggregation(SPAN_DURATION_MRI),
  269. query: '',
  270. orderBy: 'desc',
  271. isHidden: false,
  272. },
  273. ],
  274. '',
  275. DisplayType.LINE
  276. );
  277. }