Browse Source

ref(profiling): drop old onboarding (#37439)

* feat(profiling): add select project step

* feat(profiling): use platformicons

* test(profilingOnboardingModal): fix test

* ref(review): remove supported util, usecallback and opt to throw

* fix(tsc): dont pass leading icon if platform is unknown

* feat(profiling): add upload symbols step

* feat(profiling): add extra steps to onboarding

* fix(onboardingmodal): tag is a div

* ref(profiling): drop old onboarding

* fix(profilingonboarding): use the new flag and update wording (#37506)

* fix(profilingonboarding): use the new flag and update wording

* fix(profilingonboarding): invalidate flag if there is data

* fix(profiling): properly use my projects and all projects

* fix(profiling): remove redundant call
Jonas 2 years ago
parent
commit
71ad63e317

+ 8 - 10
static/app/components/profiling/ProfilingOnboarding/profilingOnboardingModal.tsx

@@ -226,19 +226,17 @@ function AndroidInstallSteps() {
       <li>
         <StepTitle>{t('Setup Performance Monitoring')}</StepTitle>
         {t(
-          `For Sentry to ingest profiles, we first require you to setup performance monitoring.`
+          `For Sentry to ingest profiles, we first require you to setup performance monitoring. To set up performance monitoring,`
         )}{' '}
         <ExternalLink
           openInNewTab
           href="https://docs.sentry.io/platforms/android/performance/"
         >
-          {t('Lear more about performance monitoring.')}
+          {t('follow our step by step instructions here.')}
         </ExternalLink>
       </li>
       <li>
-        <StepTitle>
-          {t('Enable profiling in your app by configuring the SDKs like below:')}
-        </StepTitle>
+        <StepTitle>{t('Setup Profiling')}</StepTitle>
         <CodeContainer>
           {`<application>
   <meta-data android:name="io.sentry.dsn" android:value="..." />
@@ -265,13 +263,13 @@ function IOSInstallSteps() {
       <li>
         <StepTitle>{t('Setup Performance Monitoring')}</StepTitle>
         {t(
-          `For Sentry to ingest profiles, we first require you to setup performance monitoring.`
+          `For Sentry to ingest profiles, we first require you to setup performance monitoring. To set up performance monitoring,`
         )}{' '}
         <ExternalLink
           openInNewTab
           href="https://docs.sentry.io/platforms/apple/guides/ios/performance/"
         >
-          {t('Lear more about performance monitoring.')}
+          {t('follow our step by step instructions here.')}
         </ExternalLink>
       </li>
       <li>
@@ -320,9 +318,9 @@ function AndroidSendDebugFilesInstruction({
         <h3>{t('Setup Profiling')}</h3>
       </ModalHeader>
       <p>
-        {t(`The most straightforward way to provide Sentry with debug information files is to
-        upload them using sentry-cli. Depending on your workflow, you may want to upload
-        as part of your build pipeline or when deploying and publishing your application.`)}{' '}
+        {t(
+          `If you want to see de-obfuscated stack traces, you'll need to use ProGuard with Sentry. To do so, upload the ProGuard mapping files by either the recommended method of using our Gradle integration or manually by using sentry-cli.`
+        )}{' '}
         <ExternalLink href="https://docs.sentry.io/product/cli/dif/">
           {t('Learn more about Debug Information Files.')}
         </ExternalLink>

+ 0 - 4
static/app/routes.tsx

@@ -1702,10 +1702,6 @@ function buildRoutes() {
       component={make(() => import('sentry/views/profiling'))}
     >
       <IndexRoute component={make(() => import('sentry/views/profiling/content'))} />
-      <Route
-        path="onboarding/"
-        component={make(() => import('sentry/views/profiling/legacyOnboarding'))}
-      />
       <Route
         path="summary/:projectId/"
         component={make(() => import('sentry/views/profiling/profileSummary'))}

+ 2 - 1
static/app/types/project.tsx

@@ -34,6 +34,7 @@ export type Project = {
   groupingAutoUpdate: boolean;
   groupingConfig: string;
   hasAccess: boolean;
+  hasProfiles: boolean;
   hasSessions: boolean;
   id: string;
   isBookmarked: boolean;
@@ -41,8 +42,8 @@ export type Project = {
   isMember: boolean;
   organization: Organization;
   plugins: Plugin[];
-
   processingIssues: number;
+
   relayPiiConfig: string;
   subjectTemplate: string;
   teams: Team[];

+ 0 - 8
static/app/utils/profiling/routes.tsx

@@ -7,14 +7,6 @@ export function generateProfilingRoute({orgSlug}: {orgSlug: Organization['slug']
   return `/organizations/${orgSlug}/profiling/`;
 }
 
-export function generateProfilingOnboardingRoute({
-  orgSlug,
-}: {
-  orgSlug: Organization['slug'];
-}): Path {
-  return `/organizations/${orgSlug}/profiling/onboarding/`;
-}
-
 export function generateProfileSummaryRoute({
   orgSlug,
   projectSlug,

+ 98 - 19
static/app/views/profiling/content.tsx

@@ -1,8 +1,9 @@
-import {useCallback, useEffect} from 'react';
+import {Fragment, useCallback, useEffect, useMemo} from 'react';
 import {browserHistory, InjectedRouter} from 'react-router';
 import styled from '@emotion/styled';
 import {Location} from 'history';
 
+import {openModal} from 'sentry/actionCreators/modal';
 import Button from 'sentry/components/button';
 import DatePageFilter from 'sentry/components/datePageFilter';
 import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
@@ -13,22 +14,66 @@ import PageFiltersContainer from 'sentry/components/organizations/pageFilters/co
 import PageHeading from 'sentry/components/pageHeading';
 import Pagination from 'sentry/components/pagination';
 import {ProfileTransactionsTable} from 'sentry/components/profiling/profileTransactionsTable';
+import {ProfilingOnboardingModal} from 'sentry/components/profiling/ProfilingOnboarding/profilingOnboardingModal';
 import ProjectPageFilter from 'sentry/components/projectPageFilter';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import SmartSearchBar, {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
 import {MAX_QUERY_LENGTH} from 'sentry/constants';
+import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
 import {t} from 'sentry/locale';
 import {PageContent} from 'sentry/styles/organization';
 import space from 'sentry/styles/space';
+import {Project} from 'sentry/types';
+import {PageFilters} from 'sentry/types/core';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
 import {useProfileTransactions} from 'sentry/utils/profiling/hooks/useProfileTransactions';
-import {generateProfilingOnboardingRoute} from 'sentry/utils/profiling/routes';
 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 {ProfileCharts} from './landing/profileCharts';
+import {ProfilingOnboardingPanel} from './profilingOnboardingPanel';
+
+function hasSetupProfilingForAtLeastOneProject(
+  selectedProjects: PageFilters['projects'],
+  projects: Project[]
+): boolean {
+  const projectIDsToProjectTable = projects.reduce<Record<string, Project>>(
+    (acc, project) => {
+      acc[project.id] = project;
+      return acc;
+    },
+    {}
+  );
+
+  if (selectedProjects[0] === ALL_ACCESS_PROJECTS || selectedProjects.length === 0) {
+    const projectWithProfiles = projects.find(p => {
+      const project = projectIDsToProjectTable[String(p)];
+
+      if (!project) {
+        // Shouldnt happen, but lets be safe and just not do anything
+        return false;
+      }
+      return project.hasProfiles;
+    });
+
+    return projectWithProfiles !== undefined;
+  }
+
+  const projectWithProfiles = selectedProjects.find(p => {
+    const project = projectIDsToProjectTable[String(p)];
+
+    if (!project) {
+      // Shouldnt happen, but lets be safe and just not do anything
+      return false;
+    }
+    return project.hasProfiles;
+  });
+
+  return projectWithProfiles !== undefined;
+}
 
 interface ProfilingContentProps {
   location: Location;
@@ -42,6 +87,7 @@ function ProfilingContent({location, router}: ProfilingContentProps) {
   const query = decodeScalar(location.query.query, '');
   const profileFilters = useProfileFilters({query: '', selection});
   const transactions = useProfileTransactions({cursor, query, selection});
+  const {projects} = useProjects();
 
   useEffect(() => {
     trackAdvancedAnalyticsEvent('profiling_views.landing', {
@@ -63,9 +109,23 @@ function ProfilingContent({location, router}: ProfilingContentProps) {
     [location]
   );
 
+  // Open the modal on demand
   const onSetupProfilingClick = useCallback(() => {
-    browserHistory.push(generateProfilingOnboardingRoute({orgSlug: organization.slug}));
-  }, [organization.slug]);
+    openModal(props => {
+      return <ProfilingOnboardingModal {...props} />;
+    });
+  }, []);
+
+  const shouldShowProfilingOnboardingPanel = useMemo((): boolean => {
+    if (transactions.type !== 'resolved') {
+      return false;
+    }
+
+    if (transactions.data.transactions.length > 0) {
+      return false;
+    }
+    return !hasSetupProfilingForAtLeastOneProject(selection.projects, projects);
+  }, [selection.projects, projects, transactions]);
 
   return (
     <SentryDocumentTitle title={t('Profiling')} orgSlug={organization.slug}>
@@ -96,21 +156,40 @@ function ProfilingContent({location, router}: ProfilingContentProps) {
                     maxQueryLength={MAX_QUERY_LENGTH}
                   />
                 </ActionBar>
-                <ProfileCharts router={router} query={query} selection={selection} />
-                <ProfileTransactionsTable
-                  error={
-                    transactions.type === 'errored' ? t('Unable to load profiles') : null
-                  }
-                  isLoading={transactions.type === 'loading'}
-                  transactions={
-                    transactions.type === 'resolved' ? transactions.data.transactions : []
-                  }
-                />
-                <Pagination
-                  pageLinks={
-                    transactions.type === 'resolved' ? transactions.data.pageLinks : null
-                  }
-                />
+                {shouldShowProfilingOnboardingPanel ? (
+                  <ProfilingOnboardingPanel>
+                    <Button href="https://docs.sentry.io/" external>
+                      {t('Read Docs')}
+                    </Button>
+                    <Button onClick={onSetupProfilingClick} priority="primary">
+                      {t('Setup Profiling')}
+                    </Button>
+                  </ProfilingOnboardingPanel>
+                ) : (
+                  <Fragment>
+                    <ProfileCharts router={router} query={query} selection={selection} />
+                    <ProfileTransactionsTable
+                      error={
+                        transactions.type === 'errored'
+                          ? t('Unable to load profiles')
+                          : null
+                      }
+                      isLoading={transactions.type === 'loading'}
+                      transactions={
+                        transactions.type === 'resolved'
+                          ? transactions.data.transactions
+                          : []
+                      }
+                    />
+                    <Pagination
+                      pageLinks={
+                        transactions.type === 'resolved'
+                          ? transactions.data.pageLinks
+                          : null
+                      }
+                    />
+                  </Fragment>
+                )}
               </Layout.Main>
             </Layout.Body>
           </StyledPageContent>

+ 2 - 80
static/app/views/profiling/index.tsx

@@ -1,84 +1,16 @@
-import {useCallback, useEffect, useState} from 'react';
-import * as Sentry from '@sentry/react';
-
-import {PromptData, promptsCheck, promptsUpdate} from 'sentry/actionCreators/prompts';
 import Feature from 'sentry/components/acl/feature';
 import Alert from 'sentry/components/alert';
-import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {t} from 'sentry/locale';
 import {PageContent} from 'sentry/styles/organization';
-import {Organization, RequestState} from 'sentry/types';
-import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
-import useApi from 'sentry/utils/useApi';
+import {Organization} from 'sentry/types';
 import withOrganization from 'sentry/utils/withOrganization';
 
-import LegacyProfilingOnboarding from './legacyProfilingOnboarding';
-
-function shouldShowLegacyProfilingOnboarding(state: RequestState<PromptData>): boolean {
-  if (state.type === 'resolved') {
-    return typeof state.data?.dismissedTime !== 'number';
-  }
-
-  return false;
-}
-
 type Props = {
   children: React.ReactChildren;
   organization: Organization;
 };
 
 function ProfilingContainer({organization, children}: Props) {
-  const api = useApi();
-
-  const [requestState, setRequestState] = useState<RequestState<PromptData>>({
-    type: 'initial',
-  });
-
-  // Fetch prompt data and see if we need to show the onboarding.
-  useEffect(() => {
-    setRequestState({type: 'loading'});
-    promptsCheck(api, {
-      organizationId: organization.id,
-      feature: 'profiling_onboarding',
-    })
-      .then(data => {
-        setRequestState({type: 'resolved', data});
-      })
-      .catch(e => {
-        Sentry.captureException(e);
-        setRequestState({type: 'errored', error: t('Error: Unable to load prompt data')});
-      });
-  }, [api, organization]);
-
-  // Eagerly update state and update check
-  const dismissPrompt = useCallback(
-    (status: 'done' | 'dismissed') => {
-      setRequestState({type: 'resolved', data: {dismissedTime: Date.now()}});
-      trackAdvancedAnalyticsEvent('profiling_views.onboarding_action', {
-        action: status,
-        organization,
-      });
-
-      return promptsUpdate(api, {
-        feature: 'profiling_onboarding',
-        organizationId: organization.id,
-        // This will always send dismissed, becuse we dont actually
-        // care about the snooze mechanism. It would be awkward to suddenly
-        // creep a full page into view.
-        status: 'dismissed',
-      });
-    },
-    [organization, api]
-  );
-
-  const handleDone = useCallback(() => {
-    dismissPrompt('done');
-  }, [dismissPrompt]);
-
-  const handleDismiss = useCallback(() => {
-    dismissPrompt('dismissed');
-  }, [dismissPrompt]);
-
   return (
     <Feature
       hookName="feature-disabled:profiling-page"
@@ -90,17 +22,7 @@ function ProfilingContainer({organization, children}: Props) {
         </PageContent>
       )}
     >
-      {requestState.type === 'loading' ? (
-        <LoadingIndicator />
-      ) : shouldShowLegacyProfilingOnboarding(requestState) ? (
-        <LegacyProfilingOnboarding
-          organization={organization}
-          onDismissClick={handleDismiss}
-          onDoneClick={handleDone}
-        />
-      ) : (
-        children
-      )}
+      {children}
     </Feature>
   );
 }

+ 0 - 23
static/app/views/profiling/legacyOnboarding.tsx

@@ -1,23 +0,0 @@
-import {useCallback} from 'react';
-import {browserHistory} from 'react-router';
-
-import {generateProfilingRoute} from 'sentry/utils/profiling/routes';
-import useOrganization from 'sentry/utils/useOrganization';
-
-import LegacyProfilingOnboarding from './legacyProfilingOnboarding';
-
-export default function LegacyOnboarding() {
-  const organization = useOrganization();
-
-  const onDismissClick = useCallback(() => {
-    browserHistory.push(generateProfilingRoute({orgSlug: organization.slug}));
-  }, [organization.slug]);
-
-  return (
-    <LegacyProfilingOnboarding
-      organization={organization}
-      onDoneClick={onDismissClick}
-      onDismissClick={onDismissClick}
-    />
-  );
-}

+ 0 - 148
static/app/views/profiling/legacyProfilingOnboarding.tsx

@@ -1,148 +0,0 @@
-import {useEffect} from 'react';
-import styled from '@emotion/styled';
-
-import Alert from 'sentry/components/alert';
-import Button from 'sentry/components/button';
-import FeatureBadge from 'sentry/components/featureBadge';
-import * as Layout from 'sentry/components/layouts/thirds';
-import ExternalLink from 'sentry/components/links/externalLink';
-import List from 'sentry/components/list';
-import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
-import {t} from 'sentry/locale';
-import {PageContent} from 'sentry/styles/organization';
-import space from 'sentry/styles/space';
-import {Organization} from 'sentry/types/organization';
-import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
-
-interface Props {
-  onDismissClick: () => void;
-  onDoneClick: () => void;
-  organization: Organization;
-}
-
-export default function LegacyProfilingOnboarding(props: Props) {
-  useEffect(() => {
-    trackAdvancedAnalyticsEvent('profiling_views.onboarding', {
-      organization: props.organization,
-    });
-  }, [props.organization]);
-
-  return (
-    <SentryDocumentTitle title={t('Profiling')} orgSlug={props.organization.slug}>
-      <StyledPageContent>
-        <Main>
-          <Layout.Title>
-            {t('Welcome to Sentry Profiling')}
-            <FeatureBadge type="alpha" />
-          </Layout.Title>
-          <Content>
-            <p>
-              {t(`With Sentry Profiling you can instrument your native iOS and Android apps to
-        collect profiles for your transactions. This gives you a unique insight into what
-        is on the execution stack at any point during the duration of the transaction.
-        Data is collected in production, on real devices with real users.`)}
-            </p>
-
-            <Alert>
-              {t(
-                `Profiling is only possible with sentry-cocoa and sentry-java SDKs right now. We don’t support React Native or Flutter yet.`
-              )}
-            </Alert>
-
-            <ProfilingSteps symbol="colored-numeric">
-              <li>
-                {t(
-                  'Make sure your SDKs are upgraded to 6.0.0 (sentry-java) or 7.13.0 (sentry-cocoa).'
-                )}
-              </li>
-              <li>
-                {t(
-                  `Setup performance transactions in your app if you haven’t already → `
-                )}{' '}
-                <ExternalLink
-                  openInNewTab
-                  href="https://docs.sentry.io/product/performance/"
-                >
-                  {t('https://docs.sentry.io/product/performance/')}
-                </ExternalLink>
-              </li>
-              <li>
-                {t('Enable profiling in your app by configuring the SDKs like below:')}
-                <pre>
-                  <code>{`SentrySDK.start { options in
-    options.dsn = "..."
-    options.tracesSampleRate = 1.0 // Make sure transactions are enabled
-    options.enableProfiling = true
-}`}</code>
-                </pre>
-
-                <pre>
-                  <code>
-                    {`<application>
-  <meta-data android:name="io.sentry.dsn" android:value="..." />
-  <meta-data android:name="io.sentry.traces.sample-rate" android:value="1.0" />
-  <meta-data android:name="io.sentry.traces.profiling.enable" android:value="true" />
-</application>`}
-                  </code>
-                </pre>
-              </li>
-              <li>
-                {t('Join the discussion on')}{' '}
-                <ExternalLink openInNewTab href="https://discord.gg/FvQuVVCD">
-                  Discord
-                </ExternalLink>{' '}
-                {t(
-                  'or email us at profiling@sentry.io with any questions or if you need help setting it all up! There’s also a page with some more details and a troubleshooting section at'
-                )}{' '}
-                <ExternalLink
-                  openInNewTab
-                  href="https://sentry.notion.site/Profiling-Beta-Testing-Instructions-413ecdd9fcb34b3a8b57806280bf2ecb"
-                >
-                  {t('our notion page')}
-                </ExternalLink>
-              </li>
-
-              <Actions>
-                <Button priority="primary" onClick={props.onDoneClick}>
-                  {t("I'm done")}
-                </Button>
-                <Button onClick={props.onDismissClick}>{t('Dismiss')}</Button>
-              </Actions>
-            </ProfilingSteps>
-          </Content>
-        </Main>
-      </StyledPageContent>
-    </SentryDocumentTitle>
-  );
-}
-
-const Content = styled('div')`
-  margin: ${space(2)} 0 ${space(3)} 0;
-`;
-
-const Actions = styled('div')`
-  margin-top: ${space(4)};
-
-  > button:not(:first-child) {
-    margin-left: ${space(2)};
-  }
-`;
-
-const StyledPageContent = styled(PageContent)`
-  background-color: ${p => p.theme.background};
-`;
-
-const ProfilingSteps = styled(List)`
-  li {
-    margin-bottom: ${space(1)};
-    position: relative;
-  }
-`;
-
-const Main = styled('div')`
-  width: 100%;
-
-  pre:not(:last-child) {
-    margin-top: ${space(2)};
-  }
-`;

+ 55 - 0
static/app/views/profiling/profilingOnboardingPanel.tsx

@@ -0,0 +1,55 @@
+import styled from '@emotion/styled';
+
+import emptyStateImg from 'sentry-images/spot/performance-empty-state.svg';
+
+import ButtonBar from 'sentry/components/buttonBar';
+import OnboardingPanel from 'sentry/components/onboardingPanel';
+import {t} from 'sentry/locale';
+
+interface ProfilingOnboardingPanelProps {
+  children: React.ReactNode;
+}
+
+export function ProfilingOnboardingPanel(props: ProfilingOnboardingPanelProps) {
+  return (
+    <OnboardingPanel image={<AlertsImage src={emptyStateImg} />}>
+      <h3>{t('Function level insights')}</h3>
+      <p>
+        {t(
+          'Discover slow-to-execute or resource intensive functions within your application'
+        )}
+      </p>
+      <ButtonList gap={1}>{props.children}</ButtonList>
+    </OnboardingPanel>
+  );
+}
+
+const AlertsImage = styled('img')`
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    user-select: none;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    width: 220px;
+    margin-top: auto;
+    margin-bottom: auto;
+    transform: translateX(-50%);
+    left: 50%;
+  }
+
+  @media (min-width: ${p => p.theme.breakpoints.large}) {
+    transform: translateX(-30%);
+    width: 380px;
+    min-width: 380px;
+  }
+
+  @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
+    transform: translateX(-30%);
+    width: 420px;
+    min-width: 420px;
+  }
+`;
+
+const ButtonList = styled(ButtonBar)`
+  grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
+`;

+ 4 - 1
tests/js/spec/components/profiling/profilingOnboarding/profilingOnboardingModal.spec.tsx

@@ -9,7 +9,10 @@ const MockRenderModalProps: ModalRenderProps = {
   Body: ({children}) => <div>{children}</div>,
   Header: ({children}) => <div>{children}</div>,
   Footer: ({children}) => <div>{children}</div>,
-} as ModalRenderProps;
+  CloseButton: ({children}) => <div>{children}</div>,
+  closeModal: jest.fn(),
+  onDismiss: jest.fn(),
+} as unknown as ModalRenderProps;
 
 function selectProject(project: Project) {
   if (!project.name) {