Browse Source

ref(alerts): Add issue alert configuration types (#58707)

Scott Cooper 1 year ago
parent
commit
c3c2fed5aa

+ 4 - 11
fixtures/js-stubs/projectAlertRuleConfiguration.ts

@@ -1,15 +1,8 @@
-import {
-  IssueAlertRuleActionTemplate,
-  IssueAlertRuleConditionTemplate,
-} from 'sentry/types/alerts';
+import type {IssueAlertConfiguration} from 'sentry/types/alerts';
 
-type Config = {
-  actions: IssueAlertRuleActionTemplate[];
-  conditions: IssueAlertRuleConditionTemplate[];
-  filters: IssueAlertRuleConditionTemplate[];
-};
-
-export function ProjectAlertRuleConfiguration(params: Partial<Config> = {}): Config {
+export function ProjectAlertRuleConfiguration(
+  params: Partial<IssueAlertConfiguration> = {}
+): IssueAlertConfiguration {
   return {
     actions: [
       {

+ 133 - 20
static/app/types/alerts.tsx

@@ -6,15 +6,29 @@ export const enum IssueAlertActionType {
   SLACK = 'sentry.integrations.slack.notify_action.SlackNotifyServiceAction',
   NOTIFY_EMAIL = 'sentry.mail.actions.NotifyEmailAction',
   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',
+  PAGER_DUTY = 'sentry.integrations.pagerduty.notify_action.PagerDutyNotifyServiceAction',
+  OPSGENIE = 'sentry.integrations.opsgenie.notify_action.OpsgenieNotifyTeamAction',
+
+  /**
+   * Legacy integrations
+   */
+  NOTIFY_EVENT_ACTION = 'sentry.rules.actions.notify_event.NotifyEventAction',
+
+  /**
+   * Webhooks
+   */
+  NOTIFY_EVENT_SERVICE_ACTION = 'sentry.rules.actions.notify_event_service.NotifyEventServiceAction',
+
+  /**
+   * Ticket integrations
+   */
   JIRA_CREATE_TICKET = 'sentry.integrations.jira.notify_action.JiraCreateTicketAction',
   JIRA_SERVER_CREATE_TICKET = 'sentry.integrations.jira_server.notify_action.JiraServerCreateTicketAction',
   GITHUB_CREATE_TICKET = 'sentry.integrations.github.notify_action.GitHubCreateTicketAction',
   GITHUB_ENTERPRISE_CREATE_TICKET = 'sentry.integrations.github_enterprise.notify_action.GitHubEnterpriseCreateTicketAction',
   AZURE_DEVOPS_CREATE_TICKET = 'sentry.integrations.vsts.notify_action.AzureDevopsCreateTicketAction',
-  SENTRY_APP = 'sentry.rules.actions.notify_event_sentry_app.NotifyEventSentryAppAction',
-  MS_TEAMS = 'sentry.integrations.msteams.notify_action.MsTeamsNotifyServiceAction',
-  PAGER_DUTY = 'sentry.integrations.pagerduty.notify_action.PagerDutyNotifyServiceAction',
-  OPSGENIE = 'sentry.integrations.opsgenie.notify_action.OpsgenieNotifyTeamAction',
 }
 
 export const enum IssueAlertConditionType {
@@ -38,23 +52,122 @@ export const enum IssueAlertFilterType {
   LEVEL = 'sentry.rules.filters.level.LevelFilter',
 }
 
+interface IssueAlertFormFieldChoice {
+  type: 'choice';
+  choices?: Array<[key: string | number, name: string]>;
+  initial?: string;
+  placeholder?: string;
+}
+
+interface IssueAlertFormFieldString {
+  type: 'string';
+  initial?: string;
+  placeholder?: string;
+}
+
+interface IssueAlertFormFieldNumber {
+  type: 'number';
+  initial?: string;
+  placeholder?: number | string;
+}
+
+/**
+ * The fields that are used to render the form for an action or condition.
+ */
 type IssueAlertRuleFormField =
-  | {
-      type: 'choice';
-      choices?: [string, string][];
-      initial?: string;
-      placeholder?: string;
-    }
-  | {
-      type: 'string';
-      initial?: string;
-      placeholder?: string;
-    }
-  | {
-      type: 'number';
-      initial?: string;
-      placeholder?: number | string;
-    };
+  | IssueAlertFormFieldChoice
+  | IssueAlertFormFieldString
+  | IssueAlertFormFieldNumber;
+
+/**
+ * All issue alert configuration objects have these properties.
+ */
+interface IssueAlertConfigBase {
+  enabled: boolean;
+  label: string;
+  /**
+   * "Send a Slack notification"
+   */
+  prompt?: string;
+}
+
+/**
+ * Generic alert configuration. Do not add properties unless they are used by all filters.
+ */
+interface IssueAlertGenericActionConfig extends IssueAlertConfigBase {
+  id:
+    | `${IssueAlertActionType.SLACK}`
+    | `${IssueAlertActionType.NOTIFY_EMAIL}`
+    | `${IssueAlertActionType.DISCORD}`
+    | `${IssueAlertActionType.SENTRY_APP}`
+    | `${IssueAlertActionType.MS_TEAMS}`
+    | `${IssueAlertActionType.PAGER_DUTY}`
+    | `${IssueAlertActionType.OPSGENIE}`
+    | `${IssueAlertActionType.NOTIFY_EVENT_ACTION}`
+    | `${IssueAlertActionType.NOTIFY_EVENT_SERVICE_ACTION}`;
+  formFields?: Record<string, IssueAlertRuleFormField>;
+}
+
+/**
+ * Currently filters and conditions are basically the same, just with different IDs.
+ * Do not add properties unless they are used by all filters.
+ */
+export interface IssueAlertGenericConditionConfig extends IssueAlertConfigBase {
+  id: `${IssueAlertConditionType}` | `${IssueAlertFilterType}`;
+  formFields?: Record<string, IssueAlertRuleFormField>;
+}
+
+/**
+ * The object describing the options the slack action can use.
+ */
+interface IssueAlertSlackConfig extends IssueAlertConfigBase {
+  formFields: {
+    channel: IssueAlertFormFieldString;
+    channel_id: IssueAlertFormFieldString;
+    tags: IssueAlertFormFieldString;
+    workspace: IssueAlertFormFieldChoice;
+  };
+  id: `${IssueAlertActionType.SLACK}`;
+}
+
+interface IssueAlertTicketIntegrationConfig extends IssueAlertConfigBase {
+  actionType: 'ticket';
+  formFields: SchemaFormConfig;
+  id:
+    | `${IssueAlertActionType.JIRA_CREATE_TICKET}`
+    | `${IssueAlertActionType.JIRA_SERVER_CREATE_TICKET}`
+    | `${IssueAlertActionType.GITHUB_CREATE_TICKET}`
+    | `${IssueAlertActionType.GITHUB_ENTERPRISE_CREATE_TICKET}`
+    | `${IssueAlertActionType.AZURE_DEVOPS_CREATE_TICKET}`;
+  link: string;
+  ticketType: string;
+}
+
+interface IssueAlertSentryAppIntegrationConfig extends IssueAlertConfigBase {
+  actionType: 'sentryapp';
+  formFields: SchemaFormConfig;
+  id: `${IssueAlertActionType.SENTRY_APP}`;
+  sentryAppInstallationUuid: string;
+}
+
+/**
+ * The actions that an organization has enabled and can be used to create an issue alert.
+ */
+export type IssueAlertConfigurationAction =
+  | IssueAlertGenericActionConfig
+  | IssueAlertTicketIntegrationConfig
+  | IssueAlertSentryAppIntegrationConfig
+  | IssueAlertSlackConfig;
+
+/**
+ * Describes the actions, filters, and conditions that can be used
+ * to create an issue alert.
+ */
+export interface IssueAlertConfiguration {
+  actions: IssueAlertConfigurationAction[];
+  conditions: IssueAlertGenericConditionConfig[];
+  filters: IssueAlertGenericConditionConfig[];
+}
 
 /**
  * These templates that tell the UI how to render the action or condition

+ 17 - 21
static/app/views/alerts/rules/issue/index.tsx

@@ -54,11 +54,11 @@ import {
 import {
   IssueAlertActionType,
   IssueAlertConditionType,
+  IssueAlertConfiguration,
   IssueAlertFilterType,
   IssueAlertRule,
   IssueAlertRuleAction,
   IssueAlertRuleActionTemplate,
-  IssueAlertRuleConditionTemplate,
   UnsavedIssueAlertRule,
 } from 'sentry/types/alerts';
 import {metric, trackAnalytics} from 'sentry/utils/analytics';
@@ -119,7 +119,7 @@ const defaultRule: UnsavedIssueAlertRule = {
 
 const POLLING_MAX_TIME_LIMIT = 3 * 60000;
 
-type ConditionOrActionProperty = 'conditions' | 'actions' | 'filters';
+type ConfigurationKey = keyof IssueAlertConfiguration;
 
 type RuleTaskResponse = {
   status: 'pending' | 'failed' | 'success';
@@ -146,11 +146,7 @@ type Props = {
 } & RouteComponentProps<RouteParams, {}>;
 
 type State = DeprecatedAsyncView['state'] & {
-  configs: {
-    actions: IssueAlertRuleActionTemplate[];
-    conditions: IssueAlertRuleConditionTemplate[];
-    filters: IssueAlertRuleConditionTemplate[];
-  } | null;
+  configs: IssueAlertConfiguration | null;
   detailedError: null | {
     [key: string]: string[];
   };
@@ -624,7 +620,7 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
   };
 
   handlePropertyChange = <T extends keyof IssueAlertRuleAction>(
-    type: ConditionOrActionProperty,
+    type: ConfigurationKey,
     idx: number,
     prop: T,
     val: IssueAlertRuleAction[T]
@@ -636,7 +632,10 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
     });
   };
 
-  getInitialValue = (type: ConditionOrActionProperty, id: string) => {
+  getInitialValue = (
+    type: ConfigurationKey,
+    id: string
+  ): IssueAlertConfiguration[ConfigurationKey] => {
     const configuration = this.state.configs?.[type]?.find(c => c.id === id);
 
     const hasChangeAlerts =
@@ -660,7 +659,7 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
   };
 
   handleResetRow = <T extends keyof IssueAlertRuleAction>(
-    type: ConditionOrActionProperty,
+    type: ConfigurationKey,
     idx: number,
     prop: T,
     val: IssueAlertRuleAction[T]
@@ -681,10 +680,7 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
     });
   };
 
-  handleAddRow = (
-    type: ConditionOrActionProperty,
-    item: IssueAlertRuleActionTemplate
-  ) => {
+  handleAddRow = (type: ConfigurationKey, item: IssueAlertRuleActionTemplate) => {
     this.setState(prevState => {
       const clonedState = cloneDeep(prevState);
 
@@ -710,7 +706,7 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
     });
   };
 
-  handleDeleteRow = (type: ConditionOrActionProperty, idx: number) => {
+  handleDeleteRow = (type: ConfigurationKey, idx: number) => {
     this.setState(prevState => {
       const clonedState = cloneDeep(prevState);
 
@@ -760,7 +756,7 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
     }));
   };
 
-  getConditions() {
+  getConditions(): IssueAlertConfiguration['conditions'] | null {
     const {organization} = this.props;
 
     if (!organization.features.includes('change-alerts')) {
@@ -770,10 +766,10 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
     return (
       this.state.configs?.conditions?.map(condition =>
         CHANGE_ALERT_CONDITION_IDS.includes(condition.id)
-          ? ({
+          ? {
               ...condition,
               label: `${CHANGE_ALERT_PLACEHOLDERS_LABELS[condition.id]}...`,
-            } as IssueAlertRuleConditionTemplate)
+            }
           : condition
       ) ?? null
     );
@@ -890,11 +886,11 @@ class IssueRuleEditor extends DeprecatedAsyncView<Props, State> {
 
   displayNoConditionsWarning(): boolean {
     const {rule} = this.state;
-    const acceptedNoisyActionIds = [
+    const acceptedNoisyActionIds: string[] = [
       // Webhooks
-      'sentry.rules.actions.notify_event_service.NotifyEventServiceAction',
+      IssueAlertActionType.NOTIFY_EVENT_SERVICE_ACTION,
       // Legacy integrations
-      'sentry.rules.actions.notify_event.NotifyEventAction',
+      IssueAlertActionType.NOTIFY_EVENT_ACTION,
     ];
 
     return (

+ 2 - 3
static/app/views/alerts/rules/issue/ruleNode.tsx

@@ -17,11 +17,10 @@ import {
   AssigneeTargetType,
   IssueAlertActionType,
   IssueAlertConditionType,
+  IssueAlertConfiguration,
   IssueAlertFilterType,
   IssueAlertRuleAction,
-  IssueAlertRuleActionTemplate,
   IssueAlertRuleCondition,
-  IssueAlertRuleConditionTemplate,
   MailActionTargetType,
 } from 'sentry/types/alerts';
 import MemberTeamFields from 'sentry/views/alerts/rules/issue/memberTeamFields';
@@ -236,7 +235,7 @@ interface Props {
   project: Project;
   incompatibleBanner?: boolean;
   incompatibleRule?: boolean;
-  node?: IssueAlertRuleActionTemplate | IssueAlertRuleConditionTemplate | null;
+  node?: IssueAlertConfiguration[keyof IssueAlertConfiguration][number] | null;
   ownership?: null | IssueOwnership;
 }
 

+ 16 - 10
static/app/views/alerts/rules/issue/ruleNodeList.tsx

@@ -8,6 +8,8 @@ import {IssueOwnership, Organization, Project} from 'sentry/types';
 import {
   IssueAlertActionType,
   IssueAlertConditionType,
+  IssueAlertConfiguration,
+  IssueAlertGenericConditionConfig,
   IssueAlertRuleAction,
   IssueAlertRuleActionTemplate,
   IssueAlertRuleCondition,
@@ -34,7 +36,7 @@ type Props = {
   /**
    * All available actions or conditions
    */
-  nodes: IssueAlertRuleActionTemplate[] | IssueAlertRuleConditionTemplate[] | null;
+  nodes: IssueAlertConfiguration[keyof IssueAlertConfiguration] | null;
   onAddRow: (
     value: IssueAlertRuleActionTemplate | IssueAlertRuleConditionTemplate
   ) => void;
@@ -159,14 +161,18 @@ class RuleNodeList extends Component<Props> {
   getNode = (
     template: IssueAlertRuleAction | IssueAlertRuleCondition,
     itemIdx: number
-  ): IssueAlertRuleActionTemplate | IssueAlertRuleConditionTemplate | null => {
+  ): IssueAlertConfiguration[keyof IssueAlertConfiguration][number] | null => {
     const {nodes, items, organization, onPropertyChange} = this.props;
     const node = nodes?.find(n => {
-      return (
-        n.id === template.id &&
+      if ('sentryAppInstallationUuid' in n) {
         // Match more than just the id for sentryApp actions, they share the same id
-        n.sentryAppInstallationUuid === template.sentryAppInstallationUuid
-      );
+        return (
+          n.id === template.id &&
+          n.sentryAppInstallationUuid === template.sentryAppInstallationUuid
+        );
+      }
+
+      return n.id === template.id;
     });
 
     if (!node) {
@@ -180,13 +186,13 @@ class RuleNodeList extends Component<Props> {
       return node;
     }
 
-    const item = items[itemIdx] as IssueAlertRuleCondition;
+    const item = items[itemIdx];
 
-    let changeAlertNode: IssueAlertRuleConditionTemplate = {
-      ...node,
+    let changeAlertNode: IssueAlertGenericConditionConfig = {
+      ...(node as IssueAlertGenericConditionConfig),
       label: node.label.replace('...', ' {comparisonType}'),
       formFields: {
-        ...node.formFields,
+        ...(node.formFields as IssueAlertGenericConditionConfig['formFields']),
         comparisonType: {
           type: 'choice',
           choices: COMPARISON_TYPE_CHOICES,