Browse Source

ref(billing): Iterate through categories for quota notifs FE (#80037)

Closes
https://getsentry.atlassian.net/browse/RV-1114?atlOrigin=eyJpIjoiOWJhNmM5ZDIyNTVhNDAyYmI0NzRmMjIxMzUwZGZkYTYiLCJwIjoiaiJ9
Isabella Enriquez 4 months ago
parent
commit
9c5bfff2d3

+ 11 - 0
static/app/constants/index.tsx

@@ -256,6 +256,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Errors'),
     productName: t('Error Monitoring'),
     uid: 1,
+    isBilledCategory: true,
   },
   [DataCategoryExact.TRANSACTION]: {
     name: DataCategoryExact.TRANSACTION,
@@ -265,6 +266,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Transactions'),
     productName: t('Performance Monitoring'),
     uid: 2,
+    isBilledCategory: true,
   },
   [DataCategoryExact.ATTACHMENT]: {
     name: DataCategoryExact.ATTACHMENT,
@@ -274,6 +276,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Attachments'),
     productName: t('Attachments'),
     uid: 4,
+    isBilledCategory: true,
   },
   [DataCategoryExact.PROFILE]: {
     name: DataCategoryExact.PROFILE,
@@ -283,6 +286,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Profiles'),
     productName: t('Continuous Profiling'),
     uid: 6,
+    isBilledCategory: false,
   },
   [DataCategoryExact.REPLAY]: {
     name: DataCategoryExact.REPLAY,
@@ -292,6 +296,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Session Replays'),
     productName: t('Session Replay'),
     uid: 7,
+    isBilledCategory: true,
   },
   [DataCategoryExact.TRANSACTION_PROCESSED]: {
     name: DataCategoryExact.TRANSACTION_PROCESSED,
@@ -301,6 +306,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Transactions'),
     productName: t('Performance Monitoring'),
     uid: 8,
+    isBilledCategory: false,
   },
   [DataCategoryExact.TRANSACTION_INDEXED]: {
     name: DataCategoryExact.TRANSACTION_INDEXED,
@@ -310,6 +316,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Indexed Transactions'),
     productName: t('Performance Monitoring'),
     uid: 9,
+    isBilledCategory: false,
   },
   [DataCategoryExact.MONITOR]: {
     name: DataCategoryExact.MONITOR,
@@ -319,6 +326,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Monitor Check-Ins'),
     productName: t('Cron Monitoring'),
     uid: 10,
+    isBilledCategory: false,
   },
   [DataCategoryExact.SPAN]: {
     name: DataCategoryExact.SPAN,
@@ -328,6 +336,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Spans'),
     productName: t('Tracing'),
     uid: 12,
+    isBilledCategory: true,
   },
   [DataCategoryExact.MONITOR_SEAT]: {
     name: DataCategoryExact.MONITOR_SEAT,
@@ -337,6 +346,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Cron Monitors'),
     productName: t('Cron Monitoring'),
     uid: 13,
+    isBilledCategory: true,
   },
   [DataCategoryExact.PROFILE_DURATION]: {
     name: DataCategoryExact.PROFILE_DURATION,
@@ -346,6 +356,7 @@ export const DATA_CATEGORY_INFO = {
     titleName: t('Profile Hours'),
     productName: t('Continuous Profiling'),
     uid: 17,
+    isBilledCategory: false, // TODO(Continuous Profiling GA): make true for launch to show spend notification toggle
   },
 } as const satisfies Record<DataCategoryExact, DataCategoryInfo>;
 

+ 1 - 0
static/app/types/core.tsx

