Repository: getsentry/sentry
Pull request: feat(inbound-filters): Update settings page to show new legacy browser filters
this pr updates the inbound filters settings page to show the new legacy browser filters. Showing the new filters will be controlled by feature flag. 

There are a few scenarios covered in this update. If the feature flag is off, then only the old filters will be shown. If the flag is on, the new filters will be shown + any filters the project already had. For example, a new project created with the feature flag, will only show the new 8 filters. An existing project with the existing Safari and Android filters on will show these 2 + all the new filters. If they turn off the old filters, they will not be able to turn them back on. This is meant to help transition over to using the new filters. 

<img width="1061" alt="Screenshot 2024-01-22 at 11 44 17 AM" src="https://github.com/getsentry/sentry/assets/46740234/00af0351-0bc4-469f-8fce-b47ee44b173b">


closes https://github.com/getsentry/sentry/issues/63057

Problem statement:
Update settings page to show new filters
Update the settings page to show the new filters. This will be rolled out gradually. users with existing filters will be shown the new filter as being on and will still be able to turn off the filter.

Hints from issue discussion:

Commit message: fix test

Patch to review:
diff --git a/static/app/views/settings/project/projectFilters/index.spec.tsx b/static/app/views/settings/project/projectFilters/index.spec.tsx
index 4af37962f14098..6abc2246029f6d 100644
--- a/static/app/views/settings/project/projectFilters/index.spec.tsx
+++ b/static/app/views/settings/project/projectFilters/index.spec.tsx
@@ -109,15 +109,17 @@ describe('ProjectFilters', function () {
     renderComponent();
 
     expect(
-      await screen.findByRole('checkbox', {name: 'Internet Explorer Version 8 and lower'})
+      await screen.findByRole('checkbox', {
+        name: 'Internet Explorer (Deprecated) Version 8 and lower',
+      })
     ).toBeChecked();
 
     expect(
-      screen.getByRole('checkbox', {name: 'Internet Explorer Version 9'})
+      screen.getByRole('checkbox', {name: 'Internet Explorer (Deprecated) Version 9'})
     ).toBeChecked();
 
     expect(
-      screen.getByRole('checkbox', {name: 'Internet Explorer Version 10'})
+      screen.getByRole('checkbox', {name: 'Internet Explorer (Deprecated) Version 10'})
     ).not.toBeChecked();
   });
 
