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]); }