parseMetricWidgetsQueryParam.tsx 8.5 KB

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