Browse Source

feat(vitals-alert): remove vitals alert fe (#40504)

Removing the FE code for the vitals alert CTA experiment which we are
closing.
Stephen Cefali 2 years ago
parent
commit
b885fd17b2

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

@@ -1,23 +0,0 @@
-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,
-  FCP: 1800,
-  appStartCold: 5000,
-  appStartWarm: 2000,
-};
-
-// these were determined using a Looker query and might change over time
-export const SENTRY_CUSTOMERS: Record<VitalsKey, number> = {
-  LCP: 948,
-  FCP: 760,
-  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;

+ 0 - 18
static/app/components/performance/vitalsAlert/types.tsx

@@ -1,18 +0,0 @@
-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 interface VitalsResult extends BaseVitalsResult {
-  projectData: Array<BaseVitalsResult & {projectId: string}>;
-}

+ 0 - 42
static/app/components/performance/vitalsAlert/utils.tsx

@@ -1,42 +0,0 @@
-import {SENTRY_CUSTOMERS} from './constants';
-import {VitalsKey, VitalsResult} from './types';
-
-export function getRelativeDiff(value: number, benchmark: number) {
-  // get the difference and divide it by our benchmark
-  return (value - benchmark) / benchmark;
-}
-
-export function getWorstVital(data: VitalsResult): VitalsKey | null {
-  let worstField: VitalsKey | null = null;
-  let worstDecrease = 0;
-  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 as VitalsKey;
-      }
-    }
-  }
-  if (worstDecrease > 0) {
-    return worstField;
-  }
-  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}`);
-  }
-}

+ 0 - 234
static/app/components/performance/vitalsAlert/vitalsAlertCTA.tsx

@@ -1,234 +0,0 @@
-import {useEffect} from 'react';
-import styled from '@emotion/styled';
-import maxBy from 'lodash/maxBy';
-
-import {promptsUpdate} from 'sentry/actionCreators/prompts';
-import {
-  NotificationBar,
-  StyledNotificationBarIconInfo,
-} from 'sentry/components/alerts/notificationBar';
-import Button from 'sentry/components/button';
-import ButtonBar from 'sentry/components/buttonBar';
-import ExternalLink from 'sentry/components/links/externalLink';
-import {IconClose} from 'sentry/icons';
-import {t, tct} from 'sentry/locale';
-import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
-import useApi from 'sentry/utils/useApi';
-import useOrganization from 'sentry/utils/useOrganization';
-
-import {
-  INDUSTRY_STANDARDS,
-  MIN_VITAL_COUNT_FOR_DISPLAY,
-  SENTRY_CUSTOMERS,
-} from './constants';
-import {VitalsKey, VitalsResult} from './types';
-import {getCountParameterName, getRelativeDiff, getWorstVital} from './utils';
-
-interface Props {
-  data: VitalsResult;
-  dismissAlert: () => void;
-}
-
-function getPercentage(diff: number) {
-  return <strong>{Math.abs(Math.round(diff * 100))}%</strong>;
-}
-
-function getDocsLink(vital: VitalsKey) {
-  switch (vital) {
-    case 'FCP':
-      return 'https://docs.sentry.io/product/performance/web-vitals/#first-contentful-paint-fcp';
-    case 'LCP':
-      return 'https://docs.sentry.io/product/performance/web-vitals/#largest-contentful-paint-lcp';
-    default:
-      // just one link for mobile vitals
-      return 'https://docs.sentry.io/product/performance/mobile-vitals/#app-start';
-  }
-}
-
-function getVitalsType(vital: VitalsKey) {
-  return ['FCP', 'LCP'].includes(vital) ? 'web' : 'mobile';
-}
-
-export default function VitalsAlertCTA({data, dismissAlert}: Props) {
-  const organization = useOrganization();
-  // persist to dismiss alert
-  const api = useApi({persistInFlight: true});
-  const vital = getWorstVital(data);
-  const userVitalValue = vital ? data[vital] : 0;
-  const userVitalCount = vital ? data[getCountParameterName(vital)] : 0;
-  const sentryDiff = vital ? getRelativeDiff(userVitalValue, SENTRY_CUSTOMERS[vital]) : 0;
-  const industryDiff = vital
-    ? getRelativeDiff(userVitalValue, INDUSTRY_STANDARDS[vital])
-    : 0;
-
-  // must have the global-views
-  // and either be an owner/manager or the org allows open membership
-  const canSeeAllProjects =
-    organization.features.includes('global-views') &&
-    (['owner', 'manager'].includes(organization.orgRole || '') ||
-      organization.features.includes('open-membership'));
-
-  // 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 industryDiffPercentage = getPercentage(industryDiff);
-  const sentryDiffPercentage = getPercentage(sentryDiff);
-  const vitalsType = vital ? getVitalsType(vital) : null;
-
-  const getAnalyticsParams = () => {
-    // shouldn't call any analytics function if this is missing
-    // but this check helps us with typing
-    if (!vital || !vitalsType) {
-      throw new Error('Cannot get analytics params without vital');
-    }
-    return {
-      vital,
-      vitals_type: vitalsType,
-      organization,
-      user_vital_value: userVitalValue,
-      user_vital_count: userVitalCount,
-      sentry_diff: sentryDiff,
-      industry_diff: industryDiff,
-      can_see_all_projects: canSeeAllProjects,
-    } as const;
-  };
-
-  const showVitalsAlert = () => {
-    // check if we have the vital and the count is at least at the min
-    if (!vital || userVitalCount < 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 either be able to see all proejcts or we can pick a specific project
-    return canSeeAllProjects || bestProjectData;
-  };
-
-  useEffect(() => {
-    if (!vital || !showVitalsAlert()) {
-      return;
-    }
-    trackAdvancedAnalyticsEvent('vitals_alert.displayed', getAnalyticsParams());
-  });
-
-  if (!vital || !showVitalsAlert()) {
-    return null;
-  }
-
-  function getVitalWithLink() {
-    if (!vital) {
-      throw new Error('Cannot get vitals link without vital');
-    }
-    const url = new URL(getDocsLink(vital));
-    url.searchParams.append('referrer', 'vitals-alert');
-    return (
-      <ExternalLink
-        onClick={() => {
-          trackAdvancedAnalyticsEvent('vitals_alert.clicked_docs', getAnalyticsParams());
-        }}
-        href={url.toString()}
-      >
-        {vital}
-      </ExternalLink>
-    );
-  }
-
-  const getText = () => {
-    const args = {
-      vital: getVitalWithLink(),
-      industryDiffPercentage,
-      sentryDiffPercentage,
-    };
-    // different language if we are better than the industry average
-    if (industryDiff < 0) {
-      return tct(
-        "Your organization's [vital] is [industryDiffPercentage] lower than the industry standard, but [sentryDiffPercentage] higher than typical Sentry users.",
-        args
-      );
-    }
-    return tct(
-      "Your organization's [vital] is [industryDiffPercentage] higher than the industry standard and [sentryDiffPercentage] higher than typical Sentry users.",
-      args
-    );
-  };
-
-  const getVitalsURL = () => {
-    const performanceRoot = `/organizations/${organization.slug}/performance`;
-    const baseParams: Record<string, string | undefined> = {
-      statsPeriod: '7d',
-      referrer: `vitals-alert-${vital.toLowerCase()}`,
-      project: canSeeAllProjects ? '-1' : bestProjectData?.projectId,
-    };
-
-    // we can land on a specific web vital
-    if (vitalsType === 'web') {
-      const searchParams = new URLSearchParams({
-        ...baseParams,
-        vitalName: `measurements.${vital.toLowerCase()}`,
-      });
-      return `${performanceRoot}/vitaldetail/?${searchParams}`;
-    }
-    // otherwise it's just the mobile vital screen
-    const searchParams = new URLSearchParams({
-      ...baseParams,
-      landingDisplay: 'mobile',
-    });
-    return `${performanceRoot}/?${searchParams}`;
-  };
-
-  const buttonText = vitalsType === 'web' ? t('See Web Vitals') : t('See Mobile Vitals');
-  const dismissAndPromptUpdate = () => {
-    promptsUpdate(api, {
-      organizationId: organization?.id,
-      feature: 'vitals_alert',
-      status: 'dismissed',
-    });
-    dismissAlert();
-  };
-
-  return (
-    <NotificationBar>
-      <StyledNotificationBarIconInfo />
-      {getText()}
-      <NotificationBarButtons gap={1}>
-        <Button
-          to={getVitalsURL()}
-          size="xs"
-          onClick={() => {
-            dismissAndPromptUpdate();
-            trackAdvancedAnalyticsEvent(
-              'vitals_alert.clicked_see_vitals',
-              getAnalyticsParams()
-            );
-          }}
-        >
-          {buttonText}
-        </Button>
-
-        <Button
-          icon={<IconClose />}
-          onClick={() => {
-            dismissAndPromptUpdate();
-            trackAdvancedAnalyticsEvent('vitals_alert.dismissed', getAnalyticsParams());
-          }}
-          size="xs"
-          priority="link"
-          title={t('Dismiss')}
-          aria-label={t('Dismiss')}
-        />
-      </NotificationBarButtons>
-    </NotificationBar>
-  );
-}
-
-const NotificationBarButtons = styled(ButtonBar)`
-  margin-left: auto;
-  white-space: nowrap;
-`;

+ 0 - 6
static/app/data/experimentConfig.tsx

@@ -13,12 +13,6 @@ export const unassignedValue = -1;
  * Frontend experiment configuration object
  */
 export const experimentList = [
-  {
-    key: 'VitalsAlertExperiment',
-    type: ExperimentType.Organization,
-    parameter: 'exposed',
-    assignments: [0, 1],
-  },
   {
     key: 'OnboardingHighlightWizardExperiment',
     type: ExperimentType.Organization,

+ 0 - 18
static/app/utils/analytics/growthAnalyticsEvents.tsx

@@ -46,16 +46,6 @@ type SampleEvent = {
   source: string;
 };
 
-type VitalsAlert = {
-  can_see_all_projects: boolean;
-  industry_diff: number;
-  sentry_diff: number;
-  user_vital_count: number;
-  user_vital_value: number;
-  vital: string;
-  vitals_type: 'mobile' | 'web';
-};
-
 // define the event key to payload mappings
 export type GrowthEventParameters = {
   'assistant.guide_cued': {
@@ -155,10 +145,6 @@ export type GrowthEventParameters = {
   'sdk_updates.clicked': {};
   'sdk_updates.seen': {};
   'sdk_updates.snoozed': {};
-  'vitals_alert.clicked_docs': VitalsAlert;
-  'vitals_alert.clicked_see_vitals': VitalsAlert;
-  'vitals_alert.dismissed': VitalsAlert;
-  'vitals_alert.displayed': VitalsAlert;
 };
 
 type GrowthAnalyticsKey = keyof GrowthEventParameters;
@@ -222,10 +208,6 @@ export const growthEventMap: Record<GrowthAnalyticsKey, string | null> = {
   'sample_event.button_viewed': null, // high-volume event
   'sample_event.created': 'Sample Event Created',
   'sample_event.failed': 'Sample Event Failed',
-  'vitals_alert.clicked_see_vitals': 'Vitals Alert: Clicked See Vitals',
-  'vitals_alert.dismissed': 'Vitals Alert: Dismissed',
-  'vitals_alert.clicked_docs': 'Vitals Alert: Clicked Docs',
-  'vitals_alert.displayed': 'Vitals Alert: Displayed',
   'growth.onboarding_wizard_clicked_more_details':
     'Onboarding Wizard: Clicked More Details',
   'growth.onboarding_wizard_interacted': 'Onboarding Wizard: Interacted',

+ 0 - 7
static/app/utils/analytics/performanceAnalyticsEvents.tsx

@@ -131,11 +131,6 @@ export type PerformanceEventParameters = {
     direction: string;
     widget_type: string;
   };
-  'performance_views.vital_detail.comparison_viewed': {
-    count: number;
-    p75: number;
-    vital: string;
-  };
   'performance_views.vital_detail.switch_vital': {
     from_vital: string;
     to_vital: string;
@@ -227,8 +222,6 @@ export const performanceEventMap: Record<PerformanceEventKey, string | null> = {
   'performance_views.transaction_summary.view':
     'Performance Views: Transaction Summary View',
   'performance_views.filter_dropdown.selection': 'Performance Views: Filter Dropdown',
-  'performance_views.vital_detail.comparison_viewed':
-    'Performance Views: Vital Detail Comparison Viewed',
   'performance_views.relative_breakdown.selection':
     'Performance Views: Select Relative Breakdown',
 };

+ 0 - 15
static/app/views/performance/vitalDetail/vitalDetailContent.tsx

@@ -48,7 +48,6 @@ import {
 } from './utils';
 import VitalChart from './vitalChart';
 import VitalInfo from './vitalInfo';
-import VitalsComparison from './vitalsComparison';
 
 const FRONTEND_VITALS = [WebVital.FCP, WebVital.LCP, WebVital.FID, WebVital.CLS];
 
@@ -205,20 +204,6 @@ function VitalDetailContent(props: Props) {
             onSearch={handleSearch}
           />
         </FilterActions>
-        {organization.experiments.VitalsAlertExperiment ? (
-          <VitalsComparison
-            {...{
-              organization,
-              location,
-              vital,
-              project,
-              end,
-              environment,
-              statsPeriod,
-              start,
-            }}
-          />
-        ) : null}
         <VitalChart
           organization={organization}
           query={query}

+ 0 - 231
static/app/views/performance/vitalDetail/vitalsComparison.tsx

@@ -1,231 +0,0 @@
-import React, {useEffect} from 'react';
-import styled from '@emotion/styled';
-import {Location} from 'history';
-
-import {
-  INDUSTRY_STANDARDS,
-  MIN_VITAL_COUNT_FOR_DISPLAY,
-  SENTRY_CUSTOMERS,
-} from 'sentry/components/performance/vitalsAlert/constants';
-import QuestionTooltip from 'sentry/components/questionTooltip';
-import Tag from 'sentry/components/tag';
-import {t, tct} from 'sentry/locale';
-import space from 'sentry/styles/space';
-import {Organization} from 'sentry/types';
-import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
-import EventView from 'sentry/utils/discover/eventView';
-import {WebVital} from 'sentry/utils/fields';
-import VitalsCardDiscoverQuery from 'sentry/utils/performance/vitals/vitalsCardsDiscoverQuery';
-import {webVitalMeh, webVitalPoor} from 'sentry/views/performance/vitalDetail/utils';
-
-type Score = 'poor' | 'meh' | 'good';
-
-type ViewProps = Pick<
-  EventView,
-  'environment' | 'project' | 'start' | 'end' | 'statsPeriod'
->;
-
-type Props = ViewProps & {
-  location: Location;
-  organization: Organization;
-  vital: WebVital | WebVital[];
-};
-
-const SUPPORTED_VITALS = ['measurements.fcp', 'measurements.lcp'];
-
-function getScore(vital: WebVital, value: number): Score {
-  const poorScore = webVitalPoor[vital];
-  const mehScore = webVitalMeh[vital];
-  if (value > poorScore) {
-    return 'poor';
-  }
-  if (value > mehScore) {
-    return 'meh';
-  }
-  return 'good';
-}
-
-function getIndicatorString(score: Score) {
-  switch (score) {
-    case 'poor':
-      return t('Poor');
-    case 'meh':
-      return t('Meh');
-    default:
-      return t('Good');
-  }
-}
-
-function getTagLevel(score: Score) {
-  switch (score) {
-    case 'poor':
-      return 'error';
-    case 'meh':
-      return 'warning';
-    default:
-      return 'success';
-  }
-}
-
-function MetricsCard({
-  title,
-  vital,
-  value,
-  tooltip,
-}: {
-  title: string;
-  tooltip: string;
-  value: number;
-  vital: WebVital;
-}) {
-  // round to 2 decimals if <10s, otherwise use just 1 decimal
-  const score = getScore(vital, value);
-  const numDecimals = value >= 10_000 ? 1 : 2;
-  const timeInSeconds = value / 1000.0;
-  return (
-    <MetricsCardWrapper>
-      <MetricsTitle>
-        {title} (p75) <StyledQuestionTooltip title={tooltip} size="xs" />
-      </MetricsTitle>
-      <ScoreWrapper>
-        <ScoreContent>{timeInSeconds.toFixed(numDecimals)}s</ScoreContent>
-        <TagWrapper>
-          <StyledTag type={getTagLevel(score)}>{getIndicatorString(score)}</StyledTag>
-        </TagWrapper>
-      </ScoreWrapper>
-    </MetricsCardWrapper>
-  );
-}
-
-function ContentWrapper({
-  organization,
-  vital,
-  children,
-  count,
-  p75,
-}: {
-  children: React.ReactNode;
-  count: number;
-  organization: Organization;
-  p75: number;
-  vital: WebVital;
-}) {
-  useEffect(() => {
-    trackAdvancedAnalyticsEvent('performance_views.vital_detail.comparison_viewed', {
-      organization,
-      vital,
-      count,
-      p75,
-    });
-  });
-  return <Container>{children}</Container>;
-}
-
-function VitalsComparison(props: Props) {
-  const {location, vital: _vital, organization} = props;
-  const vitals = Array.isArray(_vital) ? _vital : [_vital];
-  const vital = vitals[0];
-  if (!SUPPORTED_VITALS.includes(vital)) {
-    return null;
-  }
-  return (
-    <VitalsCardDiscoverQuery location={location} vitals={vitals}>
-      {({isLoading, vitalsData}) => {
-        if (isLoading || !vitalsData) {
-          return null;
-        }
-        const data = vitalsData[vital];
-        if (!data || !data.p75) {
-          return null;
-        }
-        const {p75} = data;
-        const lookupName = vital === 'measurements.fcp' ? 'FCP' : 'LCP';
-        const sentryStandard = SENTRY_CUSTOMERS[lookupName];
-        const industryStandard = INDUSTRY_STANDARDS[lookupName];
-        const count = vitalsData[vital].total;
-        // only show it if we hit the min number
-        if (count < MIN_VITAL_COUNT_FOR_DISPLAY) {
-          return null;
-        }
-        return (
-          <ContentWrapper {...{organization, vital, count, p75}}>
-            <MetricsCard
-              title={t('Selected Projects')}
-              vital={vital}
-              value={p75}
-              tooltip={tct(
-                "25% of your project's transactions have an [lookupName] greater than this number. Good, Bad, Meh segmentation is based on Google industry standards.",
-                {lookupName}
-              )}
-            />
-            <MetricsCard
-              title={t('Sentry Peers')}
-              vital={vital}
-              value={sentryStandard}
-              tooltip={tct(
-                '20% of Sentry customers have a p75 [lookupName] lower than this.',
-                {lookupName}
-              )}
-            />
-            <MetricsCard
-              title={t('Industry Standard')}
-              vital={vital}
-              value={industryStandard}
-              tooltip={tct(
-                "Calculated as a Good [lookupName] based on Google's industry standards.",
-                {lookupName}
-              )}
-            />
-          </ContentWrapper>
-        );
-      }}
-    </VitalsCardDiscoverQuery>
-  );
-}
-
-export default VitalsComparison;
-
-const Container = styled('div')`
-  display: grid;
-  grid-template-columns: 1fr 1fr 1fr;
-  gap: ${space(2)};
-`;
-
-const ScoreContent = styled('h6')`
-  margin: auto;
-`;
-
-const ScoreWrapper = styled('div')`
-  display: flex;
-  align-items: center;
-`;
-
-const MetricsCardWrapper = styled('div')`
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  border: 1px ${p => p.theme.gray200};
-  border-radius: 4px;
-  border-style: solid;
-  align-items: center;
-  height: 57px;
-  padding: ${space(2)};
-  margin-bottom: ${space(2)};
-`;
-
-const StyledTag = styled(Tag)`
-  margin-left: ${space(1)};
-`;
-
-const MetricsTitle = styled('span')`
-  font-size: 14px;
-`;
-
-const TagWrapper = styled('span')`
-  margin: auto;
-`;
-
-const StyledQuestionTooltip = styled(QuestionTooltip)`
-  position: relative;
-  top: 1px;
-`;