Просмотр исходного кода

feat(anr-rate): Add ANR score card to project details (#42949)

Adds ANR scorecard to project details page
replacing Apdex if it's an android project.

![Screen Shot 2023-01-09 at 10 47 25
AM](https://user-images.githubusercontent.com/63818634/211349223-34106b7f-0d57-49a9-9de3-ce3b741c8364.png)

closes https://github.com/getsentry/sentry/issues/42952
Shruthi 2 лет назад
Родитель
Сommit
5aba4765c4

+ 6 - 0
static/app/actionCreators/sessions.tsx

@@ -12,6 +12,8 @@ export type DoSessionsRequestOptions = {
   environment?: Readonly<string[]>;
   groupBy?: string[];
   includeAllArgs?: boolean;
+  includeSeries?: boolean;
+  includeTotals?: boolean;
   interval?: string;
   limit?: number;
   orderBy?: string;
@@ -36,6 +38,8 @@ export const doSessionsRequest = (
     orderBy,
     query,
     includeAllArgs = false,
+    includeSeries,
+    includeTotals,
     statsPeriodStart,
     statsPeriodEnd,
     limit,
@@ -62,6 +66,8 @@ export const doSessionsRequest = (
       statsPeriod,
       statsPeriodStart,
       statsPeriodEnd,
+      includeSeries: includeSeries === false ? '0' : '1',
+      includeTotals: includeTotals === false ? '0' : '1',
     }).filter(([, value]) => defined(value) && value !== '')
   );
 

+ 2 - 2
static/app/views/dashboardsV2/widgetCard/releaseWidgetQueries.spec.tsx

@@ -215,14 +215,14 @@ describe('Dashboards > ReleaseWidgetQueries', function () {
     expect(mock).toHaveBeenCalledWith(
       '/organizations/org-slug/sessions/',
       expect.objectContaining({
-        query: {
+        query: expect.objectContaining({
           environment: ['prod'],
           field: ['count_unique(user)'],
           groupBy: ['session.status'],
           interval: '30m',
           project: [1],
           statsPeriod: '14d',
-        },
+        }),
       })
     );
   });

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

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

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

@@ -0,0 +1,106 @@
+import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
+
+import {PageFilters} from 'sentry/types';
+import {ProjectAnrScoreCard} from 'sentry/views/projectDetail/projectScoreCards/projectAnrScoreCard';
+
+describe('ProjectDetail > ProjectAnr', function () {
+  let endpointMock, endpointMockPreviousPeriod;
+
+  const organization = TestStubs.Organization();
+
+  const selection = {
+    projects: [1],
+    environments: [],
+    datetime: {
+      period: '7d',
+      start: null,
+      end: null,
+      utc: false,
+    },
+  } as PageFilters;
+
+  beforeEach(function () {
+    endpointMock = MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/sessions/`,
+      match: [MockApiClient.matchQuery({statsPeriod: '7d'})],
+      body: {
+        groups: [
+          {
+            by: {},
+            totals: {
+              'foreground_anr_rate()': 0.11561866125760649,
+            },
+          },
+        ],
+      },
+      status: 200,
+    });
+
+    endpointMockPreviousPeriod = MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/sessions/`,
+      match: [MockApiClient.matchQuery({start: '2017-10-03T02:41:20.000'})], // setup mocks a constant current date, so this works
+      body: {
+        groups: [
+          {
+            by: {},
+            totals: {
+              'foreground_anr_rate()': 0.08558558558558559,
+            },
+          },
+        ],
+      },
+      status: 200,
+    });
+  });
+
+  afterEach(function () {
+    MockApiClient.clearMockResponses();
+  });
+
+  it('calls api with anr rate', async function () {
+    render(
+      <ProjectAnrScoreCard
+        organization={{...organization}}
+        selection={selection}
+        isProjectStabilized
+        query="release:abc"
+      />
+    );
+
+    expect(endpointMock).toHaveBeenCalledWith(
+      `/organizations/${organization.slug}/sessions/`,
+      expect.objectContaining({
+        query: {
+          environment: [],
+          field: ['foreground_anr_rate()'],
+          includeSeries: '0',
+          includeTotals: '1',
+          interval: '1h',
+          project: [1],
+          query: 'release:abc',
+          statsPeriod: '7d',
+        },
+      })
+    );
+
+    expect(endpointMockPreviousPeriod).toHaveBeenCalledWith(
+      `/organizations/${organization.slug}/sessions/`,
+      expect.objectContaining({
+        query: {
+          end: '2017-10-10T02:41:20.000',
+          environment: [],
+          field: ['foreground_anr_rate()'],
+          includeSeries: '0',
+          includeTotals: '1',
+          interval: '1h',
+          project: [1],
+          query: 'release:abc',
+          start: '2017-10-03T02:41:20.000',
+        },
+      })
+    );
+
+    await waitFor(() => expect(screen.getByText('11.562%')).toBeInTheDocument());
+    await waitFor(() => expect(screen.getByText('0.03%')).toBeInTheDocument());
+  });
+});

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

