From e8b3263e4795e082847b7c4c157f9e5cb859a411 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 3 Oct 2025 17:15:55 -0400 Subject: [PATCH 1/2] feat(metrics): Stubs for query params in explore metrics Some basic stubs for the query params to be used in explore metrics. Currently using react state but can easily be swapped out later. --- static/app/utils/useResettableState.tsx | 20 +++++ .../traceItemSearchQueryBuilder.tsx | 9 ++ .../logs/logsStateQueryParamsProvider.tsx | 19 +--- static/app/views/explore/logs/styles.tsx | 2 +- .../app/views/explore/metrics/metricRow.tsx | 72 +++++++++++++++ .../explore/metrics/metricsQueryParams.tsx | 62 +++++++++++++ .../app/views/explore/metrics/metricsTab.tsx | 88 ++++++++++++------- .../app/views/explore/metrics/traceMetric.tsx | 3 + 8 files changed, 225 insertions(+), 50 deletions(-) create mode 100644 static/app/utils/useResettableState.tsx create mode 100644 static/app/views/explore/metrics/metricRow.tsx create mode 100644 static/app/views/explore/metrics/metricsQueryParams.tsx create mode 100644 static/app/views/explore/metrics/traceMetric.tsx diff --git a/static/app/utils/useResettableState.tsx b/static/app/utils/useResettableState.tsx new file mode 100644 index 00000000000000..5ae651bc1aea7a --- /dev/null +++ b/static/app/utils/useResettableState.tsx @@ -0,0 +1,20 @@ +import {useCallback, useRef, useState} from 'react'; + +import {defined} from 'sentry/utils'; + +export function useResettableState(defaultValue: () => T) { + const defaultValueBoxed = useRef(defaultValue); + defaultValueBoxed.current = defaultValue; + + const [state, _setState] = useState(defaultValueBoxed.current()); + + const setState = useCallback((newState: T | null | undefined) => { + if (defined(newState)) { + _setState(newState); + } else if (newState === null) { + _setState(defaultValueBoxed.current()); + } + }, []); + + return [state, setState] as const; +} diff --git a/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx b/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx index 13e908649176fc..14b5407b6aa3f1 100644 --- a/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx +++ b/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx @@ -212,6 +212,9 @@ function itemTypeToRecentSearches(itemType: TraceItemDataset) { if (itemType === TraceItemDataset.SPANS) { return SavedSearchType.SPAN; } + if (itemType === TraceItemDataset.TRACEMETRICS) { + return SavedSearchType.METRIC; + } return SavedSearchType.LOG; } @@ -219,6 +222,9 @@ function itemTypeToFilterKeySections(itemType: TraceItemDataset) { if (itemType === TraceItemDataset.SPANS) { return SPANS_FILTER_KEY_SECTIONS; } + if (itemType === TraceItemDataset.TRACEMETRICS) { + return []; + } return LOGS_FILTER_KEY_SECTIONS; } @@ -226,5 +232,8 @@ function itemTypeToDefaultPlaceholder(itemType: TraceItemDataset) { if (itemType === TraceItemDataset.SPANS) { return t('Search for spans, users, tags, and more'); } + if (itemType === TraceItemDataset.TRACEMETRICS) { + return t('Search for metrics, users, tags, and more'); + } return t('Search for logs, users, tags, and more'); } diff --git a/static/app/views/explore/logs/logsStateQueryParamsProvider.tsx b/static/app/views/explore/logs/logsStateQueryParamsProvider.tsx index 75dd6ec070d536..3cb2a27d7a7683 100644 --- a/static/app/views/explore/logs/logsStateQueryParamsProvider.tsx +++ b/static/app/views/explore/logs/logsStateQueryParamsProvider.tsx @@ -1,7 +1,7 @@ import type {ReactNode} from 'react'; import {useCallback, useMemo, useState} from 'react'; -import {defined} from 'sentry/utils'; +import {useResettableState} from 'sentry/utils/useResettableState'; import {defaultLogFields} from 'sentry/views/explore/contexts/logs/fields'; import {defaultSortBys} from 'sentry/views/explore/contexts/pageParamsContext/sortBys'; import { @@ -91,20 +91,3 @@ export function LogsStateQueryParamsProvider({ function defaultAggregateFields() { return [...defaultGroupBys(), ...defaultVisualizes()]; } - -function useResettableState(defaultValue: () => T) { - const [state, _setState] = useState(defaultValue()); - - const setState = useCallback( - (newState: T | null | undefined) => { - if (defined(newState)) { - _setState(newState); - } else if (newState === null) { - _setState(defaultValue()); - } - }, - [defaultValue] - ); - - return [state, setState] as const; -} diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index b8479366723703..09ed609d9bbdcc 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -390,7 +390,7 @@ export const TopSectionBody = styled(Body)` } `; -export const BottomSectionBody = styled('div')<{sidebarOpen: boolean}>` +export const BottomSectionBody = styled('div')<{sidebarOpen?: boolean}>` flex: 1; padding: ${space(1)} ${space(2)} ${space(3)} ${space(2)}; background-color: ${p => p.theme.backgroundSecondary}; diff --git a/static/app/views/explore/metrics/metricRow.tsx b/static/app/views/explore/metrics/metricRow.tsx new file mode 100644 index 00000000000000..683434ab7735c2 --- /dev/null +++ b/static/app/views/explore/metrics/metricRow.tsx @@ -0,0 +1,72 @@ +import {useMemo} from 'react'; + +import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/context'; +import { + TraceItemSearchQueryBuilder, + useSearchQueryBuilderProps, + type TraceItemSearchQueryBuilderProps, +} from 'sentry/views/explore/components/traceItemSearchQueryBuilder'; +import {useMetricVisualize} from 'sentry/views/explore/metrics/metricsQueryParams'; +import {type TraceMetric} from 'sentry/views/explore/metrics/traceMetric'; +import { + useQueryParamsGroupBys, + useQueryParamsQuery, + useSetQueryParamsQuery, +} from 'sentry/views/explore/queryParams/context'; +import {TraceItemDataset} from 'sentry/views/explore/types'; + +interface MetricRowProps { + traceMetric: TraceMetric; +} + +export function MetricRow({traceMetric}: MetricRowProps) { + const query = useQueryParamsQuery(); + const setQuery = useSetQueryParamsQuery(); + + const tracesItemSearchQueryBuilderProps: TraceItemSearchQueryBuilderProps = + useMemo(() => { + return { + itemType: TraceItemDataset.TRACEMETRICS, + numberAttributes: {}, + stringAttributes: {}, + numberSecondaryAliases: {}, + stringSecondaryAliases: {}, + initialQuery: query, + onSearch: setQuery, + searchSource: 'ourmetrics', + }; + }, [query, setQuery]); + + const searchQueryBuilderProviderProps = useSearchQueryBuilderProps( + tracesItemSearchQueryBuilderProps + ); + + return ( + + + + ); +} + +interface MetricToolbarProps { + traceMetric: TraceMetric; + tracesItemSearchQueryBuilderProps: TraceItemSearchQueryBuilderProps; +} + +function MetricToolbar({ + tracesItemSearchQueryBuilderProps, + traceMetric, +}: MetricToolbarProps) { + const visualize = useMetricVisualize(); + const groupBys = useQueryParamsGroupBys(); + const query = useQueryParamsQuery(); + return ( +
+ {traceMetric.name}/{visualize.yAxis}/ by {groupBys.join(',')}/ where {query} + +
+ ); +} diff --git a/static/app/views/explore/metrics/metricsQueryParams.tsx b/static/app/views/explore/metrics/metricsQueryParams.tsx new file mode 100644 index 00000000000000..8f29e1319ded1e --- /dev/null +++ b/static/app/views/explore/metrics/metricsQueryParams.tsx @@ -0,0 +1,62 @@ +import type {ReactNode} from 'react'; +import {useCallback, useMemo} from 'react'; + +import {useResettableState} from 'sentry/utils/useResettableState'; +import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; +import { + QueryParamsContextProvider, + useQueryParamsVisualizes, +} from 'sentry/views/explore/queryParams/context'; +import {ReadableQueryParams} from 'sentry/views/explore/queryParams/readableQueryParams'; +import {VisualizeFunction} from 'sentry/views/explore/queryParams/visualize'; +import type {WritableQueryParams} from 'sentry/views/explore/queryParams/writableQueryParams'; + +interface MetricsQueryParamsProviderProps { + children: ReactNode; +} + +export function MetricsQueryParamsProvider({children}: MetricsQueryParamsProviderProps) { + const [query, setQuery] = useResettableState(() => ''); + + const readableQueryParams = useMemo(() => { + return new ReadableQueryParams({ + extrapolate: true, + mode: Mode.AGGREGATE, + query, + + cursor: '', + fields: ['id', 'timestamp'], + sortBys: [{field: 'timestamp', kind: 'desc'}], + + aggregateCursor: '', + aggregateFields: [new VisualizeFunction('sum(value)')], + aggregateSortBys: [{field: 'sum(value)', kind: 'desc'}], + }); + }, [query]); + + const setWritableQueryParams = useCallback( + (writableQueryParams: WritableQueryParams) => { + setQuery(writableQueryParams.query); + }, + [setQuery] + ); + + return ( + + {children} + + ); +} + +export function useMetricVisualize() { + const visualizes = useQueryParamsVisualizes(); + if (visualizes.length === 1) { + return visualizes[0]!; + } + throw new Error('Only 1 visualize per metric allowed'); +} diff --git a/static/app/views/explore/metrics/metricsTab.tsx b/static/app/views/explore/metrics/metricsTab.tsx index 156855400f691f..1c51fcaac7b068 100644 --- a/static/app/views/explore/metrics/metricsTab.tsx +++ b/static/app/views/explore/metrics/metricsTab.tsx @@ -1,17 +1,19 @@ +import {Fragment, useMemo} from 'react'; + import * as Layout from 'sentry/components/layouts/thirds'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter'; -import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/context'; import {t} from 'sentry/locale'; -import {useSearchQueryBuilderProps} from 'sentry/views/explore/components/traceItemSearchQueryBuilder'; import { BottomSectionBody, FilterBarContainer, StyledPageFilterBar, TopSectionBody, } from 'sentry/views/explore/logs/styles'; -import {TraceItemDataset} from 'sentry/views/explore/types'; +import {MetricRow} from 'sentry/views/explore/metrics/metricRow'; +import {MetricsQueryParamsProvider} from 'sentry/views/explore/metrics/metricsQueryParams'; +import {type TraceMetric} from 'sentry/views/explore/metrics/traceMetric'; import type {PickableDays} from 'sentry/views/explore/utils'; type LogsTabProps = PickableDays; @@ -21,34 +23,58 @@ export function MetricsTabContent({ maxPickableDays, relativeOptions, }: LogsTabProps) { - const searchQueryBuilderProviderProps = useSearchQueryBuilderProps({ - itemType: TraceItemDataset.TRACEMETRICS, - numberAttributes: {}, - stringAttributes: {}, - numberSecondaryAliases: {}, - stringSecondaryAliases: {}, - initialQuery: '', - searchSource: 'ourmetrics', - }); return ( - - - - - - - - - - - - - Currently in development - + + + + + ); +} + +function MetricsTabFilterSection({ + defaultPeriod, + maxPickableDays, + relativeOptions, +}: PickableDays) { + return ( + + + + + + + + + + + + ); +} + +function MetricsTabBodySection() { + const traceMetrics: TraceMetric[] = useMemo(() => { + return [{name: 'myfirstmetric'}, {name: 'mysecondmetric'}]; + }, []); + + return ( + + {traceMetrics.map((traceMetric, index) => { + return ( + // TODO: figure out a better `key` + + + + ); + })} + ); } diff --git a/static/app/views/explore/metrics/traceMetric.tsx b/static/app/views/explore/metrics/traceMetric.tsx new file mode 100644 index 00000000000000..8126d97611c13b --- /dev/null +++ b/static/app/views/explore/metrics/traceMetric.tsx @@ -0,0 +1,3 @@ +export interface TraceMetric { + name: string; +} From 644c32e7714e51c17d719ca13bcf5c25eee87b1a Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 3 Oct 2025 17:51:23 -0400 Subject: [PATCH 2/2] Update static/app/views/explore/metrics/metricRow.tsx Co-authored-by: Nar Saynorath --- static/app/views/explore/metrics/metricRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/explore/metrics/metricRow.tsx b/static/app/views/explore/metrics/metricRow.tsx index 683434ab7735c2..55c3cb4ea7147e 100644 --- a/static/app/views/explore/metrics/metricRow.tsx +++ b/static/app/views/explore/metrics/metricRow.tsx @@ -33,7 +33,7 @@ export function MetricRow({traceMetric}: MetricRowProps) { stringSecondaryAliases: {}, initialQuery: query, onSearch: setQuery, - searchSource: 'ourmetrics', + searchSource: 'tracemetrics', }; }, [query, setQuery]);