Browse Source

feat(performance): Add mobile landing page (#27136)

This adds the first iteration of the mobile landing page to performance.
Tony Xiao 3 years ago
parent
commit
5ddcebbd91

+ 2 - 0
src/sentry/utils/snuba.py

@@ -1642,6 +1642,8 @@ def is_duration_measurement(key):
         "measurements.fid",
         "measurements.ttfb",
         "measurements.ttfb.requesttime",
+        "measurements.app_start_cold",
+        "measurements.app_start_warm",
     ]
 
 

+ 8 - 1
static/app/utils/discover/fields.tsx

@@ -625,7 +625,12 @@ export enum WebVital {
   RequestTime = 'measurements.ttfb.requesttime',
 }
 
-const MEASUREMENTS: Readonly<Record<WebVital, ColumnType>> = {
+export enum MobileVital {
+  AppStartCold = 'measurements.app_start_cold',
+  AppStartWarm = 'measurements.app_start_warm',
+}
+
+const MEASUREMENTS: Readonly<Record<WebVital | MobileVital, ColumnType>> = {
   [WebVital.FP]: 'duration',
   [WebVital.FCP]: 'duration',
   [WebVital.LCP]: 'duration',
@@ -633,6 +638,8 @@ const MEASUREMENTS: Readonly<Record<WebVital, ColumnType>> = {
   [WebVital.CLS]: 'number',
   [WebVital.TTFB]: 'duration',
   [WebVital.RequestTime]: 'duration',
+  [MobileVital.AppStartCold]: 'duration',
+  [MobileVital.AppStartWarm]: 'duration',
 };
 
 // This list contains fields/functions that are available with performance-view feature.

+ 162 - 0
static/app/views/performance/data.tsx

@@ -44,6 +44,8 @@ export enum PERFORMANCE_TERM {
   DURATION_DISTRIBUTION = 'durationDistribution',
   USER_MISERY_NEW = 'userMiseryNew',
   APDEX_NEW = 'apdexNew',
+  APP_START_COLD = 'appStartCold',
+  APP_START_WARM = 'appStartWarm',
 }
 
 export type TooltipOption = SelectValue<string> & {
@@ -248,6 +250,89 @@ export function getBackendAxisOptions(
   ];
 }
 
+export function getMobileAxisOptions(
+  organization: LightWeightOrganization
+): AxisOption[] {
+  return [
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_COLD),
+      value: `p50(measurements.app_start_cold)`,
+      label: t('Cold Start Duration p50'),
+      field: 'p50(measurements.app_start_cold)',
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_COLD),
+      value: `p75(measurements.app_start_cold)`,
+      label: t('Cold Start Duration p75'),
+      field: 'p75(measurements.app_start_cold)',
+      isLeftDefault: true,
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_COLD),
+      value: `p95(measurements.app_start_cold)`,
+      label: t('Cold Start Duration p95'),
+      field: 'p95(measurements.app_start_cold)',
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_COLD),
+      value: `p99(measurements.app_start_cold)`,
+      label: t('Cold Start Duration p99'),
+      field: 'p99(measurements.app_start_cold)',
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
+      value: 'app_start_cold_distribution',
+      label: t('Cold Start Distribution'),
+      field: 'measurements.app_start_cold',
+      isDistribution: true,
+      isRightDefault: true,
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_WARM),
+      value: `p50(measurements.app_start_warm)`,
+      label: t('Warm Start Duration p50'),
+      field: 'p50(measurements.app_start_warm)',
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_WARM),
+      value: `p75(measurements.app_start_warm)`,
+      label: t('Warm Start Duration p75'),
+      field: 'p75(measurements.app_start_warm)',
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_WARM),
+      value: `p95(measurements.app_start_warm)`,
+      label: t('Warm Start Duration p95'),
+      field: 'p95(measurements.app_start_warm)',
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_WARM),
+      value: `p99(measurements.app_start_warm)`,
+      label: t('Warm Start Duration p99'),
+      field: 'p99(measurements.app_start_warm)',
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
+      value: 'app_start_warm_distribution',
+      label: t('Warm Start Distribution'),
+      field: 'measurements.app_start_warm',
+      isDistribution: true,
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.TPM),
+      value: 'tpm()',
+      label: t('Transactions Per Minute'),
+      field: 'tpm()',
+    },
+    {
+      tooltip: getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE),
+      value: 'failure_rate()',
+      label: t('Failure Rate'),
+      field: 'failure_rate()',
+    },
+  ];
+}
+
 type TermFormatter = (organization: LightWeightOrganization) => string;
 
 const PERFORMANCE_TERMS: Record<PERFORMANCE_TERM, TermFormatter> = {
@@ -291,6 +376,10 @@ const PERFORMANCE_TERMS: Record<PERFORMANCE_TERM, TermFormatter> = {
     t(
       'Apdex is the ratio of both satisfactory and tolerable response times to all response times. To adjust the tolerable threshold, go to project performance settings.'
     ),
+  appStartCold: () =>
+    t('Cold start is a measure of the application start up time from scratch.'),
+  appStartWarm: () =>
+    t('Warm start is a measure of the application start up time while still in memory.'),
 };
 
 export function getTermHelp(
@@ -441,6 +530,77 @@ function generateBackendPerformanceEventView(
   return eventView;
 }
 
+function generateMobilePerformanceEventView(
+  organization: LightWeightOrganization,
+  location: Location
+): EventView {
+  const {query} = location;
+
+  const fields = [
+    organization.features.includes('team-key-transactions')
+      ? 'team_key_transaction'
+      : 'key_transaction',
+    'transaction',
+    'project',
+    'transaction.op',
+    'tpm()',
+    'p50(measurements.app_start_cold)',
+    'p95(measurements.app_start_cold)',
+    'p50(measurements.app_start_warm)',
+    'p95(measurements.app_start_warm)',
+    'failure_rate()',
+  ];
+
+  const featureFields = organization.features.includes('project-transaction-threshold')
+    ? ['apdex()', 'count_unique(user)', 'count_miserable(user)', 'user_misery()']
+    : [
+        `apdex(${organization.apdexThreshold})`,
+        'count_unique(user)',
+        `count_miserable(user,${organization.apdexThreshold})`,
+        `user_misery(${organization.apdexThreshold})`,
+      ];
+
+  const hasStartAndEnd = query.start && query.end;
+  const savedQuery: NewQuery = {
+    id: undefined,
+    name: t('Performance'),
+    query: 'event.type:transaction',
+    projects: [],
+    fields: [...fields, ...featureFields],
+    version: 2,
+  };
+
+  const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
+  widths[savedQuery.fields.length - 1] = '110';
+  savedQuery.widths = widths;
+
+  if (!query.statsPeriod && !hasStartAndEnd) {
+    savedQuery.range = DEFAULT_STATS_PERIOD;
+  }
+  savedQuery.orderby = decodeScalar(query.sort, '-tpm');
+
+  const searchQuery = decodeScalar(query.query, '');
+  const conditions = tokenizeSearch(searchQuery);
+
+  // This is not an override condition since we want the duration to appear in the search bar as a default.
+  if (!conditions.hasTag('transaction.duration')) {
+    conditions.setTagValues('transaction.duration', ['<15m']);
+  }
+
+  // If there is a bare text search, we want to treat it as a search
+  // on the transaction name.
+  if (conditions.query.length > 0) {
+    // the query here is a user entered condition, no need to escape it
+    conditions.setTagValues('transaction', [`*${conditions.query.join(' ')}*`], false);
+    conditions.query = [];
+  }
+  savedQuery.query = conditions.formatString();
+
+  const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
+  eventView.additionalConditions.addTagValues('event.type', ['transaction']);
+  return eventView;
+}
+
 function generateFrontendPageloadPerformanceEventView(
   organization: LightWeightOrganization,
   location: Location
@@ -600,6 +760,8 @@ export function generatePerformanceEventView(
       return generateFrontendOtherPerformanceEventView(organization, location);
     case LandingDisplayField.BACKEND:
       return generateBackendPerformanceEventView(organization, location);
+    case LandingDisplayField.MOBILE:
+      return generateMobilePerformanceEventView(organization, location);
     default:
       return eventView;
   }

+ 40 - 1
static/app/views/performance/landing/content.tsx

@@ -6,6 +6,7 @@ import {Location} from 'history';
 import Feature from 'app/components/acl/feature';
 import DropdownControl, {DropdownItem} from 'app/components/dropdownControl';
 import SearchBar from 'app/components/events/searchBar';
+import FeatureBadge from 'app/components/featureBadge';
 import * as TeamKeyTransactionManager from 'app/components/performance/teamKeyTransactionsManager';
 import {MAX_QUERY_LENGTH} from 'app/constants';
 import {t} from 'app/locale';
@@ -24,6 +25,7 @@ import {
   getBackendAxisOptions,
   getFrontendAxisOptions,
   getFrontendOtherAxisOptions,
+  getMobileAxisOptions,
 } from '../data';
 import Table from '../table';
 import {getTransactionSearchQuery} from '../utils';
@@ -33,6 +35,7 @@ import {
   BACKEND_COLUMN_TITLES,
   FRONTEND_OTHER_COLUMN_TITLES,
   FRONTEND_PAGELOAD_COLUMN_TITLES,
+  MOBILE_COLUMN_TITLES,
 } from './data';
 import {
   getCurrentLandingDisplay,
@@ -110,6 +113,8 @@ class LandingContent extends Component<Props, State> {
         return this.renderLandingFrontend(false);
       case LandingDisplayField.BACKEND:
         return this.renderLandingBackend();
+      case LandingDisplayField.MOBILE:
+        return this.renderLandingMobile();
       default:
         throw new Error(`Unknown display: ${display}`);
     }
@@ -194,6 +199,37 @@ class LandingContent extends Component<Props, State> {
     );
   };
 
+  renderLandingMobile = () => {
+    const {organization, location, projects, eventView, setError} = this.props;
+
+    const axisOptions = getMobileAxisOptions(organization);
+    const {leftAxis, rightAxis} = getDisplayAxes(axisOptions, location);
+
+    const columnTitles = MOBILE_COLUMN_TITLES;
+
+    return (
+      <Fragment>
+        <DoubleAxisDisplay
+          eventView={eventView}
+          organization={organization}
+          location={location}
+          axisOptions={axisOptions}
+          leftAxis={leftAxis}
+          rightAxis={rightAxis}
+        />
+        <Table
+          eventView={eventView}
+          projects={projects}
+          organization={organization}
+          location={location}
+          setError={setError}
+          summaryConditions={eventView.getQueryWithAdditionalConditions()}
+          columnTitles={columnTitles}
+        />
+      </Fragment>
+    );
+  };
+
   renderLandingAll = () => {
     const {organization, location, router, projects, eventView, setError} = this.props;
 
@@ -245,7 +281,9 @@ class LandingContent extends Component<Props, State> {
             buttonProps={{prefix: t('Display')}}
             label={currentLandingDisplay.label}
           >
-            {LANDING_DISPLAYS.map(({label, field}) => (
+            {LANDING_DISPLAYS.filter(
+              ({isShown}) => !isShown || isShown(organization)
+            ).map(({alpha, label, field}) => (
               <DropdownItem
                 key={field}
                 onSelect={this.handleLandingDisplayChange}
@@ -254,6 +292,7 @@ class LandingContent extends Component<Props, State> {
                 isActive={field === currentLandingDisplay.field}
               >
                 {label}
+                {alpha && <FeatureBadge type="alpha" noTooltip />}
               </DropdownItem>
             ))}
           </DropdownControl>

+ 14 - 0
static/app/views/performance/landing/data.tsx

@@ -35,3 +35,17 @@ export const BACKEND_COLUMN_TITLES = [
   'users',
   'user misery',
 ];
+
+export const MOBILE_COLUMN_TITLES = [
+  'transaction',
+  'project',
+  'operation',
+  'tpm',
+  'p50 cold start',
+  'p95 cold start',
+  'p50 warm start',
+  'p95 warm start',
+  'failure_rate',
+  'users',
+  'user misery',
+];

+ 8 - 0
static/app/views/performance/landing/utils.tsx

@@ -30,6 +30,7 @@ export enum LandingDisplayField {
   FRONTEND_PAGELOAD = 'frontend_pageload',
   FRONTEND_OTHER = 'frontend_other',
   BACKEND = 'backend',
+  MOBILE = 'mobile',
 }
 
 export const LANDING_DISPLAYS = [
@@ -49,6 +50,13 @@ export const LANDING_DISPLAYS = [
     label: 'Backend',
     field: LandingDisplayField.BACKEND,
   },
+  {
+    label: 'Mobile',
+    field: LandingDisplayField.MOBILE,
+    isShown: (organization: Organization) =>
+      organization.features.includes('performance-mobile-vitals'),
+    alpha: true,
+  },
 ];
 
 export function getCurrentLandingDisplay(