Browse Source

feat(anr): Show related perf issues in ANR details (#46325)

Adds a "suspect root issues" section if it's an ANR
event that has the anr-improvements flag enabled
and potential suspect performance issues in the trace.
The only allowed perf issues that can contribute to an
anr are file io on main thread and db calls on the main thread.

![Screenshot 2023-03-24 at 2 32 25
PM](https://user-images.githubusercontent.com/63818634/227610765-e7208f5f-2095-48a6-b2e5-aa637977e30f.png)

blocked by https://github.com/getsentry/sentry/pull/46322
Shruthi 1 year ago
parent
commit
6be3a03f33

+ 113 - 0
static/app/components/events/interfaces/performance/anrRootCause.tsx

@@ -0,0 +1,113 @@
+import {Fragment, useContext} from 'react';
+import styled from '@emotion/styled';
+
+import {EventDataSection} from 'sentry/components/events/eventDataSection';
+import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
+import ShortId from 'sentry/components/group/inboxBadges/shortId';
+import ProjectBadge from 'sentry/components/idBadge/projectBadge';
+import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
+import {Event, Organization} from 'sentry/types';
+import {QuickTraceContext} from 'sentry/utils/performance/quickTrace/quickTraceContext';
+import useProjects from 'sentry/utils/useProjects';
+
+enum AnrRootCauseAllowlist {
+  PerformanceFileIOMainThreadGroupType = 1008,
+  PerformanceDBMainThreadGroupType = 1013,
+}
+
+interface Props {
+  event: Event;
+  organization: Organization;
+}
+
+export function AnrRootCause({organization}: Props) {
+  const quickTrace = useContext(QuickTraceContext);
+  const {projects} = useProjects();
+
+  if (
+    !quickTrace ||
+    quickTrace.error ||
+    quickTrace.trace === null ||
+    quickTrace.trace.length === 0 ||
+    quickTrace.trace[0]?.performance_issues?.length === 0
+  ) {
+    return null;
+  }
+
+  const potentialAnrRootCause = quickTrace.trace[0].performance_issues.filter(issue =>
+    Object.values(AnrRootCauseAllowlist).includes(issue.type as AnrRootCauseAllowlist)
+  );
+
+  if (potentialAnrRootCause.length === 0) {
+    return null;
+  }
+
+  return (
+    <EventDataSection
+      title={t('Suspect Root Issues')}
+      type="suspect-root-issues"
+      help={t(
+        'Suspect Root Issues identifies potential Performance Issues that may be contributing to this ANR.'
+      )}
+    >
+      {potentialAnrRootCause.map(issue => {
+        const project = projects.find(p => p.id === issue.project_id.toString());
+        return (
+          <IssueSummary key={issue.issue_id}>
+            <Title>
+              <TitleWithLink
+                to={{
+                  pathname: `/organizations/${organization.id}/issues/${issue.issue_id}/${
+                    issue.event_id ? `events/${issue.event_id}/` : ''
+                  }`,
+                }}
+              >
+                {issue.title}
+                <Fragment>
+                  <Spacer />
+                  <Subtitle title={issue.culprit}>{issue.culprit}</Subtitle>
+                </Fragment>
+              </TitleWithLink>
+            </Title>
+            {issue.issue_short_id && (
+              <ShortId
+                shortId={issue.issue_short_id}
+                avatar={
+                  project && <ProjectBadge project={project} hideName avatarSize={12} />
+                }
+              />
+            )}
+          </IssueSummary>
+        );
+      })}
+    </EventDataSection>
+  );
+}
+
+const IssueSummary = styled('div')`
+  padding-bottom: ${space(2)};
+`;
+
+/**
+ * &nbsp; is used instead of margin/padding to split title and subtitle
+ * into 2 separate text nodes on the HTML AST. This allows the
+ * title to be highlighted without spilling over to the subtitle.
+ */
+const Spacer = () => <span style={{display: 'inline-block', width: 10}}>&nbsp;</span>;
+
+const Subtitle = styled('div')`
+  font-size: ${p => p.theme.fontSizeRelativeSmall};
+  font-weight: 300;
+  color: ${p => p.theme.subText};
+`;
+
+const TitleWithLink = styled(GlobalSelectionLink)`
+  display: flex;
+  font-weight: 600;
+`;
+
+const Title = styled('div')`
+  line-height: 1;
+  margin-bottom: ${space(0.5)};
+`;

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

@@ -35,8 +35,11 @@ export type TraceError = {
 };
 
 export type TracePerformanceIssue = Omit<TraceError, 'issue' | 'span'> & {
+  culprit: string;
   span: string[];
   suspect_spans: string[];
+  type: number;
+  issue_short_id?: string;
 };
 
 export type TraceLite = EventLite[];

+ 135 - 2
static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx

@@ -7,11 +7,14 @@ import {act, render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
 import {EntryType, Event, Group, IssueCategory, IssueType} from 'sentry/types';
 import {Organization} from 'sentry/types/organization';
 import {Project} from 'sentry/types/project';
+import {QuickTraceEvent} from 'sentry/utils/performance/quickTrace/types';
 import GroupEventDetails, {
   GroupEventDetailsProps,
 } from 'sentry/views/issueDetails/groupEventDetails/groupEventDetails';
 import {ReprocessingStatus} from 'sentry/views/issueDetails/utils';
 
+const TRACE_ID = '797cda4e24844bdc90e0efe741616047';
+
 const makeDefaultMockData = (
   organization?: Organization,
   project?: Project
@@ -32,7 +35,18 @@ const makeDefaultMockData = (
       dateCreated: '2019-03-20T00:00:00.000Z',
       errors: [],
       entries: [],
-      tags: [{key: 'environment', value: 'dev'}],
+      tags: [
+        {key: 'environment', value: 'dev'},
+        {key: 'mechanism', value: 'ANR'},
+      ],
+      contexts: {
+        trace: {
+          trace_id: TRACE_ID,
+          span_id: 'b0e6f15b45c36b12',
+          op: 'ui.action.click',
+          type: 'trace',
+        },
+      },
     }),
   };
 };
@@ -67,11 +81,57 @@ const TestComponent = (props: Partial<GroupEventDetailsProps>) => {
   return <GroupEventDetails {...mergedProps} />;
 };
 
+const mockedTrace = (project: Project) => {
+  return {
+    event_id: '8806ea4691c24fc7b1c77ecd78df574f',
+    span_id: 'b0e6f15b45c36b12',
+    transaction: 'MainActivity.add_attachment',
+    'transaction.duration': 1000,
+    'transaction.op': 'navigation',
+    project_id: parseInt(project.id, 10),
+    project_slug: project.slug,
+    parent_span_id: null,
+    parent_event_id: null,
+    generation: 0,
+    errors: [
+      {
+        event_id: 'c6971a73454646338bc3ec80c70f8891',
+        issue_id: 104,
+        span: 'b0e6f15b45c36b12',
+        project_id: parseInt(project.id, 10),
+        project_slug: project.slug,
+        title: 'ApplicationNotResponding: ANR for at least 5000 ms.',
+        level: 'error',
+        issue: '',
+      },
+    ],
+    performance_issues: [
+      {
+        event_id: '8806ea4691c24fc7b1c77ecd78df574f',
+        issue_id: 110,
+        issue_short_id: 'SENTRY-ANDROID-1R',
+        span: ['b0e6f15b45c36b12'],
+        suspect_spans: ['89930aab9a0314d4'],
+        project_id: parseInt(project.id, 10),
+        project_slug: project.slug,
+        title: 'File IO on Main Thread',
+        level: 'info',
+        culprit: 'MainActivity.add_attachment',
+        type: 1008,
+      },
+    ],
+    timestamp: 1678290375.150561,
+    start_timestamp: 1678290374.150561,
+    children: [],
+  } as QuickTraceEvent;
+};
+
 const mockGroupApis = (
   organization: Organization,
   project: Project,
   group: Group,
-  event: Event
+  event: Event,
+  trace?: QuickTraceEvent
 ) => {
   MockApiClient.addMockResponse({
     url: `/issues/${group.id}/`,
@@ -103,6 +163,16 @@ const mockGroupApis = (
     body: [],
   });
 
+  MockApiClient.addMockResponse({
+    url: `/organizations/${organization.slug}/events-trace/${TRACE_ID}/`,
+    body: trace ? [trace] : [],
+  });
+
+  MockApiClient.addMockResponse({
+    url: `/organizations/${organization.slug}/events-trace-light/${TRACE_ID}/`,
+    body: trace ? [trace] : [],
+  });
+
   MockApiClient.addMockResponse({
     url: `/groups/${group.id}/integrations/`,
     body: [],
@@ -502,4 +572,67 @@ describe('Platform Integrations', () => {
 
     expect(componentsRequest).toHaveBeenCalled();
   });
+
+  describe('ANR Root Cause', () => {
+    beforeEach(() => {
+      MockApiClient.clearMockResponses();
+    });
+    it('shows anr root cause', async () => {
+      const {organization} = initializeOrg();
+      const props = makeDefaultMockData({
+        ...organization,
+        features: ['anr-improvements'],
+      });
+      mockGroupApis(
+        props.organization,
+        props.project,
+        props.group,
+        props.event,
+        mockedTrace(props.project)
+      );
+      const routerContext = TestStubs.routerContext();
+      await act(async () => {
+        render(<TestComponent group={props.group} event={props.event} />, {
+          organization: props.organization,
+          context: routerContext,
+        });
+        await tick();
+      });
+
+      expect(
+        screen.getByRole('heading', {
+          name: /suspect root issues/i,
+        })
+      ).toBeInTheDocument();
+      expect(screen.getByText('File IO on Main Thread')).toBeInTheDocument();
+    });
+
+    it('does not render root issues section if related perf issues do not exist', async () => {
+      const {organization} = initializeOrg();
+      const props = makeDefaultMockData({
+        ...organization,
+        features: ['anr-improvements'],
+      });
+      const trace = mockedTrace(props.project);
+      mockGroupApis(props.organization, props.project, props.group, props.event, {
+        ...trace,
+        performance_issues: [],
+      });
+      const routerContext = TestStubs.routerContext();
+      await act(async () => {
+        render(<TestComponent group={props.group} event={props.event} />, {
+          organization: props.organization,
+          context: routerContext,
+        });
+        await tick();
+      });
+
+      expect(
+        screen.queryByRole('heading', {
+          name: /suspect root issues/i,
+        })
+      ).not.toBeInTheDocument();
+      expect(screen.queryByText('File IO on Main Thread')).not.toBeInTheDocument();
+    });
+  });
 });

+ 6 - 0
static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx

@@ -15,6 +15,7 @@ import {EventSdk} from 'sentry/components/events/eventSdk';
 import {EventTagsAndScreenshot} from 'sentry/components/events/eventTagsAndScreenshot';
 import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy';
 import {EventGroupingInfo} from 'sentry/components/events/groupingInfo';
+import {AnrRootCause} from 'sentry/components/events/interfaces/performance/anrRootCause';
 import {Resources} from 'sentry/components/events/interfaces/performance/resources';
 import {SpanEvidenceSection} from 'sentry/components/events/interfaces/performance/spanEvidence';
 import {EventPackageData} from 'sentry/components/events/packageData';
@@ -67,6 +68,8 @@ const GroupEventDetailsContent = ({
   const organization = useOrganization();
   const location = useLocation();
   const hasReplay = Boolean(event?.tags?.find(({key}) => key === 'replayId')?.value);
+  const isANR = event?.tags?.find(({key}) => key === 'mechanism')?.value === 'ANR';
+  const hasAnrImprovementsFeature = organization.features.includes('anr-improvements');
 
   if (!event) {
     return (
@@ -107,6 +110,9 @@ const GroupEventDetailsContent = ({
       <GroupEventEntry entryType={EntryType.EXCEPTION} {...eventEntryProps} />
       <GroupEventEntry entryType={EntryType.STACKTRACE} {...eventEntryProps} />
       <GroupEventEntry entryType={EntryType.THREADS} {...eventEntryProps} />
+      {hasAnrImprovementsFeature && isANR && (
+        <AnrRootCause event={event} organization={organization} />
+      )}
       {group.issueCategory === IssueCategory.PERFORMANCE && (
         <SpanEvidenceSection
           event={event as EventTransaction}