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

feat(mobile-ui): Render screen list table (#70064)

Queries spans metrics for the new frame metrics and displays them in a
table. The queries are missing a filter on `transaction.op:ui.load` because
that tag was missed on data extraction. There are TODOs to fill it in when
it becomes available.
Nar Saynorath 10 месяцев назад
Родитель
Сommit
e195e15ff3

+ 1 - 1
static/app/views/performance/mobile/components/screensTable.tsx

@@ -63,7 +63,7 @@ export function ScreensTable({
     if (data.meta.fields[column.key] === 'percent_change') {
       return (
         <PercentChangeCell
-          deltaValue={parseFloat(row[column.key] as string)}
+          deltaValue={parseFloat(row[column.key] as string) || 0}
           preferredPolarity="-"
         />
       );

+ 2 - 2
static/app/views/performance/mobile/ui/index.tsx

@@ -1,7 +1,7 @@
 import Feature from 'sentry/components/acl/feature';
 import useOrganization from 'sentry/utils/useOrganization';
 import ScreensTemplate from 'sentry/views/performance/mobile/components/screensTemplate';
-import {ScreensView, YAxis} from 'sentry/views/performance/mobile/screenload/screens';
+import {UIScreens} from 'sentry/views/performance/mobile/ui/screens';
 import {ROUTE_NAMES} from 'sentry/views/starfish/utils/routeNames';
 
 export default function ResponsivenessModule() {
@@ -13,7 +13,7 @@ export default function ResponsivenessModule() {
       organization={organization}
     >
       <ScreensTemplate
-        content={<ScreensView yAxes={[YAxis.SLOW_FRAME_RATE, YAxis.FROZEN_FRAME_RATE]} />}
+        content={<UIScreens />}
         compatibilityProps={{
           compatibleSDKNames: ['sentry.cocoa', 'sentry.java.android'],
           docsUrl: 'www.docs.sentry.io', // TODO: Add real docs URL

+ 3 - 0
static/app/views/performance/mobile/ui/referrers.tsx

@@ -0,0 +1,3 @@
+export enum Referrer {
+  OVERVIEW_SCREENS_TABLE = 'api.performance.module.ui.screen-table',
+}

+ 86 - 0
static/app/views/performance/mobile/ui/screens/index.spec.tsx

@@ -0,0 +1,86 @@
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import usePageFilters from 'sentry/utils/usePageFilters';
+import {Referrer} from 'sentry/views/performance/mobile/ui/referrers';
+import {UIScreens} from 'sentry/views/performance/mobile/ui/screens';
+import {useReleaseSelection} from 'sentry/views/starfish/queries/useReleases';
+
+jest.mock('sentry/utils/usePageFilters');
+jest.mock('sentry/views/starfish/queries/useReleases');
+
+jest.mocked(useReleaseSelection).mockReturnValue({
+  primaryRelease: 'com.example.vu.android@2.10.5',
+  isLoading: false,
+  secondaryRelease: 'com.example.vu.android@2.10.3+42',
+});
+
+describe('Performance Mobile UI Screens', () => {
+  const project = ProjectFixture({platform: 'apple-ios'});
+
+  beforeEach(() => {
+    MockApiClient.clearMockResponses();
+
+    jest.mocked(usePageFilters).mockReturnValue({
+      isReady: true,
+      desyncedFilters: new Set(),
+      pinnedFilters: new Set(),
+      shouldPersist: true,
+      selection: {
+        datetime: {
+          period: '10d',
+          start: null,
+          end: null,
+          utc: false,
+        },
+        environments: [],
+        projects: [parseInt(project.id, 10)],
+      },
+    });
+  });
+
+  it('queries for the correct table data', async () => {
+    const tableRequestMock = MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/events/',
+      body: [],
+      match: [MockApiClient.matchQuery({referrer: Referrer.OVERVIEW_SCREENS_TABLE})],
+    });
+
+    render(<UIScreens />);
+
+    expect(await screen.findByRole('columnheader', {name: 'Screen'})).toBeInTheDocument();
+    [
+      'Slow (R1)',
+      'Slow (R2)',
+      'Frozen (R1)',
+      'Frozen (R2)',
+      'Delay (R1)',
+      'Delay (R2)',
+    ].forEach(header => {
+      expect(screen.getByRole('columnheader', {name: header})).toBeInTheDocument();
+    });
+    expect(screen.getAllByRole('columnheader', {name: 'Change'})).toHaveLength(3);
+
+    expect(tableRequestMock).toHaveBeenCalledWith(
+      '/organizations/org-slug/events/',
+      expect.objectContaining({
+        query: expect.objectContaining({
+          field: [
+            'project.id',
+            'transaction',
+            'avg_if(mobile.slow_frames,release,com.example.vu.android@2.10.5)',
+            'avg_if(mobile.slow_frames,release,com.example.vu.android@2.10.3+42)',
+            'avg_if(mobile.frozen_frames,release,com.example.vu.android@2.10.5)',
+            'avg_if(mobile.frozen_frames,release,com.example.vu.android@2.10.3+42)',
+            'avg_if(mobile.frames_delay,release,com.example.vu.android@2.10.5)',
+            'avg_if(mobile.frames_delay,release,com.example.vu.android@2.10.3+42)',
+            'avg_compare(mobile.slow_frames,release,com.example.vu.android@2.10.5,com.example.vu.android@2.10.3+42)',
+            'avg_compare(mobile.frozen_frames,release,com.example.vu.android@2.10.5,com.example.vu.android@2.10.3+42)',
+            'avg_compare(mobile.frames_delay,release,com.example.vu.android@2.10.5,com.example.vu.android@2.10.3+42)',
+          ],
+        }),
+      })
+    );
+  });
+});

+ 123 - 0
static/app/views/performance/mobile/ui/screens/index.tsx

@@ -0,0 +1,123 @@
+import styled from '@emotion/styled';
+
+import SearchBar from 'sentry/components/performance/searchBar';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {NewQuery} from 'sentry/types';
+import EventView from 'sentry/utils/discover/eventView';
+import {DiscoverDatasets} from 'sentry/utils/discover/types';
+import {decodeScalar} from 'sentry/utils/queryString';
+import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import useRouter from 'sentry/utils/useRouter';
+import {prepareQueryForLandingPage} from 'sentry/views/performance/data';
+import {getFreeTextFromQuery} from 'sentry/views/performance/mobile/screenload/screens';
+import {useTableQuery} from 'sentry/views/performance/mobile/screenload/screens/screensTable';
+import {Referrer} from 'sentry/views/performance/mobile/ui/referrers';
+import {UIScreensTable} from 'sentry/views/performance/mobile/ui/screens/table';
+import {getTransactionSearchQuery} from 'sentry/views/performance/utils';
+import {useReleaseSelection} from 'sentry/views/starfish/queries/useReleases';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
+import {appendReleaseFilters} from 'sentry/views/starfish/utils/releaseComparison';
+
+export function UIScreens() {
+  const router = useRouter();
+  const {selection} = usePageFilters();
+  const location = useLocation();
+  const organization = useOrganization();
+  const {query: locationQuery} = location;
+
+  const {
+    primaryRelease,
+    secondaryRelease,
+    isLoading: isReleasesLoading,
+  } = useReleaseSelection();
+
+  // TODO: Add transaction.op:ui.load when collecting begins
+  const query = new MutableSearch([]);
+
+  const searchQuery = decodeScalar(locationQuery.query, '');
+  if (searchQuery) {
+    query.addStringFilter(prepareQueryForLandingPage(searchQuery, false));
+  }
+
+  const queryString = appendReleaseFilters(query, primaryRelease, secondaryRelease);
+
+  // TODO: Replace with a default sort on the count column when added
+  const orderby = decodeScalar(locationQuery.sort, '');
+  const newQuery: NewQuery = {
+    name: '',
+    fields: [
+      SpanMetricsField.PROJECT_ID,
+      'transaction',
+      `avg_if(mobile.slow_frames,release,${primaryRelease})`,
+      `avg_if(mobile.slow_frames,release,${secondaryRelease})`,
+      `avg_if(mobile.frozen_frames,release,${primaryRelease})`,
+      `avg_if(mobile.frozen_frames,release,${secondaryRelease})`,
+      `avg_if(mobile.frames_delay,release,${primaryRelease})`,
+      `avg_if(mobile.frames_delay,release,${secondaryRelease})`,
+      `avg_compare(mobile.slow_frames,release,${primaryRelease},${secondaryRelease})`,
+      `avg_compare(mobile.frozen_frames,release,${primaryRelease},${secondaryRelease})`,
+      `avg_compare(mobile.frames_delay,release,${primaryRelease},${secondaryRelease})`,
+    ],
+    query: queryString,
+    dataset: DiscoverDatasets.SPANS_METRICS,
+    version: 2,
+    projects: selection.projects,
+  };
+  newQuery.orderby = orderby;
+  const tableEventView = EventView.fromNewQueryWithLocation(newQuery, location);
+
+  const {
+    data: topTransactionsData,
+    isLoading: topTransactionsLoading,
+    pageLinks,
+  } = useTableQuery({
+    eventView: tableEventView,
+    enabled: !isReleasesLoading,
+    referrer: Referrer.OVERVIEW_SCREENS_TABLE,
+  });
+
+  // TODO: Add transaction.op:ui.load when collecting begins
+  const tableSearchFilters = new MutableSearch([]);
+
+  const derivedQuery = getTransactionSearchQuery(location, tableEventView.query);
+
+  return (
+    <div>
+      <StyledSearchBar
+        eventView={tableEventView}
+        onSearch={search => {
+          router.push({
+            pathname: router.location.pathname,
+            query: {
+              ...location.query,
+              cursor: undefined,
+              query: String(search).trim() || undefined,
+            },
+          });
+        }}
+        organization={organization}
+        query={getFreeTextFromQuery(derivedQuery)}
+        placeholder={t('Search for Screen')}
+        additionalConditions={
+          new MutableSearch(
+            appendReleaseFilters(tableSearchFilters, primaryRelease, secondaryRelease)
+          )
+        }
+      />
+      <UIScreensTable
+        eventView={tableEventView}
+        data={topTransactionsData}
+        isLoading={topTransactionsLoading}
+        pageLinks={pageLinks}
+      />
+    </div>
+  );
+}
+
+const StyledSearchBar = styled(SearchBar)`
+  margin-bottom: ${space(1)};
+`;

+ 132 - 0
static/app/views/performance/mobile/ui/screens/table.tsx

@@ -0,0 +1,132 @@
+import {Fragment} from 'react';
+import * as qs from 'query-string';
+
+import Link from 'sentry/components/links/link';
+import {t} from 'sentry/locale';
+import type {TableData} from 'sentry/utils/discover/discoverQuery';
+import type EventView from 'sentry/utils/discover/eventView';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import {normalizeUrl} from 'sentry/utils/withDomainRequired';
+import TopResultsIndicator from 'sentry/views/discover/table/topResultsIndicator';
+import {ScreensTable} from 'sentry/views/performance/mobile/components/screensTable';
+import {TOP_SCREENS} from 'sentry/views/performance/mobile/screenload/screens';
+import {
+  PRIMARY_RELEASE_ALIAS,
+  SECONDARY_RELEASE_ALIAS,
+} from 'sentry/views/starfish/components/releaseSelector';
+import {useReleaseSelection} from 'sentry/views/starfish/queries/useReleases';
+
+type Props = {
+  data: TableData | undefined;
+  eventView: EventView;
+  isLoading: boolean;
+  pageLinks: string | undefined;
+};
+
+export function UIScreensTable({data, eventView, isLoading, pageLinks}: Props) {
+  const location = useLocation();
+  const organization = useOrganization();
+  const {primaryRelease, secondaryRelease} = useReleaseSelection();
+
+  const columnNameMap = {
+    transaction: t('Screen'),
+    [`avg_if(mobile.slow_frames,release,${primaryRelease})`]: t(
+      'Slow (%s)',
+      PRIMARY_RELEASE_ALIAS
+    ),
+    [`avg_if(mobile.slow_frames,release,${secondaryRelease})`]: t(
+      'Slow (%s)',
+      SECONDARY_RELEASE_ALIAS
+    ),
+    [`avg_if(mobile.frozen_frames,release,${primaryRelease})`]: t(
+      'Frozen (%s)',
+      PRIMARY_RELEASE_ALIAS
+    ),
+    [`avg_if(mobile.frozen_frames,release,${secondaryRelease})`]: t(
+      'Frozen (%s)',
+      SECONDARY_RELEASE_ALIAS
+    ),
+    [`avg_if(mobile.frames_delay,release,${primaryRelease})`]: t(
+      'Delay (%s)',
+      PRIMARY_RELEASE_ALIAS
+    ),
+    [`avg_if(mobile.frames_delay,release,${secondaryRelease})`]: t(
+      'Delay (%s)',
+      SECONDARY_RELEASE_ALIAS
+    ),
+    [`avg_compare(mobile.slow_frames,release,${primaryRelease},${secondaryRelease})`]:
+      t('Change'),
+    [`avg_compare(mobile.frozen_frames,release,${primaryRelease},${secondaryRelease})`]:
+      t('Change'),
+    [`avg_compare(mobile.frames_delay,release,${primaryRelease},${secondaryRelease})`]:
+      t('Change'),
+    // TODO: Counts
+  };
+
+  function renderBodyCell(column, row): React.ReactNode | null {
+    if (!data) {
+      return null;
+    }
+
+    const index = data.data.indexOf(row);
+
+    const field = String(column.key);
+
+    if (field === 'transaction') {
+      return (
+        <Fragment>
+          <TopResultsIndicator count={TOP_SCREENS} index={index} />
+          <Link
+            to={normalizeUrl(
+              `/organizations/${
+                organization.slug
+              }/performance/mobile/ui/spans/?${qs.stringify({
+                ...location.query,
+                project: row['project.id'],
+                transaction: row.transaction,
+                primaryRelease,
+                secondaryRelease,
+              })}`
+            )}
+            style={{display: `block`, width: `100%`}}
+          >
+            {row.transaction}
+          </Link>
+        </Fragment>
+      );
+    }
+
+    return null;
+  }
+
+  return (
+    <ScreensTable
+      columnNameMap={columnNameMap}
+      data={data}
+      eventView={eventView}
+      isLoading={isLoading}
+      pageLinks={pageLinks}
+      columnOrder={[
+        'transaction',
+        `avg_if(mobile.slow_frames,release,${primaryRelease})`,
+        `avg_if(mobile.slow_frames,release,${secondaryRelease})`,
+        `avg_compare(mobile.slow_frames,release,${primaryRelease},${secondaryRelease})`,
+        `avg_if(mobile.frozen_frames,release,${primaryRelease})`,
+        `avg_if(mobile.frozen_frames,release,${secondaryRelease})`,
+        `avg_compare(mobile.frozen_frames,release,${primaryRelease},${secondaryRelease})`,
+        `avg_if(mobile.frames_delay,release,${primaryRelease})`,
+        `avg_if(mobile.frames_delay,release,${secondaryRelease})`,
+        `avg_compare(mobile.frames_delay,release,${primaryRelease},${secondaryRelease})`,
+      ]}
+      // TODO: Add default sort on count column
+      defaultSort={[
+        {
+          key: `avg_compare(mobile.frames_delay,release,${primaryRelease},${secondaryRelease})`,
+          order: 'desc',
+        },
+      ]}
+      customBodyCellRenderer={renderBodyCell}
+    />
+  );
+}