diff --git a/static/app/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted.tsx b/static/app/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted.tsx index c961296601dd78..f52f480e9dc623 100644 --- a/static/app/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted.tsx +++ b/static/app/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted.tsx @@ -35,12 +35,12 @@ export function useLoadGettingStarted({ ); const projectKeys = useProjectKeys({orgSlug, projSlug}); - const platformPath = getPlatformPath(platform); useEffect(() => { async function getGettingStartedDoc() { if ( + !platformPath || (productType === 'replay' && !replayPlatforms.includes(platform.id)) || (productType === 'feedback' && !feedbackOnboardingPlatforms.includes(platform.id)) ) { diff --git a/static/app/components/profiling/ProfilingOnboarding/profilingOnboardingSidebar.tsx b/static/app/components/profiling/ProfilingOnboarding/profilingOnboardingSidebar.tsx deleted file mode 100644 index 4cb3bc002598a8..00000000000000 --- a/static/app/components/profiling/ProfilingOnboarding/profilingOnboardingSidebar.tsx +++ /dev/null @@ -1,371 +0,0 @@ -import {useEffect, useMemo, useState} from 'react'; -import styled from '@emotion/styled'; - -import {Button} from 'sentry/components/button'; -import {CompactSelect} from 'sentry/components/compactSelect'; -import IdBadge from 'sentry/components/idBadge'; -import LoadingIndicator from 'sentry/components/loadingIndicator'; -import useOnboardingDocs from 'sentry/components/onboardingWizard/useOnboardingDocs'; -import { - DocumentationWrapper, - OnboardingStep, -} from 'sentry/components/sidebar/onboardingStep'; -import { - EventIndicator, - TaskSidebar, - TaskSidebarList, -} from 'sentry/components/sidebar/taskSidebar'; -import type {CommonSidebarProps} from 'sentry/components/sidebar/types'; -import {SidebarPanelKey} from 'sentry/components/sidebar/types'; -import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters'; -import platforms from 'sentry/data/platforms'; -import {t, tct} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; -import type {Project, SelectValue} from 'sentry/types'; -import {trackAnalytics} from 'sentry/utils/analytics'; -import EventWaiter from 'sentry/utils/eventWaiter'; -import useApi from 'sentry/utils/useApi'; -import useOrganization from 'sentry/utils/useOrganization'; -import usePageFilters from 'sentry/utils/usePageFilters'; -import usePrevious from 'sentry/utils/usePrevious'; -import useProjects from 'sentry/utils/useProjects'; - -import {makeDocKeyMap, splitProjectsByProfilingSupport} from './util'; - -export function ProfilingOnboardingSidebar(props: CommonSidebarProps) { - const {currentPanel, collapsed, hidePanel, orientation} = props; - const isActive = currentPanel === SidebarPanelKey.PROFILING_ONBOARDING; - const organization = useOrganization(); - const hasProjectAccess = organization.access.includes('project:read'); - - const {projects} = useProjects(); - - const [currentProject, setCurrentProject] = useState(); - const pageFilters = usePageFilters(); - - const {supported: supportedProjects, unsupported: unsupportedProjects} = useMemo( - () => splitProjectsByProfilingSupport(projects), - [projects] - ); - - useEffect(() => { - // we'll only ever select an unsupportedProject if they do not have a supported project in their organization - if (supportedProjects.length === 0 && unsupportedProjects.length > 0) { - if (pageFilters.selection.projects[0] === ALL_ACCESS_PROJECTS) { - setCurrentProject(unsupportedProjects[0]); - return; - } - - setCurrentProject( - // there's an edge case where an org w/ a single project may be unsupported but for whatever reason there is no project selection so we can't select a project - // in those cases we'll simply default to the first unsupportedProject - unsupportedProjects.find( - p => p.id === String(pageFilters.selection.projects[0]) - ) ?? unsupportedProjects[0] - ); - return; - } - // if it's My Projects or All Projects, pick the first supported project - if ( - pageFilters.selection.projects.length === 0 || - pageFilters.selection.projects[0] === ALL_ACCESS_PROJECTS - ) { - setCurrentProject(supportedProjects[0]); - return; - } - - // if it's a list of projects, pick the first one that's supported - const supportedProjectsById = supportedProjects.reduce((mapping, project) => { - mapping[project.id] = project; - return mapping; - }, {}); - - for (const projectId of pageFilters.selection.projects) { - if (supportedProjectsById[String(projectId)]) { - setCurrentProject(supportedProjectsById[String(projectId)]); - return; - } - } - }, [ - pageFilters.selection.projects, - currentProject, - supportedProjects, - unsupportedProjects, - ]); - - const projectSelectOptions = useMemo(() => { - const supportedProjectItems: SelectValue[] = supportedProjects.map( - project => { - return { - value: project.id, - textValue: project.id, - label: ( - - ), - }; - } - ); - - const unsupportedProjectItems: SelectValue[] = unsupportedProjects.map( - project => { - return { - value: project.id, - textValue: project.id, - label: ( - - ), - disabled: true, - }; - } - ); - return [ - { - label: t('Supported'), - options: supportedProjectItems, - }, - { - label: t('Unsupported'), - options: unsupportedProjectItems, - }, - ]; - }, [supportedProjects, unsupportedProjects]); - - if (!isActive || !hasProjectAccess) { - return null; - } - - return ( - { - trackAnalytics('profiling_views.onboarding_action', { - organization, - action: 'dismissed', - }); - hidePanel(); - }} - > - - {t('Profile Code')} -
{ - // we need to stop bubbling the CompactSelect click event - // failing to do so will cause the sidebar panel to close - // the event.target will be unmounted by the time the panel listener - // receives the event and assume the click was outside the panel - e.stopPropagation(); - }} - > - - ) : ( - t('Select a project') - ) - } - value={currentProject?.id} - onChange={opt => setCurrentProject(projects.find(p => p.id === opt.value))} - triggerProps={{'aria-label': currentProject?.slug}} - options={projectSelectOptions} - position="bottom-end" - /> -
- {currentProject && ( - - )} -
-
- ); -} - -function OnboardingContent({ - currentProject, - isSupported, -}: { - currentProject: Project; - isSupported: boolean; -}) { - const currentPlatform = platforms.find(p => p.id === currentProject?.platform); - const api = useApi(); - const organization = useOrganization(); - const [received, setReceived] = useState(false); - const previousProject = usePrevious(currentProject); - useEffect(() => { - if (!currentProject || !previousProject) { - return; - } - if (previousProject.id !== currentProject.id) { - setReceived(false); - } - }, [currentProject, previousProject]); - - const docKeysMap = useMemo(() => makeDocKeyMap(currentPlatform?.id), [currentPlatform]); - const docKeys = useMemo( - () => (docKeysMap ? Object.values(docKeysMap) : []), - [docKeysMap] - ); - - const {docContents, isLoading, hasOnboardingContents} = useOnboardingDocs({ - docKeys, - project: currentProject, - isPlatformSupported: isSupported, - }); - - if (isLoading) { - return ; - } - - if (!currentPlatform) { - return ( - -

- {t( - `Your project's platform has not been set. Please select your project's platform before proceeding.` - )} -

- -
- ); - } - - if (!isSupported) { - // this content will only be presented if the org only has one project and its not supported - // in these scenarios we will auto-select the unsupported project and render this message - return ( - -

- {tct( - 'Fiddlesticks. Profiling isn’t available for your [platform] project yet. Reach out to us on Discord for more information.', - {platform: currentPlatform?.name || currentProject.slug} - )} -

- -
- ); - } - - if (!docKeysMap || !hasOnboardingContents) { - return ( - -

- {tct( - 'Fiddlesticks. This checklist isn’t available for your [project] project yet, but for now, go to Sentry docs for installation details.', - {project: currentProject.slug} - )} -

- -
- ); - } - - const alertContent = docContents[docKeysMap['0-alert']]; - - return ( - - {alertContent && ( - - )} -

- {t( - `Adding Profiling to your %s project is simple. Make sure you've got these basics down.`, - currentPlatform!.name - )} -

- {Object.entries(docKeysMap).map(entry => { - const [key, docKey] = entry; - if (key === '0-alert') { - return null; - } - - const content = docContents[docKey]; - if (!content) { - return null; - } - return ( -
- -
- ); - })} - { - trackAnalytics('profiling_views.onboarding_action', { - organization, - action: 'done', - }); - setReceived(true); - }} - > - {() => (received ? : )} - -
- ); -} - -function EventReceivedIndicator() { - return ( - - {t("We've received this project's first profile!")} - - ); -} - -function EventWaitingIndicator() { - return ( - - {t("Waiting for this project's first profile.")} - - ); -} - -const Heading = styled('div')` - display: flex; - color: ${p => p.theme.activeText}; - font-size: ${p => p.theme.fontSizeExtraSmall}; - text-transform: uppercase; - font-weight: ${p => p.theme.fontWeightBold}; - line-height: 1; - margin-top: ${space(3)}; -`; - -const StyledIdBadge = styled(IdBadge)` - overflow: hidden; - white-space: nowrap; - flex-shrink: 1; -`; - -const ContentContainer = styled('div')` - margin: ${space(2)} 0; -`; diff --git a/static/app/components/profiling/ProfilingOnboarding/util.ts b/static/app/components/profiling/ProfilingOnboarding/util.ts deleted file mode 100644 index 705e427b733e43..00000000000000 --- a/static/app/components/profiling/ProfilingOnboarding/util.ts +++ /dev/null @@ -1,118 +0,0 @@ -import partition from 'lodash/partition'; - -import type {PlatformKey, Project} from 'sentry/types/project'; -import type {SupportedProfilingPlatformSDK} from 'sentry/utils/profiling/platforms'; -import {getDocsPlatformSDKForPlatform} from 'sentry/utils/profiling/platforms'; - -export const profilingOnboardingDocKeys = [ - '0-alert', - '1-install', - '2-configure-performance', - '3-configure-profiling', - '4-upload', -] as const; - -export const browserProfilingOnboardingDocKeysWithDocumentPolicy = [ - '1-install', - '2-configure-document-policy', - '3-configure', -] as const; - -type ProfilingOnboardingDocKeys = (typeof profilingOnboardingDocKeys)[number]; -type BrowserProfilingOnboardingDocKeys = - (typeof browserProfilingOnboardingDocKeysWithDocumentPolicy)[number]; - -export const supportedPlatformExpectedDocKeys: Record< - SupportedProfilingPlatformSDK, - ProfilingOnboardingDocKeys[] | BrowserProfilingOnboardingDocKeys[] -> = { - android: ['1-install', '2-configure-performance', '3-configure-profiling', '4-upload'], - 'apple-ios': [ - '1-install', - '2-configure-performance', - '3-configure-profiling', - '4-upload', - ], - go: ['0-alert', '1-install', '2-configure-performance', '3-configure-profiling'], - node: ['1-install', '2-configure-performance', '3-configure-profiling'], - python: ['1-install', '2-configure-performance', '3-configure-profiling'], - php: ['1-install', '2-configure-performance', '3-configure-profiling'], - 'php-laravel': ['1-install', '2-configure-performance', '3-configure-profiling'], - 'php-symfony2': ['1-install', '2-configure-performance', '3-configure-profiling'], - ruby: ['0-alert', '1-install', '2-configure-performance', '3-configure-profiling'], - 'javascript-nextjs': ['1-install', '2-configure-performance', '3-configure-profiling'], - 'javascript-remix': ['1-install', '2-configure-performance', '3-configure-profiling'], - 'javascript-sveltekit': [ - '1-install', - '2-configure-performance', - '3-configure-profiling', - ], - javascript: ['1-install', '2-configure-document-policy', '3-configure'], - 'javascript-react': ['1-install', '2-configure-document-policy', '3-configure'], - 'javascript-angular': ['1-install', '2-configure-document-policy', '3-configure'], - 'javascript-vue': ['1-install', '2-configure-document-policy', '3-configure'], - 'react-native': [ - '0-alert', - '1-install', - '2-configure-performance', - '3-configure-profiling', - ], - flutter: ['0-alert', '1-install', '2-configure-performance', '3-configure-profiling'], - 'dart-flutter': [ - '0-alert', - '1-install', - '2-configure-performance', - '3-configure-profiling', - ], -}; - -function makeDocKey(platformId: SupportedProfilingPlatformSDK, key: string) { - if (platformId === 'javascript-nextjs') { - return `node-javascript-nextjs-profiling-onboarding-${key}`; - } - if (platformId === 'javascript-remix') { - return `node-javascript-remix-profiling-onboarding-${key}`; - } - if (platformId === 'javascript-sveltekit') { - return `node-javascript-sveltekit-profiling-onboarding-${key}`; - } - return `${platformId}-profiling-onboarding-${key}`; -} - -type DocKeyMap = Record< - (ProfilingOnboardingDocKeys | BrowserProfilingOnboardingDocKeys)[number], - string ->; -export function makeDocKeyMap(platformId: PlatformKey | undefined) { - const docsPlatform = getDocsPlatformSDKForPlatform(platformId); - - if (!platformId || !docsPlatform) { - return null; - } - - const expectedDocKeys: ( - | ProfilingOnboardingDocKeys - | BrowserProfilingOnboardingDocKeys - )[] = supportedPlatformExpectedDocKeys[docsPlatform]; - - if (!expectedDocKeys) { - return null; - } - - return expectedDocKeys.reduce((acc: DocKeyMap, key) => { - acc[key] = makeDocKey(docsPlatform, key); - return acc; - }, {} as DocKeyMap); -} - -export function splitProjectsByProfilingSupport(projects: Project[]): { - supported: Project[]; - unsupported: Project[]; -} { - const [supported, unsupported] = partition( - projects, - project => project.platform && getDocsPlatformSDKForPlatform(project.platform) - ); - - return {supported, unsupported}; -} diff --git a/static/app/components/profiling/profilingOnboardingSidebar.tsx b/static/app/components/profiling/profilingOnboardingSidebar.tsx new file mode 100644 index 00000000000000..8f6adba227fe3f --- /dev/null +++ b/static/app/components/profiling/profilingOnboardingSidebar.tsx @@ -0,0 +1,215 @@ +import {useEffect, useMemo, useState} from 'react'; +import styled from '@emotion/styled'; +import partition from 'lodash/partition'; + +import {CompactSelect} from 'sentry/components/compactSelect'; +import IdBadge from 'sentry/components/idBadge'; +import {SdkDocumentation} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation'; +import {ProductSolution} from 'sentry/components/onboarding/productSelection'; +import {TaskSidebar} from 'sentry/components/sidebar/taskSidebar'; +import type {CommonSidebarProps} from 'sentry/components/sidebar/types'; +import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters'; +import platforms from 'sentry/data/platforms'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import type {SelectValue} from 'sentry/types/core'; +import type {Project} from 'sentry/types/project'; +import {trackAnalytics} from 'sentry/utils/analytics'; +import {getDocsPlatformSDKForPlatform} from 'sentry/utils/profiling/platforms'; +import useOrganization from 'sentry/utils/useOrganization'; +import usePageFilters from 'sentry/utils/usePageFilters'; +import useProjects from 'sentry/utils/useProjects'; + +function splitProjectsByProfilingSupport(projects: Project[]): { + supported: Project[]; + unsupported: Project[]; +} { + const [supported, unsupported] = partition( + projects, + project => project.platform && getDocsPlatformSDKForPlatform(project.platform) + ); + + return {supported, unsupported}; +} + +const PROFILING_ONBOARDING_STEPS = [ + ProductSolution.PERFORMANCE_MONITORING, + ProductSolution.PROFILING, +]; + +export function ProfilingOnboardingSidebar(props: CommonSidebarProps) { + const pageFilters = usePageFilters(); + const organization = useOrganization(); + const {projects} = useProjects(); + + const [currentProject, setCurrentProject] = useState(); + + const {supported: supportedProjects, unsupported: unsupportedProjects} = useMemo( + () => splitProjectsByProfilingSupport(projects), + [projects] + ); + + useEffect(() => { + if (currentProject) return; + + // we'll only ever select an unsupportedProject if they do not have a supported project in their organization + if (supportedProjects.length === 0 && unsupportedProjects.length > 0) { + if (pageFilters.selection.projects[0] === ALL_ACCESS_PROJECTS) { + setCurrentProject(unsupportedProjects[0]); + return; + } + + setCurrentProject( + // there's an edge case where an org w/ a single project may be unsupported but for whatever reason there is no project selection so we can't select a project + // in those cases we'll simply default to the first unsupportedProject + unsupportedProjects.find( + p => p.id === String(pageFilters.selection.projects[0]) + ) ?? unsupportedProjects[0] + ); + return; + } + // if it's My Projects or All Projects, pick the first supported project + if ( + pageFilters.selection.projects.length === 0 || + pageFilters.selection.projects[0] === ALL_ACCESS_PROJECTS + ) { + setCurrentProject(supportedProjects[0]); + return; + } + + // if it's a list of projects, pick the first one that's supported + const supportedProjectsById = supportedProjects.reduce((mapping, project) => { + mapping[project.id] = project; + return mapping; + }, {}); + + for (const projectId of pageFilters.selection.projects) { + if (supportedProjectsById[String(projectId)]) { + setCurrentProject(supportedProjectsById[String(projectId)]); + return; + } + } + }, [ + currentProject, + pageFilters.selection.projects, + supportedProjects, + unsupportedProjects, + ]); + + const projectSelectOptions = useMemo(() => { + const supportedProjectItems: SelectValue[] = supportedProjects.map( + project => { + return { + value: project.id, + textValue: project.id, + label: ( + + ), + }; + } + ); + + const unsupportedProjectItems: SelectValue[] = unsupportedProjects.map( + project => { + return { + value: project.id, + textValue: project.id, + label: ( + + ), + disabled: true, + }; + } + ); + return [ + { + label: t('Supported'), + options: supportedProjectItems, + }, + { + label: t('Unsupported'), + options: unsupportedProjectItems, + }, + ]; + }, [supportedProjects, unsupportedProjects]); + + const currentPlatform = currentProject?.platform + ? platforms.find(p => p.id === currentProject.platform) + : undefined; + + return ( + { + trackAnalytics('profiling_views.onboarding_action', { + organization, + action: 'dismissed', + }); + props.hidePanel(); + }} + > + + {t('Profile Code')} +
{ + // we need to stop bubbling the CompactSelect click event + // failing to do so will cause the sidebar panel to close + // the event.target will be unmounted by the time the panel listener + // receives the event and assume the click was outside the panel + e.stopPropagation(); + }} + > + + ) : ( + t('Select a project') + ) + } + value={currentProject?.id} + onChange={opt => setCurrentProject(projects.find(p => p.id === opt.value))} + triggerProps={{'aria-label': currentProject?.slug}} + options={projectSelectOptions} + position="bottom-end" + /> +
+ {currentProject && currentPlatform ? ( + + ) : null} +
+
+ ); +} + +const Content = styled('div')` + padding: ${space(2)}; +`; + +const Heading = styled('div')` + display: flex; + color: ${p => p.theme.activeText}; + font-size: ${p => p.theme.fontSizeExtraSmall}; + text-transform: uppercase; + font-weight: ${p => p.theme.fontWeightBold}; + line-height: 1; + margin-top: ${space(3)}; +`; + +const StyledIdBadge = styled(IdBadge)` + overflow: hidden; + white-space: nowrap; + flex-shrink: 1; +`; diff --git a/static/app/components/sidebar/index.tsx b/static/app/components/sidebar/index.tsx index 8813daf74a4aa2..c52fe3e1b73f24 100644 --- a/static/app/components/sidebar/index.tsx +++ b/static/app/components/sidebar/index.tsx @@ -56,7 +56,7 @@ import {MODULE_SIDEBAR_TITLE as HTTP_MODULE_SIDEBAR_TITLE} from 'sentry/views/in import {MODULE_TITLES} from 'sentry/views/insights/settings'; import MetricsOnboardingSidebar from 'sentry/views/metrics/ddmOnboarding/sidebar'; -import {ProfilingOnboardingSidebar} from '../profiling/ProfilingOnboarding/profilingOnboardingSidebar'; +import {ProfilingOnboardingSidebar} from '../profiling/profilingOnboardingSidebar'; import Broadcasts from './broadcasts'; import SidebarHelp from './help';