Browse Source

feat(stats): adds metrics to org stats (#69498)

Ogi 10 months ago
parent
commit
c3fafa1c26

+ 8 - 0
static/app/constants/index.tsx

@@ -318,6 +318,14 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Cron Monitors'),
     uid: 13,
   },
+  [DataCategoryExact.METRICS]: {
+    name: DataCategoryExact.METRICS,
+    apiName: 'metrics',
+    plural: 'metrics',
+    displayName: 'metrics',
+    titleName: t('Metrics'),
+    uid: 19,
+  },
 } as const satisfies Record<DataCategoryExact, DataCategoryInfo>;
 
 // Special Search characters

+ 1 - 0
static/app/types/core.tsx

@@ -97,6 +97,7 @@ export enum DataCategoryExact {
   TRANSACTION_INDEXED = 'transaction_indexed',
   MONITOR = 'monitor',
   MONITOR_SEAT = 'monitorSeat',
+  METRICS = 'metrics',
 }
 
 export interface DataCategoryInfo {

+ 6 - 0
static/app/utils/metrics/features.tsx

@@ -21,6 +21,12 @@ export function hasCustomMetrics(organization: Organization) {
   return hasMetricsUI(organization) && hasMetricsSidebarItem(organization);
 }
 
+export function hasMetricStats(organization: Organization) {
+  return (
+    hasCustomMetrics(organization) && organization.features.includes('metrics-stats')
+  );
+}
+
 /**
  * Returns the forceMetricsLayer query param for the alert
  * wrapped in an object so it can be spread into existing query params

+ 22 - 1
static/app/views/organizationStats/usageChart/index.tsx

@@ -1,3 +1,4 @@
+import {useMemo} from 'react';
 import {type Theme, useTheme} from '@emotion/react';
 import styled from '@emotion/styled';
 import Color from 'color';
@@ -22,7 +23,9 @@ import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {DataCategoryInfo, IntervalPeriod, SelectValue} from 'sentry/types/core';
 import {parsePeriodToHours, statsPeriodToDays} from 'sentry/utils/dates';
+import {hasCustomMetrics} from 'sentry/utils/metrics/features';
 import commonTheme from 'sentry/utils/theme';
+import useOrganization from 'sentry/utils/useOrganization';
 
 import {formatUsageWithUnits} from '../utils';
 
@@ -85,6 +88,12 @@ export const CHART_OPTIONS_DATACATEGORY: CategoryOption[] = [
     disabled: false,
     yAxisMinInterval: 100,
   },
+  {
+    label: DATA_CATEGORY_INFO.metrics.titleName,
+    value: DATA_CATEGORY_INFO.metrics.plural,
+    disabled: false,
+    yAxisMinInterval: 100,
+  },
 ];
 
 export enum ChartDataTransform {
@@ -348,6 +357,18 @@ function UsageChartBody({
   handleDataTransformation = cumulativeTotalDataTransformation,
 }: UsageChartProps) {
   const theme = useTheme();
+  const organization = useOrganization();
+
+  const filteredOptions = useMemo(() => {
+    return categoryOptions.filter(option => {
+      if (option.value !== DATA_CATEGORY_INFO.metrics.plural) {
+        return true;
+      }
+      return (
+        hasCustomMetrics(organization) && organization.features.includes('metrics-stats')
+      );
+    });
+  }, [organization, categoryOptions]);
 
   if (isLoading) {
     return (
@@ -379,7 +400,7 @@ function UsageChartBody({
     xAxisLabelInterval,
     yAxisMinInterval,
   } = chartMetadata({
-    categoryOptions,
+    categoryOptions: filteredOptions,
     dataCategory,
     handleDataTransformation: handleDataTransformation!,
     usageStats,

+ 53 - 5
static/app/views/organizationStats/usageStatsOrg.tsx

@@ -16,12 +16,13 @@ import ErrorBoundary from 'sentry/components/errorBoundary';
 import NotAvailable from 'sentry/components/notAvailable';
 import type {ScoreCardProps} from 'sentry/components/scoreCard';
 import ScoreCard from 'sentry/components/scoreCard';
-import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
+import {DATA_CATEGORY_INFO, DEFAULT_STATS_PERIOD} from 'sentry/constants';
 import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {DataCategoryInfo, IntervalPeriod, Organization} from 'sentry/types';
 import {Outcome} from 'sentry/types';
 import {parsePeriodToHours} from 'sentry/utils/dates';
+import {hasMetricStats} from 'sentry/utils/metrics/features';
 
 import {
   FORMAT_DATETIME_DAILY,
@@ -51,6 +52,7 @@ export interface UsageStatsOrganizationProps extends WithRouterProps {
 
 type UsageStatsOrganizationState = {
   orgStats: UsageSeries | undefined;
+  metricOrgStats?: UsageSeries | undefined;
 } & DeprecatedAsyncComponent['state'];
 
 /**
@@ -78,7 +80,10 @@ class UsageStatsOrganization<
   }
 
   getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
-    return [['orgStats', this.endpointPath, {query: this.endpointQuery}]];
+    return [
+      ['orgStats', this.endpointPath, {query: this.endpointQuery}],
+      ...this.metricsEndpoint,
+    ];
   }
 
   /** List of components to render on single-project view */
@@ -120,6 +125,51 @@ class UsageStatsOrganization<
     };
   }
 
