diff --git a/static/app/components/onboarding/gettingStartedDoc/contentBlocks/defaultRenderers.tsx b/static/app/components/onboarding/gettingStartedDoc/contentBlocks/defaultRenderers.tsx new file mode 100644 index 00000000000000..f5dcf7ca7f13ab --- /dev/null +++ b/static/app/components/onboarding/gettingStartedDoc/contentBlocks/defaultRenderers.tsx @@ -0,0 +1,104 @@ +import {css} from '@emotion/react'; +import styled from '@emotion/styled'; + +import {Alert} from 'sentry/components/core/alert'; +import {useRendererContext} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/rendererContext'; +import type { + BlockRenderers, + ContentBlock, +} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/types'; +import { + CssVariables, + renderBlocks, +} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/utils'; +import { + OnboardingCodeSnippet, + TabbedCodeSnippet, +} from 'sentry/components/onboarding/gettingStartedDoc/onboardingCodeSnippet'; + +const baseBlockStyles = css` + :not(:last-child) { + margin-bottom: var(${CssVariables.BLOCK_SPACING}); + } +`; + +function AlertBlock({ + alertType, + text, + showIcon, + system, + trailingItems, +}: Extract) { + return ( +
+ + {text} + +
+ ); +} + +function CodeBlock(block: Extract) { + if ('code' in block) { + return ( +
+ + {block.code} + +
+ ); + } + + const tabsWithValues = block.tabs.map(tab => ({ + ...tab, + value: tab.label, + })); + + return ( +
+ +
+ ); +} + +function ConditionalBlock({ + condition, + content, +}: Extract) { + const {renderers} = useRendererContext(); + + if (condition) { + return renderBlocks(content, renderers); + } + + return null; +} + +function CustomBlock(block: Extract) { + return
{block.content}
; +} + +function TextBlock(block: Extract) { + return {block.text}; +} + +const TextBlockWrapper = styled('div')` + ${baseBlockStyles} + + code:not([class*='language-']) { + color: ${p => p.theme.pink400}; + } +`; + +export const defaultRenderers: BlockRenderers = { + text: TextBlock, + code: CodeBlock, + custom: CustomBlock, + alert: AlertBlock, + conditional: ConditionalBlock, +}; diff --git a/static/app/components/onboarding/gettingStartedDoc/contentBlocks/renderer.tsx b/static/app/components/onboarding/gettingStartedDoc/contentBlocks/renderer.tsx new file mode 100644 index 00000000000000..ce38335380e23d --- /dev/null +++ b/static/app/components/onboarding/gettingStartedDoc/contentBlocks/renderer.tsx @@ -0,0 +1,67 @@ +import {useMemo} from 'react'; +import styled from '@emotion/styled'; + +import {defaultRenderers} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/defaultRenderers'; +import {RendererContext} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/rendererContext'; +import type { + BlockRenderers, + ContentBlock, +} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/types'; +import { + CssVariables, + renderBlocks, +} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/utils'; +import {space} from 'sentry/styles/space'; + +interface Props { + /** + * The content blocks to be rendered. + */ + contentBlocks: Array; + /** + * The class name to be applied to the root element. + */ + className?: string; + /** + * A custom renderer for the content blocks. + * If not provided, the default renderer will be used. + * The renderers object must have a key for each content block type. + */ + renderers?: Partial; + /** + * The spacing between the content blocks. + * Available as a CSS variable `var(${CssVariables.BLOCK_SPACING})` for styling of child elements. + */ + spacing?: string; +} + +const NO_RENDERERS = {}; +const DEFAULT_SPACING = space(2); + +export function ContentBlocksRenderer({ + contentBlocks, + renderers: customRenderers = NO_RENDERERS, + spacing = DEFAULT_SPACING, + className, +}: Props) { + const contextValue = useMemo( + () => ({ + renderers: { + ...defaultRenderers, + ...customRenderers, + }, + }), + [customRenderers] + ); + return ( + + + {renderBlocks(contentBlocks, contextValue.renderers)} + + + ); +} + +const Wrapper = styled('div')<{spacing: string}>` + ${CssVariables.BLOCK_SPACING}: ${p => p.spacing}; +`; diff --git a/static/app/components/onboarding/gettingStartedDoc/contentBlocks/rendererContext.tsx b/static/app/components/onboarding/gettingStartedDoc/contentBlocks/rendererContext.tsx new file mode 100644 index 00000000000000..ad31c485863939 --- /dev/null +++ b/static/app/components/onboarding/gettingStartedDoc/contentBlocks/rendererContext.tsx @@ -0,0 +1,18 @@ +import {createContext, useContext} from 'react'; + +import type {BlockRenderers} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/types'; + +export const RendererContext = createContext< + | undefined + | { + renderers: BlockRenderers; + } +>(undefined); + +export const useRendererContext = () => { + const context = useContext(RendererContext); + if (!context) { + throw new Error('useRendererContext must be used within a RendererContext'); + } + return context; +}; diff --git a/static/app/components/onboarding/gettingStartedDoc/contentBlocks/types.tsx b/static/app/components/onboarding/gettingStartedDoc/contentBlocks/types.tsx new file mode 100644 index 00000000000000..95fb1beae7a38c --- /dev/null +++ b/static/app/components/onboarding/gettingStartedDoc/contentBlocks/types.tsx @@ -0,0 +1,70 @@ +import type {AlertProps} from 'sentry/components/core/alert'; +import type {CodeSnippetTab} from 'sentry/components/onboarding/gettingStartedDoc/onboardingCodeSnippet'; + +type BaseBlock = { + type: T; +}; + +/** + * Renders the Alert component + */ +type AlertBlock = BaseBlock<'alert'> & { + alertType: AlertProps['type']; + text: React.ReactNode; + type: 'alert'; + icon?: AlertProps['icon']; + showIcon?: AlertProps['showIcon']; + system?: AlertProps['system']; + trailingItems?: AlertProps['trailingItems']; +}; + +// The value of the tab is omitted and inferred from the label in the renderer +type CodeTabWithoutValue = Omit; +type SingleCodeBlock = BaseBlock<'code'> & Omit; +type MultipleCodeBlock = BaseBlock<'code'> & { + tabs: CodeTabWithoutValue[]; +}; +/** + * Code blocks can either render a single code snippet or multiple code snippets in a tabbed interface. + */ +type CodeBlock = SingleCodeBlock | MultipleCodeBlock; + +/** + * Conditional blocks are used to render content based on a condition. + */ +type ConditionalBlock = BaseBlock<'conditional'> & { + condition: boolean; + content: ContentBlock[]; +}; + +/** + * Text blocks are used to render one paragraph of text. + */ +type TextBlock = BaseBlock<'text'> & { + /** + * Only meant for text or return values of translation functions (t, tct, tn). + * + * **Do not** use this with custom react elements but instead use the `custom` block type. + */ + text: React.ReactNode; +}; + +/** + * Custom blocks can be used to render any content that is not covered by the other block types. + */ +type CustomBlock = BaseBlock<'custom'> & { + content: React.ReactNode; +}; + +export type ContentBlock = + | AlertBlock + | CodeBlock + | ConditionalBlock + | CustomBlock + | TextBlock; + +export type BlockRenderers = { + [key in ContentBlock['type']]: ( + block: Extract + ) => React.ReactNode; +}; diff --git a/static/app/components/onboarding/gettingStartedDoc/contentBlocks/utils.tsx b/static/app/components/onboarding/gettingStartedDoc/contentBlocks/utils.tsx new file mode 100644 index 00000000000000..04a6b214179c95 --- /dev/null +++ b/static/app/components/onboarding/gettingStartedDoc/contentBlocks/utils.tsx @@ -0,0 +1,27 @@ +import type { + BlockRenderers, + ContentBlock, +} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/types'; + +export enum CssVariables { + BLOCK_SPACING = '--block-spacing', +} + +export function renderBlocks( + contentBlocks: Array, + renderers: BlockRenderers +) { + return contentBlocks.map((block, index) => { + if (!block) { + return null; + } + // Need to cast here as ts bugs out on the return type and does not allow assigning the key prop + const RendererComponent = renderers[block.type] as ( + block: ContentBlock + ) => React.ReactNode; + + // The index actually works well as a key here + // as long as the conditional block is used instead of JS logic to edit the blocks array + return ; + }); +} diff --git a/static/app/components/onboarding/gettingStartedDoc/onboardingCodeSnippet.tsx b/static/app/components/onboarding/gettingStartedDoc/onboardingCodeSnippet.tsx index 0a6e19a51f4cf9..966532900cc8f2 100644 --- a/static/app/components/onboarding/gettingStartedDoc/onboardingCodeSnippet.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/onboardingCodeSnippet.tsx @@ -1,9 +1,10 @@ -import {Fragment, useCallback, useState} from 'react'; +import {Fragment, useCallback, useMemo, useState} from 'react'; import {createPortal} from 'react-dom'; import beautify from 'js-beautify'; import {CodeSnippet} from 'sentry/components/codeSnippet'; import {AuthTokenGenerator} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator'; +import {PACKAGE_LOADING_PLACEHOLDER} from 'sentry/utils/gettingStartedDocs/getPackageVersion'; interface OnboardingCodeSnippetProps extends Omit, 'onAfterHighlight'> {} @@ -39,11 +40,18 @@ export function OnboardingCodeSnippet({ setAuthTokenNodes(replaceTokensWithSpan(element)); }, []); + const partialLoading = useMemo( + () => children.includes(PACKAGE_LOADING_PLACEHOLDER), + [children] + ); + return ( diff --git a/static/app/components/onboarding/gettingStartedDoc/replay/tracePropagationMessage.tsx b/static/app/components/onboarding/gettingStartedDoc/replay/tracePropagationMessage.tsx index 09af0eaf1150b3..777a49df31df46 100644 --- a/static/app/components/onboarding/gettingStartedDoc/replay/tracePropagationMessage.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/replay/tracePropagationMessage.tsx @@ -1,5 +1,6 @@ import {Alert} from 'sentry/components/core/alert'; import ExternalLink from 'sentry/components/links/externalLink'; +import type {ContentBlock} from 'sentry/components/onboarding/gettingStartedDoc/types'; import {tct} from 'sentry/locale'; export default function TracePropagationMessage() { @@ -16,3 +17,16 @@ export default function TracePropagationMessage() { ); } + +export const tracePropagationBlock: ContentBlock = { + type: 'alert', + alertType: 'info', + text: tct( + `To see replays for backend errors, ensure that you have set up trace propagation. To learn more, [link:read the docs].`, + { + link: ( + + ), + } + ), +}; diff --git a/static/app/components/onboarding/gettingStartedDoc/step.tsx b/static/app/components/onboarding/gettingStartedDoc/step.tsx index 092cc7e275fa37..9fdc9c9b777661 100644 --- a/static/app/components/onboarding/gettingStartedDoc/step.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/step.tsx @@ -3,6 +3,7 @@ import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import {Button} from 'sentry/components/core/button'; +import {ContentBlocksRenderer} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/renderer'; import { OnboardingCodeSnippet, TabbedCodeSnippet, @@ -64,6 +65,7 @@ export function Step({ title, type, configurations, + content, additionalInfo, description, onOptionalToggleClick, @@ -71,10 +73,14 @@ export function Step({ trailingItems, codeHeader, ...props -}: React.HTMLAttributes & OnboardingStep) { +}: Omit, 'content'> & OnboardingStep) { const [showOptionalConfig, setShowOptionalConfig] = useState(false); - const config = ( + const config = content ? ( + + + + ) : ( {description && {description}} diff --git a/static/app/components/onboarding/gettingStartedDoc/types.ts b/static/app/components/onboarding/gettingStartedDoc/types.ts index d27b6e8b8053c1..d11aecf6e1e4c2 100644 --- a/static/app/components/onboarding/gettingStartedDoc/types.ts +++ b/static/app/components/onboarding/gettingStartedDoc/types.ts @@ -1,9 +1,14 @@ +import type React from 'react'; + import type {Client} from 'sentry/api'; +import type {ContentBlock} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/types'; import type {CodeSnippetTab} from 'sentry/components/onboarding/gettingStartedDoc/onboardingCodeSnippet'; import type {ReleaseRegistrySdk} from 'sentry/components/onboarding/gettingStartedDoc/useSourcePackageRegistries'; import type {Organization} from 'sentry/types/organization'; import type {PlatformKey, Project, ProjectKey} from 'sentry/types/project'; +export type {ContentBlock} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/types'; + type GeneratorFunction = (params: Params) => T; type WithGeneratorProperties, Params> = { [key in keyof T]: GeneratorFunction; @@ -20,8 +25,10 @@ export type Configuration = { code?: string | CodeSnippetTab[]; /** * Nested configurations provide a convenient way to accommodate diverse layout styles, like the Spring Boot configuration. + * @deprecated Use `content` instead */ configurations?: Configuration[]; + /** * A brief description of the configuration */ @@ -53,10 +60,12 @@ export enum StepType { interface BaseStepProps { /** * Additional information to be displayed below the configurations + * @deprecated Use `content` instead */ additionalInfo?: React.ReactNode; /** * Content that goes directly above the code snippet + * @deprecated Use `content` instead */ codeHeader?: React.ReactNode; /** @@ -65,10 +74,16 @@ interface BaseStepProps { collapsible?: boolean; /** * An array of configurations to be displayed + * @deprecated Use `content` instead */ configurations?: Configuration[]; + /** + * The content blocks to display + */ + content?: ContentBlock[]; /** * A brief description of the step + * @deprecated Use `content` instead */ description?: React.ReactNode | React.ReactNode[]; /** diff --git a/static/app/components/onboarding/gettingStartedDoc/utils/index.tsx b/static/app/components/onboarding/gettingStartedDoc/utils/index.tsx index 8ae1e36456f3d8..2d29beb24d537d 100644 --- a/static/app/components/onboarding/gettingStartedDoc/utils/index.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/utils/index.tsx @@ -1,6 +1,10 @@ import {Button} from 'sentry/components/core/button'; import ExternalLink from 'sentry/components/links/externalLink'; -import type {DocsParams} from 'sentry/components/onboarding/gettingStartedDoc/types'; +import {OnboardingCodeSnippet} from 'sentry/components/onboarding/gettingStartedDoc/onboardingCodeSnippet'; +import type { + DocsParams, + OnboardingStep, +} from 'sentry/components/onboarding/gettingStartedDoc/types'; import {IconCopy} from 'sentry/icons/iconCopy'; import {t, tct} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -19,60 +23,61 @@ export function getUploadSourceMapsStep({ }: DocsParams & { description?: React.ReactNode; guideLink?: string; -}) { +}): OnboardingStep { + function trackEvent(eventName: string) { + if (!organization || !projectId || !platformKey) { + return; + } + + trackAnalytics(eventName, { + project_id: projectId, + platform: platformKey, + organization, + }); + } + return { collapsible: true, title: t('Upload Source Maps (Optional)'), - description: description ?? ( -

- {tct( - 'Automatically upload your source maps to enable readable stack traces for Errors. If you prefer to manually set up source maps, please follow [guideLink:this guide].', - { - guideLink: , - } - )} -

- ), - configurations: [ + content: [ { - language: 'bash', - code: getSourceMapsWizardSnippet({ - isSelfHosted, - organization, - projectSlug, - }), - onCopy: () => { - if (!organization || !projectId || !platformKey) { - return; - } - - trackAnalytics( - newOrg - ? 'onboarding.source_maps_wizard_button_copy_clicked' - : 'project_creation.source_maps_wizard_button_copy_clicked', + type: 'text', + text: + description ?? + tct( + 'Automatically upload your source maps to enable readable stack traces for Errors. If you prefer to manually set up source maps, please follow [guideLink:this guide].', { - project_id: projectId, - platform: platformKey, - organization, + guideLink: , } - ); - }, - onSelectAndCopy: () => { - if (!organization || !projectId || !platformKey) { - return; - } - - trackAnalytics( - newOrg - ? 'onboarding.source_maps_wizard_selected_and_copied' - : 'project_creation.source_maps_wizard_selected_and_copied', - { - project_id: projectId, - platform: platformKey, - organization, + ), + }, + { + type: 'custom', + content: ( + + trackEvent( + newOrg + ? 'onboarding.source_maps_wizard_button_copy_clicked' + : 'project_creation.source_maps_wizard_button_copy_clicked' + ) } - ); - }, + onSelectAndCopy={() => + trackEvent( + newOrg + ? 'onboarding.source_maps_wizard_selected_and_copied' + : 'project_creation.source_maps_wizard_selected_and_copied' + ) + } + > + {getSourceMapsWizardSnippet({ + isSelfHosted, + organization, + projectSlug, + })} + + ), }, ], }; @@ -87,23 +92,26 @@ function CopyRulesButton({rules}: {rules: string}) { ); } -export function getAIRulesForCodeEditorStep({rules}: {rules: string}) { +export function getAIRulesForCodeEditorStep({rules}: {rules: string}): OnboardingStep { return { collapsible: true, title: t('AI Rules for Code Editors (Optional)'), - description: tct( - 'Sentry provides a set of rules you can use to help your LLM use Sentry correctly. Copy this file and add it to your projects rules configuration. When created as a rules file this should be placed alongside other editor specific rule files. For example, if you are using Cursor, place this file in the [code:.cursorrules] directory.', - { - code: , - } - ), trailingItems: , - configurations: [ + content: [ + { + type: 'text', + text: tct( + 'Sentry provides a set of rules you can use to help your LLM use Sentry correctly. Copy this file and add it to your projects rules configuration. When created as a rules file this should be placed alongside other editor specific rule files. For example, if you are using Cursor, place this file in the [code:.cursorrules] directory.', + { + code: , + } + ), + }, { - code: [ + type: 'code', + tabs: [ { label: 'Markdown', - value: 'md', language: 'md', filename: 'rules.md', code: rules, diff --git a/static/app/components/onboarding/gettingStartedDoc/utils/profilingOnboarding.tsx b/static/app/components/onboarding/gettingStartedDoc/utils/profilingOnboarding.tsx index a939e6ba9b7a11..bafb7c76b21ae0 100644 --- a/static/app/components/onboarding/gettingStartedDoc/utils/profilingOnboarding.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/utils/profilingOnboarding.tsx @@ -1,7 +1,5 @@ import type React from 'react'; -import {Fragment} from 'react'; -import {TabbedCodeSnippet} from 'sentry/components/onboarding/gettingStartedDoc/onboardingCodeSnippet'; import type { DocsParams, OnboardingStep, @@ -11,31 +9,29 @@ import {t, tct} from 'sentry/locale'; export function getProfilingDocumentHeaderConfigurationStep(): OnboardingStep { return { title: 'Add Document-Policy: js-profiling header', - description: ( - -

- {tct( - `For the JavaScript browser profiler to start, the document response header needs + content: [ + { + type: 'text', + text: t( + `For the JavaScript browser profiler to start, the document response header needs to include a Document-Policy header key with the js-profiling value. How you do this will depend on how your assets are served. If you're using a server like Express, you'll be able to use the response.set function to set the header value. - `, - {} - )} -

- -
- ), + ` + ), + }, + { + type: 'code', + tabs: [ + { + label: 'Express', + language: 'javascript', + code: `response.set('Document-Policy', 'js-profiling')`, + }, + ], + }, + ], }; } diff --git a/static/app/components/updatedEmptyState.tsx b/static/app/components/updatedEmptyState.tsx index d3032bd2580d25..88bfb90c13ed24 100644 --- a/static/app/components/updatedEmptyState.tsx +++ b/static/app/components/updatedEmptyState.tsx @@ -7,6 +7,7 @@ import {ButtonBar} from 'sentry/components/core/button/buttonBar'; import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator'; +import {ContentBlocksRenderer} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/renderer'; import { OnboardingCodeSnippet, TabbedCodeSnippet, @@ -182,20 +183,27 @@ export default function UpdatedEmptyState({project}: {project?: Project}) { const title = step?.title ?? StepTitles[step?.type ?? 'install']; return ( -
- {step?.description ? ( - {step.description} - ) : null} - {step?.configurations?.map((configuration, configIndex) => ( - - ))} - {step?.additionalInfo ? ( - {step.additionalInfo} - ) : null} -
+ {step?.content ? ( + + ) : ( +
+ {step?.description ? ( + {step.description} + ) : null} + {step?.configurations?.map((configuration, configIndex) => ( + + ))} + {step?.additionalInfo ? ( + {step.additionalInfo} + ) : null} +
+ )} {index === steps.length - 1 ? ( = { install: params => [ { type: StepType.INSTALL, - description: t( - `Install the SDK via %s:`, - packageManagerName[params.platformOptions.packageManager] - ), - configurations: [ + content: [ + { + type: 'text', + text: t( + `Install the SDK via %s:`, + packageManagerName[params.platformOptions.packageManager] + ), + }, { - description: tct( + type: 'text', + text: tct( 'To see source context in Sentry, you have to generate an auth token by visiting the [link:Organization Tokens] settings. You can then set the token as an environment variable that is used by the build plugins.', { link: , } ), + }, + { + type: 'code', language: 'bash', code: `SENTRY_AUTH_TOKEN=___ORG_AUTH_TOKEN___`, }, - ...(params.platformOptions.packageManager === PackageManager.GRADLE - ? [ - { - language: 'groovy', - partialLoading: params.sourcePackageRegistries?.isLoading, - 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: ( - - ), - } - ), - code: getGradleInstallSnippet(params), - }, - ] - : []), - ...(params.platformOptions.packageManager === PackageManager.MAVEN - ? [ - { - language: 'xml', - partialLoading: params.sourcePackageRegistries?.isLoading, - description: tct( - 'The [link:Sentry Maven Plugin] automatically installs the Sentry SDK as well as available integrations for your dependencies. Add the following to your [code:pom.xml] file:', - { - code: , - link: ( - - ), - } - ), - code: getMavenInstallSnippet(params), - }, - ] - : []), - ...(params.platformOptions.packageManager === PackageManager.SBT - ? [ - { - description: tct( - 'Add the sentry SDK to your [code:libraryDependencies]:', - { - code: , - } - ), - language: 'scala', - partialLoading: params.sourcePackageRegistries?.isLoading, - code: `libraryDependencies += "io.sentry" % "sentry" % "${getPackageVersion( - params, - 'sentry.java', - '6.27.0' - )}"`, - }, - ] - : []), - ...(params.platformOptions.opentelemetry === YesNo.YES - ? [ - { - description: tct( - "When running your application, please add our [code:sentry-opentelemetry-agent] to the [code:java] command. You can download the latest version of the [code:sentry-opentelemetry-agent.jar] from [linkMC:MavenCentral]. It's also available as a [code:ZIP] containing the [code:JAR] used on this page on [linkGH:GitHub].", - { - code: , - linkMC: ( - - ), - linkGH: ( - - ), - } - ), - language: 'bash', - code: getOpenTelemetryRunSnippet(params), - }, - ] - : []), - ], - additionalInfo: tct( - 'If you prefer to manually upload your source code to Sentry, please refer to [link:Manually Uploading Source Context].', { - link: ( - + type: 'conditional', + condition: params.platformOptions.packageManager === PackageManager.GRADLE, + content: [ + { + type: 'text', + text: 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: ( + + ), + } + ), + }, + { + type: 'code', + language: 'groovy', + code: getGradleInstallSnippet(params), + }, + ], + }, + { + type: 'conditional', + condition: params.platformOptions.packageManager === PackageManager.MAVEN, + content: [ + { + type: 'text', + text: tct( + 'The [link:Sentry Maven Plugin] automatically installs the Sentry SDK as well as available integrations for your dependencies. Add the following to your [code:pom.xml] file:', + { + code: , + link: ( + + ), + } + ), + }, + { + type: 'code', + language: 'xml', + code: getMavenInstallSnippet(params), + }, + ], + }, + { + type: 'conditional', + condition: params.platformOptions.packageManager === PackageManager.SBT, + content: [ + { + type: 'text', + text: tct('Add the sentry SDK to your [code:libraryDependencies]:', { + code: , + }), + }, + { + type: 'code', + language: 'scala', + code: `libraryDependencies += "io.sentry" % "sentry" % "${getPackageVersion( + params, + 'sentry.java', + '6.27.0' + )}"`, + }, + ], + }, + { + type: 'conditional', + condition: params.platformOptions.opentelemetry === YesNo.YES, + content: [ + { + type: 'text', + text: tct( + "When running your application, please add our [code:sentry-opentelemetry-agent] to the [code:java] command. You can download the latest version of the [code:sentry-opentelemetry-agent.jar] from [linkMC:MavenCentral]. It's also available as a [code:ZIP] containing the [code:JAR] used on this page on [linkGH:GitHub].", + { + code: , + linkMC: ( + + ), + linkGH: ( + + ), + } + ), + }, + { + type: 'code', + language: 'bash', + code: getOpenTelemetryRunSnippet(params), + }, + ], + }, + { + type: 'text', + text: tct( + 'If you prefer to manually upload your source code to Sentry, please refer to [link:Manually Uploading Source Context].', + { + link: ( + + ), + } ), - } - ), + }, + ], }, ], configure: params => [ params.platformOptions.opentelemetry === YesNo.YES ? { type: StepType.CONFIGURE, - description: tct( - "Here's the [code:sentry.properties] file that goes with the [code:java] command above:", + content: [ { - code: , - } - ), - configurations: [ + type: 'text', + text: tct( + "Here's the [code:sentry.properties] file that goes with the [code:java] command above:", + { + code: , + } + ), + }, { + type: 'code', language: 'java', code: getSentryPropertiesSnippet(params), }, @@ -311,11 +341,15 @@ const onboarding: OnboardingConfig = { } : { type: StepType.CONFIGURE, - description: t( - "Configure Sentry as soon as possible in your application's lifecycle:" - ), - configurations: [ + content: [ + { + type: 'text', + text: t( + "Configure Sentry as soon as possible in your application's lifecycle:" + ), + }, { + type: 'code', language: 'java', code: getConfigureSnippet(params), }, @@ -325,30 +359,32 @@ const onboarding: OnboardingConfig = { verify: () => [ { type: StepType.VERIFY, - description: tct( - 'Trigger your first event from your development environment by intentionally creating an error with the [code:Sentry#captureException] method, to test that everything is working:', - {code: } - ), - configurations: [ + content: [ + { + type: 'text', + text: tct( + 'Trigger your first event from your development environment by intentionally creating an error with the [code:Sentry#captureException] method, to test that everything is working:', + {code: } + ), + }, { + type: 'code', language: 'java', code: getVerifySnippet(), }, + { + type: 'text', + text: t( + "If you're new to Sentry, use the email alert to access your account and complete a product tour." + ), + }, + { + type: 'text', + text: t( + "If you're an existing user and have disabled alerts, you won't receive this email." + ), + }, ], - 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: () => [ diff --git a/static/app/gettingStartedDocs/javascript/react.tsx b/static/app/gettingStartedDocs/javascript/react.tsx index 38462e20cc1a91..225d28381debef 100644 --- a/static/app/gettingStartedDocs/javascript/react.tsx +++ b/static/app/gettingStartedDocs/javascript/react.tsx @@ -1,11 +1,10 @@ -import {Fragment} from 'react'; - import ExternalLink from 'sentry/components/links/externalLink'; import {buildSdkConfig} from 'sentry/components/onboarding/gettingStartedDoc/buildSdkConfig'; import crashReportCallout from 'sentry/components/onboarding/gettingStartedDoc/feedback/crashReportCallout'; import widgetCallout from 'sentry/components/onboarding/gettingStartedDoc/feedback/widgetCallout'; -import TracePropagationMessage from 'sentry/components/onboarding/gettingStartedDoc/replay/tracePropagationMessage'; +import {tracePropagationBlock} from 'sentry/components/onboarding/gettingStartedDoc/replay/tracePropagationMessage'; import type { + ContentBlock, Docs, DocsParams, OnboardingConfig, @@ -22,10 +21,6 @@ import { getFeedbackConfigOptions, getFeedbackConfigureDescription, } from 'sentry/components/onboarding/gettingStartedDoc/utils/feedbackOnboarding'; -import { - getProfilingDocumentHeaderConfigurationStep, - MaybeBrowserProfilingBetaWarning, -} from 'sentry/components/onboarding/gettingStartedDoc/utils/profilingOnboarding'; import { getReplayConfigOptions, getReplayConfigureDescription, @@ -125,9 +120,9 @@ const getVerifySnippet = () => ` return ; `; +// TODO: Remove once the other product areas support content blocks const getInstallConfig = () => [ { - language: 'bash', code: [ { label: 'npm', @@ -151,39 +146,63 @@ const getInstallConfig = () => [ }, ]; +const installSnippetBlock: ContentBlock = { + type: 'code', + tabs: [ + { + label: 'npm', + language: 'bash', + code: 'npm install --save @sentry/react', + }, + { + label: 'yarn', + language: 'bash', + code: 'yarn add @sentry/react', + }, + { + label: 'pnpm', + language: 'bash', + code: 'pnpm add @sentry/react', + }, + ], +}; + const onboarding: OnboardingConfig = { - introduction: params => ( - - -

- {tct( - "In this quick guide you'll use [strong:npm], [strong:yarn], or [strong:pnpm] to set up:", - { - strong: , - } - )} -

-
- ), + introduction: () => + tct( + "In this quick guide you'll use [strong:npm], [strong:yarn], or [strong:pnpm] to set up:", + { + strong: , + } + ), install: () => [ { type: StepType.INSTALL, - description: tct( - 'Add the Sentry SDK as a dependency using [code:npm], [code:yarn], or [code:pnpm]:', - {code: } - ), - configurations: getInstallConfig(), + content: [ + { + type: 'text', + text: tct( + 'Add the Sentry SDK as a dependency using [code:npm], [code:yarn], or [code:pnpm]:', + {code: } + ), + }, + installSnippetBlock, + ], }, ], configure: (params: Params) => [ { type: StepType.CONFIGURE, - description: t( - "Initialize Sentry as early as possible in your application's lifecycle." - ), - configurations: [ + content: [ { - code: [ + type: 'text', + text: t( + "Initialize Sentry as early as possible in your application's lifecycle." + ), + }, + { + type: 'code', + tabs: [ { label: 'JavaScript', value: 'javascript', @@ -191,11 +210,12 @@ const onboarding: OnboardingConfig = { code: getSdkSetupSnippet(params), }, ], - additionalInfo: params.isReplaySelected ? : null, }, - ...(params.isProfilingSelected - ? [getProfilingDocumentHeaderConfigurationStep()] - : []), + { + type: 'conditional', + condition: params.isReplaySelected, + content: [tracePropagationBlock], + }, ], }, getUploadSourceMapsStep({ @@ -332,12 +352,16 @@ logger.fatal("Database connection pool exhausted", { verify: () => [ { type: StepType.VERIFY, - description: t( - "This snippet contains an intentional error and can be used as a test to make sure that everything's working as expected." - ), - configurations: [ + content: [ { - code: [ + type: 'text', + text: t( + "This snippet contains an intentional error and can be used as a test to make sure that everything's working as expected." + ), + }, + { + type: 'code', + tabs: [ { label: 'React', value: 'react', diff --git a/static/app/utils/gettingStartedDocs/getPackageVersion.ts b/static/app/utils/gettingStartedDocs/getPackageVersion.ts index b7e94d9be71661..797640bc5209e7 100644 --- a/static/app/utils/gettingStartedDocs/getPackageVersion.ts +++ b/static/app/utils/gettingStartedDocs/getPackageVersion.ts @@ -1,8 +1,10 @@ import type {DocsParams} from 'sentry/components/onboarding/gettingStartedDoc/types'; import {t} from 'sentry/locale'; +export const PACKAGE_LOADING_PLACEHOLDER = t('loading\u2026'); + export function getPackageVersion(params: DocsParams, name: string, fallback: string) { return params.sourcePackageRegistries.isLoading - ? t('loading\u2026') + ? PACKAGE_LOADING_PLACEHOLDER : (params.sourcePackageRegistries.data?.[name]?.version ?? fallback); } diff --git a/static/app/views/insights/agentMonitoring/views/onboarding.tsx b/static/app/views/insights/agentMonitoring/views/onboarding.tsx index e67da319648930..342b08d00add3c 100644 --- a/static/app/views/insights/agentMonitoring/views/onboarding.tsx +++ b/static/app/views/insights/agentMonitoring/views/onboarding.tsx @@ -8,6 +8,7 @@ import {LinkButton} from 'sentry/components/core/button/linkButton'; import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator'; +import {ContentBlocksRenderer} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/renderer'; import { OnboardingCodeSnippet, TabbedCodeSnippet, @@ -158,7 +159,11 @@ function StepRenderer({ stepKey={step.type || step.title} title={step.title || (step.type && StepTitles[step.type])} > - + {step.content ? ( + + ) : ( + + )} diff --git a/static/app/views/performance/onboarding.tsx b/static/app/views/performance/onboarding.tsx index 1a9ecb942b2891..27acfe43ce7b24 100644 --- a/static/app/views/performance/onboarding.tsx +++ b/static/app/views/performance/onboarding.tsx @@ -27,12 +27,14 @@ import FeatureTourModal, { TourText, } from 'sentry/components/modals/featureTourModal'; import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator'; +import {ContentBlocksRenderer} from 'sentry/components/onboarding/gettingStartedDoc/contentBlocks/renderer'; import { OnboardingCodeSnippet, TabbedCodeSnippet, } from 'sentry/components/onboarding/gettingStartedDoc/onboardingCodeSnippet'; import type { Configuration, + ContentBlock, DocsParams, } from 'sentry/components/onboarding/gettingStartedDoc/types'; import {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types'; @@ -369,6 +371,19 @@ function WaitingIndicator({ ); } +function RenderBlocksOrFallback({ + contentBlocks, + children, +}: { + children: React.ReactNode; + contentBlocks?: ContentBlock[]; +}) { + if (contentBlocks && contentBlocks.length > 0) { + return ; + } + return children; +} + type ConfigurationStepProps = { api: Client; configuration: Configuration; @@ -377,6 +392,7 @@ type ConfigurationStepProps = { showWaitingIndicator: boolean; stepKey: string; title: React.ReactNode; + contentBlocks?: ContentBlock[]; }; function ConfigurationStep({ @@ -385,13 +401,14 @@ function ConfigurationStep({ api, organization, project, + contentBlocks, configuration, showWaitingIndicator, }: ConfigurationStepProps) { return (
-
+ {configuration.description} {configuration.code ? ( @@ -415,7 +432,7 @@ function ConfigurationStep({ {showWaitingIndicator ? ( ) : null} -
+ @@ -673,7 +690,7 @@ export function Onboarding({organization, project}: OnboardingProps) { >
-
+ {installStep.description} {installStep.configurations?.map((configuration, index) => (
@@ -694,7 +711,7 @@ export function Onboarding({organization, project}: OnboardingProps) { {!configureStep.configurations && !verifyStep.configurations ? eventWaitingIndicator : null} -
+
@@ -727,7 +744,7 @@ export function Onboarding({organization, project}: OnboardingProps) { ) : null} {verifyStep.configurations || verifyStep.description ? ( -
+ {verifyStep.description} {verifyStep.configurations?.map((configuration, index) => (
@@ -746,7 +763,7 @@ export function Onboarding({organization, project}: OnboardingProps) {
))} {eventWaitingIndicator} -
+ {received ? (