Skip to content

Commit 86a3e84

Browse files
authored
feat(metrics): settings cardinality indicator (#72207)
1 parent 557fa29 commit 86a3e84

File tree

7 files changed

+164
-25
lines changed

7 files changed

+164
-25
lines changed

static/app/types/metrics.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type MetricsAggregate =
1313

1414
export type MetricType = 'c' | 'd' | 'g' | 'e' | 's';
1515

16-
export type UseCase = 'custom' | 'transactions' | 'sessions' | 'spans';
16+
export type UseCase = 'custom' | 'transactions' | 'sessions' | 'spans' | 'metric_stats';
1717

1818
export type MRI = `${MetricType}:${UseCase}${string}@${string}`;
1919

static/app/utils/metrics/constants.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313

1414
export const METRICS_DOCS_URL = 'https://docs.sentry.io/product/metrics/';
1515

16+
export const DEFAULT_METRICS_CARDINALITY_LIMIT = 5000;
17+
1618
export const metricDisplayTypeOptions = [
1719
{
1820
value: MetricDisplayType.LINE,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type {MRI} from 'sentry/types';
2+
import type {Project} from 'sentry/types/project';
3+
import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery';
4+
5+
type Props = {
6+
project: Project;
7+
};
8+
9+
const CARDINALITY_QUERIES = [
10+
{
11+
name: 'a',
12+
mri: 'g:metric_stats/cardinality@none' as MRI,
13+
op: 'max',
14+
groupBy: ['mri'],
15+
query: '!mri:"" cardinality.window:3600',
16+
orderBy: 'desc' as 'desc' | 'asc',
17+
},
18+
];
19+
20+
const CARDINALITY_DATE_TIME = {
21+
period: '1h',
22+
start: null,
23+
end: null,
24+
utc: null,
25+
};
26+
27+
const CARDINALITY_INTERVAL = '1h';
28+
29+
export function useMetricsCardinality({project}: Props) {
30+
const cardinalityQuery = useMetricsQuery(
31+
CARDINALITY_QUERIES,
32+
{
33+
environments: [],
34+
datetime: CARDINALITY_DATE_TIME,
35+
projects: [parseInt(project.id, 10)],
36+
},
37+
{interval: CARDINALITY_INTERVAL, includeSeries: false}
38+
);
39+
40+
if (cardinalityQuery.data?.data[0]) {
41+
const data = cardinalityQuery.data.data[0].reduce(
42+
(acc, group) => {
43+
acc[group.by.mri] = group.totals;
44+
return acc;
45+
},
46+
{} as Record<string, number>
47+
);
48+
return {...cardinalityQuery, data};
49+
}
50+
51+
return {...cardinalityQuery, data: undefined};
52+
}

static/app/utils/metrics/useMetricsQuery.spec.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ describe('getMetricsQueryApiRequestPayload', () => {
6262
end: '2023-01-31T00:00:00.000Z',
6363
project: [1],
6464
environment: ['production'],
65+
includeSeries: true,
6566
interval: '2h',
6667
});
6768

@@ -96,6 +97,7 @@ describe('getMetricsQueryApiRequestPayload', () => {
9697
statsPeriod: '7d',
9798
project: [1],
9899
environment: ['production'],
100+
includeSeries: true,
99101
interval: '30m',
100102
});
101103

@@ -126,13 +128,15 @@ describe('getMetricsQueryApiRequestPayload', () => {
126128

127129
const result = getMetricsQueryApiRequestPayload([metric], filters, {
128130
interval: '123m',
131+
includeSeries: false,
129132
});
130133

131134
expect(result.query).toEqual({
132135
start: '2023-01-01T00:00:00.000Z',
133136
end: '2023-01-02T00:00:00.000Z',
134137
project: [1],
135138
environment: ['production'],
139+
includeSeries: false,
136140
interval: '123m',
137141
});
138142

@@ -169,6 +173,7 @@ describe('getMetricsQueryApiRequestPayload', () => {
169173
end: '2023-01-02T00:00:00.000Z',
170174
project: [1],
171175
environment: ['production'],
176+
includeSeries: true,
172177
interval: '5m',
173178
});
174179

@@ -204,6 +209,7 @@ describe('getMetricsQueryApiRequestPayload', () => {
204209
end: '2023-01-02T00:00:00.000Z',
205210
project: [1],
206211
environment: ['production'],
212+
includeSeries: true,
207213
interval: '5m',
208214
});
209215

@@ -241,6 +247,7 @@ describe('getMetricsQueryApiRequestPayload', () => {
241247
end: '2023-01-02T00:00:00.000Z',
242248
project: [1],
243249
environment: ['production'],
250+
includeSeries: true,
244251
interval: '5m',
245252
});
246253

static/app/utils/metrics/useMetricsQuery.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ export function getMetricsQueryApiRequestPayload(
7777
{
7878
intervalLadder,
7979
interval: intervalParam,
80+
includeSeries = true,
8081
}: {
81-
autoOrder?: boolean;
82+
includeSeries?: boolean;
8283
interval?: string;
8384
intervalLadder?: MetricsDataIntervalLadder;
8485
} = {}
@@ -151,6 +152,7 @@ export function getMetricsQueryApiRequestPayload(
151152
project: projects,
152153
environment: environments,
153154
interval,
155+
includeSeries,
154156
},
155157
body: {
156158
queries: requestQueries,
@@ -162,7 +164,11 @@ export function getMetricsQueryApiRequestPayload(
162164
export function useMetricsQuery(
163165
queries: MetricsQueryApiQueryParams[],
164166
{projects, environments, datetime}: PageFilters,
165-
overrides: {interval?: string; intervalLadder?: MetricsDataIntervalLadder} = {},
167+
overrides: {
168+
includeSeries?: boolean;
169+
interval?: string;
170+
intervalLadder?: MetricsDataIntervalLadder;
171+
} = {},
166172
enableRefetch = true
167173
) {
168174
const organization = useOrganization();

static/app/views/settings/projectMetrics/cardinalityLimit.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@ import {t} from 'sentry/locale';
1010
import type {Project} from 'sentry/types/project';
1111
import useOrganization from 'sentry/utils/useOrganization';
1212

13+
import {DEFAULT_METRICS_CARDINALITY_LIMIT} from '../../../utils/metrics/constants';
14+
1315
type Props = {
1416
project: Project;
1517
};
1618

17-
const DEFAULT_LIMIT = '5000';
18-
1919
function transformData(data) {
2020
const limit = data.relayCustomMetricCardinalityLimit;
2121
return {
2222
relayCustomMetricCardinalityLimit:
23-
limit === '' || limit === DEFAULT_LIMIT ? null : limit,
23+
limit === '' || limit === DEFAULT_METRICS_CARDINALITY_LIMIT.toString()
24+
? null
25+
: limit,
2426
};
2527
}
2628

@@ -35,7 +37,8 @@ export function CardinalityLimit({project}: Props) {
3537
saveOnBlur
3638
initialData={{
3739
relayCustomMetricCardinalityLimit:
38-
project.relayCustomMetricCardinalityLimit ?? DEFAULT_LIMIT,
40+
project.relayCustomMetricCardinalityLimit ??
41+
DEFAULT_METRICS_CARDINALITY_LIMIT.toString(),
3942
}}
4043
>
4144
<Panel>

static/app/views/settings/projectMetrics/projectMetrics.tsx

Lines changed: 87 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,27 @@ import {PanelTable} from 'sentry/components/panels/panelTable';
1212
import SearchBar from 'sentry/components/searchBar';
1313
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
1414
import {TabList, TabPanels, Tabs} from 'sentry/components/tabs';
15+
import {Tooltip} from 'sentry/components/tooltip';
1516
import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
17+
import {IconArrow, IconWarning} from 'sentry/icons';
1618
import {t, tct} from 'sentry/locale';
1719
import {space} from 'sentry/styles/space';
18-
import type {MetricMeta, Organization, Project} from 'sentry/types';
19-
import {browserHistory} from 'sentry/utils/browserHistory';
20-
import {METRICS_DOCS_URL} from 'sentry/utils/metrics/constants';
20+
import type {MetricMeta} from 'sentry/types/metrics';
21+
import type {Organization} from 'sentry/types/organization';
22+
import type {Project} from 'sentry/types/project';
23+
import {
24+
DEFAULT_METRICS_CARDINALITY_LIMIT,
25+
METRICS_DOCS_URL,
26+
} from 'sentry/utils/metrics/constants';
2127
import {getReadableMetricType} from 'sentry/utils/metrics/formatters';
2228
import {formatMRI} from 'sentry/utils/metrics/mri';
2329
import {useBlockMetric} from 'sentry/utils/metrics/useBlockMetric';
30+
import {useMetricsCardinality} from 'sentry/utils/metrics/useMetricsCardinality';
2431
import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
2532
import {decodeScalar} from 'sentry/utils/queryString';
2633
import routeTitleGen from 'sentry/utils/routeTitle';
2734
import {middleEllipsis} from 'sentry/utils/string/middleEllipsis';
35+
import {useNavigate} from 'sentry/utils/useNavigate';
2836
import {useMetricsOnboardingSidebar} from 'sentry/views/metrics/ddmOnboarding/useMetricsOnboardingSidebar';
2937
import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
3038
import TextBlock from 'sentry/views/settings/components/text/textBlock';
@@ -43,35 +51,68 @@ enum BlockingStatusTab {
4351
DISABLED = 'disabled',
4452
}
4553

54+
type MetricWithCardinality = MetricMeta & {cardinality: number};
55+
4656
function ProjectMetrics({project, location}: Props) {
47-
const {data: meta, isLoading} = useMetricsMeta(
57+
const metricsMeta = useMetricsMeta(
4858
{projects: [parseInt(project.id, 10)]},
4959
['custom'],
5060
false
5161
);
62+
63+
const metricsCardinality = useMetricsCardinality({
64+
project,
65+
});
66+
67+
const sortedMeta = useMemo(() => {
68+
if (!metricsMeta.data) {
69+
return [];
70+
}
71+
72+
if (!metricsCardinality.data) {
73+
return metricsMeta.data.map(meta => ({...meta, cardinality: 0}));
74+
}
75+
76+
return metricsMeta.data
77+
.map(({mri, ...rest}) => {
78+
return {
79+
mri,
80+
cardinality: metricsCardinality.data[mri] ?? 0,
81+
...rest,
82+
};
83+
})
84+
.sort((a, b) => {
85+
return b.cardinality - a.cardinality;
86+
}) as MetricWithCardinality[];
87+
}, [metricsCardinality.data, metricsMeta.data]);
88+
5289
const query = decodeScalar(location.query.query, '').trim();
53-
const {activateSidebar} = useMetricsOnboardingSidebar();
54-
const [selectedTab, setSelectedTab] = useState(BlockingStatusTab.ACTIVE);
5590

91+
const metrics = sortedMeta.filter(
92+
({mri, type, unit}) =>
93+
mri.includes(query) ||
94+
getReadableMetricType(type).includes(query) ||
95+
unit.includes(query)
96+
);
97+
98+
const isLoading = metricsMeta.isLoading || metricsCardinality.isLoading;
99+
100+
const navigate = useNavigate();
56101
const debouncedSearch = useMemo(
57102
() =>
58103
debounce(
59104
(searchQuery: string) =>
60-
browserHistory.replace({
105+
navigate({
61106
pathname: location.pathname,
62107
query: {...location.query, query: searchQuery},
63108
}),
64109
DEFAULT_DEBOUNCE_DURATION
65110
),
66-
[location.pathname, location.query]
111+
[location.pathname, location.query, navigate]
67112
);
68113

69-
const metrics = meta.filter(
70-
({mri, type, unit}) =>
71-
mri.includes(query) ||
72-
getReadableMetricType(type).includes(query) ||
73-
unit.includes(query)
74-
);
114+
const {activateSidebar} = useMetricsOnboardingSidebar();
115+
const [selectedTab, setSelectedTab] = useState(BlockingStatusTab.ACTIVE);
75116

76117
return (
77118
<Fragment>
@@ -151,21 +192,27 @@ function ProjectMetrics({project, location}: Props) {
151192

152193
interface MetricsTableProps {
153194
isLoading: boolean;
154-
metrics: MetricMeta[];
195+
metrics: MetricWithCardinality[];
155196
project: Project;
156197
query: string;
157198
}
158199

159200
function MetricsTable({metrics, isLoading, query, project}: MetricsTableProps) {
160201
const blockMetricMutation = useBlockMetric(project);
161202
const {hasAccess} = useAccess({access: ['project:write']});
203+
const cardinalityLimit =
204+
project.relayCustomMetricCardinalityLimit ?? DEFAULT_METRICS_CARDINALITY_LIMIT;
162205

163206
return (
164207
<StyledPanelTable
165208
headers={[
166209
t('Metric'),
210+
<Cell right key="cardinality">
211+
<IconArrow size="xs" direction="down" />
212+
213+
{t('Cardinality')}
214+
</Cell>,
167215
<Cell right key="type">
168-
{' '}
169216
{t('Type')}
170217
</Cell>,
171218
<Cell right key="unit">
@@ -183,8 +230,9 @@ function MetricsTable({metrics, isLoading, query, project}: MetricsTableProps) {
183230
isEmpty={metrics.length === 0}
184231
isLoading={isLoading}
185232
>
186-
{metrics.map(({mri, type, unit, blockingStatus}) => {
233+
{metrics.map(({mri, type, unit, cardinality, blockingStatus}) => {
187234
const isBlocked = blockingStatus[0]?.isBlocked;
235+
const isCardinalityLimited = cardinality >= cardinalityLimit;
188236
return (
189237
<Fragment key={mri}>
190238
<Cell>
@@ -196,6 +244,19 @@ function MetricsTable({metrics, isLoading, query, project}: MetricsTableProps) {
196244
{middleEllipsis(formatMRI(mri), 65, /\.|-|_/)}
197245
</Link>
198246
</Cell>
247+
<Cell right>
248+
{isCardinalityLimited && (
249+
<Tooltip
250+
title={tct(
251+
'The tag cardinality of this metric exceeded our limit of [cardinalityLimit], which led to the data being dropped',
252+
{cardinalityLimit}
253+
)}
254+
>
255+
<StyledIconWarning size="sm" color="red300" />
256+
</Tooltip>
257+
)}
258+
{cardinality}
259+
</Cell>
199260
<Cell right>
200261
<Tag>{getReadableMetricType(type)}</Tag>
201262
</Cell>
@@ -241,14 +302,22 @@ const SearchWrapper = styled('div')`
241302
`;
242303

243304
const StyledPanelTable = styled(PanelTable)`
244-
grid-template-columns: 1fr repeat(3, minmax(115px, min-content));
305+
grid-template-columns: 1fr repeat(4, min-content);
245306
`;
246307

247308
const Cell = styled('div')<{right?: boolean}>`
248309
display: flex;
249310
align-items: center;
250311
align-self: stretch;
312+
gap: ${space(0.5)};
251313
justify-content: ${p => (p.right ? 'flex-end' : 'flex-start')};
252314
`;
253315

316+
const StyledIconWarning = styled(IconWarning)`
317+
margin-top: ${space(0.5)};
318+
&:hover {
319+
cursor: pointer;
320+
}
321+
`;
322+
254323
export default ProjectMetrics;

0 commit comments

Comments
 (0)