Browse Source

feat(anr-rate): Add "open in issues" cta to anr card (#43157)

This is in the project details page. Will populate
some of the other cards with appropriate CTAs
in a follow up. The ANR card is still behind a flag.

![Screen Shot 2023-01-11 at 8 44 27
PM](https://user-images.githubusercontent.com/63818634/211956057-ac9905e8-758e-42a0-a3cb-ee6f4fee6f00.png)
Shruthi 2 years ago
parent
commit
e20b3fca3a

+ 24 - 5
static/app/components/scoreCard.tsx

@@ -11,18 +11,30 @@ type Props = {
   title: React.ReactNode;
   className?: string;
   help?: React.ReactNode;
+  renderOpenButton?: () => React.ReactNode;
   score?: React.ReactNode;
   trend?: React.ReactNode;
   trendStatus?: 'good' | 'bad';
 };
 
-function ScoreCard({title, score, help, trend, trendStatus, className}: Props) {
+function ScoreCard({
+  title,
+  score,
+  help,
+  trend,
+  trendStatus,
+  className,
+  renderOpenButton,
+}: Props) {
   return (
     <ScorePanel className={className}>
-      <HeaderTitle>
-        <Title>{title}</Title>
-        {help && <QuestionTooltip title={help} size="sm" position="top" />}
-      </HeaderTitle>
+      <HeaderWrapper>
+        <HeaderTitle>
+          <Title>{title}</Title>
+          {help && <QuestionTooltip title={help} size="sm" position="top" />}
+        </HeaderTitle>
+        {renderOpenButton?.()}
+      </HeaderWrapper>
 
       <ScoreWrapper>
         <Score>{score ?? '\u2014'}</Score>
@@ -70,6 +82,13 @@ export const Title = styled('div')`
   font-weight: 600;
 `;
 
+const HeaderWrapper = styled('div')`
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+`;
+
 export const ScoreWrapper = styled('div')`
   display: flex;
   flex-direction: row;

+ 1 - 0
static/app/views/projectDetail/projectDetail.tsx

@@ -289,6 +289,7 @@ class ProjectDetail extends AsyncView<Props, State> {
                   hasTransactions={hasTransactions}
                   query={query}
                   project={project}
+                  location={location}
                 />
                 {isProjectStabilized && (
                   <Fragment>

+ 35 - 1
static/app/views/projectDetail/projectScoreCards/projectAnrScoreCard.spec.tsx

@@ -1,3 +1,4 @@
+import {initializeOrg} from 'sentry-test/initializeOrg';
 import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
 
 import {PageFilters} from 'sentry/types';
@@ -6,7 +7,16 @@ import {ProjectAnrScoreCard} from 'sentry/views/projectDetail/projectScoreCards/
 describe('ProjectDetail > ProjectAnr', function () {
   let endpointMock, endpointMockPreviousPeriod;
 
-  const organization = TestStubs.Organization();
+  const {organization, router, routerContext} = initializeOrg({
+    ...initializeOrg(),
+    router: {
+      ...initializeOrg().router,
+      location: {
+        ...initializeOrg().router.location,
+        query: {project: '1', statsPeriod: '7d'},
+      },
+    },
+  });
 
   const selection = {
     projects: [1],
@@ -64,6 +74,7 @@ describe('ProjectDetail > ProjectAnr', function () {
         selection={selection}
         isProjectStabilized
         query="release:abc"
+        location={router.location}
       />
     );
 
@@ -103,4 +114,27 @@ describe('ProjectDetail > ProjectAnr', function () {
     await waitFor(() => expect(screen.getByText('11.562%')).toBeInTheDocument());
     await waitFor(() => expect(screen.getByText('0.03%')).toBeInTheDocument());
   });
+
+  it('renders open in issues CTA', async function () {
+    organization.features = ['discover-basic', 'anr-rate'];
+    render(
+      <ProjectAnrScoreCard
+        organization={{...organization}}
+        selection={selection}
+        isProjectStabilized
+        query="release:abc"
+        location={router.location}
+      />,
+      {
+        context: routerContext,
+      }
+    );
+
+    await waitFor(() => expect(screen.getByText('11.562%')).toBeInTheDocument());
+
+    expect(screen.getByRole('button', {name: 'Open in Issues'})).toHaveAttribute(
+      'href',
+      '/organizations/org-slug/issues/?project=1&query=mechanism%3AANR%20release%3Aabc&sort=freq&statsPeriod=7d'
+    );
+  });
 });

+ 30 - 0
static/app/views/projectDetail/projectScoreCards/projectAnrScoreCard.tsx

@@ -1,11 +1,15 @@
 import {Fragment, useEffect, useState} from 'react';
+import {Location} from 'history';
+import pick from 'lodash/pick';
 import round from 'lodash/round';
 
 import {doSessionsRequest} from 'sentry/actionCreators/sessions';
+import Button from 'sentry/components/button';
 import {shouldFetchPreviousPeriod} from 'sentry/components/charts/utils';
 import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
 import {parseStatsPeriod} from 'sentry/components/organizations/timeRangeSelector/utils';
 import ScoreCard from 'sentry/components/scoreCard';
+import {URL_PARAM} from 'sentry/constants/pageFilters';
 import {IconArrow} from 'sentry/icons/iconArrow';
 import {t} from 'sentry/locale';
 import {PageFilters} from 'sentry/types';
@@ -20,6 +24,7 @@ import {
 
 type Props = {
   isProjectStabilized: boolean;
+  location: Location;
   organization: Organization;
   selection: PageFilters;
   query?: string;
@@ -29,6 +34,7 @@ export function ProjectAnrScoreCard({
   isProjectStabilized,
   organization,
   selection,
+  location,
   query,
 }: Props) {
   const {environments, projects, datetime} = selection;
@@ -142,6 +148,29 @@ export function ProjectAnrScoreCard({
     ) : null;
   }
 
+  const endpointPath = `/organizations/${organization.slug}/issues/`;
+
+  const issueQuery = ['mechanism:ANR', query].join(' ').trim();
+
+  const queryParams = {
+    ...normalizeDateTimeParams(pick(location.query, [...Object.values(URL_PARAM)])),
+    query: issueQuery,
+    sort: 'freq',
+  };
+
+  const issueSearch = {
+    pathname: endpointPath,
+    query: queryParams,
+  };
+
+  function renderButton() {
+    return (
+      <Button data-test-id="issues-open" size="xs" to={issueSearch}>
+        {t('Open in Issues')}
+      </Button>
+    );
+  }
+
   return (
     <ScoreCard
       title={t('Foreground ANR Rate')}
@@ -149,6 +178,7 @@ export function ProjectAnrScoreCard({
       score={value ? formatPercentage(value, 3) : '\u2014'}
       trend={renderTrend()}
       trendStatus={trendStatus}
+      renderOpenButton={renderButton}
     />
   );
 }

+ 4 - 0
static/app/views/projectDetail/projectScoreCards/projectScoreCards.tsx

@@ -1,4 +1,5 @@
 import styled from '@emotion/styled';
+import {Location} from 'history';
 
 import space from 'sentry/styles/space';
 import {
@@ -16,6 +17,7 @@ import ProjectVelocityScoreCard from './projectVelocityScoreCard';
 type Props = {
   hasSessions: boolean | null;
   isProjectStabilized: boolean;
+  location: Location;
   organization: Organization;
   selection: PageFilters;
   hasTransactions?: boolean;
@@ -30,6 +32,7 @@ function ProjectScoreCards({
   hasSessions,
   hasTransactions,
   query,
+  location,
   project,
 }: Props) {
   return (
@@ -65,6 +68,7 @@ function ProjectScoreCards({
           selection={selection}
           isProjectStabilized={isProjectStabilized}
           query={query}
+          location={location}
         />
       ) : (
         <ProjectApdexScoreCard