utils.tsx 9.2 KB

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