+  // Metric stats are not reported when grouping by category, so we make a separate request
+  // and combine the results
+  get metricsEndpoint(): [string, string, {query: Record<string, any>}][] {
+    if (hasMetricStats(this.props.organization)) {
+      return [
+        [
+          'metricOrgStats',
+          this.endpointPath,
+          {
+            query: {
+              ...this.endpointQuery,
+              category: DATA_CATEGORY_INFO.metrics.apiName,
+              groupBy: ['outcome'],
+            },
+          },
+        ],
+      ];
+    }
+    return [];
+  }
+
+  // Combines non-metric and metric stats
+  get orgStats() {
+    const {orgStats, metricOrgStats} = this.state;
+
+    if (!orgStats || !metricOrgStats) {
+      return orgStats;
+    }
+
+    const metricsGroups = metricOrgStats.groups.map(group => {
+      return {
+        ...group,
+        by: {
+          ...group.by,
+          category: DATA_CATEGORY_INFO.metrics.apiName,
+        },
+      };
+    });
+
+    return {
+      ...orgStats,
+      groups: [...orgStats.groups, ...metricsGroups],
+    };
+  }
+
   get chartData(): {
     cardStats: {
       accepted?: string;
@@ -138,10 +188,8 @@ class UsageStatsOrganization<
     chartTransform: ChartDataTransform;
     dataError?: Error;
   } {
-    const {orgStats} = this.state;
-
     return {
-      ...this.mapSeriesToChart(orgStats),
+      ...this.mapSeriesToChart(this.orgStats),
       ...this.chartDateRange,
       ...this.chartTransform,
     };

+ 6 - 0
static/app/views/organizationStats/usageStatsPerMin.tsx

@@ -1,5 +1,6 @@
 import styled from '@emotion/styled';
 
+import {DATA_CATEGORY_INFO} from 'sentry/constants';
 import {t} from 'sentry/locale';
 import type {DataCategoryInfo, Organization} from 'sentry/types';
 import {Outcome} from 'sentry/types';
@@ -76,6 +77,11 @@ function UsageStatsPerMin({dataCategory, organization, projectIds}: Props) {
     );
   };
 
+  // Metrics stats ingestion is delayed, so we can't show this for metrics right now
+  if (dataCategory === DATA_CATEGORY_INFO.metrics.plural) {
+    return null;
+  }
+
   return (
     <Wrapper>
       {minuteData()} {t('in last min')}