@@ -104,6 +104,7 @@ export enum DataCategoryExact {
 export interface DataCategoryInfo {
   apiName: string;
   displayName: string;
+  isBilledCategory: boolean;
   name: DataCategoryExact;
   plural: string;
   productName: string;

+ 23 - 89
static/app/views/settings/account/notifications/fields2.tsx

@@ -1,8 +1,10 @@
 import {Fragment} from 'react';
+import upperFirst from 'lodash/upperFirst';
 
 import type {Field} from 'sentry/components/forms/types';
 import ExternalLink from 'sentry/components/links/externalLink';
 import QuestionTooltip from 'sentry/components/questionTooltip';
+import {DATA_CATEGORY_INFO} from 'sentry/constants';
 import {t, tct} from 'sentry/locale';
 import {getDocsLinkForEventType} from 'sentry/views/settings/account/notifications/utils';
 
@@ -139,6 +141,26 @@ export const NOTIFICATION_SETTING_FIELDS: Record<string, Field> = {
   },
 };
 
+const CATEGORY_QUOTA_FIELDS = Object.values(DATA_CATEGORY_INFO)
+  .filter(categoryInfo => categoryInfo.isBilledCategory)
+  .map(categoryInfo => {
+    return {
+      name: 'quota' + upperFirst(categoryInfo.plural),
+      label: categoryInfo.titleName,
+      help: tct(
+        `Receive notifications about your [displayName] quotas. [learnMore:Learn more]`,
+        {
+          displayName: categoryInfo.displayName,
+          learnMore: <ExternalLink href={getDocsLinkForEventType(categoryInfo.name)} />,
+        }
+      ),
+      choices: [
+        ['always', t('On')],
+        ['never', t('Off')],
+      ] as const,
+    };
+  });
+
 // TODO(isabella): Once spend vis notifs are GA, remove this
 // partial field definition for quota sub-categories
 export const QUOTA_FIELDS = [
@@ -151,95 +173,7 @@ export const QUOTA_FIELDS = [
       ['never', t('100%')],
     ] as const,
   },
-  {
-    name: 'quotaErrors',
-    label: t('Errors'),
-    help: tct('Receive notifications about your error quotas. [learnMore:Learn more]', {
-      learnMore: <ExternalLink href={getDocsLinkForEventType('error')} />,
-    }),
-    choices: [
-      ['always', t('On')],
-      ['never', t('Off')],
-    ] as const,
-  },
-  {
-    name: 'quotaTransactions',
-    label: t('Transactions'),
-    help: tct(
-      'Receive notifications about your transaction quota. [learnMore:Learn more]',
-      {
-        learnMore: <ExternalLink href={getDocsLinkForEventType('transaction')} />,
-      }
-    ),
-    choices: [
-      ['always', t('On')],
-      ['never', t('Off')],
-    ] as const,
-  },
-  {
-    name: 'quotaSpans',
-    label: t('Spans'),
-    help: tct('Receive notifications about your spans quotas. [learnMore:Learn more]', {
-      learnMore: <ExternalLink href={getDocsLinkForEventType('span')} />,
-    }),
-    choices: [
-      ['always', t('On')],
-      ['never', t('Off')],
-    ] as const,
-  },
-  {
-    name: 'quotaReplays',
-    label: t('Replays'),
-    help: tct('Receive notifications about your replay quotas. [learnMore:Learn more]', {
-      learnMore: <ExternalLink href={getDocsLinkForEventType('replay')} />,
-    }),
-    choices: [
-      ['always', t('On')],
-      ['never', t('Off')],
-    ] as const,
-  },
-  {
-    name: 'quotaAttachments',
-    label: t('Attachments'),
-    help: tct(
-      'Receive notifications about your attachment quota. [learnMore:Learn more]',
-      {
-        learnMore: <ExternalLink href={getDocsLinkForEventType('attachment')} />,
-      }
-    ),
-    choices: [
-      ['always', t('On')],
-      ['never', t('Off')],
-    ] as const,
-  },
-  {
-    name: 'quotaMonitorSeats',
-    label: t('Cron Monitors'),
-    help: tct(
-      'Receive notifications about your cron monitor quotas. [learnMore:Learn more]',
-      {
-        learnMore: <ExternalLink href={getDocsLinkForEventType('monitorSeat')} />,
-      }
-    ),
-    choices: [
-      ['always', t('On')],
-      ['never', t('Off')],
-    ] as const,
-  },
-  {
-    name: 'quotaProfileDuration',
-    label: t('Continuous Profiling'),
-    help: tct(
-      'Receive notifications about your continuous profiling quota. [learnMore:Learn more]',
-      {
-        learnMore: <ExternalLink href={getDocsLinkForEventType('profileDuration')} />,
-      }
-    ),
-    choices: [
-      ['always', t('On')],
-      ['never', t('Off')],
-    ] as const,
-  },
+  ...CATEGORY_QUOTA_FIELDS,
   {
     name: 'quotaSpendAllocations',
     label: (

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

@@ -147,7 +147,7 @@ describe('NotificationSettingsByType', function () {
     expect(projectsMock).toHaveBeenCalledTimes(1);
   });
 
-  it('renders all the quota subcatories', async function () {
+  it('renders all the quota subcategories', async function () {
     renderComponent({notificationType: 'quota'});
 
     // check for all the quota subcategories
@@ -161,7 +161,7 @@ describe('NotificationSettingsByType', function () {
     ).toBeInTheDocument();
     expect(screen.getByText('Errors')).toBeInTheDocument();
     expect(screen.getByText('Transactions')).toBeInTheDocument();
-    expect(screen.getByText('Replays')).toBeInTheDocument();
+    expect(screen.getByText('Session Replays')).toBeInTheDocument();
     expect(screen.getByText('Attachments')).toBeInTheDocument();
     expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
     expect(screen.queryByText('Spans')).not.toBeInTheDocument();
@@ -330,10 +330,10 @@ describe('NotificationSettingsByType', function () {
 
     expect(screen.getByText('Errors')).toBeInTheDocument();
     expect(screen.getByText('Spans')).toBeInTheDocument();
-    expect(screen.getByText('Replays')).toBeInTheDocument();
+    expect(screen.getByText('Session Replays')).toBeInTheDocument();
     expect(screen.getByText('Attachments')).toBeInTheDocument();
     expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
-    expect(screen.getByText('Continuous Profiling')).toBeInTheDocument();
+    expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument(); // TODO(Continuous Profiling GA): should be in document
     expect(screen.queryByText('Transactions')).not.toBeInTheDocument();
 
     const editSettingMock = MockApiClient.addMockResponse({
@@ -349,7 +349,7 @@ describe('NotificationSettingsByType', function () {
     });
 
     // toggle spans quota notifications off
-    await selectEvent.select(screen.getAllByText('On')[2], 'Off');
+    await selectEvent.select(screen.getAllByText('On')[4], 'Off');
 
     expect(editSettingMock).toHaveBeenCalledTimes(1);
     expect(editSettingMock).toHaveBeenCalledWith(
@@ -379,11 +379,11 @@ describe('NotificationSettingsByType', function () {
 
     expect(screen.getByText('Errors')).toBeInTheDocument();
     expect(screen.getByText('Spans')).toBeInTheDocument();
-    expect(screen.getByText('Replays')).toBeInTheDocument();
+    expect(screen.getByText('Session Replays')).toBeInTheDocument();
     expect(screen.getByText('Attachments')).toBeInTheDocument();
     expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
     expect(screen.getByText('Transactions')).toBeInTheDocument();
-    expect(screen.getByText('Continuous Profiling')).toBeInTheDocument();
+    expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument(); // TODO(Continuous Profiling GA): should be in document
   });
 
   it('spend notifications on org with am1 org only', async function () {
@@ -399,7 +399,7 @@ describe('NotificationSettingsByType', function () {
     expect(await screen.getAllByText('Spend Notifications').length).toEqual(2);
 
     expect(screen.getByText('Errors')).toBeInTheDocument();
-    expect(screen.getByText('Replays')).toBeInTheDocument();
+    expect(screen.getByText('Session Replays')).toBeInTheDocument();
     expect(screen.getByText('Attachments')).toBeInTheDocument();
     expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
     expect(screen.getByText('Transactions')).toBeInTheDocument();
@@ -420,10 +420,10 @@ describe('NotificationSettingsByType', function () {
 
     expect(screen.getByText('Errors')).toBeInTheDocument();
     expect(screen.getByText('Spans')).toBeInTheDocument();
-    expect(screen.getByText('Replays')).toBeInTheDocument();
+    expect(screen.getByText('Session Replays')).toBeInTheDocument();
     expect(screen.getByText('Attachments')).toBeInTheDocument();
     expect(screen.getByText('Spend Allocations')).toBeInTheDocument();
-    expect(screen.getByText('Continuous Profiling')).toBeInTheDocument();
+    expect(screen.queryByText('Continuous Profiling')).not.toBeInTheDocument(); // TODO(Continuous Profiling GA): should be in document
     expect(screen.queryByText('Transactions')).not.toBeInTheDocument();
 
     const editSettingMock = MockApiClient.addMockResponse({
@@ -439,7 +439,7 @@ describe('NotificationSettingsByType', function () {
     });
 
     // toggle spans quota notifications off
-    await selectEvent.select(screen.getAllByText('On')[1], 'Off');
+    await selectEvent.select(screen.getAllByText('On')[3], 'Off');
 
     expect(editSettingMock).toHaveBeenCalledTimes(1);
     expect(editSettingMock).toHaveBeenCalledWith(

+ 8 - 14
static/app/views/settings/account/notifications/utils.tsx

@@ -1,3 +1,4 @@
+import {DataCategoryExact} from 'sentry/types/core';
 import type {OrganizationSummary} from 'sentry/types/organization';
 import type {Project} from 'sentry/types/project';
 import {NOTIFICATION_SETTINGS_PATHNAMES} from 'sentry/views/settings/account/notifications/constants';
@@ -37,29 +38,22 @@ export const groupByOrganization = (
  * Returns a link to docs on explaining how to manage quotas for that event type
  */
 export function getDocsLinkForEventType(
-  event:
-    | 'error'
-    | 'transaction'
-    | 'attachment'
-    | 'replay'
-    | 'monitorSeat'
-    | 'span'
-    | 'profileDuration'
+  event: DataCategoryExact | string // TODO(isabella): get rid of strings after removing need for backward compatibility on gs
 ) {
   switch (event) {
-    case 'transaction':
+    case DataCategoryExact.TRANSACTION || 'transaction':
       // For pre-AM3 plans prior to June 11th, 2024
       return 'https://docs.sentry.io/pricing/quotas/legacy-manage-transaction-quota/';
-    case 'span':
+    case DataCategoryExact.SPAN || 'span':
       // For post-AM3 plans after June 11th, 2024
       return 'https://docs.sentry.io/pricing/quotas/manage-transaction-quota/';
-    case 'attachment':
+    case DataCategoryExact.ATTACHMENT || 'attachment':
       return 'https://docs.sentry.io/product/accounts/quotas/manage-attachments-quota/#2-rate-limiting';
-    case 'replay':
+    case DataCategoryExact.REPLAY || 'replay':
       return 'https://docs.sentry.io/product/session-replay/';
-    case 'monitorSeat':
+    case DataCategoryExact.MONITOR_SEAT || 'monitorSeat':
       return 'https://docs.sentry.io/product/crons/';
-    case 'profileDuration':
+    case DataCategoryExact.PROFILE_DURATION || 'profileDuration':
       return 'https://docs.sentry.io/product/explore/profiling/';
     default:
       return 'https://docs.sentry.io/product/accounts/quotas/manage-event-stream-guide/#common-workflows-for-managing-your-event-stream';