Browse Source

feat(configurable-thresholds): User project thresholds for Apdex and User Misery in Performance (#26360)

If feature flag is enabled, project thresholds will be used
in the Performance Landing table and the Transaction
Summary cards to calculate Apdex and User Misery.
Shruthi 3 years ago
parent
commit
207f4f6625

+ 13 - 3
static/app/utils/discover/fieldRenderers.tsx

@@ -435,11 +435,17 @@ const SPECIAL_FUNCTIONS: SpecialFunctions = {
   user_misery: data => {
   user_misery: data => {
     let userMiseryField: string = '';
     let userMiseryField: string = '';
     let countMiserableUserField: string = '';
     let countMiserableUserField: string = '';
+    let projectThresholdConfig: string = '';
     for (const field in data) {
     for (const field in data) {
       if (field.startsWith('user_misery')) {
       if (field.startsWith('user_misery')) {
         userMiseryField = field;
         userMiseryField = field;
-      } else if (field.startsWith('count_miserable_user')) {
+      } else if (
+        field.startsWith('count_miserable_user') ||
+        field.startsWith('count_miserable_new_user')
+      ) {
         countMiserableUserField = field;
         countMiserableUserField = field;
+      } else if (field === 'project_threshold_config') {
+        projectThresholdConfig = field;
       }
       }
     }
     }
 
 
@@ -450,7 +456,10 @@ const SPECIAL_FUNCTIONS: SpecialFunctions = {
     const uniqueUsers = data.count_unique_user;
     const uniqueUsers = data.count_unique_user;
     const userMisery = data[userMiseryField];
     const userMisery = data[userMiseryField];
 
 
-    const miseryLimit = parseInt(userMiseryField.split('_').pop() || '', 10) || undefined;
+    let miseryLimit = parseInt(userMiseryField.split('_').pop() || '', 10);
+    if (isNaN(miseryLimit)) {
+      miseryLimit = projectThresholdConfig ? data[projectThresholdConfig][1] : undefined;
+    }
 
 
     let miserableUsers: number | undefined;
     let miserableUsers: number | undefined;
 
 
@@ -460,7 +469,8 @@ const SPECIAL_FUNCTIONS: SpecialFunctions = {
         10
         10
       );
       );
       miserableUsers =
       miserableUsers =
-        countMiserableMiseryLimit === miseryLimit
+        countMiserableMiseryLimit === miseryLimit ||
+        (isNaN(countMiserableMiseryLimit) && projectThresholdConfig)
           ? data[countMiserableUserField]
           ? data[countMiserableUserField]
           : undefined;
           : undefined;
     }
     }

+ 11 - 0
static/app/utils/discover/fields.tsx

@@ -568,6 +568,9 @@ export const TRACING_FIELDS = [
   'apdex',
   'apdex',
   'count_miserable',
   'count_miserable',
   'user_misery',
   'user_misery',
+  'apdex_new',
+  'count_miserable_new',
+  'user_misery_new',
   'eps',
   'eps',
   'epm',
   'epm',
   ...Object.keys(MEASUREMENTS),
   ...Object.keys(MEASUREMENTS),
@@ -746,6 +749,14 @@ export function aggregateFunctionOutputType(
     return 'duration';
     return 'duration';
   }
   }
 
 
+  // This is temporary since these don't fulfill any of
+  // the conditions above. Will be removed when these fields
+  // are added to the list of aggregations with an explicit
+  // return type.
+  if (funcName.startsWith('user_misery_new') || funcName.startsWith('apdex_new')) {
+    return 'number';
+  }
+
   return null;
   return null;
 }
 }
 
 

+ 102 - 54
static/app/views/performance/data.tsx

