Skip to content
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

Feat(Escalation policies): A Hackweek project #76386

Draft
wants to merge 39 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c50c1f9
CRUDL APIs for escalation policies and rotation schedules
mikejihbe Aug 14, 2024
3aed160
CRUDL for rotation schedules and escalation policies
mikejihbe Aug 14, 2024
b1e24ba
Add schedule layer coalescing logic and tests
mikejihbe Aug 19, 2024
a4f3c51
Fix up tests
mikejihbe Aug 19, 2024
569ab55
Fix up tests
mikejihbe Aug 19, 2024
382d490
Add coalesced schedules to schedule serializers. use UTC everywhere e…
mikejihbe Aug 19, 2024
b6cf20d
Add endpoints and tests for querying escalation policy states
mikejihbe Aug 19, 2024
118458e
update datastore
mikejihbe Aug 19, 2024
a14dd5c
Seed WIP
corps Aug 20, 2024
dae20f6
Let migration be unsafe for now
corps Aug 20, 2024
cac957c
Typo fix
corps Aug 20, 2024
8d911e0
Add rotation periods to each layer serialization
mikejihbe Aug 20, 2024
69ecc4c
feat(escalation_policies): Add FE hooks for Rotation Schedule and Esc…
MichaelSun48 Aug 20, 2024
54772f2
Add page routes for schedule and escalation policies (#76408)
MichaelSun48 Aug 20, 2024
8e1b516
Add occurrence list page
mikejihbe Aug 20, 2024
f6c41af
Camelcase serialized responses
mikejihbe Aug 20, 2024
7dfc1cc
Fix data formatting issue
corps Aug 20, 2024
d3fa496
Fix team serialization in APIs
mikejihbe Aug 20, 2024
27e3bce
Add a REALLY FUGLY occurrences list that allows for updating state of…
mikejihbe Aug 20, 2024
f1b06f4
feat(escalation_policies): Add preliminary design for escalation poli…
MichaelSun48 Aug 20, 2024
6375c1a
Wire up escalation policies
mikejihbe Aug 20, 2024
5b7d709
feat(escalation_policies): Add basic, unfinished designs for schedule…
MichaelSun48 Aug 20, 2024
c55a66c
Feat(Escalation policies): Escalation event action
RyanSkonnord Aug 21, 2024
abcc3d6
hook up rotation schedule list
mikejihbe Aug 21, 2024
48b6281
Unify schedule timelines and make timeWindowConfig work properly
mikejihbe Aug 21, 2024
45a339e
Get basic schedule timeline view working
mikejihbe Aug 21, 2024
2fe434e
remove schedule name to clean up the rotation timeline
mikejihbe Aug 21, 2024
c0d7570
Add description to rotation schedules
mikejihbe Aug 21, 2024
5399bfc
Adding escalation job
corps Aug 21, 2024
a366d41
Merge branch 'hackweek/escalation_policies_alert_rule_action' into ha…
RyanSkonnord Aug 21, 2024
4bd515a
feat(escalation_policies): Various improvements to the occurrences ta…
MichaelSun48 Aug 21, 2024
4ffed7f
Fix serializer
mikejihbe Aug 21, 2024
8fd4d5b
add tooltip for schedule periods (#76477)
MichaelSun48 Aug 22, 2024
8ee4222
WIP
corps Aug 22, 2024
4c0f542
WIP
corps Aug 22, 2024
0623757
WIP
corps Aug 22, 2024
a889ecd
Add requisite copy-paste to NotifyEscalationAction
RyanSkonnord Aug 22, 2024
9e0a3a1
Works!
corps Aug 22, 2024
1743ed6
chore(escalation_policies): Minor UI Improvements across the board (#…
MichaelSun48 Aug 22, 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
Prev Previous commit
Next Next commit
WIP
  • Loading branch information
corps committed Aug 22, 2024
commit 8ee4222876f4c20fd1cd9bec6dd87e5baafff3ef
1 change: 1 addition & 0 deletions src/sentry/notifications/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ class ActionTargetType(Enum):
ISSUE_OWNERS = "IssueOwners"
TEAM = "Team"
MEMBER = "Member"
POLICY = "Policy"


ACTION_CHOICES = [
Expand Down
44 changes: 29 additions & 15 deletions src/sentry/rules/actions/escalation.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
from collections.abc import Callable, Generator, Sequence

from django.db.models import Q

from sentry.escalation_policies import EscalationPolicy, trigger_escalation_policy
from sentry.eventstore.models import GroupEvent
from sentry.mail.actions import NotifyEmailTarget
from sentry.mail.forms.notify_email import NotifyEmailForm
from sentry.notifications.types import ActionTargetType, FallthroughChoiceType
from sentry.rules.actions.base import EventAction
from sentry.rules.base import CallbackFuture
from sentry.types.actor import ActorType
from sentry.types.rules import RuleFuture

FALLTHROUGH_CHOICES = [
(FallthroughChoiceType.NO_ONE.value, "No One"),
]

ACTION_CHOICES = [
(ActionTargetType.POLICY.value, "Escalation Policy"),
]


class NotifyEscalationAction(EventAction):
"""Used for triggering a messages according to escalation policies."""
Expand All @@ -21,6 +27,13 @@ class NotifyEscalationAction(EventAction):

form_cls = NotifyEmailForm

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_fields = {
"targetType": {"type": "escalationAction", "choices": ACTION_CHOICES},
"fallthroughType": {"type": "choice", "choices": FALLTHROUGH_CHOICES},
}

@staticmethod
def _create_trigger_escalation_callback(
policy: EscalationPolicy,
Expand All @@ -33,16 +46,17 @@ def callback(event: GroupEvent, futures: Sequence[RuleFuture]) -> None:
def after(
self, event: GroupEvent, notification_uuid: str | None = None
) -> Generator[CallbackFuture]:
user_ids: list[int] = []
team_ids: list[int] = []
target = NotifyEmailTarget.unpack(self)
for recipient in target.get_eligible_recipients(event):
if recipient.actor_type == ActorType.USER:
user_ids.append(recipient.id)
if recipient.actor_type == ActorType.TEAM:
team_ids.append(recipient.id)

query = EscalationPolicy.objects.filter(organization_id=event.group.organization.id)
query = query.filter(Q(user_id__in=user_ids) | Q(team_id__in=team_ids))
for policy in query:
yield self.future(self._create_trigger_escalation_callback(policy))
# plz figure out the general form for notification targets that can account for
# dynamic, lazy, targets.
assert target.target_type == ActionTargetType.POLICY, "OMG REFACTOR THIS LATER"

# Figure out healthy, secure scoping
policy = EscalationPolicy.objects.filter(
organization_id=event.organization.id, id=target.target_identifier
).first()

if policy is None:
return

yield self.future(self._create_trigger_escalation_callback(policy))
126 changes: 126 additions & 0 deletions static/app/components/policySelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {useCallback, useMemo, useRef} from 'react';
import {createFilter} from 'react-select';
import type {Theme} from '@emotion/react';

import type {
ControlProps,
GeneralSelectValue,
StylesConfig,
} from 'sentry/components/forms/controls/selectControl';
import SelectControl from 'sentry/components/forms/controls/selectControl';
import {space} from 'sentry/styles/space';
import type {Organization} from 'sentry/types/organization';
import {
type EscalationPolicy,
useFetchEscalationPolicies,
} from 'sentry/views/escalationPolicies/queries/useFetchEscalationPolicies';

const optionFilter = createFilter({
stringify: option => `${option.label} ${option.value}`,
});

const filterOption = (canditate, input) =>
canditate.data.value === optionFilter(canditate, input);

const getOptionValue = (option: PolicyOption) => option.value;

const placeholderSelectStyles: StylesConfig = {
input: (provided, state) => {
// XXX: The `state.theme` is an emotion theme object, but it is not typed
// as the emotion theme object in react-select
const theme = state.theme as unknown as Theme;

return {
...provided,
display: 'grid',
gridTemplateColumns: 'max-content 1fr',
alignItems: 'center',
gridGap: space(1),
':before': {
backgroundColor: theme.backgroundSecondary,
height: 24,
width: 24,
borderRadius: 3,
content: '""',
display: 'block',
},
};
},
placeholder: provided => ({
...provided,
paddingLeft: 32,
}),
};

type Props = {
// onChange: (value: any) => any;
/**
* Received via withOrganization
* Note: withOrganization collects it from the context, this is not type safe
*/
organization: Organization;
} & ControlProps;

type PolicyOption = GeneralSelectValue & {};

function PolicySelector(props: Props) {
const {organization, onChange, ...extraProps} = props;
const {multiple} = props;

const {isFetching, data: policies} = useFetchEscalationPolicies({
orgSlug: organization.slug,
});
// TODO(ts) This type could be improved when react-select types are better.
const selectRef = useRef<any>(null);

const createOption = useCallback(
(policy: EscalationPolicy): PolicyOption => ({
value: policy.id + '',
label: policy.name,
// leadingItems: <IdBadge team={team} hideName />,
// searchKey: team.slug,
}),
[]
);

const handleChange = useCallback(
(newValue: PolicyOption | PolicyOption[]) => {
if (multiple) {
const options = newValue as PolicyOption[];
onChange?.(options);
return;
}

const option = newValue as PolicyOption;
onChange?.(option);
},
[multiple, onChange]
);

const options = useMemo(() => {
if (policies) return policies.map(createOption);
return [];
}, [createOption, policies]);

const styles = useMemo(
() => ({
...(multiple ? {} : placeholderSelectStyles),
}),
[multiple]
);

return (
<SelectControl
ref={selectRef}
options={options}
getOptionValue={getOptionValue}
filterOption={filterOption}
styles={styles}
isLoading={isFetching}
onChange={handleChange}
{...extraProps}
/>
);
}

export default PolicySelector;
3 changes: 3 additions & 0 deletions static/app/types/alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {IssueConfigField} from './integrations';
export const enum IssueAlertActionType {
SLACK = 'sentry.integrations.slack.notify_action.SlackNotifyServiceAction',
NOTIFY_EMAIL = 'sentry.mail.actions.NotifyEmailAction',
NOTIFY_ESCALATION = 'sentry.mail.actions.NotifyEscalationAction',
DISCORD = 'sentry.integrations.discord.notify_action.DiscordNotifyServiceAction',
SENTRY_APP = 'sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction',
MS_TEAMS = 'sentry.integrations.msteams.notify_action.MsTeamsNotifyServiceAction',
Expand Down Expand Up @@ -101,6 +102,7 @@ interface IssueAlertGenericActionConfig extends IssueAlertConfigBase {
id:
| `${IssueAlertActionType.SLACK}`
| `${IssueAlertActionType.NOTIFY_EMAIL}`
| `${IssueAlertActionType.NOTIFY_ESCALATION}`
| `${IssueAlertActionType.DISCORD}`
| `${IssueAlertActionType.SENTRY_APP}`
| `${IssueAlertActionType.MS_TEAMS}`
Expand Down Expand Up @@ -264,6 +266,7 @@ export enum MailActionTargetType {
TEAM = 'Team',
MEMBER = 'Member',
RELEASE_MEMBERS = 'ReleaseMembers',
POLICY = 'Policy',
}

export enum AssigneeTargetType {
Expand Down
15 changes: 14 additions & 1 deletion static/app/views/alerts/rules/issue/memberTeamFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styled from '@emotion/styled';

import SelectControl from 'sentry/components/forms/controls/selectControl';
import PanelItem from 'sentry/components/panels/panelItem';
import PolicySelector from 'sentry/components/policySelector';
import SelectMembers from 'sentry/components/selectMembers';
import TeamSelector from 'sentry/components/teamSelector';
import {space} from 'sentry/styles/space';
Expand All @@ -25,6 +26,7 @@ type Props = {
project: Project;
ruleData: IssueAlertRuleAction | IssueAlertRuleCondition;
teamValue: string | number;
policyValue?: string | number;
};

class MemberTeamFields extends Component<Props> {
Expand Down Expand Up @@ -64,11 +66,14 @@ class MemberTeamFields extends Component<Props> {
ruleData,
memberValue,
teamValue,
policyValue,
options,
} = this.props;

const teamSelected = ruleData.targetType === teamValue;
const memberSelected = ruleData.targetType === memberValue;
const policySelected = ruleData.targetType === policyValue;
console.log({policySelected});

const selectControlStyles = {
control: provided => ({
Expand All @@ -90,7 +95,7 @@ class MemberTeamFields extends Component<Props> {
onChange={this.handleChangeActorType}
/>
</SelectWrapper>
{(teamSelected || memberSelected) && (
{(teamSelected || memberSelected || policySelected) && (
<SelectWrapper>
{teamSelected ? (
<TeamSelector
Expand All @@ -113,6 +118,14 @@ class MemberTeamFields extends Component<Props> {
styles={selectControlStyles}
onChange={this.handleChangeActorId}
/>
) : policySelected ? (
<PolicySelector
onChange={this.handleChangeActorId}
organization={organization}
key={policyValue}
value={`${ruleData.targetIdentifier}`}
styles={selectControlStyles}
/>
) : null}
</SelectWrapper>
)}
Expand Down
32 changes: 31 additions & 1 deletion static/app/views/alerts/rules/issue/ruleNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {releaseHealth} from 'sentry/data/platformCategories';
import {IconDelete, IconSettings} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Choices, Organization, Project} from 'sentry/types';
import type {Choices} from 'sentry/types';
import type {
IssueAlertConfiguration,
IssueAlertRuleAction,
Expand All @@ -26,6 +26,8 @@ import {
IssueAlertFilterType,
MailActionTargetType,
} from 'sentry/types/alerts';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import MemberTeamFields from 'sentry/views/alerts/rules/issue/memberTeamFields';
import SentryAppRuleModal from 'sentry/views/alerts/rules/issue/sentryAppRuleModal';
import TicketRuleModal from 'sentry/views/alerts/rules/issue/ticketRuleModal';
Expand Down Expand Up @@ -129,8 +131,33 @@ function MailActionFields({
{value: MailActionTargetType.TEAM, label: t('Team')},
{value: MailActionTargetType.MEMBER, label: t('Member')},
]}
teamValue={MailActionTargetType.TEAM}
memberValue={MailActionTargetType.MEMBER}
policyValue={MailActionTargetType.POLICY}
/>
);
}

function EscalationActionFields({
data,
organization,
project,
disabled,
onMemberTeamChange,
}: FieldProps) {
const isInitialized = data.targetType !== undefined && `${data.targetType}`.length > 0;
return (
<MemberTeamFields
disabled={disabled}
project={project}
organization={organization}
loading={!isInitialized}
ruleData={data as IssueAlertRuleAction}
onChange={onMemberTeamChange}
options={[{value: MailActionTargetType.POLICY, label: t('Schedule')}]}
memberValue={MailActionTargetType.MEMBER}
teamValue={MailActionTargetType.TEAM}
policyValue={MailActionTargetType.POLICY}
/>
);
}
Expand Down Expand Up @@ -283,6 +310,7 @@ function RuleNode({
);
}

console.log(fieldConfig);
switch (fieldConfig.type) {
case 'choice':
return <ChoiceField {...fieldProps} />;
Expand All @@ -294,6 +322,8 @@ function RuleNode({
return <MailActionFields {...fieldProps} />;
case 'assignee':
return <AssigneeFilterFields {...fieldProps} />;
case 'escalationAction':
return <EscalationActionFields {...fieldProps} />;
default:
return null;
}
Expand Down
Loading