Skip to content

Commit b5204a6

Browse files
authored
feat(aci): add event frequency data condition node (#91786)
adding the event frequency data condition node which has special branching. other special branching data condition nodes (unique user and percent sessions) will come in the next PR. this required adding another `updateIfConditionType` action to the automation builder reducer in order to update the data condition's `comparison_type` depending on which option was selected (count/frequency) https://github.com/user-attachments/assets/ef2df300-1b53-4a5e-929c-5291adbdc598
1 parent e972b09 commit b5204a6

File tree

10 files changed

+224
-2
lines changed

10 files changed

+224
-2
lines changed

static/app/components/workflowEngine/form/automationBuilderRowLine.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {space} from 'sentry/styles/space';
33

44
export function RowLine({children}: {children: React.ReactNode}) {
55
return (
6-
<Flex align="center" gap={space(1)}>
6+
<Flex align="center" gap={space(1)} wrap="wrap">
77
{children}
88
</Flex>
99
);

static/app/components/workflowEngine/form/automationBuilderSelectField.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const StyledSelectField = styled(SelectField)`
2323
> div {
2424
padding-left: 0;
2525
}
26+
border: none;
2627
`;
2728

2829
export const selectControlStyles = {

static/app/types/workflowEngine/dataConditions.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export enum DataConditionType {
5454
PERCENT_SESSIONS_PERCENT = 'percent_sessions_percent',
5555
EVENT_UNIQUE_USER_FREQUENCY_WITH_CONDITIONS_COUNT = 'event_unique_user_frequency_with_conditions_count',
5656
EVENT_UNIQUE_USER_FREQUENCY_WITH_CONDITIONS_PERCENT = 'event_unique_user_frequency_with_conditions_percent',
57+
58+
// frequency types for UI only
59+
EVENT_FREQUENCY = 'event_frequency',
5760
}
5861

5962
export enum DataConditionGroupLogicType {

static/app/views/automations/components/actionFilters/constants.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const FILTER_DATA_CONDITION_TYPES = [
2121
DataConditionType.EVENT_ATTRIBUTE,
2222
DataConditionType.TAGGED_EVENT,
2323
DataConditionType.LEVEL,
24+
DataConditionType.EVENT_FREQUENCY,
2425
];
2526

2627
export enum MatchType {
@@ -96,6 +97,16 @@ export enum Level {
9697
SAMPLING = 0,
9798
}
9899

100+
export enum Interval {
101+
ONE_MINUTE = '1m',
102+
FIVE_MINUTES = '5m',
103+
FIFTEEN_MINUTES = '15m',
104+
ONE_HOUR = '1h',
105+
ONE_DAY = '1d',
106+
ONE_WEEK = '1w',
107+
THIRTY_DAYS = '30d',
108+
}
109+
99110
export const MATCH_CHOICES = [
100111
{value: MatchType.CONTAINS, label: 'contains'},
101112
{value: MatchType.EQUAL, label: 'equals'},
@@ -152,3 +163,22 @@ export const LEVEL_CHOICES = [
152163
{value: Level.DEBUG, label: t('debug')},
153164
{value: Level.SAMPLING, label: t('sampling')},
154165
];
166+
167+
export const INTERVAL_CHOICES = [
168+
{value: Interval.ONE_MINUTE, label: t('in one minute')},
169+
{value: Interval.FIVE_MINUTES, label: t('in 5 minutes')},
170+
{value: Interval.FIFTEEN_MINUTES, label: t('in 15 minutes')},
171+
{value: Interval.ONE_HOUR, label: t('in one hour')},
172+
{value: Interval.ONE_DAY, label: t('in one day')},
173+
{value: Interval.ONE_WEEK, label: t('in one week')},
174+
{value: Interval.THIRTY_DAYS, label: t('in 30 days')},
175+
];
176+
177+
export const COMPARISON_INTERVAL_CHOICES = [
178+
{value: Interval.FIVE_MINUTES, label: t('5 minutes ago')},
179+
{value: Interval.FIFTEEN_MINUTES, label: t('15 minutes ago')},
180+
{value: Interval.ONE_HOUR, label: t('one hour ago')},
181+
{value: Interval.ONE_DAY, label: t('one day ago')},
182+
{value: Interval.ONE_WEEK, label: t('one week ago')},
183+
{value: Interval.THIRTY_DAYS, label: t('30 days ago')},
184+
];
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import AutomationBuilderNumberField from 'sentry/components/workflowEngine/form/automationBuilderNumberField';
2+
import AutomationBuilderSelectField from 'sentry/components/workflowEngine/form/automationBuilderSelectField';
3+
import {tct} from 'sentry/locale';
4+
import {DataConditionType} from 'sentry/types/workflowEngine/dataConditions';
5+
import {
6+
COMPARISON_INTERVAL_CHOICES,
7+
INTERVAL_CHOICES,
8+
} from 'sentry/views/automations/components/actionFilters/constants';
9+
import {useDataConditionNodeContext} from 'sentry/views/automations/components/dataConditionNodes';
10+
11+
export default function EventFrequencyNode() {
12+
return tct('Number of events in an issue is [select]', {
13+
select: <ComparisonTypeField />,
14+
});
15+
}
16+
17+
function ComparisonTypeField() {
18+
const {condition, condition_id, onUpdateType} = useDataConditionNodeContext();
19+
20+
if (condition.comparison_type === DataConditionType.EVENT_FREQUENCY_COUNT) {
21+
return <CountBranch />;
22+
}
23+
if (condition.comparison_type === DataConditionType.EVENT_FREQUENCY_PERCENT) {
24+
return <PercentBranch />;
25+
}
26+
27+
return (
28+
<AutomationBuilderSelectField
29+
name={`${condition_id}.comparison_type`}
30+
value={condition.comparison_type}
31+
options={[
32+
{
33+
label: 'more than...',
34+
value: DataConditionType.EVENT_FREQUENCY_COUNT,
35+
},
36+
{
37+
label: 'relatively higher than...',
38+
value: DataConditionType.EVENT_FREQUENCY_PERCENT,
39+
},
40+
]}
41+
onChange={(value: DataConditionType) => {
42+
onUpdateType(value);
43+
}}
44+
/>
45+
);
46+
}
47+
48+
function CountBranch() {
49+
return tct('more than [value] [interval]', {
50+
value: <ValueField />,
51+
interval: <IntervalField />,
52+
});
53+
}
54+
55+
function PercentBranch() {
56+
return tct('[value] higher [interval] compared to [comparison_interval]', {
57+
value: <ValueField />,
58+
interval: <IntervalField />,
59+
comparison_interval: <ComparisonIntervalField />,
60+
});
61+
}
62+
63+
function ValueField() {
64+
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
65+
return (
66+
<AutomationBuilderNumberField
67+
name={`${condition_id}.comparison.value`}
68+
value={condition.comparison.value}
69+
min={1}
70+
step={1}
71+
onChange={(value: string) => {
72+
onUpdate({
73+
value,
74+
});
75+
}}
76+
/>
77+
);
78+
}
79+
80+
function IntervalField() {
81+
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
82+
return (
83+
<AutomationBuilderSelectField
84+
name={`${condition_id}.comparison.interval`}
85+
value={condition.comparison.interval}
86+
options={INTERVAL_CHOICES}
87+
onChange={(value: string) => {
88+
onUpdate({
89+
interval: value,
90+
});
91+
}}
92+
/>
93+
);
94+
}
95+
96+
function ComparisonIntervalField() {
97+
const {condition, condition_id, onUpdate} = useDataConditionNodeContext();
98+
return (
99+
<AutomationBuilderSelectField
100+
name={`${condition_id}.comparison.comparison_interval`}
101+
value={condition.comparison.comparison_interval}
102+
options={COMPARISON_INTERVAL_CHOICES}
103+
onChange={(value: string) => {
104+
onUpdate({
105+
comparison_interval: value,
106+
});
107+
}}
108+
/>
109+
);
110+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ function ActionFilterBlock({groupIndex}: ActionFilterBlockProps) {
158158
updateCondition={(index, comparison) =>
159159
actions.updateIfCondition(groupIndex, index, comparison)
160160
}
161+
updateConditionType={(index, type) =>
162+
actions.updateIfConditionType(groupIndex, index, type)
163+
}
161164
/>
162165
</Flex>
163166
</Step>

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export function useAutomationBuilderReducer() {
3030
return removeIfCondition(state, action, formModel);
3131
case 'UPDATE_IF_CONDITION':
3232
return updateIfCondition(state, action);
33+
case 'UPDATE_IF_CONDITION_TYPE':
34+
return updateIfConditionType(state, action);
3335
case 'ADD_IF_ACTION':
3436
return addIfAction(state, action);
3537
case 'REMOVE_IF_ACTION':
@@ -82,6 +84,16 @@ export function useAutomationBuilderReducer() {
8284
dispatch({type: 'REMOVE_IF_CONDITION', groupIndex, conditionIndex}),
8385
[dispatch]
8486
),
87+
updateIfConditionType: useCallback(
88+
(groupIndex: number, conditionIndex: number, conditionType: DataConditionType) =>
89+
dispatch({
90+
type: 'UPDATE_IF_CONDITION_TYPE',
91+
groupIndex,
92+
conditionIndex,
93+
conditionType,
94+
}),
95+
[dispatch]
96+
),
8597
updateIfCondition: useCallback(
8698
(groupIndex: number, conditionIndex: number, comparison: Record<string, any>) =>
8799
dispatch({type: 'UPDATE_IF_CONDITION', groupIndex, conditionIndex, comparison}),
@@ -140,6 +152,11 @@ interface AutomationActions {
140152
conditionIndex: number,
141153
comparison: Record<string, any>
142154
) => void;
155+
updateIfConditionType: (
156+
groupIndex: number,
157+
conditionIndex: number,
158+
conditionType: DataConditionType
159+
) => void;
143160
updateIfLogicType: (groupIndex: number, logicType: DataConditionGroupLogicType) => void;
144161
updateWhenCondition: (index: number, comparison: Record<string, any>) => void;
145162
updateWhenLogicType: (logicType: DataConditionGroupLogicType) => void;
@@ -217,6 +234,13 @@ type RemoveIfConditionAction = {
217234
type: 'REMOVE_IF_CONDITION';
218235
};
219236

237+
type UpdateIfConditionTypeAction = {
238+
conditionIndex: number;
239+
conditionType: DataConditionType;
240+
groupIndex: number;
241+
type: 'UPDATE_IF_CONDITION_TYPE';
242+
};
243+
220244
type UpdateIfConditionAction = {
221245
comparison: Record<string, any>;
222246
conditionIndex: number;
@@ -258,6 +282,7 @@ type AutomationBuilderAction =
258282
| RemoveIfAction
259283
| AddIfConditionAction
260284
| RemoveIfConditionAction
285+
| UpdateIfConditionTypeAction
261286
| UpdateIfConditionAction
262287
| AddIfActionAction
263288
| RemoveIfActionAction
@@ -425,6 +450,27 @@ function removeIfCondition(
425450
};
426451
}
427452

453+
function updateIfConditionType(
454+
state: AutomationBuilderState,
455+
action: UpdateIfConditionTypeAction
456+
): AutomationBuilderState {
457+
const {groupIndex, conditionIndex, conditionType} = action;
458+
return {
459+
...state,
460+
actionFilters: state.actionFilters.map((group, i) => {
461+
if (i !== groupIndex) {
462+
return group;
463+
}
464+
return {
465+
...group,
466+
conditions: group.conditions.map((c, j) =>
467+
j === conditionIndex ? {...c, comparison_type: conditionType} : c
468+
),
469+
};
470+
}),
471+
};
472+
}
473+
428474
function updateIfCondition(
429475
state: AutomationBuilderState,
430476
action: UpdateIfConditionAction

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ export default function AutomationBuilderRow({onDelete, children}: RowProps) {
2727
}
2828

2929
const RowContainer = styled('div')<{incompatible?: boolean}>`
30+
display: flex;
3031
background-color: ${p => p.theme.backgroundSecondary};
3132
border-radius: ${p => p.theme.borderRadius};
3233
border: 1px ${p => p.theme.innerBorder} solid;
3334
border-color: ${p => (p.incompatible ? p.theme.red200 : 'none')};
3435
position: relative;
3536
padding: ${space(0.75)} ${space(1.5)};
37+
min-height: 46px;
38+
align-items: center;
3639
`;
3740

3841
const DeleteButton = styled(Button)`

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ interface DataConditionNodeListProps {
2121
onDeleteRow: (id: number) => void;
2222
placeholder: string;
2323
updateCondition: (index: number, condition: Record<string, any>) => void;
24+
updateConditionType?: (index: number, type: DataConditionType) => void;
2425
}
2526

2627
export default function DataConditionNodeList({
@@ -31,6 +32,7 @@ export default function DataConditionNodeList({
3132
onAddRow,
3233
onDeleteRow,
3334
updateCondition,
35+
updateConditionType,
3436
}: DataConditionNodeListProps) {
3537
const options = Array.from(dataConditionNodesMap.entries())
3638
.map(([value, {label}]) => ({value, label}))
@@ -48,6 +50,7 @@ export default function DataConditionNodeList({
4850
condition,
4951
condition_id: `${group}.conditions.${i}`,
5052
onUpdate: newCondition => updateCondition(i, newCondition),
53+
onUpdateType: type => updateConditionType && updateConditionType(i, type),
5154
}}
5255
>
5356
<Node />

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from 'sentry/types/workflowEngine/dataConditions';
88
import AgeComparisonNode from 'sentry/views/automations/components/actionFilters/ageComparison';
99
import EventAttributeNode from 'sentry/views/automations/components/actionFilters/eventAttribute';
10+
import EventFrequencyNode from 'sentry/views/automations/components/actionFilters/eventFrequency';
1011
import IssueOccurrencesNode from 'sentry/views/automations/components/actionFilters/issueOccurrences';
1112
import IssuePriorityNode from 'sentry/views/automations/components/actionFilters/issuePriority';
1213
import LatestAdoptedReleaseNode from 'sentry/views/automations/components/actionFilters/latestAdoptedRelease';
@@ -16,7 +17,8 @@ import TaggedEventNode from 'sentry/views/automations/components/actionFilters/t
1617
interface DataConditionNodeProps {
1718
condition: NewDataCondition;
1819
condition_id: string;
19-
onUpdate: (condition: Record<string, any>) => void;
20+
onUpdate: (comparison: Record<string, any>) => void;
21+
onUpdateType: (type: DataConditionType) => void;
2022
}
2123

2224
export const DataConditionNodeContext = createContext<DataConditionNodeProps | null>(
@@ -113,4 +115,25 @@ export const dataConditionNodesMap = new Map<DataConditionType, DataConditionNod
113115
dataCondition: <LevelNode />,
114116
},
115117
],
118+
[
119+
DataConditionType.EVENT_FREQUENCY,
120+
{
121+
label: t('Number of events'),
122+
dataCondition: <EventFrequencyNode />,
123+
},
124+
],
125+
[
126+
DataConditionType.EVENT_FREQUENCY_COUNT,
127+
{
128+
label: t('Number of events'),
129+
dataCondition: <EventFrequencyNode />,
130+
},
131+
],
132+
[
133+
DataConditionType.EVENT_FREQUENCY_PERCENT,
134+
{
135+
label: t('Number of events'),
136+
dataCondition: <EventFrequencyNode />,
137+
},
138+
],
116139
]);

0 commit comments

Comments
 (0)