@@ -42,6 +42,8 @@ export enum PERFORMANCE_TERM {
   USER_MISERY = 'userMisery',
   USER_MISERY = 'userMisery',
   STATUS_BREAKDOWN = 'statusBreakdown',
   STATUS_BREAKDOWN = 'statusBreakdown',
   DURATION_DISTRIBUTION = 'durationDistribution',
   DURATION_DISTRIBUTION = 'durationDistribution',
+  USER_MISERY_NEW = 'userMiseryNew',
+  APDEX_NEW = 'apdexNew',
 }
 }
 
 
 export type TooltipOption = SelectValue<string> & {
 export type TooltipOption = SelectValue<string> & {
@@ -260,6 +262,14 @@ const PERFORMANCE_TERMS: Record<PERFORMANCE_TERM, TermFormatter> = {
     t(
     t(
       'Distribution buckets counts of transactions at specifics times for your current date range'
       'Distribution buckets counts of transactions at specifics times for your current date range'
     ),
     ),
+  userMiseryNew: () =>
+    t(
+      "User Misery is a score that represents the number of unique users who have experienced load times 4x the project's configured threshold. Adjust project threshold in project performance settings."
+    ),
+  apdexNew: () =>
+    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.'
+    ),
 };
 };
 
 
 export function getTermHelp(
 export function getTermHelp(
@@ -278,25 +288,37 @@ function generateGenericPerformanceEventView(
 ): EventView {
 ): EventView {
   const {query} = location;
   const {query} = location;
 
 
+  const fields = [
+    'key_transaction',
+    'transaction',
+    'project',
+    'tpm()',
+    'p50()',
+    'p95()',
+    'failure_rate()',
+  ];
+
+  const featureFields = organization.features.includes('project-transaction-threshold')
+    ? [
+        `apdex_new()`,
+        'count_unique(user)',
+        `count_miserable_new(user)`,
+        `user_misery_new()`,
+      ]
+    : [
+        `apdex(${organization.apdexThreshold})`,
+        'count_unique(user)',
+        `count_miserable(user,${organization.apdexThreshold})`,
+        `user_misery(${organization.apdexThreshold})`,
+      ];
+
   const hasStartAndEnd = query.start && query.end;
   const hasStartAndEnd = query.start && query.end;
   const savedQuery: NewQuery = {
   const savedQuery: NewQuery = {
     id: undefined,
     id: undefined,
     name: t('Performance'),
     name: t('Performance'),
     query: 'event.type:transaction',
     query: 'event.type:transaction',
     projects: [],
     projects: [],
-    fields: [
-      'key_transaction',
-      'transaction',
-      'project',
-      'tpm()',
-      'p50()',
-      'p95()',
-      'failure_rate()',
-      `apdex(${organization.apdexThreshold})`,
-      'count_unique(user)',
-      `count_miserable(user,${organization.apdexThreshold})`,
-      `user_misery(${organization.apdexThreshold})`,
-    ],
+    fields: [...fields, ...featureFields],
     version: 2,
     version: 2,
   };
   };
 
 
@@ -336,27 +358,39 @@ function generateBackendPerformanceEventView(
 ): EventView {
 ): EventView {
   const {query} = location;
   const {query} = location;
 
 
+  const fields = [
+    'key_transaction',
+    'transaction',
+    'project',
+    'transaction.op',
+    'http.method',
+    'tpm()',
+    'p50()',
+    'p95()',
+    'failure_rate()',
+  ];
+
+  const featureFields = organization.features.includes('project-transaction-threshold')
+    ? [
+        `apdex_new()`,
+        'count_unique(user)',
+        `count_miserable_new(user)`,
+        `user_misery_new()`,
+      ]
+    : [
+        `apdex(${organization.apdexThreshold})`,
+        'count_unique(user)',
+        `count_miserable(user,${organization.apdexThreshold})`,
+        `user_misery(${organization.apdexThreshold})`,
+      ];
+
   const hasStartAndEnd = query.start && query.end;
   const hasStartAndEnd = query.start && query.end;
   const savedQuery: NewQuery = {
   const savedQuery: NewQuery = {
     id: undefined,
     id: undefined,
     name: t('Performance'),
     name: t('Performance'),
     query: 'event.type:transaction',
     query: 'event.type:transaction',
     projects: [],
     projects: [],
-    fields: [
-      'key_transaction',
-      'transaction',
-      'project',
-      'transaction.op',
-      'http.method',
-      'tpm()',
-      'p50()',
-      'p95()',
-      'failure_rate()',
-      `apdex(${organization.apdexThreshold})`,
-      'count_unique(user)',
-      `count_miserable(user,${organization.apdexThreshold})`,
-      `user_misery(${organization.apdexThreshold})`,
-    ],
+    fields: [...fields, ...featureFields],
     version: 2,
     version: 2,
   };
   };
 
 
@@ -396,25 +430,32 @@ function generateFrontendPageloadPerformanceEventView(
 ): EventView {
 ): EventView {
   const {query} = location;
   const {query} = location;
 
 
+  const fields = [
+    'key_transaction',
+    'transaction',
+    'project',
+    'tpm()',
+    'p75(measurements.fcp)',
+    'p75(measurements.lcp)',
+    'p75(measurements.fid)',
+    'p75(measurements.cls)',
+  ];
+
+  const featureFields = organization.features.includes('project-transaction-threshold')
+    ? ['count_unique(user)', `count_miserable_new(user)`, `user_misery_new()`]
+    : [
+        'count_unique(user)',
+        `count_miserable(user,${organization.apdexThreshold})`,
+        `user_misery(${organization.apdexThreshold})`,
+      ];
+
   const hasStartAndEnd = query.start && query.end;
   const hasStartAndEnd = query.start && query.end;
   const savedQuery: NewQuery = {
   const savedQuery: NewQuery = {
     id: undefined,
     id: undefined,
     name: t('Performance'),
     name: t('Performance'),
     query: 'event.type:transaction',
     query: 'event.type:transaction',
     projects: [],
     projects: [],
-    fields: [
-      'key_transaction',
-      'transaction',
-      'project',
-      'tpm()',
-      'p75(measurements.fcp)',
-      'p75(measurements.lcp)',
-      'p75(measurements.fid)',
-      'p75(measurements.cls)',
-      'count_unique(user)',
-      `count_miserable(user,${organization.apdexThreshold})`,
-      `user_misery(${organization.apdexThreshold})`,
-    ],
+    fields: [...fields, ...featureFields],
     version: 2,
     version: 2,
   };
   };
 
 
@@ -456,25 +497,32 @@ function generateFrontendOtherPerformanceEventView(
 ): EventView {
 ): EventView {
   const {query} = location;
   const {query} = location;
 
 
+  const fields = [
+    'key_transaction',
+    'transaction',
+    'project',
+    'transaction.op',
+    'tpm()',
+    'p50(transaction.duration)',
+    'p75(transaction.duration)',
+    'p95(transaction.duration)',
+  ];
+
+  const featureFields = organization.features.includes('project-transaction-threshold')
+    ? ['count_unique(user)', `count_miserable_new(user)`, `user_misery_new()`]
+    : [
+        'count_unique(user)',
+        `count_miserable(user,${organization.apdexThreshold})`,
+        `user_misery(${organization.apdexThreshold})`,
+      ];
+
   const hasStartAndEnd = query.start && query.end;
   const hasStartAndEnd = query.start && query.end;
   const savedQuery: NewQuery = {
   const savedQuery: NewQuery = {
     id: undefined,
     id: undefined,
     name: t('Performance'),
     name: t('Performance'),
     query: 'event.type:transaction',
     query: 'event.type:transaction',
     projects: [],
     projects: [],
-    fields: [
-      'key_transaction',
-      'transaction',
-      'project',
-      'transaction.op',
-      'tpm()',
-      'p50(transaction.duration)',
-      'p75(transaction.duration)',
-      'p95(transaction.duration)',
-      'count_unique(user)',
-      `count_miserable(user,${organization.apdexThreshold})`,
-      `user_misery(${organization.apdexThreshold})`,
-    ],
+    fields: [...fields, ...featureFields],
     version: 2,
     version: 2,
   };
   };
 
 

+ 3 - 1
static/app/views/performance/table.tsx

@@ -316,7 +316,9 @@ class Table extends React.Component<Props, State> {
       // via a prepended column
       // via a prepended column
       .filter(
       .filter(
         (col: TableColumn<React.ReactText>) =>
         (col: TableColumn<React.ReactText>) =>
-          col.name !== 'key_transaction' && !col.name.startsWith('count_miserable')
+          col.name !== 'key_transaction' &&
+          !col.name.startsWith('count_miserable') &&
+          col.name !== 'project_threshold_config'
       )
       )
       .map((col: TableColumn<React.ReactText>, i: number) => {
       .map((col: TableColumn<React.ReactText>, i: number) => {
         if (typeof widths[i] === 'number') {
         if (typeof widths[i] === 'number') {

+ 45 - 14
static/app/views/performance/transactionSummary/index.tsx

@@ -16,7 +16,13 @@ import {GlobalSelection, Organization, Project} from 'app/types';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
 import DiscoverQuery from 'app/utils/discover/discoverQuery';
 import DiscoverQuery from 'app/utils/discover/discoverQuery';
 import EventView from 'app/utils/discover/eventView';
 import EventView from 'app/utils/discover/eventView';
-import {Column, isAggregateField, WebVital} from 'app/utils/discover/fields';
+import {
+  AggregationKey,
+  Column,
+  isAggregateField,
+  QueryFieldValue,
+  WebVital,
+} from 'app/utils/discover/fields';
 import {removeHistogramQueryStrings} from 'app/utils/performance/histogram';
 import {removeHistogramQueryStrings} from 'app/utils/performance/histogram';
 import {decodeScalar} from 'app/utils/queryString';
 import {decodeScalar} from 'app/utils/queryString';
 import {stringifyQueryObject, tokenizeSearch} from 'app/utils/tokenizeSearch';
 import {stringifyQueryObject, tokenizeSearch} from 'app/utils/tokenizeSearch';
@@ -143,15 +149,7 @@ class TransactionSummary extends Component<Props, State> {
       []
       []
     );
     );
 
 
-    return eventView.withColumns([
-      {
-        kind: 'function',
-        function: ['apdex', threshold, undefined],
-      },
-      {
-        kind: 'function',
-        function: ['count_miserable', 'user', threshold],
-      },
+    const totalsColumns: QueryFieldValue[] = [
       {
       {
         kind: 'function',
         kind: 'function',
         function: ['p95', '', undefined],
         function: ['p95', '', undefined],
@@ -172,10 +170,43 @@ class TransactionSummary extends Component<Props, State> {
         kind: 'function',
         kind: 'function',
         function: ['tpm', '', undefined],
         function: ['tpm', '', undefined],
       },
       },
-      {
-        kind: 'function',
-        function: ['user_misery', threshold, undefined],
-      },
+    ];
+
+    const featureColumns: QueryFieldValue[] = organization.features.includes(
+      'project-transaction-threshold'
+    )
+      ? [
+          {
+            kind: 'function',
+            function: ['count_miserable_new' as AggregationKey, 'user', undefined],
+          },
+          {
+            kind: 'function',
+            function: ['user_misery_new' as AggregationKey, '', undefined],
+          },
+          {
+            kind: 'function',
+            function: ['apdex_new' as AggregationKey, '', undefined],
+          },
+        ]
+      : [
+          {
+            kind: 'function',
+            function: ['count_miserable', 'user', threshold],
+          },
+          {
+            kind: 'function',
+            function: ['user_misery', threshold, undefined],
+          },
+          {
+            kind: 'function',
+            function: ['apdex', threshold, undefined],
+          },
+        ];
+
+    return eventView.withColumns([
+      ...totalsColumns,
+      ...featureColumns,
       ...vitals.map(
       ...vitals.map(
         vital =>
         vital =>
           ({
           ({

+ 12 - 2
static/app/views/performance/transactionSummary/sidebarCharts.tsx

@@ -159,6 +159,16 @@ function SidebarCharts({
   const environment = eventView.environment;
   const environment = eventView.environment;
   const threshold = organization.apdexThreshold;
   const threshold = organization.apdexThreshold;
 
 
+  let apdexKey: string;
+  let apdexPerformanceTerm: PERFORMANCE_TERM;
+  if (organization.features.includes('project-transaction-threshold')) {
+    apdexKey = 'apdex_new';
+    apdexPerformanceTerm = PERFORMANCE_TERM.APDEX_NEW;
+  } else {
+    apdexKey = `apdex_${threshold}`;
+    apdexPerformanceTerm = PERFORMANCE_TERM.APDEX;
+  }
+
   return (
   return (
     <RelativeBox>
     <RelativeBox>
       <ChartLabel top="0px">
       <ChartLabel top="0px">
@@ -166,14 +176,14 @@ function SidebarCharts({
           {t('Apdex')}
           {t('Apdex')}
           <QuestionTooltip
           <QuestionTooltip
             position="top"
             position="top"
-            title={getTermHelp(organization, PERFORMANCE_TERM.APDEX)}
+            title={getTermHelp(organization, apdexPerformanceTerm)}
             size="sm"
             size="sm"
           />
           />
         </ChartTitle>
         </ChartTitle>
         <ChartSummaryValue
         <ChartSummaryValue
           isLoading={isLoading}
           isLoading={isLoading}
           error={error}
           error={error}
-          value={totals ? formatFloat(totals[`apdex_${threshold}`], 4) : null}
+          value={totals ? formatFloat(totals[apdexKey], 4) : null}
         />
         />
       </ChartLabel>
       </ChartLabel>
 
 

+ 17 - 4
static/app/views/performance/transactionSummary/userStats.tsx

@@ -41,11 +41,19 @@ function UserStats({
   transactionName,
   transactionName,
 }: Props) {
 }: Props) {
   let userMisery = error !== null ? <div>{'\u2014'}</div> : <Placeholder height="34px" />;
   let userMisery = error !== null ? <div>{'\u2014'}</div> : <Placeholder height="34px" />;
-  const threshold = organization.apdexThreshold;
 
 
   if (!isLoading && error === null && totals) {
   if (!isLoading && error === null && totals) {
-    const miserableUsers = totals[`count_miserable_user_${threshold}`];
-    const userMiseryScore = totals[`user_misery_${threshold}`];
+    let miserableUsers, threshold: number | undefined;
+    let userMiseryScore: number;
+    if (organization.features.includes('project-transaction-threshold')) {
+      threshold = totals.project_threshold_config[1];
+      miserableUsers = totals.count_miserable_new_user;
+      userMiseryScore = totals.user_misery_new;
+    } else {
+      threshold = organization.apdexThreshold;
+      miserableUsers = totals[`count_miserable_user_${threshold}`];
+      userMiseryScore = totals[`user_misery_${threshold}`];
+    }
     const totalUsers = totals.count_unique_user;
     const totalUsers = totals.count_unique_user;
     userMisery = (
     userMisery = (
       <UserMisery
       <UserMisery
@@ -100,7 +108,12 @@ function UserStats({
         {t('User Misery')}
         {t('User Misery')}
         <QuestionTooltip
         <QuestionTooltip
           position="top"
           position="top"
-          title={getTermHelp(organization, PERFORMANCE_TERM.USER_MISERY)}
+          title={getTermHelp(
+            organization,
+            organization.features.includes('project-transaction-threshold')
+              ? PERFORMANCE_TERM.USER_MISERY_NEW
+              : PERFORMANCE_TERM.USER_MISERY
+          )}
           size="sm"
           size="sm"
         />
         />
       </SectionHeading>
       </SectionHeading>

+ 69 - 1
tests/js/spec/views/performance/data.spec.jsx

@@ -1,7 +1,7 @@
 import {generatePerformanceEventView} from 'app/views/performance/data';
 import {generatePerformanceEventView} from 'app/views/performance/data';
 
 
 describe('generatePerformanceEventView()', function () {
 describe('generatePerformanceEventView()', function () {
-  const organization = TestStubs.Organization();
+  const organization = TestStubs.Organization({apdexThreshold: 400});
 
 
   it('generates default values', function () {
   it('generates default values', function () {
     const result = generatePerformanceEventView(organization, {
     const result = generatePerformanceEventView(organization, {
@@ -90,4 +90,72 @@ describe('generatePerformanceEventView()', function () {
       expect.stringContaining('event.type:transaction')
       expect.stringContaining('event.type:transaction')
     );
     );
   });
   });
+
+  it('gets the right column', function () {
+    const result = generatePerformanceEventView(organization, {
+      query: {
+        query: 'key:value tag:value',
+      },
+    });
+    expect(result.fields).toEqual(
+      expect.arrayContaining([expect.objectContaining({field: 'user_misery(400)'})])
+    );
+    expect(result.fields).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({field: 'count_miserable(user,400)'}),
+      ])
+    );
+    expect(result.fields).toEqual(
+      expect.arrayContaining([expect.objectContaining({field: 'apdex(400)'})])
+    );
+
+    expect(result.fields).not.toEqual(
+      expect.arrayContaining([expect.objectContaining({field: 'uuser_misery_new()'})])
+    );
+    expect(result.fields).not.toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({field: 'count_miserable_new(user)'}),
+      ])
+    );
+    expect(result.fields).not.toEqual(
+      expect.arrayContaining([expect.objectContaining({field: 'apdex_new()'})])
+    );
+
+    const newOrganization = TestStubs.Organization({
+      apdexThreshold: 400,
+      features: [
+        'transaction-event',
+        'performance-view',
+        'project-transaction-threshold',
+      ],
+    });
+    const newResult = generatePerformanceEventView(newOrganization, {
+      query: {
+        query: 'key:value tag:value',
+      },
+    });
+    expect(newResult.fields).toEqual(
+      expect.arrayContaining([expect.objectContaining({field: 'user_misery_new()'})])
+    );
+    expect(newResult.fields).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({field: 'count_miserable_new(user)'}),
+      ])
+    );
+    expect(newResult.fields).toEqual(
+      expect.arrayContaining([expect.objectContaining({field: 'apdex_new()'})])
+    );
+
+    expect(newResult.fields).not.toEqual(
+      expect.arrayContaining([expect.objectContaining({field: 'user_misery(400)'})])
+    );
+    expect(newResult.fields).not.toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({field: 'count_miserable(user,400)'}),
+      ])
+    );
+    expect(newResult.fields).not.toEqual(
+      expect.arrayContaining([expect.objectContaining({field: 'apdex(400)'})])
+    );
+  });
 });
 });