parseMetricWidgetsQueryParam.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import type {MetricAggregation} from 'sentry/types/metrics';
  2. import {getDefaultAggregation, isMetricsAggregation} from 'sentry/utils/metrics';
  3. import {DEFAULT_SORT_STATE, NO_QUERY_ID} from 'sentry/utils/metrics/constants';
  4. import {isMRI} from 'sentry/utils/metrics/mri';
  5. import {
  6. type BaseWidgetParams,
  7. type FocusedMetricsSeries,
  8. MetricChartOverlayType,
  9. MetricDisplayType,
  10. MetricExpressionType,
  11. type MetricsEquationWidget,
  12. type MetricsQueryWidget,
  13. type MetricsWidget,
  14. type SortState,
  15. } from 'sentry/utils/metrics/types';
  16. import {getUniqueQueryIdGenerator} from 'sentry/views/metrics/utils/uniqueQueryId';
  17. function isRecord(value: unknown): value is Record<string, unknown> {
  18. return typeof value === 'object' && value !== null && !Array.isArray(value);
  19. }
  20. function isMetricDisplayType(value: unknown): value is MetricDisplayType {
  21. return Object.values(MetricDisplayType).includes(value as MetricDisplayType);
  22. }
  23. function getMRIParam(widget: Record<string, unknown>) {
  24. return 'mri' in widget && isMRI(widget.mri) ? widget.mri : undefined;
  25. }
  26. function parseStringParam(
  27. widget: Record<string, unknown>,
  28. key: string
  29. ): string | undefined {
  30. const value = widget[key];
  31. return typeof value === 'string' ? value : undefined;
  32. }
  33. function parseNumberParam(
  34. widget: Record<string, unknown>,
  35. key: string
  36. ): number | undefined {
  37. const value = widget[key];
  38. return typeof value === 'number' && !Number.isNaN(value) ? value : undefined;
  39. }
  40. function parseBooleanParam(
  41. widget: Record<string, unknown>,
  42. key: string
  43. ): boolean | undefined {
  44. const value = widget[key];
  45. return typeof value === 'boolean' ? value : undefined;
  46. }
  47. function parseArrayParam<T extends Exclude<any, undefined>>(
  48. widget: object,
  49. key: string,
  50. entryParser: (entry: unknown) => T | undefined
  51. ): T[] {
  52. if (!(key in widget)) {
  53. return [];
  54. }
  55. // allow single values instead of arrays
  56. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  57. if (!Array.isArray(widget[key])) {
  58. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  59. const entry = entryParser(widget[key]);
  60. return entry === undefined ? [] : [entry];
  61. }
  62. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  63. return widget[key]
  64. .map(entryParser)
  65. .filter((entry: any): entry is T => entry !== undefined);
  66. }
  67. function parseFocusedSeries(series: any): FocusedMetricsSeries | undefined {
  68. if (!isRecord(series)) {
  69. return undefined;
  70. }
  71. const id = parseStringParam(series, 'id');
  72. const groupBy =
  73. 'groupBy' in series && isRecord(series.groupBy)
  74. ? (series.groupBy as Record<string, string>)
  75. : undefined;
  76. if (!id) {
  77. return undefined;
  78. }
  79. return {id, groupBy};
  80. }
  81. function parseSortParam(widget: Record<string, unknown>, key: string): SortState {
  82. const sort = widget[key];
  83. if (!isRecord(sort)) {
  84. return DEFAULT_SORT_STATE;
  85. }
  86. const name = parseStringParam(sort, 'name');
  87. const order =
  88. 'order' in sort && (sort.order === 'desc' || sort.order === 'asc')
  89. ? sort.order
  90. : DEFAULT_SORT_STATE.order;
  91. if (name && (name === 'name' || isMetricsAggregation(name))) {
  92. return {name, order};
  93. }
  94. return {name: undefined, order};
  95. }
  96. function isValidId(n: number | undefined): n is number {
  97. return n !== undefined && Number.isInteger(n) && n >= 0;
  98. }
  99. function parseQueryType(
  100. widget: Record<string, unknown>,
  101. key: string
  102. ): MetricExpressionType | undefined {
  103. const value = widget[key];
  104. return typeof value === 'number' && Object.values(MetricExpressionType).includes(value)
  105. ? value
  106. : undefined;
  107. }
  108. function parseAggregation(
  109. widget: Record<string, unknown>
  110. ): MetricAggregation | undefined {
  111. const aggregation =
  112. parseStringParam(widget, 'aggregation') ?? parseStringParam(widget, 'op');
  113. if (!aggregation) {
  114. return undefined;
  115. }
  116. return aggregation as MetricAggregation;
  117. }
  118. function parseQueryWidget(
  119. widget: Record<string, unknown>,
  120. baseWidgetParams: BaseWidgetParams
  121. ): MetricsQueryWidget | null {
  122. const mri = getMRIParam(widget);
  123. // If we cannot retrieve an MRI, there is nothing to display
  124. if (!mri) {
  125. return null;
  126. }
  127. return {
  128. mri,
  129. aggregation: parseAggregation(widget) ?? getDefaultAggregation(mri),
  130. condition: parseNumberParam(widget, 'condition'),
  131. query: parseStringParam(widget, 'query') ?? '',
  132. groupBy: parseArrayParam(widget, 'groupBy', entry =>
  133. typeof entry === 'string' ? entry : undefined
  134. ),
  135. powerUserMode: parseBooleanParam(widget, 'powerUserMode') ?? false,
  136. ...baseWidgetParams,
  137. type: MetricExpressionType.QUERY,
  138. };
  139. }
  140. function parseFormulaWidget(
  141. widget: Record<string, unknown>,
  142. baseWidgetParams: BaseWidgetParams
  143. ): MetricsEquationWidget | null {
  144. const formula = parseStringParam(widget, 'formula');
  145. // If we cannot retrieve a formula, there is nothing to display
  146. if (formula === undefined) {
  147. return null;
  148. }
  149. return {
  150. formula,
  151. ...baseWidgetParams,
  152. type: MetricExpressionType.EQUATION,
  153. };
  154. }
  155. function parseQueryId(widget: Record<string, unknown>, key: string): number {
  156. const value = parseNumberParam(widget, key);
  157. return isValidId(value) ? value : NO_QUERY_ID;
  158. }
  159. function fillIds(
  160. entries: MetricsWidget[],
  161. indicesWithoutId: Set<number>,
  162. usedIds: Set<number>
  163. ): MetricsWidget[] {
  164. if (indicesWithoutId.size > 0) {
  165. const generateId = getUniqueQueryIdGenerator(usedIds);
  166. for (const index of indicesWithoutId) {
  167. const widget = entries[index];
  168. if (!widget) {
  169. continue;
  170. }
  171. widget.id = generateId.next().value;
  172. }
  173. }
  174. return entries;
  175. }
  176. export function parseMetricWidgetsQueryParam(queryParam?: string): MetricsWidget[] {
  177. let currentWidgets: unknown = undefined;
  178. try {
  179. currentWidgets = JSON.parse(queryParam || '');
  180. } catch (_) {
  181. currentWidgets = [];
  182. }
  183. // It has to be an array and non-empty
  184. if (!Array.isArray(currentWidgets)) {
  185. currentWidgets = [];
  186. }
  187. const queries: MetricsQueryWidget[] = [];
  188. const usedQueryIds = new Set<number>();
  189. const queryIndicesWithoutId = new Set<number>();
  190. const formulas: MetricsEquationWidget[] = [];
  191. const usedFormulaIds = new Set<number>();
  192. const formulaIndicesWithoutId = new Set<number>();
  193. (currentWidgets as unknown[]).forEach((widget: unknown) => {
  194. if (!isRecord(widget)) {
  195. return;
  196. }
  197. const type = parseQueryType(widget, 'type') ?? MetricExpressionType.QUERY;
  198. const id = parseQueryId(widget, 'id');
  199. if (
  200. type === MetricExpressionType.QUERY ? usedQueryIds.has(id) : usedFormulaIds.has(id)
  201. ) {
  202. // We drop widgets with duplicate ids
  203. return;
  204. }
  205. if (id !== NO_QUERY_ID) {
  206. if (type === MetricExpressionType.QUERY) {
  207. usedQueryIds.add(id);
  208. } else {
  209. usedFormulaIds.add(id);
  210. }
  211. }
  212. const displayType = parseStringParam(widget, 'displayType');
  213. const baseWidgetParams: BaseWidgetParams = {
  214. type,
  215. id: !isValidId(id) ? NO_QUERY_ID : id,
  216. displayType: isMetricDisplayType(displayType)
  217. ? displayType
  218. : MetricDisplayType.LINE,
  219. focusedSeries: parseArrayParam(widget, 'focusedSeries', parseFocusedSeries),
  220. sort: parseSortParam(widget, 'sort'),
  221. isHidden: parseBooleanParam(widget, 'isHidden') ?? false,
  222. overlays: widget.overlays
  223. ? parseArrayParam(widget, 'overlays', entry => entry as MetricChartOverlayType)
  224. : [MetricChartOverlayType.SAMPLES],
  225. };
  226. switch (type) {
  227. case MetricExpressionType.QUERY: {
  228. const query = parseQueryWidget(widget, baseWidgetParams);
  229. if (!query) {
  230. break;
  231. }
  232. queries.push(query);
  233. if (query.id === NO_QUERY_ID) {
  234. queryIndicesWithoutId.add(queries.length - 1);
  235. }
  236. break;
  237. }
  238. case MetricExpressionType.EQUATION: {
  239. const formula = parseFormulaWidget(widget, baseWidgetParams);
  240. if (!formula) {
  241. break;
  242. }
  243. formulas.push(formula);
  244. if (formula.id === NO_QUERY_ID) {
  245. formulaIndicesWithoutId.add(formulas.length - 1);
  246. }
  247. break;
  248. }
  249. default:
  250. break;
  251. }
  252. });
  253. // We can reset the id if there is only one widget
  254. if (queries.length === 1) {
  255. queries[0]!.id = 0;
  256. }
  257. if (formulas.length === 1) {
  258. formulas[0]!.id = 0;
  259. }
  260. return [
  261. ...fillIds(queries, queryIndicesWithoutId, usedQueryIds),
  262. ...fillIds(formulas, formulaIndicesWithoutId, usedFormulaIds),
  263. ];
  264. }