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;