Browse Source

feat(issue-alerts): adds experiment for default alert rule action (#35828)

This PR adds an experiment to show a default alert rule action for issue alerts for orgs with 5 or fewer members. The default action is just sending a notification to the user who is making the alert. This experiment is going to be coupled with another change which will change the default notification settings to have Slack enabled by default. The goal is to see if having a default action will lead to better engagement, especially with Slack since users may not even realize such an alert rule action actually will go to their Slack if they have it set up. If this experiment goes well, we could potentially add a default value action for larger orgs as well.
Stephen Cefali 2 years ago
parent
commit
a1b3ba63eb

+ 6 - 0
static/app/data/experimentConfig.tsx

@@ -25,6 +25,12 @@ export const experimentList = [
     parameter: 'exposed',
     assignments: [0, 1],
   },
+  {
+    key: 'DefaultIssueAlertActionExperiment',
+    type: ExperimentType.Organization,
+    parameter: 'exposed',
+    assignments: [0, 1],
+  },
 ] as const;
 
 export const experimentConfig = experimentList.reduce(

+ 6 - 0
static/app/types/hooks.tsx

@@ -82,6 +82,11 @@ type FirstPartyIntegrationAdditionalCTAProps = {
 
 type GuideUpdateCallback = (nextGuide: Guide | null, opts: {dismissed?: boolean}) => void;
 
+type DefaultAlertRuleActionHook = (
+  callback: (showDefaultAction: boolean) => void,
+  organization: Organization
+) => void;
+
 type CodeOwnersCTAProps = {
   organization: Organization;
   project: Project;
@@ -214,6 +219,7 @@ export type SettingsHooks = {
  * and perform some sort of callback logic
  */
 type CallbackHooks = {
+  'callback:default-action-alert-rule': DefaultAlertRuleActionHook;
   'callback:on-guide-update': GuideUpdateCallback;
 };
 

+ 47 - 1
static/app/views/alerts/rules/issue/index.tsx

@@ -35,6 +35,8 @@ import {Panel, PanelBody} from 'sentry/components/panels';
 import {ALL_ENVIRONMENTS_KEY} from 'sentry/constants';
 import {IconChevron} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
+import ConfigStore from 'sentry/stores/configStore';
+import HookStore from 'sentry/stores/hookStore';
 import space from 'sentry/styles/space';
 import {Environment, OnboardingTaskKey, Organization, Project, Team} from 'sentry/types';
 import {
@@ -50,6 +52,7 @@ import {getDisplayName} from 'sentry/utils/environment';
 import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
 import recreateRoute from 'sentry/utils/recreateRoute';
 import routeTitleGen from 'sentry/utils/routeTitle';
+import withExperiment from 'sentry/utils/withExperiment';
 import withOrganization from 'sentry/utils/withOrganization';
 import withProjects from 'sentry/utils/withProjects';
 import {
@@ -108,7 +111,9 @@ type RuleTaskResponse = {
 type RouteParams = {orgId: string; projectId?: string; ruleId?: string};
 
 type Props = {
+  experimentAssignment: 0 | 1;
   location: Location;
+  logExperiment: () => void;
   organization: Organization;
   project: Project;
   projects: Project[];
@@ -157,6 +162,44 @@ class IssueRuleEditor extends AsyncView<Props, State> {
     window.clearTimeout(this.pollingTimeout);
   }
 
+  componentDidMount() {
+    const {params, organization, experimentAssignment, logExperiment} = this.props;
+    // only new rules
+    if (params.ruleId) {
+      return;
+    }
+    // check if there is a callback registered
+    const callback = HookStore.get('callback:default-action-alert-rule')[0];
+    if (!callback) {
+      return;
+    }
+    // let hook decide when we want to select a default alert rule
+    callback((showDefaultAction: boolean) => {
+      if (showDefaultAction) {
+        const user = ConfigStore.get('user');
+        const {rule} = this.state;
+        // always log the experiment if we meet the basic requirements decided by the hook
+        logExperiment();
+        if (experimentAssignment) {
+          // this will add a default alert rule action
+          // to send notifications in
+          this.setState({
+            rule: {
+              ...rule,
+              actions: [
+                {
+                  id: 'sentry.mail.actions.NotifyEmailAction',
+                  targetIdentifier: user.id,
+                  targetType: 'Member',
+                } as any, // Need to fix IssueAlertRuleAction typing
+              ],
+            } as UnsavedIssueAlertRule,
+          });
+        }
+      }
+    }, organization);
+  }
+
   componentDidUpdate(_prevProps: Props, prevState: State) {
     if (prevState.project.id === this.state.project.id) {
       return;
@@ -1178,7 +1221,10 @@ class IssueRuleEditor extends AsyncView<Props, State> {
   }
 }
 
-export default withOrganization(withProjects(IssueRuleEditor));
+export default withExperiment(withOrganization(withProjects(IssueRuleEditor)), {
+  experiment: 'DefaultIssueAlertActionExperiment',
+  injectLogExperiment: true,
+});
 
 // TODO(ts): Understand why styled is not correctly inheriting props here
 const StyledForm = styled(Form)<Form['props']>`