From 17bf080cb9224301c52e616d4bd9d5d5bedfe9a8 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Wed, 11 Nov 2026 13:55:53 -0500 Subject: [PATCH 1/5] style(onboarding): Add layered agent setup treatment Co-Authored-By: Evan Purkhiser --- .../agenticProgress/agentSetupCard.tsx | 26 ++- .../agenticProgress/agenticProgressList.tsx | 25 ++- .../agenticProgress/agenticSetup.tsx | 201 +++++++++++------- .../agenticProgress/layeredContainer.tsx | 55 +++++ .../onboarding/components/newWelcome.tsx | 22 +- .../components/welcomeAgentSetup.tsx | 1 + .../app/views/onboarding/onboarding.spec.tsx | 4 +- 7 files changed, 233 insertions(+), 101 deletions(-) create mode 100644 static/app/components/onboarding/agenticProgress/layeredContainer.tsx diff --git a/static/app/components/onboarding/agenticProgress/agentSetupCard.tsx b/static/app/components/onboarding/agenticProgress/agentSetupCard.tsx index 3f4f0f0c18d5..561965b7c65b 100644 --- a/static/app/components/onboarding/agenticProgress/agentSetupCard.tsx +++ b/static/app/components/onboarding/agenticProgress/agentSetupCard.tsx @@ -18,6 +18,7 @@ interface AgentSetupCardProps { onSelectSnippet: (source: AgentSetupCopySource) => void; run: AgenticProgressRun | undefined; feedbackSource?: string; + unframed?: boolean; } export function AgentSetupCard({ @@ -25,29 +26,38 @@ export function AgentSetupCard({ onCopyCommand, onSelectSnippet, run, + unframed = false, }: AgentSetupCardProps) { return ( - + - + {t('Let your agent set up Sentry')} {t('Recommended')} - + {t('Paste one prompt and your agent does the rest.')} - {tct('[emphasis:Detects your stack]', {emphasis: })} - + + {tct('[emphasis:Detects your stack]', {emphasis: })} + + {tct('[emphasis:Installs and configures the SDKs] in your code', { emphasis: , })} - + {tct('[emphasis:Sends a test error] and confirms it, so you know it works', { emphasis: , })} @@ -67,11 +77,11 @@ export function AgentSetupCard({ ); } -function Benefit({children}: {children: ReactNode}) { +function Benefit({children, large}: {children: ReactNode; large: boolean}) { return ( - {children} + {children} ); } diff --git a/static/app/components/onboarding/agenticProgress/agenticProgressList.tsx b/static/app/components/onboarding/agenticProgress/agenticProgressList.tsx index 6d8dd2f91c0f..8f52353b835a 100644 --- a/static/app/components/onboarding/agenticProgress/agenticProgressList.tsx +++ b/static/app/components/onboarding/agenticProgress/agenticProgressList.tsx @@ -273,10 +273,12 @@ export function AgenticProgressList({ extraContentByStage, header, stages, + unframed = false, }: { stages: AgenticProgressStageState[]; extraContentByStage?: Partial>; header?: React.ReactNode; + unframed?: boolean; }) { const [hasEntered, setHasEntered] = useState(false); useEffect(() => { @@ -288,11 +290,11 @@ export function AgenticProgressList({ stage.stage === 'create_project'); const projectSlugs = createProjectStage?.extra?.projectSlugs ?? []; @@ -431,7 +441,7 @@ export function AgenticProgress({ animate={{opacity: 1}} exit={{opacity: 0}} > - + ) : ( } diff --git a/static/app/components/onboarding/agenticProgress/agenticSetup.tsx b/static/app/components/onboarding/agenticProgress/agenticSetup.tsx index 9a2f083e6fd8..d62dec04e918 100644 --- a/static/app/components/onboarding/agenticProgress/agenticSetup.tsx +++ b/static/app/components/onboarding/agenticProgress/agenticSetup.tsx @@ -17,6 +17,7 @@ import {t} from 'sentry/locale'; import {AgenticProgress} from './agenticProgressList'; import {AgentSetupCard, type AgentSetupCopySource} from './agentSetupCard'; +import {LayeredContainer} from './layeredContainer'; import type {AgenticProgressRun} from './types'; const MotionContainer = motion.create(Container); @@ -35,6 +36,7 @@ interface AgenticSetupProps { /** The host supplies the action for continuing with browser setup. */ onSetupInBrowser: () => void; run: AgenticProgressRun | undefined; + appearance?: 'default' | 'layered'; onboardingCode?: string; } @@ -51,13 +53,68 @@ export function AgenticSetup({ hasProgressFailed, onRefresh, run, + appearance = 'default', }: AgenticSetupProps) { const hasRunFailed = run?.runStatus === 'failed' || run?.runStatus === 'cancelled'; const showsProgress = Boolean(run) && (isAgentConnected || run?.runStatus === 'completed' || hasRunFailed); + const isLayered = appearance === 'layered'; + + const primaryContent = ( + + + {run && showsProgress ? ( + + + + ) : ( + + + + )} + + + ); return ( - + - - - {run && showsProgress ? ( - - - - ) : ( - + + - - - )} - - + {primaryContent} + + + + ) : ( + primaryContent + )} + )} + + ) : ( + + + {t('or')} + - - + - - - - - - {t('Set up manually')} - - - - - - - - {t( - 'Connect a repo, choose what to instrument and where alerts land, then follow the instructions for your project' - )} - - - - - + > + + + + + + {t('Set up manually')} + + + + + + + + {t( + 'Connect a repo, choose what to instrument and where alerts land, then follow the instructions for your project' + )} + + + + + + )} ); } + +const AccentText = styled('span')` + color: ${p => p.theme.tokens.content.accent}; +`; diff --git a/static/app/components/onboarding/agenticProgress/layeredContainer.tsx b/static/app/components/onboarding/agenticProgress/layeredContainer.tsx index b06b83b38195..0a3afca7d411 100644 --- a/static/app/components/onboarding/agenticProgress/layeredContainer.tsx +++ b/static/app/components/onboarding/agenticProgress/layeredContainer.tsx @@ -1,56 +1,52 @@ import type {ReactNode} from 'react'; +import {css, useTheme, type Theme} from '@emotion/react'; import styled from '@emotion/styled'; import {Container} from '@sentry/scraps/layout'; +// eslint-disable-next-line @sentry/scraps/no-token-import -- the brand stack uses named categorical colors the theme does not expose +import {color} from '@sentry/scraps/tokens'; export function LayeredContainer({children}: {children: ReactNode}) { + const theme = useTheme(); + return ( - - {frameProps => ( - - - {surfaceProps => ( - {children} - )} - - - )} + + {surfaceProps => {children}} ); } -const LayeredFrame = styled('div')` - isolation: isolate; - - &::before, - &::after { - content: ''; - position: absolute; - inset: 0; - border-radius: inherit; - } - - &::before { - z-index: 0; - transform: translateY(18px); - background: ${p => p.theme.colors.red400}; +function getLayerColors(theme: Theme) { + if (theme.type === 'dark') { + return { + accent: color.categorical.dark.blurple, + middle: color.categorical.dark.magenta, + bottom: color.categorical.dark.salmon, + }; } - &::after { - z-index: 1; - transform: translateY(12px); - background: ${p => p.theme.colors.pink600}; - } -`; + return { + accent: color.blue.light.opaque1100, + middle: color.categorical.light.magenta, + bottom: color.categorical.light.salmon, + }; +} const LayeredSurface = styled('div')` - z-index: 2; - box-shadow: 0 6px 0 ${p => p.theme.colors.blue600}; + ${p => { + const layers = getLayerColors(p.theme); + return css` + border: 1px solid ${layers.accent}; + box-shadow: + 0 2px 0 ${layers.accent}, + 0 6px 0 ${layers.middle}, + 0 10px 0 ${layers.bottom}; + `; + }} `; diff --git a/static/app/views/onboarding/components/newWelcome.tsx b/static/app/views/onboarding/components/newWelcome.tsx index efff768b0676..b047968a2a30 100644 --- a/static/app/views/onboarding/components/newWelcome.tsx +++ b/static/app/views/onboarding/components/newWelcome.tsx @@ -145,12 +145,15 @@ export function NewWelcomeUI(props: StepProps) { const showAgentLanding = showAgentSetup && !showAgentHeading; const scmHeading = showAgentHeading ? getAgentHeading({hasRunFailed, isSetupComplete}) - : { - title: showAgentSetup - ? t('Ship happens.') - : t("Code breaks.\nWe'll help you fix it faster"), - description: t('Monitor, debug, and fix your code, all in one place.'), - }; + : showAgentSetup + ? { + title: t('Code breaks. Fix it faster'), + description: t('Detect, diagnose, and debug, all in one place'), + } + : { + title: t("Code breaks.\nWe'll help you fix it faster"), + description: t('Monitor, debug, and fix your code, all in one place.'), + }; useWelcomeAnalyticsEffect({showAgentSetup}); @@ -179,7 +182,7 @@ export function NewWelcomeUI(props: StepProps) { @@ -190,12 +193,7 @@ export function NewWelcomeUI(props: StepProps) { paddingBottom={showAgentLanding ? '2xl' : 'xl'} > - + {scmHeading.title} diff --git a/static/app/views/onboarding/onboarding.spec.tsx b/static/app/views/onboarding/onboarding.spec.tsx index 1c2f173f02e4..cba59ee40e58 100644 --- a/static/app/views/onboarding/onboarding.spec.tsx +++ b/static/app/views/onboarding/onboarding.spec.tsx @@ -636,13 +636,15 @@ describe('Onboarding', () => { }); renderOnboarding('welcome'); - expect(screen.getByRole('heading', {name: 'Ship happens.'})).toBeInTheDocument(); + expect( + screen.getByRole('heading', {name: 'Code breaks. Fix it faster'}) + ).toBeInTheDocument(); act(resolveAgenticRunRequest); expect(await screen.findByRole('heading', {name: heading})).toBeInTheDocument(); expect( - screen.queryByRole('heading', {name: 'Ship happens.'}) + screen.queryByRole('heading', {name: 'Code breaks. Fix it faster'}) ).not.toBeInTheDocument(); expect(screen.queryByText('sntryh_****')).not.toBeInTheDocument(); } From 7822834259bf59a58496707b20df259a54e6983e Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 02:58:09 +0000 Subject: [PATCH 5/5] ref(onboarding): Share one agent setup card style across hosts Project creation now renders the same setup card, copy, spacing, and manual setup link as the onboarding landing. The layered appearance only adds the tilt and stacked drop shadow on top of the shared frame. Use the new welcome copy and spacing everywhere instead of branching. --- .../agenticProgress/agentSetupCard.tsx | 43 ++---- .../agenticProgress/agenticSetup.tsx | 126 ++++-------------- ...ayeredContainer.tsx => setupCardFrame.tsx} | 34 +++-- static/app/components/onboarding/consts.ts | 5 - .../onboarding/components/newWelcome.tsx | 26 +--- .../app/views/onboarding/onboarding.spec.tsx | 4 +- .../projectInstall/scmCreateProject.spec.tsx | 4 +- 7 files changed, 73 insertions(+), 169 deletions(-) rename static/app/components/onboarding/agenticProgress/{layeredContainer.tsx => setupCardFrame.tsx} (58%) diff --git a/static/app/components/onboarding/agenticProgress/agentSetupCard.tsx b/static/app/components/onboarding/agenticProgress/agentSetupCard.tsx index aad337ca8649..ace348d8f548 100644 --- a/static/app/components/onboarding/agenticProgress/agentSetupCard.tsx +++ b/static/app/components/onboarding/agenticProgress/agentSetupCard.tsx @@ -7,7 +7,7 @@ import {Heading, Text} from '@sentry/scraps/text'; import {AgentHandoffPrompt} from 'sentry/components/agentHandoff/agentHandoffPrompt'; import {AgentHandoffFlow} from 'sentry/components/agentHandoff/useAgentHandoff'; -import {t, tct} from 'sentry/locale'; +import {t} from 'sentry/locale'; import type {AgenticProgressRun} from './types'; @@ -18,7 +18,6 @@ interface AgentSetupCardProps { onSelectSnippet: (source: AgentSetupCopySource) => void; run: AgenticProgressRun | undefined; feedbackSource?: string; - unframed?: boolean; } export function AgentSetupCard({ @@ -26,49 +25,25 @@ export function AgentSetupCard({ onCopyCommand, onSelectSnippet, run, - unframed = false, }: AgentSetupCardProps) { - const emphasis = unframed ? : ; - return ( - - - + + + {t('Let your agent set up Sentry')} {t('Recommended')} - + {t('Paste one prompt and your agent does the rest')} - - - {tct('[emphasis:Detects your stack] and installs the Sentry CLI', {emphasis})} - - - {tct('[emphasis:Installs and configures] the right SDKs', { - emphasis, - })} - - - {tct('[emphasis:Sends a test error] to confirm it works', { - emphasis, - })} - + + {t('Detects your stack and installs the Sentry CLI')} + {t('Installs and configures the right SDKs')} + {t('Sends a test error to confirm it works')} diff --git a/static/app/components/onboarding/agenticProgress/agenticSetup.tsx b/static/app/components/onboarding/agenticProgress/agenticSetup.tsx index 6cb6da01445e..8ebd54b9f210 100644 --- a/static/app/components/onboarding/agenticProgress/agenticSetup.tsx +++ b/static/app/components/onboarding/agenticProgress/agenticSetup.tsx @@ -1,24 +1,18 @@ import styled from '@emotion/styled'; import {IconChevron} from '@sentry/icons/chevron'; -import {IconSliders} from '@sentry/icons/sliders'; import {AnimatePresence, motion} from 'framer-motion'; import {Alert} from '@sentry/scraps/alert'; import {Button} from '@sentry/scraps/button'; -import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout'; +import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; -import {ToggleCard} from '@sentry/scraps/toggleCard'; -import { - SETUP_CARD_ICON_SIZE, - SETUP_CARD_MARKER_PX, -} from 'sentry/components/onboarding/consts'; import {ScmCollapsibleReveal} from 'sentry/components/onboarding/scm/scmCollapsibleReveal'; import {t, tct} from 'sentry/locale'; import {AgenticProgress} from './agenticProgressList'; import {AgentSetupCard, type AgentSetupCopySource} from './agentSetupCard'; -import {LayeredContainer} from './layeredContainer'; +import {SetupCardFrame} from './setupCardFrame'; import type {AgenticProgressRun} from './types'; const MotionContainer = motion.create(Container); @@ -59,27 +53,9 @@ export function AgenticSetup({ const hasRunFailed = run?.runStatus === 'failed' || run?.runStatus === 'cancelled'; const showsProgress = Boolean(run) && (isAgentConnected || run?.runStatus === 'completed' || hasRunFailed); - const isLayered = appearance === 'layered'; - // Progress and failure views stay identical to the default appearance. - const isLayeredLanding = isLayered && !showsProgress; - - const setupCard = ( - - ); return ( - + - {isLayered ? ( - - - {setupCard} - - - ) : ( - setupCard - )} + + + )} @@ -164,72 +139,25 @@ export function AgenticSetup({ - {isLayeredLanding ? ( - - {buttonProps => ( - - )} - - ) : ( - - - {t('or')} - - - + {buttonProps => ( + + )} + ); diff --git a/static/app/components/onboarding/agenticProgress/layeredContainer.tsx b/static/app/components/onboarding/agenticProgress/setupCardFrame.tsx similarity index 58% rename from static/app/components/onboarding/agenticProgress/layeredContainer.tsx rename to static/app/components/onboarding/agenticProgress/setupCardFrame.tsx index 0a3afca7d411..c9ec022b3790 100644 --- a/static/app/components/onboarding/agenticProgress/layeredContainer.tsx +++ b/static/app/components/onboarding/agenticProgress/setupCardFrame.tsx @@ -6,18 +6,34 @@ import {Container} from '@sentry/scraps/layout'; // eslint-disable-next-line @sentry/scraps/no-token-import -- the brand stack uses named categorical colors the theme does not expose import {color} from '@sentry/scraps/tokens'; -export function LayeredContainer({children}: {children: ReactNode}) { +interface SetupCardFrameProps { + children: ReactNode; + /** Tilts the card and adds the stacked brand drop shadow. */ + layered: boolean; +} + +export function SetupCardFrame({children, layered}: SetupCardFrameProps) { const theme = useTheme(); return ( - - {surfaceProps => {children}} + + + {surfaceProps => + layered ? ( + {children} + ) : ( +
{children}
+ ) + } +
); } diff --git a/static/app/components/onboarding/consts.ts b/static/app/components/onboarding/consts.ts index ae19c21c1af2..959ac52c267a 100644 --- a/static/app/components/onboarding/consts.ts +++ b/static/app/components/onboarding/consts.ts @@ -1,10 +1,5 @@ -import type {IconSize} from '@sentry/scraps/theme'; - /** * Shared layout constants for SCM onboarding steps. * Matches the Figma main content area width. */ export const SCM_STEP_CONTENT_WIDTH = '520px'; - -export const SETUP_CARD_ICON_SIZE: IconSize = 'md'; -export const SETUP_CARD_MARKER_PX = '24px'; diff --git a/static/app/views/onboarding/components/newWelcome.tsx b/static/app/views/onboarding/components/newWelcome.tsx index b047968a2a30..b19c221df9f0 100644 --- a/static/app/views/onboarding/components/newWelcome.tsx +++ b/static/app/views/onboarding/components/newWelcome.tsx @@ -142,18 +142,12 @@ export function NewWelcomeUI(props: StepProps) { } = useOnboardingAgentSetupRun({enabled: showAgentSetup}); const showAgentHeading = showAgentSetup && (isAgentConnected || isSetupComplete || hasRunFailed); - const showAgentLanding = showAgentSetup && !showAgentHeading; const scmHeading = showAgentHeading ? getAgentHeading({hasRunFailed, isSetupComplete}) - : showAgentSetup - ? { - title: t('Code breaks. Fix it faster'), - description: t('Detect, diagnose, and debug, all in one place'), - } - : { - title: t("Code breaks.\nWe'll help you fix it faster"), - description: t('Monitor, debug, and fix your code, all in one place.'), - }; + : { + title: t('Code breaks. Fix it faster'), + description: t('Detect, diagnose, and debug, all in one place'), + }; useWelcomeAnalyticsEffect({showAgentSetup}); @@ -188,22 +182,14 @@ export function NewWelcomeUI(props: StepProps) { - + {scmHeading.title} - + {scmHeading.description} diff --git a/static/app/views/onboarding/onboarding.spec.tsx b/static/app/views/onboarding/onboarding.spec.tsx index cba59ee40e58..ab4044cc103c 100644 --- a/static/app/views/onboarding/onboarding.spec.tsx +++ b/static/app/views/onboarding/onboarding.spec.tsx @@ -547,7 +547,9 @@ describe('Onboarding', () => { expect( screen.getByRole('heading', {name: 'Let your agent set up Sentry'}) ).toBeInTheDocument(); - expect(screen.getByText('Detects your stack')).toBeInTheDocument(); + expect( + screen.getByText('Detects your stack and installs the Sentry CLI') + ).toBeInTheDocument(); expect(screen.getByRole('button', {name: 'Advanced options'})).toBeInTheDocument(); expect( screen.getByRole('link', {name: 'See the full instructions'}) diff --git a/static/app/views/projectInstall/scmCreateProject.spec.tsx b/static/app/views/projectInstall/scmCreateProject.spec.tsx index f0a962b7fcad..cbe596271ff8 100644 --- a/static/app/views/projectInstall/scmCreateProject.spec.tsx +++ b/static/app/views/projectInstall/scmCreateProject.spec.tsx @@ -1299,7 +1299,9 @@ describe('ScmCreateProject', () => { const session = request.mock.calls[0]?.[1]?.data; expect(session.clientRunId).not.toBe(onboardingSession.agenticProgressClientRunId); expect(session.onboardingCode).toBeUndefined(); - expect(screen.getByText('Detects your stack')).toBeInTheDocument(); + expect( + screen.getByText('Detects your stack and installs the Sentry CLI') + ).toBeInTheDocument(); expect(JSON.parse(sessionStorage.getItem('onboarding') ?? '{}')).toEqual( onboardingSession );