Skip to content

Commit 5b45390

Browse files
committed
fix(onboarding): Harden SCM project creation against duplicates
Address several gaps between SCM project creation and the legacy flow: - Use useExperiment in useBackActions to preserve SCM context when going back from setup-docs, instead of a brittle step-ID check - Guard the Create button against teams/projects still loading so the duplicate check cannot be bypassed by a race - Pre-populate name and team from an existing project when the user navigates back after the project already received events - Update the existing project in place via PUT rather than creating a duplicate or leaving an orphan
1 parent 101823f commit 5b45390

File tree

2 files changed

+60
-6
lines changed

2 files changed

+60
-6
lines changed

static/app/views/onboarding/scmProjectDetails.spec.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
OnboardingContextProvider,
1010
type OnboardingSessionState,
1111
} from 'sentry/components/onboarding/onboardingContext';
12+
import {ProjectsStore} from 'sentry/stores/projectsStore';
1213
import {TeamStore} from 'sentry/stores/teamStore';
1314
import type {OnboardingSelectedSDK} from 'sentry/types/onboarding';
1415
import * as analytics from 'sentry/utils/analytics';
@@ -44,6 +45,7 @@ describe('ScmProjectDetails', () => {
4445
beforeEach(() => {
4546
sessionStorageWrapper.clear();
4647
TeamStore.loadInitialData([teamWithAccess]);
48+
ProjectsStore.loadInitialData([]);
4749

4850
// useCreateNotificationAction queries messaging integrations on mount
4951
MockApiClient.addMockResponse({

static/app/views/onboarding/scmProjectDetails.tsx

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {Container, Flex, Stack} from '@sentry/scraps/layout';
77
import {Text} from '@sentry/scraps/text';
88

99
import {addErrorMessage} from 'sentry/actionCreators/indicator';
10+
import {update as updateProject} from 'sentry/actionCreators/projects';
1011
import {useOnboardingContext} from 'sentry/components/onboarding/onboardingContext';
1112
import {useCreateProjectAndRules} from 'sentry/components/onboarding/useCreateProjectAndRules';
1213
import {TeamSelector} from 'sentry/components/teamSelector';
@@ -15,7 +16,9 @@ import {t} from 'sentry/locale';
1516
import type {Team} from 'sentry/types/organization';
1617
import {trackAnalytics} from 'sentry/utils/analytics';
1718
import {slugify} from 'sentry/utils/slugify';
19+
import {useApi} from 'sentry/utils/useApi';
1820
import {useOrganization} from 'sentry/utils/useOrganization';
21+
import {useProjects} from 'sentry/utils/useProjects';
1922
import {useTeams} from 'sentry/utils/useTeams';
2023
import {
2124
DEFAULT_ISSUE_ALERT_OPTIONS_VALUES,
@@ -32,10 +35,12 @@ import type {StepProps} from './types';
3235
const PROJECT_DETAILS_WIDTH = '285px';
3336

3437
export function ScmProjectDetails({onComplete}: StepProps) {
38+
const api = useApi();
3539
const organization = useOrganization();
36-
const {selectedPlatform, selectedFeatures, setCreatedProjectSlug} =
40+
const {selectedPlatform, selectedFeatures, createdProjectSlug, setCreatedProjectSlug} =
3741
useOnboardingContext();
38-
const {teams} = useTeams();
42+
const {teams, fetching: isLoadingTeams} = useTeams();
43+
const {projects, initiallyLoaded: projectsLoaded} = useProjects();
3944
const createProjectAndRules = useCreateProjectAndRules();
4045
useEffect(() => {
4146
trackAnalytics('onboarding.scm_project_details_step_viewed', {organization});
@@ -44,12 +49,21 @@ export function ScmProjectDetails({onComplete}: StepProps) {
4449
const firstAdminTeam = teams.find((team: Team) => team.access.includes('team:admin'));
4550
const defaultName = slugify(selectedPlatform?.key ?? '');
4651

47-
// State tracks user edits; derived values fall back to defaults from context/teams
52+
// If the onboarding context still holds a created project slug (e.g. the
53+
// user went back after the project already received its first event),
54+
// look it up in the store so we can pre-populate and update in place.
55+
const existingProject = createdProjectSlug
56+
? projects.find(p => p.slug === createdProjectSlug)
57+
: undefined;
58+
59+
// State tracks user edits; derived values fall back to the existing
60+
// project if one was found, then to defaults from context/teams.
4861
const [projectName, setProjectName] = useState<string | null>(null);
4962
const [teamSlug, setTeamSlug] = useState<string | null>(null);
5063

51-
const projectNameResolved = projectName ?? defaultName;
52-
const teamSlugResolved = teamSlug ?? firstAdminTeam?.slug ?? '';
64+
const projectNameResolved = projectName ?? existingProject?.slug ?? defaultName;
65+
const teamSlugResolved =
66+
teamSlug ?? existingProject?.teams[0]?.slug ?? firstAdminTeam?.slug ?? '';
5367

5468
const [alertRuleConfig, setAlertRuleConfig] = useState<AlertRuleOptions>(
5569
DEFAULT_ISSUE_ALERT_OPTIONS_VALUES
@@ -94,13 +108,51 @@ export function ScmProjectDetails({onComplete}: StepProps) {
94108
projectNameResolved.length > 0 &&
95109
teamSlugResolved.length > 0 &&
96110
!!selectedPlatform &&
97-
!createProjectAndRules.isPending;
111+
!createProjectAndRules.isPending &&
112+
!isLoadingTeams &&
113+
projectsLoaded;
98114

99115
async function handleCreateProject() {
100116
if (!selectedPlatform || !canSubmit) {
101117
return;
102118
}
103119

120+
if (existingProject) {
121+
// A project was already created in a previous pass through this step.
122+
// TODO(onboarding): pre-populate and apply alertRuleConfig changes
123+
// from the existing project's rules.
124+
const platformChanged = existingProject.platform !== selectedPlatform.key;
125+
126+
if (platformChanged) {
127+
// Platform changed — abandon the old project and fall through to
128+
// create a new one, matching legacy onboarding behavior.
129+
} else if (projectName === null && teamSlug === null) {
130+
// Nothing changed — reuse the existing project as-is.
131+
setCreatedProjectSlug(existingProject.slug);
132+
onComplete(undefined, selectedFeatures ? {product: selectedFeatures} : undefined);
133+
return;
134+
} else {
135+
// Same platform but name or team changed — update in place.
136+
try {
137+
const updated = await updateProject(api, {
138+
orgId: organization.slug,
139+
projectId: existingProject.slug,
140+
data: {name: projectNameResolved},
141+
});
142+
143+
setCreatedProjectSlug(updated.slug);
144+
onComplete(
145+
undefined,
146+
selectedFeatures ? {product: selectedFeatures} : undefined
147+
);
148+
} catch (error) {
149+
addErrorMessage(t('Failed to update project'));
150+
Sentry.captureException(error);
151+
}
152+
return;
153+
}
154+
}
155+
104156
trackAnalytics('onboarding.scm_project_details_create_clicked', {organization});
105157

106158
try {

0 commit comments

Comments
 (0)