Skip to content

Commit d453e09

Browse files
committed
fix(onboarding): Handle existing projects and loading races in SCM flow
When the user navigates back from setup-docs after the project has already received events, the project is not deleted. Handle this by detecting the existing project via createdProjectSlug and persisted form state: - Platform changed: abandon old project, create new (matching legacy) - No changes: reuse existing project as-is - Name/team changed: update existing project in place via PUT Also guard the Create button against teams and projects still loading so the existence check cannot be bypassed by a race.
1 parent f1ef345 commit d453e09

File tree

2 files changed

+81
-17
lines changed

2 files changed

+81
-17
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: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ 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} 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';
1314
import {IconGroup, IconProject, IconSiren} from 'sentry/icons';
1415
import {t} from 'sentry/locale';
16+
import type {OnboardingSelectedSDK} from 'sentry/types/onboarding';
1517
import type {Team} from 'sentry/types/organization';
1618
import {trackAnalytics} from 'sentry/utils/analytics';
1719
import {slugify} from 'sentry/utils/slugify';
20+
import {useApi} from 'sentry/utils/useApi';
1821
import {useOrganization} from 'sentry/utils/useOrganization';
22+
import {useProjects} from 'sentry/utils/useProjects';
1923
import {useTeams} from 'sentry/utils/useTeams';
2024
import {
2125
DEFAULT_ISSUE_ALERT_OPTIONS_VALUES,
@@ -32,15 +36,18 @@ import type {StepProps} from './types';
3236
const PROJECT_DETAILS_WIDTH = '285px';
3337

3438
export function ScmProjectDetails({onComplete}: StepProps) {
39+
const api = useApi();
3540
const organization = useOrganization();
3641
const {
3742
selectedPlatform,
3843
selectedFeatures,
44+
createdProjectSlug,
3945
setCreatedProjectSlug,
4046
projectDetailsForm,
4147
setProjectDetailsForm,
4248
} = useOnboardingContext();
43-
const {teams} = useTeams();
49+
const {teams, fetching: isLoadingTeams} = useTeams();
50+
const {projects, initiallyLoaded: projectsLoaded} = useProjects();
4451
const createProjectAndRules = useCreateProjectAndRules();
4552
useEffect(() => {
4653
trackAnalytics('onboarding.scm_project_details_step_viewed', {organization});
@@ -58,6 +65,12 @@ export function ScmProjectDetails({onComplete}: StepProps) {
5865
projectDetailsForm?.teamSlug ?? null
5966
);
6067

68+
const [isSaving, setIsSaving] = useState(false);
69+
70+
// Safety-net: verify the previously created project still exists in the store.
71+
const projectExists =
72+
!!createdProjectSlug && projects.some(p => p.slug === createdProjectSlug);
73+
6174
const projectNameResolved = projectName ?? defaultName;
6275
const teamSlugResolved = teamSlug ?? firstAdminTeam?.slug ?? '';
6376

@@ -104,34 +117,35 @@ export function ScmProjectDetails({onComplete}: StepProps) {
104117
projectNameResolved.length > 0 &&
105118
teamSlugResolved.length > 0 &&
106119
!!selectedPlatform &&
107-
!createProjectAndRules.isPending;
120+
!isSaving &&
121+
!isLoadingTeams &&
122+
projectsLoaded;
108123

109-
async function handleCreateProject() {
110-
if (!selectedPlatform || !canSubmit) {
124+
function persistFormState() {
125+
if (!selectedPlatform) {
111126
return;
112127
}
128+
setProjectDetailsForm({
129+
projectName: projectNameResolved,
130+
teamSlug: teamSlugResolved,
131+
alertRuleConfig,
132+
platform: selectedPlatform.key,
133+
});
134+
}
113135

114-
trackAnalytics('onboarding.scm_project_details_create_clicked', {organization});
115-
136+
async function createNewProject(platform: OnboardingSelectedSDK) {
137+
setIsSaving(true);
116138
try {
117139
const {project} = await createProjectAndRules.mutateAsync({
118140
projectName: projectNameResolved,
119-
platform: selectedPlatform,
141+
platform,
120142
team: teamSlugResolved,
121143
alertRuleConfig: getRequestDataFragment(alertRuleConfig),
122144
createNotificationAction: () => undefined,
123145
});
124146

125-
// Store the project slug separately so onboarding.tsx can find
126-
// the project via useRecentCreatedProject without corrupting
127-
// selectedPlatform.key (which the platform features step needs).
128147
setCreatedProjectSlug(project.slug);
129-
setProjectDetailsForm({
130-
projectName: projectNameResolved,
131-
teamSlug: teamSlugResolved,
132-
alertRuleConfig,
133-
platform: selectedPlatform.key,
134-
});
148+
persistFormState();
135149

136150
trackAnalytics('onboarding.scm_project_details_create_succeeded', {
137151
organization,
@@ -143,6 +157,54 @@ export function ScmProjectDetails({onComplete}: StepProps) {
143157
trackAnalytics('onboarding.scm_project_details_create_failed', {organization});
144158
addErrorMessage(t('Failed to create project'));
145159
Sentry.captureException(error);
160+
} finally {
161+
setIsSaving(false);
162+
}
163+
}
164+
165+
async function updateExistingProject(slug: string) {
166+
setIsSaving(true);
167+
try {
168+
const updated = await update(api, {
169+
orgId: organization.slug,
170+
projectId: slug,
171+
data: {name: projectNameResolved},
172+
});
173+
174+
setCreatedProjectSlug(updated.slug);
175+
persistFormState();
176+
onComplete(undefined, selectedFeatures ? {product: selectedFeatures} : undefined);
177+
} catch (error) {
178+
addErrorMessage(t('Failed to update project'));
179+
Sentry.captureException(error);
180+
} finally {
181+
setIsSaving(false);
182+
}
183+
}
184+
185+
function handleCreateProject() {
186+
if (!selectedPlatform || !canSubmit) {
187+
return;
188+
}
189+
190+
trackAnalytics('onboarding.scm_project_details_create_clicked', {organization});
191+
192+
if (projectExists && createdProjectSlug) {
193+
const platformChanged = projectDetailsForm?.platform !== selectedPlatform.key;
194+
195+
if (platformChanged) {
196+
// Platform changed — abandon the old project and create a new one,
197+
// matching legacy onboarding behavior.
198+
createNewProject(selectedPlatform);
199+
} else if (projectName === null && teamSlug === null) {
200+
// Nothing changed — reuse the existing project as-is.
201+
onComplete(undefined, selectedFeatures ? {product: selectedFeatures} : undefined);
202+
} else {
203+
// Same platform but name or team changed — update in place.
204+
updateExistingProject(createdProjectSlug);
205+
}
206+
} else {
207+
createNewProject(selectedPlatform);
146208
}
147209
}
148210

@@ -218,7 +280,7 @@ export function ScmProjectDetails({onComplete}: StepProps) {
218280
priority="primary"
219281
onClick={handleCreateProject}
220282
disabled={!canSubmit}
221-
busy={createProjectAndRules.isPending}
283+
busy={isSaving}
222284
icon={<IconProject />}
223285
>
224286
{t('Create project')}

0 commit comments

Comments
 (0)