123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- import type {MRI} from 'sentry/types/metrics';
- import {getDefaultAggregate} from 'sentry/utils/metrics';
- import {
- DEFAULT_SORT_STATE,
- emptyMetricsQueryWidget,
- NO_QUERY_ID,
- } from 'sentry/utils/metrics/constants';
- import {isMRI} from 'sentry/utils/metrics/mri';
- import {
- type BaseWidgetParams,
- type FocusedMetricsSeries,
- MetricChartOverlayType,
- MetricDisplayType,
- MetricExpressionType,
- type MetricsEquationWidget,
- type MetricsQueryWidget,
- type MetricsWidget,
- type SortState,
- } from 'sentry/utils/metrics/types';
- import {getUniqueQueryIdGenerator} from 'sentry/views/metrics/utils/uniqueQueryId';
- function isRecord(value: unknown): value is Record<string, unknown> {
- return typeof value === 'object' && value !== null && !Array.isArray(value);
- }
- function isMetricDisplayType(value: unknown): value is MetricDisplayType {
- return Object.values(MetricDisplayType).includes(value as MetricDisplayType);
- }
- function getMRIParam(widget: Record<string, unknown>) {
- return 'mri' in widget && isMRI(widget.mri) ? widget.mri : undefined;
- }
- function parseStringParam(
- widget: Record<string, unknown>,
- key: string
- ): string | undefined {
- const value = widget[key];
- return typeof value === 'string' ? value : undefined;
- }
- function parseNumberParam(
- widget: Record<string, unknown>,
- key: string
- ): number | undefined {
- const value = widget[key];
- return typeof value === 'number' && !Number.isNaN(value) ? value : undefined;
- }
- function parseBooleanParam(
- widget: Record<string, unknown>,
- key: string
- ): boolean | undefined {
- const value = widget[key];
- return typeof value === 'boolean' ? value : undefined;
- }
- function parseArrayParam<T extends Exclude<any, undefined>>(
- widget: object,
- key: string,
- entryParser: (entry: unknown) => T | undefined
- ): T[] {
- if (!(key in widget)) {
- return [];
- }
- // allow single values instead of arrays
- if (!Array.isArray(widget[key])) {
- const entry = entryParser(widget[key]);
- return entry === undefined ? [] : [entry];
- }
- return widget[key].map(entryParser).filter((entry): entry is T => entry !== undefined);
- }
- function parseFocusedSeries(series: any): FocusedMetricsSeries | undefined {
- if (!isRecord(series)) {
- return undefined;
- }
- const id = parseStringParam(series, 'id');
- const groupBy =
- 'groupBy' in series && isRecord(series.groupBy)
- ? (series.groupBy as Record<string, string>)
- : undefined;
- if (!id) {
- return undefined;
- }
- return {id, groupBy};
- }
- function parseSortParam(widget: Record<string, unknown>, key: string): SortState {
- const sort = widget[key];
- if (!isRecord(sort)) {
- return DEFAULT_SORT_STATE;
- }
- const name = parseStringParam(sort, 'name');
- const order =
- 'order' in sort && (sort.order === 'desc' || sort.order === 'asc')
- ? sort.order
- : DEFAULT_SORT_STATE.order;
- if (
- name === 'name' ||
- name === 'avg' ||
- name === 'min' ||
- name === 'max' ||
- name === 'sum' ||
- name === 'total'
- ) {
- return {name, order};
- }
- return {name: undefined, order};
- }
- function isValidId(n: number | undefined): n is number {
- return n !== undefined && Number.isInteger(n) && n >= 0;
- }
- function parseQueryType(
- widget: Record<string, unknown>,
- key: string
- ): MetricExpressionType | undefined {
- const value = widget[key];
- return typeof value === 'number' && Object.values(MetricExpressionType).includes(value)
- ? value
- : undefined;
- }
- function parseQueryWidget(
- widget: Record<string, unknown>,
- baseWidgetParams: BaseWidgetParams
- ): MetricsQueryWidget | null {
- const mri = getMRIParam(widget);
- // If we cannot retrieve an MRI, there is nothing to display
- if (!mri) {
- return null;
- }
- return {
- mri,
- op: parseStringParam(widget, 'op') ?? getDefaultAggregate(mri),
- query: parseStringParam(widget, 'query') ?? '',
- groupBy: parseArrayParam(widget, 'groupBy', entry =>
- typeof entry === 'string' ? entry : undefined
- ),
- powerUserMode: parseBooleanParam(widget, 'powerUserMode') ?? false,
- ...baseWidgetParams,
- type: MetricExpressionType.QUERY,
- };
- }
- function parseFormulaWidget(
- widget: Record<string, unknown>,
- baseWidgetParams: BaseWidgetParams
- ): MetricsEquationWidget | null {
- const formula = parseStringParam(widget, 'formula');
- // If we cannot retrieve a formula, there is nothing to display
- if (formula === undefined) {
- return null;
- }
- return {
- formula,
- ...baseWidgetParams,
- type: MetricExpressionType.EQUATION,
- };
- }
- function parseQueryId(widget: Record<string, unknown>, key: string): number {
- const value = parseNumberParam(widget, key);
- return isValidId(value) ? value : NO_QUERY_ID;
- }
- function fillIds(
- entries: MetricsWidget[],
- indicesWithoutId: Set<number>,
- usedIds: Set<number>
- ): MetricsWidget[] {
- if (indicesWithoutId.size > 0) {
- const generateId = getUniqueQueryIdGenerator(usedIds);
- for (const index of indicesWithoutId) {
- const widget = entries[index];
- if (!widget) {
- continue;
- }
- widget.id = generateId.next().value;
- }
- }
- return entries;
- }
- export function parseMetricWidgetsQueryParam(
- queryParam?: string,
- defaultMRI?: MRI
- ): MetricsWidget[] {
- let currentWidgets: unknown = undefined;
- try {
- currentWidgets = JSON.parse(queryParam || '');
- } catch (_) {
- currentWidgets = [];
- }
- // It has to be an array and non-empty
- if (!Array.isArray(currentWidgets)) {
- currentWidgets = [];
- }
- const queries: MetricsQueryWidget[] = [];
- const usedQueryIds = new Set<number>();
- const queryIndicesWithoutId = new Set<number>();
- const formulas: MetricsEquationWidget[] = [];
- const usedFormulaIds = new Set<number>();
- const formulaIndicesWithoutId = new Set<number>();
- (currentWidgets as unknown[]).forEach((widget: unknown) => {
- if (!isRecord(widget)) {
- return;
- }
- const type = parseQueryType(widget, 'type') ?? MetricExpressionType.QUERY;
- const id = parseQueryId(widget, 'id');
- if (
- type === MetricExpressionType.QUERY ? usedQueryIds.has(id) : usedFormulaIds.has(id)
- ) {
- // We drop widgets with duplicate ids
- return;
- }
- if (id !== NO_QUERY_ID) {
- if (type === MetricExpressionType.QUERY) {
- usedQueryIds.add(id);
- } else {
- usedFormulaIds.add(id);
- }
- }
- const displayType = parseStringParam(widget, 'displayType');
- const baseWidgetParams: BaseWidgetParams = {
- type,
- id: !isValidId(id) ? NO_QUERY_ID : id,
- displayType: isMetricDisplayType(displayType)
- ? displayType
- : MetricDisplayType.LINE,
- focusedSeries: parseArrayParam(widget, 'focusedSeries', parseFocusedSeries),
- sort: parseSortParam(widget, 'sort'),
- isHidden: parseBooleanParam(widget, 'isHidden') ?? false,
- overlays: widget.overlays
- ? parseArrayParam(widget, 'overlays', entry => entry as MetricChartOverlayType)
- : [MetricChartOverlayType.SAMPLES],
- };
- switch (type) {
- case MetricExpressionType.QUERY: {
- const query = parseQueryWidget(widget, baseWidgetParams);
- if (!query) {
- break;
- }
- queries.push(query);
- if (query.id === NO_QUERY_ID) {
- queryIndicesWithoutId.add(queries.length - 1);
- }
- break;
- }
- case MetricExpressionType.EQUATION: {
- const formula = parseFormulaWidget(widget, baseWidgetParams);
- if (!formula) {
- break;
- }
- formulas.push(formula);
- if (formula.id === NO_QUERY_ID) {
- formulaIndicesWithoutId.add(formulas.length - 1);
- }
- break;
- }
- default:
- break;
- }
- });
- // Iterate over the widgets without an id and assign them a unique one
- if (queries.length === 0) {
- const mri = defaultMRI || emptyMetricsQueryWidget.mri;
- queries.push({
- ...emptyMetricsQueryWidget,
- mri,
- op: getDefaultAggregate(mri),
- });
- }
- // We can reset the id if there is only one widget
- if (queries.length === 1) {
- queries[0].id = 0;
- }
- if (formulas.length === 1) {
- formulas[0].id = 0;
- }
- return [
- ...fillIds(queries, queryIndicesWithoutId, usedQueryIds),
- ...fillIds(formulas, formulaIndicesWithoutId, usedFormulaIds),
- ];
- }
|