Browse Source

feat(starfish): Add new mobile module routes (#54412)

Adds new routes for some of the new mobile
starfish modules. Adds a few charts. From the
initial design sessions, it seems like there will
be some consistency in what we present in these
views - metric charts + a screens table + some
kind of a device classification breakdown.

![Screenshot 2023-08-08 at 5 13 56
PM](https://github.com/getsentry/sentry/assets/63818634/dfb730d4-3cd0-4224-9f2f-082ca91330ee)
![Screenshot 2023-08-08 at 5 18 09
PM](https://github.com/getsentry/sentry/assets/63818634/2113d6cd-c736-4a5c-8816-dcd170c1e0d3)
![Screenshot 2023-08-08 at 5 17 47
PM](https://github.com/getsentry/sentry/assets/63818634/d91b9db3-6d5c-4804-9e19-63ea64adb3d4)
Shruthi 1 year ago
parent
commit
d4c3f900a3

+ 19 - 0
static/app/routes.tsx

@@ -1690,6 +1690,25 @@ function buildRoutes() {
           component={make(() => import('sentry/views/starfish/views/spanSummaryPage'))}
         />
       </Route>
+      <Route path="initialization/">
+        <IndexRoute
+          component={make(
+            () => import('sentry/views/starfish/modules/mobile/initialization')
+          )}
+        />
+      </Route>
+      <Route path="pageload/">
+        <IndexRoute
+          component={make(() => import('sentry/views/starfish/modules/mobile/pageload'))}
+        />
+      </Route>
+      <Route path="responsiveness/">
+        <IndexRoute
+          component={make(
+            () => import('sentry/views/starfish/modules/mobile/responsiveness')
+          )}
+        />
+      </Route>
       <Route path="spans/">
         <IndexRoute component={make(() => import('sentry/views/starfish/views/spans'))} />
         <Route

+ 13 - 0
static/app/views/starfish/components/releaseSelector.tsx

@@ -2,6 +2,7 @@ import {browserHistory} from 'react-router';
 import styled from '@emotion/styled';
 
 import {CompactSelect} from 'sentry/components/compactSelect';
+import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import {t} from 'sentry/locale';
 import {defined} from 'sentry/utils';
 import {decodeScalar} from 'sentry/utils/queryString';
@@ -52,6 +53,18 @@ export function ReleaseSelector({selectorName, selectorKey}: Props) {
   );
 }
 
+export function ReleaseComparisonSelector() {
+  return (
+    <PageFilterBar condensed>
+      <ReleaseSelector selectorKey="primaryRelease" selectorName={t('Primary Release')} />
+      <ReleaseSelector
+        selectorKey="secondaryRelease"
+        selectorName={t('Secondary Release')}
+      />
+    </PageFilterBar>
+  );
+}
+
 const StyledCompactSelect = styled(CompactSelect)`
   @media (min-width: ${p => p.theme.breakpoints.small}) {
     max-width: 300px;

+ 63 - 0
static/app/views/starfish/modules/mobile/initialization.tsx

@@ -0,0 +1,63 @@
+import styled from '@emotion/styled';
+
+import * as Layout from 'sentry/components/layouts/thirds';
+import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import {space} from 'sentry/styles/space';
+import {
+  PageErrorAlert,
+  PageErrorProvider,
+} from 'sentry/utils/performance/contexts/pageError';
+import useOrganization from 'sentry/utils/useOrganization';
+import StarfishDatePicker from 'sentry/views/starfish/components/datePicker';
+import {ReleaseComparisonSelector} from 'sentry/views/starfish/components/releaseSelector';
+import {StarfishPageFiltersContainer} from 'sentry/views/starfish/components/starfishPageFiltersContainer';
+import {StarfishProjectSelector} from 'sentry/views/starfish/components/starfishProjectSelector';
+import {ROUTE_NAMES} from 'sentry/views/starfish/utils/routeNames';
+import {ScreensView, YAxis} from 'sentry/views/starfish/views/screens';
+
+export default function InitializationModule() {
+  const organization = useOrganization();
+
+  return (
+    <SentryDocumentTitle title={ROUTE_NAMES.initialization} orgSlug={organization.slug}>
+      <Layout.Page>
+        <PageErrorProvider>
+          <Layout.Header>
+            <Layout.HeaderContent>
+              <Layout.Title>{ROUTE_NAMES.initialization}</Layout.Title>
+            </Layout.HeaderContent>
+          </Layout.Header>
+
+          <Layout.Body>
+            <Layout.Main fullWidth>
+              <PageErrorAlert />
+              <StarfishPageFiltersContainer>
+                <SearchContainerWithFilterAndMetrics>
+                  <PageFilterBar condensed>
+                    <StarfishProjectSelector />
+                    <StarfishDatePicker />
+                  </PageFilterBar>
+                  <ReleaseComparisonSelector />
+                </SearchContainerWithFilterAndMetrics>
+                <ScreensView yAxes={[YAxis.WARM_START, YAxis.COLD_START]} />
+              </StarfishPageFiltersContainer>
+            </Layout.Main>
+          </Layout.Body>
+        </PageErrorProvider>
+      </Layout.Page>
+    </SentryDocumentTitle>
+  );
+}
+
+const SearchContainerWithFilterAndMetrics = styled('div')`
+  display: grid;
+  grid-template-rows: auto auto auto;
+  gap: ${space(2)};
+  margin-bottom: ${space(2)};
+
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    grid-template-rows: auto;
+    grid-template-columns: auto 1fr auto;
+  }
+`;

+ 63 - 0
static/app/views/starfish/modules/mobile/pageload.tsx

@@ -0,0 +1,63 @@
+import styled from '@emotion/styled';
+
+import * as Layout from 'sentry/components/layouts/thirds';
+import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import {space} from 'sentry/styles/space';
+import {
+  PageErrorAlert,
+  PageErrorProvider,
+} from 'sentry/utils/performance/contexts/pageError';
+import useOrganization from 'sentry/utils/useOrganization';
+import StarfishDatePicker from 'sentry/views/starfish/components/datePicker';
+import {ReleaseComparisonSelector} from 'sentry/views/starfish/components/releaseSelector';
+import {StarfishPageFiltersContainer} from 'sentry/views/starfish/components/starfishPageFiltersContainer';
+import {StarfishProjectSelector} from 'sentry/views/starfish/components/starfishProjectSelector';
+import {ROUTE_NAMES} from 'sentry/views/starfish/utils/routeNames';
+import {ScreensView, YAxis} from 'sentry/views/starfish/views/screens';
+
+export default function PageloadModule() {
+  const organization = useOrganization();
+
+  return (
+    <SentryDocumentTitle title={ROUTE_NAMES.pageload} orgSlug={organization.slug}>
+      <Layout.Page>
+        <PageErrorProvider>
+          <Layout.Header>
+            <Layout.HeaderContent>
+              <Layout.Title>{ROUTE_NAMES.pageload}</Layout.Title>
+            </Layout.HeaderContent>
+          </Layout.Header>
+
+          <Layout.Body>
+            <Layout.Main fullWidth>
+              <PageErrorAlert />
+              <StarfishPageFiltersContainer>
+                <SearchContainerWithFilterAndMetrics>
+                  <PageFilterBar condensed>
+                    <StarfishProjectSelector />
+                    <StarfishDatePicker />
+                  </PageFilterBar>
+                  <ReleaseComparisonSelector />
+                </SearchContainerWithFilterAndMetrics>
+                <ScreensView yAxes={[YAxis.TTID, YAxis.TTFD]} />
+              </StarfishPageFiltersContainer>
+            </Layout.Main>
+          </Layout.Body>
+        </PageErrorProvider>
+      </Layout.Page>
+    </SentryDocumentTitle>
+  );
+}
+
+const SearchContainerWithFilterAndMetrics = styled('div')`
+  display: grid;
+  grid-template-rows: auto auto auto;
+  gap: ${space(2)};
+  margin-bottom: ${space(2)};
+
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    grid-template-rows: auto;
+    grid-template-columns: auto 1fr auto;
+  }
+`;

+ 63 - 0
static/app/views/starfish/modules/mobile/responsiveness.tsx

@@ -0,0 +1,63 @@
+import styled from '@emotion/styled';
+
+import * as Layout from 'sentry/components/layouts/thirds';
+import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import {space} from 'sentry/styles/space';
+import {
+  PageErrorAlert,
+  PageErrorProvider,
+} from 'sentry/utils/performance/contexts/pageError';
+import useOrganization from 'sentry/utils/useOrganization';
+import StarfishDatePicker from 'sentry/views/starfish/components/datePicker';
+import {ReleaseComparisonSelector} from 'sentry/views/starfish/components/releaseSelector';
+import {StarfishPageFiltersContainer} from 'sentry/views/starfish/components/starfishPageFiltersContainer';
+import {StarfishProjectSelector} from 'sentry/views/starfish/components/starfishProjectSelector';
+import {ROUTE_NAMES} from 'sentry/views/starfish/utils/routeNames';
+import {ScreensView, YAxis} from 'sentry/views/starfish/views/screens';
+
+export default function ResponsivenessModule() {
+  const organization = useOrganization();
+
+  return (
+    <SentryDocumentTitle title={ROUTE_NAMES.responsiveness} orgSlug={organization.slug}>
+      <Layout.Page>
+        <PageErrorProvider>
+          <Layout.Header>
+            <Layout.HeaderContent>
+              <Layout.Title>{ROUTE_NAMES.responsiveness}</Layout.Title>
+            </Layout.HeaderContent>
+          </Layout.Header>
+
+          <Layout.Body>
+            <Layout.Main fullWidth>
+              <PageErrorAlert />
+              <StarfishPageFiltersContainer>
+                <SearchContainerWithFilterAndMetrics>
+                  <PageFilterBar condensed>
+                    <StarfishProjectSelector />
+                    <StarfishDatePicker />
+                  </PageFilterBar>
+                  <ReleaseComparisonSelector />
+                </SearchContainerWithFilterAndMetrics>
+                <ScreensView yAxes={[YAxis.SLOW_FRAME_RATE, YAxis.FROZEN_FRAME_RATE]} />
+              </StarfishPageFiltersContainer>
+            </Layout.Main>
+          </Layout.Body>
+        </PageErrorProvider>
+      </Layout.Page>
+    </SentryDocumentTitle>
+  );
+}
+
+const SearchContainerWithFilterAndMetrics = styled('div')`
+  display: grid;
+  grid-template-rows: auto auto auto;
+  gap: ${space(2)};
+  margin-bottom: ${space(2)};
+
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    grid-template-rows: auto;
+    grid-template-columns: auto 1fr auto;
+  }
+`;

+ 3 - 0
static/app/views/starfish/utils/routeNames.tsx

@@ -6,4 +6,7 @@ export const ROUTE_NAMES = {
   'endpoint-overview': t('Endpoint Overview'),
   'span-summary': t('Span Summary'),
   'web-service': t('Web Service'),
+  initialization: t('App Initialization'),
+  pageload: t('Pageload'),
+  responsiveness: t('Responsiveness'),
 };

+ 11 - 11
static/app/views/starfish/views/mobileServiceView/index.tsx

@@ -38,11 +38,11 @@ export function MobileStarfishView() {
   const location = useLocation();
   const {data: releases, isLoading: isReleasesLoading} = useReleases();
 
-  const release1 =
-    decodeScalar(location.query.release1) ?? releases?.[0]?.version ?? undefined;
+  const primaryRelease =
+    decodeScalar(location.query.primaryRelease) ?? releases?.[0]?.version ?? undefined;
 
-  const release2 =
-    decodeScalar(location.query.release2) ?? releases?.[0]?.version ?? undefined;
+  const secondaryRelease =
+    decodeScalar(location.query.secondaryRelease) ?? releases?.[0]?.version ?? undefined;
 
   const query = new MutableSearch(['event.type:transaction', 'transaction.op:ui.load']);
 
@@ -65,8 +65,8 @@ export function MobileStarfishView() {
           'avg(measurements.frames_frozen_rate)',
         ],
         query:
-          defined(release1) && release1 !== ''
-            ? query.copy().addStringFilter(`release:${release1}`).formatString()
+          defined(primaryRelease) && primaryRelease !== ''
+            ? query.copy().addStringFilter(`release:${primaryRelease}`).formatString()
             : query.formatString(),
         dataset: DiscoverDatasets.METRICS,
         version: 2,
@@ -96,8 +96,8 @@ export function MobileStarfishView() {
           'avg(measurements.frames_frozen_rate)',
         ],
         query:
-          defined(release2) && release2 !== ''
-            ? query.copy().addStringFilter(`release:${release2}`).formatString()
+          defined(secondaryRelease) && secondaryRelease !== ''
+            ? query.copy().addStringFilter(`release:${secondaryRelease}`).formatString()
             : query.formatString(),
         dataset: DiscoverDatasets.METRICS,
         version: 2,
@@ -108,7 +108,7 @@ export function MobileStarfishView() {
       },
       pageFilter.selection
     ),
-    enabled: !isReleasesLoading && release1 !== release2,
+    enabled: !isReleasesLoading && primaryRelease !== secondaryRelease,
     referrer: 'api.starfish-web-service.span-category-breakdown-timeseries',
     initialData: {},
   });
@@ -129,7 +129,7 @@ export function MobileStarfishView() {
 
     if (defined(firstReleaseSeries)) {
       Object.keys(firstReleaseSeries).forEach(yAxis => {
-        const label = `${release1}`;
+        const label = `${primaryRelease}`;
         if (yAxis in transformedSeries) {
           transformedSeries[yAxis].push({
             seriesName: label,
@@ -148,7 +148,7 @@ export function MobileStarfishView() {
 
     if (defined(secondReleaseSeries)) {
       Object.keys(secondReleaseSeries).forEach(yAxis => {
-        const label = `${release2}`;
+        const label = `${secondaryRelease}`;
         if (yAxis in transformedSeries) {
           transformedSeries[yAxis].push({
             seriesName: label,

+ 223 - 0
static/app/views/starfish/views/screens/index.tsx

@@ -0,0 +1,223 @@
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import _EventsRequest from 'sentry/components/charts/eventsRequest';
+import {getInterval} from 'sentry/components/charts/utils';
+import LoadingContainer from 'sentry/components/loading/loadingContainer';
+import {PerformanceLayoutBodyRow} from 'sentry/components/performance/layouts';
+import {CHART_PALETTE} from 'sentry/constants/chartPalette';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {Series, SeriesDataUnit} from 'sentry/types/echarts';
+import {defined} from 'sentry/utils';
+import {tooltipFormatterUsingAggregateOutputType} from 'sentry/utils/discover/charts';
+import EventView from 'sentry/utils/discover/eventView';
+import {AggregationOutputType} from 'sentry/utils/discover/fields';
+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 usePageFilters from 'sentry/utils/usePageFilters';
+import Chart, {useSynchronizeCharts} from 'sentry/views/starfish/components/chart';
+import MiniChartPanel from 'sentry/views/starfish/components/miniChartPanel';
+import {useReleases} from 'sentry/views/starfish/queries/useReleases';
+import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants';
+import {useEventsStatsQuery} from 'sentry/views/starfish/utils/useEventsStatsQuery';
+
+export enum YAxis {
+  WARM_START,
+  COLD_START,
+  TTID,
+  TTFD,
+  SLOW_FRAME_RATE,
+  FROZEN_FRAME_RATE,
+}
+
+export const YAXIS_COLUMNS: Readonly<Record<YAxis, string>> = {
+  [YAxis.WARM_START]: 'avg(measurements.app_start_warm)',
+  [YAxis.COLD_START]: 'avg(measurements.app_start_cold)',
+  [YAxis.TTID]: 'avg(measurements.time_to_initial_display)',
+  [YAxis.TTFD]: 'avg(measurements.time_to_full_display)',
+  [YAxis.SLOW_FRAME_RATE]: 'avg(measurements.frames_slow_rate)',
+  [YAxis.FROZEN_FRAME_RATE]: 'avg(measurements.frames_frozen_rate)',
+};
+
+export const READABLE_YAXIS_LABELS: Readonly<Record<YAxis, string>> = {
+  [YAxis.WARM_START]: 'avg(app_start_warm)',
+  [YAxis.COLD_START]: 'avg(app_start_cold)',
+  [YAxis.TTID]: 'avg(time_to_initial_display)',
+  [YAxis.TTFD]: 'avg(time_to_full_display)',
+  [YAxis.SLOW_FRAME_RATE]: 'avg(frames_slow_rate)',
+  [YAxis.FROZEN_FRAME_RATE]: 'avg(frames_frozen_rate)',
+};
+
+export const CHART_TITLES: Readonly<Record<YAxis, string>> = {
+  [YAxis.WARM_START]: t('App Warm Start'),
+  [YAxis.COLD_START]: t('App Cold Start'),
+  [YAxis.TTID]: t('Time To Initial Display'),
+  [YAxis.TTFD]: t('Time To Full Display'),
+  [YAxis.SLOW_FRAME_RATE]: t('Slow Frame Rate'),
+  [YAxis.FROZEN_FRAME_RATE]: t('Frozen Frame Rate'),
+};
+
+export const OUTPUT_TYPE: Readonly<Record<YAxis, AggregationOutputType>> = {
+  [YAxis.WARM_START]: 'duration',
+  [YAxis.COLD_START]: 'duration',
+  [YAxis.TTID]: 'duration',
+  [YAxis.TTFD]: 'duration',
+  [YAxis.SLOW_FRAME_RATE]: 'percentage',
+  [YAxis.FROZEN_FRAME_RATE]: 'percentage',
+};
+
+export function ScreensView({yAxes}: {yAxes: YAxis[]}) {
+  const pageFilter = usePageFilters();
+  const location = useLocation();
+  const {data: releases, isLoading: isReleasesLoading} = useReleases();
+
+  const yAxisCols = yAxes.map(val => YAXIS_COLUMNS[val]);
+
+  const primaryRelease =
+    decodeScalar(location.query.primaryRelease) ?? releases?.[0]?.version ?? undefined;
+
+  const secondaryRelease =
+    decodeScalar(location.query.secondaryRelease) ?? releases?.[0]?.version ?? undefined;
+
+  const query = new MutableSearch(['event.type:transaction', 'transaction.op:ui.load']);
+
+  let queryString: string = query.formatString();
+  if (
+    defined(primaryRelease) &&
+    defined(secondaryRelease) &&
+    primaryRelease !== secondaryRelease
+  ) {
+    queryString = query
+      .copy()
+      .addStringFilter(`release:[${primaryRelease},${secondaryRelease}]`)
+      .formatString();
+  } else if (defined(primaryRelease)) {
+    queryString = query
+      .copy()
+      .addStringFilter(`release:${primaryRelease}`)
+      .formatString();
+  }
+
+  useSynchronizeCharts();
+  const {
+    isLoading: seriesIsLoading,
+    data: releaseSeries,
+    isError,
+  } = useEventsStatsQuery({
+    eventView: EventView.fromNewQueryWithPageFilters(
+      {
+        name: '',
+        fields: ['release', ...yAxisCols],
+        topEvents: '2',
+        orderby: yAxisCols[0],
+        yAxis: yAxisCols,
+        query: queryString,
+        dataset: DiscoverDatasets.METRICS,
+        version: 2,
+        interval: getInterval(
+          pageFilter.selection.datetime,
+          STARFISH_CHART_INTERVAL_FIDELITY
+        ),
+      },
+      pageFilter.selection
+    ),
+    enabled: !isReleasesLoading,
+    // TODO: Change referrer
+    referrer: 'api.starfish-web-service.span-category-breakdown-timeseries',
+    initialData: {},
+  });
+
+  if (isReleasesLoading) {
+    return <LoadingContainer />;
+  }
+
+  function renderCharts() {
+    const transformedSeries: {[yAxisName: string]: Series[]} = {};
+    yAxes.forEach(val => (transformedSeries[YAXIS_COLUMNS[val]] = []));
+
+    if (defined(releaseSeries)) {
+      Object.keys(releaseSeries).forEach((release, index) => {
+        Object.keys(releaseSeries[release]).forEach(yAxis => {
+          const label = `${release}`;
+          if (yAxis in transformedSeries) {
+            transformedSeries[yAxis].push({
+              seriesName: label,
+              color: CHART_PALETTE[1][index],
+              data:
+                releaseSeries[release][yAxis]?.data.map(datum => {
+                  return {
+                    name: datum[0] * 1000,
+                    value: datum[1][0].count,
+                  } as SeriesDataUnit;
+                }) ?? [],
+            });
+          }
+        });
+      });
+    }
+
+    return (
+      <Fragment>
+        {yAxes.map((val, index) => {
+          return (
+            <ChartsContainerItem key={val}>
+              <MiniChartPanel title={CHART_TITLES[val]}>
+                <Chart
+                  height={150}
+                  data={transformedSeries[yAxisCols[index]]}
+                  loading={seriesIsLoading}
+                  utc={false}
+                  grid={{
+                    left: '0',
+                    right: '0',
+                    top: '16px',
+                    bottom: '0',
+                  }}
+                  showLegend
+                  definedAxisTicks={2}
+                  isLineChart
+                  aggregateOutputFormat={OUTPUT_TYPE[val]}
+                  tooltipFormatterOptions={{
+                    valueFormatter: value =>
+                      tooltipFormatterUsingAggregateOutputType(value, OUTPUT_TYPE[val]),
+                  }}
+                  errored={isError}
+                />
+              </MiniChartPanel>
+            </ChartsContainerItem>
+          );
+        })}
+      </Fragment>
+    );
+  }
+
+  return (
+    <div data-test-id="starfish-mobile-view">
+      <StyledRow minSize={300}>
+        <ChartsContainer>{renderCharts()}</ChartsContainer>
+      </StyledRow>
+    </div>
+  );
+}
+
+const StyledRow = styled(PerformanceLayoutBodyRow)`
+  margin-bottom: ${space(2)};
+`;
+
+const ChartsContainer = styled('div')`
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: ${space(2)};
+`;
+
+const ChartsContainerItem = styled('div')`
+  flex: 1;
+`;
+
+export const Spacer = styled('div')`
+  margin-top: ${space(3)};
+`;

+ 8 - 2
static/app/views/starfish/views/webServiceView/starfishLanding.tsx

@@ -49,8 +49,14 @@ export function StarfishLanding(props: Props) {
       </PageFilterBar>
       {starfishType === StarfishType.MOBILE && (
         <PageFilterBar condensed>
-          <ReleaseSelector selectorKey="release1" selectorName={t('Release 1')} />
-          <ReleaseSelector selectorKey="release2" selectorName={t('Release 2')} />
+          <ReleaseSelector
+            selectorKey="primaryRelease"
+            selectorName={t('Primary Release')}
+          />
+          <ReleaseSelector
+            selectorKey="secondaryRelease"
+            selectorName={t('Secondary Release')}
+          />
         </PageFilterBar>
       )}
     </Fragment>