Browse Source

feat(performance) Add configurable apdex threshold at the organization level (#20001)

* WIP add some notes on org apdex settings.

* Rough in performance settings.

Still needs lots of tests.

* Update tests and remove unused access prop

The access prop on FieldFromConfig, and FormPanel was inconsistent with
the proptype definition and expected reality. Instead of Scope[] it
should be Set<Scope> as that is what all the forms that use access
expect to get.

* Remove todo.

* Use new threshold in the various places we make apdex functions.

* Update user misery thresholds

* Improve wording and add link

* Update src/sentry/static/sentry/app/routes.jsx

Co-authored-by: Tony <Zylphrex@users.noreply.github.com>

* Make tooltips dynamic, and fix missing apdex data.

* Fix store not reflecting more recent settings updates.

* Remove unused type.

Co-authored-by: Tony <Zylphrex@users.noreply.github.com>
Mark Story 4 years ago
parent
commit
1de509192f

+ 2 - 0
src/sentry/api/endpoints/organization_details.py

@@ -98,6 +98,7 @@ ORG_OPTIONS = (
     ),
     ("relayPiiConfig", "sentry:relay_pii_config", six.text_type, None),
     ("allowJoinRequests", "sentry:join_requests", bool, org_serializers.JOIN_REQUESTS_DEFAULT),
+    ("apdexThreshold", "sentry:apdex_threshold", int, None),
 )
 
 delete_logger = logging.getLogger("sentry.deletions.api")
@@ -155,6 +156,7 @@ class OrganizationSerializer(serializers.Serializer):
     trustedRelays = ListField(child=TrustedRelaySerializer(), required=False)
     allowJoinRequests = serializers.BooleanField(required=False)
     relayPiiConfig = serializers.CharField(required=False, allow_blank=True, allow_null=True)
+    apdexThreshold = serializers.IntegerField(min_value=1, required=False)
 
     @memoize
     def _has_legacy_rate_limits(self):

+ 4 - 0
src/sentry/api/serializers/models/organization.py

@@ -23,6 +23,7 @@ from sentry.constants import (
     SCRAPE_JAVASCRIPT_DEFAULT,
     JOIN_REQUESTS_DEFAULT,
     EVENTS_MEMBER_ADMIN_DEFAULT,
+    APDEX_THRESHOLD_DEFAULT,
 )
 
 from sentry.lang.native.utils import convert_crashreport_count
@@ -264,6 +265,9 @@ class DetailedOrganizationSerializer(OrganizationSerializer):
                 ),
                 "relayPiiConfig": six.text_type(obj.get_option("sentry:relay_pii_config") or u"")
                 or None,
+                "apdexThreshold": int(
+                    obj.get_option("sentry:apdex_threshold", APDEX_THRESHOLD_DEFAULT)
+                ),
             }
         )
 

+ 1 - 0
src/sentry/constants.py

@@ -511,6 +511,7 @@ REQUIRE_SCRUB_IP_ADDRESS_DEFAULT = False
 SCRAPE_JAVASCRIPT_DEFAULT = True
 TRUSTED_RELAYS_DEFAULT = None
 JOIN_REQUESTS_DEFAULT = True
+APDEX_THRESHOLD_DEFAULT = 300
 
 # `sentry:events_member_admin` - controls whether the 'member' role gets the event:admin scope
 EVENTS_MEMBER_ADMIN_DEFAULT = True

+ 11 - 0
src/sentry/static/sentry/app/routes.jsx

