diff --git a/static/app/utils/analytics/metricsAnalyticsEvent.tsx b/static/app/utils/analytics/metricsAnalyticsEvent.tsx index 585b67b3b06d33..c56b83a33194c5 100644 --- a/static/app/utils/analytics/metricsAnalyticsEvent.tsx +++ b/static/app/utils/analytics/metricsAnalyticsEvent.tsx @@ -11,19 +11,21 @@ export type MetricsAnalyticsEventParameters = { metric_panels_with_group_bys_count: number; metric_queries_count: number; project_count: number; + title: string; }; 'metrics.explorer.panel.metadata': { - columns: readonly string[]; - columns_count: number; + aggregate_function: string; confidences: string[]; dataScanned: string; dataset: string; empty_buckets_percentage: number[]; + group_bys: readonly string[]; interval: string; + metric_name: string; + metric_type: string; query_status: 'success' | 'error' | 'pending'; sample_counts: number[]; table_result_length: number; - table_result_missing_root: number; table_result_mode: 'metric samples' | 'aggregates'; table_result_sort: string[]; user_queries: string; diff --git a/static/app/views/explore/hooks/useAnalytics.tsx b/static/app/views/explore/hooks/useAnalytics.tsx index ab01dfbff3c505..ec993fbc1fab47 100644 --- a/static/app/views/explore/hooks/useAnalytics.tsx +++ b/static/app/views/explore/hooks/useAnalytics.tsx @@ -1,4 +1,4 @@ -import {useEffect, useMemo, useRef, type RefObject} from 'react'; +import {useEffect, useEffectEvent, useMemo, useRef, type RefObject} from 'react'; import * as Sentry from '@sentry/react'; import {useOrganizationSeerSetup} from 'sentry/components/events/autofix/useOrganizationSeerSetup'; @@ -8,6 +8,7 @@ import type {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalytics import type {Sort} from 'sentry/utils/discover/fields'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types'; @@ -15,6 +16,7 @@ import {useChartSelection} from 'sentry/views/explore/components/attributeBreakd import {useLogsAutoRefreshEnabled} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext'; import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; import {formatSort} from 'sentry/views/explore/contexts/pageParamsContext/sortBys'; +import {getTitleFromLocation} from 'sentry/views/explore/contexts/pageParamsContext/title'; import type {AggregatesTableResult} from 'sentry/views/explore/hooks/useExploreAggregatesTable'; import type {SpansTableResult} from 'sentry/views/explore/hooks/useExploreSpansTable'; import type {TracesTableResult} from 'sentry/views/explore/hooks/useExploreTracesTable'; @@ -23,9 +25,13 @@ import {type useLogsAggregatesTable} from 'sentry/views/explore/logs/useLogsAggr import type {UseInfiniteLogsQueryResult} from 'sentry/views/explore/logs/useLogsQuery'; import {useMetricAggregatesTable} from 'sentry/views/explore/metrics/hooks/useMetricAggregatesTable'; import {useMetricSamplesTable} from 'sentry/views/explore/metrics/hooks/useMetricSamplesTable'; +import type {TraceMetric} from 'sentry/views/explore/metrics/metricQuery'; +import {useMetricVisualize} from 'sentry/views/explore/metrics/metricsQueryParams'; +import {isEmptyTraceMetric} from 'sentry/views/explore/metrics/utils'; import type {ReadableExploreQueryParts} from 'sentry/views/explore/multiQueryMode/locationUtils'; import { useQueryParamsFields, + useQueryParamsGroupBys, useQueryParamsQuery, useQueryParamsSearch, useQueryParamsTitle, @@ -480,7 +486,6 @@ export function useLogAnalytics({ yAxes: string[]; }) { const organization = useOrganization(); - const { data: {hasExceededPerformanceUsageLimit}, isLoading: isLoadingSubscriptionDetails, @@ -716,7 +721,7 @@ export function useMetricsPanelAnalytics({ metricSamplesTableResult, metricTimeseriesResult, mode, - yAxis, + traceMetric, sortBys, aggregateSortBys, panelIndex, @@ -729,7 +734,7 @@ export function useMetricsPanelAnalytics({ metricTimeseriesResult: ReturnType; mode: Mode; sortBys: readonly Sort[]; - yAxis: string; + traceMetric: TraceMetric; panelIndex?: number; }) { const organization = useOrganization(); @@ -741,7 +746,9 @@ export function useMetricsPanelAnalytics({ : (metricSamplesTableResult.result.meta?.dataScanned ?? ''); const search = useQueryParamsSearch(); const query = useQueryParamsQuery(); - const fields = useQueryParamsFields(); + const groupBys = useQueryParamsGroupBys(); + const visualize = useMetricVisualize(); + const aggregateFunctionBox = useBox(visualize.parsedFunction?.name ?? ''); const tableError = mode === Mode.AGGREGATE @@ -762,65 +769,58 @@ export function useMetricsPanelAnalytics({ const timeseriesDataBox = useBox(metricTimeseriesResult.data); const searchStringBox = useBox(useMemo(() => search.formatString(), [search])); const searchTokensLengthBox = useBox(useMemo(() => search.tokens.length, [search])); - const fieldsBox = useBox(fields); + const groupBysBox = useBox(groupBys); const dataScannedBox = useBox(dataScanned); - const yAxisBox = useBox(yAxis); + const metricNameBox = useBox(traceMetric.name); + const metricTypeBox = useBox(traceMetric.type); + const intervalBox = useBox(interval); const queryStatusBox = useBox(query_status); const isTopNBox = useBox(isTopN); - useEffect(() => { - if ( - mode !== Mode.SAMPLES || - metricSamplesTableResult.result.isFetching || - metricTimeseriesResult.isPending || - !dataScannedBox.current || - !yAxisBox.current - ) { - return; - } - - trackAnalytics('metrics.explorer.panel.metadata', { - organization, + const getAttributes = useEffectEvent((resultMode: 'metric samples' | 'aggregates') => { + return { dataset, + metric_name: metricNameBox.current, + metric_type: metricTypeBox.current, dataScanned: dataScannedBox.current, - columns: fieldsBox.current, - columns_count: fieldsBox.current.length, - confidences: computeConfidence([yAxisBox.current], timeseriesDataBox.current), + group_bys: groupBysBox.current, + confidences: computeConfidence([metricNameBox.current], timeseriesDataBox.current), empty_buckets_percentage: computeEmptyBuckets( - [yAxisBox.current], + [metricNameBox.current], timeseriesDataBox.current ), - interval: intervalBox.current, - query_status, - sample_counts: computeVisualizeSampleTotals( - [yAxisBox.current], - timeseriesDataBox.current, - isTopNBox.current - ), table_result_length: resultLengthBox.current, - table_result_missing_root: 0, - table_result_mode: 'metric samples', + table_result_mode: resultMode, table_result_sort: JSON.parse(formattedSortBysBox.current), user_queries: searchStringBox.current, user_queries_count: searchTokensLengthBox.current, panel_index: panelIndex, - }); + aggregate_function: aggregateFunctionBox.current, + interval: intervalBox.current, + query_status: queryStatusBox.current as 'pending' | 'error' | 'success', + sample_counts: computeVisualizeSampleTotals( + [metricNameBox.current], + timeseriesDataBox.current, + isTopNBox.current + ), + }; + }); - info( - fmt`metric.explorer.panel.metadata: - organization: ${organization.slug} - dataScanned: ${dataScannedBox.current} - dataset: ${dataset} - query: ${query} - fields: ${fieldsBox.current} - query_status: ${queryStatusBox.current} - result_length: ${String(resultLengthBox.current)} - user_queries: ${searchStringBox.current} - user_queries_count: ${String(searchTokensLengthBox.current)} - `, - {isAnalytics: true} - ); + useEffect(() => { + if ( + mode !== Mode.SAMPLES || + metricSamplesTableResult.result.isFetching || + metricTimeseriesResult.isPending || + !dataScannedBox.current || + !metricNameBox.current + ) { + return; + } + const attributes = getAttributes('metric samples'); + + trackAnalytics('metrics.explorer.panel.metadata', {...attributes, organization}); + info('metric.explorer.panel.metadata', {...attributes, isAnalytics: true}); }, [ organization, dataset, @@ -829,18 +829,20 @@ export function useMetricsPanelAnalytics({ metricTimeseriesResult.isPending, panelIndex, dataScannedBox, - fieldsBox, intervalBox, queryStatusBox, isTopNBox, resultLengthBox, formattedSortBysBox, - yAxisBox, + metricNameBox, timeseriesDataBox, searchStringBox, searchTokensLengthBox, query_status, query, + aggregateFunctionBox, + groupBysBox, + metricTypeBox, ]); useEffect(() => { @@ -849,52 +851,14 @@ export function useMetricsPanelAnalytics({ metricAggregatesTableResult.result.isPending || metricTimeseriesResult.isPending || !dataScannedBox.current || - !yAxisBox.current + !metricNameBox.current ) { return; } - trackAnalytics('metrics.explorer.panel.metadata', { - organization, - dataset, - dataScanned: dataScannedBox.current, - columns: fieldsBox.current, - columns_count: fieldsBox.current.length, - confidences: computeConfidence([yAxisBox.current], timeseriesDataBox.current), - empty_buckets_percentage: computeEmptyBuckets( - [yAxisBox.current], - timeseriesDataBox.current - ), - interval: intervalBox.current, - query_status: queryStatusBox.current as 'pending' | 'error' | 'success', - sample_counts: computeVisualizeSampleTotals( - [yAxisBox.current], - timeseriesDataBox.current, - isTopNBox.current - ), - table_result_length: aggregatesResultLengthBox.current, - table_result_missing_root: 0, - table_result_mode: 'aggregates', - table_result_sort: JSON.parse(formattedAggregateSortBysBox.current), - user_queries: searchStringBox.current, - user_queries_count: searchTokensLengthBox.current, - panel_index: panelIndex, - }); - - info( - fmt`metric.explorer.panel.metadata: - organization: ${organization.slug} - dataScanned: ${dataScannedBox.current} - dataset: ${dataset} - query: ${query} - fields: ${fieldsBox.current} - query_status: ${queryStatusBox.current} - result_length: ${String(aggregatesResultLengthBox.current)} - user_queries: ${searchStringBox.current} - user_queries_count: ${String(searchTokensLengthBox.current)} - `, - {isAnalytics: true} - ); + const attributes = getAttributes('aggregates'); + trackAnalytics('metrics.explorer.panel.metadata', {...attributes, organization}); + info('metric.explorer.panel.metadata', {...attributes, isAnalytics: true}); }, [ organization, dataset, @@ -905,14 +869,13 @@ export function useMetricsPanelAnalytics({ formattedAggregateSortBysBox, aggregatesResultLengthBox, dataScannedBox, - fieldsBox, intervalBox, isTopNBox, timeseriesDataBox, queryStatusBox, searchStringBox, searchTokensLengthBox, - yAxisBox, + metricNameBox, query, query_status, ]); @@ -921,29 +884,46 @@ export function useMetricsPanelAnalytics({ export function useMetricsAnalytics({ interval, metricQueries, + areToolbarsLoading, + isMetricOptionsEmpty, }: { + areToolbarsLoading: boolean; interval: string; - metricQueries: Array<{queryParams: ReadableQueryParams}>; + isMetricOptionsEmpty: boolean; + metricQueries: Array<{metric: TraceMetric; queryParams: ReadableQueryParams}>; }) { const organization = useOrganization(); const {selection} = usePageFilters(); + const location = useLocation(); + const title = getTitleFromLocation(location); const { data: {hasExceededPerformanceUsageLimit}, isLoading: isLoadingSubscriptionDetails, } = usePerformanceSubscriptionDetails({traceItemDataset: 'default'}); - const metricPanelsWithGroupBysCount = useBox( - metricQueries.filter(mq => - mq.queryParams.groupBys.some((gb: string) => gb.trim().length > 0) - ).length - ); - const metricPanelsWithFiltersCount = useBox( - metricQueries.filter(mq => mq.queryParams.query.trim().length > 0).length + const queries = metricQueries.map(mq => mq.metric); + + const nonEmptyMetricQueries = useMemo( + () => queries.filter(q => !isEmptyTraceMetric(q)), + [queries] ); + const metricPanelsWithGroupBys = metricQueries + .filter(mq => !isEmptyTraceMetric(mq.metric)) + .filter(mq => + mq.queryParams.groupBys.some((gb: string) => gb.trim().length > 0) + ).length; + const metricPanelsWithFilters = metricQueries + .filter(mq => !isEmptyTraceMetric(mq.metric)) + .filter(mq => mq.queryParams.query.trim().length > 0).length; + useEffect(() => { - if (isLoadingSubscriptionDetails) { + if (isLoadingSubscriptionDetails || areToolbarsLoading) { + return; + } + + if (nonEmptyMetricQueries.length === 0 && !isMetricOptionsEmpty) { return; } @@ -957,10 +937,11 @@ export function useMetricsAnalytics({ environment_count: environmentCount, has_exceeded_performance_usage_limit: hasExceededPerformanceUsageLimit, interval, - metric_panels_with_filters_count: metricPanelsWithFiltersCount.current, - metric_panels_with_group_bys_count: metricPanelsWithGroupBysCount.current, - metric_queries_count: metricQueries.length, + metric_panels_with_filters_count: metricPanelsWithFilters, + metric_panels_with_group_bys_count: metricPanelsWithGroupBys, + metric_queries_count: nonEmptyMetricQueries.length, project_count: projectCount, + title: title || '', }); info( @@ -970,26 +951,30 @@ export function useMetricsAnalytics({ environment_count: ${String(environmentCount)} interval: ${interval} metric_queries_count: ${String(metricQueries.length)} - metric_panels_with_group_bys_count: ${String(metricPanelsWithGroupBysCount.current)} - metric_panels_with_filters_count: ${String(metricPanelsWithFiltersCount.current)} + metric_panels_with_group_bys_count: ${String(metricPanelsWithGroupBys)} + metric_panels_with_filters_count: ${String(metricPanelsWithFilters)} project_count: ${String(projectCount)} has_exceeded_performance_usage_limit: ${String(hasExceededPerformanceUsageLimit)} `, {isAnalytics: true} ); }, [ + areToolbarsLoading, + title, hasExceededPerformanceUsageLimit, interval, isLoadingSubscriptionDetails, - metricQueries.length, - metricPanelsWithGroupBysCount, - metricPanelsWithFiltersCount, + nonEmptyMetricQueries.length, + metricPanelsWithGroupBys, + metricPanelsWithFilters, organization, + isMetricOptionsEmpty, selection.datetime.end, selection.datetime.period, selection.datetime.start, selection.environments.length, selection.projects.length, + metricQueries.length, ]); } diff --git a/static/app/views/explore/metrics/metricPanel/index.tsx b/static/app/views/explore/metrics/metricPanel/index.tsx index 68c66f8d6b90dc..30171f11cbe6fb 100644 --- a/static/app/views/explore/metrics/metricPanel/index.tsx +++ b/static/app/views/explore/metrics/metricPanel/index.tsx @@ -72,7 +72,7 @@ export function MetricPanel({traceMetric, queryIndex}: MetricPanelProps) { metricSamplesTableResult, metricTimeseriesResult: timeseriesResult, mode, - yAxis: traceMetric.name || '', + traceMetric, sortBys, aggregateSortBys, panelIndex: queryIndex, diff --git a/static/app/views/explore/metrics/metricsTab.spec.tsx b/static/app/views/explore/metrics/metricsTab.spec.tsx index da903cab1b1f8d..72b4ab096c9991 100644 --- a/static/app/views/explore/metrics/metricsTab.spec.tsx +++ b/static/app/views/explore/metrics/metricsTab.spec.tsx @@ -14,8 +14,10 @@ import { import type {DatePageFilterProps} from 'sentry/components/organizations/datePageFilter'; import {trackAnalytics} from 'sentry/utils/analytics'; +import {TraceItemAttributeProvider} from 'sentry/views/explore/contexts/traceItemAttributeContext'; import {MetricsTabContent} from 'sentry/views/explore/metrics/metricsTab'; import {MultiMetricsQueryParamsProvider} from 'sentry/views/explore/metrics/multiMetricsQueryParams'; +import {TraceItemDataset} from 'sentry/views/explore/types'; jest.mock('sentry/utils/analytics'); const trackAnalyticsMock = jest.mocked(trackAnalytics); @@ -45,11 +47,18 @@ describe('MetricsTabContent', () => { start: '2025-04-10T14%3A37%3A55', end: '2025-04-10T20%3A04%3A51', metric: ['bar||distribution'], + title: 'Test Title', }, }); function ProviderWrapper({children}: {children: React.ReactNode}) { - return {children}; + return ( + + + {children} + + + ); } const initialRouterConfig = { @@ -112,6 +121,12 @@ describe('MetricsTabContent', () => { body: [], }); + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/recent-searches/`, + method: 'POST', + body: [], + }); + MockApiClient.addMockResponse({ url: `/subscriptions/${organization.slug}/`, method: 'GET', @@ -196,24 +211,50 @@ describe('MetricsTabContent', () => { expect(toolbars).toHaveLength(1); await waitFor(() => { - expect(trackAnalyticsMock).toHaveBeenCalledWith( + expect(trackAnalyticsMock).toHaveBeenNthCalledWith( + 1, 'metrics.explorer.metadata', expect.objectContaining({ organization, metric_queries_count: 1, + metric_panels_with_filters_count: 0, + metric_panels_with_group_bys_count: 0, + datetime_selection: '--14d', + environment_count: 0, + has_exceeded_performance_usage_limit: false, + interval: '1h', + project_count: 1, + title: 'Test Title', }) ); }); await waitFor(() => { - expect(trackAnalyticsMock).toHaveBeenCalledWith( + expect(trackAnalyticsMock).toHaveBeenNthCalledWith( + 2, 'metrics.explorer.panel.metadata', expect.objectContaining({ - organization, - panel_index: 0, // First panel should have index 0 + panel_index: 0, + query_status: 'success', + sample_counts: [0], + table_result_length: 6, + table_result_mode: 'aggregates', + table_result_sort: ['-timestamp'], + user_queries: '', + user_queries_count: 0, + aggregate_function: 'per_second', + confidences: ['null'], + dataScanned: 'full', + dataset: 'metrics', + empty_buckets_percentage: [], + group_bys: [], + interval: '1h', + metric_name: 'bar', + metric_type: 'distribution', }) ); }); + expect(trackAnalyticsMock).toHaveBeenCalledTimes(2); trackAnalyticsMock.mockClear(); @@ -228,33 +269,257 @@ describe('MetricsTabContent', () => { 1, 'metrics.explorer.panel.metadata', expect.objectContaining({ - organization, - panel_index: 1, // Only the new panel should fire + panel_index: 1, + query_status: 'success', + sample_counts: [0], + table_result_length: 6, + table_result_mode: 'aggregates', + table_result_sort: ['-timestamp'], + user_queries: '', + user_queries_count: 0, + aggregate_function: 'per_second', + confidences: ['null'], + dataScanned: 'full', + dataset: 'metrics', + empty_buckets_percentage: [], + group_bys: [], + interval: '1h', + metric_name: 'bar', + metric_type: 'distribution', }) ); expect(trackAnalyticsMock).toHaveBeenNthCalledWith( 2, 'metrics.explorer.metadata', - expect.objectContaining({metric_queries_count: 2}) + expect.objectContaining({ + metric_queries_count: 2, + metric_panels_with_filters_count: 0, + metric_panels_with_group_bys_count: 0, + project_count: 1, + environment_count: 0, + has_exceeded_performance_usage_limit: false, + interval: '1h', + title: 'Test Title', + }) ); expect(trackAnalyticsMock).toHaveBeenCalledTimes(2); - trackAnalyticsMock.mockClear(); - // Picking a new metric on the first panel should only fire one event for the panel update await userEvent.click(within(toolbars[0]!).getByRole('button', {name: 'bar'})); await userEvent.click(within(toolbars[0]!).getByRole('option', {name: 'foo'})); expect(within(toolbars[0]!).getByRole('button', {name: 'foo'})).toBeInTheDocument(); await waitFor(() => { - expect(trackAnalyticsMock).toHaveBeenCalledTimes(1); + expect(trackAnalyticsMock).toHaveBeenNthCalledWith( + 1, + 'metrics.explorer.panel.metadata', + expect.objectContaining({ + panel_index: 0, + aggregate_function: 'per_second', + group_bys: [], + metric_name: 'foo', + metric_type: 'distribution', + }) + ); + }); + + expect(trackAnalyticsMock).toHaveBeenCalledTimes(1); + }); + + it('should fire analytics when group by is changed', async () => { + const metricQueryWithGroupBy = JSON.stringify({ + metric: {name: 'bar', type: 'distribution'}, + query: '', + aggregateFields: [ + {groupBy: 'environment'}, + {yAxes: ['per_second(bar)'], displayType: 'line'}, + ], + aggregateSortBys: [], + mode: 'aggregate', + }); + + const initialRouterConfigWithGroupBy = { + location: { + pathname: '/organizations/:orgId/explore/metrics/', + query: { + start: '2025-04-10T14%3A37%3A55', + end: '2025-04-10T20%3A04%3A51', + metric: [metricQueryWithGroupBy], + title: 'Test Title', + }, + }, + route: '/organizations/:orgId/explore/metrics/', + }; + + render( + + + , + { + initialRouterConfig: initialRouterConfigWithGroupBy, + organization, + } + ); + + const toolbars = screen.getAllByTestId('metric-toolbar'); + expect(toolbars).toHaveLength(1); + + await waitFor(() => { + expect(trackAnalyticsMock).toHaveBeenNthCalledWith( + 1, + 'metrics.explorer.panel.metadata', + expect.objectContaining({ + panel_index: 0, + aggregate_function: 'per_second', + group_bys: ['environment'], + metric_name: 'bar', + metric_type: 'distribution', + }) + ); }); expect(trackAnalyticsMock).toHaveBeenNthCalledWith( - 1, - 'metrics.explorer.panel.metadata', - expect.objectContaining({panel_index: 0}) + 2, + 'metrics.explorer.metadata', + expect.objectContaining({ + metric_panels_with_filters_count: 0, + metric_panels_with_group_bys_count: 1, + metric_queries_count: 1, + }) + ); + + expect(trackAnalyticsMock).toHaveBeenCalledTimes(2); + }); + + it('should fire analytics when filter is changed', async () => { + render( + + + , + { + initialRouterConfig, + organization, + } + ); + + const toolbars = screen.getAllByTestId('metric-toolbar'); + expect(toolbars).toHaveLength(1); + + await waitFor(() => { + expect(within(toolbars[0]!).getByRole('button', {name: 'bar'})).toBeInTheDocument(); + }); + + trackAnalyticsMock.mockClear(); + + const searchInput = within(toolbars[0]!).getByRole('combobox'); + await userEvent.click(searchInput); + await userEvent.type(searchInput, 'has:environment{enter}'); + + await waitFor(() => { + expect(trackAnalyticsMock).toHaveBeenCalledWith( + 'metrics.explorer.panel.metadata', + expect.objectContaining({ + panel_index: 0, + user_queries: 'has:environment', + user_queries_count: 1, + }) + ); + }); + + await waitFor(() => { + expect(trackAnalyticsMock).toHaveBeenCalledWith( + 'metrics.explorer.metadata', + expect.objectContaining({ + metric_panels_with_filters_count: 1, + metric_panels_with_group_bys_count: 0, + metric_queries_count: 1, + }) + ); + }); + }); + + it('should fire analytics with no metrics available', async () => { + MockApiClient.clearMockResponses(); + setupPageFilters(); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + method: 'GET', + body: {data: []}, + match: [ + MockApiClient.matchQuery({ + dataset: 'tracemetrics', + referrer: 'api.explore.metric-options', + }), + ], + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/recent-searches/`, + method: 'GET', + body: [], + }); + + MockApiClient.addMockResponse({ + url: `/subscriptions/${organization.slug}/`, + method: 'GET', + body: {}, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/trace-items/attributes/`, + method: 'GET', + body: [], + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/stats_v2/`, + method: 'GET', + body: {}, + }); + + render( + + + , + { + initialRouterConfig, + organization, + } ); + + const toolbars = screen.getAllByTestId('metric-toolbar'); + expect(toolbars).toHaveLength(1); + + await waitFor(() => { + expect( + within(toolbars[0]!).getByRole('button', {name: 'None'}) + ).toBeInTheDocument(); + }); + + expect(screen.getByTestId('metric-panel')).toBeInTheDocument(); + + // Only the explorer metadata event should be fired, not the panel event + await waitFor(() => { + expect(trackAnalyticsMock).toHaveBeenNthCalledWith( + 1, + 'metrics.explorer.metadata', + expect.objectContaining({ + organization, + metric_queries_count: 0, + metric_panels_with_filters_count: 0, + metric_panels_with_group_bys_count: 0, + datetime_selection: '--14d', + environment_count: 0, + has_exceeded_performance_usage_limit: false, + interval: '1h', + project_count: 1, + title: 'Test Title', + }) + ); + }); + + expect(trackAnalyticsMock).toHaveBeenCalledTimes(1); }); }); diff --git a/static/app/views/explore/metrics/metricsTab.tsx b/static/app/views/explore/metrics/metricsTab.tsx index 4aa15416f6a6b2..f975f903b8a575 100644 --- a/static/app/views/explore/metrics/metricsTab.tsx +++ b/static/app/views/explore/metrics/metricsTab.tsx @@ -17,6 +17,7 @@ import { import {ToolbarVisualizeAddChart} from 'sentry/views/explore/components/toolbar/toolbarVisualize'; import {useMetricsAnalytics} from 'sentry/views/explore/hooks/useAnalytics'; import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval'; +import {useMetricOptions} from 'sentry/views/explore/hooks/useMetricOptions'; import {MetricPanel} from 'sentry/views/explore/metrics/metricPanel'; import {MetricsQueryParamsProvider} from 'sentry/views/explore/metrics/metricsQueryParams'; import {MetricToolbar} from 'sentry/views/explore/metrics/metricToolbar'; @@ -101,7 +102,15 @@ function MetricsQueryBuilderSection() { function MetricsTabBodySection() { const metricQueries = useMultiMetricsQueryParams(); const [interval] = useChartInterval(); - useMetricsAnalytics({interval, metricQueries}); + const {isFetching: areToolbarsLoading, isMetricOptionsEmpty} = useMetricOptions({ + enabled: true, + }); + useMetricsAnalytics({ + interval, + metricQueries, + areToolbarsLoading, + isMetricOptionsEmpty, + }); return ( diff --git a/static/app/views/explore/metrics/utils.tsx b/static/app/views/explore/metrics/utils.tsx index c7e92babf01747..e362fba0c5d3ad 100644 --- a/static/app/views/explore/metrics/utils.tsx +++ b/static/app/views/explore/metrics/utils.tsx @@ -206,3 +206,7 @@ export function updateVisualizeYAxis( chartType: undefined, }); } + +export function isEmptyTraceMetric(traceMetric: TraceMetric): boolean { + return traceMetric.name === ''; +}