From a39527a106f4146f83598bce79d9b53cc84017ac Mon Sep 17 00:00:00 2001 From: Jay Goss Date: Wed, 15 Apr 2026 18:00:14 -0500 Subject: [PATCH] feat(onboarding): Gate SCM_PROJECT_DETAILS step with feature flag When `onboarding-scm-project-details` flag is absent, skip the project details step and auto-create the project with defaults (platform key as name, first admin team, default alert rules) during the platform features Continue action. Also update useBackActions to use useExperiment for the SCM context preservation check, since the hardcoded step ID no longer works when the step is removed. Refs VDY-82 --- .../app/views/onboarding/onboarding.spec.tsx | 2 +- static/app/views/onboarding/onboarding.tsx | 10 ++- .../onboarding/scmPlatformFeatures.spec.tsx | 4 ++ .../views/onboarding/scmPlatformFeatures.tsx | 69 ++++++++++++++++++- .../app/views/onboarding/useBackActions.tsx | 13 ++-- 5 files changed, 90 insertions(+), 8 deletions(-) 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, ] );