@@ -0,0 +1,154 @@
+import {Fragment, useEffect, useState} from 'react';
+import round from 'lodash/round';
+
+import {doSessionsRequest} from 'sentry/actionCreators/sessions';
+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 {IconArrow} from 'sentry/icons/iconArrow';
+import {t} from 'sentry/locale';
+import {PageFilters} from 'sentry/types';
+import {Organization, SessionApiResponse} from 'sentry/types/organization';
+import {formatAbbreviatedNumber, formatPercentage} from 'sentry/utils/formatters';
+import {getPeriod} from 'sentry/utils/getPeriod';
+import useApi from 'sentry/utils/useApi';
+import {
+  getSessionTermDescription,
+  SessionTerm,
+} from 'sentry/views/releases/utils/sessionTerm';
+
+type Props = {
+  isProjectStabilized: boolean;
+  organization: Organization;
+  selection: PageFilters;
+  query?: string;
+};
+
+export function ProjectAnrScoreCard({
+  isProjectStabilized,
+  organization,
+  selection,
+  query,
+}: Props) {
+  const {environments, projects, datetime} = selection;
+  const {start, end, period} = datetime;
+
+  const api = useApi();
+
+  const [sessionsData, setSessionsData] = useState<SessionApiResponse | null>(null);
+  const [previousSessionData, setPreviousSessionsData] =
+    useState<SessionApiResponse | null>(null);
+
+  useEffect(() => {
+    let unmounted = false;
+
+    const requestData = {
+      orgSlug: organization.slug,
+      field: ['foreground_anr_rate()'],
+      environment: environments,
+      project: projects,
+      query,
+      includeSeries: false,
+    };
+
+    doSessionsRequest(api, {...requestData, ...normalizeDateTimeParams(datetime)}).then(
+      response => {
+        if (unmounted) {
+          return;
+        }
+
+        setSessionsData(response);
+      }
+    );
+    return () => {
+      unmounted = true;
+    };
+  }, [api, datetime, environments, organization.slug, projects, query]);
+
+  useEffect(() => {
+    let unmounted = false;
+    if (
+      !shouldFetchPreviousPeriod({
+        start,
+        end,
+        period,
+      })
+    ) {
+      setPreviousSessionsData(null);
+    } else {
+      const requestData = {
+        orgSlug: organization.slug,
+        field: ['foreground_anr_rate()'],
+        environment: environments,
+        project: projects,
+        query,
+        includeSeries: false,
+      };
+
+      const {start: previousStart} = parseStatsPeriod(
+        getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: true})
+          .statsPeriod!
+      );
+
+      const {start: previousEnd} = parseStatsPeriod(
+        getPeriod({period, start: undefined, end: undefined}, {shouldDoublePeriod: false})
+          .statsPeriod!
+      );
+
+      doSessionsRequest(api, {
+        ...requestData,
+        start: previousStart,
+        end: previousEnd,
+      }).then(response => {
+        if (unmounted) {
+          return;
+        }
+
+        setPreviousSessionsData(response);
+      });
+    }
+    return () => {
+      unmounted = true;
+    };
+  }, [start, end, period, api, organization.slug, environments, projects, query]);
+
+  const value = sessionsData
+    ? sessionsData.groups[0].totals['foreground_anr_rate()']
+    : null;
+
+  const previousValue = previousSessionData
+    ? previousSessionData.groups[0].totals['foreground_anr_rate()']
+    : null;
+
+  const hasCurrentAndPrevious = previousValue && value;
+  const trend = hasCurrentAndPrevious ? round(value - previousValue, 4) : null;
+  const trendStatus = !trend ? undefined : trend < 0 ? 'good' : 'bad';
+
+  if (!isProjectStabilized) {
+    return null;
+  }
+
+  function renderTrend() {
+    return trend ? (
+      <Fragment>
+        {trend >= 0 ? (
+          <IconArrow direction="up" size="xs" />
+        ) : (
+          <IconArrow direction="down" size="xs" />
+        )}
+        {`${formatAbbreviatedNumber(Math.abs(trend))}\u0025`}
+      </Fragment>
+    ) : null;
+  }
+
+  return (
+    <ScoreCard
+      title={t('Foreground ANR Rate')}
+      help={getSessionTermDescription(SessionTerm.FOREGROUND_ANR_RATE, null)}
+      score={value ? formatPercentage(value, 3) : '\u2014'}
+      trend={renderTrend()}
+      trendStatus={trendStatus}
+    />
+  );
+}

+ 25 - 8
static/app/views/projectDetail/projectScoreCards/projectScoreCards.tsx

@@ -1,8 +1,14 @@
 import styled from '@emotion/styled';
 
 import space from 'sentry/styles/space';
-import {Organization, PageFilters, SessionFieldWithOperation} from 'sentry/types';
+import {
+  Organization,
+  PageFilters,
+  Project,
+  SessionFieldWithOperation,
+} from 'sentry/types';
 
+import {ProjectAnrScoreCard} from './projectAnrScoreCard';
 import ProjectApdexScoreCard from './projectApdexScoreCard';
 import ProjectStabilityScoreCard from './projectStabilityScoreCard';
 import ProjectVelocityScoreCard from './projectVelocityScoreCard';
@@ -13,6 +19,7 @@ type Props = {
   organization: Organization;
   selection: PageFilters;
   hasTransactions?: boolean;
+  project?: Project;
   query?: string;
 };
 
@@ -23,6 +30,7 @@ function ProjectScoreCards({
   hasSessions,
   hasTransactions,
   query,
+  project,
 }: Props) {
   return (
     <CardWrapper>
@@ -51,13 +59,22 @@ function ProjectScoreCards({
         query={query}
       />
 
-      <ProjectApdexScoreCard
-        organization={organization}
-        selection={selection}
-        isProjectStabilized={isProjectStabilized}
-        hasTransactions={hasTransactions}
-        query={query}
-      />
+      {organization.features.includes('anr-rate') && project?.platform === 'android' ? (
+        <ProjectAnrScoreCard
+          organization={organization}
+          selection={selection}
+          isProjectStabilized={isProjectStabilized}
+          query={query}
+        />
+      ) : (
+        <ProjectApdexScoreCard
+          organization={organization}
+          selection={selection}
+          isProjectStabilized={isProjectStabilized}
+          hasTransactions={hasTransactions}
+          query={query}
+        />
+      )}
     </CardWrapper>
   );
 }