Skip to content

Commit 50fb9c6

Browse files
mjqclaude
andcommitted
feat(perf): Add Percentile dropdown to EAP sampled events table
Enable the Percentile filter (p50/p75/p95/p99/p100) for EAP mode, which was previously hidden. Fetches percentile thresholds via useSpans with p50(span.duration) through p100(span.duration), applies a span.duration filter to the table and count queries, and carries the filter into the Open in Explore URL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 68d95e6 commit 50fb9c6

File tree

4 files changed

+167
-33
lines changed

4 files changed

+167
-33
lines changed

static/app/views/performance/eap/overviewSpansTable.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,10 @@ const PROFILE_COLUMN: OverviewSpansColumn = {
112112
type Props = {
113113
eventView: EventView;
114114
transactionName: string;
115+
maxDuration?: number;
115116
};
116117

117-
export function OverviewSpansTable({eventView, transactionName}: Props) {
118+
export function OverviewSpansTable({eventView, transactionName, maxDuration}: Props) {
118119
const {selection} = usePageFilters();
119120
const location = useLocation();
120121
const {projects} = useProjects();
@@ -148,10 +149,16 @@ export function OverviewSpansTable({eventView, transactionName}: Props) {
148149
const defaultQuery = new MutableSearch(searchQuery);
149150
defaultQuery.setFilterValues('is_transaction', ['true']);
150151
defaultQuery.setFilterValues('transaction', [transactionName]);
152+
if (maxDuration !== undefined && maxDuration > 0) {
153+
defaultQuery.setFilterValues('span.duration', [`<=${maxDuration.toFixed(0)}`]);
154+
}
151155

152156
const countQuery = new MutableSearch(searchQuery);
153157
countQuery.setFilterValues('is_transaction', ['true']);
154158
countQuery.setFilterValues('transaction', [transactionName]);
159+
if (maxDuration !== undefined && maxDuration > 0) {
160+
countQuery.setFilterValues('span.duration', [`<=${maxDuration.toFixed(0)}`]);
161+
}
155162

156163
const {data: numEvents, error: numEventsError} = useSpans(
157164
{

static/app/views/performance/transactionSummary/transactionEvents/content.tsx

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ import {
5252
} from 'sentry/views/performance/utils';
5353

5454
import {EventsTable} from './eventsTable';
55-
import type {EventsDisplayFilterName} from './utils';
56-
import {getEventsFilterOptions} from './utils';
55+
import {EventsDisplayFilterName, getEventsFilterOptions} from './utils';
5756

5857
function EAPSearchBar({
5958
projects,
@@ -207,8 +206,18 @@ export function EventsContent(props: Props) {
207206
webVital,
208207
]);
209208

209+
const {eventsDisplayFilterName, percentileValues} = props;
210+
const maxDuration =
211+
eventsDisplayFilterName === EventsDisplayFilterName.P100
212+
? undefined
213+
: percentileValues?.[eventsDisplayFilterName];
214+
210215
const table = shouldUseEAP ? (
211-
<OverviewSpansTable eventView={eventView} transactionName={transactionName} />
216+
<OverviewSpansTable
217+
eventView={eventView}
218+
transactionName={transactionName}
219+
maxDuration={maxDuration}
220+
/>
212221
) : (
213222
<EventsTable
214223
theme={theme}
@@ -310,19 +319,17 @@ function Search(props: Props) {
310319
/>
311320
)}
312321
</StyledSearchBarWrapper>
313-
{!shouldUseEAP && (
314-
<CompactSelect
315-
trigger={triggerProps => (
316-
<OverlayTrigger.Button {...triggerProps} prefix={t('Percentile')} />
317-
)}
318-
value={eventsDisplayFilterName}
319-
onChange={opt => onChangeEventsDisplayFilter(opt.value)}
320-
options={Object.entries(eventsFilterOptions).map(([name, filter]) => ({
321-
value: name as EventsDisplayFilterName,
322-
label: filter.label,
323-
}))}
324-
/>
325-
)}
322+
<CompactSelect
323+
trigger={triggerProps => (
324+
<OverlayTrigger.Button {...triggerProps} prefix={t('Percentile')} />
325+
)}
326+
value={eventsDisplayFilterName}
327+
onChange={opt => onChangeEventsDisplayFilter(opt.value)}
328+
options={Object.entries(eventsFilterOptions).map(([name, filter]) => ({
329+
value: name as EventsDisplayFilterName,
330+
label: filter.label,
331+
}))}
332+
/>
326333
{!shouldUseEAP && (
327334
<LinkButton
328335
to={eventView.getResultsViewUrlTarget(
@@ -344,7 +351,16 @@ function OpenInExploreButton({
344351
location,
345352
organization,
346353
transactionName,
347-
}: Pick<Props, 'location' | 'organization' | 'transactionName'>) {
354+
eventsDisplayFilterName,
355+
percentileValues,
356+
}: Pick<
357+
Props,
358+
| 'location'
359+
| 'organization'
360+
| 'transactionName'
361+
| 'eventsDisplayFilterName'
362+
| 'percentileValues'
363+
>) {
348364
const {selection} = usePageFilters();
349365

350366
if (!organization.features.includes('visibility-explore-view')) {
@@ -361,6 +377,14 @@ function OpenInExploreButton({
361377
query.setFilterValues('is_transaction', ['true']);
362378
query.setFilterValues('transaction', [transactionName]);
363379

380+
const maxDuration =
381+
eventsDisplayFilterName === EventsDisplayFilterName.P100
382+
? undefined
383+
: percentileValues?.[eventsDisplayFilterName];
384+
if (maxDuration !== undefined && maxDuration > 0) {
385+
query.setFilterValues('span.duration', [`<=${maxDuration.toFixed(0)}`]);
386+
}
387+
364388
const exploreUrl = getExploreUrl({
365389
organization,
366390
selection,
@@ -379,11 +403,13 @@ const FilterActions = styled('div')<{eap: boolean}>`
379403
margin-bottom: ${p => p.theme.space.xl};
380404
381405
@media (min-width: ${p => p.theme.breakpoints.sm}) {
382-
grid-template-columns: ${p => (p.eap ? 'auto 1fr auto' : 'repeat(4, min-content)')};
406+
grid-template-columns: ${p =>
407+
p.eap ? 'auto 1fr auto auto' : 'repeat(4, min-content)'};
383408
}
384409
385410
@media (min-width: ${p => p.theme.breakpoints.xl}) {
386-
grid-template-columns: ${p => (p.eap ? 'auto 1fr auto' : 'auto auto 1fr auto auto')};
411+
grid-template-columns: ${p =>
412+
p.eap ? 'auto 1fr auto auto' : 'auto auto 1fr auto auto'};
387413
}
388414
`;
389415

static/app/views/performance/transactionSummary/transactionEvents/index.tsx

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import type {Location} from 'history';
22

33
import * as Layout from 'sentry/components/layouts/thirds';
44
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
5+
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
56
import {trackAnalytics} from 'sentry/utils/analytics';
67
import {DiscoverQuery} from 'sentry/utils/discover/discoverQuery';
78
import {removeHistogramQueryStrings} from 'sentry/utils/performance/histogram';
89
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
910
import {useLocation} from 'sentry/utils/useLocation';
1011
import {useNavigate} from 'sentry/utils/useNavigate';
12+
import {useSpans} from 'sentry/views/insights/common/queries/useDiscover';
1113
import {useTransactionSummaryEAP} from 'sentry/views/performance/eap/useTransactionSummaryEAP';
14+
import {SEGMENT_SPANS_CURSOR} from 'sentry/views/performance/eap/utils';
1215
import {
1316
decodeFilterFromLocation,
1417
filterToLocationQuery,
@@ -23,11 +26,14 @@ import {useTransactionSummaryContext} from 'sentry/views/performance/transaction
2326
import {EventsContent} from './content';
2427
import {
2528
decodeEventsDisplayFilterFromLocation,
29+
EAP_PERCENTILE_FIELDS,
2630
EventsDisplayFilterName,
31+
filterEventsDisplayToEAPLocationQuery,
2732
filterEventsDisplayToLocationQuery,
2833
getEventsFilterOptions,
2934
getPercentilesEventView,
3035
getWebVital,
36+
mapEAPPercentileValues,
3137
mapPercentileValues,
3238
} from './utils';
3339

@@ -125,20 +131,8 @@ function TransactionEvents() {
125131

126132
if (shouldUseEAP) {
127133
return (
128-
<EventsContent
129-
location={location}
130-
organization={organization}
131-
eventView={eventView}
132-
transactionName={transactionName}
133-
spanOperationBreakdownFilter={spanOperationBreakdownFilter}
134+
<EAPTransactionEvents
134135
onChangeSpanOperationBreakdownFilter={onChangeSpanOperationBreakdownFilter}
135-
eventsDisplayFilterName={EventsDisplayFilterName.P100}
136-
onChangeEventsDisplayFilter={onChangeEventsDisplayFilter}
137-
percentileValues={undefined}
138-
projectId={projectId}
139-
projects={projects}
140-
webVital={webVital}
141-
setError={setError}
142136
/>
143137
);
144138
}
@@ -184,4 +178,79 @@ function TransactionEvents() {
184178
);
185179
}
186180

181+
function EAPTransactionEvents({
182+
onChangeSpanOperationBreakdownFilter,
183+
}: {
184+
onChangeSpanOperationBreakdownFilter: (
185+
newFilter: SpanOperationBreakdownFilter | undefined
186+
) => void;
187+
}) {
188+
const {organization, eventView, transactionName, setError, projectId, projects} =
189+
useTransactionSummaryContext();
190+
const {selection} = usePageFilters();
191+
const location = useLocation();
192+
const navigate = useNavigate();
193+
const eventsDisplayFilterName = decodeEventsDisplayFilterFromLocation(location);
194+
const spanOperationBreakdownFilter = decodeFilterFromLocation(location);
195+
const webVital = getWebVital(location);
196+
197+
const percentileQuery = new MutableSearch('');
198+
percentileQuery.setFilterValues('is_transaction', ['true']);
199+
percentileQuery.setFilterValues('transaction', [transactionName]);
200+
201+
const {data: percentileData} = useSpans(
202+
{
203+
search: percentileQuery,
204+
fields: [...EAP_PERCENTILE_FIELDS],
205+
pageFilters: selection,
206+
},
207+
'api.insights.transaction-events-percentiles'
208+
);
209+
210+
const percentileValues = mapEAPPercentileValues(percentileData);
211+
212+
const onChangeEventsDisplayFilter = (newFilterName: EventsDisplayFilterName) => {
213+
trackAnalytics(
214+
'performance_views.transactionEvents.display_filter_dropdown.selection',
215+
{
216+
organization,
217+
action: newFilterName as string,
218+
}
219+
);
220+
221+
const nextQuery: Location['query'] = {
222+
...removeHistogramQueryStrings(location, [ZOOM_START, ZOOM_END]),
223+
...filterEventsDisplayToEAPLocationQuery(newFilterName),
224+
[SEGMENT_SPANS_CURSOR]: undefined,
225+
};
226+
227+
if (newFilterName === EventsDisplayFilterName.P100) {
228+
delete nextQuery.showTransactions;
229+
}
230+
231+
navigate({
232+
pathname: location.pathname,
233+
query: nextQuery,
234+
});
235+
};
236+
237+
return (
238+
<EventsContent
239+
location={location}
240+
organization={organization}
241+
eventView={eventView}
242+
transactionName={transactionName}
243+
spanOperationBreakdownFilter={spanOperationBreakdownFilter}
244+
onChangeSpanOperationBreakdownFilter={onChangeSpanOperationBreakdownFilter}
245+
eventsDisplayFilterName={eventsDisplayFilterName}
246+
onChangeEventsDisplayFilter={onChangeEventsDisplayFilter}
247+
percentileValues={percentileValues}
248+
projectId={projectId}
249+
projects={projects}
250+
webVital={webVital}
251+
setError={setError}
252+
/>
253+
);
254+
}
255+
187256
export default TransactionEvents;

static/app/views/performance/transactionSummary/transactionEvents/utils.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {DiscoverDatasets} from 'sentry/utils/discover/types';
1414
import {WebVital} from 'sentry/utils/fields';
1515
import {decodeScalar} from 'sentry/utils/queryString';
1616
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
17+
import {QueryParameterNames} from 'sentry/views/insights/common/views/queryParameters';
1718
import type {DomainView} from 'sentry/views/insights/pages/useFilters';
1819
import {
1920
decodeFilterFromLocation,
@@ -302,3 +303,34 @@ export function getWebVital(location: Location): WebVital | undefined {
302303
}
303304
return undefined;
304305
}
306+
307+
export const EAP_PERCENTILE_FIELDS = [
308+
'p50(span.duration)',
309+
'p75(span.duration)',
310+
'p95(span.duration)',
311+
'p99(span.duration)',
312+
'p100(span.duration)',
313+
] as const;
314+
315+
export function mapEAPPercentileValues(
316+
data: Array<Record<string, number>>
317+
): PercentileValues {
318+
const row = data[0];
319+
return {
320+
p50: row?.['p50(span.duration)'] ?? 0,
321+
p75: row?.['p75(span.duration)'] ?? 0,
322+
p95: row?.['p95(span.duration)'] ?? 0,
323+
p99: row?.['p99(span.duration)'] ?? 0,
324+
p100: row?.['p100(span.duration)'] ?? 0,
325+
};
326+
}
327+
328+
export function filterEventsDisplayToEAPLocationQuery(option: EventsDisplayFilterName) {
329+
const query: Record<string, string> = {
330+
showTransactions: option,
331+
};
332+
if (option !== EventsDisplayFilterName.P100) {
333+
query[QueryParameterNames.SPANS_SORT] = '-span.duration';
334+
}
335+
return query;
336+
}

0 commit comments

Comments
 (0)