Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {MetricDetectorFixture} from 'sentry-fixture/detectors';

import {render, screen} from 'sentry-test/reactTestingLibrary';

import {MetricDetectorDetailsDetect} from './detect';

describe('MetricDetectorDetailsDetect', () => {
it('renders dataset, visualize, where, interval, and threshold', () => {
const detector = MetricDetectorFixture();

render(<MetricDetectorDetailsDetect detector={detector} />);

// Dataset
expect(screen.getByText('Dataset:')).toBeInTheDocument();
expect(screen.getByText('Errors')).toBeInTheDocument();

// Visualize (aggregate)
expect(screen.getByText('Visualize')).toBeInTheDocument();
// Aggregate function
expect(screen.getByText('count()')).toBeInTheDocument();
// Query
expect(screen.getByText('Where')).toBeInTheDocument();
expect(screen.getByLabelText('is:unresolved')).toBeInTheDocument();

// Interval is 60s by default in fixture
expect(screen.getByText('Interval:')).toBeInTheDocument();
expect(screen.getByText('1 minute')).toBeInTheDocument();

// Threshold label for static detection
expect(screen.getByText('Threshold:')).toBeInTheDocument();
expect(screen.getByText('Static threshold')).toBeInTheDocument();
});

it('renders human readable priority conditions for static detection', () => {
const detector = MetricDetectorFixture();

render(<MetricDetectorDetailsDetect detector={detector} />);

expect(screen.getByText('High')).toBeInTheDocument();
expect(screen.getByText(/Above 8/)).toBeInTheDocument();

expect(screen.getByText('Resolved')).toBeInTheDocument();
expect(screen.getByText(/Below or equal to 8/)).toBeInTheDocument();
});

it('renders percent change description with delta window', () => {
const detector = MetricDetectorFixture({
config: {detectionType: 'percent', comparisonDelta: 60},
});

render(<MetricDetectorDetailsDetect detector={detector} />);

expect(screen.getByText('Percent change')).toBeInTheDocument();
expect(screen.getByText(/8% higher than the previous 1 minute/)).toBeInTheDocument();

expect(screen.getByText('Resolved')).toBeInTheDocument();
expect(
screen.getByText(/Less than 8% lower than the previous 1 minute/)
).toBeInTheDocument();
});

it('renders dynamic detection notice', () => {
const detector = MetricDetectorFixture({
config: {detectionType: 'dynamic'},
});

render(<MetricDetectorDetailsDetect detector={detector} />);

expect(screen.getByText('Dynamic threshold')).toBeInTheDocument();
expect(
screen.getByText('Sentry will automatically update priority.')
).toBeInTheDocument();
});
});
150 changes: 141 additions & 9 deletions static/app/views/detectors/components/details/metric/detect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {Fragment} from 'react';
import styled from '@emotion/styled';

import {Grid} from '@sentry/scraps/layout';

import {Flex} from 'sentry/components/core/layout';
import {Heading, Text} from 'sentry/components/core/text';
import {Tooltip} from 'sentry/components/core/tooltip';
Expand All @@ -10,19 +12,149 @@ import {
} from 'sentry/components/searchQueryBuilder/formattedQuery';
import {Container} from 'sentry/components/workflowEngine/ui/container';
import {t} from 'sentry/locale';
import {
DataConditionType,
DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL,
DetectorPriorityLevel,
} from 'sentry/types/workflowEngine/dataConditions';
import type {
MetricCondition,
MetricDetector,
SnubaQueryDataSource,
} from 'sentry/types/workflowEngine/detectors';
import {getExactDuration} from 'sentry/utils/duration/getExactDuration';
import {PriorityDot} from 'sentry/views/detectors/components/priorityDot';
import {getDatasetConfig} from 'sentry/views/detectors/datasetConfig/getDatasetConfig';
import {getDetectorDataset} from 'sentry/views/detectors/datasetConfig/getDetectorDataset';
import {getMetricDetectorSuffix} from 'sentry/views/detectors/utils/metricDetectorSuffix';

function getDetectorTypeLabel(detector: MetricDetector) {
if (detector.config.detectionType === 'dynamic') {
return t('Dynamic threshold');
}
if (detector.config.detectionType === 'percent') {
return t('Percent change');
}
return t('Static threshold');
}

