diff --git a/static/app/views/explore/metrics/metricQuery.tsx b/static/app/views/explore/metrics/metricQuery.tsx index 959fa9bdf0e8ee..f6611a6522898e 100644 --- a/static/app/views/explore/metrics/metricQuery.tsx +++ b/static/app/views/explore/metrics/metricQuery.tsx @@ -1,8 +1,15 @@ import type {Sort} from 'sentry/utils/discover/fields'; import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; import type {AggregateField} from 'sentry/views/explore/queryParams/aggregateField'; +import {isGroupBy, type GroupBy} from 'sentry/views/explore/queryParams/groupBy'; import {ReadableQueryParams} from 'sentry/views/explore/queryParams/readableQueryParams'; -import {VisualizeFunction} from 'sentry/views/explore/queryParams/visualize'; +import { + isBaseVisualize, + isVisualize, + Visualize, + VisualizeFunction, + type BaseVisualize, +} from 'sentry/views/explore/queryParams/visualize'; import {ChartType} from 'sentry/views/insights/common/components/chart'; export interface TraceMetric { @@ -37,6 +44,23 @@ export function decodeMetricsQueryParams(value: string): BaseMetricQuery | null return null; } + const rawAggregateFields = json.aggregateFields; + if (!Array.isArray(rawAggregateFields)) { + return null; + } + + const visualizes = rawAggregateFields + .filter(isBaseVisualize) + .flatMap(vis => Visualize.fromJSON(vis)); + + if (!visualizes.length) { + return null; + } + + const groupBys = rawAggregateFields.filter(isGroupBy); + + const aggregateFields = [...visualizes, ...groupBys]; + return { metric: {name: metric}, queryParams: new ReadableQueryParams({ @@ -49,7 +73,7 @@ export function decodeMetricsQueryParams(value: string): BaseMetricQuery | null sortBys: defaultSortBys(), aggregateCursor: '', - aggregateFields: defaultAggregateFields(), + aggregateFields, aggregateSortBys: defaultAggregateSortBys(), }), }; @@ -59,6 +83,14 @@ export function encodeMetricQueryParams(metricQuery: BaseMetricQuery): string { return JSON.stringify({ metric: metricQuery.metric.name, query: metricQuery.queryParams.query, + aggregateFields: metricQuery.queryParams.aggregateFields.map(field => { + if (isVisualize(field)) { + return field.serialize(); + } + + // Keep Group By as-is + return field; + }), }); } diff --git a/static/app/views/explore/metrics/metricRow/aggregateDropdown.tsx b/static/app/views/explore/metrics/metricRow/aggregateDropdown.tsx new file mode 100644 index 00000000000000..d802cdc7b61724 --- /dev/null +++ b/static/app/views/explore/metrics/metricRow/aggregateDropdown.tsx @@ -0,0 +1,96 @@ +import {CompactSelect} from 'sentry/components/core/compactSelect'; +import {defined} from 'sentry/utils'; +import { + useMetricVisualize, + useSetMetricVisualize, +} from 'sentry/views/explore/metrics/metricsQueryParams'; + +const OPTIONS_BY_TYPE: Record> = { + counter: [ + { + label: 'sum', + value: 'sum', + }, + ], + distribution: [ + { + label: 'p50', + value: 'p50', + }, + { + label: 'p75', + value: 'p75', + }, + { + label: 'p90', + value: 'p90', + }, + { + label: 'p95', + value: 'p95', + }, + { + label: 'p99', + value: 'p99', + }, + { + label: 'avg', + value: 'avg', + }, + { + label: 'min', + value: 'min', + }, + { + label: 'max', + value: 'max', + }, + { + label: 'sum', + value: 'sum', + }, + { + label: 'count', + value: 'count', + }, + ], + gauge: [ + { + label: 'min', + value: 'min', + }, + { + label: 'max', + value: 'max', + }, + { + label: 'avg', + value: 'avg', + }, + { + label: 'last', + value: 'last', + }, + ], +}; + +export function AggregateDropdown({type}: {type: string | undefined}) { + const visualize = useMetricVisualize(); + const setVisualize = useSetMetricVisualize(); + + return ( + { + setVisualize( + visualize.replace({ + yAxis: `${option.value}(${visualize.parsedFunction?.arguments?.[0] ?? ''})`, + }) + ); + }} + /> + ); +} diff --git a/static/app/views/explore/metrics/metricRow.tsx b/static/app/views/explore/metrics/metricRow/index.tsx similarity index 82% rename from static/app/views/explore/metrics/metricRow.tsx rename to static/app/views/explore/metrics/metricRow/index.tsx index d29978e298c788..50b0be8a7ccc49 100644 --- a/static/app/views/explore/metrics/metricRow.tsx +++ b/static/app/views/explore/metrics/metricRow/index.tsx @@ -11,6 +11,7 @@ import { } from 'sentry/views/explore/components/traceItemSearchQueryBuilder'; import {useMetricOptions} from 'sentry/views/explore/hooks/useMetricOptions'; import {type TraceMetric} from 'sentry/views/explore/metrics/metricQuery'; +import {AggregateDropdown} from 'sentry/views/explore/metrics/metricRow/aggregateDropdown'; import { useMetricVisualize, useSetMetricName, @@ -20,7 +21,6 @@ import { useQueryParamsQuery, useSetQueryParamsQuery, } from 'sentry/views/explore/queryParams/context'; -import type {VisualizeFunction} from 'sentry/views/explore/queryParams/visualize'; import {TraceItemDataset} from 'sentry/views/explore/types'; interface MetricRowProps { @@ -67,7 +67,7 @@ function MetricToolbar({ tracesItemSearchQueryBuilderProps, traceMetric, }: MetricToolbarProps) { - const visualize = useMetricVisualize() as VisualizeFunction; + const visualize = useMetricVisualize(); const groupBys = useQueryParamsGroupBys(); const query = useQueryParamsQuery(); const {data: metricOptionsData} = useMetricOptions(); @@ -78,20 +78,29 @@ function MetricToolbar({ ...(metricOptionsData?.data?.map(option => ({ label: `${option['metric.name']} (${option['metric.type']})`, value: option['metric.name'], + type: option['metric.type'], })) ?? []), // TODO(nar): Remove these when we actually have metrics served // This is only used for providing an option to test current selection behavior { - label: 'test-metric', - value: 'test-metric', + label: 'test-distribution', + value: 'test-distribution', + type: 'distribution' as const, }, { - label: 'mock-metric', - value: 'mock-metric', + label: 'test-gauge', + value: 'test-gauge', + type: 'gauge' as const, }, ]; }, [metricOptionsData]); + // TODO(nar): This should come from the metric data context + // so we can display different types with conflicting names + const currentMetricType = useMemo(() => { + return metricOptions?.find(metricData => metricData.value === traceMetric.name)?.type; + }, [metricOptions, traceMetric.name]); + return (
{traceMetric.name}/{visualize.yAxis}/ by {groupBys.join(',')}/ where {query} @@ -104,15 +113,7 @@ function MetricToolbar({ setMetricName(option.value); }} /> - + {t('by')} {t('where')} diff --git a/static/app/views/explore/metrics/metricsQueryParams.tsx b/static/app/views/explore/metrics/metricsQueryParams.tsx index 3a439d9f396d70..842f4ef5f286f7 100644 --- a/static/app/views/explore/metrics/metricsQueryParams.tsx +++ b/static/app/views/explore/metrics/metricsQueryParams.tsx @@ -8,10 +8,15 @@ import type {AggregateField} from 'sentry/views/explore/queryParams/aggregateFie import { QueryParamsContextProvider, useQueryParamsVisualizes, + useSetQueryParamsVisualizes, } from 'sentry/views/explore/queryParams/context'; import {isGroupBy} from 'sentry/views/explore/queryParams/groupBy'; import {ReadableQueryParams} from 'sentry/views/explore/queryParams/readableQueryParams'; -import {parseVisualize} from 'sentry/views/explore/queryParams/visualize'; +import { + isVisualizeFunction, + parseVisualize, + VisualizeFunction, +} from 'sentry/views/explore/queryParams/visualize'; import type {WritableQueryParams} from 'sentry/views/explore/queryParams/writableQueryParams'; interface MetricNameContextValue { @@ -40,6 +45,7 @@ export function MetricsQueryParamsProvider({ (writableQueryParams: WritableQueryParams) => { const newQueryParams = updateQueryParams(queryParams, { query: getUpdatedValue(writableQueryParams.query, defaultQuery), + aggregateFields: writableQueryParams.aggregateFields, }); setQueryParams(newQueryParams); @@ -83,10 +89,10 @@ function getUpdatedValue( return undefined; } -export function useMetricVisualize() { +export function useMetricVisualize(): VisualizeFunction { const visualizes = useQueryParamsVisualizes(); - if (visualizes.length === 1) { - return visualizes[0]!; + if (visualizes.length === 1 && isVisualizeFunction(visualizes[0]!)) { + return visualizes[0]; } throw new Error('Only 1 visualize per metric allowed'); } @@ -96,6 +102,17 @@ export function useSetMetricName() { return setMetricName; } +export function useSetMetricVisualize() { + const setVisualizes = useSetQueryParamsVisualizes(); + const setVisualize = useCallback( + (newVisualize: VisualizeFunction) => { + setVisualizes([newVisualize.serialize()]); + }, + [setVisualizes] + ); + return setVisualize; +} + function updateQueryParams( readableQueryParams: ReadableQueryParams, writableQueryParams: WritableQueryParams diff --git a/static/app/views/explore/metrics/multiMetricsQueryParams.tsx b/static/app/views/explore/metrics/multiMetricsQueryParams.tsx index 6537b700307f8d..06270d51589fc1 100644 --- a/static/app/views/explore/metrics/multiMetricsQueryParams.tsx +++ b/static/app/views/explore/metrics/multiMetricsQueryParams.tsx @@ -1,5 +1,4 @@ -import type {ReactNode} from 'react'; -import {useMemo} from 'react'; +import {useMemo, type ReactNode} from 'react'; import type {Location} from 'history'; import {defined} from 'sentry/utils'; diff --git a/static/app/views/explore/queryParams/visualize.ts b/static/app/views/explore/queryParams/visualize.ts index 7967f535a270d5..6baf63430c1e24 100644 --- a/static/app/views/explore/queryParams/visualize.ts +++ b/static/app/views/explore/queryParams/visualize.ts @@ -85,7 +85,7 @@ export class VisualizeFunction extends Visualize { this.parsedFunction = parseFunction(yAxis); } - clone(): Visualize { + clone(): VisualizeFunction { return new VisualizeFunction(this.yAxis, { chartType: this.selectedChartType, visible: this.visible, @@ -100,7 +100,7 @@ export class VisualizeFunction extends Visualize { chartType?: ChartType; visible?: boolean; yAxis?: string; - }): Visualize { + }): VisualizeFunction { return new VisualizeFunction(yAxis ?? this.yAxis, { chartType: chartType ?? this.selectedChartType, visible: visible ?? this.visible,