diff --git a/static/app/components/onboarding/gettingStartedDoc/layout.tsx b/static/app/components/onboarding/gettingStartedDoc/layout.tsx index 859696c8f23559..ebcdba87d00012 100644 --- a/static/app/components/onboarding/gettingStartedDoc/layout.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/layout.tsx @@ -7,6 +7,7 @@ import List from 'sentry/components/list'; import ListItem from 'sentry/components/list/listItem'; import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator'; import {Step, StepProps} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {NextStep} from 'sentry/components/onboarding/gettingStartedDoc/types'; import {PlatformOptionsControl} from 'sentry/components/onboarding/platformOptionsControl'; import {ProductSelection} from 'sentry/components/onboarding/productSelection'; import {t} from 'sentry/locale'; @@ -19,12 +20,6 @@ const ProductSelectionAvailabilityHook = HookOrDefault({ defaultComponent: ProductSelection, }); -type NextStep = { - description: string; - link: string; - name: string; -}; - export type LayoutProps = { projectSlug: string; steps: StepProps[]; @@ -52,14 +47,16 @@ export function Layout({ return ( - {introduction && {introduction}} - - {platformOptions ? ( - - ) : null} +
+ {introduction && {introduction}} + + {platformOptions ? ( + + ) : null} +
{steps.map(step => ( @@ -86,6 +83,12 @@ export function Layout({ ); } +const Header = styled('div')` + display: flex; + flex-direction: column; + gap: ${space(2)}; +`; + const Divider = styled('hr')<{withBottomMargin?: boolean}>` height: 1px; width: 100%; @@ -104,7 +107,6 @@ const Introduction = styled('div')` display: flex; flex-direction: column; gap: ${space(1)}; - padding-bottom: ${space(2)}; `; const Wrapper = styled('div')` diff --git a/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx b/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx new file mode 100644 index 00000000000000..eeed71b3fdac90 --- /dev/null +++ b/static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx @@ -0,0 +1,174 @@ +import {Fragment, useMemo} from 'react'; +import styled from '@emotion/styled'; + +import HookOrDefault from 'sentry/components/hookOrDefault'; +import ExternalLink from 'sentry/components/links/externalLink'; +import List from 'sentry/components/list'; +import ListItem from 'sentry/components/list/listItem'; +import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator'; +import {Step} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {Docs, DocsParams} from 'sentry/components/onboarding/gettingStartedDoc/types'; +import {useSourcePackageRegistries} from 'sentry/components/onboarding/gettingStartedDoc/useSourcePackageRegistries'; +import { + PlatformOptionsControl, + useUrlPlatformOptions, +} from 'sentry/components/onboarding/platformOptionsControl'; +import { + ProductSelection, + ProductSolution, +} from 'sentry/components/onboarding/productSelection'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import type {PlatformKey, Project} from 'sentry/types'; +import useOrganization from 'sentry/utils/useOrganization'; + +const ProductSelectionAvailabilityHook = HookOrDefault({ + hookName: 'component:product-selection-availability', + defaultComponent: ProductSelection, +}); + +export type OnboardingLayoutProps = { + docsConfig: Docs; + dsn: string; + platformKey: PlatformKey; + projectId: Project['id']; + projectSlug: Project['slug']; + activeProductSelection?: ProductSolution[]; + newOrg?: boolean; +}; + +const EMPTY_ARRAY: never[] = []; + +export function OnboardingLayout({ + docsConfig, + dsn, + platformKey, + projectId, + projectSlug, + activeProductSelection = EMPTY_ARRAY, + newOrg, +}: OnboardingLayoutProps) { + const organization = useOrganization(); + const {isLoading: isLoadingRegistry, data: registryData} = + useSourcePackageRegistries(organization); + const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions); + + const {platformOptions} = docsConfig; + + const {introduction, steps, nextSteps} = useMemo(() => { + const {onboarding} = docsConfig; + + const docParams: DocsParams = { + dsn, + organization, + platformKey, + projectId, + projectSlug, + isPerformanceSelected: activeProductSelection.includes( + ProductSolution.PERFORMANCE_MONITORING + ), + isProfilingSelected: activeProductSelection.includes(ProductSolution.PROFILING), + isReplaySelected: activeProductSelection.includes(ProductSolution.SESSION_REPLAY), + sourcePackageRegistries: { + isLoading: isLoadingRegistry, + data: registryData, + }, + platformOptions: selectedOptions, + newOrg, + }; + + return { + introduction: onboarding.introduction?.(docParams), + steps: [ + ...onboarding.install(docParams), + ...onboarding.configure(docParams), + ...onboarding.verify(docParams), + ], + nextSteps: onboarding.nextSteps?.(docParams) || [], + }; + }, [ + activeProductSelection, + docsConfig, + dsn, + isLoadingRegistry, + newOrg, + organization, + platformKey, + projectId, + projectSlug, + registryData, + selectedOptions, + ]); + + return ( + + +
+ {introduction &&
{introduction}
} + + {platformOptions ? ( + + ) : null} +
+ + + {steps.map(step => ( + + ))} + + {nextSteps.length > 0 && ( + + +

{t('Next Steps')}

+ + {nextSteps.map(step => ( + + {step.name} + {': '} + {step.description} + + ))} + +
+ )} +
+
+ ); +} + +const Header = styled('div')` + display: flex; + flex-direction: column; + gap: ${space(2)}; +`; + +const Divider = styled('hr')<{withBottomMargin?: boolean}>` + height: 1px; + width: 100%; + background: ${p => p.theme.border}; + border: none; + ${p => p.withBottomMargin && `margin-bottom: ${space(3)}`} +`; + +const Steps = styled('div')` + display: flex; + flex-direction: column; + gap: 1.5rem; +`; + +const Wrapper = styled('div')` + h4 { + margin-bottom: 0.5em; + } + && { + p { + margin-bottom: 0; + } + h5 { + margin-bottom: 0; + } + } +`; diff --git a/static/app/components/onboarding/gettingStartedDoc/sdkDocumentation.tsx b/static/app/components/onboarding/gettingStartedDoc/sdkDocumentation.tsx index cb9b7cf0640e99..0f9804299e9497 100644 --- a/static/app/components/onboarding/gettingStartedDoc/sdkDocumentation.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/sdkDocumentation.tsx @@ -1,6 +1,8 @@ import {useEffect, useState} from 'react'; import LoadingIndicator from 'sentry/components/loadingIndicator'; +import {OnboardingLayout} from 'sentry/components/onboarding/gettingStartedDoc/onboardingLayout'; +import {Docs} from 'sentry/components/onboarding/gettingStartedDoc/types'; import {useSourcePackageRegistries} from 'sentry/components/onboarding/gettingStartedDoc/useSourcePackageRegistries'; import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import type { @@ -15,10 +17,10 @@ import {useApiQuery} from 'sentry/utils/queryClient'; type SdkDocumentationProps = { activeProductSelection: ProductSolution[]; organization: Organization; - platform: PlatformIntegration | null; + platform: PlatformIntegration; + projectId: Project['id']; projectSlug: Project['slug']; newOrg?: boolean; - projectId?: Project['id']; }; export type ModuleProps = { @@ -32,6 +34,11 @@ export type ModuleProps = { sourcePackageRegistries?: ReturnType; }; +function isFunctionalComponent(obj: any): obj is React.ComponentType { + // As we only use function components in the docs this should suffice + return typeof obj === 'function'; +} + // Loads the component containing the documentation for the specified platform export function SdkDocumentation({ platform, @@ -44,7 +51,7 @@ export function SdkDocumentation({ const sourcePackageRegistries = useSourcePackageRegistries(organization); const [module, setModule] = useState; + default: Docs | React.ComponentType; }>(null); // TODO: This will be removed once we no longer rely on sentry-docs to load platform icons @@ -89,7 +96,7 @@ export function SdkDocumentation({ if (projectKeysIsError || projectKeysIsLoading) { return; } - + setModule(null); async function getGettingStartedDoc() { const mod = await import( /* webpackExclude: /.spec/ */ @@ -104,18 +111,33 @@ export function SdkDocumentation({ return ; } - const {default: GettingStartedDoc} = module; + const {default: docs} = module; + + if (isFunctionalComponent(docs)) { + const GettingStartedDoc = docs; + return ( + + ); + } return ( - ); } diff --git a/static/app/components/onboarding/gettingStartedDoc/step.tsx b/static/app/components/onboarding/gettingStartedDoc/step.tsx index d9377e11c30f0f..8873a55b014c98 100644 --- a/static/app/components/onboarding/gettingStartedDoc/step.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/step.tsx @@ -112,6 +112,7 @@ type ConfigurationType = { partialLoading?: boolean; }; +// TODO(aknaus): move to types interface BaseStepProps { /** * Additional information to be displayed below the configurations @@ -121,7 +122,7 @@ interface BaseStepProps { /** * A brief description of the step */ - description?: React.ReactNode; + description?: React.ReactNode | React.ReactNode[]; } interface StepPropsWithTitle extends BaseStepProps { title: string; @@ -227,7 +228,7 @@ const Configurations = styled(Configuration)` margin-top: ${space(2)}; `; -const Description = styled(Configuration)` +const Description = styled('div')` code { color: ${p => p.theme.pink400}; } diff --git a/static/app/components/onboarding/gettingStartedDoc/types.ts b/static/app/components/onboarding/gettingStartedDoc/types.ts new file mode 100644 index 00000000000000..157f77e8e6f975 --- /dev/null +++ b/static/app/components/onboarding/gettingStartedDoc/types.ts @@ -0,0 +1,74 @@ +import type {StepProps} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import type {ReleaseRegistrySdk} from 'sentry/components/onboarding/gettingStartedDoc/useSourcePackageRegistries'; +import type {Organization, PlatformKey, Project} from 'sentry/types'; + +type GeneratorFunction = (params: Params) => T; +type WithGeneratorProperties, Params> = { + [key in keyof T]: GeneratorFunction; +}; + +export interface PlatformOption { + /** + * Array of items for the option. Each one representing a selectable value. + */ + items: { + label: string; + value: Value; + }[]; + /** + * The name of the option + */ + label: string; + /** + * The default value to be used on initial render + */ + defaultValue?: string; +} + +export type BasePlatformOptions = Record>; + +export type SelectedPlatformOptions< + PlatformOptions extends BasePlatformOptions = BasePlatformOptions, +> = { + [key in keyof PlatformOptions]: PlatformOptions[key]['items'][number]['value']; +}; + +export interface DocsParams< + PlatformOptions extends BasePlatformOptions = BasePlatformOptions, +> { + dsn: string; + isPerformanceSelected: boolean; + isProfilingSelected: boolean; + isReplaySelected: boolean; + organization: Organization; + platformKey: PlatformKey; + platformOptions: SelectedPlatformOptions; + projectId: Project['id']; + projectSlug: Project['slug']; + sourcePackageRegistries: {isLoading: boolean; data?: ReleaseRegistrySdk}; + newOrg?: boolean; +} + +export interface NextStep { + description: string; + link: string; + name: string; +} + +export interface OnboardingConfig< + PlatformOptions extends BasePlatformOptions = BasePlatformOptions, +> extends WithGeneratorProperties< + { + configure: StepProps[]; + install: StepProps[]; + verify: StepProps[]; + introduction?: React.ReactNode | React.ReactNode[]; + nextSteps?: NextStep[]; + }, + DocsParams + > {} + +export interface Docs { + onboarding: OnboardingConfig; + platformOptions?: PlatformOptions; +} diff --git a/static/app/components/onboarding/gettingStartedDoc/useSourcePackageRegistries.tsx b/static/app/components/onboarding/gettingStartedDoc/useSourcePackageRegistries.tsx index 6f21e1d996c5c4..11c8303d5af6a9 100644 --- a/static/app/components/onboarding/gettingStartedDoc/useSourcePackageRegistries.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/useSourcePackageRegistries.tsx @@ -4,7 +4,7 @@ import {Organization} from 'sentry/types'; import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse'; import {useApiQuery} from 'sentry/utils/queryClient'; -type ReleaseRegistrySdk = Record< +export type ReleaseRegistrySdk = Record< string, { canonical: string; diff --git a/static/app/components/onboarding/platformOptionsControl.spec.tsx b/static/app/components/onboarding/platformOptionsControl.spec.tsx index cc291f5b896f6c..6740ac0d81c043 100644 --- a/static/app/components/onboarding/platformOptionsControl.spec.tsx +++ b/static/app/components/onboarding/platformOptionsControl.spec.tsx @@ -1,10 +1,8 @@ import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; -import { - PlatformOption, - PlatformOptionsControl, -} from 'sentry/components/onboarding/platformOptionsControl'; +import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types'; +import {PlatformOptionsControl} from 'sentry/components/onboarding/platformOptionsControl'; describe('Onboarding Product Selection', function () { const platformOptions: Record = { diff --git a/static/app/components/onboarding/platformOptionsControl.tsx b/static/app/components/onboarding/platformOptionsControl.tsx index 2a737297f3bd09..bded8f935c4a86 100644 --- a/static/app/components/onboarding/platformOptionsControl.tsx +++ b/static/app/components/onboarding/platformOptionsControl.tsx @@ -1,58 +1,46 @@ import {useMemo} from 'react'; import styled from '@emotion/styled'; +import { + BasePlatformOptions, + PlatformOption, + SelectedPlatformOptions, +} from 'sentry/components/onboarding/gettingStartedDoc/types'; import {SegmentedControl} from 'sentry/components/segmentedControl'; import {space} from 'sentry/styles/space'; import useRouter from 'sentry/utils/useRouter'; -export interface PlatformOption { - /** - * Array of items for the option. Each one representing a selectable value. - */ - items: { - label: string; - value: K; - }[]; - /** - * The name of the option - */ - label: string; - /** - * The default value to be used on initial render - */ - defaultValue?: string; -} - /** * Hook that returns the currently selected platform option values from the URL * it will fallback to the defaultValue or the first option value if the value in the URL is not valid or not present */ -export function useUrlPlatformOptions( - platformOptions: Record -): Record { +export function useUrlPlatformOptions( + platformOptions?: PlatformOptions +): SelectedPlatformOptions { const router = useRouter(); const {query} = router.location; - return useMemo( - () => - Object.keys(platformOptions).reduce( - (acc, key) => { - const defaultValue = platformOptions[key as K].defaultValue; - const values = platformOptions[key as K].items.map(({value}) => value); - acc[key] = values.includes(query[key]) ? query[key] : defaultValue ?? values[0]; - return acc; - }, - {} as Record - ), - [platformOptions, query] - ); + return useMemo(() => { + if (!platformOptions) { + return {} as SelectedPlatformOptions; + } + + return Object.keys(platformOptions).reduce((acc, key) => { + const defaultValue = platformOptions[key].defaultValue; + const values = platformOptions[key].items.map(({value}) => value); + acc[key as keyof PlatformOptions] = values.includes(query[key]) + ? query[key] + : defaultValue ?? values[0]; + return acc; + }, {} as SelectedPlatformOptions); + }, [platformOptions, query]); } type OptionControlProps = { /** * The platform options for which the control is rendered */ - option: PlatformOption; + option: PlatformOption; /** * Value of the currently selected item */ @@ -113,7 +101,6 @@ export function PlatformOptionsControl({platformOptions}: PlatformOptionsControl } const Options = styled('div')` - padding-top: ${space(2)}; display: flex; flex-wrap: wrap; gap: ${space(1)}; diff --git a/static/app/components/onboarding/productSelection.tsx b/static/app/components/onboarding/productSelection.tsx index d46f9f2ad97783..5675e963f7ed1d 100644 --- a/static/app/components/onboarding/productSelection.tsx +++ b/static/app/components/onboarding/productSelection.tsx @@ -18,6 +18,7 @@ import {decodeList} from 'sentry/utils/queryString'; import useRouter from 'sentry/utils/useRouter'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; +// TODO(aknaus): move to types export enum ProductSolution { ERROR_MONITORING = 'error-monitoring', PERFORMANCE_MONITORING = 'performance-monitoring', @@ -340,7 +341,7 @@ export function ProductSelection({ return ( {showPackageManagerInfo && ( - + {lazyLoader ? tct('In this quick guide you’ll use our [loaderScript] to set up:', { loaderScript: Loader Script, @@ -473,5 +474,5 @@ const TooltipDescription = styled('div')` `; const AlternativeInstallationAlert = styled(Alert)` - margin-top: ${space(3)}; + margin-bottom: 0px; `; diff --git a/static/app/gettingStartedDocs/android/android.tsx b/static/app/gettingStartedDocs/android/android.tsx index 13e6bf2f58a559..75e637b8ad8758 100644 --- a/static/app/gettingStartedDocs/android/android.tsx +++ b/static/app/gettingStartedDocs/android/android.tsx @@ -6,10 +6,8 @@ import ListItem from 'sentry/components/list/listItem'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; -import { - PlatformOption, - useUrlPlatformOptions, -} from 'sentry/components/onboarding/platformOptionsControl'; +import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types'; +import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; diff --git a/static/app/gettingStartedDocs/java/java.tsx b/static/app/gettingStartedDocs/java/java.tsx index 968f3c5e2bab91..d7720cd83eeaf9 100644 --- a/static/app/gettingStartedDocs/java/java.tsx +++ b/static/app/gettingStartedDocs/java/java.tsx @@ -5,10 +5,8 @@ import Link from 'sentry/components/links/link'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; -import { - PlatformOption, - useUrlPlatformOptions, -} from 'sentry/components/onboarding/platformOptionsControl'; +import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types'; +import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; diff --git a/static/app/gettingStartedDocs/java/log4j2.tsx b/static/app/gettingStartedDocs/java/log4j2.tsx index 55ac276ae72458..b94654ee881537 100644 --- a/static/app/gettingStartedDocs/java/log4j2.tsx +++ b/static/app/gettingStartedDocs/java/log4j2.tsx @@ -5,10 +5,8 @@ import Link from 'sentry/components/links/link'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; -import { - PlatformOption, - useUrlPlatformOptions, -} from 'sentry/components/onboarding/platformOptionsControl'; +import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types'; +import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {t, tct} from 'sentry/locale'; export enum PackageManager { diff --git a/static/app/gettingStartedDocs/java/logback.tsx b/static/app/gettingStartedDocs/java/logback.tsx index 8284bb8c6d308b..966ef97e590e71 100644 --- a/static/app/gettingStartedDocs/java/logback.tsx +++ b/static/app/gettingStartedDocs/java/logback.tsx @@ -5,10 +5,8 @@ import Link from 'sentry/components/links/link'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; -import { - PlatformOption, - useUrlPlatformOptions, -} from 'sentry/components/onboarding/platformOptionsControl'; +import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types'; +import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {t, tct} from 'sentry/locale'; export enum PackageManager { diff --git a/static/app/gettingStartedDocs/java/spring-boot.spec.tsx b/static/app/gettingStartedDocs/java/spring-boot.spec.tsx index f4e5a42510da2a..a335dc23d8ba8f 100644 --- a/static/app/gettingStartedDocs/java/spring-boot.spec.tsx +++ b/static/app/gettingStartedDocs/java/spring-boot.spec.tsx @@ -1,28 +1,106 @@ -import {render, screen} from 'sentry-test/reactTestingLibrary'; - -import {StepTitle} from 'sentry/components/onboarding/gettingStartedDoc/step'; - -import { - GettingStartedWithSpringBoot, - PackageManager, - SpringBootVersion, - steps, -} from './spring-boot'; - -describe('GettingStartedWithSpringBoot', function () { - it('renders doc correctly', function () { - render(); - - // Steps - for (const step of steps({ - dsn: 'test-dsn', - springBootVersion: SpringBootVersion.V2, - packageManager: PackageManager.MAVEN, - hasPerformance: true, - })) { - expect( - screen.getByRole('heading', {name: step.title ?? StepTitle[step.type]}) - ).toBeInTheDocument(); - } +import {renderWithOnboardingLayout} from 'sentry-test/onboarding/renderWithOnboardingLayout'; +import {screen} from 'sentry-test/reactTestingLibrary'; +import {textWithMarkupMatcher} from 'sentry-test/utils'; + +import docs, {PackageManager, SpringBootVersion} from './spring-boot'; + +describe('java-spring-boot onboarding docs', function () { + it('renders gradle docs correctly', async function () { + renderWithOnboardingLayout(docs, { + releaseRegistry: { + 'sentry.java.android.gradle-plugin': { + version: '1.99.9', + }, + }, + }); + + // Renders main headings + expect(screen.getByRole('heading', {name: 'Install'})).toBeInTheDocument(); + expect(screen.getByRole('heading', {name: 'Configure SDK'})).toBeInTheDocument(); + expect(screen.getByRole('heading', {name: 'Verify'})).toBeInTheDocument(); + + // Renders SDK version from registry + expect( + await screen.findByText( + textWithMarkupMatcher(/id "io\.sentry\.jvm\.gradle" version "1\.99\.9"/) + ) + ).toBeInTheDocument(); + }); + + it('renders maven docs correctly', async function () { + renderWithOnboardingLayout(docs, { + releaseRegistry: { + 'sentry.java.spring-boot.jakarta': { + version: '2.99.9', + }, + 'sentry.java.mavenplugin': { + version: '3.99.9', + }, + }, + selectedOptions: { + packageManager: PackageManager.MAVEN, + }, + }); + + // Renders main headings + expect(screen.getByRole('heading', {name: 'Install'})).toBeInTheDocument(); + expect(screen.getByRole('heading', {name: 'Configure SDK'})).toBeInTheDocument(); + expect(screen.getByRole('heading', {name: 'Verify'})).toBeInTheDocument(); + + // Renders SDK version from registry + expect( + await screen.findByText( + textWithMarkupMatcher( + /sentry-spring-boot-starter-jakarta<\/artifactId>\s*2\.99\.9<\/version>/m + ) + ) + ).toBeInTheDocument(); + + expect( + await screen.findByText( + textWithMarkupMatcher( + /sentry-maven-plugin<\/artifactId>\s*3\.99\.9<\/version>/m + ) + ) + ).toBeInTheDocument(); + }); + + it('renders maven with spring boot 2 docs correctly', async function () { + renderWithOnboardingLayout(docs, { + releaseRegistry: { + 'sentry.java.spring-boot': { + version: '2.99.9', + }, + 'sentry.java.mavenplugin': { + version: '3.99.9', + }, + }, + selectedOptions: { + packageManager: PackageManager.MAVEN, + springBootVersion: SpringBootVersion.V2, + }, + }); + + // Renders main headings + expect(screen.getByRole('heading', {name: 'Install'})).toBeInTheDocument(); + expect(screen.getByRole('heading', {name: 'Configure SDK'})).toBeInTheDocument(); + expect(screen.getByRole('heading', {name: 'Verify'})).toBeInTheDocument(); + + // Renders SDK version from registry + expect( + await screen.findByText( + textWithMarkupMatcher( + /sentry-spring-boot-starter<\/artifactId>\s*2\.99\.9<\/version>/m + ) + ) + ).toBeInTheDocument(); + + expect( + await screen.findByText( + textWithMarkupMatcher( + /sentry-maven-plugin<\/artifactId>\s*3\.99\.9<\/version>/m + ) + ) + ).toBeInTheDocument(); }); }); diff --git a/static/app/gettingStartedDocs/java/spring-boot.tsx b/static/app/gettingStartedDocs/java/spring-boot.tsx index 6c09c9eca25c41..1bf95281a8ac60 100644 --- a/static/app/gettingStartedDocs/java/spring-boot.tsx +++ b/static/app/gettingStartedDocs/java/spring-boot.tsx @@ -2,14 +2,13 @@ import {Fragment} from 'react'; import ExternalLink from 'sentry/components/links/externalLink'; import Link from 'sentry/components/links/link'; -import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; -import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; import { - PlatformOption, - useUrlPlatformOptions, -} from 'sentry/components/onboarding/platformOptionsControl'; -import {ProductSolution} from 'sentry/components/onboarding/productSelection'; + BasePlatformOptions, + Docs, + DocsParams, + OnboardingConfig, +} from 'sentry/components/onboarding/gettingStartedDoc/types'; import {t, tct} from 'sentry/locale'; export enum SpringBootVersion { @@ -22,20 +21,7 @@ export enum PackageManager { MAVEN = 'maven', } -type PlaformOptionKey = 'springBootVersion' | 'packageManager'; - -interface StepsParams { - dsn: string; - hasPerformance: boolean; - organizationSlug?: string; - packageManager?: PackageManager; - projectSlug?: string; - sourcePackageRegistries?: ModuleProps['sourcePackageRegistries']; - springBootVersion?: SpringBootVersion; -} - -// Configuration Start -const platformOptions: Record = { +const platformOptions = { springBootVersion: { label: t('Spring Boot Version'), items: [ @@ -62,67 +48,12 @@ const platformOptions: Record = { }, ], }, -}; +} satisfies BasePlatformOptions; -const introduction = ( -

- {tct( - "Sentry's integration with [springBootLink:Spring Boot] supports Spring Boot 2.1.0 and above. If you're on an older version, use [legacyIntegrationLink:our legacy integration].", - { - springBootLink: , - legacyIntegrationLink: ( - - ), - } - )} -

-); +type PlatformOptions = typeof platformOptions; +type Params = DocsParams; -export const steps = ({ - dsn, - sourcePackageRegistries, - projectSlug, - organizationSlug, - springBootVersion, - packageManager, - hasPerformance, -}: StepsParams): LayoutProps['steps'] => [ - { - type: StepType.INSTALL, - configurations: [ - { - description: ( -

- {tct( - 'To see source context in Sentry, you have to generate an auth token by visiting the [link:Organization Auth Tokens] settings. You can then set the token as an environment variable that is used by the build plugins.', - { - link: , - } - )} -

- ), - language: 'bash', - code: ` -SENTRY_AUTH_TOKEN=___ORG_AUTH_TOKEN___ - `, - }, - packageManager === PackageManager.GRADLE - ? { - description: ( -

- {tct( - 'The [link:Sentry Gradle Plugin] automatically installs the Sentry SDK as well as available integrations for your dependencies. Add the following to your [code:build.gradle] file:', - { - code: , - link: ( - - ), - } - )} -

- ), - language: 'groovy', - code: ` +const getGradleInstallSnippet = (params: Params) => ` buildscript { repositories { mavenCentral() @@ -131,10 +62,10 @@ buildscript { plugins { id "io.sentry.jvm.gradle" version "${ - sourcePackageRegistries?.isLoading + params.sourcePackageRegistries.isLoading ? t('\u2026loading') - : sourcePackageRegistries?.data?.['sentry.java.android.gradle-plugin']?.version ?? - '3.12.0' + : params.sourcePackageRegistries.data?.['sentry.java.android.gradle-plugin'] + ?.version ?? '3.12.0' }" } @@ -144,87 +75,67 @@ sentry { // code as part of your stack traces in Sentry. includeSourceContext = true - org = "${organizationSlug}" - projectName = "${projectSlug}" + org = "${params.organization.slug}" + projectName = "${params.projectSlug}" authToken = System.getenv("SENTRY_AUTH_TOKEN") -} - `, - } - : { - description: t('Install using Maven:'), - configurations: [ - { - language: 'xml', - partialLoading: sourcePackageRegistries?.isLoading, - code: - springBootVersion === SpringBootVersion.V3 - ? ` +}`; + +const getMavenInstallSnippet = (params: Params) => + params.platformOptions.springBootVersion === SpringBootVersion.V3 + ? ` - io.sentry - sentry-spring-boot-starter-jakarta - ${ - sourcePackageRegistries?.isLoading - ? t('\u2026loading') - : sourcePackageRegistries?.data?.['sentry.java.spring-boot.jakarta']?.version ?? - '6.28.0' - } + io.sentry + sentry-spring-boot-starter-jakarta + ${ + params.sourcePackageRegistries?.isLoading + ? t('\u2026loading') + : params.sourcePackageRegistries?.data?.['sentry.java.spring-boot.jakarta'] + ?.version ?? '6.28.0' + } ` - : ` + : ` - io.sentry - sentry-spring-boot-starter - ${ - sourcePackageRegistries?.isLoading - ? t('\u2026loading') - : sourcePackageRegistries?.data?.['sentry.java.spring-boot']?.version ?? '6.28.0' - } -`, - additionalInfo: ( -

- {tct( - 'If you use Logback for logging you may also want to send error logs to Sentry. Add a dependency to the [sentryLogbackCode:sentry-logback] module. Sentry Spring Boot Starter will auto-configure [sentryAppenderCode:SentryAppender].', - {sentryAppenderCode: , sentryLogbackCode: } - )} -

- ), - }, - { - language: 'xml', - code: ` + io.sentry + sentry-spring-boot-starter + ${ + params.sourcePackageRegistries?.isLoading + ? t('\u2026loading') + : params.sourcePackageRegistries?.data?.['sentry.java.spring-boot']?.version ?? + '6.28.0' + } +`; + +const getLogbackInstallSnippet = (params: Params) => ` io.sentry sentry-logback ${ - sourcePackageRegistries?.isLoading + params.sourcePackageRegistries?.isLoading ? t('\u2026loading') - : sourcePackageRegistries?.data?.['sentry.java.logback']?.version ?? '6.28.0' + : params.sourcePackageRegistries?.data?.['sentry.java.logback']?.version ?? + '6.28.0' } - - `, - }, - { - language: 'xml', - description: t( - 'To upload your source code to Sentry so it can be shown in stack traces, use our Maven plugin.' - ), - code: ` +`; + +const getMavenPluginSnippet = (params: Params) => ` io.sentry sentry-maven-plugin ${ - sourcePackageRegistries?.isLoading + params.sourcePackageRegistries?.isLoading ? t('\u2026loading') - : sourcePackageRegistries?.data?.['sentry.java.mavenplugin']?.version ?? '0.0.4' + : params.sourcePackageRegistries?.data?.['sentry.java.mavenplugin']?.version ?? + '0.0.4' } true - ${organizationSlug} + ${params.organization.slug} - ${projectSlug} + ${params.projectSlug} @@ -243,75 +154,30 @@ sentry { ... - - `, - }, - ], - }, - ], - }, - { - type: StepType.CONFIGURE, - description: ( -

- {tct( - 'Open up [applicationPropertiesCode:src/main/application.properties] (or [applicationYmlCode:src/main/application.yml]) and configure the DSN, and any other settings you need:', - { - applicationPropertiesCode: , - applicationYmlCode: , - } - )} -

- ), - configurations: [ - { - code: [ - { - label: 'Properties', - value: 'properties', - language: 'properties', - code: ` -sentry.dsn=${dsn}${ - hasPerformance - ? ` +`; + +const getConfigurationPropertiesSnippet = (params: Params) => ` +sentry.dsn=${params.dsn}${ + params.isPerformanceSelected + ? ` # Set traces-sample-rate to 1.0 to capture 100% of transactions for performance monitoring. # We recommend adjusting this value in production. sentry.traces-sample-rate=1.0` - : '' - }`, - }, - { - label: 'YAML', - value: 'yaml', - language: 'properties', - code: ` + : '' +}`; + +const getConfigurationYamlSnippet = (params: Params) => ` sentry: - dsn: ${dsn}${ - hasPerformance + dsn: ${params.dsn}${ + params.isPerformanceSelected ? ` # Set traces-sample-rate to 1.0 to capture 100% of transactions for performance monitoring. # We recommend adjusting this value in production. sentry.traces-sample-rate: 1.0` : '' - }`, - }, - ], - }, - ], - }, - { - type: StepType.VERIFY, - description: t( - 'Then create an intentional error, so you can test that everything is working using either Java or Kotlin:' - ), - configurations: [ - { - code: [ - { - label: 'Java', - value: 'java', - language: 'javascript', // TODO: This shouldn't be javascript but because of better formatting we use it for now - code: ` + }`; + +const getVerifyJavaSnippet = () => ` import java.lang.Exception; import io.sentry.Sentry; @@ -319,13 +185,9 @@ try { throw new Exception("This is a test."); } catch (Exception e) { Sentry.captureException(e); -}`, - }, - { - label: 'Kotlin', - value: 'kotlin', - language: 'javascript', // TODO: This shouldn't be javascript but because of better formatting we use it for now - code: ` +}`; + +const getVerifyKotlinSnippet = () => ` import java.lang.Exception import io.sentry.Sentry @@ -333,80 +195,166 @@ try { throw Exception("This is a test.") } catch (e: Exception) { Sentry.captureException(e) -}`, - }, - ], - }, - ], - additionalInfo: ( - -

- {t( - "If you're new to Sentry, use the email alert to access your account and complete a product tour." - )} -

-

- {t( - "If you're an existing user and have disabled alerts, you won't receive this email." - )} -

-
- ), - }, -]; +}`; -export const nextSteps = [ - { - id: 'examples', - name: t('Examples'), - description: t('Check out our sample applications.'), - link: 'https://github.com/getsentry/sentry-java/tree/main/sentry-samples', - }, - { - id: 'performance-monitoring', - name: t('Performance Monitoring'), - description: t( - 'Stay ahead of latency issues and trace every slow transaction to a poor-performing API call or database query.' +const onboarding: OnboardingConfig = { + introduction: () => + tct( + "Sentry's integration with [springBootLink:Spring Boot] supports Spring Boot 2.1.0 and above. If you're on an older version, use [legacyIntegrationLink:our legacy integration].", + { + springBootLink: , + legacyIntegrationLink: ( + + ), + } ), - link: 'https://docs.sentry.io/platforms/java/guides/spring-boot/performance/', - }, -]; -// Configuration End - -export function GettingStartedWithSpringBoot({ - dsn, - sourcePackageRegistries, - projectSlug, - organization, - activeProductSelection = [], - ...props -}: ModuleProps) { - const optionValues = useUrlPlatformOptions(platformOptions); - - const hasPerformance = activeProductSelection.includes( - ProductSolution.PERFORMANCE_MONITORING - ); - - const nextStepDocs = [...nextSteps]; + install: (params: Params) => [ + { + type: StepType.INSTALL, + configurations: [ + { + description: tct( + 'To see source context in Sentry, you have to generate an auth token by visiting the [link:Organization Auth Tokens] settings. You can then set the token as an environment variable that is used by the build plugins.', + { + link: , + } + ), + language: 'bash', + code: `SENTRY_AUTH_TOKEN=___ORG_AUTH_TOKEN___`, + }, + params.platformOptions.packageManager === PackageManager.GRADLE + ? { + description: tct( + 'The [link:Sentry Gradle Plugin] automatically installs the Sentry SDK as well as available integrations for your dependencies. Add the following to your [code:build.gradle] file:', + { + code: , + link: ( + + ), + } + ), + language: 'groovy', + code: getGradleInstallSnippet(params), + } + : { + description: t('Install using Maven:'), + configurations: [ + { + language: 'xml', + partialLoading: params.sourcePackageRegistries?.isLoading, + code: getMavenInstallSnippet(params), + additionalInfo: tct( + 'If you use Logback for logging you may also want to send error logs to Sentry. Add a dependency to the [sentryLogbackCode:sentry-logback] module. Sentry Spring Boot Starter will auto-configure [sentryAppenderCode:SentryAppender].', + {sentryAppenderCode: , sentryLogbackCode: } + ), + }, + { + language: 'xml', + code: getLogbackInstallSnippet(params), + }, + { + language: 'xml', + description: t( + 'To upload your source code to Sentry so it can be shown in stack traces, use our Maven plugin.' + ), + code: getMavenPluginSnippet(params), + }, + ], + }, + ], + }, + ], + configure: (params: Params) => [ + { + type: StepType.CONFIGURE, + description: tct( + 'Open up [applicationPropertiesCode:src/main/application.properties] (or [applicationYmlCode:src/main/application.yml]) and configure the DSN, and any other settings you need:', + { + applicationPropertiesCode: , + applicationYmlCode: , + } + ), + configurations: [ + { + code: [ + { + label: 'Properties', + value: 'properties', + language: 'properties', + code: getConfigurationPropertiesSnippet(params), + }, + { + label: 'YAML', + value: 'yaml', + language: 'properties', + code: getConfigurationYamlSnippet(params), + }, + ], + }, + ], + }, + ], + verify: () => [ + { + type: StepType.VERIFY, + description: t( + 'Then create an intentional error, so you can test that everything is working using either Java or Kotlin:' + ), + configurations: [ + { + code: [ + { + label: 'Java', + value: 'java', + language: 'javascript', // TODO: This shouldn't be javascript but because of better formatting we use it for now + code: getVerifyJavaSnippet(), + }, + { + label: 'Kotlin', + value: 'kotlin', + language: 'javascript', // TODO: This shouldn't be javascript but because of better formatting we use it for now + code: getVerifyKotlinSnippet(), + }, + ], + }, + ], + additionalInfo: ( + +

+ {t( + "If you're new to Sentry, use the email alert to access your account and complete a product tour." + )} +

+

+ {t( + "If you're an existing user and have disabled alerts, you won't receive this email." + )} +

+
+ ), + }, + ], + nextSteps: () => [ + { + id: 'examples', + name: t('Examples'), + description: t('Check out our sample applications.'), + link: 'https://github.com/getsentry/sentry-java/tree/main/sentry-samples', + }, + { + id: 'performance-monitoring', + name: t('Performance Monitoring'), + description: t( + 'Stay ahead of latency issues and trace every slow transaction to a poor-performing API call or database query.' + ), + link: 'https://docs.sentry.io/platforms/java/guides/spring-boot/performance/', + }, + ], +}; - return ( - - ); -} +const docs: Docs = { + onboarding, + platformOptions, +}; -export default GettingStartedWithSpringBoot; +export default docs; diff --git a/static/app/gettingStartedDocs/java/spring.tsx b/static/app/gettingStartedDocs/java/spring.tsx index 4ba7a020e33447..39b1e678fb23b9 100644 --- a/static/app/gettingStartedDocs/java/spring.tsx +++ b/static/app/gettingStartedDocs/java/spring.tsx @@ -5,10 +5,8 @@ import Link from 'sentry/components/links/link'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; -import { - PlatformOption, - useUrlPlatformOptions, -} from 'sentry/components/onboarding/platformOptionsControl'; +import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types'; +import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {t, tct} from 'sentry/locale'; export enum SpringVersion { diff --git a/static/app/gettingStartedDocs/javascript/angular.tsx b/static/app/gettingStartedDocs/javascript/angular.tsx index deb466cb42f54e..e1e345c84b9261 100644 --- a/static/app/gettingStartedDocs/javascript/angular.tsx +++ b/static/app/gettingStartedDocs/javascript/angular.tsx @@ -1,11 +1,9 @@ import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types'; import {getUploadSourceMapsStep} from 'sentry/components/onboarding/gettingStartedDoc/utils'; -import { - PlatformOption, - useUrlPlatformOptions, -} from 'sentry/components/onboarding/platformOptionsControl'; +import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; import type {Organization, PlatformKey} from 'sentry/types'; diff --git a/static/app/gettingStartedDocs/javascript/vue.tsx b/static/app/gettingStartedDocs/javascript/vue.tsx index 1115f409829283..289434107de8fb 100644 --- a/static/app/gettingStartedDocs/javascript/vue.tsx +++ b/static/app/gettingStartedDocs/javascript/vue.tsx @@ -1,11 +1,9 @@ import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; +import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types'; import {getUploadSourceMapsStep} from 'sentry/components/onboarding/gettingStartedDoc/utils'; -import { - PlatformOption, - useUrlPlatformOptions, -} from 'sentry/components/onboarding/platformOptionsControl'; +import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; import type {Organization, PlatformKey} from 'sentry/types'; diff --git a/static/app/gettingStartedDocs/kotlin/kotlin.tsx b/static/app/gettingStartedDocs/kotlin/kotlin.tsx index 8b5fac0e3c3ad3..fb48b37237f461 100644 --- a/static/app/gettingStartedDocs/kotlin/kotlin.tsx +++ b/static/app/gettingStartedDocs/kotlin/kotlin.tsx @@ -5,10 +5,8 @@ import Link from 'sentry/components/links/link'; import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout'; import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step'; -import { - PlatformOption, - useUrlPlatformOptions, -} from 'sentry/components/onboarding/platformOptionsControl'; +import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types'; +import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl'; import {ProductSolution} from 'sentry/components/onboarding/productSelection'; import {t, tct} from 'sentry/locale'; diff --git a/static/app/gettingStartedDocs/ruby/rails.tsx b/static/app/gettingStartedDocs/ruby/rails.tsx index 99fa49bb758923..cfd727fd617fa9 100644 --- a/static/app/gettingStartedDocs/ruby/rails.tsx +++ b/static/app/gettingStartedDocs/ruby/rails.tsx @@ -8,8 +8,8 @@ import {t, tct} from 'sentry/locale'; // Configuration Start const introduction = ( - {t('In Rails, all uncaught exceptions will be automatically reported.')} - {t('We support Rails 5 and newer.')} +

{t('In Rails, all uncaught exceptions will be automatically reported.')}

+

{t('We support Rails 5 and newer.')}

); export const steps = ({ diff --git a/static/app/views/onboarding/setupDocsLoader.tsx b/static/app/views/onboarding/setupDocsLoader.tsx index 13cd5a7dc84b85..577b1aff6c6c89 100644 --- a/static/app/views/onboarding/setupDocsLoader.tsx +++ b/static/app/views/onboarding/setupDocsLoader.tsx @@ -16,6 +16,7 @@ import { } from 'sentry/components/onboarding/productSelection'; import {IconChevron} from 'sentry/icons'; import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; import type {PlatformKey} from 'sentry/types'; import {Organization, Project, ProjectKey} from 'sentry/types'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -142,12 +143,14 @@ export function SetupDocsLoader({ return ( - +
+ +
{projectKeyUpdateError && ( { + releaseRegistry?: DeepPartial; + selectedOptions?: Partial>; + selectedProducts?: ProductSolution[]; +} + +export function renderWithOnboardingLayout< + PlatformOptions extends BasePlatformOptions = BasePlatformOptions, +>(docsConfig: Docs, options: Options = {}) { + const { + releaseRegistry = {}, + selectedProducts = [ + ProductSolution.PERFORMANCE_MONITORING, + ProductSolution.PROFILING, + ProductSolution.SESSION_REPLAY, + ], + selectedOptions = {}, + } = options; + + const {organization, routerContext} = initializeOrg({ + router: { + location: { + query: selectedOptions, + }, + }, + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/sdks/`, + body: releaseRegistry, + }); + + render( + , + { + organization, + context: routerContext, + } + ); +}