Browse Source

feat(perf): Add performance onboarding checklist (#35448)

Co-authored-by: Ash Anand <0Calories@users.noreply.github.com>
Alberto Leal 2 years ago
parent
commit
f2613aa9c6

+ 6 - 11
static/app/components/onboardingWizard/sidebar.tsx

@@ -10,10 +10,10 @@ import {CommonSidebarProps} from 'sentry/components/sidebar/types';
 import Tooltip from 'sentry/components/tooltip';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
-import {OnboardingTask, OnboardingTaskKey, Organization, Project} from 'sentry/types';
+import {OnboardingTask, OnboardingTaskKey, Project} from 'sentry/types';
 import testableTransition from 'sentry/utils/testableTransition';
 import useApi from 'sentry/utils/useApi';
-import withOrganization from 'sentry/utils/withOrganization';
+import useOrganization from 'sentry/utils/useOrganization';
 import withProjects from 'sentry/utils/withProjects';
 import {usePersistedOnboardingState} from 'sentry/views/onboarding/targetedOnboarding/utils';
 
@@ -24,7 +24,6 @@ import {findActiveTasks, findCompleteTasks, findUpcomingTasks, taskIsDone} from
 
 type Props = Pick<CommonSidebarProps, 'orientation' | 'collapsed'> & {
   onClose: () => void;
-  organization: Organization;
   projects: Project[];
 };
 
@@ -67,14 +66,10 @@ const upcomingTasksHeading = (
 );
 const completedTasksHeading = <Heading key="complete">{t('Completed')}</Heading>;
 
-function OnboardingWizardSidebar({
-  organization,
-  collapsed,
-  orientation,
-  onClose,
-  projects,
-}: Props) {
+function OnboardingWizardSidebar({collapsed, orientation, onClose, projects}: Props) {
   const api = useApi();
+  const organization = useOrganization();
+
   const [onboardingState, setOnboardingState] = usePersistedOnboardingState();
 
   const markCompletionTimeout = useRef<number | undefined>();
@@ -269,4 +264,4 @@ const TopRight = styled('img')`
   width: 60%;
 `;
 
-export default withOrganization(withProjects(OnboardingWizardSidebar));
+export default withProjects(OnboardingWizardSidebar);

+ 8 - 5
static/app/components/onboardingWizard/task.tsx

@@ -1,5 +1,4 @@
 import {forwardRef} from 'react';
-import {withRouter, WithRouterProps} from 'react-router';
 import styled from '@emotion/styled';
 import {motion} from 'framer-motion';
 import moment from 'moment';
@@ -16,6 +15,7 @@ import space from 'sentry/styles/space';
 import {AvatarUser, OnboardingTask, OnboardingTaskKey, Organization} from 'sentry/types';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import testableTransition from 'sentry/utils/testableTransition';
+import {useRouteContext} from 'sentry/utils/useRouteContext';
 import withOrganization from 'sentry/utils/withOrganization';
 
 import SkipConfirm from './skipConfirm';
@@ -33,7 +33,7 @@ const recordAnalytics = (
     action,
   });
 
-type Props = WithRouterProps & {
+type Props = {
   forwardedRef: React.Ref<HTMLDivElement>;
   /**
    * Fired when a task is completed. This will typically happen if there is a
@@ -52,7 +52,10 @@ type Props = WithRouterProps & {
   task: OnboardingTask;
 };
 
-function Task({router, task, onSkip, onMarkComplete, forwardedRef, organization}: Props) {
+function Task(props: Props) {
+  const {task, onSkip, onMarkComplete, forwardedRef, organization} = props;
+  const routeContext = useRouteContext();
+  const {router} = routeContext;
   const handleSkip = () => {
     recordAnalytics(task, organization, 'skipped');
     onSkip(task.task);
@@ -67,7 +70,7 @@ function Task({router, task, onSkip, onMarkComplete, forwardedRef, organization}
     }
 
     if (task.actionType === 'action') {
-      task.action();
+      task.action(routeContext);
     }
 
     if (task.actionType === 'app') {
@@ -285,7 +288,7 @@ TaskBlankAvatar.defaultProps = {
   transition,
 };
 
-const WrappedTask = withOrganization(withRouter(Task));
+const WrappedTask = withOrganization(Task);
 
 export default forwardRef<
   HTMLDivElement,

+ 40 - 2
static/app/components/onboardingWizard/taskConfig.tsx

@@ -1,8 +1,10 @@
 import styled from '@emotion/styled';
 
 import {openInviteMembersModal} from 'sentry/actionCreators/modal';
+import {navigateTo} from 'sentry/actionCreators/navigation';
 import {Client} from 'sentry/api';
 import {taskIsDone} from 'sentry/components/onboardingWizard/utils';
+import {filterProjects} from 'sentry/components/performanceOnboarding/utils';
 import {sourceMaps} from 'sentry/data/platformCategories';
 import {t} from 'sentry/locale';
 import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator';
@@ -154,8 +156,44 @@ export function getOnboardingTasks({
       ),
       skippable: true,
       requisites: [OnboardingTaskKey.FIRST_PROJECT],
-      actionType: 'external',
-      location: 'https://docs.sentry.io/product/performance/getting-started/',
+      actionType: 'action',
+      action: ({router}) => {
+        if (!organization.features?.includes('performance-onboarding-checklist')) {
+          window.open(
+            'https://docs.sentry.io/product/performance/getting-started/',
+            '_blank'
+          );
+          return;
+        }
+
+        // TODO: add analytics here for this specific action.
+
+        if (!projects) {
+          navigateTo(`/organizations/${organization.slug}/performance/`, router);
+          return;
+        }
+
+        const {projectsWithoutFirstTransactionEvent, projectsForOnboarding} =
+          filterProjects(projects);
+
+        if (projectsWithoutFirstTransactionEvent.length <= 0) {
+          navigateTo(`/organizations/${organization.slug}/performance/`, router);
+          return;
+        }
+
+        if (projectsForOnboarding.length) {
+          navigateTo(
+            `/organizations/${organization.slug}/performance/?project=${projectsForOnboarding[0].id}#performance-sidequest`,
+            router
+          );
+          return;
+        }
+
+        navigateTo(
+          `/organizations/${organization.slug}/performance/?project=${projectsWithoutFirstTransactionEvent[0].id}#performance-sidequest`,
+          router
+        );
+      },
       display: true,
       SupplementComponent: withApi(({api, task, onCompleteTask}: FirstEventWaiterProps) =>
         !!projects?.length && task.requisiteTasks.length === 0 && !task.completionSeen ? (

+ 351 - 0
static/app/components/performanceOnboarding/sidebar.tsx

@@ -0,0 +1,351 @@
+import {Fragment, useEffect, useState} from 'react';
+import styled from '@emotion/styled';
+
+import HighlightTopRightPattern from 'sentry-images/pattern/highlight-top-right.svg';
+
+import Button from 'sentry/components/button';
+import DropdownMenuControlV2 from 'sentry/components/dropdownMenuControlV2';
+import {MenuItemProps} from 'sentry/components/dropdownMenuItemV2';
+import IdBadge from 'sentry/components/idBadge';
+import LoadingIndicator from 'sentry/components/loadingIndicator';
+import SidebarPanel from 'sentry/components/sidebar/sidebarPanel';
+import {CommonSidebarProps, SidebarPanelKey} from 'sentry/components/sidebar/types';
+import {withoutPerformanceSupport} from 'sentry/data/platformCategories';
+import platforms from 'sentry/data/platforms';
+import {t, tct} from 'sentry/locale';
+import PageFiltersStore from 'sentry/stores/pageFiltersStore';
+import {useLegacyStore} from 'sentry/stores/useLegacyStore';
+import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator';
+import space from 'sentry/styles/space';
+import {Project} from 'sentry/types';
+import EventWaiter from 'sentry/utils/eventWaiter';
+import useApi from 'sentry/utils/useApi';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePrevious from 'sentry/utils/usePrevious';
+import useProjects from 'sentry/utils/useProjects';
+
+import OnBoardingStep from './step';
+import usePerformanceOnboardingDocs, {
+  generateOnboardingDocKeys,
+} from './usePerformanceOnboardingDocs';
+import {filterProjects} from './utils';
+
+function PerformanceOnboardingSidebar(props: CommonSidebarProps) {
+  const {currentPanel, collapsed, hidePanel, orientation} = props;
+  const isActive = currentPanel === SidebarPanelKey.PerformanceOnboarding;
+  const organization = useOrganization();
+  const hasProjectAccess = organization.access.includes('project:read');
+
+  const {projects, initiallyLoaded: projectsLoaded} = useProjects();
+
+  const [currentProject, setCurrentProject] = useState<Project | undefined>(undefined);
+
+  const {selection, isReady} = useLegacyStore(PageFiltersStore);
+
+  const {projectsWithoutFirstTransactionEvent, projectsForOnboarding} =
+    filterProjects(projects);
+
+  useEffect(() => {
+    if (
+      currentProject ||
+      projects.length === 0 ||
+      !isReady ||
+      !isActive ||
+      projectsWithoutFirstTransactionEvent.length <= 0
+    ) {
+      return;
+    }
+    // Establish current project
+
+    const projectMap: Record<string, Project> = projects.reduce((acc, project) => {
+      acc[project.id] = project;
+      return acc;
+    }, {});
+
+    if (selection.projects.length) {
+      const projectSelection = selection.projects.map(
+        projectId => projectMap[String(projectId)]
+      );
+
+      // Among the project selection, find a project that has performance onboarding docs support, and has not sent
+      // a first transaction event.
+      const maybeProject = projectSelection.find(project =>
+        projectsForOnboarding.includes(project)
+      );
+      if (maybeProject) {
+        setCurrentProject(maybeProject);
+        return;
+      }
+
+      // Among the project selection, find a project that has not sent a first transaction event
+      const maybeProjectFallback = projectSelection.find(project =>
+        projectsWithoutFirstTransactionEvent.includes(project)
+      );
+      if (maybeProjectFallback) {
+        setCurrentProject(maybeProjectFallback);
+        return;
+      }
+    }
+
+    // Among the projects, find a project that has performance onboarding docs support, and has not sent
+    // a first transaction event.
+    if (projectsForOnboarding.length) {
+      setCurrentProject(projectsForOnboarding[0]);
+      return;
+    }
+
+    // Otherwise, pick a first project that has not sent a first transaction event.
+    setCurrentProject(projectsWithoutFirstTransactionEvent[0]);
+  }, [
+    selection.projects,
+    projects,
+    isActive,
+    isReady,
+    projectsForOnboarding,
+    projectsWithoutFirstTransactionEvent,
+    currentProject,
+  ]);
+
+  if (
+    !isActive ||
+    !hasProjectAccess ||
+    currentProject === undefined ||
+    !projectsLoaded ||
+    !projects ||
+    projects.length <= 0 ||
+    projectsWithoutFirstTransactionEvent.length <= 0
+  ) {
+    return null;
+  }
+
+  const items: MenuItemProps[] = projectsWithoutFirstTransactionEvent.reduce(
+    (acc: MenuItemProps[], project) => {
+      const itemProps: MenuItemProps = {
+        key: project.id,
+        label: (
+          <StyledIdBadge project={project} avatarSize={16} hideOverflow disableLink />
+        ),
+        onAction: function switchProject() {
+          setCurrentProject(project);
+        },
+      };
+
+      if (currentProject.id === project.id) {
+        acc.unshift(itemProps);
+      } else {
+        acc.push(itemProps);
+      }
+
+      return acc;
+    },
+    []
+  );
+
+  return (
+    <TaskSidebarPanel
+      orientation={orientation}
+      collapsed={collapsed}
+      hidePanel={hidePanel}
+    >
+      <TopRightBackgroundImage src={HighlightTopRightPattern} />
+      <TaskList>
+        <Heading>{t('Boost Performance')}</Heading>
+        <DropdownMenuControlV2
+          items={items}
+          triggerLabel={
+            <StyledIdBadge
+              project={currentProject}
+              avatarSize={16}
+              hideOverflow
+              disableLink
+            />
+          }
+          triggerProps={{
+            'aria-label': currentProject.slug,
+          }}
+          placement="bottom left"
+        />
+        <OnboardingContent currentProject={currentProject} />
+      </TaskList>
+    </TaskSidebarPanel>
+  );
+}
+
+function OnboardingContent({currentProject}: {currentProject: Project}) {
+  const api = useApi();
+  const organization = useOrganization();
+  const previousProject = usePrevious(currentProject);
+  const [received, setReceived] = useState<boolean>(false);
+
+  useEffect(() => {
+    if (previousProject.id !== currentProject.id) {
+      setReceived(false);
+    }
+  }, [previousProject.id, currentProject.id]);
+
+  const {docContents, isLoading, hasOnboardingContents} =
+    usePerformanceOnboardingDocs(currentProject);
+
+  if (isLoading) {
+    return <LoadingIndicator />;
+  }
+
+  const currentPlatform = currentProject.platform
+    ? platforms.find(p => p.id === currentProject.platform)
+    : undefined;
+
+  const doesNotSupportPerformance = currentProject.platform
+    ? withoutPerformanceSupport.has(currentProject.platform)
+    : false;
+
+  if (doesNotSupportPerformance) {
+    return (
+      <Fragment>
+        <div>
+          {tct(
+            'Fiddlesticks. Performance isn’t available for your [platform] project yet but we’re definitely still working on it. Stay tuned.',
+            {platform: currentPlatform?.name || currentProject.slug}
+          )}
+        </div>
+        <div>
+          <Button size="sm" href="https://docs.sentry.io/platforms/" external>
+            {t('Go to Sentry Documentation')}
+          </Button>
+        </div>
+      </Fragment>
+    );
+  }
+
+  if (!currentPlatform || !hasOnboardingContents) {
+    return (
+      <Fragment>
+        <div>
+          {tct(
+            'Fiddlesticks. This checklist isn’t available for your [project] project yet, but for now, go to Sentry docs for installation details.',
+            {project: currentProject.slug}
+          )}
+        </div>
+        <div>
+          <Button
+            size="sm"
+            href="https://docs.sentry.io/product/performance/getting-started/"
+            external
+          >
+            {t('Go to documentation')}
+          </Button>
+        </div>
+      </Fragment>
+    );
+  }
+
+  const docKeys = generateOnboardingDocKeys(currentPlatform.id);
+
+  return (
+    <Fragment>
+      <div>
+        {tct(
+          `Adding Performance to your [platform] project is simple. Make sure you've got these basics down.`,
+          {platform: currentPlatform?.name || currentProject.slug}
+        )}
+      </div>
+      {docKeys.map((docKey, index) => {
+        let footer: React.ReactNode = null;
+
+        if (index === 2) {
+          footer = (
+            <EventWaiter
+              api={api}
+              organization={organization}
+              project={currentProject}
+              eventType="transaction"
+              onIssueReceived={() => {
+                setReceived(true);
+              }}
+            >
+              {() => (received ? <EventReceivedIndicator /> : <EventWaitingIndicator />)}
+            </EventWaiter>
+          );
+        }
+        return (
+          <div key={index}>
+            <OnBoardingStep
+              docKey={docKey}
+              project={currentProject}
+              docContent={docContents[docKey]}
+            />
+            {footer}
+          </div>
+        );
+      })}
+    </Fragment>
+  );
+}
+
+const TaskSidebarPanel = styled(SidebarPanel)`
+  width: 450px;
+`;
+
+const TopRightBackgroundImage = styled('img')`
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 60%;
+  user-select: none;
+`;
+
+const TaskList = styled('div')`
+  display: grid;
+  grid-auto-flow: row;
+  grid-template-columns: 100%;
+  gap: ${space(1)};
+  margin: 50px ${space(4)} ${space(4)} ${space(4)};
+`;
+
+const Heading = styled('div')`
+  display: flex;
+  color: ${p => p.theme.purple300};
+  font-size: ${p => p.theme.fontSizeExtraSmall};
+  text-transform: uppercase;
+  font-weight: 600;
+  line-height: 1;
+  margin-top: ${space(3)};
+`;
+
+const StyledIdBadge = styled(IdBadge)`
+  overflow: hidden;
+  white-space: nowrap;
+  flex-shrink: 1;
+`;
+
+const PulsingIndicator = styled('div')`
+  ${pulsingIndicatorStyles};
+  margin-right: ${space(1)};
+`;
+
+const EventWaitingIndicator = styled((p: React.HTMLAttributes<HTMLDivElement>) => (
+  <div {...p}>
+    <PulsingIndicator />
+    {t(`Waiting for this project's first transaction event`)}
+  </div>
+))`
+  display: flex;
+  align-items: center;
+  flex-grow: 1;
+  font-size: ${p => p.theme.fontSizeMedium};
+  color: ${p => p.theme.pink300};
+`;
+
+const EventReceivedIndicator = styled((p: React.HTMLAttributes<HTMLDivElement>) => (
+  <div {...p}>
+    {'🎉 '}
+    {t(`We've received this project's first transaction event!`)}
+  </div>
+))`
+  display: flex;
+  align-items: center;
+  flex-grow: 1;
+  font-size: ${p => p.theme.fontSizeMedium};
+  color: ${p => p.theme.green300};
+`;
+
+export default PerformanceOnboardingSidebar;

+ 108 - 0
static/app/components/performanceOnboarding/step.tsx

@@ -0,0 +1,108 @@
+import 'prism-sentry/index.css';
+
+import {useState} from 'react';
+import styled from '@emotion/styled';
+
+import CheckboxFancy from 'sentry/components/checkboxFancy/checkboxFancy';
+import space from 'sentry/styles/space';
+import {Project} from 'sentry/types';
+import localStorage from 'sentry/utils/localStorage';
+
+type Props = {
+  docContent: string | undefined;
+  docKey: string;
+  project: Project;
+};
+
+function OnBoardingStep(props: Props) {
+  const {docKey, project, docContent} = props;
+
+  const [increment, setIncrement] = useState<number>(0);
+
+  if (!docContent) {
+    return null;
+  }
+
+  const localStorageKey = `perf-onboarding-${project.id}-${docKey}`;
+
+  function isChecked() {
+    return localStorage.getItem(localStorageKey) === 'check';
+  }
+
+  return (
+    <Wrapper>
+      <TaskCheckBox>
+        <CheckboxFancy
+          size="22px"
+          isChecked={isChecked()}
+          onClick={event => {
+            event.preventDefault();
+            event.stopPropagation();
+
+            if (isChecked()) {
+              localStorage.removeItem(localStorageKey);
+            } else {
+              localStorage.setItem(localStorageKey, 'check');
+            }
+            setIncrement(increment + 1);
+
+            return;
+          }}
+        />
+      </TaskCheckBox>
+      <DocumentationWrapper dangerouslySetInnerHTML={{__html: docContent}} />
+    </Wrapper>
+  );
+}
+
+const Wrapper = styled('div')`
+  position: relative;
+`;
+
+const TaskCheckBox = styled('div')`
+  float: left;
+  margin-right: ${space(1.5)};
+  height: 27px;
+  display: flex;
+  align-items: center;
+  z-index: 2;
+  position: relative;
+`;
+
+const DocumentationWrapper = styled('div')`
+  line-height: 1.5;
+
+  .gatsby-highlight {
+    margin-bottom: ${space(3)};
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  .alert {
+    margin-bottom: ${space(3)};
+    border-radius: ${p => p.theme.borderRadius};
+  }
+
+  pre {
+    word-break: break-all;
+    white-space: pre-wrap;
+  }
+
+  blockquote {
+    padding: ${space(1)};
+    margin-left: 0;
+    background: ${p => p.theme.alert.info.backgroundLight};
+    border-left: 2px solid ${p => p.theme.alert.info.border};
+  }
+  blockquote > *:last-child {
+    margin-bottom: 0;
+  }
+
+  /* Ensures documentation content is placed behind the checkbox */
+  z-index: 1;
+  position: relative;
+`;
+
+export default OnBoardingStep;

+ 141 - 0
static/app/components/performanceOnboarding/usePerformanceOnboardingDocs.tsx

@@ -0,0 +1,141 @@
+import {useEffect, useState} from 'react';
+import * as Sentry from '@sentry/react';
+
+import {loadDocs} from 'sentry/actionCreators/projects';
+import {
+  PlatformKey,
+  withoutPerformanceSupport,
+  withPerformanceOnboarding,
+} from 'sentry/data/platformCategories';
+import platforms from 'sentry/data/platforms';
+import {Project} from 'sentry/types';
+import useApi from 'sentry/utils/useApi';
+import useOrganization from 'sentry/utils/useOrganization';
+
+export function generateOnboardingDocKeys(platform: PlatformKey): string[] {
+  return ['1-install', '2-configure', '3-verify'].map(
+    key => `${platform}-performance-onboarding-${key}`
+  );
+}
+
+const INITIAL_LOADING_DOCS = {};
+const INITIAL_DOC_CONTENTS = {};
+
+function usePerformanceOnboardingDocs(project: Project) {
+  const organization = useOrganization();
+  const api = useApi();
+
+  const [loadingDocs, setLoadingDocs] =
+    useState<Record<string, boolean>>(INITIAL_LOADING_DOCS);
+  const [docContents, setDocContents] =
+    useState<Record<string, string>>(INITIAL_DOC_CONTENTS);
+
+  const currentPlatform = project.platform
+    ? platforms.find(p => p.id === project.platform)
+    : undefined;
+
+  const hasPerformanceOnboarding = currentPlatform
+    ? withPerformanceOnboarding.has(currentPlatform.id)
+    : false;
+
+  const doesNotSupportPerformance = currentPlatform
+    ? withoutPerformanceSupport.has(currentPlatform.id)
+    : false;
+
+  useEffect(() => {
+    if (!currentPlatform || !hasPerformanceOnboarding || doesNotSupportPerformance) {
+      if (loadingDocs !== INITIAL_LOADING_DOCS) {
+        setLoadingDocs(INITIAL_LOADING_DOCS);
+      }
+      if (docContents !== INITIAL_DOC_CONTENTS) {
+        setDocContents(INITIAL_DOC_CONTENTS);
+      }
+      return;
+    }
+
+    const docKeys = generateOnboardingDocKeys(currentPlatform.id);
+
+    docKeys.forEach(docKey => {
+      if (docKey in loadingDocs) {
+        // If a documentation content is loading, we should not attempt to fetch it again.
+        // otherwise, if it's not loading, we should only fetch at most once.
+        // Any errors that occurred will be captured via Sentry.
+        return;
+      }
+
+      const setLoadingDoc = (loadingState: boolean) =>
+        setLoadingDocs(prevState => {
+          return {
+            ...prevState,
+            [docKey]: loadingState,
+          };
+        });
+
+      const setDocContent = (docContent: string | undefined) =>
+        setDocContents(prevState => {
+          if (docContent === undefined) {
+            const newState = {
+              ...prevState,
+            };
+            delete newState[docKey];
+            return newState;
+          }
+          return {
+            ...prevState,
+            [docKey]: docContent,
+          };
+        });
+
+      setLoadingDoc(true);
+
+      loadDocs(api, organization.slug, project.slug, docKey as any)
+        .then(({html}) => {
+          setDocContent(html as string);
+          setLoadingDoc(false);
+        })
+        .catch(error => {
+          Sentry.captureException(error);
+          setDocContent(undefined);
+          setLoadingDoc(false);
+        });
+    });
+  }, [
+    currentPlatform,
+    hasPerformanceOnboarding,
+    doesNotSupportPerformance,
+    api,
+    loadingDocs,
+    organization.slug,
+    project.slug,
+    docContents,
+  ]);
+
+  if (!currentPlatform || !hasPerformanceOnboarding || doesNotSupportPerformance) {
+    return {
+      isLoading: false,
+      hasOnboardingContents: false,
+      docContents: {},
+    };
+  }
+
+  const docKeys = generateOnboardingDocKeys(currentPlatform.id);
+
+  const isLoading = docKeys.some(key => {
+    if (key in loadingDocs) {
+      return !!loadingDocs[key];
+    }
+    return true;
+  });
+
+  const hasOnboardingContents = docKeys.every(
+    key => typeof docContents[key] === 'string'
+  );
+
+  return {
+    isLoading,
+    hasOnboardingContents,
+    docContents,
+  };
+}
+
+export default usePerformanceOnboardingDocs;

+ 24 - 0
static/app/components/performanceOnboarding/utils.tsx

@@ -0,0 +1,24 @@
+import {
+  withoutPerformanceSupport,
+  withPerformanceOnboarding,
+} from 'sentry/data/platformCategories';
+import {Project} from 'sentry/types';
+
+export function filterProjects(rawProjects: Project[]) {
+  // filter on projects that have not sent a first transaction event
+  const projectsWithoutFirstTransactionEvent = rawProjects.filter(
+    p =>
+      p.firstTransactionEvent === false &&
+      (!p.platform || !withoutPerformanceSupport.has(p.platform))
+  );
+
+  // additionally filter on projects that have performance onboarding checklist support
+  const projectsForOnboarding = projectsWithoutFirstTransactionEvent.filter(
+    p => p.platform && withPerformanceOnboarding.has(p.platform)
+  );
+
+  return {
+    projectsWithoutFirstTransactionEvent,
+    projectsForOnboarding,
+  };
+}

+ 12 - 1
static/app/components/sidebar/index.tsx

@@ -7,6 +7,7 @@ import {hideSidebar, showSidebar} from 'sentry/actionCreators/preferences';
 import Feature from 'sentry/components/acl/feature';
 import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import HookOrDefault from 'sentry/components/hookOrDefault';
+import PerformanceOnboardingSidebar from 'sentry/components/performanceOnboarding/sidebar';
 import {
   IconChevron,
   IconDashboard,
@@ -54,6 +55,10 @@ type Props = {
   organization?: Organization;
 };
 
+function activatePanel(panel: SidebarPanelKey) {
+  SidebarPanelStore.activatePanel(panel);
+}
+
 function togglePanel(panel: SidebarPanelKey) {
   SidebarPanelStore.togglePanel(panel);
 }
@@ -100,7 +105,7 @@ function Sidebar({location, organization}: Props) {
   // Trigger panels depending on the location hash
   useEffect(() => {
     if (location?.hash === '#welcome') {
-      togglePanel(SidebarPanelKey.OnboardingWizard);
+      activatePanel(SidebarPanelKey.OnboardingWizard);
     }
   }, [location?.hash]);
 
@@ -340,6 +345,12 @@ function Sidebar({location, organization}: Props) {
 
       {hasOrganization && (
         <SidebarSectionGroup>
+          <PerformanceOnboardingSidebar
+            currentPanel={activePanel}
+            onShowPanel={() => togglePanel(SidebarPanelKey.PerformanceOnboarding)}
+            hidePanel={hidePanel}
+            {...sidebarItemProps}
+          />
           <SidebarSection noMargin noPadding>
             <OnboardingStatus
               org={organization}

+ 1 - 0
static/app/components/sidebar/types.tsx

@@ -4,6 +4,7 @@ export enum SidebarPanelKey {
   Broadcasts = 'broadcasts',
   OnboardingWizard = 'todos',
   ServiceIncidents = 'statusupdate',
+  PerformanceOnboarding = 'performance_onboarding',
 }
 
 export type CommonSidebarProps = {

+ 13 - 0
static/app/data/platformCategories.tsx

@@ -183,6 +183,19 @@ export const performance: PlatformKey[] = [
   'node-connect',
 ];
 
+// List of platforms that have performance onboarding checklist content
+export const withPerformanceOnboarding: Set<PlatformKey> = new Set([
+  'javascript',
+  'javascript-react',
+]);
+
+// List of platforms that do not have performance support. We make use of this list in the product to not provide any Performance
+// views such as Performance onboarding checklist.
+export const withoutPerformanceSupport: Set<PlatformKey> = new Set([
+  'elixir',
+  'minidump',
+]);
+
 export const releaseHealth: PlatformKey[] = [
   // frontend
   'javascript',

Some files were not shown because too many files changed in this diff