Browse Source

feat(spend-visibility): Update user notification settings with spend (#69933)

Awaiting new feature flag

Closes https://getsentry.atlassian.net/browse/RV-1591


![image](https://github.com/getsentry/sentry/assets/45607721/cb392ae0-3400-4db5-a2c7-e8a37ebc1457)

![image](https://github.com/getsentry/sentry/assets/45607721/49495e41-714c-4b6c-8478-dea5b8f5d2d6)
Isabella Enriquez 10 months ago
parent
commit
b0ee032bae

+ 10 - 0
static/app/views/settings/account/accountNotificationFineTuning.tsx

@@ -215,6 +215,16 @@ class AccountNotificationFineTuning extends DeprecatedAsyncView<Props, State> {
 
     const isProject = isGroupedByProject(fineTuneType) && organizations.length > 0;
     const field = ACCOUNT_NOTIFICATION_FIELDS[fineTuneType];
+    // TODO(isabella): once GA, remove this
+    if (
+      fineTuneType === 'quota' &&
+      organizations.some(org => org.features?.includes('spend-visibility-notifications'))
+    ) {
+      field.title = t('Spend Notifications');
+      field.description = t(
+        'Control the notifications you receive for organization spend.'
+      );
+    }
     const {title, description} = field;
 
     const [stateKey] = isProject ? this.getEndpoints()[2] : [];

+ 1 - 0
static/app/views/settings/account/notifications/fields.tsx

@@ -66,6 +66,7 @@ export const ACCOUNT_NOTIFICATION_FIELDS: Record<string, FineTuneField> = {
     // No choices here because it's going to have dynamic content
     // Component will create choices,
   },
+  // TODO(isabella): Once GA, replace the following with Spend Notifications
   quota: {
     title: t('Quota Notifications'),
     description: t(

+ 21 - 2
static/app/views/settings/account/notifications/fields2.tsx

@@ -76,7 +76,7 @@ export const NOTIFICATION_SETTING_FIELDS: Record<string, Field> = {
       ['always', t('On')],
       ['never', t('Off')],
     ],
-    help: t('Error, transaction, and attachment quota limits.'),
+    help: t('Error, transaction, replay, attachment, and cron monitor quota limits.'),
   },
   reports: {
     name: 'reports',
@@ -127,6 +127,7 @@ export const NOTIFICATION_SETTING_FIELDS: Record<string, Field> = {
   },
 };
 
+// TODO(isabella): Once spend vis notifs are GA, remove this
 // partial field definition for quota sub-categories
 export const QUOTA_FIELDS = [
   {
@@ -192,7 +193,7 @@ export const QUOTA_FIELDS = [
     name: 'quotaMonitorSeats',
     label: t('Cron Monitors'),
     help: tct(
-      'Receive notifications about your cron monitors quotas. [learnMore:Learn more]',
+      'Receive notifications about your cron monitor quotas. [learnMore:Learn more]',
       {
         learnMore: <ExternalLink href={getDocsLinkForEventType('monitorSeat')} />,
       }
@@ -217,3 +218,21 @@ export const QUOTA_FIELDS = [
     ] as const,
   },
 ];
+
+export const SPEND_FIELDS = [
+  {
+    name: 'quotaWarnings',
+    label: t('Spend Notifications'),
+    help: tct(
+      'Receive notifications when your spend crosses predefined or custom thresholds. [learnMore:Learn more]',
+      {
+        learnMore: <ExternalLink href={'#'} />, // TODO(isabella): replace with proper link
+      }
+    ),
+    choices: [
+      ['always', t('On')],
+      ['never', t('Off')],
+    ] as const,
+  },
+  ...QUOTA_FIELDS.slice(1),
+];

+ 23 - 0
static/app/views/settings/account/notifications/notificationSettings.spec.tsx

@@ -1,3 +1,5 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+
 import {initializeOrg} from 'sentry-test/initializeOrg';
 import {render, screen} from 'sentry-test/reactTestingLibrary';
 
@@ -74,4 +76,25 @@ describe('NotificationSettings', function () {
     }
     expect(screen.getByText('Issue Alerts')).toBeInTheDocument();
   });
+
+  it('renders spend section instead of quota section with feature flag', async function () {
+    const {routerContext, organization} = initializeOrg({
+      organization: {
+        features: ['slack-overage-notifications', 'spend-visibility-notifications'],
+      },
+    });
+
+    const organizationNoFlag = OrganizationFixture();
+    organizationNoFlag.features.push('slack-overage-notifications');
+
+    renderMockRequests({});
+
+    render(<NotificationSettings organizations={[organization, organizationNoFlag]} />, {
+      context: routerContext,
+    });
+
+    expect(await screen.findByText('Spend')).toBeInTheDocument();
+
+    expect(screen.queryByText('Quota')).not.toBeInTheDocument();
+  });
 });

+ 5 - 0
static/app/views/settings/account/notifications/notificationSettings.tsx

@@ -45,7 +45,12 @@ function NotificationSettings({organizations}: NotificationSettingsProps) {
   });
 
   const renderOneSetting = (type: string) => {
+    // TODO(isabella): Once GA, remove this
     const field = NOTIFICATION_SETTING_FIELDS[type];
+    if (type === 'quota' && checkFeatureFlag('spend-visibility-notifications')) {
+      field.label = t('Spend');
+      field.help = t('Notifications that help avoid surprise invoices.');
+    }
     return (
       <FieldWrapper key={type}>
         <div>

+ 16 - 0
static/app/views/settings/account/notifications/notificationSettingsByType.spec.tsx

@@ -259,4 +259,20 @@ describe('NotificationSettingsByType', function () {
     await selectEvent.select(multiSelect, ['Email']);
     expect(changeProvidersMock).toHaveBeenCalledTimes(1);
   });
+
+  it('renders spend notifications page instead of quota notifications with flag', async function () {
+    const organizationWithFlag = OrganizationFixture();
+    organizationWithFlag.features.push('spend-visibility-notifications');
+    const organizationNoFlag = OrganizationFixture();
+    renderComponent({
+      notificationType: 'quota',
+      organizations: [organizationWithFlag, organizationNoFlag],
+    });
+
+    expect(await screen.getAllByText('Spend Notifications').length).toEqual(2);
+    expect(screen.queryByText('Quota Notifications')).not.toBeInTheDocument();
+    expect(
+      screen.getByText('Control the notifications you receive for organization spend.')
+    ).toBeInTheDocument();
+  });
 });

+ 51 - 17
static/app/views/settings/account/notifications/notificationSettingsByType.tsx

@@ -27,7 +27,7 @@ import type {
 } from './constants';
 import {SUPPORTED_PROVIDERS} from './constants';
 import {ACCOUNT_NOTIFICATION_FIELDS} from './fields';
-import {NOTIFICATION_SETTING_FIELDS, QUOTA_FIELDS} from './fields2';
+import {NOTIFICATION_SETTING_FIELDS, QUOTA_FIELDS, SPEND_FIELDS} from './fields2';
 import NotificationSettingsByEntity from './notificationSettingsByEntity';
 import type {Identity} from './types';
 import UnlinkedAlert from './unlinkedAlert';
@@ -183,7 +183,7 @@ class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State
   }
 
   getFields(): Field[] {
-    const {notificationType} = this.props;
+    const {notificationType, organizations} = this.props;
 
     const help = isGroupedByProject(notificationType)
       ? t('This is the default for all projects.')
@@ -193,20 +193,42 @@ class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State
     // if a quota notification is not disabled, add in our dependent fields
     // but do not show the top level controller
     if (notificationType === 'quota') {
-      fields.push(
-        ...QUOTA_FIELDS.map(field => ({
-          ...field,
-          type: 'select' as const,
-          getData: data => {
-            return {
-              type: field.name,
-              scopeType: 'user',
-              scopeIdentifier: ConfigStore.get('user').id,
-              value: data[field.name],
-            };
-          },
-        }))
-      );
+      if (
+        organizations.some(organization =>
+          organization.features?.includes('spend-visibility-notifications')
+        )
+      ) {
+        fields.push(
+          ...SPEND_FIELDS.map(field => ({
+            ...field,
+            type: 'select' as const,
+            getData: data => {
+              return {
+                type: field.name,
+                scopeType: 'user',
+                scopeIdentifier: ConfigStore.get('user').id,
+                value: data[field.name],
+              };
+            },
+          }))
+        );
+      } else {
+        // TODO(isabella): Once GA, remove this case
+        fields.push(
+          ...QUOTA_FIELDS.map(field => ({
+            ...field,
+            type: 'select' as const,
+            getData: data => {
+              return {
+                type: field.name,
+                scopeType: 'user',
+                scopeIdentifier: ConfigStore.get('user').id,
+                value: data[field.name],
+              };
+            },
+          }))
+        );
+      }
     } else {
       const defaultField: Field = Object.assign(
         {},
@@ -342,10 +364,22 @@ class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent<Props, State
   };
 
   renderBody() {
-    const {notificationType} = this.props;
+    const {notificationType, organizations} = this.props;
     const {notificationOptions} = this.state;
     const unlinkedSlackOrgs = this.getUnlinkedOrgs('slack');
+    const notificationDetails = ACCOUNT_NOTIFICATION_FIELDS[notificationType];
+    // TODO(isabella): Once GA, remove this
+    if (
+      notificationType === 'quota' &&
+      organizations.some(org => org.features?.includes('spend-visibility-notifications'))
+    ) {
+      notificationDetails.title = t('Spend Notifications');
+      notificationDetails.description = t(
+        'Control the notifications you receive for organization spend.'
+      );
+    }
     const {title, description} = ACCOUNT_NOTIFICATION_FIELDS[notificationType];
+
     const entityType = isGroupedByProject(notificationType) ? 'project' : 'organization';
     return (
       <Fragment>