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