diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx
index 2b6352db9df3c0..40ae817e7cbe2e 100644
--- a/static/app/views/performance/newTraceDetails/trace.spec.tsx
+++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx
@@ -217,6 +217,17 @@ function mockMetricsResponse() {
});
}
+function mockEventsResponse() {
+ MockApiClient.addMockResponse({
+ url: '/organizations/org-slug/events/',
+ method: 'GET',
+ body: {
+ data: [],
+ queries: [],
+ },
+ });
+}
+
function getVirtualizedContainer(): HTMLElement {
const virtualizedContainer = screen.queryByTestId('trace-virtualized-list');
if (!virtualizedContainer) {
@@ -277,6 +288,7 @@ async function keyboardNavigationTestSetup() {
mockTraceRootEvent('0');
mockTraceEventDetails();
mockMetricsResponse();
+ mockEventsResponse();
const value = render(, {router});
const virtualizedContainer = getVirtualizedContainer();
@@ -334,6 +346,7 @@ async function pageloadTestSetup() {
mockTraceRootEvent('0');
mockTraceEventDetails();
mockMetricsResponse();
+ mockEventsResponse();
const value = render(, {router});
const virtualizedContainer = getVirtualizedContainer();
@@ -392,6 +405,7 @@ async function nestedTransactionsTestSetup() {
mockTraceRootEvent('0');
mockTraceEventDetails();
mockMetricsResponse();
+ mockEventsResponse();
const value = render(, {router});
const virtualizedContainer = getVirtualizedContainer();
@@ -448,6 +462,7 @@ async function searchTestSetup() {
mockTraceRootEvent('0');
mockTraceEventDetails();
mockMetricsResponse();
+ mockEventsResponse();
const value = render(, {router});
const virtualizedContainer = getVirtualizedContainer();
@@ -508,6 +523,7 @@ async function simpleTestSetup() {
mockTraceRootEvent('0');
mockTraceEventDetails();
mockMetricsResponse();
+ mockEventsResponse();
const value = render(, {router});
const virtualizedContainer = getVirtualizedContainer();
@@ -611,6 +627,7 @@ async function completeTestSetup() {
mockTraceRootEvent('0');
mockTraceEventDetails();
mockMetricsResponse();
+ mockEventsResponse();
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/project_slug:error0/',
@@ -864,6 +881,7 @@ describe('trace view', () => {
mockTraceResponse();
mockTraceMetaResponse();
mockTraceTagsResponse();
+ mockEventsResponse();
render(, {router});
expect(await screen.findByText(/assembling the trace/i)).toBeInTheDocument();
@@ -874,6 +892,7 @@ describe('trace view', () => {
mockTraceResponse({statusCode: 404});
mockTraceMetaResponse({statusCode: 404});
mockTraceTagsResponse({statusCode: 404});
+ mockEventsResponse();
render(, {router});
expect(await screen.findByText(/we failed to load your trace/i)).toBeInTheDocument();
@@ -890,6 +909,7 @@ describe('trace view', () => {
});
mockTraceMetaResponse({statusCode: 404});
mockTraceTagsResponse({statusCode: 404});
+ mockEventsResponse();
render(, {router});
expect(await screen.findByText(/we failed to load your trace/i)).toBeInTheDocument();
@@ -905,6 +925,7 @@ describe('trace view', () => {
});
mockTraceMetaResponse();
mockTraceTagsResponse();
+ mockEventsResponse();
render(, {router});
expect(
@@ -1608,6 +1629,7 @@ describe('trace view', () => {
mockTraceRootEvent('0');
mockTraceEventDetails();
mockMetricsResponse();
+ mockEventsResponse();
mockTraceResponse({
body: {
diff --git a/static/app/views/performance/newTraceDetails/trace.tsx b/static/app/views/performance/newTraceDetails/trace.tsx
index 53fafa7f4caee7..50a4d0e90922ff 100644
--- a/static/app/views/performance/newTraceDetails/trace.tsx
+++ b/static/app/views/performance/newTraceDetails/trace.tsx
@@ -71,6 +71,7 @@ import {
isTraceNode,
isTransactionNode,
} from './traceGuards';
+import {TraceLevelOpsBreakdown} from './traceLevelOpsBreakdown';
import type {TraceReducerState} from './traceState';
function computeNextIndexFromAction(
@@ -401,6 +402,9 @@ export function Trace({
className="TraceScrollbarContainer"
ref={manager.registerHorizontalScrollBarContainerRef}
>
+ {trace_id ? (
+
+ ) : null}
@@ -840,6 +844,9 @@ const TraceStylingWrapper = styled('div')`
overflow-x: auto;
overscroll-behavior: none;
will-change: transform;
+ z-index: 10;
+ display: flex;
+ align-items: center;
.TraceScrollbarScroller {
height: 1px;
diff --git a/static/app/views/performance/newTraceDetails/traceLevelOpsBreakdown.tsx b/static/app/views/performance/newTraceDetails/traceLevelOpsBreakdown.tsx
new file mode 100644
index 00000000000000..5c85d8381e70fb
--- /dev/null
+++ b/static/app/views/performance/newTraceDetails/traceLevelOpsBreakdown.tsx
@@ -0,0 +1,128 @@
+import styled from '@emotion/styled';
+
+import {addErrorMessage} from 'sentry/actionCreators/indicator';
+import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
+import Placeholder from 'sentry/components/placeholder';
+import {IconCircleFill} from 'sentry/icons/iconCircleFill';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {NewQuery} from 'sentry/types/organization';
+import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
+import {DiscoverDatasets} from 'sentry/utils/discover/types';
+import type {Color} from 'sentry/utils/theme';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+
+import {useTraceEventView} from './useTraceEventView';
+import {type TraceViewQueryParams, useTraceQueryParams} from './useTraceQueryParams';
+
+function useTraceLevelOpsQuery(
+ traceSlug: string,
+ params: TraceViewQueryParams,
+ partialSavedQuery: Partial
+) {
+ const location = useLocation();
+ const organization = useOrganization();
+ const eventView = useTraceEventView(traceSlug, params, {
+ ...partialSavedQuery,
+ dataset: DiscoverDatasets.SPANS_EAP,
+ });
+
+ return useDiscoverQuery({
+ eventView,
+ orgSlug: organization.slug,
+ location,
+ });
+}
+
+function LoadingPlaceHolder() {
+ return (
+
+
+
+
+
+ );
+}
+
+type Props = {
+ isTraceLoading: boolean;
+ traceSlug: string;
+};
+
+export function TraceLevelOpsBreakdown({traceSlug, isTraceLoading}: Props) {
+ const urlParams = useTraceQueryParams();
+ const {
+ data: opsCountsResult,
+ isPending: isOpsCountsLoading,
+ isError: isOpsCountsError,
+ } = useTraceLevelOpsQuery(traceSlug ?? '', urlParams, {
+ fields: ['span.op', 'count()'],
+ orderby: '-count',
+ });
+ const {
+ data: totalCountResult,
+ isPending: isTotalCountLoading,
+ isError: isTotalCountError,
+ } = useTraceLevelOpsQuery(traceSlug ?? '', urlParams, {
+ fields: ['count()'],
+ });
+
+ if (isOpsCountsLoading || isTotalCountLoading || isTraceLoading) {
+ return ;
+ }
+
+ if (isOpsCountsError || isTotalCountError) {
+ addErrorMessage(t('Failed to load trace level ops breakdown'));
+ return null;
+ }
+
+ const totalCount = totalCountResult?.data[0]?.['count()'] ?? 0;
+
+ if (typeof totalCount !== 'number' || totalCount <= 0) {
+ return null;
+ }
+
+ return (
+
+ {opsCountsResult?.data.slice(0, 4).map(currOp => {
+ const operationName = currOp['span.op'];
+ const count = currOp['count()'];
+
+ if (typeof operationName !== 'string' || typeof count !== 'number') {
+ return null;
+ }
+
+ const percentage = count / totalCount;
+ const color = pickBarColor(operationName);
+ const pctLabel = isFinite(percentage) ? Math.round(percentage * 100) : '∞';
+
+ return (
+
+
+ {operationName}
+ {pctLabel}%
+
+ );
+ })}
+
+ );
+}
+
+const HighlightsOpRow = styled('div')`
+ display: flex;
+ align-items: center;
+ font-size: ${p => p.theme.fontSizeSmall};
+ gap: 5px;
+`;
+
+const Container = styled('div')`
+ display: flex;
+ align-items: center;
+ padding-left: ${space(1)};
+ gap: ${space(2)};
+`;
+
+const StyledPlaceholder = styled(Placeholder)`
+ border-radius: ${p => p.theme.borderRadius};
+`;
diff --git a/static/app/views/performance/newTraceDetails/useTraceEventView.tsx b/static/app/views/performance/newTraceDetails/useTraceEventView.tsx
index eaf5c16852a515..419d8a61873ed1 100644
--- a/static/app/views/performance/newTraceDetails/useTraceEventView.tsx
+++ b/static/app/views/performance/newTraceDetails/useTraceEventView.tsx
@@ -1,13 +1,15 @@
import {useMemo} from 'react';
import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
+import type {NewQuery} from 'sentry/types/organization';
import EventView from 'sentry/utils/discover/eventView';
import type {TraceViewQueryParams} from './useTraceQueryParams';
export function useTraceEventView(
traceSlug: string,
- params: TraceViewQueryParams
+ params: TraceViewQueryParams,
+ partialSavedQuery?: Partial
): EventView {
return useMemo(() => {
let startTimeStamp = params.start;
@@ -34,6 +36,7 @@ export function useTraceEventView(
start: startTimeStamp,
end: endTimeStamp,
range: !(startTimeStamp || endTimeStamp) ? params.statsPeriod : undefined,
+ ...partialSavedQuery,
});
- }, [params, traceSlug]);
+ }, [params, traceSlug, partialSavedQuery]);
}