parseMetricWidgetsQueryParam.tsx 7.7 KB

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