Browse Source

feat(tracing-w/o-performance): Frontend changes for displaying traceviews for traces with orphan errors. (#54555)

**This PR aims to introduce trace views for traces that contain orphan
errors, under the feature flag:
`organizations:performance-tracing-without-performance`, providing
frontend support for splitting traces response to
` transactions and orphan_errors` from the 'events-trace' endpoint under
the same flag here:
[PR](https://github.com/getsentry/sentry/pull/54166).**

**Paths for testing with yarn-dev-ui:**
- Just orphan errors:
[link](https://sentry.dev.getsentry.net:7999/performance/trace/0c2f072b73e04f0c88ea62e5ac5a97cb/?statsPeriod=14d)
- Mixup of orphan errors and transactions: 
- - Couldn't find a real example but the block below can be pasted
[here](https://github.com/getsentry/sentry/blob/abdk/tracing-wo-performance-frontend/static/app/views/performance/traceDetails/index.tsx#L100)
and then visiting this
[link](https://sentry.dev.getsentry.net:7999/performance/trace/0508a02a6aac44e3aca52ca0442bc953/?statsPeriod=14d)
should give us an idea.

```
      orphanErrors = [
          {
            "event_id": "9c65abc015f0445f82bce81ffcb31d8b",
            "issue_id": 3899254784,
            "span": "985f4b1fc8385888",
            "project_id": 1,
            "project_slug": "javascript",
            "title": "MaybeEncodingError: Error sending result: ''(1, <ExceptionInfo: KafkaException(\\'KafkaError{code=MSG_SIZE_TOO_LARGE,v...",
            "level": "error",
            "timestamp": 1690853823.946488,
            "generation": 0,
            "issue": "sadfsdfsd"
        },
        {
          "event_id": "9c65abc015f0445f82bce81ffcb31d8b",
          "issue_id": 3899254784,
          "span": "985f4b1fc8385888",
          "project_id": 1,
          "project_slug": "javascript",
          "title": "MaybeEncodingError: Error sending result: ''(1, <ExceptionInfo: KafkaException(\\'KafkaError{code=MSG_SIZE_TOO_LARGE,v...",
          "level": "error",
          "timestamp": 1690853824.446276,
          "generation": 0,
          "issue": "sadfsdfsd"
      }
      ]
```
**Issues to be addressed by upcoming PRs:**
- Exisitng scenario and trace navigator changes not made yet.
- Not yet forward compatible with Replay traces 
- Will be adding tests. 
- Search for orphan errors don't show hidden messages.
- Trace limit is exceeded by 1 (displays 101 children), when there is a
mixup of transactions and orphan errors in the trace.

**Context and Designs:**
- We now want to show trace views for traces that contain orphan errors.
[JIRA-TICKET](https://getsentry.atlassian.net/browse/PERF-2052)
- Designs: 
<img width="720" alt="Screenshot 2023-08-10 at 1 29 26 PM"
src="https://github.com/getsentry/sentry/assets/60121741/c2d5a158-454e-4dcc-968a-1770e3f1c0a8">

---------

Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
Abdkhan14 1 year ago
parent
commit
45a07ce754

+ 11 - 3
static/app/components/events/interfaces/spans/spanBar.tsx

@@ -54,8 +54,12 @@ import {
   QuickTraceContext,
   QuickTraceContext,
   QuickTraceContextChildrenProps,
   QuickTraceContextChildrenProps,
 } from 'sentry/utils/performance/quickTrace/quickTraceContext';
 } from 'sentry/utils/performance/quickTrace/quickTraceContext';
-import {QuickTraceEvent, TraceError} from 'sentry/utils/performance/quickTrace/types';
-import {isTraceFull} from 'sentry/utils/performance/quickTrace/utils';
+import {
+  QuickTraceEvent,
+  TraceError,
+  TraceFull,
+} from 'sentry/utils/performance/quickTrace/types';
+import {isTraceTransaction} from 'sentry/utils/performance/quickTrace/utils';
 import {PerformanceInteraction} from 'sentry/utils/performanceForSentry';
 import {PerformanceInteraction} from 'sentry/utils/performanceForSentry';
 import {ProfileContext} from 'sentry/views/profiling/profilesProvider';
 import {ProfileContext} from 'sentry/views/profiling/profilesProvider';
 
 
@@ -842,7 +846,11 @@ export class SpanBar extends Component<SpanBarProps, SpanBarState> {
     const {span} = this.props;
     const {span} = this.props;
     const {currentEvent} = quickTrace;
     const {currentEvent} = quickTrace;
 
 
-    if (isGapSpan(span) || !currentEvent || !isTraceFull(currentEvent)) {
+    if (
+      isGapSpan(span) ||
+      !currentEvent ||
+      !isTraceTransaction<TraceFull>(currentEvent)
+    ) {
       return null;
       return null;
     }
     }
 
 

+ 36 - 0
static/app/utils/performance/quickTrace/quickTraceQuery.spec.tsx

@@ -122,4 +122,40 @@ describe('TraceLiteQuery', function () {
 
 
     expect(await screen.findByTestId('type')).toHaveTextContent('full');
     expect(await screen.findByTestId('type')).toHaveTextContent('full');
   });
   });
+
+  it('uses trace full response with tracing without performance enabled', async function () {
+    traceLiteMock = MockApiClient.addMockResponse({
+      url: `/organizations/test-org/events-trace-light/0${traceId}/`,
+      body: [],
+      match: [MockApiClient.matchQuery({event_id: eventId})],
+    });
+    traceFullMock = MockApiClient.addMockResponse({
+      url: `/organizations/test-org/events-trace/0${traceId}/`,
+      body: {
+        transactions: [{event_id: eventId, children: []}],
+        orphan_errors: [],
+      },
+    });
+    traceMetaMock = MockApiClient.addMockResponse({
+      url: `/organizations/test-org/events-trace-meta/0${traceId}/`,
+      body: {
+        projects: 4,
+        transactions: 5,
+        errors: 2,
+      },
+    });
+    event.contexts.trace.trace_id = `0${traceId}`;
+
+    const organization = TestStubs.Organization();
+    organization.features = ['performance-tracing-without-performance'];
+
+    render(
+      <QuickTraceQuery event={event} location={location} orgSlug="test-org">
+        {renderQuickTrace}
+      </QuickTraceQuery>,
+      {organization}
+    );
+
+    expect(await screen.findByTestId('type')).toHaveTextContent('full');
+  });
 });
 });

+ 44 - 13
static/app/utils/performance/quickTrace/quickTraceQuery.tsx

@@ -4,12 +4,18 @@ import {Event} from 'sentry/types/event';
 import {DiscoverQueryProps} from 'sentry/utils/discover/genericDiscoverQuery';
 import {DiscoverQueryProps} from 'sentry/utils/discover/genericDiscoverQuery';
 import {TraceFullQuery} from 'sentry/utils/performance/quickTrace/traceFullQuery';
 import {TraceFullQuery} from 'sentry/utils/performance/quickTrace/traceFullQuery';
 import TraceLiteQuery from 'sentry/utils/performance/quickTrace/traceLiteQuery';
 import TraceLiteQuery from 'sentry/utils/performance/quickTrace/traceLiteQuery';
