From 37ff05c202196c85f47692c2e75814483e6e784b Mon Sep 17 00:00:00 2001 From: Jay Goss Date: Fri, 3 Apr 2026 12:00:59 -0500 Subject: [PATCH 1/4] feat(onboarding): Gate SCM onboarding flow with useExperiment hook Replace the feature flag check with the useExperiment hook to gate the SCM onboarding flow behind the onboarding-scm-experiment experiment instead of the onboarding-scm feature flag. --- static/app/views/onboarding/onboarding.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/app/views/onboarding/onboarding.tsx b/static/app/views/onboarding/onboarding.tsx index b92c9bb1ce7f1a..6b7b3951f2ec24 100644 --- a/static/app/views/onboarding/onboarding.tsx +++ b/static/app/views/onboarding/onboarding.tsx @@ -24,6 +24,7 @@ import type {PlatformKey} from 'sentry/types/project'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; +import {useExperiment} from 'sentry/utils/useExperiment'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -163,7 +164,10 @@ export function OnboardingWithoutContext() { onboardingContext.createdProjectSlug ?? onboardingContext.selectedPlatform?.key; const hasNewWelcomeUI = useHasNewWelcomeUI(); - const hasScmOnboarding = organization.features.includes('onboarding-scm'); + const {inExperiment: hasScmOnboarding} = useExperiment({ + feature: 'onboarding-scm-experiment', + }); + const onboardingSteps = hasScmOnboarding ? scmOnboardingSteps : legacyOnboardingSteps; const stepObj = onboardingSteps.find(({id}) => stepId === id); From 250140c9ea3070d81e276834817d086561a861a8 Mon Sep 17 00:00:00 2001 From: Jay Goss Date: Fri, 3 Apr 2026 12:17:45 -0500 Subject: [PATCH 2/4] test(onboarding): Update SCM tests to use experiment fixture The SCM onboarding flow is now gated by useExperiment instead of a feature flag. Update the test to use experiments on the org fixture and register a minimal useExperiment hook in HookStore, since gsApp's registerHooks() doesn't run in the test environment. --- .../app/views/onboarding/onboarding.spec.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/static/app/views/onboarding/onboarding.spec.tsx b/static/app/views/onboarding/onboarding.spec.tsx index 931b526d6b01b1..670eb083ad760b 100644 --- a/static/app/views/onboarding/onboarding.spec.tsx +++ b/static/app/views/onboarding/onboarding.spec.tsx @@ -22,11 +22,14 @@ import { useOnboardingContext, } from 'sentry/components/onboarding/onboardingContext'; import * as useRecentCreatedProjectHook from 'sentry/components/onboarding/useRecentCreatedProject'; +import {HookStore} from 'sentry/stores/hookStore'; import {OnboardingDrawerStore} from 'sentry/stores/onboardingDrawerStore'; import {ProjectsStore} from 'sentry/stores/projectsStore'; import {TeamStore} from 'sentry/stores/teamStore'; import type {PlatformKey} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; +import type {UseExperimentOptions} from 'sentry/utils/useExperiment'; +import {useOrganization} from 'sentry/utils/useOrganization'; import {OnboardingWithoutContext} from 'sentry/views/onboarding/onboarding'; jest.mock('sentry/utils/analytics'); @@ -630,7 +633,7 @@ describe('Onboarding', () => { describe('SCM onboarding flow', () => { const scmOrganization = OrganizationFixture({ - features: ['onboarding-scm'], + experiments: {'onboarding-scm-experiment': 'active'}, }); const githubProvider = GitHubIntegrationProviderFixture({ @@ -646,7 +649,19 @@ describe('Onboarding', () => { link: 'https://docs.sentry.io/platforms/javascript/guides/nextjs/', }; + // In production, gsApp's registerHooks() registers the real useExperiment + // hook into HookStore. That doesn't run in tests, so useExperiment falls + // back to a noop that always returns inExperiment: false. We register a + // minimal implementation here (can't import the real one due to boundary + // lint rules). + function useTestExperiment(options: UseExperimentOptions) { + const organization = useOrganization(); + const assignment = organization.experiments?.[options.feature] ?? 'control'; + return {inExperiment: assignment === 'active', experimentAssignment: assignment}; + } + beforeEach(() => { + HookStore.add('react-hook:use-experiment', useTestExperiment); MockApiClient.addMockResponse({ url: `/organizations/${scmOrganization.slug}/config/integrations/`, body: {providers: [githubProvider]}, @@ -661,6 +676,10 @@ describe('Onboarding', () => { }); }); + afterEach(() => { + HookStore.remove('react-hook:use-experiment', useTestExperiment); + }); + function renderOnboarding( step: string, options?: { From b247c416699e081557302ede8d300a8c9922477c Mon Sep 17 00:00:00 2001 From: Jay Goss Date: Tue, 7 Apr 2026 14:42:57 -0500 Subject: [PATCH 3/4] ref(onboarding): Simplify SCM tests after useExperiment features gate Now that useExperiment gates on organization.features (d389e46), the noop fallback works out of the box in tests. Remove the custom HookStore-based useTestExperiment shim and switch the fixture back to features instead of experiments. --- .../app/views/onboarding/onboarding.spec.tsx | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/static/app/views/onboarding/onboarding.spec.tsx b/static/app/views/onboarding/onboarding.spec.tsx index 670eb083ad760b..c455e569c2f20f 100644 --- a/static/app/views/onboarding/onboarding.spec.tsx +++ b/static/app/views/onboarding/onboarding.spec.tsx @@ -22,14 +22,11 @@ import { useOnboardingContext, } from 'sentry/components/onboarding/onboardingContext'; import * as useRecentCreatedProjectHook from 'sentry/components/onboarding/useRecentCreatedProject'; -import {HookStore} from 'sentry/stores/hookStore'; import {OnboardingDrawerStore} from 'sentry/stores/onboardingDrawerStore'; import {ProjectsStore} from 'sentry/stores/projectsStore'; import {TeamStore} from 'sentry/stores/teamStore'; import type {PlatformKey} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; -import type {UseExperimentOptions} from 'sentry/utils/useExperiment'; -import {useOrganization} from 'sentry/utils/useOrganization'; import {OnboardingWithoutContext} from 'sentry/views/onboarding/onboarding'; jest.mock('sentry/utils/analytics'); @@ -633,7 +630,7 @@ describe('Onboarding', () => { describe('SCM onboarding flow', () => { const scmOrganization = OrganizationFixture({ - experiments: {'onboarding-scm-experiment': 'active'}, + features: ['onboarding-scm-experiment'], }); const githubProvider = GitHubIntegrationProviderFixture({ @@ -649,19 +646,7 @@ describe('Onboarding', () => { link: 'https://docs.sentry.io/platforms/javascript/guides/nextjs/', }; - // In production, gsApp's registerHooks() registers the real useExperiment - // hook into HookStore. That doesn't run in tests, so useExperiment falls - // back to a noop that always returns inExperiment: false. We register a - // minimal implementation here (can't import the real one due to boundary - // lint rules). - function useTestExperiment(options: UseExperimentOptions) { - const organization = useOrganization(); - const assignment = organization.experiments?.[options.feature] ?? 'control'; - return {inExperiment: assignment === 'active', experimentAssignment: assignment}; - } - beforeEach(() => { - HookStore.add('react-hook:use-experiment', useTestExperiment); MockApiClient.addMockResponse({ url: `/organizations/${scmOrganization.slug}/config/integrations/`, body: {providers: [githubProvider]}, @@ -676,10 +661,6 @@ describe('Onboarding', () => { }); }); - afterEach(() => { - HookStore.remove('react-hook:use-experiment', useTestExperiment); - }); - function renderOnboarding( step: string, options?: { From 646b4249912eaa48037895941ada2c3550c7ad37 Mon Sep 17 00:00:00 2001 From: Jay Goss Date: Tue, 7 Apr 2026 14:52:41 -0500 Subject: [PATCH 4/4] ref(onboarding): Update acceptance test feature flag to match experiment Rename organizations:onboarding-scm to organizations:onboarding-scm-experiment to match the useExperiment hook now used in the onboarding component. --- tests/acceptance/test_scm_onboarding.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/acceptance/test_scm_onboarding.py b/tests/acceptance/test_scm_onboarding.py index 0f872712336f2d..027494091b185f 100644 --- a/tests/acceptance/test_scm_onboarding.py +++ b/tests/acceptance/test_scm_onboarding.py @@ -68,7 +68,7 @@ def test_scm_onboarding_happy_path(self) -> None: with ( self.feature( { - "organizations:onboarding-scm": True, + "organizations:onboarding-scm-experiment": True, "organizations:integrations-github-platform-detection": True, } ), @@ -122,7 +122,7 @@ def test_scm_onboarding_happy_path(self) -> None: def test_scm_onboarding_skip_integration(self) -> None: """Skip flow: welcome → skip connect → manual platform → create project.""" - with self.feature({"organizations:onboarding-scm": True}): + with self.feature({"organizations:onboarding-scm-experiment": True}): self.start_onboarding() # SCM Connect: skip @@ -178,7 +178,7 @@ def test_scm_onboarding_with_integration_install(self) -> None: with ( self.feature( { - "organizations:onboarding-scm": True, + "organizations:onboarding-scm-experiment": True, "organizations:integrations-github-platform-detection": True, } ), @@ -279,7 +279,7 @@ def test_scm_onboarding_detection_error_falls_back_to_manual_picker(self) -> Non with ( self.feature( { - "organizations:onboarding-scm": True, + "organizations:onboarding-scm-experiment": True, "organizations:integrations-github-platform-detection": True, } ), @@ -339,7 +339,7 @@ def test_scm_onboarding_repo_search_no_results(self) -> None: self.create_github_integration() with ( - self.feature({"organizations:onboarding-scm": True}), + self.feature({"organizations:onboarding-scm-experiment": True}), mock.patch( "sentry.integrations.github.integration.GitHubIntegration.get_repositories", return_value=[],