diff --git a/static/app/components/events/eventStatisticalDetector/eventAffectedTransactions.tsx b/static/app/components/events/eventStatisticalDetector/eventAffectedTransactions.tsx index eeb3a8f83414ec..147ab7810af5f9 100644 --- a/static/app/components/events/eventStatisticalDetector/eventAffectedTransactions.tsx +++ b/static/app/components/events/eventStatisticalDetector/eventAffectedTransactions.tsx @@ -13,13 +13,17 @@ import {space} from 'sentry/styles/space'; import {Event, Group, Project} from 'sentry/types'; import {Series} from 'sentry/types/echarts'; import {defined} from 'sentry/utils'; +import {trackAnalytics} from 'sentry/utils/analytics'; import {tooltipFormatter} from 'sentry/utils/discover/charts'; import {Container, NumberContainer} from 'sentry/utils/discover/styles'; import {getDuration} from 'sentry/utils/formatters'; import {useProfileFunctions} from 'sentry/utils/profiling/hooks/useProfileFunctions'; import {useProfileTopEventsStats} from 'sentry/utils/profiling/hooks/useProfileTopEventsStats'; import {useRelativeDateTime} from 'sentry/utils/profiling/hooks/useRelativeDateTime'; -import {generateProfileSummaryRouteWithQuery} from 'sentry/utils/profiling/routes'; +import { + generateProfileFlamechartRouteWithQuery, + generateProfileSummaryRouteWithQuery, +} from 'sentry/utils/profiling/routes'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import useOrganization from 'sentry/utils/useOrganization'; @@ -36,6 +40,8 @@ export function EventAffectedTransactions({ const evidenceData = event.occurrence?.evidenceData; const fingerprint = evidenceData?.fingerprint; const breakpoint = evidenceData?.breakpoint; + const frameName = evidenceData?.function; + const framePackage = evidenceData?.package || evidenceData?.module; const isValid = defined(fingerprint) && defined(breakpoint); @@ -64,6 +70,8 @@ export function EventAffectedTransactions({ ); @@ -74,12 +82,16 @@ const TRANSACTIONS_LIMIT = 5; interface EventAffectedTransactionsInnerProps { breakpoint: number; fingerprint: number; + frameName: string; + framePackage: string; project: Project; } function EventAffectedTransactionsInner({ breakpoint, fingerprint, + frameName, + framePackage, project, }: EventAffectedTransactionsInnerProps) { const organization = useOrganization(); @@ -132,11 +144,38 @@ function EventAffectedTransactionsInner({ query: query ?? '', enabled: defined(query), others: false, - referrer: 'api.profiling.functions.regression.stats', // TODO: update this + referrer: 'api.profiling.functions.regression.transaction-stats', topEvents: TRANSACTIONS_LIMIT, yAxes: ['p95()', 'worst()'], }); + const examplesByTransaction = useMemo(() => { + const allExamples: Record = {}; + if (!defined(functionStats.data)) { + return allExamples; + } + + const timestamps = functionStats.data.timestamps; + const breakpointIndex = timestamps.indexOf(breakpoint); + if (breakpointIndex < 0) { + return allExamples; + } + + transactionsDeltaQuery.data?.data?.forEach(row => { + const transaction = row.transaction as string; + const data = functionStats.data.data.find( + ({axis, label}) => axis === 'worst()' && label === transaction + ); + if (!defined(data)) { + return; + } + + allExamples[transaction] = findExamplePair(data.values, breakpointIndex); + }); + + return allExamples; + }, [breakpoint, transactionsDeltaQuery, functionStats]); + const timeseriesByTransaction: Record = useMemo(() => { const allTimeseries: Record = {}; if (!defined(functionStats.data)) { @@ -161,7 +200,7 @@ function EventAffectedTransactionsInner({ value: data.values[i], }; }), - seriesName: 'p95()', + seriesName: 'p95(function.duration)', }; }); @@ -192,15 +231,77 @@ function EventAffectedTransactionsInner({ }; }, []); + function handleGoToProfile() { + trackAnalytics('profiling_views.go_to_flamegraph', { + organization, + source: 'profiling.issue.function_regression.transactions', + }); + } + return ( {(transactionsDeltaQuery.data?.data ?? []).map(transaction => { - const series = timeseriesByTransaction[transaction.transaction as string] ?? { + const transactionName = transaction.transaction as string; + const series = timeseriesByTransaction[transactionName] ?? { seriesName: 'p95()', data: [], }; + const [beforeExample, afterExample] = examplesByTransaction[ + transactionName + ] ?? [null, null]; + + let before = ( + + ); + + if (defined(beforeExample)) { + const beforeTarget = generateProfileFlamechartRouteWithQuery({ + orgSlug: organization.slug, + projectSlug: project.slug, + profileId: beforeExample, + query: { + frameName, + framePackage, + }, + }); + + before = ( + + {before} + + ); + } + + let after = ( + + ); + + if (defined(afterExample)) { + const afterTarget = generateProfileFlamechartRouteWithQuery({ + orgSlug: organization.slug, + projectSlug: project.slug, + profileId: afterExample, + query: { + frameName, + framePackage, + }, + }); + + after = ( + + {after} + + ); + } + const summaryTarget = generateProfileSummaryRouteWithQuery({ orgSlug: organization.slug, projectSlug: project.slug, @@ -237,15 +338,9 @@ function EventAffectedTransactionsInner({ position="top" > - + {before} - + {after} @@ -257,6 +352,66 @@ function EventAffectedTransactionsInner({ ); } +/** + * Find an example pair of profile ids from before and after the breakpoint. + * + * We prioritize profile ids from outside some window around the breakpoint + * because the breakpoint is not 100% accurate and giving a buffer around + * the breakpoint to so we can more accurate get a example profile from + * before and after ranges. + * + * @param examples list of example profile ids + * @param breakpointIndex the index where the breakpoint is + * @param window the window around the breakpoint to deprioritize + */ +function findExamplePair( + examples: string[], + breakpointIndex, + window = 3 +): [string | null, string | null] { + let before: string | null = null; + + for (let i = breakpointIndex - window; i < examples.length && i >= 0; i--) { + if (examples[i]) { + before = examples[i]; + break; + } + } + + if (!defined(before)) { + for ( + let i = breakpointIndex; + i < examples.length && i > breakpointIndex - window; + i-- + ) { + if (examples[i]) { + before = examples[i]; + break; + } + } + } + + let after: string | null = null; + + for (let i = breakpointIndex + window; i < examples.length; i++) { + if (examples[i]) { + after = examples[i]; + break; + } + } + + if (!defined(before)) { + for (let i = breakpointIndex; i < breakpointIndex + window; i++) { + if (examples[i]) { + after = examples[i]; + break; + } + } + } + + return [before, after]; +} + const ListContainer = styled('div')` display: grid; grid-template-columns: 1fr auto auto; diff --git a/static/app/components/events/eventStatisticalDetector/eventFunctionComparisonList.tsx b/static/app/components/events/eventStatisticalDetector/eventFunctionComparisonList.tsx index d32033752b97e6..3bef9f40a136af 100644 --- a/static/app/components/events/eventStatisticalDetector/eventFunctionComparisonList.tsx +++ b/static/app/components/events/eventStatisticalDetector/eventFunctionComparisonList.tsx @@ -255,7 +255,7 @@ function EventList({ onClick={() => { trackAnalytics('profiling_views.go_to_flamegraph', { organization, - source: 'profiling.issue.function_regression', + source: 'profiling.issue.function_regression.list', }); }} > diff --git a/static/app/utils/analytics/profilingAnalyticsEvents.tsx b/static/app/utils/analytics/profilingAnalyticsEvents.tsx index af11548f43e3e6..f0f5ce3847d38a 100644 --- a/static/app/utils/analytics/profilingAnalyticsEvents.tsx +++ b/static/app/utils/analytics/profilingAnalyticsEvents.tsx @@ -11,7 +11,8 @@ type ProfilingEventSource = | 'profiling.function_trends.improvement' | 'profiling.function_trends.regression' | 'profiling.global_suspect_functions' - | 'profiling.issue.function_regression' + | 'profiling.issue.function_regression.list' + | 'profiling.issue.function_regression.transactions' | 'profiling_transaction.suspect_functions_table' | 'profiling_transaction.slowest_functions_table' | 'profiling_transaction.regressed_functions_table' diff --git a/static/app/utils/profiling/hooks/useProfileEventsStats.tsx b/static/app/utils/profiling/hooks/useProfileEventsStats.tsx index 9c1595b5e1a814..6bceab64957156 100644 --- a/static/app/utils/profiling/hooks/useProfileEventsStats.tsx +++ b/static/app/utils/profiling/hooks/useProfileEventsStats.tsx @@ -148,22 +148,21 @@ export function transformSingleSeries( dataset: 'discover' | 'profiles' | 'profileFunctions', yAxis: F, rawSeries: any, - label?: string, - formatter?: any + label?: string ) { - if (!defined(formatter)) { - const type = - rawSeries.meta.fields[yAxis] ?? rawSeries.meta.fields[getAggregateAlias(yAxis)]; - formatter = - type === 'duration' - ? makeFormatTo( - rawSeries.meta.units[yAxis] ?? - rawSeries.meta.units[getAggregateAlias(yAxis)] ?? - 'nanoseconds', - 'milliseconds' - ) - : value => value; - } + const type = + rawSeries.meta.fields[yAxis] ?? rawSeries.meta.fields[getAggregateAlias(yAxis)]; + const formatter = + type === 'duration' + ? makeFormatTo( + rawSeries.meta.units[yAxis] ?? + rawSeries.meta.units[getAggregateAlias(yAxis)] ?? + 'nanoseconds', + 'milliseconds' + ) + : type === 'string' + ? value => value || '' + : value => value; const series: EventsStatsSeries['data'][number] = { axis: yAxis, diff --git a/static/app/utils/profiling/hooks/useProfileTopEventsStats.tsx b/static/app/utils/profiling/hooks/useProfileTopEventsStats.tsx index c69a6aba0e076c..97087d9913d872 100644 --- a/static/app/utils/profiling/hooks/useProfileTopEventsStats.tsx +++ b/static/app/utils/profiling/hooks/useProfileTopEventsStats.tsx @@ -4,7 +4,6 @@ import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilte import {EventsStatsSeries, PageFilters} from 'sentry/types'; import {defined} from 'sentry/utils'; import {transformSingleSeries} from 'sentry/utils/profiling/hooks/useProfileEventsStats'; -import {makeFormatTo} from 'sentry/utils/profiling/units/units'; import {useApiQuery} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; @@ -99,10 +98,6 @@ function transformTopEventsStatsResponse( let firstSeries = true; - // TODO: the formatter should be inferred but the response does - // not contain the meta at this time - const formatter = makeFormatTo('nanoseconds', 'milliseconds'); - for (const label of Object.keys(rawData)) { for (const yAxis of yAxes) { let dataForYAxis = rawData[label]; @@ -113,13 +108,7 @@ function transformTopEventsStatsResponse( continue; } - const transformed = transformSingleSeries( - dataset, - yAxis, - dataForYAxis, - label, - formatter - ); + const transformed = transformSingleSeries(dataset, yAxis, dataForYAxis, label); if (firstSeries) { meta = transformed.meta;