utils.tsx 9.1 KB

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