diff --git a/static/app/utils/analytics/metricsAnalyticsEvent.tsx b/static/app/utils/analytics/metricsAnalyticsEvent.tsx index 8cd65a42a082b5..a607eb334cbdbf 100644 --- a/static/app/utils/analytics/metricsAnalyticsEvent.tsx +++ b/static/app/utils/analytics/metricsAnalyticsEvent.tsx @@ -2,6 +2,33 @@ import type {Organization} from 'sentry/types/organization'; import type {PlatformKey} from 'sentry/types/project'; export type MetricsAnalyticsEventParameters = { + 'metrics.explorer.metadata': { + datetime_selection: string; + environment_count: number; + has_exceeded_performance_usage_limit: boolean | null; + interval: string; + metric_panels_with_filters_count: number; + metric_panels_with_group_bys_count: number; + metric_queries_count: number; + project_count: number; + }; + 'metrics.explorer.panel.metadata': { + columns: readonly string[]; + columns_count: number; + confidences: string[]; + dataScanned: string; + dataset: string; + empty_buckets_percentage: number[]; + interval: 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; + user_queries_count: number; + }; 'metrics.explorer.setup_button_clicked': { organization: Organization; platform: PlatformKey | 'unknown'; @@ -32,6 +59,8 @@ export type MetricsAnalyticsEventParameters = { type MetricsAnalyticsEventKey = keyof MetricsAnalyticsEventParameters; export const metricsAnalyticsEventMap: Record = { + 'metrics.explorer.metadata': 'Metric Explorer Pageload Metadata', + 'metrics.explorer.panel.metadata': 'Metric Explorer Panel Metadata', 'metrics.explorer.setup_button_clicked': 'Metrics Setup Button Clicked', 'metrics.onboarding': 'Metrics Explore Empty State (Onboarding)', 'metrics.onboarding_platform_docs_viewed': diff --git a/static/app/views/explore/hooks/useAnalytics.tsx b/static/app/views/explore/hooks/useAnalytics.tsx index 9c7e12c6d8decc..4d937783faf2d6 100644 --- a/static/app/views/explore/hooks/useAnalytics.tsx +++ b/static/app/views/explore/hooks/useAnalytics.tsx @@ -9,6 +9,7 @@ import type {Sort} from 'sentry/utils/discover/fields'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import useOrganization from 'sentry/utils/useOrganization'; +import usePageFilters from 'sentry/utils/usePageFilters'; import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types'; import {useLogsAutoRefreshEnabled} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext'; import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; @@ -19,6 +20,8 @@ import type {TracesTableResult} from 'sentry/views/explore/hooks/useExploreTrace import {useTopEvents} from 'sentry/views/explore/hooks/useTopEvents'; import {type useLogsAggregatesTable} from 'sentry/views/explore/logs/useLogsAggregatesTable'; 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 {ReadableExploreQueryParts} from 'sentry/views/explore/multiQueryMode/locationUtils'; import { useQueryParamsFields, @@ -27,6 +30,7 @@ import { useQueryParamsTitle, useQueryParamsVisualizes, } from 'sentry/views/explore/queryParams/context'; +import type {ReadableQueryParams} from 'sentry/views/explore/queryParams/readableQueryParams'; import {Visualize} from 'sentry/views/explore/queryParams/visualize'; import {useSpansDataset} from 'sentry/views/explore/spans/spansQueryParams'; import { @@ -685,6 +689,260 @@ function computeEmptyBuckets( }); } +export function useMetricsPanelAnalytics({ + interval, + isTopN, + metricAggregatesTableResult, + metricSamplesTableResult, + metricTimeseriesResult, + mode, + yAxis, + sortBys, + aggregateSortBys, +}: { + aggregateSortBys: readonly Sort[]; + interval: string; + isTopN: boolean; + metricAggregatesTableResult: ReturnType; + metricSamplesTableResult: ReturnType; + metricTimeseriesResult: ReturnType; + mode: Mode; + sortBys: readonly Sort[]; + yAxis: string; +}) { + const organization = useOrganization(); + + const dataset = DiscoverDatasets.METRICS; + const dataScanned = metricSamplesTableResult.result.meta?.dataScanned ?? ''; + const search = useQueryParamsSearch(); + const query = useQueryParamsQuery(); + const fields = useQueryParamsFields(); + + const tableError = + mode === Mode.AGGREGATE + ? (metricAggregatesTableResult.result.error?.message ?? '') + : (metricSamplesTableResult.error?.message ?? ''); + const query_status = tableError ? 'error' : 'success'; + + const aggregatesResultLengthBox = useBox( + metricAggregatesTableResult.result.data?.length || 0 + ); + const resultLengthBox = useBox(metricSamplesTableResult.result.data?.length || 0); + const fieldsBox = useBox(fields); + const yAxesBox = useBox([yAxis]); + const sortBysBox = useBox(sortBys.map(formatSort)); + const aggregateSortBysBox = useBox(aggregateSortBys.map(formatSort)); + + const timeseriesData = useBox(metricTimeseriesResult.data); + + useEffect(() => { + if (mode !== Mode.SAMPLES) { + return; + } + + if (metricSamplesTableResult.result.isFetching) { + return; + } + + trackAnalytics('metrics.explorer.panel.metadata', { + organization, + dataset, + dataScanned, + columns: fieldsBox.current, + columns_count: fieldsBox.current.length, + confidences: computeConfidence(yAxesBox.current, timeseriesData.current), + empty_buckets_percentage: computeEmptyBuckets( + yAxesBox.current, + timeseriesData.current + ), + interval, + query_status, + sample_counts: computeVisualizeSampleTotals( + yAxesBox.current, + timeseriesData.current, + isTopN + ), + table_result_length: resultLengthBox.current, + table_result_missing_root: 0, + table_result_mode: 'metric samples', + table_result_sort: sortBysBox.current, + user_queries: search.formatString(), + user_queries_count: search.tokens.length, + }); + + info( + fmt`metric.explorer.panel.metadata: + organization: ${organization.slug} + dataScanned: ${dataScanned} + dataset: ${dataset} + query: ${query} + fields: ${fieldsBox.current} + query_status: ${query_status} + result_length: ${String(resultLengthBox.current)} + user_queries: ${search.formatString()} + user_queries_count: ${String(search.tokens.length)} + `, + {isAnalytics: true} + ); + }, [ + organization, + dataset, + dataScanned, + query, + fieldsBox, + interval, + query_status, + isTopN, + metricSamplesTableResult.result.isFetching, + search, + timeseriesData, + mode, + resultLengthBox, + sortBysBox, + yAxesBox, + ]); + + useEffect(() => { + if (mode !== Mode.AGGREGATE) { + return; + } + + if (metricAggregatesTableResult.result.isPending) { + return; + } + + trackAnalytics('metrics.explorer.panel.metadata', { + organization, + dataset, + dataScanned, + columns: fieldsBox.current, + columns_count: fieldsBox.current.length, + confidences: computeConfidence([yAxis], timeseriesData.current), + empty_buckets_percentage: computeEmptyBuckets([yAxis], timeseriesData.current), + interval, + query_status, + sample_counts: computeVisualizeSampleTotals( + [yAxis], + timeseriesData.current, + isTopN + ), + table_result_length: aggregatesResultLengthBox.current, + table_result_missing_root: 0, + table_result_mode: 'aggregates', + table_result_sort: aggregateSortBysBox.current, + user_queries: search.formatString(), + user_queries_count: search.tokens.length, + }); + + info( + fmt`metric.explorer.panel.metadata: + organization: ${organization.slug} + dataScanned: ${dataScanned} + dataset: ${dataset} + query: ${query} + fields: ${fieldsBox.current} + query_status: ${query_status} + result_length: ${String(aggregatesResultLengthBox.current)} + user_queries: ${search.formatString()} + user_queries_count: ${String(search.tokens.length)} + `, + {isAnalytics: true} + ); + }, [ + aggregateSortBysBox, + aggregatesResultLengthBox, + dataScanned, + dataset, + fieldsBox, + interval, + isTopN, + metricAggregatesTableResult.result.isPending, + timeseriesData, + mode, + organization, + query, + query_status, + search, + yAxis, + ]); +} + +export function useMetricsAnalytics({ + interval, + metricQueries, +}: { + interval: string; + metricQueries: Array<{queryParams: ReadableQueryParams}>; +}) { + const organization = useOrganization(); + const {selection} = usePageFilters(); + + const { + data: {hasExceededPerformanceUsageLimit}, + isLoading: isLoadingSubscriptionDetails, + } = usePerformanceSubscriptionDetails({traceItemDataset: 'default'}); + + const metricQueriesCount = useBox(metricQueries.length); + 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 + ); + + useEffect(() => { + if (isLoadingSubscriptionDetails) { + return; + } + + const datetimeSelection = `${selection.datetime.start || ''}-${selection.datetime.end || ''}-${selection.datetime.period || ''}`; + const projectCount = selection.projects.length; + const environmentCount = selection.environments.length; + + trackAnalytics('metrics.explorer.metadata', { + organization, + datetime_selection: datetimeSelection, + 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: metricQueriesCount.current, + project_count: projectCount, + }); + + info( + fmt`metrics.explorer.metadata: + organization: ${organization.slug} + datetime_selection: ${datetimeSelection} + environment_count: ${String(environmentCount)} + interval: ${interval} + metric_queries_count: ${String(metricQueriesCount.current)} + metric_panels_with_group_bys_count: ${String(metricPanelsWithGroupBysCount.current)} + metric_panels_with_filters_count: ${String(metricPanelsWithFiltersCount.current)} + project_count: ${String(projectCount)} + has_exceeded_performance_usage_limit: ${String(hasExceededPerformanceUsageLimit)} + `, + {isAnalytics: true} + ); + }, [ + hasExceededPerformanceUsageLimit, + interval, + isLoadingSubscriptionDetails, + metricQueriesCount, + metricPanelsWithGroupBysCount, + metricPanelsWithFiltersCount, + organization, + selection.datetime.end, + selection.datetime.period, + selection.datetime.start, + selection.environments.length, + selection.projects.length, + ]); +} + function useBox(value: T): RefObject { const box = useRef(value); box.current = value; diff --git a/static/app/views/explore/metrics/metricPanel/index.tsx b/static/app/views/explore/metrics/metricPanel/index.tsx index f13f1076eff97a..9d85a60a814ba4 100644 --- a/static/app/views/explore/metrics/metricPanel/index.tsx +++ b/static/app/views/explore/metrics/metricPanel/index.tsx @@ -2,11 +2,26 @@ import {useState} from 'react'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; +import {useMetricsPanelAnalytics} from 'sentry/views/explore/hooks/useAnalytics'; +import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval'; +import {useTopEvents} from 'sentry/views/explore/hooks/useTopEvents'; +import {TraceSamplesTableColumns} from 'sentry/views/explore/metrics/constants'; +import {useMetricAggregatesTable} from 'sentry/views/explore/metrics/hooks/useMetricAggregatesTable'; +import {useMetricSamplesTable} from 'sentry/views/explore/metrics/hooks/useMetricSamplesTable'; import {useMetricTimeseries} from 'sentry/views/explore/metrics/hooks/useMetricTimeseries'; import {useTableOrientationControl} from 'sentry/views/explore/metrics/hooks/useOrientationControl'; import {SideBySideOrientation} from 'sentry/views/explore/metrics/metricPanel/sideBySideOrientation'; import {StackedOrientation} from 'sentry/views/explore/metrics/metricPanel/stackedOrientation'; import {type TraceMetric} from 'sentry/views/explore/metrics/metricQuery'; +import {getMetricTableColumnType} from 'sentry/views/explore/metrics/utils'; +import { + useQueryParamsAggregateSortBys, + useQueryParamsMode, + useQueryParamsSortBys, +} from 'sentry/views/explore/queryParams/context'; + +const RESULT_LIMIT = 50; +const TWO_MINUTE_DELAY = 120; interface MetricPanelProps { queryIndex: number; @@ -25,6 +40,41 @@ export function MetricPanel({traceMetric, queryIndex}: MetricPanelProps) { enabled: Boolean(traceMetric.name), }); + const columns = TraceSamplesTableColumns; + const fields = columns.filter(c => getMetricTableColumnType(c) !== 'stat'); + + const metricSamplesTableResult = useMetricSamplesTable({ + disabled: !traceMetric?.name, + limit: RESULT_LIMIT, + traceMetric, + fields, + ingestionDelaySeconds: TWO_MINUTE_DELAY, + }); + + const metricAggregatesTableResult = useMetricAggregatesTable({ + enabled: Boolean(traceMetric.name), + limit: RESULT_LIMIT, + traceMetric, + }); + + const mode = useQueryParamsMode(); + const sortBys = useQueryParamsSortBys(); + const aggregateSortBys = useQueryParamsAggregateSortBys(); + const [interval] = useChartInterval(); + const topEvents = useTopEvents(); + + useMetricsPanelAnalytics({ + interval, + isTopN: !!topEvents, + metricAggregatesTableResult, + metricSamplesTableResult, + metricTimeseriesResult: timeseriesResult, + mode, + yAxis: traceMetric.name || '', + sortBys, + aggregateSortBys, + }); + return ( diff --git a/static/app/views/explore/metrics/metricsTab.tsx b/static/app/views/explore/metrics/metricsTab.tsx index 631ed0823791d0..78cb4b4e2f9eef 100644 --- a/static/app/views/explore/metrics/metricsTab.tsx +++ b/static/app/views/explore/metrics/metricsTab.tsx @@ -13,6 +13,8 @@ import { ExploreControlSection, } from 'sentry/views/explore/components/styles'; 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 {MetricPanel} from 'sentry/views/explore/metrics/metricPanel'; import {MetricsQueryParamsProvider} from 'sentry/views/explore/metrics/metricsQueryParams'; import {MetricToolbar} from 'sentry/views/explore/metrics/metricToolbar'; @@ -108,6 +110,8 @@ function MetricsQueryBuilderSection() { function MetricsTabBodySection() { const metricQueries = useMultiMetricsQueryParams(); + const [interval] = useChartInterval(); + useMetricsAnalytics({interval, metricQueries}); return ( diff --git a/static/app/views/performance/newTraceDetails/traceTypeWarnings/usePerformanceSubscriptionDetails.tsx b/static/app/views/performance/newTraceDetails/traceTypeWarnings/usePerformanceSubscriptionDetails.tsx index e20cb3081a8504..3dccea07da0cc8 100644 --- a/static/app/views/performance/newTraceDetails/traceTypeWarnings/usePerformanceSubscriptionDetails.tsx +++ b/static/app/views/performance/newTraceDetails/traceTypeWarnings/usePerformanceSubscriptionDetails.tsx @@ -19,6 +19,11 @@ type Subscription = { logBytes: { usageExceeded: boolean; }; + } + | { + traceMetrics: { + usageExceeded: boolean; + }; }; planDetails: { billingInterval: 'monthly' | 'annual'; @@ -35,7 +40,7 @@ export function usePerformanceSubscriptionDetails({ }: { // Default refers to the existing behaviour for either spans or transactions. // Otherwise used to discern exactly which usage limit was exceeded in explore pages. - traceItemDataset: 'logs' | 'default'; + traceItemDataset: 'logs' | 'metrics' | 'default'; }) { const organization = useOrganization(); @@ -62,7 +67,7 @@ export function usePerformanceSubscriptionDetails({ function subscriptionHasExceededPerformanceUsageLimit( subscription: Subscription | undefined, - traceItemDataset: 'logs' | 'default' + traceItemDataset: 'logs' | 'metrics' | 'default' ) { let hasExceededExploreItemUsageLimit = false; const dataCategories = subscription?.categories; @@ -72,6 +77,11 @@ function subscriptionHasExceededPerformanceUsageLimit( hasExceededExploreItemUsageLimit = dataCategories.logBytes?.usageExceeded || false; } + } else if (traceItemDataset === 'metrics') { + if ('traceMetrics' in dataCategories) { + hasExceededExploreItemUsageLimit = + dataCategories.traceMetrics?.usageExceeded || false; + } } else if (traceItemDataset === 'default') { if ('transactions' in dataCategories) { hasExceededExploreItemUsageLimit =