Skip to content

Commit e73f05d

Browse files
authored
feat(aci): Move threshold info into detect section of metric monitor details (#103926)
1 parent a5fb4da commit e73f05d

File tree

6 files changed

+249
-149
lines changed

6 files changed

+249
-149
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {MetricDetectorFixture} from 'sentry-fixture/detectors';
2+
3+
import {render, screen} from 'sentry-test/reactTestingLibrary';
4+
5+
import {MetricDetectorDetailsDetect} from './detect';
6+
7+
describe('MetricDetectorDetailsDetect', () => {
8+
it('renders dataset, visualize, where, interval, and threshold', () => {
9+
const detector = MetricDetectorFixture();
10+
11+
render(<MetricDetectorDetailsDetect detector={detector} />);
12+
13+
// Dataset
14+
expect(screen.getByText('Dataset:')).toBeInTheDocument();
15+
expect(screen.getByText('Errors')).toBeInTheDocument();
16+
17+
// Visualize (aggregate)
18+
expect(screen.getByText('Visualize')).toBeInTheDocument();
19+
// Aggregate function
20+
expect(screen.getByText('count()')).toBeInTheDocument();
21+
// Query
22+
expect(screen.getByText('Where')).toBeInTheDocument();
23+
expect(screen.getByLabelText('is:unresolved')).toBeInTheDocument();
24+
25+
// Interval is 60s by default in fixture
26+
expect(screen.getByText('Interval:')).toBeInTheDocument();
27+
expect(screen.getByText('1 minute')).toBeInTheDocument();
28+
29+
// Threshold label for static detection
30+
expect(screen.getByText('Threshold:')).toBeInTheDocument();
31+
expect(screen.getByText('Static threshold')).toBeInTheDocument();
32+
});
33+
34+
it('renders human readable priority conditions for static detection', () => {
35+
const detector = MetricDetectorFixture();
36+
37+
render(<MetricDetectorDetailsDetect detector={detector} />);
38+
39+
expect(screen.getByText('High')).toBeInTheDocument();
40+
expect(screen.getByText(/Above 8/)).toBeInTheDocument();
41+
42+
expect(screen.getByText('Resolved')).toBeInTheDocument();
43+
expect(screen.getByText(/Below or equal to 8/)).toBeInTheDocument();
44+
});
45+
46+
it('renders percent change description with delta window', () => {
47+
const detector = MetricDetectorFixture({
48+
config: {detectionType: 'percent', comparisonDelta: 60},
49+
});
50+
51+
render(<MetricDetectorDetailsDetect detector={detector} />);
52+
53+
expect(screen.getByText('Percent change')).toBeInTheDocument();
54+
expect(screen.getByText(/8% higher than the previous 1 minute/)).toBeInTheDocument();
55+
56+
expect(screen.getByText('Resolved')).toBeInTheDocument();
57+
expect(
58+
screen.getByText(/Less than 8% lower than the previous 1 minute/)
59+
).toBeInTheDocument();
60+
});
61+
62+
it('renders dynamic detection notice', () => {
63+
const detector = MetricDetectorFixture({
64+
config: {detectionType: 'dynamic'},
65+
});
66+
67+
render(<MetricDetectorDetailsDetect detector={detector} />);
68+
69+
expect(screen.getByText('Dynamic threshold')).toBeInTheDocument();
70+
expect(
71+
screen.getByText('Sentry will automatically update priority.')
72+
).toBeInTheDocument();
73+
});
74+
});

static/app/views/detectors/components/details/metric/detect.tsx

Lines changed: 141 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {Fragment} from 'react';
22
import styled from '@emotion/styled';
33

4+
import {Grid} from '@sentry/scraps/layout';
5+
46
import {Flex} from 'sentry/components/core/layout';
57
import {Heading, Text} from 'sentry/components/core/text';
68
import {Tooltip} from 'sentry/components/core/tooltip';
@@ -10,19 +12,149 @@ import {
1012
} from 'sentry/components/searchQueryBuilder/formattedQuery';
1113
import {Container} from 'sentry/components/workflowEngine/ui/container';
1214
import {t} from 'sentry/locale';
15+
import {
16+
DataConditionType,
17+
DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL,
18+
DetectorPriorityLevel,
19+
} from 'sentry/types/workflowEngine/dataConditions';
1320
import type {
21+
MetricCondition,
1422
MetricDetector,
15-
SnubaQueryDataSource,
1623
} from 'sentry/types/workflowEngine/detectors';
1724
import {getExactDuration} from 'sentry/utils/duration/getExactDuration';
25+
import {PriorityDot} from 'sentry/views/detectors/components/priorityDot';
1826
import {getDatasetConfig} from 'sentry/views/detectors/datasetConfig/getDatasetConfig';
1927
import {getDetectorDataset} from 'sentry/views/detectors/datasetConfig/getDetectorDataset';
28+
import {getMetricDetectorSuffix} from 'sentry/views/detectors/utils/metricDetectorSuffix';
29+
30+
function getDetectorTypeLabel(detector: MetricDetector) {
31+
if (detector.config.detectionType === 'dynamic') {
32+
return t('Dynamic threshold');
33+
}
34+
if (detector.config.detectionType === 'percent') {
35+
return t('Percent change');
36+
}
37+
return t('Static threshold');
38+
}
2039

21-
interface MetricDetectorDetectProps {
22-
detector: MetricDetector;
40+
function getConditionLabel({condition}: {condition: MetricCondition}) {
41+
switch (condition.conditionResult) {
42+
case DetectorPriorityLevel.OK:
43+
return t('Resolved');
44+
case DetectorPriorityLevel.LOW:
45+
return t('Low');
46+
case DetectorPriorityLevel.MEDIUM:
47+
return t('Medium');
48+
case DetectorPriorityLevel.HIGH:
49+
return t('High');
50+
default:
51+
return t('Unknown');
52+
}
2353
}
2454

25-
function SnubaQueryDetails({dataSource}: {dataSource: SnubaQueryDataSource}) {
55+
function makeDirectionText(condition: MetricCondition) {
56+
switch (condition.type) {
57+
case DataConditionType.GREATER:
58+
return t('Above');
59+
case DataConditionType.LESS:
60+
return t('Below');
61+
case DataConditionType.EQUAL:
62+
return t('Equal to');
63+
case DataConditionType.NOT_EQUAL:
64+
return t('Not equal to');
65+
case DataConditionType.GREATER_OR_EQUAL:
66+
return t('Above or equal to');
67+
case DataConditionType.LESS_OR_EQUAL:
68+
return t('Below or equal to');
69+
default:
70+
return t('Unknown');
71+
}
72+
}
73+
74+
function getConditionDescription({
75+
aggregate,
76+
config,
77+
condition,
78+
}: {
79+
aggregate: string;
80+
condition: MetricCondition;
81+
config: MetricDetector['config'];
82+
}) {
83+
const comparisonValue =
84+
typeof condition.comparison === 'number' ? String(condition.comparison) : '';
85+
const unit = getMetricDetectorSuffix(config.detectionType, aggregate);
86+
87+
if (config.detectionType === 'percent') {
88+
const direction =
89+
condition.type === DataConditionType.GREATER ? t('higher') : t('lower');
90+
const delta = config.comparisonDelta;
91+
const timeRange = getExactDuration(delta);
92+
93+
if (condition.conditionResult === DetectorPriorityLevel.OK) {
94+
return t(
95+
`Less than %(comparisonValue)s%(unit)s %(direction)s than the previous %(timeRange)s`,
96+
{
97+
comparisonValue,
98+
unit,
99+
direction,
100+
timeRange,
101+
}
102+
);
103+
}
104+
105+
return t(
106+
`%(comparisonValue)s%(unit)s %(direction)s than the previous %(timeRange)s`,
107+
{
108+
comparisonValue,
109+
unit,
110+
direction,
111+
timeRange,
112+
}
113+
);
114+
}
115+
116+
return `${makeDirectionText(condition)} ${comparisonValue}${unit}`;
117+
}
118+
119+
function DetectorPriorities({detector}: {detector: MetricDetector}) {
120+
if (detector.config.detectionType === 'dynamic') {
121+
return <div>{t('Sentry will automatically update priority.')}</div>;
122+
}
123+
124+
const conditions = detector.conditionGroup?.conditions || [];
125+
126+
return (
127+
<Grid columns="auto 1fr" gap="sm lg" align="start">
128+
{conditions.map((condition, index) => (
129+
<Fragment key={index}>
130+
<Flex align="center" gap="sm">
131+
<PriorityDot
132+
priority={
133+
condition.conditionResult === DetectorPriorityLevel.OK
134+
? 'resolved'
135+
: DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL[
136+
condition.conditionResult as keyof typeof DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL
137+
]
138+
}
139+
/>
140+
<Text>{getConditionLabel({condition})}</Text>
141+
</Flex>
142+
<Text>
143+
{getConditionDescription({
144+
aggregate: detector.dataSources[0].queryObj.snubaQuery.aggregate,
145+
condition,
146+
config: detector.config,
147+
})}
148+
</Text>
149+
</Fragment>
150+
))}
151+
</Grid>
152+
);
153+
}
154+
155+
export function MetricDetectorDetailsDetect({detector}: {detector: MetricDetector}) {
156+
const dataSource = detector.dataSources[0];
157+
26158
if (!dataSource.queryObj) {
27159
return <Container>{t('Query not found.')}</Container>;
28160
}
@@ -75,16 +207,16 @@ function SnubaQueryDetails({dataSource}: {dataSource: SnubaQueryDataSource}) {
75207
<Heading as="h4">{t('Interval:')}</Heading>
76208
<Value>{getExactDuration(dataSource.queryObj.snubaQuery.timeWindow)}</Value>
77209
</Flex>
210+
<Flex gap="xs" align="baseline">
211+
<Heading as="h4">{t('Threshold:')}</Heading>
212+
<Value>{getDetectorTypeLabel(detector)}</Value>
213+
</Flex>
214+
<DetectorPriorities detector={detector} />
78215
</Flex>
79216
</Container>
80217
);
81218
}
82219

83-
export function MetricDetectorDetailsDetect({detector}: MetricDetectorDetectProps) {
84-
const dataSource = detector.dataSources?.[0];
85-
return <SnubaQueryDetails dataSource={dataSource} />;
86-
}
87-
88220
const Query = styled('dl')`
89221
display: grid;
90222
grid-template-columns: auto minmax(0, 1fr);

0 commit comments

Comments
 (0)