Browse Source

ref(metrics): Update cardinality limit logic (#74271)

Priscila Oliveira 7 months ago
parent
commit
c0c89d29cd

+ 148 - 0
static/app/components/metrics/metricQuerySelect.spec.tsx

@@ -0,0 +1,148 @@
+import type React from 'react';
+
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import {MetricQuerySelect} from 'sentry/components/metrics/metricQuerySelect';
+import type {MetricsQueryApiResponse, PageFilters} from 'sentry/types';
+import {
+  useVirtualMetricsContext,
+  VirtualMetricsContextProvider,
+} from 'sentry/utils/metrics/virtualMetricsContext';
+import importedUsePageFilters from 'sentry/utils/usePageFilters';
+
+jest.mock('sentry/utils/usePageFilters');
+
+const usePageFilters = jest.mocked(importedUsePageFilters);
+
+const makeFilterProps = (
+  filters: Partial<PageFilters>
+): ReturnType<typeof importedUsePageFilters> => {
+  return {
+    isReady: true,
+    shouldPersist: true,
+    desyncedFilters: new Set(),
+    pinnedFilters: new Set(),
+    selection: {
+      projects: [1],
+      environments: ['prod'],
+      datetime: {start: new Date(), end: new Date(), period: '14d', utc: true},
+      ...filters,
+    },
+  };
+};
+
+const SELECTED_MRI = 'c:custom/span_attribute_66@none';
+
+function MetricQuerySelectWithMRI(
+  props: Omit<React.ComponentProps<typeof MetricQuerySelect>, 'mri'>
+) {
+  const {getVirtualMRI} = useVirtualMetricsContext();
+  const mri = getVirtualMRI(SELECTED_MRI);
+
+  if (!mri) {
+    return null;
+  }
+
+  return <MetricQuerySelect {...props} mri={mri} />;
+}
+
+function renderMockRequests({
+  orgSlug,
+  projectId,
+  metricsQueryApiResponse,
+}: {
+  orgSlug: string;
+  projectId: string;
+  metricsQueryApiResponse?: Partial<MetricsQueryApiResponse>;
+}) {
+  MockApiClient.addMockResponse({
+    url: `/organizations/${orgSlug}/metrics/query/`,
+    method: 'POST',
+    body: metricsQueryApiResponse ?? {
+      data: [
+        [
+          {
+            by: {
+              mri: SELECTED_MRI,
+            },
+            totals: 2703.0,
+          },
+        ],
+      ],
+      start: '2024-07-16T21:00:00Z',
+      end: '2024-07-17T22:00:00Z',
+    },
+  });
+  MockApiClient.addMockResponse({
+    url: `/organizations/${orgSlug}/metrics/extraction-rules/`,
+    method: 'GET',
+    body: [
+      {
+        spanAttribute: 'span.duration',
+        aggregates: ['count'],
+        unit: 'millisecond',
+        tags: ['browser.name'],
+        conditions: [
+          {
+            id: 66,
+            value: '',
+            mris: ['c:custom/span_attribute_66@none'],
+          },
+        ],
+        projectId,
+        createdById: 3242858,
+        dateAdded: '2024-07-17T07:06:33.253094Z',
+        dateUpdated: '2024-07-17T21:27:54.742586Z',
+      },
+    ],
+  });
+}
+
+describe('Metric Query Select', function () {
+  const {project, organization} = initializeOrg();
+
+  it('shall display cardinality limit warning', async function () {
+    renderMockRequests({orgSlug: organization.slug, projectId: project.id});
+
+    usePageFilters.mockImplementation(() =>
+      makeFilterProps({projects: [Number(project.id)]})
+    );
+
+    render(
+      <VirtualMetricsContextProvider>
+        <MetricQuerySelectWithMRI onChange={jest.fn()} conditionId={66} />
+      </VirtualMetricsContextProvider>
+    );
+
+    expect(
+      await screen.findByLabelText('Exceeding the cardinality limit warning')
+    ).toBeInTheDocument();
+  });
+
+  it('shall NOT display cardinality limit warning', async function () {
+    renderMockRequests({
+      orgSlug: organization.slug,
+      projectId: project.id,
+      metricsQueryApiResponse: {
+        data: [],
+      },
+    });
+
+    usePageFilters.mockImplementation(() =>
+      makeFilterProps({projects: [Number(project.id)]})
+    );
+
+    render(
+      <VirtualMetricsContextProvider>
+        <MetricQuerySelectWithMRI onChange={jest.fn()} conditionId={66} />
+      </VirtualMetricsContextProvider>
+    );
+
+    expect(await screen.findByText(/query/i)).toBeInTheDocument();
+
+    expect(
+      screen.queryByLabelText('Exceeding the cardinality limit warning')
+    ).not.toBeInTheDocument();
+  });
+});

+ 151 - 0
static/app/components/metrics/metricQuerySelect.tsx

@@ -0,0 +1,151 @@
+import styled from '@emotion/styled';
+
+import {Button} from 'sentry/components/button';
+import {CompactSelect} from 'sentry/components/compactSelect';
+import {Tooltip} from 'sentry/components/tooltip';
+import {IconAdd, IconInfo, IconWarning} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {MetricsExtractionCondition, MRI} from 'sentry/types/metrics';
+import {useCardinalityLimitedMetricVolume} from 'sentry/utils/metrics/useCardinalityLimitedMetricVolume';
+import {useVirtualMetricsContext} from 'sentry/utils/metrics/virtualMetricsContext';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import {useSelectedProjects} from 'sentry/views/metrics/utils/useSelectedProjects';
+import {openExtractionRuleEditModal} from 'sentry/views/settings/projectMetrics/metricsExtractionRuleEditModal';
+
+interface Props {
+  mri: MRI;
+  onChange: (conditionId: number) => void;
+  conditionId?: number;
+}
+
+export function MetricQuerySelect({onChange, conditionId, mri}: Props) {
+  const pageFilters = usePageFilters();
+  const {data: cardinality} = useCardinalityLimitedMetricVolume(pageFilters.selection);
+  const {getConditions} = useVirtualMetricsContext();
+
+  const isCardinalityLimited = (condition?: MetricsExtractionCondition): boolean => {
+    if (!cardinality || !condition) {
+      return false;
+    }
+    return condition.mris.some(conditionMri => cardinality[conditionMri] > 0);
+  };
+
+  const spanConditions = getConditions(mri);
+
+  return (
+    <CompactSelect
+      size="md"
+      triggerProps={{
+        prefix: t('Query'),
+        icon: isCardinalityLimited(spanConditions.find(c => c.id === conditionId)) ? (
+          <CardinalityWarningIcon />
+        ) : null,
+      }}
+      options={spanConditions.map(condition => ({
+        label: condition.value ? (
+          <Tooltip showOnlyOnOverflow title={condition.value} skipWrapper>
+            <QueryLabel>{condition.value}</QueryLabel>
+          </Tooltip>
+        ) : (
+          t('All spans')
+        ),
+        trailingItems: [
+          isCardinalityLimited(condition) ? (
+            <CardinalityWarningIcon key="cardinality-warning" />
+          ) : undefined,
+        ],
+        textValue: condition.value || t('All spans'),
+        value: condition.id,
+      }))}
+      value={conditionId}
+      onChange={({value}) => {
+        onChange(value);
+      }}
+      menuFooter={({closeOverlay}) => (
+        <QueryFooter mri={mri} closeOverlay={closeOverlay} />
+      )}
+    />
+  );
+}
+
+function CardinalityWarningIcon() {
+  return (
+    <Tooltip
+      isHoverable
+      title={t(
+        "This query is exeeding the cardinality limit. Remove tags or add more filters in the metric's settings to receive accurate data."
+      )}
+      skipWrapper
+    >
+      <IconWarning
+        size="xs"
+        color="yellow300"
+        role="image"
+        aria-label={t('Exceeding the cardinality limit warning')}
+      />
+    </Tooltip>
+  );
+}
+
+function QueryFooter({mri, closeOverlay}: {closeOverlay: () => void; mri: MRI}) {
+  const {getVirtualMeta, getExtractionRule} = useVirtualMetricsContext();
+  const selectedProjects = useSelectedProjects();
+
+  const metricMeta = getVirtualMeta(mri);
+  const project = selectedProjects.find(p => p.id === String(metricMeta.projectIds[0]));
+
+  if (!project) {
+    return null;
+  }
+  return (
+    <QueryFooterWrapper>
+      <Button
+        size="xs"
+        icon={<IconAdd isCircled />}
+        onClick={() => {
+          closeOverlay();
+          const extractionRule = getExtractionRule(mri);
+          if (!extractionRule) {
+            return;
+          }
+          openExtractionRuleEditModal({metricExtractionRule: extractionRule});
+        }}
+      >
+        {t('Add Query')}
+      </Button>
+      <InfoWrapper>
+        <Tooltip
+          title={t(
+            'Ideally, you can visualize span data by any property you want. However, our infrastructure has limits as well, so pretty please define in advance what you want to see.'
+          )}
+          skipWrapper
+        >
+          <IconInfo size="xs" />
+        </Tooltip>
+        {t('What are queries?')}
+      </InfoWrapper>
+    </QueryFooterWrapper>
+  );
+}
+
+const InfoWrapper = styled('div')`
+  display: flex;
+  align-items: center;
+  gap: ${space(0.5)};
+  font-size: ${p => p.theme.fontSizeExtraSmall};
+  color: ${p => p.theme.subText};
+`;
+
+const QueryFooterWrapper = styled('div')`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  min-width: 250px;
+`;
+
+const QueryLabel = styled('code')`
+  padding-left: 0;
+  max-width: 350px;
+  ${p => p.theme.overflowEllipsis}
+`;

+ 7 - 124
static/app/components/metrics/queryBuilder.tsx

@@ -3,30 +3,26 @@ import styled from '@emotion/styled';
 import uniqBy from 'lodash/uniqBy';
 
 import GuideAnchor from 'sentry/components/assistant/guideAnchor';
-import {Button} from 'sentry/components/button';
 import type {SelectOption} from 'sentry/components/compactSelect';
 import {CompactSelect} from 'sentry/components/compactSelect';
+import {MetricQuerySelect} from 'sentry/components/metrics/metricQuerySelect';
 import {MetricSearchBar} from 'sentry/components/metrics/metricSearchBar';
 import {MRISelect} from 'sentry/components/metrics/mriSelect';
 import {Tooltip} from 'sentry/components/tooltip';
-import {IconAdd, IconInfo, IconWarning} from 'sentry/icons';
+import {IconWarning} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import type {MetricsExtractionCondition, MRI} from 'sentry/types/metrics';
+import type {MRI} from 'sentry/types/metrics';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import {getDefaultAggregation, isAllowedAggregation} from 'sentry/utils/metrics';
-import {DEFAULT_METRICS_CARDINALITY_LIMIT} from 'sentry/utils/metrics/constants';
 import {parseMRI} from 'sentry/utils/metrics/mri';
 import type {MetricsQuery} from 'sentry/utils/metrics/types';
 import {useIncrementQueryMetric} from 'sentry/utils/metrics/useIncrementQueryMetric';
-import {useMetricsCardinality} from 'sentry/utils/metrics/useMetricsCardinality';
 import {useVirtualizedMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
 import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags';
 import {useVirtualMetricsContext} from 'sentry/utils/metrics/virtualMetricsContext';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
-import {useSelectedProjects} from 'sentry/views/metrics/utils/useSelectedProjects';
-import {openExtractionRuleEditModal} from 'sentry/views/settings/projectMetrics/metricsExtractionRuleEditModal';
 
 type QueryBuilderProps = {
   index: number;
@@ -45,7 +41,6 @@ export const QueryBuilder = memo(function QueryBuilder({
   const pageFilters = usePageFilters();
   const {getConditions, getVirtualMeta, resolveVirtualMRI, getTags} =
     useVirtualMetricsContext();
-  const {data: cardinality} = useMetricsCardinality(pageFilters.selection);
 
   const {
     data: meta,
@@ -212,14 +207,6 @@ export const QueryBuilder = memo(function QueryBuilder({
   );
 
   const projectIdStrings = useMemo(() => projectIds.map(String), [projectIds]);
-  const spanConditions = getConditions(metricsQuery.mri);
-
-  const getMaxCardinality = (condition?: MetricsExtractionCondition) => {
-    if (!cardinality || !condition) {
-      return 0;
-    }
-    return condition.mris.reduce((acc, mri) => Math.max(acc, cardinality[mri] || 0), 0);
-  };
 
   return (
     <QueryBuilderWrapper>
@@ -237,40 +224,12 @@ export const QueryBuilder = memo(function QueryBuilder({
             />
           </GuideAnchor>
           {selectedMeta?.type === 'v' ? (
-            <CompactSelect
-              size="md"
-              triggerProps={{
-                prefix: t('Filter'),
-                icon:
-                  getMaxCardinality(
-                    spanConditions.find(c => c.id === metricsQuery.condition)
-                  ) > DEFAULT_METRICS_CARDINALITY_LIMIT ? (
-                    <CardinalityWarningIcon />
-                  ) : null,
-              }}
-              options={spanConditions.map(condition => ({
-                label: condition.value ? (
-                  <Tooltip showOnlyOnOverflow title={condition.value} skipWrapper>
-                    <QueryLabel>{condition.value}</QueryLabel>
-                  </Tooltip>
-                ) : (
-                  t('All spans')
-                ),
-                trailingItems: [
-                  getMaxCardinality(condition) > DEFAULT_METRICS_CARDINALITY_LIMIT ? (
-                    <CardinalityWarningIcon key="cardinality-warning" />
-                  ) : undefined,
-                ],
-                textValue: condition.value || t('All spans'),
-                value: condition.id,
-              }))}
-              value={metricsQuery.condition}
-              onChange={({value}) => {
+            <MetricQuerySelect
+              mri={metricsQuery.mri}
+              conditionId={metricsQuery.condition}
+              onChange={value => {
                 onChange({condition: value});
               }}
-              menuFooter={({closeOverlay}) => (
-                <QueryFooter mri={metricsQuery.mri} closeOverlay={closeOverlay} />
-              )}
             />
           ) : null}
         </FlexBlock>
@@ -333,20 +292,6 @@ export const QueryBuilder = memo(function QueryBuilder({
   );
 });
 
-function CardinalityWarningIcon() {
-  return (
-    <Tooltip
-      isHoverable
-      title={t(
-        "This query is exeeding the cardinality limit. Remove tags or add more filters in the metric's settings to receive accurate data."
-      )}
-      skipWrapper
-    >
-      <IconWarning size="xs" color="yellow300" />
-    </Tooltip>
-  );
-}
-
 function TagWarningIcon() {
   return (
     <TooltipIconWrapper>
@@ -359,47 +304,6 @@ function TagWarningIcon() {
   );
 }
 
-function QueryFooter({mri, closeOverlay}) {
-  const {getVirtualMeta, getExtractionRule} = useVirtualMetricsContext();
-  const selectedProjects = useSelectedProjects();
-
-  const metricMeta = getVirtualMeta(mri);
-  const project = selectedProjects.find(p => p.id === String(metricMeta.projectIds[0]));
-
-  if (!project) {
-    return null;
-  }
-  return (
-    <QueryFooterWrapper>
-      <Button
-        size="xs"
-        icon={<IconAdd isCircled />}
-        onClick={() => {
-          closeOverlay();
-          const extractionRule = getExtractionRule(mri);
-          if (!extractionRule) {
-            return;
-          }
-          openExtractionRuleEditModal({metricExtractionRule: extractionRule});
-        }}
-      >
-        {t('Add Filter')}
-      </Button>
-      <InfoWrapper>
-        <Tooltip
-          title={t(
-            'Ideally, you can visualize span data by any property you want. However, our infrastructure has limits as well, so pretty please define in advance what you want to see.'
-          )}
-          skipWrapper
-        >
-          <IconInfo size="xs" />
-        </Tooltip>
-        {t('What are filters?')}
-      </InfoWrapper>
-    </QueryFooterWrapper>
-  );
-}
-
 const TooltipIconWrapper = styled('span')`
   margin-top: ${space(0.25)};
 `;
@@ -429,24 +333,3 @@ const SearchBarWrapper = styled('div')`
   flex: 1;
   min-width: 200px;
 `;
-
-const QueryLabel = styled('code')`
-  padding-left: 0;
-  max-width: 350px;
-  ${p => p.theme.overflowEllipsis}
-`;
-
-const InfoWrapper = styled('div')`
-  display: flex;
-  align-items: center;
-  gap: ${space(0.5)};
-  font-size: ${p => p.theme.fontSizeExtraSmall};
-  color: ${p => p.theme.subText};
-`;
-
-const QueryFooterWrapper = styled('div')`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  min-width: 250px;
-`;

+ 0 - 2
static/app/utils/metrics/constants.tsx

@@ -13,8 +13,6 @@ import {
 
 export const METRICS_DOCS_URL = 'https://docs.sentry.io/product/metrics/';
 
-export const DEFAULT_METRICS_CARDINALITY_LIMIT = 140000;
-
 export const metricDisplayTypeOptions = [
   {
     value: MetricDisplayType.LINE,

+ 4 - 4
static/app/utils/metrics/useMetricsCardinality.tsx → static/app/utils/metrics/useCardinalityLimitedMetricVolume.tsx

@@ -10,10 +10,10 @@ type Props = {
 const CARDINALITY_QUERIES = [
   {
     name: 'a',
-    mri: 'g:metric_stats/cardinality@none',
-    aggregation: 'max',
+    mri: 'c:metric_stats/volume@none',
+    aggregation: 'sum',
     groupBy: ['mri'],
-    query: '!mri:"" cardinality.window:3600',
+    query: '!mri:"" outcome.id:6',
     orderBy: 'desc' as 'desc' | 'asc',
   } as MetricsQueryApiQueryParams,
 ];
@@ -27,7 +27,7 @@ const CARDINALITY_DATE_TIME = {
 
 const CARDINALITY_INTERVAL = '1h';
 
-export function useMetricsCardinality({projects}: Props) {
+export function useCardinalityLimitedMetricVolume({projects}: Props) {
   const cardinalityQuery = useMetricsQuery(
     CARDINALITY_QUERIES,
     {

+ 22 - 28
static/app/views/settings/projectMetrics/customMetricsTable.tsx

@@ -9,16 +9,15 @@ import {TabList, TabPanels, Tabs} from 'sentry/components/tabs';
 import {Tooltip} from 'sentry/components/tooltip';
 import {IconArrow} from 'sentry/icons';
 import {IconWarning} from 'sentry/icons/iconWarning';
-import {t, tct} from 'sentry/locale';
+import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {MetricMeta} from 'sentry/types/metrics';
 import type {Project} from 'sentry/types/project';
-import {DEFAULT_METRICS_CARDINALITY_LIMIT} from 'sentry/utils/metrics/constants';
 import {hasCustomMetricsExtractionRules} from 'sentry/utils/metrics/features';
 import {getReadableMetricType} from 'sentry/utils/metrics/formatters';
 import {formatMRI, isExtractedCustomMetric} from 'sentry/utils/metrics/mri';
 import {useBlockMetric} from 'sentry/utils/metrics/useBlockMetric';
-import {useMetricsCardinality} from 'sentry/utils/metrics/useMetricsCardinality';
+import {useCardinalityLimitedMetricVolume} from 'sentry/utils/metrics/useCardinalityLimitedMetricVolume';
 import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
 import {middleEllipsis} from 'sentry/utils/string/middleEllipsis';
 import useOrganization from 'sentry/utils/useOrganization';
@@ -48,7 +47,7 @@ export function CustomMetricsTable({project}: Props) {
     false
   );
 
-  const metricsCardinality = useMetricsCardinality({
+  const metricsCardinality = useCardinalityLimitedMetricVolume({
     projects: [parseInt(project.id, 10)],
   });
 
@@ -61,7 +60,6 @@ export function CustomMetricsTable({project}: Props) {
 
     // Do not show internal extracted metrics in this table
     const filteredMeta = metricsMeta.data.filter(meta => !isExtractedCustomMetric(meta));
-
     if (!metricsCardinality.data) {
       return filteredMeta.map(meta => ({...meta, cardinality: 0}));
     }
@@ -75,7 +73,12 @@ export function CustomMetricsTable({project}: Props) {
         };
       })
       .sort((a, b) => {
-        return b.cardinality - a.cardinality;
+        // First sort by cardinality (descending)
+        if (b.cardinality !== a.cardinality) {
+          return b.cardinality - a.cardinality;
+        }
+        // If cardinality is the same, sort by name (ascending)
+        return a.mri.localeCompare(b.mri);
       }) as MetricWithCardinality[];
   }, [metricsCardinality.data, metricsMeta.data]);
 
@@ -149,18 +152,13 @@ interface MetricsTableProps {
 function MetricsTable({metrics, isLoading, query, project}: MetricsTableProps) {
   const blockMetricMutation = useBlockMetric(project);
   const {hasAccess} = useAccess({access: ['project:write'], project});
-  const cardinalityLimit =
-    // Retrive limit from BE
-    project.relayCustomMetricCardinalityLimit ?? DEFAULT_METRICS_CARDINALITY_LIMIT;
 
   return (
     <MetricsPanelTable
       headers={[
-        t('Metric'),
-        <Cell right key="cardinality">
+        <Cell key="metric">
           <IconArrow size="xs" direction="down" />
-
-          {t('Cardinality')}
+          {t('Metric')}
         </Cell>,
         <Cell right key="type">
           {t('Type')}
@@ -182,10 +180,19 @@ function MetricsTable({metrics, isLoading, query, project}: MetricsTableProps) {
     >
       {metrics.map(({mri, type, unit, cardinality, blockingStatus}) => {
         const isBlocked = blockingStatus[0]?.isBlocked;
-        const isCardinalityLimited = cardinality >= cardinalityLimit;
+        const isCardinalityLimited = cardinality > 0;
         return (
           <Fragment key={mri}>
             <Cell>
+              {isCardinalityLimited && (
+                <Tooltip
+                  title={t(
+                    'The tag cardinality of this metric exceeded the limit, causing the data to be dropped.'
+                  )}
+                >
+                  <StyledIconWarning size="sm" color="yellow300" />
+                </Tooltip>
+              )}
               <Link
                 to={`/settings/projects/${project.slug}/metrics/${encodeURIComponent(
                   mri
@@ -194,19 +201,6 @@ function MetricsTable({metrics, isLoading, query, project}: MetricsTableProps) {
                 {middleEllipsis(formatMRI(mri), 65, /\.|-|_/)}
               </Link>
             </Cell>
-            <Cell right>
-              {isCardinalityLimited && (
-                <Tooltip
-                  title={tct(
-                    'The tag cardinality of this metric exceeded our limit of [cardinalityLimit], which led to the data being dropped',
-                    {cardinalityLimit}
-                  )}
-                >
-                  <StyledIconWarning size="sm" color="red300" />
-                </Tooltip>
-              )}
-              {cardinality}
-            </Cell>
             <Cell right>
               <Tag>{getReadableMetricType(type)}</Tag>
             </Cell>
@@ -250,7 +244,7 @@ const SearchWrapper = styled('div')`
 
 const MetricsPanelTable = styled(PanelTable)`
   margin-top: ${space(2)};
-  grid-template-columns: 1fr repeat(4, min-content);
+  grid-template-columns: 1fr repeat(3, min-content);
 `;
 
 const Cell = styled('div')<{right?: boolean}>`

+ 2 - 2
static/app/views/settings/projectMetrics/metricsExtractionRuleCreateModal.tsx

@@ -14,7 +14,7 @@ import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {MetricsExtractionRule} from 'sentry/types/metrics';
 import type {Project} from 'sentry/types/project';
-import {useMetricsCardinality} from 'sentry/utils/metrics/useMetricsCardinality';
+import {useCardinalityLimitedMetricVolume} from 'sentry/utils/metrics/useCardinalityLimitedMetricVolume';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
 import useProjects from 'sentry/utils/useProjects';
@@ -152,7 +152,7 @@ function FormWrapper({
     projectId
   );
 
-  const {data: cardinality} = useMetricsCardinality({
+  const {data: cardinality} = useCardinalityLimitedMetricVolume({
     projects: [projectId],
   });
 

+ 2 - 2
static/app/views/settings/projectMetrics/metricsExtractionRuleEditModal.tsx

@@ -9,7 +9,7 @@ import {
 } from 'sentry/actionCreators/modal';
 import {t} from 'sentry/locale';
 import type {MetricsExtractionRule} from 'sentry/types/metrics';
-import {useMetricsCardinality} from 'sentry/utils/metrics/useMetricsCardinality';
+import {useCardinalityLimitedMetricVolume} from 'sentry/utils/metrics/useCardinalityLimitedMetricVolume';
 import useOrganization from 'sentry/utils/useOrganization';
 import {
   aggregatesToGroups,
@@ -39,7 +39,7 @@ export function MetricsExtractionRuleEditModal({
     metricExtractionRule.projectId
   );
 
-  const {data: cardinality} = useMetricsCardinality({
+  const {data: cardinality} = useCardinalityLimitedMetricVolume({
     projects: [metricExtractionRule.projectId],
   });
 

+ 6 - 11
static/app/views/settings/projectMetrics/metricsExtractionRuleForm.tsx

@@ -15,7 +15,6 @@ import {space} from 'sentry/styles/space';
 import type {SelectValue} from 'sentry/types/core';
 import type {MetricAggregation, MetricsExtractionCondition} from 'sentry/types/metrics';
 import {DiscoverDatasets} from 'sentry/utils/discover/types';
-import {DEFAULT_METRICS_CARDINALITY_LIMIT} from 'sentry/utils/metrics/constants';
 import useOrganization from 'sentry/utils/useOrganization';
 import {SpanIndexedField} from 'sentry/views/insights/types';
 import {useSpanFieldSupportedTags} from 'sentry/views/performance/utils/useSpanFieldSupportedTags';
@@ -406,24 +405,20 @@ export function MetricsExtractionRuleForm({
                 );
               };
 
-              const getMaxCardinality = (condition: MetricsExtractionCondition) => {
+              const isCardinalityLimited = (
+                condition: MetricsExtractionCondition
+              ): boolean => {
                 if (!cardinality) {
-                  return 0;
+                  return false;
                 }
-                return condition.mris.reduce(
-                  (acc, mri) => Math.max(acc, cardinality[mri] || 0),
-                  0
-                );
+                return condition.mris.some(conditionMri => cardinality[conditionMri] > 0);
               };
 
               return (
                 <Fragment>
                   <ConditionsWrapper hasDelete={value.length > 1}>
                     {conditions.map((condition, index) => {
-                      const maxCardinality = getMaxCardinality(condition);
-                      const isExeedingCardinalityLimit =
-                        // TODO: Retrieve limit from BE
-                        maxCardinality >= DEFAULT_METRICS_CARDINALITY_LIMIT;
+                      const isExeedingCardinalityLimit = isCardinalityLimited(condition);
                       const hasSiblings = conditions.length > 1;
 
                       return (

+ 128 - 55
static/app/views/settings/projectMetrics/metricsExtractionRulesTable.spec.tsx

@@ -4,75 +4,148 @@ import {
   renderGlobalModal,
   screen,
   userEvent,
+  waitForElementToBeRemoved,
 } from 'sentry-test/reactTestingLibrary';
 
-import type {MetricsExtractionRule} from 'sentry/types/metrics';
+import type {MetricsQueryApiResponse} from 'sentry/types';
+import {MetricsExtractionRulesTable} from 'sentry/views/settings/projectMetrics/metricsExtractionRulesTable';
 
-import {MetricsExtractionRulesTable} from './metricsExtractionRulesTable';
-
-describe('Metrics Extraction Rules Table', function () {
-  const {project, organization} = initializeOrg();
-
-  beforeEach(function () {
-    MockApiClient.addMockResponse({
-      url: `/projects/${organization.slug}/${project.id}/metrics/extraction-rules/`,
-      method: 'GET',
-      body: [
-        {
-          spanAttribute: 'span.duration',
-          projectId: Number(project.id),
-          createdById: null,
-          dateAdded: '2021-09-29T20:00:00',
-          dateUpdated: '2021-09-29T20:00:00',
-          aggregates: [
-            'count',
-            'count_unique',
-            'min',
-            'max',
-            'sum',
-            'avg',
-            'p50',
-            'p75',
-            'p95',
-            'p99',
-          ],
-          unit: 'millisecond',
-          tags: ['release', 'environment', 'sdk.name', 'span.op'],
-          conditions: [
-            {
-              id: 1,
-              value: '',
-              mris: [
-                'g:custom/span_attribute_1@millisecond',
-                's:custom/span_attribute_1@millisecond',
-                'd:custom/span_attribute_1@millisecond',
-                'c:custom/span_attribute_1@millisecond',
-              ],
+function renderMockRequests({
+  orgSlug,
+  projectId,
+  metricsQueryApiResponse,
+}: {
+  orgSlug: string;
+  projectId: string;
+  metricsQueryApiResponse?: Partial<MetricsQueryApiResponse>;
+}) {
+  MockApiClient.addMockResponse({
+    url: `/projects/${orgSlug}/${projectId}/metrics/extraction-rules/`,
+    method: 'GET',
+    body: [
+      {
+        spanAttribute: 'span.duration',
+        aggregates: [
+          'count',
+          'count_unique',
+          'min',
+          'max',
+          'sum',
+          'avg',
+          'p50',
+          'p75',
+          'p95',
+          'p99',
+        ],
+        unit: 'millisecond',
+        tags: ['browser.name'],
+        conditions: [
+          {
+            id: 66,
+            value: '',
+            mris: [
+              'c:custom/span_attribute_66@millisecond',
+              's:custom/span_attribute_66@millisecond',
+              'd:custom/span_attribute_66@millisecond',
+              'g:custom/span_attribute_66@millisecond',
+            ],
+          },
+        ],
+        projectId,
+        createdById: 3242858,
+        dateAdded: '2024-07-17T07:06:33.253094Z',
+        dateUpdated: '2024-07-17T21:27:54.742586Z',
+      },
+      {
+        spanAttribute: 'browser.name',
+        aggregates: ['count'],
+        unit: 'none',
+        tags: ['release'],
+        conditions: [
+          {
+            id: 67,
+            value: '',
+            mris: [
+              'c:custom/span_attribute_67@none',
+              's:custom/span_attribute_67@none',
+              'd:custom/span_attribute_67@none',
+              'g:custom/span_attribute_67@none',
+            ],
+          },
+        ],
+        projectId,
+        createdById: 588685,
+        dateAdded: '2024-07-17T21:32:15.297483Z',
+        dateUpdated: '2024-07-17T21:33:41.060903Z',
+      },
+    ],
+  });
+  MockApiClient.addMockResponse({
+    url: `/organizations/${orgSlug}/spans/fields/`,
+    body: [],
+  });
+  MockApiClient.addMockResponse({
+    url: `/organizations/${orgSlug}/metrics/query/`,
+    method: 'POST',
+    body: metricsQueryApiResponse ?? {
+      data: [
+        [
+          {
+            by: {
+              mri: 'c:custom/span_attribute_67@none',
             },
-          ],
-        } satisfies MetricsExtractionRule,
+            totals: 2703.0,
+          },
+        ],
       ],
-    });
-    MockApiClient.addMockResponse({
-      url: `/organizations/${organization.slug}/spans/fields/`,
-      body: [],
-    });
-    MockApiClient.addMockResponse({
-      url: `/organizations/${organization.slug}/metrics/query/`,
-      method: 'POST',
-      body: {data: []},
-    });
+      start: '2024-07-16T21:00:00Z',
+      end: '2024-07-17T22:00:00Z',
+    },
   });
+}
+
+describe('Metrics Extraction Rules Table', function () {
+  const {project, organization} = initializeOrg();
 
   it('shall open the modal to edit a rule by clicking on edit', async function () {
+    renderMockRequests({orgSlug: organization.slug, projectId: project.id});
+
     render(<MetricsExtractionRulesTable project={project} />);
     renderGlobalModal();
 
-    const editButton = await screen.findByLabelText('Edit metric');
-    await userEvent.click(editButton);
+    const editButtons = await screen.findAllByLabelText('Edit metric');
+    await userEvent.click(editButtons[1]);
 
     expect(
       await screen.findByRole('heading', {name: /span.duration/})
     ).toBeInTheDocument();
   });
+
+  it('shall display cardinality limit warning', async function () {
+    renderMockRequests({orgSlug: organization.slug, projectId: project.id});
+
+    render(<MetricsExtractionRulesTable project={project} />);
+
+    expect(
+      await screen.findByLabelText('Exceeding the cardinality limit warning')
+    ).toBeInTheDocument();
+  });
+
+  it('shall NOT display cardinality limit warning', async function () {
+    renderMockRequests({
+      orgSlug: organization.slug,
+      projectId: project.id,
+      metricsQueryApiResponse: {
+        data: [],
+      },
+    });
+
+    render(<MetricsExtractionRulesTable project={project} />);
+
+    await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
+
+    expect(
+      screen.queryByLabelText('Exceeding the cardinality limit warning')
+    ).not.toBeInTheDocument();
+  });
 });

Some files were not shown because too many files changed in this diff