parseMetricWidgetsQueryParam.tsx 7.9 KB

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