Skip to content

feat(onboarding): Implement SCM platform & features step#111160

Merged
jaydgoss merged 46 commits intomasterfrom
jaygoss/vdy-23-scm-platform-features-step-functional-implementation-with
Mar 24, 2026
Merged

feat(onboarding): Implement SCM platform & features step#111160
jaydgoss merged 46 commits intomasterfrom
jaygoss/vdy-23-scm-platform-features-step-functional-implementation-with

Conversation

@jaydgoss
Copy link
Copy Markdown
Member

@jaydgoss jaydgoss commented Mar 19, 2026

Implement the SCM_PLATFORM_FEATURES onboarding step -- the second step in the SCM-first onboarding flow where users select a platform and features after connecting their SCM.

What it does

Two variants depending on whether the user connected their SCM in the previous step:

SCM connected: Calls the platform detection API to identify platforms from the repository. Detected platforms are rendered as selectable cards with the first auto-selected. Falls through to manual selection if detection returns no results.

Skipped SCM: Shows a searchable dropdown for manually picking from 100+ platforms. Base languages (JavaScript, Python, etc.) trigger a framework suggestion modal.

Both variants share a feature selection card grid. Error Monitoring is always on. Available features are filtered by platformProductAvailability and gated by the user's plan via getDisabledProducts (disabled features show a tooltip with the reason). Profiling requires tracing -- enabling profiling auto-enables tracing, disabling tracing auto-removes profiling. Gaming/console platforms are blocked when not enabled for the org.

How state flows

Platform and feature selections use derived state locally (e.g. selectedPlatform?.key ?? detectedPlatformKey) and are persisted to OnboardingContext in the Continue handler. Platform detection is pre-warmed in SCM_CONNECT via React Query cache sharing.

New files

  • useScmPlatformDetection.ts -- hook wrapping the platform detection API
  • scmFeatureSelectionCards.tsx -- 2-column feature card grid with heading and count
  • scmFeatureCard.tsx -- individual feature card with icon, checkbox, and tooltip
  • scmPlatformCard.tsx -- detected platform card with platform icon and name
  • scmCardButton.tsx -- shared unstyled button primitive for card click semantics
  • detectedPlatform.ts (fixture) -- test fixture for detected platform data

Follow-up

VDY-30 -- sync feature selections from context into SetupDocs URL params.

Refs VDY-23

@linear-code
Copy link
Copy Markdown

linear-code bot commented Mar 19, 2026

@github-actions
Copy link
Copy Markdown
Contributor

🚨 Warning: This pull request contains Frontend and Backend changes!

It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently.

Have questions? Please ask in the #discuss-dev-infra channel.

jaydgoss added 16 commits March 24, 2026 10:59
Remove redundant id in gaming platform check (platformInfo already has
id). When clicking "Back to recommended", also select the first
detected platform so the view switches correctly when the current
selection is not in the detected list.

Refs VDY-23
…ng check

isDisabledGamingPlatform expects Platform which requires category.
PlatformIntegration doesn't have it.

Refs VDY-23
…m, 'id' | 'type'>

The function only uses id and type, so requiring the full Platform type
(including category) was unnecessarily restrictive. This avoids needing
to synthesize a category when calling with PlatformIntegration data.

Refs VDY-23
Inline isSelected variable in platform card map and convert to concise
arrow function. Add alwaysEnabled field to FeatureMeta so the
always-on behavior of error monitoring is data-driven instead of
hardcoded ProductSolution checks.
…ards

The button's text content already provides the accessible name via
the label and description inside it. Also simplifies the "renders
only passed features" test to check checkbox count per review feedback.
…e cards

Replace Flex wrapper with Container composition pattern to eliminate
an extra div around the icon. Container is the correct primitive
since only padding is applied, not flex layout.
Move card presentation logic out of the map bodies and into dedicated
components. ScmCardButton remains the shared unstyled button base,
while each card component owns its own layout and visual treatment.
…ling-tracing dependency

Previously the SCM onboarding step let users toggle features their
plan does not support and allowed enabling profiling without tracing.
Reuse getDisabledProducts from productSelection to gate features by
plan, and mirror the profiling-requires-tracing constraint from the
legacy ProductSelection component.
…llowed cursor

Show tooltip with reason on disabled feature cards: plan-gated
features display the restriction reason, error monitoring explains
it is always enabled. Change disabled cursor from default to
not-allowed to match ProductSelection behavior.
… no platforms

