Browse Source

feat(onboarding): generic banner for unsupported features (#71201)

Adding a generic banner for when a feature is not available for a
platform. Replay already has one which is where I got the design from.

The code most probably change so feel free to review but I'm still
working on the profiling test. Need to mock the filter.


![Screenshot 2024-05-17 at 3 49
13 PM](https://github.com/getsentry/sentry/assets/132939361/e980eac7-3ce3-4b73-8095-efd3c23a36fc)
![Screenshot 2024-05-17 at 11 15
25 AM](https://github.com/getsentry/sentry/assets/132939361/8d7e2267-b352-40bb-9c1b-3bc459392632)
Athena Moghaddam 9 months ago
parent
commit
66bbc4b33f

+ 20 - 0
static/app/components/alerts/unsupportedAlert.tsx

@@ -0,0 +1,20 @@
+import Alert from 'sentry/components/alert';
+import {IconInfo} from 'sentry/icons';
+import {t} from 'sentry/locale';
+
+interface Props {
+  featureName: string;
+  projectSlug?: string;
+}
+
+export default function UnsupportedAlert({featureName, projectSlug}: Props) {
+  return (
+    <Alert data-test-id="unsupported-alert" icon={<IconInfo />}>
+      {projectSlug ? (
+        <strong>{t(`%s isn't available for %s.`, featureName, projectSlug)}</strong>
+      ) : (
+        <strong>{t(`%s isn't available for the selected projects.`, featureName)}</strong>
+      )}
+    </Alert>
+  );
+}

+ 1 - 1
static/app/components/smartSearchBar/index.tsx

@@ -477,7 +477,7 @@ class SmartSearchBar extends Component<DefaultProps & Props, State> {
 
   componentWillUnmount() {
     this.inputResizeObserver?.disconnect();
-    this.updateAutoCompleteItems.cancel();
+    this.updateAutoCompleteItems?.cancel();
     document.removeEventListener('pointerup', this.onBackgroundPointerUp);
   }
 

+ 28 - 0
static/app/views/performance/onboarding.spec.tsx

@@ -0,0 +1,28 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import Onboarding from 'sentry/views/performance/onboarding';
+
+describe('Performance Onboarding View > Unsupported Banner', function () {
+  const organization = OrganizationFixture();
+
+  it('Displays unsupported banner for unsupported projects', function () {
+    const project = ProjectFixture({
+      platform: 'nintendo-switch',
+    });
+    render(<Onboarding organization={organization} project={project} />);
+
+    expect(screen.getByTestId('unsupported-alert')).toBeInTheDocument();
+  });
+
+  it('Does not display unsupported banner for supported projects', function () {
+    const project = ProjectFixture({
+      platform: 'java',
+    });
+    render(<Onboarding organization={organization} project={project} />);
+
+    expect(screen.queryByTestId('unsupported-alert')).not.toBeInTheDocument();
+  });
+});

+ 79 - 68
static/app/views/performance/onboarding.tsx

@@ -1,4 +1,4 @@
-import {useEffect} from 'react';
+import {Fragment, useEffect} from 'react';
 import styled from '@emotion/styled';
 import * as Sentry from '@sentry/react';
 
@@ -13,6 +13,7 @@ import {
   addLoadingMessage,
   clearIndicators,
 } from 'sentry/actionCreators/indicator';
+import UnsupportedAlert from 'sentry/components/alerts/unsupportedAlert';
 import {Button} from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
 import type {TourStep} from 'sentry/components/modals/featureTourModal';
@@ -23,7 +24,10 @@ import FeatureTourModal, {
 import OnboardingPanel from 'sentry/components/onboardingPanel';
 import {filterProjects} from 'sentry/components/performanceOnboarding/utils';
 import {SidebarPanelKey} from 'sentry/components/sidebar/types';
-import {withPerformanceOnboarding} from 'sentry/data/platformCategories';
+import {
+  withoutPerformanceSupport,
+  withPerformanceOnboarding,
+} from 'sentry/data/platformCategories';
 import {t} from 'sentry/locale';
 import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
 import type {Organization} from 'sentry/types/organization';
@@ -140,6 +144,8 @@ function Onboarding({organization, project}: Props) {
   const hasPerformanceOnboarding = currentPlatform
     ? withPerformanceOnboarding.has(currentPlatform)
     : false;
+  const noPerformanceSupport =
+    currentPlatform && withoutPerformanceSupport.has(currentPlatform);
 
   let setupButton = (
     <Button
@@ -167,76 +173,81 @@ function Onboarding({organization, project}: Props) {
   }
 
   return (
-    <OnboardingPanel image={<PerfImage src={emptyStateImg} />}>
-      <h3>{t('Pinpoint problems')}</h3>
-      <p>
-        {t(
-          'Something seem slow? Track down transactions to connect the dots between 10-second page loads and poor-performing API calls or slow database queries.'
-        )}
-      </p>
-      <ButtonList gap={1}>
-        {setupButton}
-        <Button
-          data-test-id="create-sample-transaction-btn"
-          onClick={async () => {
-            trackAnalytics('performance_views.create_sample_transaction', {
-              platform: project.platform,
-              organization,
-            });
-            addLoadingMessage(t('Processing sample event...'), {
-              duration: 15000,
-            });
-            const url = `/projects/${organization.slug}/${project.slug}/create-sample-transaction/`;
-            try {
-              const eventData = await api.requestPromise(url, {method: 'POST'});
-              const traceSlug = eventData.contexts?.trace?.trace_id ?? '';
-
-              browserHistory.push(
-                generateLinkToEventInTraceView({
-                  eventId: eventData.eventID,
-                  location,
-                  projectSlug: project.slug,
-                  organization,
-                  timestamp: eventData.endTimestamp,
-                  traceSlug,
-                  demo: `${project.slug}:${eventData.eventID}`,
-                })
-              );
-              clearIndicators();
-            } catch (error) {
-              Sentry.withScope(scope => {
-                scope.setExtra('error', error);
-                Sentry.captureException(new Error('Failed to create sample event'));
-              });
-              clearIndicators();
-              addErrorMessage(t('Failed to create a new sample event'));
-              return;
-            }
-          }}
-        >
-          {t('View Sample Transaction')}
-        </Button>
-      </ButtonList>
-      <FeatureTourModal
-        steps={PERFORMANCE_TOUR_STEPS}
-        onAdvance={handleAdvance}
-        onCloseModal={handleClose}
-        doneUrl={performanceSetupUrl}
-        doneText={t('Start Setup')}
-      >
-        {({showModal}) => (
+    <Fragment>
+      {noPerformanceSupport && (
+        <UnsupportedAlert projectSlug={project.slug} featureName="Performance" />
+      )}
+      <OnboardingPanel image={<PerfImage src={emptyStateImg} />}>
+        <h3>{t('Pinpoint problems')}</h3>
+        <p>
+          {t(
+            'Something seem slow? Track down transactions to connect the dots between 10-second page loads and poor-performing API calls or slow database queries.'
+          )}
+        </p>
+        <ButtonList gap={1}>
+          {setupButton}
           <Button
-            priority="link"
-            onClick={() => {
-              trackAnalytics('performance_views.tour.start', {organization});
-              showModal();
+            data-test-id="create-sample-transaction-btn"
+            onClick={async () => {
+              trackAnalytics('performance_views.create_sample_transaction', {
+                platform: project.platform,
+                organization,
+              });
+              addLoadingMessage(t('Processing sample event...'), {
+                duration: 15000,
+              });
+              const url = `/projects/${organization.slug}/${project.slug}/create-sample-transaction/`;
+              try {
+                const eventData = await api.requestPromise(url, {method: 'POST'});
+                const traceSlug = eventData.contexts?.trace?.trace_id ?? '';
+
+                browserHistory.push(
+                  generateLinkToEventInTraceView({
+                    eventId: eventData.eventID,
+                    location,
+                    projectSlug: project.slug,
+                    organization,
+                    timestamp: eventData.endTimestamp,
+                    traceSlug,
+                    demo: `${project.slug}:${eventData.eventID}`,
+                  })
+                );
+                clearIndicators();
+              } catch (error) {
+                Sentry.withScope(scope => {
+                  scope.setExtra('error', error);
+                  Sentry.captureException(new Error('Failed to create sample event'));
+                });
+                clearIndicators();
+                addErrorMessage(t('Failed to create a new sample event'));
+                return;
+              }
             }}
           >
-            {t('Take a Tour')}
+            {t('View Sample Transaction')}
           </Button>
-        )}
-      </FeatureTourModal>
-    </OnboardingPanel>
+        </ButtonList>
+        <FeatureTourModal
+          steps={PERFORMANCE_TOUR_STEPS}
+          onAdvance={handleAdvance}
+          onCloseModal={handleClose}
+          doneUrl={performanceSetupUrl}
+          doneText={t('Start Setup')}
+        >
+          {({showModal}) => (
+            <Button
+              priority="link"
+              onClick={() => {
+                trackAnalytics('performance_views.tour.start', {organization});
+                showModal();
+              }}
+            >
+              {t('Take a Tour')}
+            </Button>
+          )}
+        </FeatureTourModal>
+      </OnboardingPanel>
+    </Fragment>
   );
 }
 

+ 58 - 0
static/app/views/profiling/content.spec.tsx

@@ -0,0 +1,58 @@
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import PageFiltersStore from 'sentry/stores/pageFiltersStore';
+import ProjectsStore from 'sentry/stores/projectsStore';
+import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
+import ProfilingContent from 'sentry/views/profiling/content';
+
+jest.mock('sentry/utils/profiling/hooks/useProfileFilters');
+
+describe('profiling Onboarding View > Unsupported Banner', function () {
+  const {router} = initializeOrg({
+    router: {
+      location: {query: {}, search: '', pathname: '/test/'},
+    },
+  });
+
+  beforeEach(function () {
+    jest.resetAllMocks();
+    PageFiltersStore.init();
+    PageFiltersStore.onInitializeUrlState(
+      {
+        projects: [],
+        environments: [],
+        datetime: {start: null, end: null, period: '24h', utc: null},
+      },
+      new Set()
+    );
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/events/',
+      method: 'GET',
+      body: {data: []},
+    });
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/profiling/filters/',
+      method: 'GET',
+      body: {data: []},
+    });
+    jest.mocked(useProfileFilters).mockReturnValue({});
+  });
+
+  it('Displays unsupported banner for unsupported projects', async function () {
+    ProjectsStore.loadInitialData([ProjectFixture({platform: 'nintendo-switch'})]);
+    render(<ProfilingContent location={router.location} />);
+    expect(await screen.findByTestId('unsupported-alert')).toBeInTheDocument();
+  });
+
+  it('Does not show unsupported banner for supported projects', async function () {
+    ProjectsStore.loadInitialData([
+      ProjectFixture({platform: 'android', hasProfiles: false}),
+    ]);
+    render(<ProfilingContent location={router.location} />);
+    expect(await screen.findByTestId('profiling-upgrade')).toBeInTheDocument();
+    expect(screen.queryByTestId('unsupported-alert')).not.toBeInTheDocument();
+  });
+});

+ 36 - 31
static/app/views/profiling/content.tsx

@@ -38,6 +38,7 @@ import {decodeScalar} from 'sentry/utils/queryString';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
 import useProjects from 'sentry/utils/useProjects';
+import {ProfilingUnsupportedAlert} from 'sentry/views/profiling/unsupportedAlert';
 import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
 
 import {LandingWidgetSelector} from './landing/landingWidgetSelector';
@@ -197,40 +198,44 @@ function ProfilingContent({location}: ProfilingContentProps) {
                 )}
               </ActionBar>
               {shouldShowProfilingOnboardingPanel ? (
-                // If user is on m2, show default
-                <ProfilingOnboardingPanel
-                  content={
-                    <ProfilingAM1OrMMXUpgrade
+                <Fragment>
+                  <ProfilingUnsupportedAlert selectedProjects={selection.projects} />
+                  <ProfilingOnboardingPanel
+                    content={
+                      // If user is on m2, show default
+                      <ProfilingAM1OrMMXUpgrade
+                        organization={organization}
+                        fallback={
+                          <Fragment>
+                            <h3>{t('Function level insights')}</h3>
+                            <p>
+                              {t(
+                                'Discover slow-to-execute or resource intensive functions within your application'
+                              )}
+                            </p>
+                          </Fragment>
+                        }
+                      />
+                    }
+                  >
+                    <ProfilingUpgradeButton
+                      data-test-id="profiling-upgrade"
                       organization={organization}
+                      priority="primary"
+                      onClick={onSetupProfilingClick}
                       fallback={
-                        <Fragment>
-                          <h3>{t('Function level insights')}</h3>
-                          <p>
-                            {t(
-                              'Discover slow-to-execute or resource intensive functions within your application'
-                            )}
-                          </p>
-                        </Fragment>
+                        <Button onClick={onSetupProfilingClick} priority="primary">
+                          {t('Set Up Profiling')}
+                        </Button>
                       }
-                    />
-                  }
-                >
-                  <ProfilingUpgradeButton
-                    organization={organization}
-                    priority="primary"
-                    onClick={onSetupProfilingClick}
-                    fallback={
-                      <Button onClick={onSetupProfilingClick} priority="primary">
-                        {t('Set Up Profiling')}
-                      </Button>
-                    }
-                  >
-                    {t('Set Up Profiling')}
-                  </ProfilingUpgradeButton>
-                  <Button href="https://docs.sentry.io/product/profiling/" external>
-                    {t('Read Docs')}
-                  </Button>
-                </ProfilingOnboardingPanel>
+                    >
+                      {t('Set Up Profiling')}
+                    </ProfilingUpgradeButton>
+                    <Button href="https://docs.sentry.io/product/profiling/" external>
+                      {t('Read Docs')}
+                    </Button>
+                  </ProfilingOnboardingPanel>
+                </Fragment>
               ) : (
                 <Fragment>
                   {organization.features.includes(

+ 38 - 0
static/app/views/profiling/unsupportedAlert.tsx

@@ -0,0 +1,38 @@
+import {useMemo} from 'react';
+
+import UnsupportedAlert from 'sentry/components/alerts/unsupportedAlert';
+import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
+import {profiling} from 'sentry/data/platformCategories';
+import useProjects from 'sentry/utils/useProjects';
+
+interface ProfilingUnsupportedAlertProps {
+  selectedProjects: Array<number>;
+}
+
+export function ProfilingUnsupportedAlert({
+  selectedProjects,
+}: ProfilingUnsupportedAlertProps) {
+  const {projects} = useProjects();
+  const withoutProfilingSupport = useMemo((): boolean => {
+    const projectsWithProfilingSupport = new Set(
+      projects
+        .filter(project => !project.platform || profiling.includes(project.platform))
+        .map(project => project.id)
+    );
+    // if it's My Projects or All projects, only show banner if none of them
+    // has profiling support
+    if (selectedProjects.length === 0 || selectedProjects[0] === ALL_ACCESS_PROJECTS) {
+      return projectsWithProfilingSupport.size === 0;
+    }
+
+    // if some projects are selected using the selector, show the banner if none of them
+    // has profiling support
+    return selectedProjects.every(
+      project => !projectsWithProfilingSupport.has(String(project))
+    );
+  }, [selectedProjects, projects]);
+
+  if (withoutProfilingSupport === false) return null;
+
+  return <UnsupportedAlert featureName="Profiling" />;
+}