Browse Source

feat(discover): Add toggle for processed event baseline (#38669)

Add the toggle component that is disabled if
the MetricsCardinality conditions aren't met.

Toggling on/off currently isn't hooked up to
anything.

![Screen Shot 2022-09-12 at 3 31 47
PM](https://user-images.githubusercontent.com/63818634/189740053-3f5c7f26-12b1-40e4-9b64-113894a40ba3.png)
Shruthi 2 years ago
parent
commit
d7e418366a

+ 127 - 8
static/app/views/eventsV2/chartFooter.spec.tsx

@@ -1,11 +1,42 @@
 import {mountWithTheme} from 'sentry-test/enzyme';
 import {initializeOrg} from 'sentry-test/initializeOrg';
+import {addMetricsDataMock} from 'sentry-test/performance/addMetricsDataMock';
 import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
 
 import {t} from 'sentry/locale';
+import {Organization} from 'sentry/types/organization';
+import {Project} from 'sentry/types/project';
 import {DisplayModes} from 'sentry/utils/discover/types';
+import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
 import ChartFooter from 'sentry/views/eventsV2/chartFooter';
 
+function metricsCardinalityWrapped(
+  children: React.ReactNode,
+  organization: Organization,
+  project: Project
+) {
+  const routerLocation: {query: {project?: number}} = {
+    query: {project: parseInt(project.id, 10)},
+  };
+
+  const router = {
+    location: routerLocation,
+  };
+
+  const initialData = initializeOrg({
+    organization,
+    projects: [project],
+    project,
+    router,
+  });
+  const location = initialData.router.location;
+  return (
+    <MetricsCardinalityProvider location={location} organization={organization}>
+      {children}
+    </MetricsCardinalityProvider>
+  );
+}
+
 describe('EventsV2 > ChartFooter', function () {
   const features = ['discover-basic'];
   const yAxisValue = ['count()', 'failure_count()'];
@@ -14,9 +45,11 @@ describe('EventsV2 > ChartFooter', function () {
     {label: 'failure_count()', value: 'failure_count()'},
   ];
 
+  const project = TestStubs.Project();
+
   afterEach(function () {});
 
-  it('renders yAxis option using OptionCheckboxSelector using entire yAxisValue', async function () {
+  it('renders yAxis option using OptionCheckboxSelector using entire yAxisValue', function () {
     const organization = TestStubs.Organization({
       features: [...features],
     });
@@ -31,7 +64,7 @@ describe('EventsV2 > ChartFooter', function () {
       projects: [],
     });
 
-    const wrapper = mountWithTheme(
+    const chartFooter = (
       <ChartFooter
         organization={organization}
         total={100}
@@ -43,11 +76,16 @@ describe('EventsV2 > ChartFooter', function () {
         onDisplayChange={() => undefined}
         onTopEventsChange={() => undefined}
         topEvents="5"
-      />,
+        showBaseline={false}
+        setShowBaseline={() => undefined}
+      />
+    );
+
+    const wrapper = mountWithTheme(
+      metricsCardinalityWrapped(chartFooter, organization, project),
       initialData.routerContext
     );
 
-    await tick();
     wrapper.update();
 
     const optionCheckboxSelector = wrapper.find('OptionSelector').last();
@@ -69,7 +107,7 @@ describe('EventsV2 > ChartFooter', function () {
       projects: [],
     });
 
-    const wrapper = mountWithTheme(
+    const chartFooter = (
       <ChartFooter
         organization={organization}
         total={100}
@@ -81,7 +119,13 @@ describe('EventsV2 > ChartFooter', function () {
         onDisplayChange={() => undefined}
         onTopEventsChange={() => undefined}
         topEvents="5"
-      />,
+        showBaseline={false}
+        setShowBaseline={() => undefined}
+      />
+    );
+
+    const wrapper = mountWithTheme(
+      metricsCardinalityWrapped(chartFooter, organization, project),
       initialData.routerContext
     );
 
@@ -98,7 +142,7 @@ describe('EventsV2 > ChartFooter', function () {
     });
     let yAxis = ['count()'];
 
-    render(
+    const chartFooter = (
       <ChartFooter
         organization={organization}
         total={100}
@@ -110,21 +154,92 @@ describe('EventsV2 > ChartFooter', function () {
         onDisplayChange={() => undefined}
         onTopEventsChange={() => undefined}
         topEvents="5"
+        showBaseline={false}
+        setShowBaseline={() => undefined}
       />
     );
 
+    render(metricsCardinalityWrapped(chartFooter, organization, project));
+
     userEvent.click(screen.getByText('count()'));
     userEvent.click(screen.getByText('failure_count()'));
     expect(yAxis).toEqual(['failure_count()']);
   });
 
+  it('disables processed baseline toggle if metrics cardinality conditions not met', function () {
+    const organization = TestStubs.Organization({
+      features: [...features, 'discover-metrics-baseline'],
+    });
+
+    const chartFooter = (
+      <ChartFooter
+        organization={organization}
+        total={100}
+        yAxisValue={['count()']}
+        yAxisOptions={yAxisOptions}
+        onAxisChange={jest.fn}
+        displayMode={DisplayModes.DEFAULT}
+        displayOptions={[{label: DisplayModes.DEFAULT, value: DisplayModes.DEFAULT}]}
+        onDisplayChange={() => undefined}
+        onTopEventsChange={() => undefined}
+        topEvents="5"
+        showBaseline={false}
+        setShowBaseline={() => undefined}
+      />
+    );
+
+    render(metricsCardinalityWrapped(chartFooter, organization, project));
+
+    expect(screen.getByText(/Processed events/i)).toBeInTheDocument();
+    expect(screen.getByTestId('processed-events-toggle')).toBeDisabled();
+  });
+
+  it('enables processed baseline toggle if metrics cardinality conditions met', function () {
+    addMetricsDataMock({
+      metricsCount: 100,
+      nullCount: 0,
+      unparamCount: 1,
+    });
+
+    const organization = TestStubs.Organization({
+      features: [
+        ...features,
+        'discover-metrics-baseline',
+        'performance-transaction-name-only-search',
+        'organizations:performance-transaction-name-only-search',
+      ],
+    });
+
+    const chartFooter = (
+      <ChartFooter
+        organization={organization}
+        total={100}
+        yAxisValue={['count()']}
+        yAxisOptions={yAxisOptions}
+        onAxisChange={jest.fn}
+        displayMode={DisplayModes.DEFAULT}
+        displayOptions={[{label: DisplayModes.DEFAULT, value: DisplayModes.DEFAULT}]}
+        onDisplayChange={() => undefined}
+        onTopEventsChange={() => undefined}
+        topEvents="5"
+        showBaseline={false}
+        setShowBaseline={() => undefined}
+      />
+    );
+
+    render(metricsCardinalityWrapped(chartFooter, organization, project));
+
+    expect(screen.getByText(/Processed events/i)).toBeInTheDocument();
+    expect(screen.getByTestId('processed-events-toggle')).toBeEnabled();
+  });
+
   it('renders multi value y-axis dropdown selector on a non-Top display', function () {
     const organization = TestStubs.Organization({
       features,
     });
     let yAxis = ['count()'];
 
-    render(
+    const chartFooter = (
       <ChartFooter
         organization={organization}
         total={100}
@@ -136,9 +251,13 @@ describe('EventsV2 > ChartFooter', function () {
         onDisplayChange={() => undefined}
         onTopEventsChange={() => undefined}
         topEvents="5"
+        showBaseline={false}
+        setShowBaseline={() => undefined}
       />
     );
 
+    render(metricsCardinalityWrapped(chartFooter, organization, project));
+
     userEvent.click(screen.getByText('count()'));
     userEvent.click(screen.getByText('failure_count()'));
     expect(yAxis).toEqual(['count()', 'failure_count()']);

+ 29 - 0
static/app/views/eventsV2/chartFooter.tsx

@@ -1,3 +1,7 @@
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import Feature from 'sentry/components/acl/feature';
 import OptionSelector from 'sentry/components/charts/optionSelector';
 import {
   ChartControls,
@@ -5,9 +9,11 @@ import {
   SectionHeading,
   SectionValue,
 } from 'sentry/components/charts/styles';
+import Switch from 'sentry/components/switchButton';
 import {t} from 'sentry/locale';
 import {Organization, SelectValue} from 'sentry/types';
 import {TOP_EVENT_MODES} from 'sentry/utils/discover/types';
+import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
 
 type Props = {
   displayMode: string;
@@ -16,6 +22,8 @@ type Props = {
   onDisplayChange: (value: string) => void;
   onTopEventsChange: (value: string) => void;
   organization: Organization;
+  setShowBaseline: (value: boolean) => void;
+  showBaseline: boolean;
   topEvents: string;
   total: number | null;
   yAxisOptions: SelectValue<string>[];
@@ -32,7 +40,11 @@ export default function ChartFooter({
   onDisplayChange,
   onTopEventsChange,
   topEvents,
+  setShowBaseline,
+  showBaseline,
+  organization,
 }: Props) {
+  const metricsCardinality = useMetricsCardinalityContext();
   const elements: React.ReactNode[] = [];
 
   elements.push(<SectionHeading key="total-label">{t('Total Events')}</SectionHeading>);
@@ -54,6 +66,18 @@ export default function ChartFooter({
     <ChartControls>
       <InlineContainer>{elements}</InlineContainer>
       <InlineContainer>
+        <Feature organization={organization} features={['discover-metrics-baseline']}>
+          <Fragment>
+            <SwitchLabel>{t('Processed events')}</SwitchLabel>
+            <Switch
+              data-test-id="processed-events-toggle"
+              isActive={showBaseline}
+              isDisabled={metricsCardinality.outcome?.forceTransactionsOnly}
+              size="lg"
+              toggle={() => setShowBaseline(!showBaseline)}
+            />
+          </Fragment>
+        </Feature>
         <OptionSelector
           title={t('Display')}
           selected={displayMode}
@@ -92,3 +116,8 @@ export default function ChartFooter({
     </ChartControls>
   );
 }
+
+const SwitchLabel = styled('div')`
+  padding-right: 4px;
+  font-weight: bold;
+`;

+ 16 - 10
static/app/views/eventsV2/results.tsx

@@ -41,6 +41,7 @@ import {
   MULTI_Y_AXIS_SUPPORTED_DISPLAY_MODES,
 } from 'sentry/utils/discover/types';
 import localStorage from 'sentry/utils/localStorage';
+import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
 import {decodeList, decodeScalar} from 'sentry/utils/queryString';
 import withApi from 'sentry/utils/withApi';
 import withOrganization from 'sentry/utils/withOrganization';
@@ -547,18 +548,23 @@ class Results extends Component<Props, State> {
                       />
                     )}
                   </CustomMeasurementsContext.Consumer>
-                  <ResultsChart
-                    router={router}
+                  <MetricsCardinalityProvider
                     organization={organization}
-                    eventView={eventView}
                     location={location}
-                    onAxisChange={this.handleYAxisChange}
-                    onDisplayChange={this.handleDisplayChange}
-                    onTopEventsChange={this.handleTopEventsChange}
-                    total={totalValues}
-                    confirmedQuery={confirmedQuery}
-                    yAxis={yAxisArray}
-                  />
+                  >
+                    <ResultsChart
+                      router={router}
+                      organization={organization}
+                      eventView={eventView}
+                      location={location}
+                      onAxisChange={this.handleYAxisChange}
+                      onDisplayChange={this.handleDisplayChange}
+                      onTopEventsChange={this.handleTopEventsChange}
+                      total={totalValues}
+                      confirmedQuery={confirmedQuery}
+                      yAxis={yAxisArray}
+                    />
+                  </MetricsCardinalityProvider>
                 </Top>
                 <Layout.Main fullWidth={!showTags}>
                   <Table

+ 43 - 36
static/app/views/eventsV2/resultsChart.spec.tsx

@@ -4,6 +4,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
 import {t} from 'sentry/locale';
 import EventView from 'sentry/utils/discover/eventView';
 import {DisplayModes} from 'sentry/utils/discover/types';
+import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
 import ResultsChart from 'sentry/views/eventsV2/resultsChart';
 
 describe('EventsV2 > ResultsChart', function () {
@@ -33,18 +34,20 @@ describe('EventsV2 > ResultsChart', function () {
 
   it('only allows default, daily, previous period, and bar display modes when multiple y axis are selected', function () {
     const wrapper = mountWithTheme(
-      <ResultsChart
-        router={TestStubs.router()}
-        organization={organization}
-        eventView={eventView}
-        location={location}
-        onAxisChange={() => undefined}
-        onDisplayChange={() => undefined}
-        total={1}
-        confirmedQuery
-        yAxis={['count()', 'failure_count()']}
-        onTopEventsChange={() => {}}
-      />,
+      <MetricsCardinalityProvider location={location} organization={organization}>
+        <ResultsChart
+          router={TestStubs.router()}
+          organization={organization}
+          eventView={eventView}
+          location={location}
+          onAxisChange={() => undefined}
+          onDisplayChange={() => undefined}
+          total={1}
+          confirmedQuery
+          yAxis={['count()', 'failure_count()']}
+          onTopEventsChange={() => {}}
+        />
+      </MetricsCardinalityProvider>,
       initialData.routerContext
     );
     const displayOptions = wrapper.find('ChartFooter').props().displayOptions;
@@ -64,18 +67,20 @@ describe('EventsV2 > ResultsChart', function () {
 
   it('does not display a chart if no y axis is selected', function () {
     const wrapper = mountWithTheme(
-      <ResultsChart
-        router={TestStubs.router()}
-        organization={organization}
-        eventView={eventView}
-        location={location}
-        onAxisChange={() => undefined}
-        onDisplayChange={() => undefined}
-        total={1}
-        confirmedQuery
-        yAxis={[]}
-        onTopEventsChange={() => {}}
-      />,
+      <MetricsCardinalityProvider location={location} organization={organization}>
+        <ResultsChart
+          router={TestStubs.router()}
+          organization={organization}
+          eventView={eventView}
+          location={location}
+          onAxisChange={() => undefined}
+          onDisplayChange={() => undefined}
+          total={1}
+          confirmedQuery
+          yAxis={[]}
+          onTopEventsChange={() => {}}
+        />
+      </MetricsCardinalityProvider>,
       initialData.routerContext
     );
     expect(wrapper.find('NoChartContainer').children().children().html()).toEqual(
@@ -91,18 +96,20 @@ describe('EventsV2 > ResultsChart', function () {
       {field: 'equation|count() + 2'},
     ];
     const wrapper = mountWithTheme(
-      <ResultsChart
-        router={TestStubs.router()}
-        organization={organization}
-        eventView={eventView}
-        location={location}
-        onAxisChange={() => undefined}
-        onDisplayChange={() => undefined}
-        total={1}
-        confirmedQuery
-        yAxis={['count()']}
-        onTopEventsChange={() => {}}
-      />,
+      <MetricsCardinalityProvider location={location} organization={organization}>
+        <ResultsChart
+          router={TestStubs.router()}
+          organization={organization}
+          eventView={eventView}
+          location={location}
+          onAxisChange={() => undefined}
+          onDisplayChange={() => undefined}
+          total={1}
+          confirmedQuery
+          yAxis={['count()']}
+          onTopEventsChange={() => {}}
+        />
+      </MetricsCardinalityProvider>,
       initialData.routerContext
     );
     const yAxisOptions = wrapper.find('ChartFooter').props().yAxisOptions;

+ 21 - 5
static/app/views/eventsV2/resultsChart.tsx

@@ -13,7 +13,7 @@ import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilte
 import {Panel} from 'sentry/components/panels';
 import Placeholder from 'sentry/components/placeholder';
 import {t} from 'sentry/locale';
-import {Organization} from 'sentry/types';
+import {Organization, SelectValue} from 'sentry/types';
 import {valueIsEqual} from 'sentry/utils';
 import {getUtcToLocalDateObject} from 'sentry/utils/dates';
 import EventView from 'sentry/utils/discover/eventView';
@@ -158,9 +158,15 @@ type ContainerProps = {
   yAxis: string[];
 };
 
-class ResultsChartContainer extends Component<ContainerProps> {
-  state = {
+type ContainerState = {
+  showBaseline: boolean;
+  yAxisOptions: SelectValue<string>[];
+};
+
+class ResultsChartContainer extends Component<ContainerProps, ContainerState> {
+  state: ContainerState = {
     yAxisOptions: this.getYAxisOptions(this.props.eventView),
+    showBaseline: true,
   };
 
   componentWillReceiveProps(nextProps) {
@@ -172,7 +178,11 @@ class ResultsChartContainer extends Component<ContainerProps> {
     }
   }
 
-  shouldComponentUpdate(nextProps: ContainerProps) {
+  shouldComponentUpdate(nextProps: ContainerProps, nextState: ContainerState) {
+    if (nextState.showBaseline !== this.state.showBaseline) {
+      return true;
+    }
+
     const {eventView, ...restProps} = this.props;
     const {eventView: nextEventView, ...restNextProps} = nextProps;
 
@@ -213,7 +223,7 @@ class ResultsChartContainer extends Component<ContainerProps> {
       yAxis,
     } = this.props;
 
-    const {yAxisOptions} = this.state;
+    const {yAxisOptions, showBaseline} = this.state;
 
     const hasQueryFeature = organization.features.includes('discover-query');
     const displayOptions = eventView
@@ -271,6 +281,12 @@ class ResultsChartContainer extends Component<ContainerProps> {
           onDisplayChange={onDisplayChange}
           onTopEventsChange={onTopEventsChange}
           topEvents={eventView.topEvents ?? TOP_N.toString()}
+          showBaseline={showBaseline}
+          setShowBaseline={value =>
+            this.setState({
+              showBaseline: value,
+            })
+          }
         />
       </StyledPanel>
     );

+ 1 - 2
static/app/views/performance/landing/index.spec.tsx

@@ -1,5 +1,6 @@
 import {browserHistory} from 'react-router';
 
+import {addMetricsDataMock} from 'sentry-test/performance/addMetricsDataMock';
 import {initializeData} from 'sentry-test/performance/initializePerformanceData';
 import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
 
@@ -11,8 +12,6 @@ import {PerformanceLanding} from 'sentry/views/performance/landing';
 import {REACT_NATIVE_COLUMN_TITLES} from 'sentry/views/performance/landing/data';
 import {LandingDisplayField} from 'sentry/views/performance/landing/utils';
 
-import {addMetricsDataMock} from './metricsDataSwitcher.spec';
-
 const WrappedComponent = ({data, withStaticFilters = false}) => {
   const eventView = generatePerformanceEventView(data.router.location, data.projects, {
     withStaticFilters,

+ 1 - 44
static/app/views/performance/landing/metricsDataSwitcher.spec.tsx

@@ -1,3 +1,4 @@
+import {addMetricsDataMock} from 'sentry-test/performance/addMetricsDataMock';
 import {initializeData} from 'sentry-test/performance/initializePerformanceData';
 import {act, render, screen} from 'sentry-test/reactTestingLibrary';
 
@@ -7,50 +8,6 @@ import {OrganizationContext} from 'sentry/views/organizationContext';
 import {generatePerformanceEventView} from 'sentry/views/performance/data';
 import {PerformanceLanding} from 'sentry/views/performance/landing';
 
-export function addMetricsDataMock(settings?: {
-  metricsCount: number;
-  nullCount: number;
-  unparamCount: number;
-  compatibleProjects?: number[];
-  dynamicSampledProjects?: number[];
-}) {
-  const compatible_projects = settings?.compatibleProjects ?? [];
-  const metricsCount = settings?.metricsCount ?? 10;
-  const unparamCount = settings?.unparamCount ?? 0;
-  const nullCount = settings?.nullCount ?? 0;
-  const dynamic_sampling_projects = settings?.dynamicSampledProjects ?? [1];
-
-  MockApiClient.addMockResponse({
-    method: 'GET',
-    url: `/organizations/org-slug/metrics-compatibility/`,
-    body: {
-      compatible_projects,
-      dynamic_sampling_projects,
-    },
-  });
-
-  MockApiClient.addMockResponse({
-    method: 'GET',
-    url: `/organizations/org-slug/metrics-compatibility-sums/`,
-    body: {
-      sum: {
-        metrics: metricsCount,
-        metrics_unparam: unparamCount,
-        metrics_null: nullCount,
-      },
-    },
-  });
-
-  MockApiClient.addMockResponse({
-    method: 'GET',
-    url: `/organizations/org-slug/events/`,
-    body: {
-      data: [{}],
-      meta: {},
-    },
-  });
-}
-
 const WrappedComponent = ({data, withStaticFilters = true}) => {
   const eventView = generatePerformanceEventView(data.router.location, data.projects, {
     withStaticFilters,

+ 43 - 0
tests/js/sentry-test/performance/addMetricsDataMock.ts

@@ -0,0 +1,43 @@
+export function addMetricsDataMock(settings?: {
+  metricsCount: number;
+  nullCount: number;
+  unparamCount: number;
+  compatibleProjects?: number[];
+  dynamicSampledProjects?: number[];
+}) {
+  const compatible_projects = settings?.compatibleProjects ?? [];
+  const metricsCount = settings?.metricsCount ?? 10;
+  const unparamCount = settings?.unparamCount ?? 0;
+  const nullCount = settings?.nullCount ?? 0;
+  const dynamic_sampling_projects = settings?.dynamicSampledProjects ?? [1];
+
+  MockApiClient.addMockResponse({
+    method: 'GET',
+    url: `/organizations/org-slug/metrics-compatibility/`,
+    body: {
+      compatible_projects,
+      dynamic_sampling_projects,
+    },
+  });
+
+  MockApiClient.addMockResponse({
+    method: 'GET',
+    url: `/organizations/org-slug/metrics-compatibility-sums/`,
+    body: {
+      sum: {
+        metrics: metricsCount,
+        metrics_unparam: unparamCount,
+        metrics_null: nullCount,
+      },
+    },
+  });
+
+  MockApiClient.addMockResponse({
+    method: 'GET',
+    url: `/organizations/org-slug/events/`,
+    body: {
+      data: [{}],
+      meta: {},
+    },
+  });
+}