interface MetricDetectorDetectProps {
detector: MetricDetector;
function getConditionLabel({condition}: {condition: MetricCondition}) {
switch (condition.conditionResult) {
case DetectorPriorityLevel.OK:
return t('Resolved');
case DetectorPriorityLevel.LOW:
return t('Low');
case DetectorPriorityLevel.MEDIUM:
return t('Medium');
case DetectorPriorityLevel.HIGH:
return t('High');
default:
return t('Unknown');
}
}

function SnubaQueryDetails({dataSource}: {dataSource: SnubaQueryDataSource}) {
function makeDirectionText(condition: MetricCondition) {
switch (condition.type) {
case DataConditionType.GREATER:
return t('Above');
case DataConditionType.LESS:
return t('Below');
case DataConditionType.EQUAL:
return t('Equal to');
case DataConditionType.NOT_EQUAL:
return t('Not equal to');
case DataConditionType.GREATER_OR_EQUAL:
return t('Above or equal to');
case DataConditionType.LESS_OR_EQUAL:
return t('Below or equal to');
default:
return t('Unknown');
}
}

function getConditionDescription({
aggregate,
config,
condition,
}: {
aggregate: string;
condition: MetricCondition;
config: MetricDetector['config'];
}) {
const comparisonValue =
typeof condition.comparison === 'number' ? String(condition.comparison) : '';
const unit = getMetricDetectorSuffix(config.detectionType, aggregate);

if (config.detectionType === 'percent') {
const direction =
condition.type === DataConditionType.GREATER ? t('higher') : t('lower');
const delta = config.comparisonDelta;
const timeRange = getExactDuration(delta);

if (condition.conditionResult === DetectorPriorityLevel.OK) {
return t(
`Less than %(comparisonValue)s%(unit)s %(direction)s than the previous %(timeRange)s`,
{
comparisonValue,
unit,
direction,
timeRange,
}
);
}

return t(
`%(comparisonValue)s%(unit)s %(direction)s than the previous %(timeRange)s`,
{
comparisonValue,
unit,
direction,
timeRange,
}
);
}

return `${makeDirectionText(condition)} ${comparisonValue}${unit}`;
}

function DetectorPriorities({detector}: {detector: MetricDetector}) {
if (detector.config.detectionType === 'dynamic') {
return <div>{t('Sentry will automatically update priority.')}</div>;
}

const conditions = detector.conditionGroup?.conditions || [];

return (
<Grid columns="auto 1fr" gap="sm lg" align="start">
{conditions.map((condition, index) => (
<Fragment key={index}>
<Flex align="center" gap="sm">
<PriorityDot
priority={
condition.conditionResult === DetectorPriorityLevel.OK
? 'resolved'
: DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL[
condition.conditionResult as keyof typeof DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL
]
}
/>
<Text>{getConditionLabel({condition})}</Text>
</Flex>
<Text>
{getConditionDescription({
aggregate: detector.dataSources[0].queryObj.snubaQuery.aggregate,
condition,
config: detector.config,
})}
</Text>
</Fragment>
))}
</Grid>
);
}

export function MetricDetectorDetailsDetect({detector}: {detector: MetricDetector}) {
const dataSource = detector.dataSources[0];

if (!dataSource.queryObj) {
return <Container>{t('Query not found.')}</Container>;
}
Expand Down Expand Up @@ -75,16 +207,16 @@ function SnubaQueryDetails({dataSource}: {dataSource: SnubaQueryDataSource}) {
<Heading as="h4">{t('Interval:')}</Heading>
<Value>{getExactDuration(dataSource.queryObj.snubaQuery.timeWindow)}</Value>
</Flex>
<Flex gap="xs" align="baseline">
<Heading as="h4">{t('Threshold:')}</Heading>
<Value>{getDetectorTypeLabel(detector)}</Value>
</Flex>
<DetectorPriorities detector={detector} />
</Flex>
</Container>
);
}

export function MetricDetectorDetailsDetect({detector}: MetricDetectorDetectProps) {
const dataSource = detector.dataSources?.[0];
return <SnubaQueryDetails dataSource={dataSource} />;
}

const Query = styled('dl')`
display: grid;
grid-template-columns: auto minmax(0, 1fr);
Expand Down
Loading
Loading