-import {QuickTraceQueryChildrenProps} from 'sentry/utils/performance/quickTrace/types';
+import {
+  QuickTraceQueryChildrenProps,
+  TraceFull,
+  TraceSplitResults,
+} from 'sentry/utils/performance/quickTrace/types';
 import {
 import {
   flattenRelevantPaths,
   flattenRelevantPaths,
   getTraceTimeRangeFromEvent,
   getTraceTimeRangeFromEvent,
   isCurrentEvent,
   isCurrentEvent,
+  isTraceSplitResult,
 } from 'sentry/utils/performance/quickTrace/utils';
 } from 'sentry/utils/performance/quickTrace/utils';
+import useOrganization from 'sentry/utils/useOrganization';
 
 
 type QueryProps = Omit<DiscoverQueryProps, 'api' | 'eventView'> & {
 type QueryProps = Omit<DiscoverQueryProps, 'api' | 'eventView'> & {
   children: (props: QuickTraceQueryChildrenProps) => React.ReactNode;
   children: (props: QuickTraceQueryChildrenProps) => React.ReactNode;
@@ -17,6 +23,7 @@ type QueryProps = Omit<DiscoverQueryProps, 'api' | 'eventView'> & {
 };
 };
 
 
 export default function QuickTraceQuery({children, event, ...props}: QueryProps) {
 export default function QuickTraceQuery({children, event, ...props}: QueryProps) {
+  const organization = useOrganization();
   const renderEmpty = () => (
   const renderEmpty = () => (
     <Fragment>
     <Fragment>
       {children({
       {children({
@@ -63,17 +70,34 @@ export default function QuickTraceQuery({children, event, ...props}: QueryProps)
               traceFullResults.error === null &&
               traceFullResults.error === null &&
               traceFullResults.traces !== null
               traceFullResults.traces !== null
             ) {
             ) {
-              for (const subtrace of traceFullResults.traces) {
-                try {
-                  const trace = flattenRelevantPaths(event, subtrace);
-                  return children({
-                    ...traceFullResults,
-                    trace,
-                    currentEvent: trace.find(e => isCurrentEvent(e, event)) ?? null,
-                  });
-                } catch {
-                  // let this fall through and check the next subtrace
-                  // or use the trace lite results
+              if (
+                organization.features.includes(
+                  'performance-tracing-without-performance'
+                ) &&
+                isTraceSplitResult<TraceSplitResults<TraceFull>, TraceFull[]>(
+                  traceFullResults.traces
+                )
+              ) {
+                traceFullResults.traces = traceFullResults.traces.transactions;
+              }
+
+              if (
+                !isTraceSplitResult<TraceSplitResults<TraceFull>, TraceFull[]>(
+                  traceFullResults.traces
+                )
+              ) {
+                for (const subtrace of traceFullResults.traces) {
+                  try {
+                    const trace = flattenRelevantPaths(event, subtrace);
+                    return children({
+                      ...traceFullResults,
+                      trace,
+                      currentEvent: trace.find(e => isCurrentEvent(e, event)) ?? null,
+                    });
+                  } catch {
+                    // let this fall through and check the next subtrace
+                    // or use the trace lite results
+                  }
                 }
                 }
               }
               }
             }
             }
@@ -103,7 +127,14 @@ export default function QuickTraceQuery({children, event, ...props}: QueryProps)
               // if we reach this point but there were some traces in the full results,
               // if we reach this point but there were some traces in the full results,
               // that means there were other transactions in the trace, but the current
               // that means there were other transactions in the trace, but the current
               // event could not be found
               // event could not be found
-              type: traceFullResults.traces?.length ? 'missing' : 'empty',
+              type:
+                traceFullResults.traces &&
+                !isTraceSplitResult<TraceSplitResults<TraceFull>, TraceFull[]>(
+                  traceFullResults.traces
+                ) &&
+                traceFullResults.traces?.length
+                  ? 'missing'
+                  : 'empty',
               currentEvent: null,
               currentEvent: null,
             });
             });
           }}
           }}

+ 20 - 4
static/app/utils/performance/quickTrace/traceFullQuery.tsx

@@ -9,6 +9,7 @@ import {
   TraceFull,
   TraceFull,
   TraceFullDetailed,
   TraceFullDetailed,
   TraceRequestProps,
   TraceRequestProps,
+  TraceSplitResults,
 } from 'sentry/utils/performance/quickTrace/types';
 } from 'sentry/utils/performance/quickTrace/types';
 import {
 import {
   getTraceRequestPayload,
   getTraceRequestPayload,
@@ -96,12 +97,27 @@ function GenericTraceFullQuery<T>({
   );
   );
 }
 }
 
 
-export function TraceFullQuery(props: Omit<QueryProps<TraceFull[]>, 'detailed'>) {
-  return <GenericTraceFullQuery<TraceFull[]> {...props} detailed={false} />;
+export function TraceFullQuery(
+  props: Omit<QueryProps<TraceFull[] | TraceSplitResults<TraceFull>>, 'detailed'>
+) {
+  return (
+    <GenericTraceFullQuery<TraceFull[] | TraceSplitResults<TraceFull>>
+      {...props}
+      detailed={false}
+    />
+  );
 }
 }
 
 
 export function TraceFullDetailedQuery(
 export function TraceFullDetailedQuery(
-  props: Omit<QueryProps<TraceFullDetailed[]>, 'detailed'>
+  props: Omit<
+    QueryProps<TraceFullDetailed[] | TraceSplitResults<TraceFullDetailed>>,
+    'detailed'
+  >
 ) {
 ) {
-  return <GenericTraceFullQuery<TraceFullDetailed[]> {...props} detailed />;
+  return (
+    <GenericTraceFullQuery<TraceFullDetailed[] | TraceSplitResults<TraceFullDetailed>>
+      {...props}
+      detailed
+    />
+  );
 }
 }

+ 7 - 0
static/app/utils/performance/quickTrace/types.tsx