@@ -128,7 +130,9 @@ describe('ProjectFilters', function () {
     const mock = createFilterMock(filter);
 
     await userEvent.click(
-      await screen.findByRole('checkbox', {name: 'Safari Version 5 and lower'})
+      await screen.findByRole('checkbox', {
+        name: 'Safari (Deprecated) Version 5 and lower',
+      })
     );
     expect(mock.mock.calls[0][0]).toBe(getFilterEndpoint(filter));
     // Have to do this because no jest matcher for JS Set
@@ -140,7 +144,7 @@ describe('ProjectFilters', function () {
 
     // Toggle filter off
     await userEvent.click(
-      screen.getByRole('checkbox', {name: 'Internet Explorer Version 11'})
+      screen.getByRole('checkbox', {name: 'Internet Explorer (Deprecated) Version 11'})
     );
     expect(Array.from(mock.mock.calls[1][1].data.subfilters)).toEqual([
       'ie_pre_9',
@@ -153,10 +157,12 @@ describe('ProjectFilters', function () {
 
     // Click ie9 and < ie9
     await userEvent.click(
-      screen.getByRole('checkbox', {name: 'Internet Explorer Version 9'})
+      screen.getByRole('checkbox', {name: 'Internet Explorer (Deprecated) Version 9'})
     );
     await userEvent.click(
-      screen.getByRole('checkbox', {name: 'Internet Explorer Version 8 and lower'})
+      screen.getByRole('checkbox', {
+        name: 'Internet Explorer (Deprecated) Version 8 and lower',
+      })
     );
 
     expect(Array.from(mock.mock.calls[1][1].data.subfilters)).toEqual([
diff --git a/static/app/views/settings/project/projectFilters/projectFiltersSettings.tsx b/static/app/views/settings/project/projectFilters/projectFiltersSettings.tsx
index 4079ea6bdd6406..9613f4718df01f 100644
--- a/static/app/views/settings/project/projectFilters/projectFiltersSettings.tsx
+++ b/static/app/views/settings/project/projectFilters/projectFiltersSettings.tsx
@@ -1,7 +1,9 @@
 import {Component, Fragment, useCallback} from 'react';
 import styled from '@emotion/styled';
 import iconAndroid from 'sentry-logos/logo-android.svg';
+import iconChrome from 'sentry-logos/logo-chrome.svg';
 import iconEdgeLegacy from 'sentry-logos/logo-edge-old.svg';
+import iconFirefox from 'sentry-logos/logo-firefox.svg';
 import iconIe from 'sentry-logos/logo-ie.svg';
 import iconOpera from 'sentry-logos/logo-opera.svg';
 import iconSafari from 'sentry-logos/logo-safari.svg';
@@ -12,6 +14,8 @@ import FeatureDisabled from 'sentry/components/acl/featureDisabled';
 import {Button} from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
 import FieldFromConfig from 'sentry/components/forms/fieldFromConfig';
+import FieldHelp from 'sentry/components/forms/fieldGroup/fieldHelp';
+import FieldLabel from 'sentry/components/forms/fieldGroup/fieldLabel';
 import Form, {FormProps} from 'sentry/components/forms/form';
 import FormField from 'sentry/components/forms/formField';
 import JsonForm from 'sentry/components/forms/jsonForm';
@@ -75,59 +79,114 @@ const filterDescriptions = {
 const LEGACY_BROWSER_SUBFILTERS = {
   ie_pre_9: {
     icon: iconIe,
-    helpText: 'Version 8 and lower',
+    helpText: '(Deprecated) Version 8 and lower',
     title: 'Internet Explorer',
+    legacy: true,
   },
   ie9: {
     icon: iconIe,
-    helpText: 'Version 9',
+    helpText: '(Deprecated) Version 9',
     title: 'Internet Explorer',
+    legacy: true,
   },
   ie10: {
     icon: iconIe,
-    helpText: 'Version 10',
+    helpText: '(Deprecated) Version 10',
     title: 'Internet Explorer',
+    legacy: true,
   },
   ie11: {
     icon: iconIe,
-    helpText: 'Version 11',
+    helpText: '(Deprecated) Version 11',
     title: 'Internet Explorer',
+    legacy: true,
   },
   safari_pre_6: {
     icon: iconSafari,
-    helpText: 'Version 5 and lower',
+    helpText: '(Deprecated) Version 5 and lower',
     title: 'Safari',
+    legacy: true,
   },
   opera_pre_15: {
     icon: iconOpera,
-    helpText: 'Version 14 and lower',
+    helpText: '(Deprecated) Version 14 and lower',
     title: 'Opera',
+    legacy: true,
   },
   opera_mini_pre_8: {
     icon: iconOpera,
-    helpText: 'Version 8 and lower',
+    helpText: '(Deprecated) Version 8 and lower',
     title: 'Opera Mini',
+    legacy: true,
   },
   android_pre_4: {
     icon: iconAndroid,
-    helpText: 'Version 3 and lower',
+    helpText: '(Deprecated) Version 3 and lower',
     title: 'Android',
+    legacy: true,
   },
   edge_pre_79: {
     icon: iconEdgeLegacy,
-    helpText: 'Version 18 and lower',
+    helpText: '(Deprecated) Version 18 and lower',
     title: 'Edge (Legacy)',
+    legacy: true,
+  },
+  ie: {
+    icon: iconIe,
+    title: 'Internet Explorer',
+    helpText: 'Verison 11 and lower',
+    legacy: false,
+  },
+  safari: {
+    icon: iconSafari,
+    title: 'Safari',
+    helpText: 'Version 11 and lower',
+    legacy: false,
+  },
+  opera: {
+    icon: iconOpera,
+    title: 'Opera',
+    helpText: 'Version 50 and lower',
+  },
+  opera_mini: {
+    icon: iconOpera,
+    title: 'Opera Mini',
+    helpText: 'Version 34 and lower',
+    legacy: false,
+  },
+  android: {
+    icon: iconAndroid,
+    title: 'Android',
+    helpText: 'Version 3 and lower',
+    legacy: false,
+  },
+  edge: {
+    icon: iconEdgeLegacy,
+    title: 'Edge',
+    helpText: 'Version 78 and lower',
+    legacy: false,
+  },
+  firefox: {
+    icon: iconFirefox,
+    title: 'Firefox',
+    helpText: 'Version 66 and lower',
+    legacy: false,
+  },
+  chrome: {
+    icon: iconChrome,
+    title: 'Chrome',
+    helpText: 'Version 62 and lower',
+    legacy: false,
   },
 };
 
-const LEGACY_BROWSER_KEYS = Object.keys(LEGACY_BROWSER_SUBFILTERS);
-
 type FormFieldProps = React.ComponentProps<typeof FormField>;
 
 type RowProps = {
   data: {
     active: string[] | boolean;
   };
+  hasLegacyBrowserUpdate: boolean;
   onToggle: (
     data: RowProps['data'],
     filters: RowState['subfilters'],
@@ -145,9 +204,15 @@ type RowState = {
 class LegacyBrowserFilterRow extends Component<RowProps, RowState> {
   constructor(props) {
     super(props);
+
     let initialSubfilters;
     if (props.data.active === true) {
-      initialSubfilters = new Set(LEGACY_BROWSER_KEYS);
+      initialSubfilters = new Set(
+        Object.keys(LEGACY_BROWSER_SUBFILTERS).filter(
+          key =>
+            LEGACY_BROWSER_SUBFILTERS[key].legacy === !this.props.hasLegacyBrowserUpdate
+        )
+      );
     } else if (props.data.active === false) {
       initialSubfilters = new Set();
     } else {
@@ -165,7 +230,12 @@ class LegacyBrowserFilterRow extends Component<RowProps, RowState> {
     let {subfilters} = this.state;
 
     if (subfilter === true) {
-      subfilters = new Set(LEGACY_BROWSER_KEYS);
+      subfilters = new Set(
+        Object.keys(LEGACY_BROWSER_SUBFILTERS).filter(
+          key =>
+            LEGACY_BROWSER_SUBFILTERS[key].legacy === !this.props.hasLegacyBrowserUpdate
+        )
+      );
     } else if (subfilter === false) {
       subfilters = new Set();
     } else if (subfilters.has(subfilter)) {
@@ -189,48 +259,65 @@ class LegacyBrowserFilterRow extends Component<RowProps, RowState> {
     return (
       <div>
         {!disabled && (
-          <BulkFilter>
-            <BulkFilterLabel>{t('Filter')}:</BulkFilterLabel>
-            <ButtonBar gap={1}>
-              <Button
-                priority="link"
-                borderless
-                onClick={this.handleToggleSubfilters.bind(this, true)}
-              >
-                {t('All')}
-              </Button>
-              <Button
-                priority="link"
-                borderless
-                onClick={this.handleToggleSubfilters.bind(this, false)}
-              >
-                {t('None')}
-              </Button>
-            </ButtonBar>
-          </BulkFilter>
+          <div>
+            <BulkFilter>
+              <FieldLabel>{t('Legacy Browser Filters')}:</FieldLabel>
+              <ButtonBar gap={1}>
+                <Button
+                  priority="link"
+                  borderless
+                  onClick={this.handleToggleSubfilters.bind(this, true)}
+                >
+                  {t('All')}
+                </Button>
+                <Button
+                  priority="link"
+                  borderless
+                  onClick={this.handleToggleSubfilters.bind(this, false)}
+                >
+                  {t('None')}
+                </Button>
+              </ButtonBar>
+            </BulkFilter>
+            <FieldHelp>
+              {t(
+                'The browser versions filtered out will be periodically evaluated and updated'
+              )}
+            </FieldHelp>
+          </div>
         )}
 
         <FilterGrid>
-          {LEGACY_BROWSER_KEYS.map(key => {
-            const subfilter = LEGACY_BROWSER_SUBFILTERS[key];
-            return (
-              <FilterGridItem key={key}>
-                <FilterGridIcon src={subfilter.icon} />
-                <div>
-                  <FilterTitle>{subfilter.title}</FilterTitle>
-                  <FilterDescription>{subfilter.helpText}</FilterDescription>
-                </div>
-                <Switch
-                  aria-label={`${subfilter.title} ${subfilter.helpText}`}
-                  isActive={this.state.subfilters.has(key)}
-                  isDisabled={disabled}
-                  css={{flexShrink: 0, marginLeft: 6}}
-                  toggle={this.handleToggleSubfilters.bind(this, key)}
-                  size="lg"
-                />
-              </FilterGridItem>
-            );
-          })}
+          {Object.keys(LEGACY_BROWSER_SUBFILTERS)
+            .filter(key => {
+              if (this.props.hasLegacyBrowserUpdate) {
+                if (!LEGACY_BROWSER_SUBFILTERS[key].legacy) {
+                  return true;
+                }
+                return [...this.state.subfilters].includes(key);
+              }
+              return LEGACY_BROWSER_SUBFILTERS[key].legacy;
+            })
+            .map(key => {
+              const subfilter = LEGACY_BROWSER_SUBFILTERS[key];
+              return (
+                <FilterGridItem key={key}>
+                  <FilterGridIcon src={subfilter.icon} />
+                  <div>
+                    <FilterTitle>{subfilter.title}</FilterTitle>
+                    <FilterDescription>{subfilter.helpText}</FilterDescription>
+                  </div>
+                  <Switch
+                    aria-label={`${subfilter.title} ${subfilter.helpText}`}
+                    isActive={[...this.state.subfilters].some(sf => sf === key)}
+                    isDisabled={disabled}
+                    css={{flexShrink: 0, marginLeft: 6}}
+                    toggle={this.handleToggleSubfilters.bind(this, key)}
+                    size="lg"
+                  />
+                </FilterGridItem>
+              );
+            })}
         </FilterGrid>
       </div>
     );
@@ -305,7 +392,7 @@ type Props = {
 };
 
 type Filter = {
-  active: boolean;
+  active: boolean | string[];
   description: string;
   hello: string;
   id: string;
@@ -316,6 +403,8 @@ export function ProjectFiltersSettings({project, params, features}: Props) {
   const organization = useOrganization();
   const {projectId: projectSlug} = params;
 
+  const hasLegacyBrowserUpdate = organization.features.includes('legacy-browser-update');
+
   const projectEndpoint = `/projects/${organization.slug}/${projectSlug}/`;
   const filtersEndpoint = `${projectEndpoint}filters/`;
 
@@ -419,6 +508,7 @@ export function ProjectFiltersSettings({project, params, features}: Props) {
                               onToggle={(_data, subfilters, event) =>
                                 handleLegacyChange({onChange, onBlur, event, subfilters})
                               }
+                              hasLegacyBrowserUpdate={hasLegacyBrowserUpdate}
                             />
                           )}
                         </FormField>
@@ -569,8 +659,3 @@ const BulkFilter = styled('div')`
   align-items: center;
   gap: ${space(0.5)};
 `;
-
-const BulkFilterLabel = styled('span')`
-  font-weight: bold;
-  margin-right: ${space(0.75)};
-`;


Code review comments:
diff_hunk: @@ -189,48 +259,65 @@ class LegacyBrowserFilterRow extends Component<RowProps, RowState> {
     return (
       <div>
         {!disabled && (
-          <BulkFilter>
-            <BulkFilterLabel>{t('Filter')}:</BulkFilterLabel>
-            <ButtonBar gap={1}>
-              <Button
-                priority="link"
-                borderless
-                onClick={this.handleToggleSubfilters.bind(this, true)}
-              >
-                {t('All')}
-              </Button>
-              <Button
-                priority="link"
-                borderless
-                onClick={this.handleToggleSubfilters.bind(this, false)}
-              >
-                {t('None')}
-              </Button>
-            </ButtonBar>
-          </BulkFilter>
+          <div>
+            <BulkFilter>
+              <FieldLabel>{t('Legacy Browser Filters')}:</FieldLabel>
+              <ButtonBar gap={1}>
+                <Button
+                  priority="link"
+                  borderless
+                  onClick={this.handleToggleSubfilters.bind(this, true)}
+                >
+                  {t('All')}
+                </Button>
+                <Button
+                  priority="link"
+                  borderless
+                  onClick={this.handleToggleSubfilters.bind(this, false)}
+                >
+                  {t('None')}
+                </Button>
+              </ButtonBar>
+            </BulkFilter>
+            <FieldHelp>
+              {t(
+                'The browser versions filtered out will be periodically evaluated and updated'
+              )}
+            </FieldHelp>
+          </div>
         )}
 
         <FilterGrid>
-          {LEGACY_BROWSER_KEYS.map(key => {
-            const subfilter = LEGACY_BROWSER_SUBFILTERS[key];
-            return (
-              <FilterGridItem key={key}>
-                <FilterGridIcon src={subfilter.icon} />
-                <div>
-                  <FilterTitle>{subfilter.title}</FilterTitle>
-                  <FilterDescription>{subfilter.helpText}</FilterDescription>
-                </div>
-                <Switch
-                  aria-label={`${subfilter.title} ${subfilter.helpText}`}
-                  isActive={this.state.subfilters.has(key)}
-                  isDisabled={disabled}
-                  css={{flexShrink: 0, marginLeft: 6}}
-                  toggle={this.handleToggleSubfilters.bind(this, key)}
-                  size="lg"
-                />
-              </FilterGridItem>
-            );
-          })}
+          {Object.keys(LEGACY_BROWSER_SUBFILTERS)
+            .filter(key => {
+              if (this.props.hasLegacyBrowserUpdate) {
+                if (!LEGACY_BROWSER_SUBFILTERS[key].legacy) {
+                  return true;
+                }
+                return [...this.state.subfilters].includes(key);
path: static/app/views/settings/project/projectFilters/projectFiltersSettings.tsx
text: @user1:
use the set functions if possible
```suggestion
                return this.state.subfilters.has(key);
```