utils.tsx 8.5 KB

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