Skip to content

Commit e5dad70

Browse files
committed
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
1 parent 6579666 commit e5dad70

File tree

6 files changed

+129
-11
lines changed

6 files changed

+129
-11
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ describe('Onboarding', () => {
630630

631631
describe('SCM onboarding flow', () => {
632632
const scmOrganization = OrganizationFixture({
633-
features: ['onboarding-scm-experiment'],
633+
features: ['onboarding-scm-experiment', 'onboarding-scm-project-details'],
634634
});
635635

636636
const githubProvider = GitHubIntegrationProviderFixture({

static/app/views/onboarding/onboarding.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,15 @@ export function OnboardingWithoutContext() {
170170
feature: 'onboarding-scm-experiment',
171171
});
172172

173-
const onboardingSteps = hasScmOnboarding ? scmOnboardingSteps : legacyOnboardingSteps;
173+
const hasProjectDetailsStep = organization.features.includes(
174+
'onboarding-scm-project-details'
175+
);
176+
177+
const scmSteps = hasProjectDetailsStep
178+
? scmOnboardingSteps
179+
: scmOnboardingSteps.filter(s => s.id !== OnboardingStepId.SCM_PROJECT_DETAILS);
180+
181+
const onboardingSteps = hasScmOnboarding ? scmSteps : legacyOnboardingSteps;
174182

175183
const stepObj = onboardingSteps.find(({id}) => stepId === id);
176184
const stepIndex = onboardingSteps.findIndex(({id}) => stepId === id);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
OnboardingContextProvider,
1111
type OnboardingSessionState,
1212
} from 'sentry/components/onboarding/onboardingContext';
13+
import {ProjectsStore} from 'sentry/stores/projectsStore';
14+
import {TeamStore} from 'sentry/stores/teamStore';
1315
import * as analytics from 'sentry/utils/analytics';
1416
import {sessionStorageWrapper} from 'sentry/utils/sessionStorage';
1517

@@ -72,6 +74,8 @@ describe('ScmPlatformFeatures', () => {
7274
beforeEach(() => {
7375
jest.clearAllMocks();
7476
sessionStorageWrapper.clear();
77+
ProjectsStore.loadInitialData([]);
78+
TeamStore.loadInitialData([]);
7579
});
7680

7781
afterEach(() => {

static/app/views/onboarding/scmPlatformFeatures.tsx

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {useCallback, useEffect, useMemo, useState} from 'react';
2+
import * as Sentry from '@sentry/react';
23
import {LayoutGroup, motion} from 'framer-motion';
34
import {PlatformIcon} from 'platformicons';
45

@@ -7,6 +8,7 @@ import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout';
78
import {Select} from '@sentry/scraps/select';
89
import {Heading} from '@sentry/scraps/text';
910

11+
import {addErrorMessage} from 'sentry/actionCreators/indicator';
1012
import {closeModal, openConsoleModal, openModal} from 'sentry/actionCreators/modal';
1113
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
1214
import {SupportedLanguages} from 'sentry/components/onboarding/frameworkSuggestionModal';
@@ -16,13 +18,17 @@ import {
1618
getDisabledProducts,
1719
platformProductAvailability,
1820
} from 'sentry/components/onboarding/productSelection';
21+
import {useCreateProject} from 'sentry/components/onboarding/useCreateProject';
1922
import {platforms} from 'sentry/data/platforms';
2023
import {t} from 'sentry/locale';
2124
import type {OnboardingSelectedSDK} from 'sentry/types/onboarding';
25+
import type {Team} from 'sentry/types/organization';
2226
import type {PlatformIntegration, PlatformKey} from 'sentry/types/project';
2327
import {trackAnalytics} from 'sentry/utils/analytics';
2428
import {isDisabledGamingPlatform} from 'sentry/utils/platform';
2529
import {useOrganization} from 'sentry/utils/useOrganization';
30+
import {useProjects} from 'sentry/utils/useProjects';
31+
import {useTeams} from 'sentry/utils/useTeams';
2632
import {ScmFeatureSelectionCards} from 'sentry/views/onboarding/components/scmFeatureSelectionCards';
2733
import {ScmPlatformCard} from 'sentry/views/onboarding/components/scmPlatformCard';
2834

@@ -84,8 +90,17 @@ export function ScmPlatformFeatures({onComplete}: StepProps) {
8490
setSelectedPlatform,
8591
selectedFeatures,
8692
setSelectedFeatures,
93+
createdProjectSlug,
94+
setCreatedProjectSlug,
8795
} = useOnboardingContext();
8896

97+
const {teams, fetching: isLoadingTeams} = useTeams();
98+
const {projects, initiallyLoaded: projectsLoaded} = useProjects();
99+
const createProject = useCreateProject();
100+
const hasProjectDetailsStep = organization.features.includes(
101+
'onboarding-scm-project-details'
102+
);
103+
89104
const [showManualPicker, setShowManualPicker] = useState(false);
90105

91106
useEffect(() => {
@@ -306,14 +321,59 @@ export function ScmPlatformFeatures({onComplete}: StepProps) {
306321
}
307322
}
308323

309-
function handleContinue() {
324+
const existingProject = createdProjectSlug
325+
? projects.find(p => p.slug === createdProjectSlug)
326+
: undefined;
327+
328+
async function handleContinue() {
310329
// Persist derived defaults to context if user accepted them
311330
if (currentPlatformKey && !selectedPlatform?.key) {
312331
setPlatform(currentPlatformKey);
313332
}
314333
if (!selectedFeatures) {
315334
setSelectedFeatures(currentFeatures);
316335
}
336+
337+
if (!hasProjectDetailsStep) {
338+
// Auto-create project with defaults when SCM_PROJECT_DETAILS step is skipped
339+
const platform =
340+
selectedPlatform ??
341+
(currentPlatformKey
342+
? toSelectedSdk(getPlatformInfo(currentPlatformKey)!)
343+
: undefined);
344+
if (!platform) {
345+
return;
346+
}
347+
348+
// If a project was already created for this platform (e.g. the user
349+
// went back after the project received its first event), reuse it.
350+
// If the platform changed, abandon the old project and create a new
351+
// one — matching legacy onboarding behavior.
352+
if (existingProject?.platform === platform.key) {
353+
onComplete(undefined, {product: currentFeatures});
354+
return;
355+
}
356+
357+
const firstAdminTeam = teams.find((team: Team) =>
358+
team.access.includes('team:admin')
359+
);
360+
361+
try {
362+
const project = await createProject.mutateAsync({
363+
name: platform.key,
364+
platform,
365+
default_rules: true,
366+
firstTeamSlug: firstAdminTeam?.slug,
367+
});
368+
setCreatedProjectSlug(project.slug);
369+
onComplete(undefined, {product: currentFeatures});
370+
} catch (error) {
371+
addErrorMessage(t('Failed to create project'));
372+
Sentry.captureException(error);
373+
}
374+
return;
375+
}
376+
317377
onComplete();
318378
}
319379

@@ -459,7 +519,12 @@ export function ScmPlatformFeatures({onComplete}: StepProps) {
459519
features: currentFeatures,
460520
}}
461521
onClick={handleContinue}
462-
disabled={!currentPlatformKey}
522+
disabled={
523+
!currentPlatformKey ||
524+
createProject.isPending ||
525+
(!hasProjectDetailsStep && (isLoadingTeams || !projectsLoaded))
526+
}
527+
busy={createProject.isPending}
463528
>
464529
{t('Continue')}
465530
</Button>

static/app/views/onboarding/scmProjectDetails.tsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {addErrorMessage} from 'sentry/actionCreators/indicator';
1010
import {update} from 'sentry/actionCreators/projects';
1111
import {useOnboardingContext} from 'sentry/components/onboarding/onboardingContext';
1212
import {useCreateProjectAndRules} from 'sentry/components/onboarding/useCreateProjectAndRules';
13+
import {useCreateProjectRules} from 'sentry/components/onboarding/useCreateProjectRules';
1314
import {TeamSelector} from 'sentry/components/teamSelector';
1415
import {IconGroup, IconProject, IconSiren} from 'sentry/icons';
1516
import {t} from 'sentry/locale';
@@ -49,6 +50,7 @@ export function ScmProjectDetails({onComplete}: StepProps) {
4950
const {teams, fetching: isLoadingTeams} = useTeams();
5051
const {projects, initiallyLoaded: projectsLoaded} = useProjects();
5152
const createProjectAndRules = useCreateProjectAndRules();
53+
const createProjectRules = useCreateProjectRules();
5254
useEffect(() => {
5355
trackAnalytics('onboarding.scm_project_details_step_viewed', {organization});
5456
}, [organization]);
@@ -171,6 +173,32 @@ export function ScmProjectDetails({onComplete}: StepProps) {
171173
data: {name: projectNameResolved},
172174
});
173175

176+
// If the alert config changed, replace the existing rule.
177+
// Onboarding only ever creates a single alert rule.
178+
if (!alertConfigUnchanged) {
179+
const [existingRule] = await api.requestPromise(
180+
`/projects/${organization.slug}/${updated.slug}/rules/`
181+
);
182+
if (existingRule) {
183+
await api.requestPromise(
184+
`/projects/${organization.slug}/${updated.slug}/rules/${existingRule.id}/`,
185+
{method: 'DELETE'}
186+
);
187+
}
188+
189+
const fragment = getRequestDataFragment(alertRuleConfig);
190+
if (fragment.shouldCreateCustomRule) {
191+
await createProjectRules.mutateAsync({
192+
projectSlug: updated.slug,
193+
name: updated.name,
194+
conditions: fragment.conditions,
195+
actions: fragment.actions,
196+
actionMatch: fragment.actionMatch,
197+
frequency: fragment.frequency,
198+
});
199+
}
200+
}
201+
174202
setCreatedProjectSlug(updated.slug);
175203
persistFormState();
176204
onComplete(undefined, selectedFeatures ? {product: selectedFeatures} : undefined);
@@ -182,6 +210,13 @@ export function ScmProjectDetails({onComplete}: StepProps) {
182210
}
183211
}
184212

213+
const savedAlert = projectDetailsForm?.alertRuleConfig;
214+
const alertConfigUnchanged =
215+
alertRuleConfig.alertSetting === savedAlert?.alertSetting &&
216+
alertRuleConfig.interval === savedAlert?.interval &&
217+
alertRuleConfig.metric === savedAlert?.metric &&
218+
alertRuleConfig.threshold === savedAlert?.threshold;
219+
185220
function handleCreateProject() {
186221
if (!selectedPlatform || !canSubmit) {
187222
return;
@@ -197,13 +232,14 @@ export function ScmProjectDetails({onComplete}: StepProps) {
197232
// matching legacy onboarding behavior.
198233
createNewProject(selectedPlatform);
199234
} else if (
200-
projectNameResolved === (projectDetailsForm?.projectName ?? defaultName) &&
201-
teamSlugResolved === (projectDetailsForm?.teamSlug ?? firstAdminTeam?.slug ?? '')
235+
projectNameResolved === projectDetailsForm?.projectName &&
236+
teamSlugResolved === projectDetailsForm?.teamSlug &&
237+
alertConfigUnchanged
202238
) {
203239
// Nothing changed — reuse the existing project as-is.
204240
onComplete(undefined, selectedFeatures ? {product: selectedFeatures} : undefined);
205241
} else {
206-
// Same platform but name or team changed — update in place.
242+
// Same platform but settings changed — update in place.
207243
updateExistingProject(createdProjectSlug);
208244
}
209245
} else {

static/app/views/onboarding/useBackActions.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
1010
import type {RequestError} from 'sentry/utils/requestError/requestError';
1111
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
1212
import {useApi} from 'sentry/utils/useApi';
13+
import {useExperiment} from 'sentry/utils/useExperiment';
1314
import {useOrganization} from 'sentry/utils/useOrganization';
1415
import type {StepDescriptor} from 'sentry/views/onboarding/types';
1516

@@ -33,6 +34,9 @@ export function useBackActions({
3334
const api = useApi();
3435
const organization = useOrganization();
3536
const onboardingContext = useOnboardingContext();
37+
const {inExperiment: hasScmOnboarding} = useExperiment({
38+
feature: 'onboarding-scm-experiment',
39+
});
3640
const currentStep = onboardingSteps[stepIndex];
3741

3842
const deleteRecentCreatedProject = useCallback(
@@ -118,21 +122,22 @@ export function useBackActions({
118122
// store data and skip project creation.
119123
// In the SCM flow, preserve context so the user keeps their SCM
120124
// connection, repo selection, and feature choices.
121-
await deleteRecentCreatedProject(prevStep.id === 'scm-project-details');
125+
await deleteRecentCreatedProject(hasScmOnboarding);
122126
}
123127

124128
if (!browserBackButton) {
125129
goToStep(prevStep);
126130
}
127131
},
128132
[
129-
goToStep,
133+
currentStep,
130134
organization,
131-
onboardingContext,
132135
isRecentCreatedProjectActive,
133136
recentCreatedProject,
134-
currentStep,
137+
onboardingContext,
138+
goToStep,
135139
deleteRecentCreatedProject,
140+
hasScmOnboarding,
136141
]
137142
);
138143

0 commit comments

Comments
 (0)