diff --git a/static/app/views/performance/newTraceDetails/trace.tsx b/static/app/views/performance/newTraceDetails/trace.tsx index 0c868e1ddbd4a0..f963828c91a9b0 100644 --- a/static/app/views/performance/newTraceDetails/trace.tsx +++ b/static/app/views/performance/newTraceDetails/trace.tsx @@ -66,6 +66,7 @@ import {useTraceState, useTraceStateDispatch} from './traceState/traceStateProvi import { isAutogroupedNode, isCollapsedNode, + isEAPSpanNode, isMissingInstrumentationNode, isSpanNode, isTraceErrorNode, @@ -664,7 +665,7 @@ function RenderTraceRow(props: { return ; } - if (isSpanNode(node)) { + if (isSpanNode(node) || isEAPSpanNode(node)) { return ; } diff --git a/static/app/views/performance/newTraceDetails/traceApi/useTrace.tsx b/static/app/views/performance/newTraceDetails/traceApi/useTrace.tsx index 94a5276b10187f..d69636c11eef5a 100644 --- a/static/app/views/performance/newTraceDetails/traceApi/useTrace.tsx +++ b/static/app/views/performance/newTraceDetails/traceApi/useTrace.tsx @@ -217,5 +217,20 @@ export function useTrace( } ); + // const eapTraceQuery = useApiQuery( + // [ + // `/organizations/${organization.slug}/trace/${options.traceSlug ?? ''}/`, + // { + // query: { + // timestamp: queryParams.timestamp, + // }, + // }, + // ], + // { + // staleTime: Infinity, + // enabled: !!options.traceSlug && !!organization.slug && mode !== 'demo', + // } + // ); + return mode === 'demo' ? demoTrace : traceQuery; } diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx index 3b14a20e97428f..6627b63360a07f 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx @@ -19,6 +19,7 @@ import {defined} from 'sentry/utils'; import {useLocation} from 'sentry/utils/useLocation'; import useProjects from 'sentry/utils/useProjects'; import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection'; +import {isEAPSpanNode} from 'sentry/views/performance/newTraceDetails/traceGuards'; import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider'; import {ProfileContext, ProfilesProvider} from 'sentry/views/profiling/profilesProvider'; @@ -44,14 +45,14 @@ function SpanNodeDetailHeader({ onTabScrollToNode, project, }: { - node: TraceTreeNode; + node: TraceTreeNode | TraceTreeNode; onTabScrollToNode: (node: TraceTreeNode) => void; organization: Organization; project: Project | undefined; }) { const hasNewTraceUi = useHasTraceNewUi(); - if (!hasNewTraceUi) { + if (!hasNewTraceUi && !isEAPSpanNode(node)) { return ( {t('Span')} @@ -246,7 +248,9 @@ export function SpanNodeDetails({ organization, onTabScrollToNode, onParentClick, -}: TraceTreeNodeDetailsProps>) { +}: TraceTreeNodeDetailsProps< + TraceTreeNode | TraceTreeNode +>) { const location = useLocation(); const hasNewTraceUi = useHasTraceNewUi(); const {projects} = useProjects(); @@ -259,6 +263,19 @@ export function SpanNodeDetails({ const profileId = typeof profileMeta === 'string' ? profileMeta : profileMeta.profiler_id; + if (isEAPSpanNode(node)) { + return ( + + + + ); + } + return ( ) { return ; } - if (isSpanNode(props.node)) { + if (isSpanNode(props.node) || isEAPSpanNode(props.node)) { return ; } diff --git a/static/app/views/performance/newTraceDetails/traceGuards.tsx b/static/app/views/performance/newTraceDetails/traceGuards.tsx index e3f9970834e453..f2a14ad2cdbebc 100644 --- a/static/app/views/performance/newTraceDetails/traceGuards.tsx +++ b/static/app/views/performance/newTraceDetails/traceGuards.tsx @@ -1,3 +1,5 @@ +import type {TraceSplitResults} from 'sentry/utils/performance/quickTrace/types'; + import {MissingInstrumentationNode} from './traceModels/missingInstrumentationNode'; import {ParentAutogroupNode} from './traceModels/parentAutogroupNode'; import {SiblingAutogroupNode} from './traceModels/siblingAutogroupNode'; @@ -21,10 +23,20 @@ export function isSpanNode( ); } +export function isEAPSpanNode( + node: TraceTreeNode +): node is TraceTreeNode { + return !!(node.value && 'is_transaction' in node.value); +} + export function isTransactionNode( node: TraceTreeNode ): node is TraceTreeNode { - return !!(node.value && 'transaction' in node.value) && !isAutogroupedNode(node); + return ( + !!(node.value && 'transaction' in node.value) && + !isAutogroupedNode(node) && + !isEAPSpanNode(node) + ); } export function isParentAutogroupedNode( @@ -65,13 +77,19 @@ export function isRootNode( export function isTraceNode( node: TraceTreeNode -): node is TraceTreeNode { +): node is TraceTreeNode> { return !!( node.value && ('orphan_errors' in node.value || 'transactions' in node.value) ); } +export function isEAPTraceNode( + node: TraceTreeNode +): node is TraceTreeNode { + return !!node.value && Array.isArray(node.value) && !isTraceNode(node); +} + export function shouldAddMissingInstrumentationSpan(sdk: string | undefined): boolean { if (!sdk) { return true; @@ -104,9 +122,13 @@ export function shouldAddMissingInstrumentationSpan(sdk: string | undefined): bo } } -export function isJavascriptSDKTransaction(transaction: TraceTree.Transaction): boolean { - return /javascript|angular|astro|backbone|ember|gatsby|nextjs|react|remix|svelte|vue/.test( - transaction.sdk_name +export function isJavascriptSDKEvent(value: TraceTree.NodeValue): boolean { + return ( + !!value && + 'sdk_name' in value && + /javascript|angular|astro|backbone|ember|gatsby|nextjs|react|remix|svelte|vue/.test( + value.sdk_name + ) ); } diff --git a/static/app/views/performance/newTraceDetails/traceHeader/index.tsx b/static/app/views/performance/newTraceDetails/traceHeader/index.tsx index 3fa899879fd757..5d58338b0034e5 100644 --- a/static/app/views/performance/newTraceDetails/traceHeader/index.tsx +++ b/static/app/views/performance/newTraceDetails/traceHeader/index.tsx @@ -27,7 +27,7 @@ import {useModuleURLBuilder} from 'sentry/views/insights/common/utils/useModuleU import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters'; import {useTraceStateDispatch} from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider'; -import {isRootTransaction} from '../../traceDetails/utils'; +import {isRootEvent} from '../../traceDetails/utils'; import type {TraceMetaQueryResults} from '../traceApi/useTraceMeta'; import TraceConfigurations from '../traceConfigurations'; import {isTraceNode} from '../traceGuards'; @@ -152,7 +152,7 @@ export const getRepresentativeTransaction = ( for (const transaction of traceNode.value.transactions || []) { // If we find a root transaction, we can stop looking and use it for the title. - if (!firstRootTransaction && isRootTransaction(transaction)) { + if (!firstRootTransaction && isRootEvent(transaction)) { firstRootTransaction = transaction; break; } else if ( diff --git a/static/app/views/performance/newTraceDetails/traceModels/traceTree.tsx b/static/app/views/performance/newTraceDetails/traceModels/traceTree.tsx index 2653d6e1523650..bac38805c2805a 100644 --- a/static/app/views/performance/newTraceDetails/traceModels/traceTree.tsx +++ b/static/app/views/performance/newTraceDetails/traceModels/traceTree.tsx @@ -12,12 +12,13 @@ import type { TracePerformanceIssue as TracePerformanceIssueType, TraceSplitResults, } from 'sentry/utils/performance/quickTrace/types'; +import {isTraceSplitResult} from 'sentry/utils/performance/quickTrace/utils'; import {collectTraceMeasurements} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree.measurements'; import type {TracePreferencesState} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences'; import type {ReplayTrace} from 'sentry/views/replays/detail/trace/useReplayTraces'; import type {ReplayRecord} from 'sentry/views/replays/types'; -import {isRootTransaction} from '../../traceDetails/utils'; +import {isRootEvent} from '../../traceDetails/utils'; import {getTraceQueryParams} from '../traceApi/useTrace'; import type {TraceMetaQueryResults} from '../traceApi/useTraceMeta'; import { @@ -25,7 +26,8 @@ import { isAutogroupedNode, isBrowserRequestSpan, isCollapsedNode, - isJavascriptSDKTransaction, + isEAPTraceNode, + isJavascriptSDKEvent, isMissingInstrumentationNode, isPageloadTransactionNode, isParentAutogroupedNode, @@ -118,6 +120,23 @@ export declare namespace TraceTree { ['trace timeline change']: (view: [number, number]) => void; } + type EAPSpan = { + children: EAPSpan[]; + duration: number; + end_timestamp: number; + event_id: string; + is_transaction: boolean; + op: string; + parent_span_id: string; + project_id: number; + project_slug: string; + start_timestamp: number; + transaction: string; + description?: string; + }; + + type EAPTrace = EAPSpan[]; + // Raw node values interface Span extends RawSpanType { measurements?: Record; @@ -128,7 +147,7 @@ export declare namespace TraceTree { sdk_name: string; } - type Trace = TraceSplitResults; + type Trace = TraceSplitResults | EAPTrace; type TraceError = TraceErrorType; type TracePerformanceIssue = TracePerformanceIssueType; type Profile = {profile_id: string} | {profiler_id: string}; @@ -143,6 +162,7 @@ export declare namespace TraceTree { | Transaction | TraceError | Span + | EAPSpan | MissingInstrumentationSpan | SiblingAutogroup | ChildrenAutogroup @@ -292,7 +312,7 @@ export class TraceTree extends TraceTreeEventDispatcher { function visit( parent: TraceTreeNode, - value: TraceTree.Transaction | TraceTree.TraceError + value: TraceTree.Transaction | TraceTree.TraceError | TraceTree.EAPSpan ) { tree.eventsCount++; tree.projects.set(value.project_id, { @@ -323,7 +343,11 @@ export class TraceTree extends TraceTreeEventDispatcher { parent.children.push(node); if (node.value && 'children' in node.value) { - for (const child of node.value.children) { + // EAP spans are not sorted by default + const children = node.value.children.sort( + (a, b) => a.start_timestamp - b.start_timestamp + ); + for (const child of children) { visit(node, child); } } @@ -1750,25 +1774,31 @@ export class TraceTree extends TraceTreeEventDispatcher { throw new TypeError('Not trace node'); } - const traceStats = trace.value.transactions?.reduce<{ - javascriptRootTransactions: TraceTree.Transaction[]; - orphans: number; - roots: number; - }>( - (stats, transaction) => { - if (isRootTransaction(transaction)) { - stats.roots++; - - if (isJavascriptSDKTransaction(transaction)) { - stats.javascriptRootTransactions.push(transaction); - } - } else { - stats.orphans++; + const traceStats = isEAPTraceNode(trace) + ? { + javascriptRootTransactions: trace.value.filter(isJavascriptSDKEvent), + orphans: trace.value.filter(span => span.parent_span_id !== null).length, + roots: trace.value.filter(span => span.parent_span_id === null).length, } - return stats; - }, - {roots: 0, orphans: 0, javascriptRootTransactions: []} - ) ?? {roots: 0, orphans: 0, javascriptRootTransactions: []}; + : trace.value.transactions?.reduce<{ + javascriptRootTransactions: TraceTree.Transaction[]; + orphans: number; + roots: number; + }>( + (stats, transaction) => { + if (isRootEvent(transaction)) { + stats.roots++; + + if (isJavascriptSDKEvent(transaction)) { + stats.javascriptRootTransactions.push(transaction); + } + } else { + stats.orphans++; + } + return stats; + }, + {roots: 0, orphans: 0, javascriptRootTransactions: []} + ) ?? {roots: 0, orphans: 0, javascriptRootTransactions: []}; if (traceStats.roots === 0) { if (traceStats.orphans > 0) { @@ -1997,9 +2027,18 @@ function traceQueueIterator( root: TraceTreeNode, visitor: ( parent: TraceTreeNode, - value: TraceTree.Transaction | TraceTree.TraceError + value: TraceTree.Transaction | TraceTree.TraceError | TraceTree.EAPSpan ) => void ) { + if (!isTraceSplitResult(trace)) { + // Finish this + const spans = [...trace].sort((a, b) => a.start_timestamp - b.start_timestamp); + for (const span of spans) { + visitor(root, span); + } + return; + } + let tIdx = 0; let oIdx = 0; diff --git a/static/app/views/performance/newTraceDetails/traceModels/traceTreeNode.tsx b/static/app/views/performance/newTraceDetails/traceModels/traceTreeNode.tsx index f39ac7e60c841f..ddff13d73eebbd 100644 --- a/static/app/views/performance/newTraceDetails/traceModels/traceTreeNode.tsx +++ b/static/app/views/performance/newTraceDetails/traceModels/traceTreeNode.tsx @@ -1,6 +1,7 @@ import type {Theme} from '@emotion/react'; import type {EventTransaction} from 'sentry/types/event'; +import {isEAPSpanNode} from 'sentry/views/performance/newTraceDetails/traceGuards'; import type {TraceTree} from './traceTree'; @@ -28,6 +29,11 @@ function isTraceAutogroup( } function shouldCollapseNodeByDefault(node: TraceTreeNode) { + // Only collapse EAP spans if they are a segments/transactions + if (isEAPSpanNode(node)) { + return node.value.is_transaction; + } + if (isTraceSpan(node.value)) { // Android creates TCP connection spans which are noisy and not useful in most cases. // Unless the span has a child txn which would indicate a continuaton of the trace, we collapse it. @@ -84,14 +90,17 @@ export class TraceTreeNode // otherwise we can only infer a timestamp. if ( value && - 'timestamp' in value && + (('end_timestamp' in value && typeof value.end_timestamp === 'number') || + ('timestamp' in value && typeof value.timestamp === 'number')) && + // Finish this 'start_timestamp' in value && - typeof value.timestamp === 'number' && typeof value.start_timestamp === 'number' ) { + const end_timestamp = + 'end_timestamp' in value ? value.end_timestamp : value.timestamp; this.space = [ value.start_timestamp * 1e3, - (value.timestamp - value.start_timestamp) * 1e3, + (end_timestamp - value.start_timestamp) * 1e3, ]; } else if (value && 'timestamp' in value && typeof value.timestamp === 'number') { this.space = [value.timestamp * 1e3, 0]; diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx index 9a0e2c03909778..97ce4440d990db 100644 --- a/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx +++ b/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx @@ -1,3 +1,4 @@ +import {isEAPSpanNode} from 'sentry/views/performance/newTraceDetails/traceGuards'; import {SpanProjectIcon} from 'sentry/views/performance/newTraceDetails/traceRow/traceIcons'; import {TraceIcons} from '../traceIcons'; @@ -14,7 +15,13 @@ import { const NO_PROFILES: any = []; -export function TraceSpanRow(props: TraceRowProps>) { +export function TraceSpanRow( + props: TraceRowProps | TraceTreeNode> +) { + const spanId = isEAPSpanNode(props.node) + ? props.node.value.event_id + : props.node.value.span_id; + return (
> {!props.node.value.description - ? props.node.value.span_id ?? 'unknown' + ? spanId ?? 'unknown' : props.node.value.description.length > 100 ? props.node.value.description.slice(0, 100).trim() + '\u2026' : props.node.value.description} diff --git a/static/app/views/performance/traceDetails/content.tsx b/static/app/views/performance/traceDetails/content.tsx index 7581cb8b7ae316..5a81f723e49eff 100644 --- a/static/app/views/performance/traceDetails/content.tsx +++ b/static/app/views/performance/traceDetails/content.tsx @@ -49,7 +49,7 @@ import {TraceDetailHeader, TraceSearchBar, TraceSearchContainer} from './styles' import TraceNotFound from './traceNotFound'; import TraceView from './traceView'; import type {TraceInfo} from './types'; -import {getTraceInfo, hasTraceData, isRootTransaction} from './utils'; +import {getTraceInfo, hasTraceData, isRootEvent} from './utils'; type IndexedFusedTransaction = { event: TraceFullDetailed | TraceError; @@ -276,7 +276,7 @@ class TraceDetailsContent extends Component { const {roots, orphans} = (traces ?? []).reduce( (counts, trace) => { - if (isRootTransaction(trace)) { + if (isRootEvent(trace)) { counts.roots++; } else { counts.orphans++; diff --git a/static/app/views/performance/traceDetails/newTraceDetailsContent.tsx b/static/app/views/performance/traceDetails/newTraceDetailsContent.tsx index 90c02e51927a33..680a1b9e129708 100644 --- a/static/app/views/performance/traceDetails/newTraceDetailsContent.tsx +++ b/static/app/views/performance/traceDetails/newTraceDetailsContent.tsx @@ -48,7 +48,7 @@ import {BrowserDisplay} from '../transactionDetails/eventMetas'; import NewTraceView from './newTraceDetailsTraceView'; import TraceNotFound from './traceNotFound'; import TraceViewDetailPanel from './traceViewDetailPanel'; -import {getTraceInfo, hasTraceData, isRootTransaction} from './utils'; +import {getTraceInfo, hasTraceData, isRootEvent} from './utils'; type Props = Pick, 'params' | 'location'> & { dateSelected: boolean; @@ -207,7 +207,7 @@ function NewTraceDetailsContent(props: Props) { const {roots, orphans} = (traces ?? []).reduce( (counts, trace) => { - if (isRootTransaction(trace)) { + if (isRootEvent(trace)) { counts.roots++; } else { counts.orphans++; diff --git a/static/app/views/performance/traceDetails/newTraceDetailsTraceView.tsx b/static/app/views/performance/traceDetails/newTraceDetailsTraceView.tsx index 9f6ba3d9abefca..f69917e7eb7558 100644 --- a/static/app/views/performance/traceDetails/newTraceDetailsTraceView.tsx +++ b/static/app/views/performance/traceDetails/newTraceDetailsTraceView.tsx @@ -46,7 +46,7 @@ import type {TraceInfo, TreeDepth} from 'sentry/views/performance/traceDetails/t import { getTraceInfo, hasTraceData, - isRootTransaction, + isRootEvent, } from 'sentry/views/performance/traceDetails/utils'; import LimitExceededMessage from './limitExceededMessage'; @@ -313,7 +313,7 @@ function NewTraceView({ const result = renderTransaction(trace, { ...acc, // if the root of a subtrace has a parent_span_id, then it must be an orphan - isOrphan: !isRootTransaction(trace), + isOrphan: !isRootEvent(trace), isLast: isLastTransaction && !hasOrphanErrors, continuingDepths: (!isLastTransaction && hasChildren) || hasOrphanErrors diff --git a/static/app/views/performance/traceDetails/traceView.tsx b/static/app/views/performance/traceDetails/traceView.tsx index 0a4f16ed0aa04b..2b13cd690b71dc 100644 --- a/static/app/views/performance/traceDetails/traceView.tsx +++ b/static/app/views/performance/traceDetails/traceView.tsx @@ -40,7 +40,7 @@ import type {TraceInfo, TreeDepth} from 'sentry/views/performance/traceDetails/t import { getTraceInfo, hasTraceData, - isRootTransaction, + isRootEvent, } from 'sentry/views/performance/traceDetails/utils'; import LimitExceededMessage from './limitExceededMessage'; @@ -296,7 +296,7 @@ export default function TraceView({ const result = renderTransaction(trace, { ...acc, // if the root of a subtrace has a parent_span_id, then it must be an orphan - isOrphan: !isRootTransaction(trace), + isOrphan: !isRootEvent(trace), isLast: isLastTransaction && !hasOrphanErrors, continuingDepths: (!isLastTransaction && hasChildren) || hasOrphanErrors diff --git a/static/app/views/performance/traceDetails/utils.tsx b/static/app/views/performance/traceDetails/utils.tsx index 6ef9b7f793329b..c7450896fbf9ad 100644 --- a/static/app/views/performance/traceDetails/utils.tsx +++ b/static/app/views/performance/traceDetails/utils.tsx @@ -228,7 +228,7 @@ export function shortenErrorTitle(title: string): string { return title.split(':')[0]!; } -export function isRootTransaction(trace: TraceFullDetailed): boolean { - // Root transactions has no parent_span_id - return trace.parent_span_id === null; +export function isRootEvent(value: TraceTree.NodeValue): boolean { + // Root events has no parent_span_id + return !!value && 'parent_span_id' in value && value.parent_span_id === null; }