Browse Source

feat(vitals-alert): reading project data in FE (#36688)

This PR adds the following logic for the vitals alert:

Ensure we have 100 transactions of the relevant vital within the organization to show the alert
Pick the project with the most relevant transactions if we can't select every project.
Stephen Cefali 2 years ago
parent
commit
c0cc43129c

+ 6 - 0
static/app/components/performance/vitalsAlert/constants.tsx

@@ -1,5 +1,7 @@
 import {VitalsKey} from './types';
 
+export const VITALS_TYPES = ['FCP', 'LCP', 'appStartCold', 'appStartWarm'] as const;
+
 // these are industry standards determined by Google (https://web.dev/defining-core-web-vitals-thresholds/)
 export const INDUSTRY_STANDARDS: Record<VitalsKey, number> = {
   LCP: 2500,
@@ -15,3 +17,7 @@ export const SENTRY_CUSTOMERS: Record<VitalsKey, number> = {
   appStartCold: 2260,
   appStartWarm: 1900,
 };
+
+// an organization must have at least this many transactions
+// of the vital we want to show
+export const MIN_VITAL_COUNT_FOR_DISPLAY = 100;

+ 16 - 6
static/app/components/performance/vitalsAlert/types.tsx

@@ -1,8 +1,18 @@
-export interface VitalsResult {
-  FCP: number | null;
-  LCP: number | null;
-  appStartCold: number | null;
-  appStartWarm: number | null;
+import {VITALS_TYPES} from './constants';
+
+export type VitalsKey = typeof VITALS_TYPES[number];
+
+type VitalsTimingResult = {
+  [key in VitalsKey]: number;
+};
+
+interface BaseVitalsResult extends VitalsTimingResult {
+  appColdStartCount: number;
+  appWarmStartCount: number;
+  fcpCount: number;
+  lcpCount: number;
 }
 
-export type VitalsKey = keyof VitalsResult;
+export interface VitalsResult extends BaseVitalsResult {
+  projectData: Array<BaseVitalsResult & {projectId: string}>;
+}

+ 18 - 4
static/app/components/performance/vitalsAlert/utils.tsx

@@ -6,18 +6,17 @@ export function getRelativeDiff(value: number, benchmark: number) {
   return (value - benchmark) / benchmark;
 }
 
-export function getWorstVital(data: VitalsResult) {
+export function getWorstVital(data: VitalsResult): VitalsKey | null {
   let worstField: VitalsKey | null = null;
   let worstDecrease = 0;
-  let field: VitalsKey;
-  for (field in data) {
+  for (const field in data) {
     const value = data[field];
     if (value) {
       const benchmark = SENTRY_CUSTOMERS[field];
       const relativeDiff = getRelativeDiff(value, benchmark);
       if (relativeDiff > worstDecrease) {
         worstDecrease = relativeDiff;
-        worstField = field;
+        worstField = field as VitalsKey;
       }
     }
   }
@@ -26,3 +25,18 @@ export function getWorstVital(data: VitalsResult) {
   }
   return null;
 }
+
+export function getCountParameterName(vital: VitalsKey) {
+  switch (vital) {
+    case 'FCP':
+      return 'fcpCount';
+    case 'LCP':
+      return 'lcpCount';
+    case 'appStartCold':
+      return 'appColdStartCount';
+    case 'appStartWarm':
+      return 'appWarmStartCount';
+    default:
+      throw new Error(`Unexpected vital ${vital}`);
+  }
+}

+ 42 - 15
static/app/components/performance/vitalsAlert/vitalsAlertCTA.tsx

@@ -1,4 +1,5 @@
 import styled from '@emotion/styled';
+import maxBy from 'lodash/maxBy';
 
 import {promptsUpdate} from 'sentry/actionCreators/prompts';
 import {
@@ -15,9 +16,13 @@ import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAna
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
 
-import {INDUSTRY_STANDARDS, SENTRY_CUSTOMERS} from './constants';
+import {
+  INDUSTRY_STANDARDS,
+  MIN_VITAL_COUNT_FOR_DISPLAY,
+  SENTRY_CUSTOMERS,
+} from './constants';
 import {VitalsKey, VitalsResult} from './types';
-import {getRelativeDiff, getWorstVital} from './utils';
+import {getCountParameterName, getRelativeDiff, getWorstVital} from './utils';
 
 interface Props {
   data: VitalsResult;
@@ -70,18 +75,36 @@ export default function VitalsAlertCTA({data, dismissAlert}: Props) {
   // persist to dismiss alert
   const api = useApi({persistInFlight: true});
   const vital = getWorstVital(data);
-  if (!vital) {
-    return null;
-  }
-  const ourValue = data[vital];
-  if (!ourValue) {
-    return null;
-  }
-  const sentryDiff = getRelativeDiff(ourValue, SENTRY_CUSTOMERS[vital]);
-  const industryDiff = getRelativeDiff(ourValue, INDUSTRY_STANDARDS[vital]);
+  const userVitalValue = vital ? data[vital] : 0;
+  const sentryDiff = vital ? getRelativeDiff(userVitalValue, SENTRY_CUSTOMERS[vital]) : 0;
+  const industryDiff = vital
+    ? getRelativeDiff(userVitalValue, INDUSTRY_STANDARDS[vital])
+    : 0;
+
+  const hasGlobalViews = organization.features.includes('global-views');
+  // find the project that has the most events of the same type
+  const bestProjectData = vital
+    ? maxBy(data.projectData, item => {
+        const parameterName = getCountParameterName(vital);
+        return item[parameterName];
+      })
+    : null;
+
+  const showVitalsAlert = () => {
+    // check if we have the vital and the count is at least at the min
+    if (!vital || userVitalValue < MIN_VITAL_COUNT_FOR_DISPLAY) {
+      return false;
+    }
+    // if worst vital is better than Sentry users, we shouldn't show this alert
+    if (sentryDiff < 0) {
+      return false;
+    }
+    // must have either global views enabled or we can pick a specific project
+    return hasGlobalViews || bestProjectData;
+  };
 
-  // if worst vital is better than Sentry users, we shouldn't show this alert
-  if (sentryDiff < 0) {
+  // TODO: log experiment
+  if (!vital || !showVitalsAlert()) {
     return null;
   }
 
@@ -109,12 +132,16 @@ export default function VitalsAlertCTA({data, dismissAlert}: Props) {
   };
 
   const getVitalsURL = () => {
-    // TODO: add logic for project selection
     const performanceRoot = `/organizations/${organization.slug}/performance`;
-    const baseParams = {
+    const baseParams: Record<string, string> = {
       statsPeriod: '7d',
       referrer: `vitals-alert-${vital.toLowerCase()}`,
     };
+    // specify a specific project if we have to and we can
+    if (!hasGlobalViews && bestProjectData) {
+      baseParams.project = bestProjectData.projectId;
+    }
+
     // we can land on a specific web vital
     if (vitalsType === 'web') {
       const searchParams = new URLSearchParams({