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.spec.tsx b/static/app/views/onboarding/scmPlatformFeatures.spec.tsx index 2d11e246cdc512..e353356b416b22 100644 --- a/static/app/views/onboarding/scmPlatformFeatures.spec.tsx +++ b/static/app/views/onboarding/scmPlatformFeatures.spec.tsx @@ -10,6 +10,8 @@ import { OnboardingContextProvider, type OnboardingSessionState, } from 'sentry/components/onboarding/onboardingContext'; +import {ProjectsStore} from 'sentry/stores/projectsStore'; +import {TeamStore} from 'sentry/stores/teamStore'; import * as analytics from 'sentry/utils/analytics'; import {sessionStorageWrapper} from 'sentry/utils/sessionStorage'; @@ -72,6 +74,8 @@ describe('ScmPlatformFeatures', () => { beforeEach(() => { jest.clearAllMocks(); sessionStorageWrapper.clear(); + ProjectsStore.loadInitialData([]); + TeamStore.loadInitialData([]); }); afterEach(() => { diff --git a/static/app/views/onboarding/scmPlatformFeatures.tsx b/static/app/views/onboarding/scmPlatformFeatures.tsx index fadfe5afce31ca..020c44e7200e7b 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,17 @@ export function ScmPlatformFeatures({onComplete}: StepProps) { setSelectedPlatform, selectedFeatures, setSelectedFeatures, + createdProjectSlug, + setCreatedProjectSlug, } = useOnboardingContext(); + const {teams, fetching: isLoadingTeams} = useTeams(); + const {projects, initiallyLoaded: projectsLoaded} = useProjects(); + const createProject = useCreateProject(); + const hasProjectDetailsStep = organization.features.includes( + 'onboarding-scm-project-details' + ); + const [showManualPicker, setShowManualPicker] = useState(false); useEffect(() => { @@ -306,7 +321,11 @@ export function ScmPlatformFeatures({onComplete}: StepProps) { } } - function handleContinue() { + const existingProject = createdProjectSlug + ? projects.find(p => p.slug === createdProjectSlug) + : undefined; + + async function handleContinue() { // Persist derived defaults to context if user accepted them if (currentPlatformKey && !selectedPlatform?.key) { setPlatform(currentPlatformKey); @@ -314,6 +333,47 @@ 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 was already created for this platform (e.g. the user + // went back after the project received its first event), reuse it. + // If the platform changed, abandon the old project and create a new + // one — matching legacy onboarding behavior. + if (existingProject?.platform === platform.key) { + 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 +519,12 @@ export function ScmPlatformFeatures({onComplete}: StepProps) { features: currentFeatures, }} onClick={handleContinue} - disabled={!currentPlatformKey} + disabled={ + !currentPlatformKey || + createProject.isPending || + (!hasProjectDetailsStep && (isLoadingTeams || !projectsLoaded)) + } + busy={createProject.isPending} > {t('Continue')} 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, ] );