import type {ReactNode} from 'react'; import {Fragment, useCallback, useEffect, useMemo, useState} from 'react'; import {browserHistory} from 'react-router'; import type {Theme} from '@emotion/react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import partition from 'lodash/partition'; import {Button} from 'sentry/components/button'; import ChartZoom from 'sentry/components/charts/chartZoom'; import {LineChart} from 'sentry/components/charts/lineChart'; import Count from 'sentry/components/count'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import IdBadge from 'sentry/components/idBadge'; import Link from 'sentry/components/links/link'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import type {CursorHandler} from 'sentry/components/pagination'; import Pagination from 'sentry/components/pagination'; import PerformanceDuration from 'sentry/components/performanceDuration'; import TextOverflow from 'sentry/components/textOverflow'; import {Tooltip} from 'sentry/components/tooltip'; import {IconArrow, IconChevron, IconWarning} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Series} from 'sentry/types/echarts'; import {trackAnalytics} from 'sentry/utils/analytics'; import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts'; import type {FunctionTrend, TrendType} from 'sentry/utils/profiling/hooks/types'; import {useProfileFunctionTrends} from 'sentry/utils/profiling/hooks/useProfileFunctionTrends'; import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes'; 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 useRouter from 'sentry/utils/useRouter'; import { Accordion, AccordionItem, ContentContainer, HeaderContainer, HeaderTitleLegend, StatusContainer, Subtitle, WidgetContainer, } from './styles'; const MAX_FUNCTIONS = 3; const DEFAULT_CURSOR_NAME = 'fnTrendCursor'; interface FunctionTrendsWidgetProps { trendFunction: 'p50()' | 'p75()' | 'p95()' | 'p99()'; trendType: TrendType; cursorName?: string; header?: ReactNode; userQuery?: string; widgetHeight?: string; } export function FunctionTrendsWidget({ cursorName = DEFAULT_CURSOR_NAME, header, trendFunction, trendType, widgetHeight, userQuery, }: FunctionTrendsWidgetProps) { const location = useLocation(); const [expandedIndex, setExpandedIndex] = useState(0); const fnTrendCursor = useMemo( () => decodeScalar(location.query[cursorName]), [cursorName, location.query] ); const handleCursor = useCallback( (cursor, pathname, query) => { browserHistory.push({ pathname, query: {...query, [cursorName]: cursor}, }); }, [cursorName] ); const trendsQuery = useProfileFunctionTrends({ trendFunction, trendType, query: userQuery, limit: MAX_FUNCTIONS, cursor: fnTrendCursor, }); useEffect(() => { setExpandedIndex(0); }, [trendsQuery.data]); const hasTrends = (trendsQuery.data?.length || 0) > 0; const isLoading = trendsQuery.isLoading; const isError = trendsQuery.isError; return ( {isLoading && ( )} {isError && ( )} {!isError && !isLoading && !hasTrends && ( {trendType === 'regression' ? (

{t('No regressed functions detected')}

) : (

{t('No improved functions detected')}

)}
)} {hasTrends && ( {(trendsQuery.data ?? []).map((f, i, l) => { return ( { const nextIndex = expandedIndex !== i ? i : (i + 1) % l.length; setExpandedIndex(nextIndex); }} func={f} /> ); })} )}
); } interface FunctionTrendsWidgetHeaderProps { handleCursor: CursorHandler; header: ReactNode; pageLinks: string | null; trendType: TrendType; } function FunctionTrendsWidgetHeader({ handleCursor, header, pageLinks, trendType, }: FunctionTrendsWidgetHeaderProps) { switch (trendType) { case 'regression': return ( {header ?? ( {t('Most Regressed Functions')} )} {t('Functions by most regressed.')} ); case 'improvement': return ( {header ?? ( {t('Most Improved Functions')} )} {t('Functions by most improved.')} ); default: throw new Error(t('Unknown trend type')); } } interface FunctionTrendsEntryProps { func: FunctionTrend; isExpanded: boolean; setExpanded: () => void; trendFunction: string; trendType: TrendType; } function FunctionTrendsEntry({ func, isExpanded, setExpanded, trendFunction, trendType, }: FunctionTrendsEntryProps) { const organization = useOrganization(); const {projects} = useProjects(); const project = projects.find(p => p.id === func.project); const [beforeExamples, afterExamples] = useMemo(() => { return partition(func.worst, ([ts, _example]) => ts <= func.breakpoint); }, [func]); let before = ; let after = ; function handleGoToProfile() { switch (trendType) { case 'improvement': trackAnalytics('profiling_views.go_to_flamegraph', { organization, source: 'profiling.function_trends.improvement', }); break; case 'regression': trackAnalytics('profiling_views.go_to_flamegraph', { organization, source: 'profiling.function_trends.regression', }); break; default: throw new Error('Unknown trend type'); } } if (project && beforeExamples.length >= 2 && afterExamples.length >= 2) { // By choosing the 2nd most recent example in each period, we guarantee the example // occurred within the period and eliminate confusion with picking an example in // the same bucket as the breakpoint. const beforeTarget = generateProfileFlamechartRouteWithQuery({ orgSlug: organization.slug, projectSlug: project.slug, profileId: beforeExamples[beforeExamples.length - 2][1], query: { frameName: func.function as string, framePackage: func.package as string, }, }); before = ( {before} ); const afterTarget = generateProfileFlamechartRouteWithQuery({ orgSlug: organization.slug, projectSlug: project.slug, profileId: afterExamples[afterExamples.length - 2][1], query: { frameName: func.function as string, framePackage: func.package as string, }, }); after = ( {after} ); } return ( {project && ( )} {func.function} , })} > {before} {after}