@@ -7,15 +7,28 @@ import {Container, Flex, Stack} from '@sentry/scraps/layout';
77import { Text } from '@sentry/scraps/text' ;
88
99import { addErrorMessage } from 'sentry/actionCreators/indicator' ;
10+ import { update } from 'sentry/actionCreators/projects' ;
1011import { useOnboardingContext } from 'sentry/components/onboarding/onboardingContext' ;
1112import { useCreateProjectAndRules } from 'sentry/components/onboarding/useCreateProjectAndRules' ;
13+ import { useCreateProjectRules } from 'sentry/components/onboarding/useCreateProjectRules' ;
1214import { TeamSelector } from 'sentry/components/teamSelector' ;
1315import { IconGroup , IconProject , IconSiren } from 'sentry/icons' ;
1416import { t } from 'sentry/locale' ;
17+ import type { IssueAlertRule } from 'sentry/types/alerts' ;
18+ import type { OnboardingSelectedSDK } from 'sentry/types/onboarding' ;
1519import type { Team } from 'sentry/types/organization' ;
1620import { trackAnalytics } from 'sentry/utils/analytics' ;
21+ import { getApiUrl } from 'sentry/utils/api/getApiUrl' ;
22+ import {
23+ type ApiQueryKey ,
24+ fetchDataQuery ,
25+ fetchMutation ,
26+ useQueryClient ,
27+ } from 'sentry/utils/queryClient' ;
1728import { slugify } from 'sentry/utils/slugify' ;
29+ import { useApi } from 'sentry/utils/useApi' ;
1830import { useOrganization } from 'sentry/utils/useOrganization' ;
31+ import { useProjects } from 'sentry/utils/useProjects' ;
1932import { useTeams } from 'sentry/utils/useTeams' ;
2033import {
2134 DEFAULT_ISSUE_ALERT_OPTIONS_VALUES ,
@@ -32,16 +45,21 @@ import type {StepProps} from './types';
3245const PROJECT_DETAILS_WIDTH = '285px' ;
3346
3447export function ScmProjectDetails ( { onComplete} : StepProps ) {
48+ const api = useApi ( ) ;
49+ const queryClient = useQueryClient ( ) ;
3550 const organization = useOrganization ( ) ;
3651 const {
3752 selectedPlatform,
3853 selectedFeatures,
54+ createdProjectSlug,
3955 setCreatedProjectSlug,
4056 projectDetailsForm,
4157 setProjectDetailsForm,
4258 } = useOnboardingContext ( ) ;
43- const { teams} = useTeams ( ) ;
59+ const { teams, fetching : isLoadingTeams } = useTeams ( ) ;
60+ const { projects, initiallyLoaded : projectsLoaded } = useProjects ( ) ;
4461 const createProjectAndRules = useCreateProjectAndRules ( ) ;
62+ const createProjectRules = useCreateProjectRules ( ) ;
4563 useEffect ( ( ) => {
4664 trackAnalytics ( 'onboarding.scm_project_details_step_viewed' , { organization} ) ;
4765 } , [ organization ] ) ;
@@ -58,6 +76,12 @@ export function ScmProjectDetails({onComplete}: StepProps) {
5876 projectDetailsForm ?. teamSlug ?? null
5977 ) ;
6078
79+ const [ isSaving , setIsSaving ] = useState ( false ) ;
80+
81+ // Safety-net: verify the previously created project still exists in the store.
82+ const projectExists =
83+ ! ! createdProjectSlug && projects . some ( p => p . slug === createdProjectSlug ) ;
84+
6185 const projectNameResolved = projectName ?? defaultName ;
6286 const teamSlugResolved = teamSlug ?? firstAdminTeam ?. slug ?? '' ;
6387
@@ -104,34 +128,35 @@ export function ScmProjectDetails({onComplete}: StepProps) {
104128 projectNameResolved . length > 0 &&
105129 teamSlugResolved . length > 0 &&
106130 ! ! selectedPlatform &&
107- ! createProjectAndRules . isPending ;
131+ ! isSaving &&
132+ ! isLoadingTeams &&
133+ projectsLoaded ;
108134
109- async function handleCreateProject ( ) {
110- if ( ! selectedPlatform || ! canSubmit ) {
135+ function persistFormState ( ) {
136+ if ( ! selectedPlatform ) {
111137 return ;
112138 }
139+ setProjectDetailsForm ( {
140+ projectName : projectNameResolved ,
141+ teamSlug : teamSlugResolved ,
142+ alertRuleConfig,
143+ platform : selectedPlatform . key ,
144+ } ) ;
145+ }
113146
114- trackAnalytics ( 'onboarding.scm_project_details_create_clicked' , { organization } ) ;
115-
147+ async function createNewProject ( platform : OnboardingSelectedSDK ) {
148+ setIsSaving ( true ) ;
116149 try {
117150 const { project} = await createProjectAndRules . mutateAsync ( {
118151 projectName : projectNameResolved ,
119- platform : selectedPlatform ,
152+ platform,
120153 team : teamSlugResolved ,
121154 alertRuleConfig : getRequestDataFragment ( alertRuleConfig ) ,
122155 createNotificationAction : ( ) => undefined ,
123156 } ) ;
124157
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).
128158 setCreatedProjectSlug ( project . slug ) ;
129- setProjectDetailsForm ( {
130- projectName : projectNameResolved ,
131- teamSlug : teamSlugResolved ,
132- alertRuleConfig,
133- platform : selectedPlatform . key ,
134- } ) ;
159+ persistFormState ( ) ;
135160
136161 trackAnalytics ( 'onboarding.scm_project_details_create_succeeded' , {
137162 organization,
@@ -143,6 +168,101 @@ export function ScmProjectDetails({onComplete}: StepProps) {
143168 trackAnalytics ( 'onboarding.scm_project_details_create_failed' , { organization} ) ;
144169 addErrorMessage ( t ( 'Failed to create project' ) ) ;
145170 Sentry . captureException ( error ) ;
171+ } finally {
172+ setIsSaving ( false ) ;
173+ }
174+ }
175+
176+ async function updateExistingProject ( slug : string ) {
177+ setIsSaving ( true ) ;
178+ try {
179+ const updated = await update ( api , {
180+ orgId : organization . slug ,
181+ projectId : slug ,
182+ data : { name : projectNameResolved } ,
183+ } ) ;
184+
185+ // If the alert config changed, replace the existing rule.
186+ // Onboarding only ever creates a single alert rule.
187+ if ( ! alertConfigUnchanged ) {
188+ const rulesQueryKey : ApiQueryKey = [
189+ getApiUrl ( '/projects/$organizationIdOrSlug/$projectIdOrSlug/rules/' , {
190+ path : {
191+ organizationIdOrSlug : organization . slug ,
192+ projectIdOrSlug : updated . slug ,
193+ } ,
194+ } ) ,
195+ ] ;
196+ const [ [ existingRule ] ] = await queryClient . fetchQuery ( {
197+ queryKey : rulesQueryKey ,
198+ queryFn : fetchDataQuery < IssueAlertRule [ ] > ,
199+ staleTime : 0 ,
200+ } ) ;
201+ if ( existingRule ) {
202+ await fetchMutation ( {
203+ method : 'DELETE' ,
204+ url : `/projects/${ organization . slug } /${ updated . slug } /rules/${ existingRule . id } /` ,
205+ } ) ;
206+ }
207+
208+ const fragment = getRequestDataFragment ( alertRuleConfig ) ;
209+ if ( fragment . shouldCreateCustomRule ) {
210+ await createProjectRules . mutateAsync ( {
211+ projectSlug : updated . slug ,
212+ name : updated . name ,
213+ conditions : fragment . conditions ,
214+ actions : fragment . actions ,
215+ actionMatch : fragment . actionMatch ,
216+ frequency : fragment . frequency ,
217+ } ) ;
218+ }
219+ }
220+
221+ setCreatedProjectSlug ( updated . slug ) ;
222+ persistFormState ( ) ;
223+ onComplete ( undefined , selectedFeatures ? { product : selectedFeatures } : undefined ) ;
224+ } catch ( error ) {
225+ addErrorMessage ( t ( 'Failed to update project' ) ) ;
226+ Sentry . captureException ( error ) ;
227+ } finally {
228+ setIsSaving ( false ) ;
229+ }
230+ }
231+
232+ const savedAlert = projectDetailsForm ?. alertRuleConfig ;
233+ const alertConfigUnchanged =
234+ alertRuleConfig . alertSetting === savedAlert ?. alertSetting &&
235+ alertRuleConfig . interval === savedAlert ?. interval &&
236+ alertRuleConfig . metric === savedAlert ?. metric &&
237+ alertRuleConfig . threshold === savedAlert ?. threshold ;
238+
239+ function handleCreateProject ( ) {
240+ if ( ! selectedPlatform || ! canSubmit ) {
241+ return ;
242+ }
243+
244+ trackAnalytics ( 'onboarding.scm_project_details_create_clicked' , { organization} ) ;
245+
246+ if ( projectExists && createdProjectSlug ) {
247+ const platformChanged = projectDetailsForm ?. platform !== selectedPlatform . key ;
248+
249+ if ( platformChanged ) {
250+ // Platform changed — abandon the old project and create a new one,
251+ // matching legacy onboarding behavior.
252+ createNewProject ( selectedPlatform ) ;
253+ } else if (
254+ projectNameResolved === projectDetailsForm ?. projectName &&
255+ teamSlugResolved === projectDetailsForm ?. teamSlug &&
256+ alertConfigUnchanged
257+ ) {
258+ // Nothing changed — reuse the existing project as-is.
259+ onComplete ( undefined , selectedFeatures ? { product : selectedFeatures } : undefined ) ;
260+ } else {
261+ // Same platform but settings changed — update in place.
262+ updateExistingProject ( createdProjectSlug ) ;
263+ }
264+ } else {
265+ createNewProject ( selectedPlatform ) ;
146266 }
147267 }
148268
@@ -218,7 +338,7 @@ export function ScmProjectDetails({onComplete}: StepProps) {
218338 priority = "primary"
219339 onClick = { handleCreateProject }
220340 disabled = { ! canSubmit }
221- busy = { createProjectAndRules . isPending }
341+ busy = { isSaving }
222342 icon = { < IconProject /> }
223343 >
224344 { t ( 'Create project' ) }
0 commit comments