When SCM is connected but platform detection errors or returns no
resolvable platforms, the component showed an empty "Recommended SDK"
section. Now fall through to the manual picker automatically when
detection finishes with no results.
…state

Derive currentPlatformKey from selectedPlatform ?? detectedPlatformKey
instead of writing to context via useEffect. Persist the detected
platform to context in the onComplete handler when the user accepts
the default without making an explicit selection.
When the user accepts the default feature selection without toggling,
selectedFeatures in context remained undefined. Now persist the
derived currentFeatures to context alongside the platform default.
…ount

The selected count could overcount if context carried stale features
from a previously selected platform. Filter against availableFeatures
so only relevant selections are counted.
Extract toSelectedSdk to eliminate 3 duplicate PlatformIntegration to
OnboardingSelectedSDK conversions in setPlatform, openConsoleModal,
and the framework suggestion modal.
Platform cards act as a single-select group but lacked accessibility
attributes. Add role="radio" and aria-checked to ScmPlatformCard,
and role="radiogroup" to the parent container.
Inline getAvailableFeaturesForPlatform one-liner, use neutral 'all'
category in toSelectedSdk since PlatformIntegration lacks category,
add tests for framework suggestion modal and gaming platform blocking,
and use RepositoryFixture instead of manual mock object.

Refs VDY-23
@jaydgoss jaydgoss force-pushed the jaygoss/vdy-23-scm-platform-features-step-functional-implementation-with branch from 765c01c to fc7d5a5 Compare March 24, 2026 16:01
…re card icons

SVGIconProps is the base type for all icon components, making it the
correct shared type for the icon prop in feature cards. IconGraphProps
was unnecessarily specific since all icons accept SVGIconProps.
@jaydgoss jaydgoss enabled auto-merge (squash) March 24, 2026 16:13
@jaydgoss jaydgoss disabled auto-merge March 24, 2026 16:17
@jaydgoss jaydgoss merged commit 37e5d12 into master Mar 24, 2026
72 of 75 checks passed
@jaydgoss jaydgoss deleted the jaygoss/vdy-23-scm-platform-features-step-functional-implementation-with branch March 24, 2026 16:39
jaydgoss added a commit that referenced this pull request Mar 26, 2026
Implement the `SCM_PROJECT_DETAILS` onboarding step -- the third step in
the SCM-first onboarding flow where users configure and create a project
after selecting a platform and features.

**Project name:** Defaults from platform key, slugified and editable.

**Team selection:** Uses the existing `TeamSelector` component,
defaulting to the first team with admin access.

**Alert frequency:** Reuses the `IssueAlertOptions` component from the
project creation page with three radio options -- high priority issues
(default), custom threshold/metric/interval, and create alerts later.

**Project creation:** Uses `useCreateProjectAndRules` for atomic project
+ alert rule creation. After creation, stores the project slug in a
separate `createdProjectSlug` context field so `useRecentCreatedProject`
in SetupDocs can find it without corrupting `selectedPlatform.key`
(which the platform features step still needs).

## PR stack

This PR is stacked on
[#111160](#111160) (SCM platform
& features step).

