Skip to content

feat(anomaly detection): add chart preview to new alert form #78238

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
80692b6
wip: add anomaly support to alert create form
natemoo-re Sep 26, 2024
ed69dfe
feat(anomaly detection): rule form anomaly chart wiring
natemoo-re Oct 4, 2024
f064ed3
fix(alert rule form): clear errors on update
natemoo-re Oct 7, 2024
2cc7f48
fix(alert rule form): missing default state
natemoo-re Oct 7, 2024
1328dc6
feat(anomaly detection): improve api error handling
natemoo-re Oct 7, 2024
00cb5f5
fix(anomaly detection): conform to seer expected values
natemoo-re Oct 8, 2024
eb0d87b
refactor(anomaly detection): improve anomaly data handling
natemoo-re Oct 8, 2024
9bf38ca
refactor(anomaly detection): extract shared logic to utils
natemoo-re Oct 8, 2024
33dee80
feat(anomaly detection): add anomaly support to threshold chart
natemoo-re Oct 8, 2024
d7ab5fb
feat(anomaly detection): pass anomalies through triggerschart
natemoo-re Oct 8, 2024
d25e2db
fix(anomaly detection): improve chart error message
natemoo-re Oct 8, 2024
0a1b10d
refactor(anomaly detection): improve chart error message
natemoo-re Oct 8, 2024
fc10a7f
test(anomaly detection): mock anomalies
natemoo-re Oct 8, 2024
3db4253
test(anomaly detection): mock anomalies endpoint
natemoo-re Oct 9, 2024
3738c32
fix: revert
natemoo-re Oct 9, 2024
e81d0cd
chore: remove explicit block
natemoo-re Oct 9, 2024
256602e
fix: array access
natemoo-re Oct 9, 2024
48a557e
chore(anomaly detection): make anomalies prop optional
natemoo-re Oct 9, 2024
5bbd576
test(anomaly detection): mock /events/anomalies/ endpoint
natemoo-re Oct 9, 2024
99f1381
test(anomaly detection): add anomaly chart util tests
natemoo-re Oct 9, 2024
f63c4a4
refactor(anomaly detection): improve Anomaly types
natemoo-re Oct 9, 2024
b94d2b9
refactor(anomaly detection): cleaner prop drilling
natemoo-re Oct 9, 2024
6e5c404
refactor(anomaly detection): adjust how anomalies are fetched
natemoo-re Oct 9, 2024
8aea5c0
fix(anomaly detection): ensure timestamps are adjusted to UTC ms
natemoo-re Oct 9, 2024
3f09b43
fix(anomaly detection): adjust types
natemoo-re Oct 9, 2024
919a419
fix(anomaly detection): change mock values to numbers
natemoo-re Oct 9, 2024
e7e09bb
fix(anomaly detection): ensure updates to thresholds refetch anomalies
natemoo-re Oct 9, 2024
973076d
fix(anomaly detection): filter out overlap between historical data an…
natemoo-re Oct 9, 2024
8618b75
fix(anomaly detection): handle string timestamps to fix chartcuterie …
natemoo-re Oct 11, 2024
540acee
Merge branch 'master' into natemoo-re/anomaly-detection-charts
natemoo-re Oct 11, 2024
906db5f
Merge branch 'master' into natemoo-re/anomaly-detection-charts
natemoo-re Oct 15, 2024
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
4 changes: 4 additions & 0 deletions static/app/views/alerts/rules/metric/create.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ describe('Incident Rules Create', function () {
url: '/organizations/org-slug/events-stats/',
body: EventsStatsFixture(),
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/anomalies/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/alert-rules/available-actions/',
body: [
Expand Down
129 changes: 9 additions & 120 deletions static/app/views/alerts/rules/metric/details/metricChartOption.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import color from 'color';
import type {MarkAreaComponentOption, YAXisComponentOption} from 'echarts';
import type {YAXisComponentOption} from 'echarts';
import moment from 'moment-timezone';

import type {AreaChartProps, AreaChartSeries} from 'sentry/components/charts/areaChart';
Expand All @@ -16,12 +16,9 @@ import {getCrashFreeRateSeries} from 'sentry/utils/sessions';
import {lightTheme as theme} from 'sentry/utils/theme';
import type {MetricRule, Trigger} from 'sentry/views/alerts/rules/metric/types';
import {AlertRuleTriggerType, Dataset} from 'sentry/views/alerts/rules/metric/types';
import {getAnomalyMarkerSeries} from 'sentry/views/alerts/rules/metric/utils/anomalyChart';
import type {Anomaly, Incident} from 'sentry/views/alerts/types';
import {
AnomalyType,
IncidentActivityType,
IncidentStatus,
} from 'sentry/views/alerts/types';
import {IncidentActivityType, IncidentStatus} from 'sentry/views/alerts/types';
import {
ALERT_CHART_MIN_MAX_BUFFER,
alertAxisFormatter,
Expand Down Expand Up @@ -140,48 +137,6 @@ function createIncidentSeries(
};
}

function createAnomalyMarkerSeries(
lineColor: string,
timestamp: string
): AreaChartSeries {
const formatter = ({value}: any) => {
const time = formatTooltipDate(moment(value), 'MMM D, YYYY LT');
return [
`<div class="tooltip-series"><div>`,
`</div>Anomaly Detected</div>`,
`<div class="tooltip-footer">${time}</div>`,
'<div class="tooltip-arrow"></div>',
].join('');
};

return {
seriesName: 'Anomaly Line',
type: 'line',
markLine: MarkLine({
silent: false,
lineStyle: {color: lineColor, type: 'dashed'},
label: {
silent: true,
show: false,
},
data: [
{
xAxis: timestamp,
},
],
tooltip: {
formatter,
},
}),
data: [],
tooltip: {
trigger: 'item',
alwaysShowContent: true,
formatter,
},
};
}

export type MetricChartData = {
rule: MetricRule;
timeseriesData: Series[];
Expand Down Expand Up @@ -263,8 +218,11 @@ export function getMetricAlertChartOption({
) / ALERT_CHART_MIN_MAX_BUFFER
)
: 0;
const firstPoint = new Date(dataArr[0]?.name).getTime();
const lastPoint = new Date(dataArr[dataArr.length - 1]?.name).getTime();
const startDate = new Date(dataArr[0]?.name);
const endDate =
dataArr.length > 1 ? new Date(dataArr[dataArr.length - 1]?.name) : new Date();
const firstPoint = startDate.getTime();
const lastPoint = endDate.getTime();
const totalDuration = lastPoint - firstPoint;
let waitingForDataDuration = 0;
let criticalDuration = 0;
Expand Down Expand Up @@ -403,77 +361,8 @@ export function getMetricAlertChartOption({
});
}
if (anomalies) {
const anomalyBlocks: MarkAreaComponentOption['data'] = [];
let start: string | undefined;
let end: string | undefined;
anomalies
.filter(anomalyts => {
const ts = new Date(anomalyts.timestamp).getTime();
return firstPoint < ts && ts < lastPoint;
})
.forEach(anomalyts => {
const {anomaly, timestamp} = anomalyts;

if (
[AnomalyType.high, AnomalyType.low].includes(anomaly.anomaly_type as string)
) {
if (!start) {
// If this is the start of an anomaly, set start
start = new Date(timestamp).toISOString();
}
// as long as we have an valid anomaly type - continue tracking until we've hit the end
end = new Date(timestamp).toISOString();
} else {
if (start && end) {
// If we've hit a non-anomaly type, push the block
anomalyBlocks.push([
{
xAxis: start,
},
{
xAxis: end,
},
]);
// Create a marker line for the start of the anomaly
series.push(createAnomalyMarkerSeries(theme.purple300, start));
}
// reset the start/end to capture the next anomaly block
start = undefined;
end = undefined;
}
});
if (start && end) {
// push in the last block
// Create a marker line for the start of the anomaly
series.push(createAnomalyMarkerSeries(theme.purple300, start));
anomalyBlocks.push([
{
xAxis: start,
},
{
xAxis: end,
},
]);
}

// NOTE: if timerange is too small - highlighted area will not be visible
// Possibly provide a minimum window size if the time range is too large?
series.push({
seriesName: '',
name: 'Anomaly',
type: 'line',
smooth: true,
data: [],
markArea: {
itemStyle: {
color: 'rgba(255, 173, 177, 0.4)',
},
silent: true, // potentially don't make this silent if we want to render the `anomaly detected` in the tooltip
data: anomalyBlocks,
},
});
series.push(...getAnomalyMarkerSeries(anomalies, {startDate, endDate}));
}

let maxThresholdValue = 0;
if (!rule.comparisonDelta && warningTrigger?.alertThreshold) {
const {alertThreshold} = warningTrigger;
Expand Down
20 changes: 19 additions & 1 deletion static/app/views/alerts/rules/metric/ruleForm.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jest.mock('sentry/utils/analytics', () => ({
}));

describe('Incident Rules Form', () => {
let organization, project, router, location;
let organization, project, router, location, anomalies;
// create wrapper
const createWrapper = props =>
render(
Expand Down Expand Up @@ -105,6 +105,11 @@ describe('Incident Rules Form', () => {
url: '/organizations/org-slug/recent-searches/',
body: [],
});
anomalies = MockApiClient.addMockResponse({
method: 'POST',
url: '/organizations/org-slug/events/anomalies/',
body: [],
});
});

afterEach(() => {
Expand Down Expand Up @@ -391,6 +396,19 @@ describe('Incident Rules Form', () => {
expect(
await screen.findByRole('textbox', {name: 'Level of responsiveness'})
).toBeInTheDocument();
expect(anomalies).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
data: expect.objectContaining({
config: {
direction: 'up',
sensitivity: AlertRuleSensitivity.MEDIUM,
expected_seasonality: AlertRuleSeasonality.AUTO,
time_period: 60,
},
}),
})
);
await userEvent.click(screen.getByLabelText('Save Rule'));

expect(createRule).toHaveBeenLastCalledWith(
Expand Down
Loading
Loading