@@ -32,6 +32,8 @@ export type TraceError = {
   project_slug: string;
   project_slug: string;
   span: string;
   span: string;
   title: string;
   title: string;
+  generation?: number;
+  timestamp?: number;
 };
 };
 
 
 export type TracePerformanceIssue = Omit<TraceError, 'issue' | 'span'> & {
 export type TracePerformanceIssue = Omit<TraceError, 'issue' | 'span'> & {
@@ -80,6 +82,11 @@ export type TraceFullDetailed = Omit<TraceFull, 'children'> & {
   tags?: EventTag[];
   tags?: EventTag[];
 };
 };
 
 
+export type TraceSplitResults<U extends TraceFull | TraceFullDetailed> = {
+  orphan_errors: TraceError[];
+  transactions: U[];
+};
+
 export type TraceProps = {
 export type TraceProps = {
   traceId: string;
   traceId: string;
   end?: string;
   end?: string;

+ 22 - 4
static/app/utils/performance/quickTrace/utils.tsx

@@ -11,10 +11,12 @@ import {DiscoverQueryProps} from 'sentry/utils/discover/genericDiscoverQuery';
 import {
 import {
   QuickTrace,
   QuickTrace,
   QuickTraceEvent,
   QuickTraceEvent,
+  TraceError,
   TraceFull,
   TraceFull,
   TraceFullDetailed,
   TraceFullDetailed,
   TraceLite,
   TraceLite,
 } from 'sentry/utils/performance/quickTrace/types';
 } from 'sentry/utils/performance/quickTrace/types';
+import {TraceRoot} from 'sentry/views/performance/traceDetails/types';
 
 
 export function isTransaction(event: Event): event is EventTransaction {
 export function isTransaction(event: Event): event is EventTransaction {
   return event.type === 'transaction';
   return event.type === 'transaction';
@@ -305,12 +307,28 @@ export function filterTrace(
   );
   );
 }
 }
 
 
-export function isTraceFull(transaction): transaction is TraceFull {
-  return Boolean((transaction as TraceFull).event_id);
+export function isTraceTransaction<U extends TraceFull | TraceFullDetailed>(
+  transaction: TraceRoot | TraceError | QuickTraceEvent | U
+): transaction is U {
+  return 'event_id' in transaction;
 }
 }
 
 
-export function isTraceFullDetailed(transaction): transaction is TraceFullDetailed {
-  return Boolean((transaction as TraceFullDetailed).event_id);
+export function isTraceError(
+  transaction: TraceRoot | TraceError | TraceFullDetailed
+): transaction is TraceError {
+  return 'level' in transaction;
+}
+
+export function isTraceRoot(
+  transaction: TraceRoot | TraceError | TraceFullDetailed
+): transaction is TraceRoot {
+  return 'traceSlug' in transaction;
+}
+
+export function isTraceSplitResult<U extends object, V extends object>(
+  result: U | V
+): result is U {
+  return 'transactions' in result;
 }
 }
 
 
 function handleProjectMeta(organization: OrganizationSummary, projects: number) {
 function handleProjectMeta(organization: OrganizationSummary, projects: number) {

+ 88 - 38
static/app/views/performance/traceDetails/content.tsx

@@ -21,7 +21,11 @@ import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
 import {getDuration} from 'sentry/utils/formatters';
 import {getDuration} from 'sentry/utils/formatters';
 import {createFuzzySearch, Fuse} from 'sentry/utils/fuzzySearch';
 import {createFuzzySearch, Fuse} from 'sentry/utils/fuzzySearch';
 import getDynamicText from 'sentry/utils/getDynamicText';
 import getDynamicText from 'sentry/utils/getDynamicText';
-import {TraceFullDetailed, TraceMeta} from 'sentry/utils/performance/quickTrace/types';
+import {
+  TraceError,
+  TraceFullDetailed,
+  TraceMeta,
+} from 'sentry/utils/performance/quickTrace/types';
 import {filterTrace, reduceTrace} from 'sentry/utils/performance/quickTrace/utils';
 import {filterTrace, reduceTrace} from 'sentry/utils/performance/quickTrace/utils';
 import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
 import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
 import Breadcrumb from 'sentry/views/performance/breadcrumb';
 import Breadcrumb from 'sentry/views/performance/breadcrumb';
@@ -31,11 +35,11 @@ import {TraceDetailHeader, TraceSearchBar, TraceSearchContainer} from './styles'
 import TraceNotFound from './traceNotFound';
 import TraceNotFound from './traceNotFound';
 import TraceView from './traceView';
 import TraceView from './traceView';
 import {TraceInfo} from './types';
 import {TraceInfo} from './types';
-import {getTraceInfo, isRootTransaction} from './utils';
+import {getTraceInfo, hasTraceData, isRootTransaction} from './utils';
 
 
 type IndexedFusedTransaction = {
 type IndexedFusedTransaction = {
+  event: TraceFullDetailed | TraceError;
   indexed: string[];
   indexed: string[];
-  transaction: TraceFullDetailed;
 };
 };
 
 
 type Props = Pick<RouteComponentProps<{traceSlug: string}, {}>, 'params' | 'location'> & {
 type Props = Pick<RouteComponentProps<{traceSlug: string}, {}>, 'params' | 'location'> & {
@@ -47,17 +51,18 @@ type Props = Pick<RouteComponentProps<{traceSlug: string}, {}>, 'params' | 'loca
   traceEventView: EventView;
   traceEventView: EventView;
   traceSlug: string;
   traceSlug: string;
   traces: TraceFullDetailed[] | null;
   traces: TraceFullDetailed[] | null;
+  orphanErrors?: TraceError[];
 };
 };
 
 
 type State = {
 type State = {
-  filteredTransactionIds: Set<string> | undefined;
+  filteredEventIds: Set<string> | undefined;
   searchQuery: string | undefined;
   searchQuery: string | undefined;
 };
 };
 
 
 class TraceDetailsContent extends Component<Props, State> {
 class TraceDetailsContent extends Component<Props, State> {
   state: State = {
   state: State = {
     searchQuery: undefined,
     searchQuery: undefined,
-    filteredTransactionIds: undefined,
+    filteredEventIds: undefined,
   };
   };
 
 
   componentDidMount() {
   componentDidMount() {
@@ -65,7 +70,10 @@ class TraceDetailsContent extends Component<Props, State> {
   }
   }
 
 
   componentDidUpdate(prevProps: Props) {
   componentDidUpdate(prevProps: Props) {
-    if (this.props.traces !== prevProps.traces) {
+    if (
+      this.props.traces !== prevProps.traces ||
+      this.props.orphanErrors !== prevProps.orphanErrors
+    ) {
       this.initFuse();
       this.initFuse();
     }
     }
   }
   }
@@ -75,8 +83,14 @@ class TraceDetailsContent extends Component<Props, State> {
   virtualScrollbarContainerRef = createRef<HTMLDivElement>();
   virtualScrollbarContainerRef = createRef<HTMLDivElement>();
 
 
   async initFuse() {
   async initFuse() {
-    if (defined(this.props.traces) && this.props.traces.length > 0) {
-      const transformed: IndexedFusedTransaction[] = this.props.traces.flatMap(trace =>
+    const {traces, orphanErrors} = this.props;
+
+    if (!hasTraceData(traces, orphanErrors)) {
+      return;
+    }
+
+    const transformedEvents: IndexedFusedTransaction[] =
+      traces?.flatMap(trace =>
         reduceTrace<IndexedFusedTransaction[]>(
         reduceTrace<IndexedFusedTransaction[]>(
           trace,
           trace,
           (acc, transaction) => {
           (acc, transaction) => {
@@ -87,7 +101,7 @@ class TraceDetailsContent extends Component<Props, State> {
             ];
             ];
 
 
             acc.push({
             acc.push({
-              transaction,
+              event: transaction,
               indexed,
               indexed,
             });
             });
 
 
@@ -95,17 +109,26 @@ class TraceDetailsContent extends Component<Props, State> {
           },
           },
           []
           []
         )
         )
-      );
+      ) ?? [];
+
+    // Include orphan error titles and project slugs during fuzzy search
+    orphanErrors?.forEach(orphanError => {
+      const indexed: string[] = [orphanError.title, orphanError.project_slug, 'Unknown'];
 
 
-      this.fuse = await createFuzzySearch(transformed, {
-        keys: ['indexed'],
-        includeMatches: true,
-        threshold: 0.6,
-        location: 0,
-        distance: 100,
-        maxPatternLength: 32,
+      transformedEvents.push({
+        indexed,
+        event: orphanError,
       });
       });
-    }
+    });
+
+    this.fuse = await createFuzzySearch(transformedEvents, {
+      keys: ['indexed'],
+      includeMatches: true,
+      threshold: 0.6,
+      location: 0,
+      distance: 100,
+      maxPatternLength: 32,
+    });
   }
   }
 
 
   renderTraceLoading() {
   renderTraceLoading() {
@@ -121,13 +144,13 @@ class TraceDetailsContent extends Component<Props, State> {
   };
   };
 
 
   filterTransactions = () => {
   filterTransactions = () => {
-    const {traces} = this.props;
-    const {filteredTransactionIds, searchQuery} = this.state;
+    const {traces, orphanErrors} = this.props;
+    const {filteredEventIds, searchQuery} = this.state;
 
 
-    if (!searchQuery || traces === null || traces.length <= 0 || !defined(this.fuse)) {
-      if (filteredTransactionIds !== undefined) {
+    if (!searchQuery || !hasTraceData(traces, orphanErrors) || !defined(this.fuse)) {
+      if (filteredEventIds !== undefined) {
         this.setState({
         this.setState({
-          filteredTransactionIds: undefined,
+          filteredEventIds: undefined,
         });
         });
       }
       }
       return;
       return;
@@ -140,24 +163,33 @@ class TraceDetailsContent extends Component<Props, State> {
        * indices. These matches are often noise, so exclude them.
        * indices. These matches are often noise, so exclude them.
        */
        */
       .filter(({matches}) => matches?.length)
       .filter(({matches}) => matches?.length)
-      .map(({item}) => item.transaction.event_id);
+      .map(({item}) => item.event.event_id);
 
 
     /**
     /**
      * Fuzzy search on ids result in seemingly random results. So switch to
      * Fuzzy search on ids result in seemingly random results. So switch to
      * doing substring matches on ids to provide more meaningful results.
      * doing substring matches on ids to provide more meaningful results.
      */
      */
-    const idMatches = traces
-      .flatMap(trace =>
+    const idMatches: string[] = [];
+    traces
+      ?.flatMap(trace =>
         filterTrace(
         filterTrace(
           trace,
           trace,
           ({event_id, span_id}) =>
           ({event_id, span_id}) =>
             event_id.includes(searchQuery) || span_id.includes(searchQuery)
             event_id.includes(searchQuery) || span_id.includes(searchQuery)
         )
         )
       )
       )
-      .map(transaction => transaction.event_id);
+      .forEach(transaction => idMatches.push(transaction.event_id));
+
+    // Include orphan error event_ids and span_ids during substring search
+    orphanErrors?.forEach(orphanError => {
+      const {event_id, span} = orphanError;
+      if (event_id.includes(searchQuery) || span.includes(searchQuery)) {
+        idMatches.push(event_id);
+      }
+    });
 
 
     this.setState({
     this.setState({
-      filteredTransactionIds: new Set([...fuseMatches, ...idMatches]),
+      filteredEventIds: new Set([...fuseMatches, ...idMatches]),
     });
     });
   };
   };
 
 
@@ -220,7 +252,7 @@ class TraceDetailsContent extends Component<Props, State> {
   }
   }
 
 
   renderTraceWarnings() {
   renderTraceWarnings() {
-    const {traces} = this.props;
+    const {traces, orphanErrors} = this.props;
 
 
     const {roots, orphans} = (traces ?? []).reduce(
     const {roots, orphans} = (traces ?? []).reduce(
       (counts, trace) => {
       (counts, trace) => {
@@ -236,6 +268,9 @@ class TraceDetailsContent extends Component<Props, State> {
 
 
     let warning: React.ReactNode = null;
     let warning: React.ReactNode = null;
 
 
+    const hasOnlyOrphanErrors =
+      hasTraceData(traces, orphanErrors) && (!traces || traces.length <= 0);
+
     if (roots === 0 && orphans > 0) {
     if (roots === 0 && orphans > 0) {
       warning = (
       warning = (
         <Alert type="info" showIcon>
         <Alert type="info" showIcon>
@@ -264,6 +299,19 @@ class TraceDetailsContent extends Component<Props, State> {
           </ExternalLink>
           </ExternalLink>
         </Alert>
         </Alert>
       );
       );
+    } else if (hasOnlyOrphanErrors) {
+      warning = (
+        <Alert type="info" showIcon>
+          {tct(
+            "The good news is we know these errors are related to each other. The bad news is you haven't enabled tracing to tell you more than that. [tracingLink: Configure Tracing]",
+            {
+              tracingLink: (
+                <ExternalLink href="https://docs.sentry.io/product/sentry-basics/tracing/distributed-tracing/" />
+              ),
+            }
+          )}
+        </Alert>
+      );
     }
     }
 
 
     return warning;
     return warning;
@@ -280,6 +328,7 @@ class TraceDetailsContent extends Component<Props, State> {
       traceSlug,
       traceSlug,
       traces,
       traces,
       meta,
       meta,
+      orphanErrors,
     } = this.props;
     } = this.props;
 
 
     if (!dateSelected) {
     if (!dateSelected) {
@@ -288,7 +337,9 @@ class TraceDetailsContent extends Component<Props, State> {
     if (isLoading) {
     if (isLoading) {
       return this.renderTraceLoading();
       return this.renderTraceLoading();
     }
     }
-    if (error !== null || traces === null || traces.length <= 0) {
+
+    const hasData = hasTraceData(traces, orphanErrors);
+    if (error !== null || !hasData) {
       return (
       return (
         <TraceNotFound
         <TraceNotFound
           meta={meta}
           meta={meta}
@@ -299,27 +350,26 @@ class TraceDetailsContent extends Component<Props, State> {
         />
         />
       );
       );
     }
     }
-    const traceInfo = getTraceInfo(traces);
+
+    const traceInfo = traces ? getTraceInfo(traces, orphanErrors) : undefined;
 
 
     return (
     return (
       <Fragment>
       <Fragment>
         {this.renderTraceWarnings()}
         {this.renderTraceWarnings()}
-        {this.renderTraceHeader(traceInfo)}
+        {traceInfo && this.renderTraceHeader(traceInfo)}
         {this.renderSearchBar()}
         {this.renderSearchBar()}
         <Margin>
         <Margin>
-          <VisuallyCompleteWithData
-            id="PerformanceDetails-TraceView"
-            hasData={!!traces.length}
-          >
+          <VisuallyCompleteWithData id="PerformanceDetails-TraceView" hasData={hasData}>
             <TraceView
             <TraceView
-              filteredTransactionIds={this.state.filteredTransactionIds}
+              filteredEventIds={this.state.filteredEventIds}
               traceInfo={traceInfo}
               traceInfo={traceInfo}
               location={location}
               location={location}
               organization={organization}
               organization={organization}
               traceEventView={traceEventView}
               traceEventView={traceEventView}
               traceSlug={traceSlug}
               traceSlug={traceSlug}
-              traces={traces}
+              traces={traces || []}
               meta={meta}
               meta={meta}
+              orphanErrors={orphanErrors || []}
             />
             />
           </VisuallyCompleteWithData>
           </VisuallyCompleteWithData>
         </Margin>
         </Margin>

+ 38 - 16
static/app/views/performance/traceDetails/index.tsx

@@ -13,7 +13,13 @@ import EventView from 'sentry/utils/discover/eventView';
 import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
 import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
 import {TraceFullDetailedQuery} from 'sentry/utils/performance/quickTrace/traceFullQuery';
 import {TraceFullDetailedQuery} from 'sentry/utils/performance/quickTrace/traceFullQuery';
 import TraceMetaQuery from 'sentry/utils/performance/quickTrace/traceMetaQuery';
 import TraceMetaQuery from 'sentry/utils/performance/quickTrace/traceMetaQuery';
-import {TraceFullDetailed, TraceMeta} from 'sentry/utils/performance/quickTrace/types';
+import {
+  TraceError,
+  TraceFullDetailed,
+  TraceMeta,
+  TraceSplitResults,
+} from 'sentry/utils/performance/quickTrace/types';
+import {isTraceSplitResult} from 'sentry/utils/performance/quickTrace/utils';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {decodeScalar} from 'sentry/utils/queryString';
 import withApi from 'sentry/utils/withApi';
 import withApi from 'sentry/utils/withApi';
 import withOrganization from 'sentry/utils/withOrganization';
 import withOrganization from 'sentry/utils/withOrganization';
@@ -79,21 +85,37 @@ class TraceSummary extends Component<Props> {
       error: QueryError | null;
       error: QueryError | null;
       isLoading: boolean;
       isLoading: boolean;
       meta: TraceMeta | null;
       meta: TraceMeta | null;
-      traces: TraceFullDetailed[] | null;
-    }) => (
-      <TraceDetailsContent
-        location={location}
-        organization={organization}
-        params={params}
-        traceSlug={traceSlug}
-        traceEventView={this.getTraceEventView()}
-        dateSelected={dateSelected}
-        isLoading={isLoading}
-        error={error}
-        traces={traces}
-        meta={meta}
-      />
-    );
+      traces: (TraceFullDetailed[] | TraceSplitResults<TraceFullDetailed>) | null;
+    }) => {
+      let transactions: TraceFullDetailed[] | undefined;
+      let orphanErrors: TraceError[] | undefined;
+      if (
+        traces &&
+        organization.features.includes('performance-tracing-without-performance') &&
+        isTraceSplitResult<TraceSplitResults<TraceFullDetailed>, TraceFullDetailed[]>(
+          traces
+        )
+      ) {
+        orphanErrors = traces.orphan_errors;
+        transactions = traces.transactions;
+      }
+
+      return (
+        <TraceDetailsContent
+          location={location}
+          organization={organization}
+          params={params}
+          traceSlug={traceSlug}
+          traceEventView={this.getTraceEventView()}
+          dateSelected={dateSelected}
+          isLoading={isLoading}
+          error={error}
+          orphanErrors={orphanErrors}
+          traces={transactions ?? (traces as TraceFullDetailed[])}
+          meta={meta}
+        />
+      );
+    };
 
 
     if (!dateSelected) {
     if (!dateSelected) {
       return content({
       return content({

+ 71 - 16
static/app/views/performance/traceDetails/traceView.tsx

@@ -1,5 +1,6 @@
 import React, {createRef, useEffect} from 'react';
 import React, {createRef, useEffect} from 'react';
 import {RouteComponentProps} from 'react-router';
 import {RouteComponentProps} from 'react-router';
+import styled from '@emotion/styled';
 import * as Sentry from '@sentry/react';
 import * as Sentry from '@sentry/react';
 
 
 import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
 import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
@@ -21,7 +22,11 @@ import {tct} from 'sentry/locale';
 import {Organization} from 'sentry/types';
 import {Organization} from 'sentry/types';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import EventView from 'sentry/utils/discover/eventView';
 import EventView from 'sentry/utils/discover/eventView';
-import {TraceFullDetailed, TraceMeta} from 'sentry/utils/performance/quickTrace/types';
+import {
+  TraceError,
+  TraceFullDetailed,
+  TraceMeta,
+} from 'sentry/utils/performance/quickTrace/types';
 import {
 import {
   TraceDetailBody,
   TraceDetailBody,
   TracePanel,
   TracePanel,
@@ -32,6 +37,7 @@ import TransactionGroup from 'sentry/views/performance/traceDetails/transactionG
 import {TraceInfo, TreeDepth} from 'sentry/views/performance/traceDetails/types';
 import {TraceInfo, TreeDepth} from 'sentry/views/performance/traceDetails/types';
 import {
 import {
   getTraceInfo,
   getTraceInfo,
+  hasTraceData,
   isRootTransaction,
   isRootTransaction,
 } from 'sentry/views/performance/traceDetails/utils';
 } from 'sentry/views/performance/traceDetails/utils';
 
 
@@ -49,8 +55,9 @@ type Props = Pick<RouteComponentProps<{}, {}>, 'location'> & {
   organization: Organization;
   organization: Organization;
   traceEventView: EventView;
   traceEventView: EventView;
   traceSlug: string;
   traceSlug: string;
-  traces: TraceFullDetailed[] | null;
-  filteredTransactionIds?: Set<string>;
+  traces: TraceFullDetailed[];
+  filteredEventIds?: Set<string>;
+  orphanErrors?: TraceError[];
   traceInfo?: TraceInfo;
   traceInfo?: TraceInfo;
 };
 };
 
 
@@ -80,11 +87,11 @@ function TraceHiddenMessage({
   );
   );
 }
 }
 
 
-function isTransactionVisible(
-  transaction: TraceFullDetailed,
-  filteredTransactionIds?: Set<string>
+function isRowVisible(
+  row: TraceFullDetailed | TraceError,
+  filteredEventIds?: Set<string>
 ): boolean {
 ): boolean {
-  return filteredTransactionIds ? filteredTransactionIds.has(transaction.event_id) : true;
+  return filteredEventIds ? filteredEventIds.has(row.event_id) : true;
 }
 }
 
 
 function generateBounds(traceInfo: TraceInfo) {
 function generateBounds(traceInfo: TraceInfo) {
@@ -103,7 +110,8 @@ export default function TraceView({
   traces,
   traces,
   traceSlug,
   traceSlug,
   traceEventView,
   traceEventView,
-  filteredTransactionIds,
+  filteredEventIds,
+  orphanErrors,
   ...props
   ...props
 }: Props) {
 }: Props) {
   const sentryTransaction = Sentry.getCurrentHub().getScope()?.getTransaction();
   const sentryTransaction = Sentry.getCurrentHub().getScope()?.getTransaction();
@@ -111,6 +119,9 @@ export default function TraceView({
     op: 'trace.render',
     op: 'trace.render',
     description: 'trace-view-content',
     description: 'trace-view-content',
   });
   });
+  const hasOrphanErrors = orphanErrors && orphanErrors.length > 0;
+  const traceHasSingleOrphanError = orphanErrors?.length === 1 && traces.length <= 0;
+
   useEffect(() => {
   useEffect(() => {
     trackAnalytics('performance_views.trace_view.view', {
     trackAnalytics('performance_views.trace_view.view', {
       organization,
       organization,
@@ -141,7 +152,7 @@ export default function TraceView({
     // Add 1 to the generation to make room for the "root trace"
     // Add 1 to the generation to make room for the "root trace"
     const generation = transaction.generation + 1;
     const generation = transaction.generation + 1;
 
 
-    const isVisible = isTransactionVisible(transaction, filteredTransactionIds);
+    const isVisible = isRowVisible(transaction, filteredEventIds);
 
 
     const accumulated: AccType = children.reduce(
     const accumulated: AccType = children.reduce(
       (acc: AccType, child: TraceFullDetailed, idx: number) => {
       (acc: AccType, child: TraceFullDetailed, idx: number) => {
@@ -216,7 +227,7 @@ export default function TraceView({
   const traceViewRef = createRef<HTMLDivElement>();
   const traceViewRef = createRef<HTMLDivElement>();
   const virtualScrollbarContainerRef = createRef<HTMLDivElement>();
   const virtualScrollbarContainerRef = createRef<HTMLDivElement>();
 
 
-  if (traces === null || traces.length <= 0) {
+  if (!hasTraceData(traces, orphanErrors)) {
     return (
     return (
       <TraceNotFound
       <TraceNotFound
         meta={meta}
         meta={meta}
@@ -242,6 +253,7 @@ export default function TraceView({
     transactionGroups: [],
     transactionGroups: [],
   };
   };
 
 
+  let lastIndex: number = 0;
   const {transactionGroups, numberOfHiddenTransactionsAbove} = traces.reduce(
   const {transactionGroups, numberOfHiddenTransactionsAbove} = traces.reduce(
     (acc, trace, index) => {
     (acc, trace, index) => {
       const isLastTransaction = index === traces.length - 1;
       const isLastTransaction = index === traces.length - 1;
@@ -253,15 +265,16 @@ export default function TraceView({
         ...acc,
         ...acc,
         // if the root of a subtrace has a parent_span_id, then it must be an orphan
         // if the root of a subtrace has a parent_span_id, then it must be an orphan
         isOrphan: !isRootTransaction(trace),
         isOrphan: !isRootTransaction(trace),
-        isLast: isLastTransaction,
+        isLast: isLastTransaction && !hasOrphanErrors,
         continuingDepths:
         continuingDepths:
-          !isLastTransaction && hasChildren
-            ? [{depth: 0, isOrphanDepth: isNextChildOrphaned}]
+          (!isLastTransaction && hasChildren) || hasOrphanErrors
+            ? [{depth: 0, isOrphanDepth: isNextChildOrphaned || Boolean(hasOrphanErrors)}]
             : [],
             : [],
         hasGuideAnchor: index === 0,
         hasGuideAnchor: index === 0,
       });
       });
 
 
       acc.index = result.lastIndex + 1;
       acc.index = result.lastIndex + 1;
+      lastIndex = Math.max(lastIndex, result.lastIndex);
       acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
       acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
       acc.transactionGroups.push(result.transactionGroup);
       acc.transactionGroups.push(result.transactionGroup);
       return acc;
       return acc;
@@ -269,9 +282,41 @@ export default function TraceView({
     accumulator
     accumulator
   );
   );
 
 
+  // Build transaction groups for orphan errors
+  if (hasOrphanErrors) {
+    orphanErrors.forEach((error, index) => {
+      const isLastError = index === orphanErrors.length - 1;
+      transactionGroups.push(
+        <TransactionGroup
+          key={error.event_id}
+          location={location}
+          organization={organization}
+          traceInfo={traceInfo}
+          transaction={{
+            ...error,
+            generation: 1,
+          }}
+          generateBounds={generateBounds(traceInfo)}
+          measurements={
+            traces && traces.length > 0
+              ? getMeasurements(traces[0], generateBounds(traceInfo))
+              : undefined
+          }
+          continuingDepths={[]}
+          isOrphan
+          isLast={isLastError}
+          index={lastIndex + index + 1}
+          isVisible={isRowVisible(error, filteredEventIds)}
+          hasGuideAnchor
+          renderedChildren={[]}
+        />
+      );
+    });
+  }
+
   const bounds = generateBounds(traceInfo);
   const bounds = generateBounds(traceInfo);
   const measurements =
   const measurements =
-    Object.keys(traces[0].measurements ?? {}).length > 0
+    traces.length > 0 && Object.keys(traces[0].measurements ?? {}).length > 0
       ? getMeasurements(traces[0], bounds)
       ? getMeasurements(traces[0], bounds)
       : undefined;
       : undefined;
 
 
@@ -284,7 +329,7 @@ export default function TraceView({
               dividerPosition={dividerPosition}
               dividerPosition={dividerPosition}
               interactiveLayerRef={virtualScrollbarContainerRef}
               interactiveLayerRef={virtualScrollbarContainerRef}
             >
             >
-              <TracePanel>
+              <StyledTracePanel>
                 <TraceViewHeaderContainer>
                 <TraceViewHeaderContainer>
                   <ScrollbarManager.Consumer>
                   <ScrollbarManager.Consumer>
                     {({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => {
                     {({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => {
@@ -348,6 +393,8 @@ export default function TraceView({
                     hasGuideAnchor={false}
                     hasGuideAnchor={false}
                     renderedChildren={transactionGroups}
                     renderedChildren={transactionGroups}
                     barColor={pickBarColor('')}
                     barColor={pickBarColor('')}
+                    traceHasSingleOrphanError={traceHasSingleOrphanError}
+                    numOfOrphanErrors={orphanErrors?.length}
                   />
                   />
                   <TraceHiddenMessage
                   <TraceHiddenMessage
                     isVisible
                     isVisible
@@ -360,7 +407,7 @@ export default function TraceView({
                     meta={meta}
                     meta={meta}
                   />
                   />
                 </TraceViewContainer>
                 </TraceViewContainer>
-              </TracePanel>
+              </StyledTracePanel>
             </ScrollbarManager.Provider>
             </ScrollbarManager.Provider>
           )}
           )}
         </DividerHandlerManager.Consumer>
         </DividerHandlerManager.Consumer>
@@ -372,3 +419,11 @@ export default function TraceView({
 
 
   return traceView;
   return traceView;
 }
 }
+
+const StyledTracePanel = styled(TracePanel)`
+  overflow: visible;
+
+  ${TraceViewContainer} {
+    overflow-x: visible;
+  }
+`;

+ 117 - 52
static/app/views/performance/traceDetails/transactionBar.tsx

@@ -1,4 +1,5 @@
 import {Component, createRef, Fragment} from 'react';
 import {Component, createRef, Fragment} from 'react';
+import styled from '@emotion/styled';
 import {Location} from 'history';
 import {Location} from 'history';
 
 
 import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import GuideAnchor from 'sentry/components/assistant/guideAnchor';
@@ -14,6 +15,7 @@ import {
   VerticalMark,
   VerticalMark,
 } from 'sentry/components/events/interfaces/spans/utils';
 } from 'sentry/components/events/interfaces/spans/utils';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
+import Link from 'sentry/components/links/link';
 import {ROW_HEIGHT, SpanBarType} from 'sentry/components/performance/waterfall/constants';
 import {ROW_HEIGHT, SpanBarType} from 'sentry/components/performance/waterfall/constants';
 import {
 import {
   Row,
   Row,
@@ -46,16 +48,22 @@ import {
   getHumanDuration,
   getHumanDuration,
   toPercent,
   toPercent,
 } from 'sentry/components/performance/waterfall/utils';
 } from 'sentry/components/performance/waterfall/utils';
+import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils';
 import {Tooltip} from 'sentry/components/tooltip';
 import {Tooltip} from 'sentry/components/tooltip';
 import {Organization} from 'sentry/types';
 import {Organization} from 'sentry/types';
 import {defined} from 'sentry/utils';
 import {defined} from 'sentry/utils';
-import {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
-import {isTraceFullDetailed} from 'sentry/utils/performance/quickTrace/utils';
+import {TraceError, TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
+import {
+  isTraceError,
+  isTraceRoot,
+  isTraceTransaction,
+} from 'sentry/utils/performance/quickTrace/utils';
 import Projects from 'sentry/utils/projects';
 import Projects from 'sentry/utils/projects';
 
 
 import {ProjectBadgeContainer} from './styles';
 import {ProjectBadgeContainer} from './styles';
 import TransactionDetail from './transactionDetail';
 import TransactionDetail from './transactionDetail';
 import {TraceInfo, TraceRoot, TreeDepth} from './types';
 import {TraceInfo, TraceRoot, TreeDepth} from './types';
+import {shortenErrorTitle} from './utils';
 
 
 const MARGIN_LEFT = 0;
 const MARGIN_LEFT = 0;
 
 
@@ -75,9 +83,12 @@ type Props = {
   removeContentSpanBarRef: (instance: HTMLDivElement | null) => void;
   removeContentSpanBarRef: (instance: HTMLDivElement | null) => void;
   toggleExpandedState: () => void;
   toggleExpandedState: () => void;
   traceInfo: TraceInfo;
   traceInfo: TraceInfo;
-  transaction: TraceRoot | TraceFullDetailed;
+  transaction: TraceRoot | TraceFullDetailed | TraceError;
   barColor?: string;
   barColor?: string;
+  isOrphanError?: boolean;
   measurements?: Map<number, VerticalMark>;
   measurements?: Map<number, VerticalMark>;
+  numOfOrphanErrors?: number;
+  traceHasSingleOrphanError?: boolean;
 };
 };
 
 
 type State = {
 type State = {
@@ -118,7 +129,12 @@ class TransactionBar extends Component<Props, State> {
 
 
   toggleDisplayDetail = () => {
   toggleDisplayDetail = () => {
     const {transaction} = this.props;
     const {transaction} = this.props;
-    if (isTraceFullDetailed(transaction)) {
+
+    if (isTraceError(transaction)) {
+      return;
+    }
+
+    if (isTraceTransaction<TraceFullDetailed>(transaction)) {
       this.setState(state => ({
       this.setState(state => ({
         showDetail: !state.showDetail,
         showDetail: !state.showDetail,
       }));
       }));
@@ -186,10 +202,11 @@ class TransactionBar extends Component<Props, State> {
   renderConnector(hasToggle: boolean) {
   renderConnector(hasToggle: boolean) {
     const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = this.props;
     const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = this.props;
 
 
-    const {generation} = transaction;
-    const eventId = isTraceFullDetailed(transaction)
-      ? transaction.event_id
-      : transaction.traceSlug;
+    const {generation = 0} = transaction;
+    const eventId =
+      isTraceTransaction<TraceFullDetailed>(transaction) || isTraceError(transaction)
+        ? transaction.event_id
+        : transaction.traceSlug;
 
 
     if (generation === 0) {
     if (generation === 0) {
       if (hasToggle) {
       if (hasToggle) {
@@ -247,11 +264,14 @@ class TransactionBar extends Component<Props, State> {
   }
   }
 
 
   renderToggle(errored: boolean) {
   renderToggle(errored: boolean) {
-    const {isExpanded, transaction, toggleExpandedState} = this.props;
-    const {children, generation} = transaction;
+    const {isExpanded, transaction, toggleExpandedState, numOfOrphanErrors} = this.props;
     const left = this.getCurrentOffset();
     const left = this.getCurrentOffset();
 
 
-    if (children.length <= 0) {
+    const hasOrphanErrors = numOfOrphanErrors && numOfOrphanErrors > 0;
+    const childrenLength =
+      (!isTraceError(transaction) && transaction.children?.length) || 0;
+    const generation = transaction.generation || 0;
+    if (childrenLength <= 0 && !hasOrphanErrors) {
       return (
       return (
         <TreeToggleContainer style={{left: `${left}px`}}>
         <TreeToggleContainer style={{left: `${left}px`}}>
           {this.renderConnector(false)}
           {this.renderConnector(false)}
@@ -278,7 +298,7 @@ class TransactionBar extends Component<Props, State> {
             toggleExpandedState();
             toggleExpandedState();
           }}
           }}
         >
         >
-          <Count value={children.length} />
+          <Count value={childrenLength + (numOfOrphanErrors ?? 0)} />
           {!isRoot && (
           {!isRoot && (
             <div>
             <div>
               <TreeToggleIcon direction={isExpanded ? 'up' : 'down'} />
               <TreeToggleIcon direction={isExpanded ? 'up' : 'down'} />
@@ -294,28 +314,44 @@ class TransactionBar extends Component<Props, State> {
     const {organization, transaction, addContentSpanBarRef, removeContentSpanBarRef} =
     const {organization, transaction, addContentSpanBarRef, removeContentSpanBarRef} =
       this.props;
       this.props;
     const left = this.getCurrentOffset();
     const left = this.getCurrentOffset();
-    const errored = isTraceFullDetailed(transaction)
-      ? transaction.errors.length + transaction.performance_issues.length > 0
+    const errored = isTraceTransaction<TraceFullDetailed>(transaction)
+      ? transaction.errors &&
+        transaction.errors.length + transaction.performance_issues.length > 0
       : false;
       : false;
 
 
-    const content = isTraceFullDetailed(transaction) ? (
+    const projectBadge = (isTraceTransaction<TraceFullDetailed>(transaction) ||
+      isTraceError(transaction)) && (
+      <Projects orgId={organization.slug} slugs={[transaction.project_slug]}>
+        {({projects}) => {
+          const project = projects.find(p => p.slug === transaction.project_slug);
+          return (
+            <ProjectBadgeContainer>
+              <Tooltip title={transaction.project_slug}>
+                <ProjectBadge
+                  project={project ? project : {slug: transaction.project_slug}}
+                  avatarSize={16}
+                  hideName
+                />
+              </Tooltip>
+            </ProjectBadgeContainer>
+          );
+        }}
+      </Projects>
+    );
+
+    const content = isTraceError(transaction) ? (
       <Fragment>
       <Fragment>
-        <Projects orgId={organization.slug} slugs={[transaction.project_slug]}>
-          {({projects}) => {
-            const project = projects.find(p => p.slug === transaction.project_slug);
-            return (
-              <ProjectBadgeContainer>
-                <Tooltip title={transaction.project_slug}>
-                  <ProjectBadge
-                    project={project ? project : {slug: transaction.project_slug}}
-                    avatarSize={16}
-                    hideName
-                  />
-                </Tooltip>
-              </ProjectBadgeContainer>
-            );
-          }}
-        </Projects>
+        {projectBadge}
+        <RowTitleContent errored>
+          <ErrorLink to={generateIssueEventTarget(transaction, organization)}>
+            <strong>{'Unknown \u2014 '}</strong>
+            {shortenErrorTitle(transaction.title)}
+          </ErrorLink>
+        </RowTitleContent>
+      </Fragment>
+    ) : isTraceTransaction<TraceFullDetailed>(transaction) ? (
+      <Fragment>
+        {projectBadge}
         <RowTitleContent errored={errored}>
         <RowTitleContent errored={errored}>
           <strong>
           <strong>
             {transaction['transaction.op']}
             {transaction['transaction.op']}
@@ -431,7 +467,8 @@ class TransactionBar extends Component<Props, State> {
     const {transaction} = this.props;
     const {transaction} = this.props;
 
 
     if (
     if (
-      !isTraceFullDetailed(transaction) ||
+      isTraceRoot(transaction) ||
+      isTraceError(transaction) ||
       !(transaction.errors.length + transaction.performance_issues.length)
       !(transaction.errors.length + transaction.performance_issues.length)
     ) {
     ) {
       return null;
       return null;
@@ -441,16 +478,26 @@ class TransactionBar extends Component<Props, State> {
   }
   }
 
 
   renderRectangle() {
   renderRectangle() {
+    if (this.props.traceHasSingleOrphanError) {
+      return null;
+    }
+
     const {transaction, traceInfo, barColor} = this.props;
     const {transaction, traceInfo, barColor} = this.props;
     const {showDetail} = this.state;
     const {showDetail} = this.state;
 
 
     // Use 1 as the difference in the case that startTimestamp === endTimestamp
     // Use 1 as the difference in the case that startTimestamp === endTimestamp
     const delta = Math.abs(traceInfo.endTimestamp - traceInfo.startTimestamp) || 1;
     const delta = Math.abs(traceInfo.endTimestamp - traceInfo.startTimestamp) || 1;
-    const startPosition = Math.abs(
-      transaction.start_timestamp - traceInfo.startTimestamp
-    );
+    const start_timestamp = isTraceError(transaction)
+      ? transaction.timestamp
+      : transaction.start_timestamp;
+
+    if (!(start_timestamp && transaction.timestamp)) {
+      return null;
+    }
+
+    const startPosition = Math.abs(start_timestamp - traceInfo.startTimestamp);
     const startPercentage = startPosition / delta;
     const startPercentage = startPosition / delta;
-    const duration = Math.abs(transaction.timestamp - transaction.start_timestamp);
+    const duration = Math.abs(transaction.timestamp - start_timestamp);
     const widthPercentage = duration / delta;
     const widthPercentage = duration / delta;
 
 
     return (
     return (
@@ -462,22 +509,26 @@ class TransactionBar extends Component<Props, State> {
         }}
         }}
       >
       >
         {this.renderPerformanceIssues()}
         {this.renderPerformanceIssues()}
-        <DurationPill
-          durationDisplay={getDurationDisplay({
-            left: startPercentage,
-            width: widthPercentage,
-          })}
-          showDetail={showDetail}
-        >
-          {getHumanDuration(duration)}
-        </DurationPill>
+        {isTraceError(transaction) ? (
+          <ErrorBadge />
+        ) : (
+          <DurationPill
+            durationDisplay={getDurationDisplay({
+              left: startPercentage,
+              width: widthPercentage,
+            })}
+            showDetail={showDetail}
+          >
+            {getHumanDuration(duration)}
+          </DurationPill>
+        )}
       </RowRectangle>
       </RowRectangle>
     );
     );
   }
   }
 
 
   renderPerformanceIssues() {
   renderPerformanceIssues() {
     const {transaction, barColor} = this.props;
     const {transaction, barColor} = this.props;
-    if (!isTraceFullDetailed(transaction)) {
+    if (isTraceError(transaction) || isTraceRoot(transaction)) {
       return null;
       return null;
     }
     }
 
 
@@ -511,7 +562,7 @@ class TransactionBar extends Component<Props, State> {
     dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
     dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
     scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps;
     scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps;
   }) {
   }) {
-    const {hasGuideAnchor, index} = this.props;
+    const {hasGuideAnchor, index, transaction} = this.props;
     const {showDetail} = this.state;
     const {showDetail} = this.state;
     const {dividerPosition} = dividerHandlerChildrenProps;
     const {dividerPosition} = dividerHandlerChildrenProps;
 
 
@@ -543,6 +594,8 @@ class TransactionBar extends Component<Props, State> {
           style={{
           style={{
             width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
             width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
             paddingTop: 0,
             paddingTop: 0,
+            alignItems: isTraceError(transaction) ? 'normal' : 'center',
+            overflow: isTraceError(transaction) ? 'visible' : 'hidden',
           }}
           }}
           showDetail={showDetail}
           showDetail={showDetail}
           onClick={this.toggleDisplayDetail}
           onClick={this.toggleDisplayDetail}
@@ -572,7 +625,7 @@ class TransactionBar extends Component<Props, State> {
     const {location, organization, isVisible, transaction} = this.props;
     const {location, organization, isVisible, transaction} = this.props;
     const {showDetail} = this.state;
     const {showDetail} = this.state;
 
 
-    if (!isTraceFullDetailed(transaction)) {
+    if (isTraceError(transaction) || isTraceRoot(transaction)) {
       return null;
       return null;
     }
     }
 
 
@@ -593,13 +646,14 @@ class TransactionBar extends Component<Props, State> {
   render() {
   render() {
     const {isVisible, transaction} = this.props;
     const {isVisible, transaction} = this.props;
     const {showDetail} = this.state;
     const {showDetail} = this.state;
-
     return (
     return (
-      <Row
+      <StyledRow
         ref={this.transactionRowDOMRef}
         ref={this.transactionRowDOMRef}
         visible={isVisible}
         visible={isVisible}
         showBorder={showDetail}
         showBorder={showDetail}
-        cursor={isTraceFullDetailed(transaction) ? 'pointer' : 'default'}
+        cursor={
+          isTraceTransaction<TraceFullDetailed>(transaction) ? 'pointer' : 'default'
+        }
       >
       >
         <ScrollbarManager.Consumer>
         <ScrollbarManager.Consumer>
           {scrollbarManagerChildrenProps => (
           {scrollbarManagerChildrenProps => (
@@ -614,7 +668,7 @@ class TransactionBar extends Component<Props, State> {
           )}
           )}
         </ScrollbarManager.Consumer>
         </ScrollbarManager.Consumer>
         {this.renderDetail()}
         {this.renderDetail()}
-      </Row>
+      </StyledRow>
     );
     );
   }
   }
 }
 }
@@ -624,3 +678,14 @@ function getOffset(generation) {
 }
 }
 
 
 export default TransactionBar;
 export default TransactionBar;
+
+const StyledRow = styled(Row)`
+  &,
+  ${RowCellContainer} {
+    overflow: visible;
+  }
+`;
+
+const ErrorLink = styled(Link)`
+  color: ${p => p.theme.error};
+`;

Some files were not shown because too many files changed in this diff