@@ -776,6 +776,17 @@ function routes() {
         component={errorHandler(LazyLoad)}
       />
 
+      <Route
+        path="performance/"
+        name={t('Performance')}
+        componentPromise={() =>
+          import(
+            /* webpackChunkName: "OrganizationPerformance" */ 'app/views/settings/organizationPerformance'
+          )
+        }
+        component={errorHandler(LazyLoad)}
+      />
+
       <Route
         path="settings/"
         componentPromise={() =>

+ 1 - 0
src/sentry/static/sentry/app/types/index.tsx

@@ -130,6 +130,7 @@ export type LightWeightOrganization = OrganizationSummary & {
   allowSharedIssues: boolean;
   dataScrubberDefaults: boolean;
   dataScrubber: boolean;
+  apdexThreshold: number;
   onboardingTasks: OnboardingTaskStatus[];
   trustedRelays: Relay[];
   role?: string;

+ 29 - 1
src/sentry/static/sentry/app/utils/discover/fields.tsx

@@ -1,3 +1,4 @@
+import {LightWeightOrganization} from 'app/types';
 import {assert} from 'app/types/utils';
 
 export type Sort = {
@@ -190,6 +191,9 @@ export const AGGREGATIONS = {
     multiPlotType: 'line',
   },
   apdex: {
+    generateDefaultValue({parameter, organization}: DefaultValueInputs) {
+      return organization.apdexThreshold?.toString() ?? parameter.defaultValue;
+    },
     parameters: [
       {
         kind: 'value',
@@ -216,6 +220,9 @@ export const AGGREGATIONS = {
     multiPlotType: 'line',
   },
   user_misery: {
+    generateDefaultValue({parameter, organization}: DefaultValueInputs) {
+      return organization.apdexThreshold?.toString() ?? parameter.defaultValue;
+    },
     parameters: [
       {
         kind: 'value',
@@ -253,11 +260,32 @@ export type AggregationOutputType = Extract<
 
 export type PlotType = 'line' | 'area';
 
+type DefaultValueInputs = {
+  parameter: AggregateParameter;
+  organization: LightWeightOrganization;
+};
+
 export type Aggregation = {
+  /**
+   * Used by functions that need to define their default values dynamically
+   * based on the organization, or parameter data.
+   */
+  generateDefaultValue?: (data: DefaultValueInputs) => string;
+  /**
+   * List of parameters for the function.
+   */
   parameters: Readonly<AggregateParameter[]>;
-  // null means to inherit from the column.
+  /**
+   * The output type. Null means to inherit from the field.
+   */
   outputType: AggregationOutputType | null;
+  /**
+   * Can this function be used in a sort result
+   */
   isSortable: boolean;
+  /**
+   * How this function should be plotted when shown in a multiseries result (top5)
+   */
   multiPlotType: PlotType;
 };
 

+ 2 - 2
src/sentry/static/sentry/app/views/eventsV2/table/columnEditCollection.tsx

@@ -10,7 +10,7 @@ import {
 } from 'app/components/events/interfaces/spans/utils';
 import {IconAdd, IconDelete, IconGrabbable} from 'app/icons';
 import {t} from 'app/locale';
-import {SelectValue, OrganizationSummary} from 'app/types';
+import {SelectValue, LightWeightOrganization} from 'app/types';
 import space from 'app/styles/space';
 import theme from 'app/utils/theme';
 import {Column} from 'app/utils/discover/fields';
@@ -22,7 +22,7 @@ import {generateFieldOptions} from '../utils';
 type Props = {
   // Input columns
   columns: Column[];
-  organization: OrganizationSummary;
+  organization: LightWeightOrganization;
   tagKeys: null | string[];
   // Fired when columns are added/removed/modified
   onChange: (columns: Column[]) => void;

+ 2 - 2
src/sentry/static/sentry/app/views/eventsV2/table/columnEditModal.tsx

@@ -8,7 +8,7 @@ import ExternalLink from 'app/components/links/externalLink';
 import {DISCOVER2_DOCS_URL} from 'app/constants';
 import {ModalRenderProps} from 'app/actionCreators/modal';
 import {t, tct} from 'app/locale';
-import {OrganizationSummary} from 'app/types';
+import {LightWeightOrganization} from 'app/types';
 import space from 'app/styles/space';
 import theme from 'app/utils/theme';
 import {Column} from 'app/utils/discover/fields';
@@ -18,7 +18,7 @@ import ColumnEditCollection from './columnEditCollection';
 
 type Props = {
   columns: Column[];
-  organization: OrganizationSummary;
+  organization: LightWeightOrganization;
   tagKeys: null | string[];
   // Fired when column selections have been applied.
   onApply: (columns: Column[]) => void;

+ 15 - 4
src/sentry/static/sentry/app/views/eventsV2/utils.tsx

@@ -4,7 +4,7 @@ import {browserHistory} from 'react-router';
 
 import {tokenizeSearch, stringifyQueryObject} from 'app/utils/tokenizeSearch';
 import {t} from 'app/locale';
-import {Event, Organization, OrganizationSummary, SelectValue} from 'app/types';
+import {Event, LightWeightOrganization, SelectValue} from 'app/types';
 import {getTitle} from 'app/utils/events';
 import {getUtcDateString} from 'app/utils/dates';
 import {URL_PARAM} from 'app/constants/globalSelectionHeader';
@@ -120,7 +120,7 @@ export function generateTitle({eventView, event}: {eventView: EventView; event?:
   return titles.join(' - ');
 }
 
-export function getPrebuiltQueries(organization: Organization) {
+export function getPrebuiltQueries(organization: LightWeightOrganization) {
   let views = ALL_VIEWS;
   if (organization.features.includes('performance-view')) {
     // insert transactions queries at index 2
@@ -435,7 +435,7 @@ function generateExpandedConditions(
 }
 
 type FieldGeneratorOpts = {
-  organization: OrganizationSummary;
+  organization: LightWeightOrganization;
   tagKeys?: string[] | null;
   aggregations?: Record<string, Aggregation>;
   fields?: Record<string, ColumnType>;
@@ -462,13 +462,24 @@ export function generateFieldOptions({
   // later as well.
   functions.forEach(func => {
     const ellipsis = aggregations[func].parameters.length ? '\u2026' : '';
+    const parameters = aggregations[func].parameters.map(param => {
+      const generator = aggregations[func].generateDefaultValue;
+      if (typeof generator === 'undefined') {
+        return param;
+      }
+      return {
+        ...param,
+        defaultValue: generator({parameter: param, organization}),
+      };
+    });
+
     fieldOptions[`function:${func}`] = {
       label: `${func}(${ellipsis})`,
       value: {
         kind: FieldValueKind.FUNCTION,
         meta: {
           name: func,
-          parameters: [...aggregations[func].parameters],
+          parameters,
         },
       },
     };

+ 1 - 1
src/sentry/static/sentry/app/views/performance/charts/chart.tsx

@@ -32,7 +32,7 @@ function computeAxisMax(data) {
   const power = Math.log10(maxValue);
   const magnitude = min([max([10 ** (power - Math.floor(power)), 0]), 10]) as number;
 
-  let scale;
+  let scale: number;
   if (magnitude <= 2.5) {
     scale = 0.2;
   } else if (magnitude <= 5) {

Some files were not shown because too many files changed in this diff