Skip to content

Commit 5a601d2

Browse files
ameliahsuJesse-Box
authored andcommitted
feat(aci): add automatic names for alerts (#102909)
used this migration helper as reference for the naming logic, without the critical/warning priority labels https://github.com/getsentry/sentry/blob/52654bebc1b5873e353b0d0f1c60b4691fcc41a7/src/sentry/workflow_engine/migration_helpers/utils.py#L51-L55 <img width="901" height="106" alt="Screenshot 2025-11-06 at 12 54 25 PM" src="https://github.com/user-attachments/assets/2c7639c5-d06a-4d0b-bee9-b1d238142701" />
1 parent e67f42b commit 5a601d2

File tree

12 files changed

+529
-90
lines changed

12 files changed

+529
-90
lines changed

static/app/views/automations/components/actions/email.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ function AssignedToTeam({teamId}: {teamId: string}) {
6363

6464
function AssignedToMember({memberId}: {memberId: number}) {
6565
const {data: user} = useUserFromId({id: memberId});
66-
return t('Notify member %s', `${user?.email ?? 'unknown'}`);
66+
return t('Notify %s', `${user?.name ?? user?.email ?? 'unknown'}`);
6767
}
6868

6969
export function EmailNode() {
@@ -111,7 +111,11 @@ function IdentifierField() {
111111
value={action.config.targetIdentifier}
112112
onChange={(value: any) => {
113113
onUpdate({
114-
config: {...action.config, targetIdentifier: value.actor.id},
114+
config: {
115+
...action.config,
116+
targetIdentifier: value.actor.id,
117+
targetDisplay: value.actor.name,
118+
},
115119
data: {},
116120
});
117121
removeError(action.id);
@@ -132,7 +136,11 @@ function IdentifierField() {
132136
value={action.config.targetIdentifier}
133137
onChange={(value: any) => {
134138
onUpdate({
135-
config: {...action.config, targetIdentifier: value.actor.id},
139+
config: {
140+
...action.config,
141+
targetIdentifier: value.actor.id,
142+
targetDisplay: value.actor.name ?? value.actor.email,
143+
},
136144
data: {},
137145
});
138146
removeError(action.id);

static/app/views/automations/components/automationBuilderContext.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,10 +490,16 @@ function getDefaultConfig(actionHandler: ActionHandler): ActionConfig {
490490
actionHandler.integrations?.[0]?.services?.[0]?.id ??
491491
actionHandler.services?.[0]?.slug ??
492492
'';
493+
const targetDisplay =
494+
actionHandler.sentryApp?.name ??
495+
actionHandler.integrations?.[0]?.services?.[0]?.name ??
496+
actionHandler.services?.[0]?.name ??
497+
'';
493498

494499
return {
495500
targetType,
496501
targetIdentifier,
502+
targetDisplay,
497503
...(actionHandler.sentryApp?.id && {
498504
sentryAppIdentifier: SentryAppIdentifier.SENTRY_APP_ID,
499505
}),

static/app/views/automations/components/automationForm.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {t} from 'sentry/locale';
1212
import type {Automation} from 'sentry/types/workflowEngine/automations';
1313
import AutomationBuilder from 'sentry/views/automations/components/automationBuilder';
1414
import EditConnectedMonitors from 'sentry/views/automations/components/editConnectedMonitors';
15+
import {useSetAutomaticAutomationName} from 'sentry/views/automations/components/forms/useSetAutomaticAutomationName';
1516

1617
const FREQUENCY_OPTIONS = [
1718
{value: 5, label: t('5 minutes')},
@@ -40,6 +41,8 @@ export default function AutomationForm({model}: {model: FormModel}) {
4041
model.setValue('environment', env || null);
4142
};
4243

44+
useSetAutomaticAutomationName();
45+
4346
return (
4447
<Flex direction="column" gap="lg">
4548
<EditConnectedMonitors

static/app/views/automations/components/automationFormData.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function getNewAutomationData(
5454
state: AutomationBuilderState
5555
): NewAutomation {
5656
const result = {
57-
name: data.name,
57+
name: data.name || 'New Alert',
5858
triggers: stripDataConditionGroupId(state.triggers),
5959
environment: data.environment,
6060
actionFilters: state.actionFilters.map(stripDataConditionGroupId),

static/app/views/automations/components/editableAutomationName.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import EditableText from 'sentry/components/editableText';
22
import FormField from 'sentry/components/forms/formField';
33
import {t} from 'sentry/locale';
4+
import {useAutomationFormContext} from 'sentry/views/automations/components/forms/context';
45

56
export function EditableAutomationName() {
7+
const {setHasSetAutomationName} = useAutomationFormContext();
8+
69
return (
710
<FormField name="name" inline={false} flexibleControlStateSize stacked>
811
{({onChange, value}) => (
912
<EditableText
1013
isDisabled={false}
1114
value={value || ''}
1215
onChange={newValue => {
16+
// Mark that the user has manually set the automation name
17+
setHasSetAutomationName(true);
1318
onChange(newValue, {
1419
target: {
1520
value: newValue,
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import {ActionFilterFixture, ActionFixture} from 'sentry-fixture/automations';
2+
3+
import {ActionTarget, ActionType} from 'sentry/types/workflowEngine/actions';
4+
import type {AutomationBuilderState} from 'sentry/views/automations/components/automationBuilderContext';
5+
6+
import {
7+
getActionDescription,
8+
getAutomationName,
9+
MAX_ACTIONS_IN_NAME,
10+
MAX_NAME_LENGTH,
11+
} from './automationNameUtils';
12+
13+
describe('automationNameUtils', () => {
14+
describe('getActionDescription', () => {
15+
it('should return correct description for EMAIL action with ISSUE_OWNERS target', () => {
16+
const action = ActionFixture({
17+
type: ActionType.EMAIL,
18+
config: {
19+
targetType: ActionTarget.ISSUE_OWNERS,
20+
},
21+
});
22+
23+
expect(getActionDescription(action)).toBe('Notify Suggested Assignees');
24+
});
25+
26+
it('should return correct description for EMAIL action with TEAM target', () => {
27+
const action = ActionFixture({
28+
type: ActionType.EMAIL,
29+
config: {
30+
targetType: ActionTarget.TEAM,
31+
targetDisplay: 'backend',
32+
},
33+
});
34+
35+
expect(getActionDescription(action)).toBe('Notify team #backend');
36+
});
37+
38+
it('should return correct description for EMAIL action with user target', () => {
39+
const action = ActionFixture({
40+
type: ActionType.EMAIL,
41+
config: {
42+
targetType: ActionTarget.USER,
43+
targetDisplay: 'john@example.com',
44+
},
45+
});
46+
47+
expect(getActionDescription(action)).toBe('Notify john@example.com');
48+
});
49+
50+
it('should return correct description for SENTRY_APP action', () => {
51+
const action = ActionFixture({
52+
type: ActionType.SENTRY_APP,
53+
config: {
54+
targetType: ActionTarget.SENTRY_APP,
55+
targetDisplay: 'My App',
56+
},
57+
});
58+
59+
expect(getActionDescription(action)).toBe('Notify via My App');
60+
});
61+
62+
it('should return correct description for WEBHOOK action', () => {
63+
const action = ActionFixture({
64+
type: ActionType.WEBHOOK,
65+
config: {
66+
targetType: null,
67+
targetDisplay: 'Custom Webhook',
68+
},
69+
});
70+
71+
expect(getActionDescription(action)).toBe('Notify via Custom Webhook');
72+
});
73+
});
74+
75+
describe('getAutomationName', () => {
76+
const createBuilderState = (actions: any[]): AutomationBuilderState =>
77+
({
78+
actionFilters: [ActionFilterFixture({actions})],
79+
}) as AutomationBuilderState;
80+
81+
it('should return "New Alert" for empty actions', () => {
82+
const builderState = createBuilderState([]);
83+
expect(getAutomationName(builderState)).toBe('New Alert');
84+
});
85+
86+
it('should return single action description', () => {
87+
const actions = [
88+
ActionFixture({
89+
type: ActionType.EMAIL,
90+
config: {
91+
targetType: ActionTarget.TEAM,
92+
targetDisplay: 'backend',
93+
},
94+
}),
95+
];
96+
const builderState = createBuilderState(actions);
97+
98+
expect(getAutomationName(builderState)).toBe('Notify team #backend');
99+
});
100+
101+
it('should join multiple actions with commas', () => {
102+
const actions = [
103+
ActionFixture({
104+
type: ActionType.EMAIL,
105+
config: {
106+
targetType: ActionTarget.TEAM,
107+
targetDisplay: 'backend',
108+
},
109+
}),
110+
ActionFixture({
111+
type: ActionType.SENTRY_APP,
112+
config: {
113+
targetType: ActionTarget.SENTRY_APP,
114+
targetDisplay: 'Slack',
115+
},
116+
}),
117+
];
118+
const builderState = createBuilderState(actions);
119+
120+
expect(getAutomationName(builderState)).toBe(
121+
'Notify team #backend, Notify via Slack'
122+
);
123+
});
124+
125+
it('should include count suffix when there are more than MAX_ACTIONS_IN_NAME actions', () => {
126+
const actions = new Array(5).fill(
127+
ActionFixture({
128+
type: ActionType.EMAIL,
129+
config: {
130+
targetType: ActionTarget.TEAM,
131+
targetDisplay: 'team',
132+
},
133+
})
134+
);
135+
const builderState = createBuilderState(actions);
136+
137+
expect(getAutomationName(builderState)).toBe(
138+
'Notify team #team, Notify team #team, Notify team #team (+2)'
139+
);
140+
});
141+
142+
it('should handle character limit by removing actions from the end', () => {
143+
// Create actions with very long descriptions to exceed the limit
144+
const longDescription = 'A'.repeat(200);
145+
const action = ActionFixture({
146+
type: ActionType.EMAIL,
147+
config: {
148+
targetType: ActionTarget.USER,
149+
targetDisplay: longDescription,
150+
},
151+
});
152+
const actions = new Array(5).fill(action);
153+
const builderState = createBuilderState(actions);
154+
155+
const result = getAutomationName(builderState);
156+
expect(result.length).toBeLessThanOrEqual(MAX_NAME_LENGTH);
157+
expect(result).toBe(getActionDescription(action) + ' (+4)');
158+
});
159+
160+
it('should fallback to "New Alert (X actions)" when even single action is too long', () => {
161+
const veryLongDescription = 'A'.repeat(MAX_NAME_LENGTH);
162+
const actions = [
163+
ActionFixture({
164+
type: ActionType.EMAIL,
165+
config: {
166+
targetType: ActionTarget.USER,
167+
targetDisplay: veryLongDescription,
168+
},
169+
}),
170+
ActionFixture({
171+
type: ActionType.EMAIL,
172+
config: {
173+
targetType: ActionTarget.USER,
174+
targetDisplay: veryLongDescription,
175+
},
176+
}),
177+
];
178+
const builderState = createBuilderState(actions);
179+
180+
const result = getAutomationName(builderState);
181+
expect(result).toBe('New Alert (2 actions)');
182+
expect(result.length).toBeLessThanOrEqual(MAX_NAME_LENGTH);
183+
});
184+
185+
it('should respect MAX_ACTIONS_IN_NAME limit', () => {
186+
const actions = new Array(10).fill(null).map(() =>
187+
ActionFixture({
188+
type: ActionType.EMAIL,
189+
config: {
190+
targetType: ActionTarget.TEAM,
191+
targetDisplay: 'team',
192+
},
193+
})
194+
);
195+
const builderState = createBuilderState(actions);
196+
197+
const result = getAutomationName(builderState);
198+
// Should only include up to MAX_ACTIONS_IN_NAME actions in the description
199+
const actionCount = (result.match(/Notify/g) || []).length;
200+
expect(actionCount).toBe(MAX_ACTIONS_IN_NAME);
201+
expect(result).toContain('(+7)'); // 10 - 3 = 7 remaining
202+
});
203+
204+
it('should handle actions from multiple action filters', () => {
205+
const builderState = {
206+
actionFilters: [
207+
ActionFilterFixture({
208+
actions: [
209+
ActionFixture({
210+
type: ActionType.EMAIL,
211+
config: {
212+
targetType: ActionTarget.TEAM,
213+
targetDisplay: 'backend',
214+
},
215+
}),
216+
],
217+
}),
218+
ActionFilterFixture({
219+
actions: [
220+
ActionFixture({
221+
type: ActionType.SENTRY_APP,
222+
config: {
223+
targetType: ActionTarget.SENTRY_APP,
224+
targetDisplay: 'Slack',
225+
},
226+
}),
227+
],
228+
}),
229+
],
230+
} as AutomationBuilderState;
231+
232+
expect(getAutomationName(builderState)).toBe(
233+
'Notify team #backend, Notify via Slack'
234+
);
235+
});
236+
});
237+
});

0 commit comments

Comments
 (0)