Browse Source

fix(onboarding): Update logic to fetch firstError in onboarding - new orgs (#48515)

Priscila Oliveira 1 year ago
parent
commit
23782246a2

+ 67 - 0
static/app/components/onboarding/useRecentCreatedProject.tsx

@@ -0,0 +1,67 @@
+import moment from 'moment';
+
+import {Group, OnboardingRecentCreatedProject, Organization, Project} from 'sentry/types';
+import {useApiQuery} from 'sentry/utils/queryClient';
+
+// Refetch the data every second
+const DEFAULT_POLL_INTERVAL_MS = 1000;
+
+type Props = {
+  orgSlug: Organization['slug'];
+  projectSlug?: Project['slug'];
+};
+
+// This hook will fetch the project details endpoint until a firstEvent(issue) is received
+// When the firstEvent is received it fetches the issues endpoint to find the first issue
+export function useRecentCreatedProject({
+  orgSlug,
+  projectSlug,
+}: Props): undefined | OnboardingRecentCreatedProject {
+  const {isLoading: isProjectLoading, data: project} = useApiQuery<Project>(
+    [`/projects/${orgSlug}/${projectSlug}/`],
+    {
+      staleTime: 0,
+      enabled: !!projectSlug,
+      refetchInterval: data => {
+        if (!data) {
+          return false;
+        }
+        const [projectData] = data as unknown as [Project | undefined, any, any];
+        return projectData?.firstEvent ? false : DEFAULT_POLL_INTERVAL_MS;
+      },
+    }
+  );
+
+  const firstEvent = project?.firstEvent;
+
+  const {data: issues} = useApiQuery<Group[]>(
+    [`/projects/${orgSlug}/${projectSlug}/issues/`],
+    {
+      staleTime: Infinity,
+      enabled: !!firstEvent,
+    }
+  );
+
+  const firstIssue =
+    !!firstEvent && issues
+      ? issues.find((issue: Group) => issue.firstSeen === firstEvent)
+      : undefined;
+
+  const olderThanOneHour = project
+    ? moment.duration(moment().diff(project.dateCreated)).asHours() > 1
+    : false;
+
+  if (isProjectLoading || !project) {
+    return undefined;
+  }
+
+  return {
+    ...project,
+    firstTransaction: !!project?.firstTransactionEvent,
+    hasReplays: !!project?.hasReplays,
+    hasSessions: !!project?.hasSessions,
+    firstError: !!firstEvent,
+    firstIssue,
+    olderThanOneHour,
+  };
+}

+ 10 - 1
static/app/types/onboarding.tsx

@@ -3,7 +3,7 @@ import {RouteContextInterface} from 'react-router';
 import {OnboardingContextProps} from 'sentry/components/onboarding/onboardingContext';
 import {Category} from 'sentry/components/platformPicker';
 import {PlatformKey} from 'sentry/data/platformCategories';
-import {Organization, PlatformIntegration, Project} from 'sentry/types';
+import {Group, Organization, PlatformIntegration, Project} from 'sentry/types';
 
 import type {AvatarUser} from './user';
 
@@ -140,3 +140,12 @@ export type OnboardingSelectedSDK = {
   language: PlatformIntegration['language'];
   type: PlatformIntegration['type'];
 };
+
+export type OnboardingRecentCreatedProject = Project & {
+  firstError: boolean;
+  firstTransaction: boolean;
+  hasReplays: boolean;
+  hasSessions: boolean;
+  olderThanOneHour: boolean;
+  firstIssue?: Group;
+};

+ 1 - 1
static/app/utils/analytics/growthAnalyticsEvents.tsx

@@ -90,7 +90,7 @@ export type GrowthEventParameters = {
   };
   'growth.onboarding_clicked_instrument_app': {source?: string};
   'growth.onboarding_clicked_setup_platform_later': PlatformParam & {
-    project_index: number;
+    project_id: string;
   };
   'growth.onboarding_clicked_skip': {source?: string};
   'growth.onboarding_load_choose_platform': {};

+ 6 - 2
static/app/views/onboarding/components/createProjectsFooter.tsx

@@ -169,6 +169,10 @@ export function CreateProjectsFooter({
           organization={organization}
           selectedPlatform={selectedPlatform}
           onConfigure={selectedFramework => {
+            onboardingContext.setData({
+              ...onboardingContext.data,
+              selectedSDK: selectedFramework,
+            });
             createPlatformProject(selectedFramework);
           }}
           onSkip={createPlatformProject}
@@ -184,7 +188,7 @@ export function CreateProjectsFooter({
         },
       }
     );
-  }, [selectedPlatform, createPlatformProject, organization]);
+  }, [selectedPlatform, createPlatformProject, onboardingContext, organization]);
 
   return (
     <GenericFooter>
@@ -199,7 +203,7 @@ export function CreateProjectsFooter({
               />
             </div>
             <PlatformsSelected>
-              {t('1 platform selected')}
+              {t('platform selected')}
               <ClearButton priority="link" onClick={clearPlatform} size="zero">
                 {t('Clear')}
               </ClearButton>

+ 24 - 40
static/app/views/onboarding/components/firstEventFooter.tsx

@@ -1,4 +1,4 @@
-import {Fragment} from 'react';
+import {useCallback} from 'react';
 import styled from '@emotion/styled';
 import {motion, Variants} from 'framer-motion';
 
@@ -9,21 +9,18 @@ import {IconCheckmark} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator';
 import {space} from 'sentry/styles/space';
-import {Group, Organization, Project} from 'sentry/types';
+import {OnboardingRecentCreatedProject, Organization} from 'sentry/types';
 import {trackAnalytics} from 'sentry/utils/analytics';
-import EventWaiter from 'sentry/utils/eventWaiter';
 import testableTransition from 'sentry/utils/testableTransition';
 import CreateSampleEventButton from 'sentry/views/onboarding/createSampleEventButton';
 
 import GenericFooter from './genericFooter';
 
 interface FirstEventFooterProps {
-  handleFirstIssueReceived: () => void;
-  hasFirstEvent: boolean;
   isLast: boolean;
   onClickSetupLater: () => void;
   organization: Organization;
-  project: Project;
+  project: OnboardingRecentCreatedProject;
 }
 
 export default function FirstEventFooter({
@@ -31,23 +28,21 @@ export default function FirstEventFooter({
   project,
   onClickSetupLater,
   isLast,
-  hasFirstEvent,
-  handleFirstIssueReceived,
 }: FirstEventFooterProps) {
   const source = 'targeted_onboarding_first_event_footer';
 
-  const getSecondaryCta = () => {
+  const getSecondaryCta = useCallback(() => {
     // if hasn't sent first event, allow skiping.
     // if last, no secondary cta
-    if (!hasFirstEvent && !isLast) {
+    if (!project?.firstError && !isLast) {
       return <Button onClick={onClickSetupLater}>{t('Next Platform')}</Button>;
     }
     return null;
-  };
+  }, [project?.firstError, isLast, onClickSetupLater]);
 
-  const getPrimaryCta = ({firstIssue}: {firstIssue: null | boolean | Group}) => {
+  const getPrimaryCta = useCallback(() => {
     // if hasn't sent first event, allow creation of sample error
-    if (!hasFirstEvent) {
+    if (!project?.firstError) {
       return (
         <CreateSampleEventButton
           project={project}
@@ -58,12 +53,11 @@ export default function FirstEventFooter({
         </CreateSampleEventButton>
       );
     }
-
     return (
       <Button
         to={`/organizations/${organization.slug}/issues/${
-          firstIssue && firstIssue !== true && 'id' in firstIssue
-            ? `${firstIssue.id}/`
+          project?.firstIssue && 'id' in project.firstIssue
+            ? `${project.firstIssue.id}/`
             : ''
         }?referrer=onboarding-first-event-footer`}
         priority="primary"
@@ -71,7 +65,7 @@ export default function FirstEventFooter({
         {t('Take me to my error')}
       </Button>
     );
-  };
+  }, [project, organization.slug]);
 
   return (
     <GridFooter>
@@ -86,30 +80,20 @@ export default function FirstEventFooter({
       >
         {t('Skip Onboarding')}
       </SkipOnboardingLink>
-      <EventWaiter
-        eventType="error"
-        onIssueReceived={handleFirstIssueReceived}
-        {...{project, organization}}
-      >
-        {({firstIssue}) => (
-          <Fragment>
-            <StatusWrapper>
-              {hasFirstEvent ? (
-                <IconCheckmark isCircled color="green400" />
-              ) : (
-                <WaitingIndicator />
-              )}
-              <AnimatedText errorReceived={hasFirstEvent}>
-                {hasFirstEvent ? t('Error Received') : t('Waiting for error')}
-              </AnimatedText>
-            </StatusWrapper>
-            <OnboardingButtonBar gap={2}>
-              {getSecondaryCta()}
-              {getPrimaryCta({firstIssue})}
-            </OnboardingButtonBar>
-          </Fragment>
+      <StatusWrapper>
+        {project?.firstError ? (
+          <IconCheckmark isCircled color="green400" />
+        ) : (
+          <WaitingIndicator />
         )}
-      </EventWaiter>
+        <AnimatedText errorReceived={project?.firstError}>
+          {project?.firstError ? t('Error Received') : t('Waiting for error')}
+        </AnimatedText>
+      </StatusWrapper>
+      <OnboardingButtonBar gap={2}>
+        {getSecondaryCta()}
+        {getPrimaryCta()}
+      </OnboardingButtonBar>
     </GridFooter>
   );
 }

+ 40 - 8
static/app/views/onboarding/onboarding.spec.tsx

@@ -7,8 +7,8 @@ import {
 } from 'sentry-test/reactTestingLibrary';
 
 import {OnboardingContextProvider} from 'sentry/components/onboarding/onboardingContext';
+import * as useRecentCreatedProjectHook from 'sentry/components/onboarding/useRecentCreatedProject';
 import {PlatformKey} from 'sentry/data/platformCategories';
-import ProjectsStore from 'sentry/stores/projectsStore';
 import {OnboardingProjectStatus, Project} from 'sentry/types';
 import Onboarding from 'sentry/views/onboarding/onboarding';
 
@@ -117,7 +117,19 @@ describe('Onboarding', function () {
       body: [],
     });
 
-    ProjectsStore.loadInitialData([nextJsProject]);
+    jest
+      .spyOn(useRecentCreatedProjectHook, 'useRecentCreatedProject')
+      .mockImplementation(() => {
+        return {
+          ...nextJsProject,
+          firstError: false,
+          firstTransaction: false,
+          hasReplays: false,
+          hasSessions: false,
+          olderThanOneHour: false,
+          firstIssue: undefined,
+        };
+      });
 
     render(
       <OnboardingContextProvider
@@ -196,7 +208,19 @@ describe('Onboarding', function () {
       body: [],
     });
 
-    ProjectsStore.loadInitialData([reactProject]);
+    jest
+      .spyOn(useRecentCreatedProjectHook, 'useRecentCreatedProject')
+      .mockImplementation(() => {
+        return {
+          ...reactProject,
+          firstError: false,
+          firstTransaction: false,
+          hasReplays: false,
+          hasSessions: false,
+          olderThanOneHour: false,
+          firstIssue: undefined,
+        };
+      });
 
     render(
       <OnboardingContextProvider
@@ -253,10 +277,6 @@ describe('Onboarding', function () {
       platform: 'javascript-react',
       id: '2',
       slug: 'javascript-react-slug',
-      firstTransactionEvent: false,
-      firstEvent: false,
-      hasReplays: false,
-      hasSessions: true,
     });
 
     const routeParams = {
@@ -289,7 +309,19 @@ describe('Onboarding', function () {
       body: [],
     });
 
-    ProjectsStore.loadInitialData([reactProject]);
+    jest
+      .spyOn(useRecentCreatedProjectHook, 'useRecentCreatedProject')
+      .mockImplementation(() => {
+        return {
+          ...reactProject,
+          firstError: false,
+          firstTransaction: false,
+          hasReplays: false,
+          hasSessions: true,
+          olderThanOneHour: false,
+          firstIssue: undefined,
+        };
+      });
 
     render(
       <OnboardingContextProvider

+ 17 - 30
static/app/views/onboarding/onboarding.tsx

@@ -2,7 +2,6 @@ import {useCallback, useContext, useEffect, useRef, useState} from 'react';
 import {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
 import {AnimatePresence, motion, MotionProps, useAnimation} from 'framer-motion';
-import moment from 'moment';
 
 import {removeProject} from 'sentry/actionCreators/projects';
 import {Button, ButtonProps} from 'sentry/components/button';
@@ -11,6 +10,7 @@ import Hook from 'sentry/components/hook';
 import Link from 'sentry/components/links/link';
 import LogoSentry from 'sentry/components/logoSentry';
 import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
+import {useRecentCreatedProject} from 'sentry/components/onboarding/useRecentCreatedProject';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {IconArrow} from 'sentry/icons';
 import {t} from 'sentry/locale';
@@ -23,7 +23,6 @@ import Redirect from 'sentry/utils/redirect';
 import testableTransition from 'sentry/utils/testableTransition';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
-import useProjects from 'sentry/utils/useProjects';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import PageCorners from 'sentry/views/onboarding/components/pageCorners';
 
@@ -67,7 +66,6 @@ function getOrganizationOnboardingSteps(): StepDescriptor[] {
 function Onboarding(props: Props) {
   const api = useApi();
   const organization = useOrganization();
-  const projects = useProjects();
   const onboardingContext = useContext(OnboardingContext);
   const selectedSDK = onboardingContext.data.selectedSDK;
   const selectedProjectSlug = selectedSDK?.key;
@@ -76,6 +74,16 @@ function Onboarding(props: Props) {
     params: {step: stepId},
   } = props;
 
+  const onboardingSteps = getOrganizationOnboardingSteps();
+  const stepObj = onboardingSteps.find(({id}) => stepId === id);
+  const stepIndex = onboardingSteps.findIndex(({id}) => stepId === id);
+
+  const recentCreatedProject = useRecentCreatedProject({
+    orgSlug: organization.slug,
+    projectSlug:
+      onboardingSteps[stepIndex].id === 'setup-docs' ? selectedProjectSlug : undefined,
+  });
+
   const cornerVariantTimeoutRed = useRef<number | undefined>(undefined);
 
   useEffect(() => {
@@ -92,41 +100,20 @@ function Onboarding(props: Props) {
     'onboarding-project-deletion-on-back-click'
   );
 
-  const onboardingSteps = getOrganizationOnboardingSteps();
-  const stepObj = onboardingSteps.find(({id}) => stepId === id);
-  const stepIndex = onboardingSteps.findIndex(({id}) => stepId === id);
-
-  const loadingProjects = !projects.initiallyLoaded;
-
-  const createdProjectId = Object.keys(onboardingContext.data.projects).find(
-    key =>
-      onboardingContext.data.projects[key].slug ===
-      onboardingContext.data.selectedSDK?.key
-  );
-
-  const recentCreatedProject =
-    !loadingProjects && createdProjectId
-      ? projects.projects.find(p => p.id === createdProjectId)
-      : undefined;
-
-  const recentCreatedProjectOlderThanOneHour = recentCreatedProject
-    ? moment.duration(moment().diff(recentCreatedProject.dateCreated)).asHours() > 1
-    : false;
-
   const shallProjectBeDeleted =
     projectDeletionOnBackClick &&
     onboardingSteps[stepIndex].id === 'setup-docs' &&
     recentCreatedProject &&
     // if the project has received a first error, we don't delete it
-    Boolean(recentCreatedProject.firstEvent) === false &&
+    recentCreatedProject.firstError === false &&
     // if the project has received a first transaction, we don't delete it
-    Boolean(recentCreatedProject.firstTransactionEvent) === false &&
+    recentCreatedProject.firstTransaction === false &&
     // if the project has replays, we don't delete it
-    Boolean(recentCreatedProject.hasReplays) === false &&
+    recentCreatedProject.hasReplays === false &&
     // if the project has sessions, we don't delete it
-    Boolean(recentCreatedProject.hasSessions) === false &&
+    recentCreatedProject.hasSessions === false &&
     // if the project is older than one hour, we don't delete it
-    recentCreatedProjectOlderThanOneHour === false;
+    recentCreatedProject.olderThanOneHour === false;
 
   const cornerVariantControl = useAnimation();
   const updateCornerVariant = () => {
@@ -392,7 +379,7 @@ function Onboarding(props: Props) {
                 route={props.route}
                 router={props.router}
                 location={props.location}
-                selectedProjectSlug={selectedProjectSlug}
+                recentCreatedProject={recentCreatedProject}
                 {...{
                   genSkipOnboardingLink,
                 }}

+ 7 - 7
static/app/views/onboarding/setupDocs.spec.tsx

@@ -101,7 +101,7 @@ describe('Onboarding Setup Docs', function () {
           genSkipOnboardingLink={() => ''}
           orgId={organization.slug}
           search=""
-          selectedProjectSlug="python"
+          recentCreatedProject={project}
         />
       </OnboardingContextProvider>,
       {
@@ -160,7 +160,7 @@ describe('Onboarding Setup Docs', function () {
             genSkipOnboardingLink={() => ''}
             orgId={organization.slug}
             search=""
-            selectedProjectSlug="javascript-react"
+            recentCreatedProject={project}
           />
         </OnboardingContextProvider>,
         {
@@ -219,7 +219,7 @@ describe('Onboarding Setup Docs', function () {
             genSkipOnboardingLink={() => ''}
             orgId={organization.slug}
             search=""
-            selectedProjectSlug="javascript-react"
+            recentCreatedProject={project}
           />
         </OnboardingContextProvider>,
         {
@@ -272,7 +272,7 @@ describe('Onboarding Setup Docs', function () {
             genSkipOnboardingLink={() => ''}
             orgId={organization.slug}
             search=""
-            selectedProjectSlug="javascript-react"
+            recentCreatedProject={project}
           />
         </OnboardingContextProvider>,
         {
@@ -325,7 +325,7 @@ describe('Onboarding Setup Docs', function () {
             genSkipOnboardingLink={() => ''}
             orgId={organization.slug}
             search=""
-            selectedProjectSlug="javascript-react"
+            recentCreatedProject={project}
           />
         </OnboardingContextProvider>,
         {
@@ -386,7 +386,7 @@ describe('Onboarding Setup Docs', function () {
             genSkipOnboardingLink={() => ''}
             orgId={organization.slug}
             search=""
-            selectedProjectSlug="javascript"
+            recentCreatedProject={project}
           />
         </OnboardingContextProvider>,
         {
@@ -432,7 +432,7 @@ describe('Onboarding Setup Docs', function () {
             genSkipOnboardingLink={() => ''}
             orgId={organization.slug}
             search=""
-            selectedProjectSlug="javascript"
+            recentCreatedProject={project}
           />
         </OnboardingContextProvider>
       );

+ 3 - 20
static/app/views/onboarding/setupDocs.tsx

@@ -25,7 +25,6 @@ import {useApiQuery} from 'sentry/utils/queryClient';
 import useApi from 'sentry/utils/useApi';
 import {useExperiment} from 'sentry/utils/useExperiment';
 import useOrganization from 'sentry/utils/useOrganization';
-import useProjects from 'sentry/utils/useProjects';
 import SetupIntroduction from 'sentry/views/onboarding/components/setupIntroduction';
 import {SetupDocsLoader} from 'sentry/views/onboarding/setupDocsLoader';
 
@@ -200,10 +199,9 @@ function ProjectDocs(props: {
   );
 }
 
-function SetupDocs({route, router, location, selectedProjectSlug}: StepProps) {
+function SetupDocs({route, router, location, recentCreatedProject: project}: StepProps) {
   const api = useApi();
   const organization = useOrganization();
-  const {projects: rawProjects} = useProjects();
 
   const {
     logExperiment: newFooterLogExperiment,
@@ -216,17 +214,10 @@ function SetupDocs({route, router, location, selectedProjectSlug}: StepProps) {
     'onboarding-heartbeat-footer'
   );
 
-  // get project
-  const projectIndex = selectedProjectSlug
-    ? rawProjects.findIndex(p => p.slug === selectedProjectSlug) ?? undefined
-    : undefined;
-  const project = projectIndex !== undefined ? rawProjects[projectIndex] : undefined;
-
   // SDK instrumentation
   const [hasError, setHasError] = useState(false);
   const [platformDocs, setPlatformDocs] = useState<PlatformDoc | null>(null);
   const [loadedPlatform, setLoadedPlatform] = useState<PlatformKey | null>(null);
-  const [hasFirstEvent, setHasFirstEvent] = useState<boolean>(!!project?.firstEvent);
 
   const currentPlatform = loadedPlatform ?? project?.platform ?? 'other';
   const [showLoaderOnboarding, setShowLoaderOnboarding] = useState(
@@ -391,19 +382,15 @@ function SetupDocs({route, router, location, selectedProjectSlug}: StepProps) {
             project={project}
             organization={organization}
             isLast
-            hasFirstEvent={hasFirstEvent}
             onClickSetupLater={() => {
               const orgIssuesURL = `/organizations/${organization.slug}/issues/?project=${project.id}&referrer=onboarding-setup-docs`;
               trackAnalytics('growth.onboarding_clicked_setup_platform_later', {
                 organization,
                 platform: currentPlatform,
-                project_index: projectIndex ?? 0,
+                project_id: project.id,
               });
               browserHistory.push(orgIssuesURL);
             }}
-            handleFirstIssueReceived={() => {
-              setHasFirstEvent(true);
-            }}
           />
         )
       ) : (
@@ -411,19 +398,15 @@ function SetupDocs({route, router, location, selectedProjectSlug}: StepProps) {
           project={project}
           organization={organization}
           isLast
-          hasFirstEvent={hasFirstEvent}
           onClickSetupLater={() => {
             const orgIssuesURL = `/organizations/${organization.slug}/issues/?project=${project.id}&referrer=onboarding-setup-docs`;
             trackAnalytics('growth.onboarding_clicked_setup_platform_later', {
               organization,
               platform: currentPlatform,
-              project_index: projectIndex ?? 0,
+              project_id: project.id,
             });
             browserHistory.push(orgIssuesURL);
           }}
-          handleFirstIssueReceived={() => {
-            setHasFirstEvent(true);
-          }}
         />
       )}
     </Fragment>

+ 2 - 2
static/app/views/onboarding/types.ts

@@ -1,6 +1,6 @@
 import {RouteComponentProps} from 'react-router';
 
-import {OnboardingSelectedSDK} from 'sentry/types';
+import {OnboardingRecentCreatedProject, OnboardingSelectedSDK} from 'sentry/types';
 
 export type StepData = {
   platform?: OnboardingSelectedSDK | null;
@@ -17,7 +17,7 @@ export type StepProps = Pick<
   orgId: string;
   search: string;
   stepIndex: number;
-  selectedProjectSlug?: string;
+  recentCreatedProject?: OnboardingRecentCreatedProject;
 };
 
 export type StepDescriptor = {