utils.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import {useMemo} from 'react';
  2. import type {MRI} from 'sentry/types';
  3. import {unescapeMetricsFormula} from 'sentry/utils/metrics';
  4. import {NO_QUERY_ID} from 'sentry/utils/metrics/constants';
  5. import {formatMRIField, MRIToField, parseField} from 'sentry/utils/metrics/mri';
  6. import {MetricDisplayType, MetricExpressionType} from 'sentry/utils/metrics/types';
  7. import type {MetricsQueryApiQueryParams} from 'sentry/utils/metrics/useMetricsQuery';
  8. import type {
  9. DashboardMetricsEquation,
  10. DashboardMetricsExpression,
  11. DashboardMetricsQuery,
  12. } from 'sentry/views/dashboards/metrics/types';
  13. import {
  14. type DashboardFilters,
  15. DisplayType,
  16. type Widget,
  17. type WidgetQuery,
  18. WidgetType,
  19. } from 'sentry/views/dashboards/types';
  20. import {getEquationSymbol} from 'sentry/views/metrics/equationSymbol copy';
  21. import {getQuerySymbol} from 'sentry/views/metrics/querySymbol';
  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 isMetricsFormula(
  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
  72. ): DashboardMetricsQuery[] {
  73. const usedIds = new Set<number>();
  74. const indizesWithoutId: number[] = [];
  75. const queries = widget.queries.map((query, index): DashboardMetricsQuery | null => {
  76. if (query.aggregates[0].startsWith('equation|')) {
  77. return null;
  78. }
  79. const id = getExpressionIdFromWidgetQuery(query);
  80. if (id === NO_QUERY_ID) {
  81. indizesWithoutId.push(index);
  82. } else {
  83. usedIds.add(id);
  84. }
  85. const parsed = parseField(query.aggregates[0]) || {mri: '' as MRI, op: ''};
  86. const orderBy = query.orderby ? query.orderby : undefined;
  87. return {
  88. id: id,
  89. type: MetricExpressionType.QUERY,
  90. mri: parsed.mri,
  91. op: parsed.op,
  92. query: extendQuery(query.conditions, dashboardFilters),
  93. groupBy: query.columns,
  94. orderBy: orderBy === 'asc' || orderBy === 'desc' ? orderBy : undefined,
  95. isHidden: !!query.isHidden,
  96. };
  97. });
  98. return fillMissingExpressionIds(queries, indizesWithoutId, usedIds).filter(
  99. (query): query is DashboardMetricsQuery => query !== null
  100. );
  101. }
  102. export function getMetricEquations(widget: Widget): DashboardMetricsEquation[] {
  103. const usedIds = new Set<number>();
  104. const indicesWithoutId: number[] = [];
  105. const equations = widget.queries.map(
  106. (query, index): DashboardMetricsEquation | null => {
  107. if (!query.aggregates[0].startsWith('equation|')) {
  108. return null;
  109. }
  110. const id = getExpressionIdFromWidgetQuery(query);
  111. if (id === NO_QUERY_ID) {
  112. indicesWithoutId.push(index);
  113. } else {
  114. usedIds.add(id);
  115. }
  116. return {
  117. id: id,
  118. type: MetricExpressionType.EQUATION,
  119. formula: query.aggregates[0].slice(9),
  120. isHidden: !!query.isHidden,
  121. } satisfies DashboardMetricsEquation;
  122. }
  123. );
  124. return fillMissingExpressionIds(equations, indicesWithoutId, usedIds).filter(
  125. (query): query is DashboardMetricsEquation => query !== null
  126. );
  127. }
  128. export function getMetricExpressions(
  129. widget: Widget,
  130. dashboardFilters?: DashboardFilters
  131. ): DashboardMetricsExpression[] {
  132. return [...getMetricQueries(widget, dashboardFilters), ...getMetricEquations(widget)];
  133. }
  134. export function useGenerateExpressionId(expressions: DashboardMetricsExpression[]) {
  135. return useMemo(() => {
  136. const usedIds = new Set<number>(expressions.map(e => e.id));
  137. return () => getUniqueQueryIdGenerator(usedIds).next().value;
  138. }, [expressions]);
  139. }
  140. export function expressionsToApiQueries(
  141. expressions: DashboardMetricsExpression[]
  142. ): MetricsQueryApiQueryParams[] {
  143. return expressions
  144. .filter(e => !(e.type === MetricExpressionType.EQUATION && e.isHidden))
  145. .map(e =>
  146. isMetricsFormula(e)
  147. ? {
  148. formula: e.formula,
  149. name: getEquationSymbol(e.id),
  150. }
  151. : {...e, name: getQuerySymbol(e.id), isQueryOnly: e.isHidden}
  152. );
  153. }
  154. export function toMetricDisplayType(displayType: unknown): MetricDisplayType {
  155. if (Object.values(MetricDisplayType).includes(displayType as MetricDisplayType)) {
  156. return displayType as MetricDisplayType;
  157. }
  158. return MetricDisplayType.LINE;
  159. }
  160. function getWidgetQuery(metricsQuery: DashboardMetricsQuery): WidgetQuery {
  161. const field = MRIToField(metricsQuery.mri, metricsQuery.op);
  162. return {
  163. name: `${metricsQuery.id}`,
  164. aggregates: [field],
  165. columns: metricsQuery.groupBy ?? [],
  166. fields: [field],
  167. conditions: metricsQuery.query ?? '',
  168. orderby: metricsQuery.orderBy ?? '',
  169. isHidden: metricsQuery.isHidden,
  170. };
  171. }
  172. function getWidgetEquation(metricsEquation: DashboardMetricsEquation): WidgetQuery {
  173. return {
  174. name: `${metricsEquation.id}`,
  175. aggregates: [`equation|${metricsEquation.formula}`],
  176. columns: [],
  177. fields: [`equation|${metricsEquation.formula}`],
  178. // Not used for equations
  179. conditions: '',
  180. orderby: '',
  181. isHidden: metricsEquation.isHidden,
  182. };
  183. }
  184. export function expressionsToWidget(
  185. expressions: DashboardMetricsExpression[],
  186. title: string,
  187. displayType: DisplayType
  188. ): Widget {
  189. return {
  190. title,
  191. // The interval has no effect on metrics widgets but the BE requires it
  192. interval: '5m',
  193. displayType: displayType,
  194. widgetType: WidgetType.METRICS,
  195. limit: 10,
  196. queries: expressions.map(e => {
  197. if (isMetricsFormula(e)) {
  198. return getWidgetEquation(e);
  199. }
  200. return getWidgetQuery(e);
  201. }),
  202. };
  203. }
  204. export function getMetricWidgetTitle(queries: DashboardMetricsExpression[]) {
  205. return queries
  206. .map(q =>
  207. isMetricsFormula(q)
  208. ? unescapeMetricsFormula(q.formula)
  209. : formatMRIField(MRIToField(q.mri, q.op))
  210. )
  211. .join(', ');
  212. }
  213. export function defaultMetricWidget(): Widget {
  214. return expressionsToWidget(
  215. [
  216. {
  217. id: 0,
  218. type: MetricExpressionType.QUERY,
  219. mri: 'd:transactions/duration@millisecond',
  220. op: 'avg',
  221. query: '',
  222. orderBy: 'desc',
  223. isHidden: false,
  224. },
  225. ],
  226. '',
  227. DisplayType.LINE
  228. );
  229. }
  230. export function filterQueriesByDisplayType(
  231. queries: DashboardMetricsQuery[],
  232. displayType: DisplayType
  233. ) {
  234. // Big number can display only one query
  235. if (displayType === DisplayType.BIG_NUMBER) {
  236. return queries.slice(0, 1);
  237. }
  238. return queries;
  239. }
  240. export function filterEquationsByDisplayType(
  241. equations: DashboardMetricsEquation[],
  242. displayType: DisplayType
  243. ) {
  244. // Big number can display only one query
  245. if (displayType === DisplayType.BIG_NUMBER) {
  246. return [];
  247. }
  248. return equations;
  249. }