utils.tsx 8.5 KB

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