utils.tsx 7.6 KB

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