Browse Source

feat(perf): show tpm and histograms as percentages for metrics (#44329)

- hide transaction event count for trends view [only when feature flag +
mep]
<img width="795" alt="Screen Shot 2023-02-08 at 11 03 15 PM"
src="https://user-images.githubusercontent.com/23648387/217661490-14ad4dbe-4a49-4c48-a5b9-57aec0b4f162.png">

- for histograms [only when feature flag + mep]
    - show event count as a % of processed event count
    - convert each bin's count into a % of total indexed event count
   
<img width="800" alt="Screen Shot 2023-02-08 at 11 02 52 PM"
src="https://user-images.githubusercontent.com/23648387/217661679-f940bbf7-bccb-4268-9a75-90be11e29874.png">
<img width="785" alt="Screen Shot 2023-02-08 at 11 03 07 PM"
src="https://user-images.githubusercontent.com/23648387/217661685-ec6bc801-f6bb-45c8-85cc-8f4d05eb39d7.png">

- for tpm [only when feature flag + mep]
   - rename tpm to "Total Transactions"
- display `indexed_tpm()/processed_tpm()` when viewing indexed events
and `processed_tpm()/processed_tpm()` when viewing processed events
   - convert each data point to `tpm()/processed_tpm()`
when showing processed events:
<img width="807" alt="Screen Shot 2023-02-08 at 11 06 52 PM"
src="https://user-images.githubusercontent.com/23648387/217662173-95e515ba-3e5c-4a95-9f86-9d27f90df93b.png">
when dropping to indexed events:
<img width="811" alt="Screen Shot 2023-02-08 at 11 07 11 PM"
src="https://user-images.githubusercontent.com/23648387/217662236-ac98a71e-30ff-4f75-a3a6-1b60abee3a15.png">

Tested for conditions:
- no feature flag + mep enabled
- feature flag + mep enabled
- feature-flag + no mep enabled
Dameli Ushbayeva 2 years ago
parent
commit
74c6d08227

+ 31 - 11
static/app/views/performance/transactionSummary/transactionOverview/charts.tsx

@@ -15,10 +15,14 @@ import {t} from 'sentry/locale';
 import {Organization, SelectValue} from 'sentry/types';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import EventView from 'sentry/utils/discover/eventView';
+import {formatPercentage} from 'sentry/utils/formatters';
 import {useMEPSettingContext} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
 import {removeHistogramQueryStrings} from 'sentry/utils/performance/histogram';
 import {decodeScalar} from 'sentry/utils/queryString';
-import {getTransactionMEPParamsIfApplicable} from 'sentry/views/performance/transactionSummary/transactionOverview/utils';
+import {
+  canUseMetricsInTransactionSummary,
+  getTransactionMEPParamsIfApplicable,
+} from 'sentry/views/performance/transactionSummary/transactionOverview/utils';
 import {DisplayModes} from 'sentry/views/performance/transactionSummary/utils';
 import {TransactionsListOption} from 'sentry/views/releases/detail/overview';
 
@@ -72,17 +76,19 @@ type Props = {
   eventView: EventView;
   location: Location;
   organization: Organization;
-  totalValues: number | null;
+  totalValue: number | null;
   withoutZerofill: boolean;
+  unfilteredTotalValue?: number | null;
 };
 
 function TransactionSummaryCharts({
-  totalValues,
+  totalValue,
   eventView,
   organization,
   location,
   currentFilter,
   withoutZerofill,
+  unfilteredTotalValue,
 }: Props) {
   function handleDisplayChange(value: string) {
     const display = decodeScalar(location.query.display, DisplayModes.DURATION);
@@ -161,6 +167,23 @@ function TransactionSummaryCharts({
     organization,
     location
   );
+  // For mep-incompatible displays hide event count
+  const hideTransactionCount =
+    canUseMetricsInTransactionSummary(organization) && display === DisplayModes.TREND;
+
+  // For partially-mep-compatible displays show event count as a %
+  const showTransactionCountAsPercentage =
+    canUseMetricsInTransactionSummary(organization) && display === DisplayModes.LATENCY;
+
+  function getTotalValue() {
+    if (totalValue === null || hideTransactionCount) {
+      return <Placeholder height="24px" />;
+    }
+    if (showTransactionCountAsPercentage && unfilteredTotalValue) {
+      return formatPercentage(totalValue / unfilteredTotalValue);
+    }
+    return totalValue.toLocaleString();
+  }
 
   return (
     <Panel>
@@ -177,6 +200,7 @@ function TransactionSummaryCharts({
             statsPeriod={eventView.statsPeriod}
             currentFilter={currentFilter}
             queryExtras={queryExtras}
+            totalCount={showTransactionCountAsPercentage ? totalValue : null}
           />
         )}
         {display === DisplayModes.DURATION && (
@@ -254,14 +278,10 @@ function TransactionSummaryCharts({
 
       <ChartControls>
         <InlineContainer>
-          <SectionHeading key="total-heading">{t('Total Transactions')}</SectionHeading>
-          <SectionValue key="total-value">
-            {totalValues === null ? (
-              <Placeholder height="24px" />
-            ) : (
-              totalValues.toLocaleString()
-            )}
-          </SectionValue>
+          <SectionHeading key="total-heading">
+            {hideTransactionCount ? '' : t('Total Transactions')}
+          </SectionHeading>
+          <SectionValue key="total-value">{getTotalValue()}</SectionValue>
         </InlineContainer>
         <InlineContainer>
           {display === DisplayModes.TREND && (

+ 13 - 3
static/app/views/performance/transactionSummary/transactionOverview/content.tsx

@@ -36,7 +36,10 @@ import withProjects from 'sentry/utils/withProjects';
 import {Actions, updateQuery} from 'sentry/views/discover/table/cellAction';
 import {TableColumn} from 'sentry/views/discover/table/types';
 import Tags from 'sentry/views/discover/tags';
-import {canUseTransactionMetricsData} from 'sentry/views/performance/transactionSummary/transactionOverview/utils';
+import {
+  canUseMetricsInTransactionSummary,
+  canUseTransactionMetricsData,
+} from 'sentry/views/performance/transactionSummary/transactionOverview/utils';
 import {
   PERCENTILE as VITAL_PERCENTILE,
   VITAL_GROUPS,
@@ -78,6 +81,7 @@ type Props = {
   spanOperationBreakdownFilter: SpanOperationBreakdownFilter;
   totalValues: Record<string, number> | null;
   transactionName: string;
+  unfilteredTotalValues?: Record<string, number> | null;
 };
 
 function SummaryContent({
@@ -92,6 +96,7 @@ function SummaryContent({
   projectId,
   transactionName,
   onChangeFilter,
+  unfilteredTotalValues,
 }: Props) {
   const routes = useRoutes();
   function handleSearch(query: string) {
@@ -177,7 +182,7 @@ function SummaryContent({
   }
 
   function generateActionBarItems(_org: Organization, _location: Location) {
-    if (!_org.features.includes('performance-metrics-backed-transaction-summary')) {
+    if (!canUseMetricsInTransactionSummary(_org)) {
       return undefined;
     }
 
@@ -210,6 +215,9 @@ function SummaryContent({
 
   const query = decodeScalar(location.query.query, '');
   const totalCount = totalValues === null ? null : totalValues['count()'];
+  const unfilteredTotalCount = unfilteredTotalValues
+    ? unfilteredTotalValues['count()']
+    : null;
 
   // NOTE: This is not a robust check for whether or not a transaction is a front end
   // transaction, however it will suffice for now.
@@ -331,9 +339,10 @@ function SummaryContent({
           organization={organization}
           location={location}
           eventView={eventView}
-          totalValues={totalCount}
+          totalValue={totalCount}
           currentFilter={spanOperationBreakdownFilter}
           withoutZerofill={hasPerformanceChartInterpolation}
+          unfilteredTotalValue={unfilteredTotalCount}
         />
         <TransactionsList
           location={location}
@@ -416,6 +425,7 @@ function SummaryContent({
           totals={totalValues}
           eventView={eventView}
           transactionName={transactionName}
+          unfilteredTotals={unfilteredTotalValues}
         />
         <SidebarSpacer />
         <Tags

+ 26 - 0
static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx

@@ -251,6 +251,32 @@ describe('Performance > TransactionSummary', function () {
         },
       ],
     });
+    // Events Mock unfiltered totals for percentage calculations
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/events/',
+      body: {
+        meta: {
+          fields: {
+            'tpm()': 'number',
+            'count()': 'number',
+          },
+        },
+        data: [
+          {
+            'count()': 2,
+            'tpm()': 1,
+          },
+        ],
+      },
+      match: [
+        (_url, options) => {
+          return (
+            options.query?.field?.includes('tpm()') &&
+            !options.query?.field?.includes('p95()')
+          );
+        },
+      ],
+    });
     // Events Transaction list response
     MockApiClient.addMockResponse({
       url: '/organizations/org-slug/events/',

+ 59 - 1
static/app/views/performance/transactionSummary/transactionOverview/index.tsx

@@ -21,7 +21,10 @@ import useApi from 'sentry/utils/useApi';
 import withOrganization from 'sentry/utils/withOrganization';
 import withPageFilters from 'sentry/utils/withPageFilters';
 import withProjects from 'sentry/utils/withProjects';
-import {getTransactionMEPParamsIfApplicable} from 'sentry/views/performance/transactionSummary/transactionOverview/utils';
+import {
+  canUseMetricsInTransactionSummary,
+  getTransactionMEPParamsIfApplicable,
+} from 'sentry/views/performance/transactionSummary/transactionOverview/utils';
 
 import {addRoutePerformanceContext} from '../../utils';
 import {
@@ -106,7 +109,25 @@ function OverviewContentWrapper(props: ChildProps) {
     queryExtras,
   });
 
+  const unfilteredQueryExtras = getTransactionMEPParamsIfApplicable(
+    mepSetting,
+    organization,
+    location,
+    true
+  );
+
+  const additionalQueryData = useDiscoverQuery({
+    eventView: getUnfilteredTotalsEventView(eventView, location),
+    orgSlug: organization.slug,
+    location,
+    transactionThreshold,
+    transactionThresholdMetric,
+    referrer: 'api.performance.transaction-summary',
+    queryExtras: unfilteredQueryExtras,
+  });
+
   const {data: tableData, isLoading, error} = queryData;
+  const {data: unfilteredTableData} = additionalQueryData;
 
   const spanOperationBreakdownFilter = decodeFilterFromLocation(location);
 
@@ -133,6 +154,13 @@ function OverviewContentWrapper(props: ChildProps) {
 
   const totals: TotalValues | null =
     (tableData?.data?.[0] as {[k: string]: number}) ?? null;
+
+  const unfilteredTotals: TotalValues | null = canUseMetricsInTransactionSummary(
+    organization
+  )
+    ? (unfilteredTableData?.data?.[0] as {[k: string]: number}) ?? null
+    : null;
+
   return (
     <SummaryContent
       location={location}
@@ -145,6 +173,7 @@ function OverviewContentWrapper(props: ChildProps) {
       totalValues={totals}
       onChangeFilter={onChangeFilter}
       spanOperationBreakdownFilter={spanOperationBreakdownFilter}
+      unfilteredTotalValues={unfilteredTotals}
     />
   );
 }
@@ -202,6 +231,35 @@ function generateEventView({
   );
 }
 
+function getUnfilteredTotalsEventView(
+  eventView: EventView,
+  location: Location
+): EventView {
+  const totalsColumns: QueryFieldValue[] = [
+    {
+      kind: 'function',
+      function: ['tpm', '', undefined, undefined],
+    },
+    {
+      kind: 'function',
+      function: ['count', '', undefined, undefined],
+    },
+  ];
+
+  const transactionName = decodeScalar(location.query.transaction);
+  const conditions = new MutableSearch('');
+
+  conditions.setFilterValues('event.type', ['transaction']);
+  if (transactionName) {
+    conditions.setFilterValues('transaction', [transactionName]);
+  }
+
+  const unfilteredEventView = eventView.withColumns([...totalsColumns]);
+  unfilteredEventView.query = conditions.formatString();
+
+  return unfilteredEventView;
+}
+
 function getTotalsEventView(
   _organization: Organization,
   eventView: EventView

+ 20 - 3
static/app/views/performance/transactionSummary/transactionOverview/latencyChart/content.tsx

@@ -9,6 +9,7 @@ import {IconWarning} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {OrganizationSummary} from 'sentry/types';
 import EventView from 'sentry/utils/discover/eventView';
+import {formatPercentage} from 'sentry/utils/formatters';
 import Histogram from 'sentry/utils/performance/histogram';
 import HistogramQuery from 'sentry/utils/performance/histogram/histogramQuery';
 import {HistogramData} from 'sentry/utils/performance/histogram/types';
@@ -31,6 +32,7 @@ type Props = ViewProps & {
   location: Location;
   organization: OrganizationSummary;
   queryExtras?: Record<string, string>;
+  totalCount?: number | null;
 };
 
 /**
@@ -52,8 +54,10 @@ function Content({
   location,
   currentFilter,
   queryExtras,
+  totalCount,
 }: Props) {
   const [zoomError, setZoomError] = useState(false);
+  const displayCountAsPercentage = !!totalCount;
 
   function handleMouseOver() {
     // Hide the zoom error tooltip on the next hover.
@@ -62,6 +66,14 @@ function Content({
     }
   }
 
+  function parseHistogramData(data: HistogramData): HistogramData {
+    // display each bin's count as a % of total count
+    if (totalCount) {
+      return data.map(({bin, count}) => ({bin, count: count / totalCount}));
+    }
+    return data;
+  }
+
   function renderChart(data: HistogramData) {
     const xAxis = {
       type: 'category' as const,
@@ -86,8 +98,11 @@ function Content({
         if (!zoomError) {
           // Replicate the necessary logic from sentry/components/charts/components/tooltip.jsx
           contents = seriesData.map(item => {
-            const label = item.seriesName;
-            const value = item.value[1].toLocaleString();
+            const label = displayCountAsPercentage ? t('Transactions') : item.seriesName;
+            const value = displayCountAsPercentage
+              ? formatPercentage(item.value[1])
+              : item.value[1].toLocaleString();
+
             return [
               '<div class="tooltip-series">',
               `<div><span class="tooltip-label">${item.marker} <strong>${label}</strong></span> ${value}</div>`,
@@ -108,9 +123,11 @@ function Content({
       },
     };
 
+    const parsedData = parseHistogramData(data);
+
     const series = {
       seriesName: t('Count'),
-      data: formatHistogramData(data, {type: 'duration'}),
+      data: formatHistogramData(parsedData, {type: 'duration'}),
     };
 
     return (

+ 1 - 0
static/app/views/performance/transactionSummary/transactionOverview/latencyChart/index.tsx

@@ -16,6 +16,7 @@ type Props = ViewProps & {
   location: Location;
   organization: OrganizationSummary;
   queryExtras?: Record<string, string>;
+  totalCount?: number | null;
 };
 
 function LatencyChart({currentFilter, ...props}: Props) {

+ 69 - 18
static/app/views/performance/transactionSummary/transactionOverview/sidebarCharts.tsx

@@ -52,9 +52,13 @@ type ContainerProps = {
   organization: Organization;
   totals: Record<string, number> | null;
   transactionName: string;
+  unfilteredTotals?: Record<string, number> | null;
 };
 
-type Props = Pick<ContainerProps, 'organization' | 'isLoading' | 'error' | 'totals'> & {
+type Props = Pick<
+  ContainerProps,
+  'organization' | 'isLoading' | 'error' | 'totals' | 'unfilteredTotals'
+> & {
   chartData: {
     chartOptions: Omit<LineChartProps, 'series'>;
     errored: boolean;
@@ -82,10 +86,27 @@ function SidebarCharts({
   chartData,
   eventView,
   transactionName,
+  unfilteredTotals,
 }: Props) {
   const location = useLocation();
   const router = useRouter();
   const theme = useTheme();
+  const displayTPMAsPercentage = !!unfilteredTotals;
+
+  function getValueFromTotals(field, totalValues, unfilteredTotalValues) {
+    if (totalValues) {
+      if (unfilteredTotalValues) {
+        return tct('[tpm]', {
+          tpm: formatPercentage(totalValues[field] / unfilteredTotalValues[field]),
+        });
+      }
+      return tct('[tpm] tpm', {
+        tpm: formatFloat(totalValues[field], 4),
+      });
+    }
+    return null;
+  }
+
   return (
     <RelativeBox>
       <ChartLabel top="0px">
@@ -124,10 +145,16 @@ function SidebarCharts({
 
       <ChartLabel top="320px">
         <ChartTitle>
-          {t('TPM')}
+          {displayTPMAsPercentage ? t('Total Transactions') : t('TPM')}
           <QuestionTooltip
             position="top"
-            title={getTermHelp(organization, PERFORMANCE_TERM.TPM)}
+            title={
+              displayTPMAsPercentage
+                ? tct('[count] events', {
+                    count: unfilteredTotals['count()'].toLocaleString(),
+                  })
+                : getTermHelp(organization, PERFORMANCE_TERM.TPM)
+            }
             size="sm"
           />
         </ChartTitle>
@@ -135,13 +162,7 @@ function SidebarCharts({
           data-test-id="tpm-summary-value"
           isLoading={isLoading}
           error={error}
-          value={
-            totals
-              ? tct('[tpm] tpm', {
-                  tpm: formatFloat(totals['tpm()'], 4),
-                })
-              : null
-          }
+          value={getValueFromTotals('tpm()', totals, unfilteredTotals)}
         />
       </ChartLabel>
 
@@ -231,6 +252,7 @@ function SidebarChartsContainer({
   error,
   totals,
   transactionName,
+  unfilteredTotals,
 }: ContainerProps) {
   const location = useLocation();
   const router = useRouter();
@@ -325,7 +347,10 @@ function SidebarChartsContainer({
         gridIndex: 2,
         splitNumber: 4,
         axisLabel: {
-          formatter: formatAbbreviatedNumber,
+          formatter: value =>
+            unfilteredTotals
+              ? formatPercentage(value, 0)
+              : formatAbbreviatedNumber(value),
           color: theme.chartLabel,
         },
         ...axisLineConfig,
@@ -338,8 +363,12 @@ function SidebarChartsContainer({
     tooltip: {
       trigger: 'axis',
       truncate: 80,
-      valueFormatter: (value, label) =>
-        tooltipFormatter(value, aggregateOutputType(label)),
+      valueFormatter: (value, label) => {
+        const shouldUsePercentageForTPM = unfilteredTotals && label === 'epm()';
+        return shouldUsePercentageForTPM
+          ? tooltipFormatter(value, 'percentage')
+          : tooltipFormatter(value, aggregateOutputType(label));
+      },
       nameFormatter(value: string) {
         return value === 'epm()' ? 'tpm()' : value;
       },
@@ -365,6 +394,7 @@ function SidebarChartsContainer({
     end,
     utc,
     totals,
+    unfilteredTotals,
   };
 
   const datetimeSelection = {
@@ -387,11 +417,32 @@ function SidebarChartsContainer({
     >
       {({results, errored, loading, reloading}) => {
         const series = results
-          ? results.map((values, i: number) => ({
-              ...values,
-              yAxisIndex: i,
-              xAxisIndex: i,
-            }))
+          ? results
+              .map(_values => {
+                if (_values.seriesName === 'epm()') {
+                  const unfilteredTotalTPM = unfilteredTotals
+                    ? unfilteredTotals['tpm()']
+                    : null;
+                  if (unfilteredTotalTPM) {
+                    return {
+                      ..._values,
+                      data: _values.data.map(point => {
+                        return {
+                          ...point,
+                          value: point.value / unfilteredTotalTPM,
+                        };
+                      }),
+                    };
+                  }
+                  return _values;
+                }
+                return _values;
+              })
+              .map((v, i: number) => ({
+                ...v,
+                yAxisIndex: i,
+                xAxisIndex: i,
+              }))
           : [];
 
         return (

+ 11 - 3
static/app/views/performance/transactionSummary/transactionOverview/utils.tsx

@@ -9,7 +9,7 @@ import {decodeScalar} from 'sentry/utils/queryString';
 import {getMEPQueryParams} from 'sentry/views/performance/landing/widgets/utils';
 import {DisplayModes} from 'sentry/views/performance/transactionSummary/utils';
 
-const DISPLAY_MAP_DENY_LIST = [DisplayModes.TREND, DisplayModes.LATENCY];
+export const DISPLAY_MAP_DENY_LIST = [DisplayModes.TREND, DisplayModes.LATENCY];
 
 export function canUseTransactionMetricsData(organization, location) {
   const isUsingMetrics = canUseMetricsData(organization);
@@ -46,15 +46,23 @@ export function canUseTransactionMetricsData(organization, location) {
 export function getTransactionMEPParamsIfApplicable(
   mepContext: MetricsEnhancedSettingContext,
   organization: Organization,
-  location: Location
+  location: Location,
+  unfiltered: boolean = false
 ) {
   if (!organization.features.includes('performance-metrics-backed-transaction-summary')) {
     return undefined;
   }
 
-  if (!canUseTransactionMetricsData(organization, location)) {
+  if (!unfiltered && !canUseTransactionMetricsData(organization, location)) {
     return undefined;
   }
 
   return getMEPQueryParams(mepContext);
 }
+
+export function canUseMetricsInTransactionSummary(organization: Organization) {
+  return (
+    canUseMetricsData(organization) &&
+    organization.features.includes('performance-metrics-backed-transaction-summary')
+  );
+}