diff --git a/static/app/components/animatedSentryLogo.tsx b/static/app/components/animatedSentryLogo.tsx
new file mode 100644
index 00000000000000..afc089e5b5f11f
--- /dev/null
+++ b/static/app/components/animatedSentryLogo.tsx
@@ -0,0 +1,104 @@
+import {useId} from 'react';
+import {useTheme} from '@emotion/react';
+
+interface AnimatedSentryLogoProps {
+ /**
+ * Draw progress from 0 (no stroke) to 1 (fully drawn).
+ * Uses the stroke-dashoffset technique from the app loading screen.
+ */
+ progress: number;
+ className?: string;
+ size?: number;
+}
+
+const DASH_TOTAL = 1150;
+
+// Filled glyph (same path as index.ejs base + mask)
+const SENTRY_GLYPH =
+ 'M223.687 85.56a28.02 28.02 0 0 0-24-13.559 28.02 28.02 0 0 0-24 13.559l-39.48 67.62a193.26 193.26 0 0 1 106.5 159.96h-27.72a166.07 166.07 0 0 0-92.76-136.32L85.687 240a95.52 95.52 0 0 1 55.38 73.02h-63.66a4.55 4.55 0 0 1-3.659-2.325 4.56 4.56 0 0 1-.06-4.335l17.64-30a64.4 64.4 0 0 0-20.16-11.4l-17.46 30a27.24 27.24 0 0 0 2.023 30.435 27.2 27.2 0 0 0 8.116 7.005 27.96 27.96 0 0 0 13.56 3.6h87.18a116.41 116.41 0 0 0-48-103.86l13.86-24a143.22 143.22 0 0 1 61.8 127.86h73.86a215.28 215.28 0 0 0-98.46-190.8l28.02-48a4.62 4.62 0 0 1 6.3-1.62c3.18 1.74 121.74 208.62 123.96 211.02a4.562 4.562 0 0 1-4.08 6.78h-28.56q.54 11.46 0 22.86h28.68a27.54 27.54 0 0 0 27.72-27.66 26.94 26.94 0 0 0-3.72-13.68z';
+
+// Stroke paths that trace the logo outline (one group from index.ejs)
+const STROKE_PATHS = [
+ 'm82.8 253.5-12.7 22-12.5 21.2-.2.3v.1l-.2.2v.3a24 24 0 00.3 22.4l.3.6A24 24 0 0076.4 332h87l-3.2-21.8A115 115 0 00112 233l17.6-30.4a147 147 0 0166.8 112l1.3 17.5h65.6l-1.3-20.2a212 212 0 00-99.3-165.8l29.6-50.8a9 9 0 013.3-3.1 9 9 0 0112.1 3.1l122.2 209.2h.1a8 8 0 011 4v.2a8.5 8.5 0 01-8.6 8.5h-38.9',
+ 'M92.5 268.7 88 276.6 71.4 305v.1a8 8 0 000 7.2l.2.4a8 8 0 006 3.4h67.3l-.6-3.4A99 99 0 0097.2 242.5l-6.8-4 33.4-57.8 6.9 4a163 163 0 0181.6 128.8l.2 2.7h33.7L246 313A196 196 0 00147.8 155.7l-7-4L178.6 87v-.1a25 25 0 0133-9.1l.8.3a25 25 0 019 8.9h.1v.2L343.7 296.4a24 24 0 013.3 12v.4a24.5 24.5 0 01-24.6 24.4H291.5',
+ 'M85.7 264.5l-8.7 15-12.5 21.3-.1.2-.1.3A15.6 15.6 0 0076.9 324h77.2l-1.8-12.6a107 107 0 00-51-75.8l25.5-44.1A155 155 0 01204.3 314l.8 10.1h49.7l-.8-11.7A204 204 0 00151.8 148.9l33.6-57.7a17 17 0 0129.2 0L336.8 300.4a16 16 0 012.2 8.2 16.5 16.5 0 01-16.6 16.6H291.5',
+];
+
+/**
+ * Sentry brand mark with stroke-draw progress, adapted from the app
+ * loading animation in static/index.ejs. Single color, progress-driven
+ * instead of the looping multicolor original.
+ */
+export function AnimatedSentryLogo({
+ progress,
+ size = 72,
+ className,
+}: AnimatedSentryLogoProps) {
+ const theme = useTheme();
+ const id = useId();
+ const gooId = `goo-layer-${id}`;
+ const maskId = `mask-layer-${id}`;
+
+ const dashOffset = DASH_TOTAL * (1 - progress);
+
+ return (
+
+ );
+}
diff --git a/static/app/views/onboarding/onboarding.tsx b/static/app/views/onboarding/onboarding.tsx
index 6bb306970484ae..73f322b5d6aab7 100644
--- a/static/app/views/onboarding/onboarding.tsx
+++ b/static/app/views/onboarding/onboarding.tsx
@@ -3,9 +3,10 @@ import styled from '@emotion/styled';
import {AnimatePresence, motion} from 'framer-motion';
import {Button} from '@sentry/scraps/button';
-import {Stack} from '@sentry/scraps/layout';
+import {Container, Stack} from '@sentry/scraps/layout';
import {Link} from '@sentry/scraps/link';
+import {AnimatedSentryLogo} from 'sentry/components/animatedSentryLogo';
import Hook from 'sentry/components/hook';
import {LogoSentry} from 'sentry/components/logoSentry';
import {
@@ -101,6 +102,16 @@ const scmOnboardingSteps: StepDescriptor[] = [
},
];
+/**
+ * The SCM steps that display the animated logo progress indicator.
+ * Order determines the progress level (first = 0, last = 1).
+ */
+const SCM_LOGO_STEPS = [
+ OnboardingStepId.SCM_CONNECT,
+ OnboardingStepId.SCM_PLATFORM_FEATURES,
+ OnboardingStepId.SCM_PROJECT_DETAILS,
+];
+
function WelcomeVariable(props: StepProps) {
const hasNewWelcomeUI = useHasNewWelcomeUI();
@@ -117,7 +128,9 @@ interface ContainerVariableProps {
function ContainerVariable(props: PropsWithChildren) {
const newWelcomeUIStep = props.hasNewWelcomeUI && props.id === OnboardingStepId.WELCOME;
- const Component = newWelcomeUIStep ? ContainerNewWelcomeUI : Container;
+ const Component = newWelcomeUIStep
+ ? OnboardingContainerNewWelcomeUI
+ : OnboardingContainer;
return (
@@ -302,6 +315,12 @@ export function OnboardingWithoutContext() {
);
};
+ const scmLogoIndex = stepObj ? SCM_LOGO_STEPS.indexOf(stepObj.id) : -1;
+ const scmLogoProgress =
+ scmLogoIndex >= 0 && SCM_LOGO_STEPS.length > 1
+ ? scmLogoIndex / (SCM_LOGO_STEPS.length - 1)
+ : null;
+
// Redirect to the first step if we end up in an invalid state
const isInvalidDocsStep = stepId === 'setup-docs' && !projectSlug;
if (!stepObj || stepIndex === -1 || isInvalidDocsStep) {
@@ -372,6 +391,11 @@ export function OnboardingWithoutContext() {
)}
+ {scmLogoProgress !== null && (
+
+
+
+ )}
{stepObj.Component && (
@@ -402,7 +426,7 @@ function Onboarding() {
);
}
-const ContainerNewWelcomeUI = styled('div')<{hasFooter: boolean}>`
+const OnboardingContainerNewWelcomeUI = styled('div')<{hasFooter: boolean}>`
flex-grow: 1;
display: flex;
flex-direction: column;
@@ -421,7 +445,7 @@ const ContainerNewWelcomeUI = styled('div')<{hasFooter: boolean}>`
}
`;
-const Container = styled('div')<{hasFooter: boolean}>`
+const OnboardingContainer = styled('div')<{hasFooter: boolean}>`
flex-grow: 1;
display: flex;
flex-direction: column;