diff --git a/static/app/views/onboarding/onboarding.spec.tsx b/static/app/views/onboarding/onboarding.spec.tsx index 9fac74c0914a16..2416385bc4c6dc 100644 --- a/static/app/views/onboarding/onboarding.spec.tsx +++ b/static/app/views/onboarding/onboarding.spec.tsx @@ -630,7 +630,7 @@ describe('Onboarding', () => { describe('SCM onboarding flow', () => { const scmOrganization = OrganizationFixture({ - features: ['onboarding-scm-experiment'], + features: ['onboarding-scm-experiment', 'onboarding-scm-project-details'], }); const githubProvider = GitHubIntegrationProviderFixture({ diff --git a/static/app/views/onboarding/onboarding.tsx b/static/app/views/onboarding/onboarding.tsx index 35e28e7d48097d..f28d75bd31a074 100644 --- a/static/app/views/onboarding/onboarding.tsx +++ b/static/app/views/onboarding/onboarding.tsx @@ -170,7 +170,15 @@ export function OnboardingWithoutContext() { feature: 'onboarding-scm-experiment', }); - const onboardingSteps = hasScmOnboarding ? scmOnboardingSteps : legacyOnboardingSteps; + const hasProjectDetailsStep = organization.features.includes( + 'onboarding-scm-project-details' + ); + + const scmSteps = hasProjectDetailsStep + ? scmOnboardingSteps + : scmOnboardingSteps.filter(s => s.id !== OnboardingStepId.SCM_PROJECT_DETAILS); + + const onboardingSteps = hasScmOnboarding ? scmSteps : legacyOnboardingSteps; const stepObj = onboardingSteps.find(({id}) => stepId === id); const stepIndex = onboardingSteps.findIndex(({id}) => stepId === id); diff --git a/static/app/views/onboarding/scmPlatformFeatures.tsx b/static/app/views/onboarding/scmPlatformFeatures.tsx index fadfe5afce31ca..3a7b58c4d8a696 100644 --- a/static/app/views/onboarding/scmPlatformFeatures.tsx +++ b/static/app/views/onboarding/scmPlatformFeatures.tsx @@ -1,4 +1,5 @@ import {useCallback, useEffect, useMemo, useState} from 'react'; +import * as Sentry from '@sentry/react'; import {LayoutGroup, motion} from 'framer-motion'; import {PlatformIcon} from 'platformicons'; @@ -7,6 +8,7 @@ import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout'; import {Select} from '@sentry/scraps/select'; import {Heading} from '@sentry/scraps/text'; +import {addErrorMessage} from 'sentry/actionCreators/indicator'; import {closeModal, openConsoleModal, openModal} from 'sentry/actionCreators/modal'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {SupportedLanguages} from 'sentry/components/onboarding/frameworkSuggestionModal'; @@ -16,13 +18,17 @@ import { getDisabledProducts, platformProductAvailability, } from 'sentry/components/onboarding/productSelection'; +import {useCreateProject} from 'sentry/components/onboarding/useCreateProject'; import {platforms} from 'sentry/data/platforms'; import {t} from 'sentry/locale'; import type {OnboardingSelectedSDK} from 'sentry/types/onboarding'; +import type {Team} from 'sentry/types/organization'; import type {PlatformIntegration, PlatformKey} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import {isDisabledGamingPlatform} from 'sentry/utils/platform'; import {useOrganization} from 'sentry/utils/useOrganization'; +import {useProjects} from 'sentry/utils/useProjects'; +import {useTeams} from 'sentry/utils/useTeams'; import {ScmFeatureSelectionCards} from 'sentry/views/onboarding/components/scmFeatureSelectionCards'; import {ScmPlatformCard} from 'sentry/views/onboarding/components/scmPlatformCard'; @@ -84,8 +90,16 @@ export function ScmPlatformFeatures({onComplete}: StepProps) { setSelectedPlatform, selectedFeatures, setSelectedFeatures, + setCreatedProjectSlug, } = useOnboardingContext(); + const {teams} = useTeams(); + const {projects} = useProjects(); + const createProject = useCreateProject(); + const hasProjectDetailsStep = organization.features.includes( + 'onboarding-scm-project-details' + ); + const [showManualPicker, setShowManualPicker] = useState(false); useEffect(() => { @@ -306,7 +320,7 @@ export function ScmPlatformFeatures({onComplete}: StepProps) { } } - function handleContinue() { + async function handleContinue() { // Persist derived defaults to context if user accepted them if (currentPlatformKey && !selectedPlatform?.key) { setPlatform(currentPlatformKey); @@ -314,6 +328,48 @@ export function ScmPlatformFeatures({onComplete}: StepProps) { if (!selectedFeatures) { setSelectedFeatures(currentFeatures); } + + if (!hasProjectDetailsStep) { + // Auto-create project with defaults when SCM_PROJECT_DETAILS step is skipped + const platform = + selectedPlatform ?? + (currentPlatformKey + ? toSelectedSdk(getPlatformInfo(currentPlatformKey)!) + : undefined); + if (!platform) { + return; + } + + // If a project for this platform already exists (e.g. the user went + // back after the project had already received its first event), skip + // creation and reuse it — mirrors useConfigureSdk logic. + const existingProject = projects.find(p => p.slug === platform.key); + if (existingProject) { + setCreatedProjectSlug(existingProject.slug); + onComplete(undefined, {product: currentFeatures}); + return; + } + + const firstAdminTeam = teams.find((team: Team) => + team.access.includes('team:admin') + ); + + try { + const project = await createProject.mutateAsync({ + name: platform.key, + platform, + default_rules: true, + firstTeamSlug: firstAdminTeam?.slug, + }); + setCreatedProjectSlug(project.slug); + onComplete(undefined, {product: currentFeatures}); + } catch (error) { + addErrorMessage(t('Failed to create project')); + Sentry.captureException(error); + } + return; + } + onComplete(); } @@ -459,7 +515,8 @@ export function ScmPlatformFeatures({onComplete}: StepProps) { features: currentFeatures, }} onClick={handleContinue} - disabled={!currentPlatformKey} + disabled={!currentPlatformKey || createProject.isPending} + busy={createProject.isPending} > {t('Continue')} diff --git a/static/app/views/onboarding/scmProjectDetails.tsx b/static/app/views/onboarding/scmProjectDetails.tsx index 65741fb574b488..11245241b425dc 100644 --- a/static/app/views/onboarding/scmProjectDetails.tsx +++ b/static/app/views/onboarding/scmProjectDetails.tsx @@ -16,6 +16,7 @@ import type {Team} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; import {slugify} from 'sentry/utils/slugify'; import {useOrganization} from 'sentry/utils/useOrganization'; +import {useProjects} from 'sentry/utils/useProjects'; import {useTeams} from 'sentry/utils/useTeams'; import { DEFAULT_ISSUE_ALERT_OPTIONS_VALUES, @@ -36,6 +37,7 @@ export function ScmProjectDetails({onComplete}: StepProps) { const {selectedPlatform, selectedFeatures, setCreatedProjectSlug} = useOnboardingContext(); const {teams} = useTeams(); + const {projects} = useProjects(); const createProjectAndRules = useCreateProjectAndRules(); useEffect(() => { trackAnalytics('onboarding.scm_project_details_step_viewed', {organization}); @@ -101,6 +103,16 @@ export function ScmProjectDetails({onComplete}: StepProps) { return; } + // If a project for this name already exists (e.g. the user went back + // after the project had already received its first event), skip + // creation and reuse it — mirrors useConfigureSdk logic. + const existingProject = projects.find(p => p.slug === projectNameResolved); + if (existingProject) { + setCreatedProjectSlug(existingProject.slug); + onComplete(undefined, selectedFeatures ? {product: selectedFeatures} : undefined); + return; + } + trackAnalytics('onboarding.scm_project_details_create_clicked', {organization}); try { diff --git a/static/app/views/onboarding/useBackActions.tsx b/static/app/views/onboarding/useBackActions.tsx index 0decafc266c0a3..0e46d66abb4566 100644 --- a/static/app/views/onboarding/useBackActions.tsx +++ b/static/app/views/onboarding/useBackActions.tsx @@ -10,6 +10,7 @@ import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse'; import type {RequestError} from 'sentry/utils/requestError/requestError'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useApi} from 'sentry/utils/useApi'; +import {useExperiment} from 'sentry/utils/useExperiment'; import {useOrganization} from 'sentry/utils/useOrganization'; import type {StepDescriptor} from 'sentry/views/onboarding/types'; @@ -33,6 +34,9 @@ export function useBackActions({ const api = useApi(); const organization = useOrganization(); const onboardingContext = useOnboardingContext(); + const {inExperiment: hasScmOnboarding} = useExperiment({ + feature: 'onboarding-scm-experiment', + }); const currentStep = onboardingSteps[stepIndex]; const deleteRecentCreatedProject = useCallback( @@ -118,7 +122,7 @@ export function useBackActions({ // store data and skip project creation. // In the SCM flow, preserve context so the user keeps their SCM // connection, repo selection, and feature choices. - await deleteRecentCreatedProject(prevStep.id === 'scm-project-details'); + await deleteRecentCreatedProject(hasScmOnboarding); } if (!browserBackButton) { @@ -126,13 +130,14 @@ export function useBackActions({ } }, [ - goToStep, + currentStep, organization, - onboardingContext, isRecentCreatedProjectActive, recentCreatedProject, - currentStep, + onboardingContext, + goToStep, deleteRecentCreatedProject, + hasScmOnboarding, ] );