Skip to content

Commit 6d88cff

Browse files
authored
Feat: Enable activated alert creation via alert rule form (#68959)
Modifies the AlertRule form to add options for activated rule creation https://github.com/getsentry/sentry/assets/6186377/234fa6ee-3200-4ab7-b121-db6afb45e6f8 NOTE: GET api's are still filtering out all activated alert rules so this details page is not rendering anything yet
1 parent 3f16935 commit 6d88cff

File tree

8 files changed

+327
-48
lines changed

8 files changed

+327
-48
lines changed

static/app/routes.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,7 @@ function buildRoutes() {
12121212
);
12131213

12141214
const alertChildRoutes = ({forCustomerDomain}: {forCustomerDomain: boolean}) => {
1215+
// ALERT CHILD ROUTES
12151216
return (
12161217
<Fragment>
12171218
<IndexRoute
@@ -1316,6 +1317,7 @@ function buildRoutes() {
13161317
</Fragment>
13171318
);
13181319
};
1320+
// ALERT ROUTES
13191321
const alertRoutes = (
13201322
<Fragment>
13211323
{USING_CUSTOMER_DOMAIN && (

static/app/types/alerts.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,8 @@ export enum MonitorType {
291291
CONTINUOUS = 0,
292292
ACTIVATED = 1,
293293
}
294+
295+
export enum ActivationConditionType {
296+
RELEASE_CREATION = 0,
297+
DEPLOY_CREATION = 1,
298+
}

static/app/views/alerts/rules/metric/actions.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ function isSavedRule(rule: MetricRule): rule is SavedMetricRule {
77
}
88

99
/**
10-
* Add a new rule or update an existing rule
10+
* Add a new alert rule or update an existing alert rule
1111
*
1212
* @param api API Client
1313
* @param orgId Organization slug
@@ -16,7 +16,7 @@ function isSavedRule(rule: MetricRule): rule is SavedMetricRule {
1616
*/
1717
export function addOrUpdateRule(
1818
api: Client,
19-
orgId: string,
19+
orgId: string, // organization slug
2020
rule: MetricRule,
2121
query?: object | any
2222
) {

static/app/views/alerts/rules/metric/ruleConditionsForm.tsx

+177-23
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {SearchInvalidTag} from 'sentry/components/smartSearchBar/searchInvalidTa
2424
import {t, tct} from 'sentry/locale';
2525
import {space} from 'sentry/styles/space';
2626
import type {Environment, Organization, Project, SelectValue} from 'sentry/types';
27+
import {ActivationConditionType, MonitorType} from 'sentry/types/alerts';
2728
import {getDisplayName} from 'sentry/utils/environment';
2829
import {hasCustomMetrics} from 'sentry/utils/metrics/features';
2930
import {getMRI} from 'sentry/utils/metrics/mri';
@@ -69,20 +70,28 @@ type Props = {
6970
disabled: boolean;
7071
onComparisonDeltaChange: (value: number) => void;
7172
onFilterSearch: (query: string, isQueryValid) => void;
73+
onMonitorTypeSelect: (activatedAlertFields: {
74+
activationCondition?: ActivationConditionType | undefined;
75+
monitorType?: MonitorType;
76+
monitorWindowSuffix?: string | undefined;
77+
monitorWindowValue?: number | undefined;
78+
}) => void;
7279
onTimeWindowChange: (value: number) => void;
7380
organization: Organization;
7481
project: Project;
7582
projects: Project[];
7683
router: InjectedRouter;
7784
thresholdChart: React.ReactNode;
7885
timeWindow: number;
86+
activationCondition?: ActivationConditionType;
7987
allowChangeEventTypes?: boolean;
8088
comparisonDelta?: number;
8189
disableProjectSelector?: boolean;
8290
isErrorMigration?: boolean;
8391
isExtrapolatedChartData?: boolean;
8492
isTransactionMigration?: boolean;
8593
loadingProjects?: boolean;
94+
monitorType?: number;
8695
};
8796

8897
type State = {
@@ -162,6 +171,20 @@ class RuleConditionsForm extends PureComponent<Props, State> {
162171
}
163172
}
164173

174+
get selectControlStyles() {
175+
return {
176+
control: (provided: {[x: string]: string | number | boolean}) => ({
177+
...provided,
178+
minWidth: 200,
179+
maxWidth: 300,
180+
}),
181+
container: (provided: {[x: string]: string | number | boolean}) => ({
182+
...provided,
183+
margin: `${space(0.5)}`,
184+
}),
185+
};
186+
}
187+
165188
renderEventTypeFilter() {
166189
const {organization, disabled, alertType, isErrorMigration} = this.props;
167190

@@ -326,8 +349,15 @@ class RuleConditionsForm extends PureComponent<Props, State> {
326349
}
327350

328351
renderInterval() {
329-
const {organization, disabled, alertType, timeWindow, onTimeWindowChange, project} =
330-
this.props;
352+
const {
353+
organization,
354+
disabled,
355+
alertType,
356+
timeWindow,
357+
onTimeWindowChange,
358+
project,
359+
monitorType,
360+
} = this.props;
331361

332362
return (
333363
<Fragment>
@@ -353,27 +383,107 @@ class RuleConditionsForm extends PureComponent<Props, State> {
353383
alertType={alertType}
354384
required
355385
/>
356-
<SelectControl
357-
name="timeWindow"
358-
styles={{
359-
control: (provided: {[x: string]: string | number | boolean}) => ({
360-
...provided,
361-
minWidth: 200,
362-
maxWidth: 300,
363-
}),
364-
container: (provided: {[x: string]: string | number | boolean}) => ({
365-
...provided,
366-
margin: `${space(0.5)}`,
367-
}),
368-
}}
369-
options={this.timeWindowOptions}
370-
required
371-
isDisabled={disabled}
372-
value={timeWindow}
373-
onChange={({value}) => onTimeWindowChange(value)}
374-
inline={false}
375-
flexibleControlStateSize
376-
/>
386+
{monitorType === MonitorType.CONTINUOUS && (
387+
<SelectControl
388+
name="timeWindow"
389+
styles={this.selectControlStyles}
390+
options={this.timeWindowOptions}
391+
required={monitorType === MonitorType.CONTINUOUS}
392+
isDisabled={disabled}
393+
value={timeWindow}
394+
onChange={({value}) => onTimeWindowChange(value)}
395+
inline={false}
396+
flexibleControlStateSize
397+
/>
398+
)}
399+
</FormRow>
400+
</Fragment>
401+
);
402+
}
403+
404+
renderMonitorTypeSelect() {
405+
const {
406+
onMonitorTypeSelect,
407+
monitorType,
408+
activationCondition,
409+
timeWindow,
410+
onTimeWindowChange,
411+
} = this.props;
412+
413+
return (
414+
<Fragment>
415+
<StyledListItem>
416+
<StyledListTitle>
417+
<div>{t('Select Monitor Type')}</div>
418+
</StyledListTitle>
419+
</StyledListItem>
420+
<FormRow>
421+
<MonitorSelect>
422+
<MonitorCard
423+
position="left"
424+
isSelected={monitorType === MonitorType.CONTINUOUS}
425+
onClick={() =>
426+
onMonitorTypeSelect({
427+
monitorType: MonitorType.CONTINUOUS,
428+
activationCondition,
429+
})
430+
}
431+
>
432+
<strong>{t('Continuous')}</strong>
433+
<div>{t('Continuously monitor trends for the metrics outlined below')}</div>
434+
</MonitorCard>
435+
<MonitorCard
436+
position="right"
437+
isSelected={monitorType === MonitorType.ACTIVATED}
438+
onClick={() =>
439+
onMonitorTypeSelect({
440+
monitorType: MonitorType.ACTIVATED,
441+
})
442+
}
443+
>
444+
<strong>Conditional</strong>
445+
{monitorType === MonitorType.ACTIVATED ? (
446+
<ActivatedAlertFields>
447+
{`${t('Monitor')} `}
448+
<SelectControl
449+
name="activationCondition"
450+
styles={this.selectControlStyles}
451+
options={[
452+
{
453+
value: ActivationConditionType.RELEASE_CREATION,
454+
label: t('New Release'),
455+
},
456+
{
457+
value: ActivationConditionType.DEPLOY_CREATION,
458+
label: t('New Deploy'),
459+
},
460+
]}
461+
required
462+
value={activationCondition}
463+
onChange={({value}) =>
464+
onMonitorTypeSelect({activationCondition: value})
465+
}
466+
inline={false}
467+
flexibleControlStateSize
468+
/>
469+
{` ${t('for')} `}
470+
<SelectControl
471+
name="timeWindow"
472+
styles={this.selectControlStyles}
473+
options={this.timeWindowOptions}
474+
value={timeWindow}
475+
onChange={({value}) => onTimeWindowChange(value)}
476+
inline={false}
477+
flexibleControlStateSize
478+
/>
479+
</ActivatedAlertFields>
480+
) : (
481+
<div>
482+
{t('Temporarily monitor specified query given activation condition')}
483+
</div>
484+
)}
485+
</MonitorCard>
486+
</MonitorSelect>
377487
</FormRow>
378488
</Fragment>
379489
);
@@ -395,6 +505,7 @@ class RuleConditionsForm extends PureComponent<Props, State> {
395505
} = this.props;
396506

397507
const {environments} = this.state;
508+
const hasActivatedAlerts = organization.features.includes('activated-alert-rules');
398509

399510
const environmentOptions: SelectValue<string | null>[] = [
400511
{
@@ -425,6 +536,7 @@ class RuleConditionsForm extends PureComponent<Props, State> {
425536
)}
426537
/>
427538
)}
539+
{hasActivatedAlerts && this.renderMonitorTypeSelect()}
428540
{!isErrorMigration && this.renderInterval()}
429541
<StyledListItem>{t('Filter events')}</StyledListItem>
430542
<FormRow noMargin columns={1 + (allowChangeEventTypes ? 1 : 0) + 1}>
@@ -653,4 +765,46 @@ const FormRow = styled('div')<{columns?: number; noMargin?: boolean}>`
653765
`}
654766
`;
655767

768+
const MonitorSelect = styled('div')`
769+
border-radius: ${p => p.theme.borderRadius};
770+
border: 1px solid ${p => p.theme.border};
771+
width: 100%;
772+
display: grid;
773+
grid-template-columns: 1fr 1fr;
774+
`;
775+
776+
type MonitorCardProps = {
777+
isSelected: boolean;
778+
/**
779+
* Adds hover and focus states to the card
780+
*/
781+
position: 'left' | 'right';
782+
};
783+
784+
const MonitorCard = styled('div')<MonitorCardProps>`
785+
padding: ${space(1)};
786+
display: flex;
787+
flex-grow: 1;
788+
flex-direction: column;
789+
cursor: pointer;
790+
791+
&:focus,
792+
&:hover {
793+
outline: 1px solid ${p => p.theme.purple200};
794+
background-color: ${p => p.theme.backgroundSecondary};
795+
}
796+
797+
border-top-left-radius: ${p => (p.position === 'left' ? p.theme.borderRadius : 0)};
798+
border-bottom-left-radius: ${p => (p.position === 'left' ? p.theme.borderRadius : 0)};
799+
border-top-right-radius: ${p => (p.position !== 'left' ? p.theme.borderRadius : 0)};
800+
border-bottom-right-radius: ${p => (p.position !== 'left' ? p.theme.borderRadius : 0)};
801+
outline: ${p => (p.isSelected ? `1px solid ${p.theme.purple400}` : 'none')};
802+
`;
803+
804+
const ActivatedAlertFields = styled('div')`
805+
display: flex;
806+
align-items: center;
807+
justify-content: space-between;
808+
`;
809+
656810
export default withApi(withProjects(RuleConditionsForm));

static/app/views/alerts/rules/metric/ruleForm.spec.tsx

+40
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import selectEvent from 'sentry-test/selectEvent';
99
import {addErrorMessage} from 'sentry/actionCreators/indicator';
1010
import type FormModel from 'sentry/components/forms/model';
1111
import ProjectsStore from 'sentry/stores/projectsStore';
12+
import {ActivationConditionType, MonitorType} from 'sentry/types/alerts';
1213
import {metric} from 'sentry/utils/analytics';
1314
import RuleFormContainer from 'sentry/views/alerts/rules/metric/ruleForm';
1415
import {Dataset} from 'sentry/views/alerts/rules/metric/types';
@@ -254,6 +255,45 @@ describe('Incident Rules Form', () => {
254255
);
255256
});
256257

258+
it('creates a rule with an activation condition', async () => {
259+
organization.features = [
260+
...organization.features,
261+
'mep-rollout-flag',
262+
'activated-alert-rules',
263+
];
264+
const rule = MetricRuleFixture({
265+
monitorType: MonitorType.ACTIVATED,
266+
activationCondition: ActivationConditionType.RELEASE_CREATION,
267+
});
268+
createWrapper({
269+
rule: {
270+
...rule,
271+
id: undefined,
272+
aggregate: 'count()',
273+
eventTypes: ['transaction'],
274+
dataset: 'transactions',
275+
},
276+
});
277+
278+
expect(await screen.findByTestId('alert-total-events')).toHaveTextContent('Total5');
279+
280+
await userEvent.click(screen.getByLabelText('Save Rule'));
281+
282+
expect(createRule).toHaveBeenCalledWith(
283+
expect.anything(),
284+
expect.objectContaining({
285+
data: expect.objectContaining({
286+
name: 'My Incident Rule',
287+
projects: ['project-slug'],
288+
aggregate: 'count()',
289+
eventTypes: ['transaction'],
290+
dataset: 'generic_metrics',
291+
thresholdPeriod: 1,
292+
}),
293+
})
294+
);
295+
});
296+
257297
it('switches to custom metric and selects event.type:error', async () => {
258298
organization.features = [...organization.features, 'performance-view'];
259299
const rule = MetricRuleFixture();

0 commit comments

Comments
 (0)