import {Fragment, useCallback, useEffect, useMemo} from 'react'; import styled from '@emotion/styled'; import type {Location} from 'history'; import {Alert} from 'sentry/components/alert'; import {Button, LinkButton} from 'sentry/components/button'; import SearchBar from 'sentry/components/events/searchBar'; import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton'; import * as Layout from 'sentry/components/layouts/thirds'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter'; import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip'; import Pagination from 'sentry/components/pagination'; import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder'; import { ProfilingAM1OrMMXUpgrade, ProfilingBetaAlertBanner, ProfilingUpgradeButton, } from 'sentry/components/profiling/billing/alerts'; import {ProfileEventsTable} from 'sentry/components/profiling/profileEventsTable'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {SidebarPanelKey} from 'sentry/components/sidebar/types'; import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar'; import {TabList, Tabs} from 'sentry/components/tabs'; import {MAX_QUERY_LENGTH} from 'sentry/constants'; import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters'; import {t} from 'sentry/locale'; import SidebarPanelStore from 'sentry/stores/sidebarPanelStore'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import {browserHistory} from 'sentry/utils/browserHistory'; import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents'; import {formatError, formatSort} from 'sentry/utils/profiling/hooks/utils'; import {decodeScalar} from 'sentry/utils/queryString'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; import { TableHeader, TableHeaderActions, TableHeaderTitle, } from 'sentry/views/explore/components/table'; import {LandingAggregateFlamegraph} from 'sentry/views/profiling/landingAggregateFlamegraph'; import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils'; import {LandingWidgetSelector} from './landing/landingWidgetSelector'; import {ProfilesChart} from './landing/profileCharts'; import {ProfilesChartWidget} from './landing/profilesChartWidget'; import {ProfilingSlowestTransactionsPanel} from './landing/profilingSlowestTransactionsPanel'; import {SlowestFunctionsTable} from './landing/slowestFunctionsTable'; import {ProfilingOnboardingPanel} from './profilingOnboardingPanel'; const LEFT_WIDGET_CURSOR = 'leftCursor'; const RIGHT_WIDGET_CURSOR = 'rightCursor'; const CURSOR_PARAMS = [LEFT_WIDGET_CURSOR, RIGHT_WIDGET_CURSOR]; interface ProfilingContentProps { location: Location; } function ProfilingContentLegacy({location}: ProfilingContentProps) { const organization = useOrganization(); const {selection} = usePageFilters(); const cursor = decodeScalar(location.query.cursor); const query = decodeScalar(location.query.query, ''); const fields = ALL_FIELDS; const sort = formatSort(decodeScalar(location.query.sort), fields, { key: 'count()', order: 'desc', }); const {projects} = useProjects(); const transactions = useProfileEvents({ cursor, fields, query, sort, referrer: 'api.profiling.landing-table', }); const transactionsError = transactions.status === 'error' ? formatError(transactions.error) : null; useEffect(() => { trackAnalytics('profiling_views.landing', { organization, }); }, [organization]); const handleSearch: SmartSearchBarProps['onSearch'] = useCallback( (searchQuery: string) => { browserHistory.push({ ...location, query: { ...location.query, cursor: undefined, query: searchQuery || undefined, }, }); }, [location] ); // Open the modal on demand const onSetupProfilingClick = useCallback(() => { trackAnalytics('profiling_views.onboarding', { organization, }); SidebarPanelStore.activatePanel(SidebarPanelKey.PROFILING_ONBOARDING); }, [organization]); const shouldShowProfilingOnboardingPanel = useMemo((): boolean => { // if it's My Projects or All projects, only show onboarding if we can't // find any projects with profiles if ( selection.projects.length === 0 || selection.projects[0] === ALL_ACCESS_PROJECTS ) { return projects.every(project => !project.hasProfiles); } // otherwise, only show onboarding if we can't find any projects with profiles // from those that were selected const projectsWithProfiles = new Set( projects.filter(project => project.hasProfiles).map(project => project.id) ); return selection.projects.every( project => !projectsWithProfiles.has(String(project)) ); }, [selection.projects, projects]); return ( {t('Profiling')} {transactionsError && ( {transactionsError} )} {organization.features.includes('search-query-builder-performance') ? ( ) : ( )} {shouldShowProfilingOnboardingPanel ? (

{t('Function level insights')}

{t( 'Discover slow-to-execute or resource intensive functions within your application' )}

} /> } > {t('Set Up Profiling')} } > {t('Set Up Profiling')} {t('Read Docs')} ) : ( {organization.features.includes( 'profiling-global-suspect-functions' ) ? ( ) : ( )} )}
); } function validateTab(tab: unknown): tab is 'flamegraph' | 'transactions' { return tab === 'flamegraph' || tab === 'transactions'; } function decodeTab(tab: unknown): 'flamegraph' | 'transactions' { // Fallback to transactions if tab is invalid. We default to transactions // because that is going to be the most common perf setup when we release. return validateTab(tab) ? tab : 'transactions'; } function ProfilingContent({location}: ProfilingContentProps) { const organization = useOrganization(); const {selection} = usePageFilters(); const {projects} = useProjects(); const tab = decodeTab(location.query.tab); useEffect(() => { trackAnalytics('profiling_views.landing', { organization, }); }, [organization]); const onTabChange = useCallback( (newTab: 'flamegraph' | 'transactions') => { browserHistory.push({ ...location, query: { ...location.query, tab: newTab, }, }); }, [location] ); const shouldShowProfilingOnboardingPanel = useMemo((): boolean => { // if it's My Projects or All projects, only show onboarding if we can't // find any projects with profiles if ( selection.projects.length === 0 || selection.projects[0] === ALL_ACCESS_PROJECTS ) { return projects.every(project => !project.hasProfiles); } // otherwise, only show onboarding if we can't find any projects with profiles // from those that were selected const projectsWithProfiles = new Set( projects.filter(project => project.hasProfiles).map(project => project.id) ); return selection.projects.every( project => !projectsWithProfiles.has(String(project)) ); }, [selection.projects, projects]); return ( {tab === 'flamegraph' ? ( ) : tab === 'transactions' ? ( ) : null} ); } interface ProfilingTabContentProps { shouldShowProfilingOnboardingPanel: boolean; tab: 'flamegraph' | 'transactions'; } function ProfilingFlamegraphTabContent(props: ProfilingTabContentProps) { return ( {props.shouldShowProfilingOnboardingPanel ? ( ) : ( )} ); } function ProfilingTransactionsContent(props: ProfilingTabContentProps) { const organization = useOrganization(); const location = useLocation(); const {selection} = usePageFilters(); const fields = ALL_FIELDS; const sort = formatSort(decodeScalar(location.query.sort), fields, { key: 'count()', order: 'desc', }); const cursor = decodeScalar(location.query.cursor); const query = decodeScalar(location.query.query, ''); const transactions = useProfileEvents({ cursor, fields, query, sort, referrer: 'api.profiling.landing-table', continuousProfilingCompat: true, }); const transactionsError = transactions.status === 'error' ? formatError(transactions.error) : null; const handleSearch: SmartSearchBarProps['onSearch'] = useCallback( (searchQuery: string) => { browserHistory.push({ ...location, query: { ...location.query, cursor: undefined, query: searchQuery || undefined, }, }); }, [location] ); return ( {transactionsError && ( {transactionsError} )} {organization.features.includes('search-query-builder-performance') ? ( ) : ( )} {props.shouldShowProfilingOnboardingPanel ? ( ) : ( {organization.features.includes('continuous-profiling-ui') ? ( ) : organization.features.includes('profiling-global-suspect-functions') ? ( ) : ( )} {t('Transactions')} )} ); } function ProfilingOnboardingCTA() { const organization = useOrganization(); // Open the modal on demand const onSetupProfilingClick = useCallback(() => { trackAnalytics('profiling_views.onboarding', { organization, }); SidebarPanelStore.activatePanel(SidebarPanelKey.PROFILING_ONBOARDING); }, [organization]); return (

{t('Function level insights')}

{t( 'Discover slow-to-execute or resource intensive functions within your application' )}

} /> } > {t('Set Up Profiling')} } > {t('Set Up Profiling')} {t('Read Docs')} ); } interface ProfilingContentPageHeaderProps { onTabChange: (newTab: 'flamegraph' | 'transactions') => void; tab: 'flamegraph' | 'transactions'; } function ProfilingContentPageHeader(props: ProfilingContentPageHeaderProps) { return ( {t('Profiling')}
{t('Transactions')} {t('Flamegraph')}
); } const ALL_FIELDS = [ 'transaction', 'project.id', 'last_seen()', 'p50()', 'p75()', 'p95()', 'p99()', 'count()', ] as const; type FieldType = (typeof ALL_FIELDS)[number]; const FlamegraphBody = styled(Layout.Body)` display: grid; grid-template-rows: 1fr; `; const FlamegraphMainLayout = styled(Layout.Main)` display: grid; grid-column: 1 / -1; grid-template-rows: min-content 1fr; `; const FlamegraphLayout = styled('div')` display: grid; grid-template-areas: 'flamegraph sidebar'; grid-template-columns: 1fr min-content; margin-top: ${space(2)}; `; const FlamegraphActionBar = styled('div')``; const FlamegraphSidebar = styled('div')` grid-area: sidebar; `; const LandingAggregateFlamegraphContainer = styled('div')` height: 100%; min-height: 300px; position: relative; border: 1px solid ${p => p.theme.border}; border-radius: ${p => p.theme.borderRadius}; margin-bottom: ${space(2)}; grid-area: flamegraph; `; const StyledLayoutHeader = styled(Layout.Header)` display: block; `; const StyledHeaderContent = styled(Layout.HeaderContent)` display: flex; align-items: center; justify-content: space-between; flex-direction: row; `; const ActionBar = styled('div')` display: grid; gap: ${space(2)}; grid-template-columns: min-content auto; margin-bottom: ${space(2)}; `; // TODO: another simple primitive that can easily be const PanelsGrid = styled('div')` display: grid; grid-template-columns: minmax(0, 1fr) 1fr; gap: ${space(2)}; @media (max-width: ${p => p.theme.breakpoints.small}) { grid-template-columns: minmax(0, 1fr); } `; const WidgetsContainer = styled('div')` display: grid; grid-template-columns: 1fr 1fr; gap: ${space(2)}; @media (max-width: ${p => p.theme.breakpoints.small}) { grid-template-columns: 1fr; } `; const StyledPagination = styled(Pagination)` margin: 0; `; function ProfilingContentWrapper(props: ProfilingContentProps) { const organization = useOrganization(); if (organization.features.includes('continuous-profiling-compat')) { return ; } return ; } export default ProfilingContentWrapper;