- [PR 1: SCM platform & features
step](#111160)
- **PR 2 (this): SCM project details step**
- [PR 3: Sync feature selections to SetupDocs URL
params](#111334)

Refs VDY-27
jaydgoss added a commit that referenced this pull request Mar 26, 2026
…RL params (#111334)

When a user selects features in the SCM_PLATFORM_FEATURES step, those
choices are stored in OnboardingContext. The SetupDocs step reads
product selections from URL params (used by the inline ProductSelection
component). This PR passes feature selections as query params through
the `onComplete` -> `goNextStep` -> `navigate` chain so they arrive in
the URL when navigating to setup-docs. No mount-time sync effect needed.

The `onComplete` callback and `goNextStep` accept an optional `query`
parameter that is forwarded to `navigate()` via `normalizeUrl`.
scmProjectDetails passes `{product: selectedFeatures}` when calling
`onComplete`, so the URL already contains the correct product selections
when SetupDocs mounts. Legacy flow is unaffected since query is only
passed when provided.

## PR stack

- [PR 1: SCM platform & features
step](#111160)
- [PR 2: SCM project details
step](#111306)
- **PR 3 (this): Sync feature selections to SetupDocs**

Refs VDY-30
jaydgoss added a commit that referenced this pull request Mar 26, 2026
Add analytics instrumentation, UI polish, and state management
improvements for the SCM connect onboarding step.

Addresses [VDY-22](https://linear.app/getsentry/issue/VDY-22). The SCM
connect step was implemented in PR #110883 with analytics and UI polish
explicitly deferred.

**Stacked on [PR
#111160](#111160 (VDY-30:
sync feature selections from context).

## Analytics

All 6 events from the ticket are tracked:

| Event | Mechanism |
|-------|-----------|
| Step viewed | `trackAnalytics` on mount |
| Provider selected | Existing `integrations.installation_start` with
`view: 'onboarding'` |
| Integration connected | Existing `integrations.installation_complete`
with `view: 'onboarding'` |
| Repo selected | `trackAnalytics` in Select onChange |
| Skip clicked | Button `analyticsEventKey` prop |
| Continue clicked | Button `analyticsEventKey` prop |

## UI polish

- Step indicator ("Step 1 of 3" + Optional tag) with audited font
size/weight/spacing
- "Why connect your repository?" benefits card with 4 items (icons,
titles, descriptions)
- Connected state uses `<Tag variant="success">` with checkmark
- Benefits card visibility: shown with title in initial state, without
title when repo selected, hidden when searching
- "Skip for now" hidden when a repo is selected
- Repo search dropdown shows error message on API failure
- Provider pills filtered to design-specified 4 (GitHub, GitLab,
Bitbucket, Azure DevOps), ordered per Figma

## Repo selector overhaul

Replaced `CompactSelect` with `Select` from scraps to get a full-width
combo box input matching the Figma design. Added a custom
`SearchControl` component that prepends a search icon inside the select
input.

## Downstream state reset

When a user goes back to repo selection and picks a different repo,
`clearDerivedState()` on the onboarding context now resets
selectedPlatform, selectedFeatures, and createdProjectSlug without
wiping the entire session (which `setSelectedPlatform(undefined)` would
do).

## Shared components extracted

- `ScmStepHeader` -- step indicator + heading + subtitle (reusable
across SCM steps)
- `ScmStepContent` -- 506px max-width content wrapper
- `ScmStepFooter` -- right-aligned 506px footer with top padding

Refs VDY-22
Copy link
Copy Markdown
Collaborator

@TkDodo TkDodo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just leaving some low importance post-merge cleanup points if you get to it.

[currentPlatformKey]
);

const disabledProducts = useMemo(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as I can see, none of the useMemo or useCallbacks in this file are necessary. All we do at the end is pass those to another component, which isn’t memoized. None of the operations seem expensive either:

const disabledProducts = getDisabledProducts(organization);

is perfectly fine 🤷 . It would remove a bunch of cognitive load and about 30 loc.

Comment on lines +102 to +112
const resolvedPlatforms = useMemo(
() =>
detectedPlatforms.reduce<ResolvedPlatform[]>((acc, detected) => {
const info = getPlatformInfo(detected.platform);
if (info) {
acc.push({...detected, info});
}
return acc;
}, []),
[detectedPlatforms]
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reducing from an array into an array can usually be represented with a simpler alternative, in this case, a flatMap:

const resolvedPlatforms = detectedPlatforms.flatMap(detected => {
  const info = getPlatformInfo(detected.platform);
  return info ? [{...detected, info}] : [];
});

Comment on lines +21 to +36
const query = useQuery({
queryKey: [
getApiUrl(`/organizations/$organizationIdOrSlug/repos/$repoId/platforms/`, {
path: {
organizationIdOrSlug: organization.slug,
repoId: repoId!,
},
}),
{method: 'GET'},
] as const,
queryFn: async context => {
return fetchDataQuery<PlatformDetectionResponse>(context);
},
staleTime: 30_000,
enabled: !!repoId,
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JonasBa I think we need better skills for how to do this in new code. The recommended approach would be apiOptions with skipToken:

const query = useQuery(
  apiOptions.as<PlatformDetectionResponse>()(
    '/organizations/$organizationIdOrSlug/repos/$repoId/platforms/',
    {
      path: repoId
        ? {organizationIdOrSlug: organization.slug, repoId}
        : skipToken,
      staleTime: 30_000,
    }
  )
);

I just told claude “rewrite this to apiOptions” and it did a good job, so we have to nudge it in that direction per default.

The advantage of using skipToken also means we don’t need a no-null assertion when building the path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants