Browse Source

ref(performance): Add sampling alert Call to action MVP - (#39510)

Priscila Oliveira 2 years ago
parent
commit
8718e920ea

+ 38 - 0
fixtures/js-stubs/outcomes.js

@@ -281,3 +281,41 @@ export function OutcomesWithReason() {
     ],
   };
 }
+
+export function OutcomesWithLowProcessedEvents() {
+  const otherOutcomesGroups = TestStubs.Outcomes().groups.filter(
+    group => group.by.outcome !== 'accepted' && group.by.outcome !== 'client_discard'
+  );
+
+  return {
+    ...TestStubs.Outcomes(),
+    groups: [
+      ...otherOutcomesGroups,
+      {
+        by: {outcome: 'accepted'},
+        totals: {'sum(quantity)': 1231342},
+        series: {
+          'sum(quantity)': [
+            0, 0, 0, 1, 94, 1, 1, 0, 566, 179, 1, 1, 1, 0, 222, 6, 287, 465, 83, 7, 0,
+            1835, 145, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0, 1, 849, 25331, 147200, 220014,
+            189001, 99590, 81288, 134522, 151489, 128585, 41643, 6404, 145, 1381,
+          ],
+        },
+      },
+      {
+        by: {outcome: 'client_discard'},
+        totals: {'sum(quantity)': 18868070},
+        series: {
+          'sum(quantity)': [
+            0, 0, 0, 259581, 246831, 278464, 290677, 242770, 242559, 248963, 250920,
+            268994, 296129, 308165, 302398, 301891, 316698, 333888, 336204, 329735,
+            323717, 317564, 312407, 307008, 301681, 299652, 276849, 274486, 298985,
+            368148, 444434, 423119, 416110, 464443, 526387, 692300, 720026, 719854,
+            719658, 719237, 717889, 719757, 718147, 719843, 712099, 643028, 545065,
+            311310,
+          ],
+        },
+      },
+    ],
+  };
+}

+ 1 - 0
fixtures/js-stubs/types.tsx

@@ -77,6 +77,7 @@ type TestStubFixtures = {
   OrganizationIntegrations: OverridableStub;
   Organizations: OverridableStub;
   Outcomes: SimpleStub;
+  OutcomesWithLowProcessedEvents: SimpleStub;
   OutcomesWithReason: SimpleStub;
   PhabricatorCreate: SimpleStub;
   PhabricatorPlugin: SimpleStub;

+ 11 - 6
static/app/main.tsx

@@ -1,4 +1,5 @@
 import {browserHistory, Router, RouterContext} from 'react-router';
+import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
 
 import DemoHeader from 'sentry/components/demo/demoHeader';
 import ThemeAndStyleProvider from 'sentry/components/themeAndStyleProvider';
@@ -19,15 +20,19 @@ function renderRouter(props: any) {
   );
 }
 
+const queryClient = new QueryClient();
+
 function Main() {
   return (
     <ThemeAndStyleProvider>
-      <PersistedStoreProvider>
-        {ConfigStore.get('demoMode') && <DemoHeader />}
-        <Router history={browserHistory} render={renderRouter}>
-          {routes()}
-        </Router>
-      </PersistedStoreProvider>
+      <QueryClientProvider client={queryClient}>
+        <PersistedStoreProvider>
+          {ConfigStore.get('demoMode') && <DemoHeader />}
+          <Router history={browserHistory} render={renderRouter}>
+            {routes()}
+          </Router>
+        </PersistedStoreProvider>
+      </QueryClientProvider>
     </ThemeAndStyleProvider>
   );
 }

+ 5 - 0
static/app/utils/analytics/samplingAnalyticsEvents.tsx

@@ -8,6 +8,9 @@ type Rule = {
 };
 
 export type SamplingEventParameters = {
+  'sampling.performance.metrics.accuracy.alert': {
+    project_id: string;
+  };
   'sampling.sdk.client.rate.change.alert': {
     project_id: string;
   };
@@ -108,6 +111,8 @@ export type SamplingEventParameters = {
 type SamplingAnalyticsKey = keyof SamplingEventParameters;
 
 export const samplingEventMap: Record<SamplingAnalyticsKey, string> = {
+  'sampling.performance.metrics.accuracy.alert':
+    'Sampling Performance Metrics Accuracy Alert',
   'sampling.sdk.client.rate.change.alert': 'Recommended sdk client rate change alert',
   'sampling.sdk.updgrades.alert': 'Recommended sdk upgrades alert',
   'sampling.sdk.incompatible.alert': 'Incompatible sdk upgrades alert',

+ 0 - 1
static/app/utils/discover/eventView.tsx

@@ -1416,7 +1416,6 @@ class EventView {
     return selectedProjectIds.map(id => projectMap[String(id)]);
   }
 }
-``;
 
 export type ImmutableEventView = Readonly<Omit<EventView, 'additionalConditions'>>;
 

+ 48 - 0
static/app/utils/useOrganizationStats.tsx

@@ -0,0 +1,48 @@
+import {useQuery, UseQueryOptions} from '@tanstack/react-query';
+
+import {ResponseMeta} from 'sentry/api';
+import {t} from 'sentry/locale';
+import {Organization, SeriesApi} from 'sentry/types';
+import handleXhrErrorResponse from 'sentry/utils/handleXhrErrorResponse';
+import useApi from 'sentry/utils/useApi';
+
+type Props = {
+  organizationSlug: Organization['slug'];
+  /**
+   * Can be used to configure how the query is fetched, cached, etc
+   */
+  queryOptions?: UseQueryOptions<SeriesApi>;
+  /**
+   * Query parameters to add to the requested URL
+   */
+  queryParameters?: Record<string, any>;
+};
+
+// Fetches the organization stats
+export function useOrganizationStats({
+  organizationSlug,
+  queryOptions,
+  queryParameters,
+}: Props) {
+  const api = useApi();
+
+  const organizationStats = useQuery<SeriesApi>(
+    ['organizationStats', organizationSlug, queryParameters],
+    async (): Promise<SeriesApi> =>
+      await api.requestPromise(`/organizations/${organizationSlug}/stats_v2/`, {
+        query: queryParameters,
+      }),
+    {
+      // refetchOnMount defaults to false as this hook can be used on different components on the same page and
+      // we generally don't want to refetch the data in each component mount.
+      refetchOnMount: false,
+      onError: error => {
+        const errorMessage = t('Unable to fetch organization stats');
+        handleXhrErrorResponse(errorMessage)(error as ResponseMeta);
+      },
+      ...queryOptions,
+    }
+  );
+
+  return organizationStats;
+}

+ 19 - 14
static/app/views/performance/content.spec.jsx

@@ -1,4 +1,5 @@
 import {browserHistory} from 'react-router';
+import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
 
 import {enforceActOnUseLegacyStoreHook, mountWithTheme} from 'sentry-test/enzyme';
 import {initializeOrg} from 'sentry-test/initializeOrg';
@@ -17,21 +18,25 @@ import {RouteContext} from 'sentry/views/routeContext';
 const FEATURES = ['performance-view'];
 
 function WrappedComponent({organization, isMEPEnabled = false, router}) {
+  const client = new QueryClient();
+
   return (
-    <RouteContext.Provider
-      value={{
-        location,
-        params: {},
-        router,
-        routes: [],
-      }}
-    >
-      <OrganizationContext.Provider value={organization}>
-        <MEPSettingProvider _isMEPEnabled={isMEPEnabled}>
-          <PerformanceContent organization={organization} location={router.location} />
-        </MEPSettingProvider>
-      </OrganizationContext.Provider>
-    </RouteContext.Provider>
+    <QueryClientProvider client={client}>
+      <RouteContext.Provider
+        value={{
+          location,
+          params: {},
+          router,
+          routes: [],
+        }}
+      >
+        <OrganizationContext.Provider value={organization}>
+          <MEPSettingProvider _isMEPEnabled={isMEPEnabled}>
+            <PerformanceContent organization={organization} location={router.location} />
+          </MEPSettingProvider>
+        </OrganizationContext.Provider>
+      </RouteContext.Provider>
+    </QueryClientProvider>
   );
 }
 

+ 90 - 0
static/app/views/performance/landing/dynamicSamplingMetricsAccuracyAlert.spec.tsx

@@ -0,0 +1,90 @@
+import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
+
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import {
+  DynamicSamplingMetricsAccuracyAlert,
+  dynamicSamplingMetricsAccuracyMessage,
+} from './dynamicSamplingMetricsAccuracyAlert';
+
+function ComponentProviders({children}: {children: React.ReactNode}) {
+  const client = new QueryClient();
+  return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
+}
+
+describe('Dynamic Sampling Alert', function () {
+  it('does not render if requirements are not met', function () {
+    const {organization, project} = initializeOrg();
+
+    const {rerender} = render(
+      <ComponentProviders>
+        <DynamicSamplingMetricsAccuracyAlert
+          organization={organization}
+          selectedProject={project}
+        />
+      </ComponentProviders>
+    );
+
+    expect(
+      screen.queryByText(dynamicSamplingMetricsAccuracyMessage)
+    ).not.toBeInTheDocument(); // required feature flags are not enabled
+
+    rerender(
+      <ComponentProviders>
+        <DynamicSamplingMetricsAccuracyAlert
+          organization={{
+            ...organization,
+            features: [
+              'server-side-sampling',
+              'server-side-sampling-ui',
+              'dynamic-sampling-performance-cta',
+            ],
+          }}
+          selectedProject={undefined}
+        />
+      </ComponentProviders>
+    );
+
+    expect(
+      screen.queryByText(dynamicSamplingMetricsAccuracyMessage)
+    ).not.toBeInTheDocument(); // project is undefined
+  });
+
+  it('renders if requirements are  met', async function () {
+    const {organization, project} = initializeOrg();
+
+    const statsV2Mock = MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/stats_v2/`,
+      method: 'GET',
+      body: TestStubs.OutcomesWithLowProcessedEvents(),
+    });
+
+    render(
+      <ComponentProviders>
+        <DynamicSamplingMetricsAccuracyAlert
+          organization={{
+            ...organization,
+            features: [
+              'server-side-sampling',
+              'server-side-sampling-ui',
+              'dynamic-sampling-performance-cta',
+            ],
+          }}
+          selectedProject={project}
+        />
+      </ComponentProviders>
+    );
+
+    expect(
+      await screen.findByText(dynamicSamplingMetricsAccuracyMessage)
+    ).toBeInTheDocument();
+
+    expect(screen.getByRole('button', {name: 'Adjust Sample Rates'})).toHaveAttribute(
+      'href',
+      `/settings/${organization.slug}/projects/${project.slug}/dynamic-sampling/rules/uniform/?referrer=performance.rate-alert`
+    );
+
+    expect(statsV2Mock).toHaveBeenCalledTimes(1);
+  });
+});

+ 98 - 0
static/app/views/performance/landing/dynamicSamplingMetricsAccuracyAlert.tsx

@@ -0,0 +1,98 @@
+import {useEffect} from 'react';
+
+import Alert from 'sentry/components/alert';
+import Button from 'sentry/components/button';
+import {t} from 'sentry/locale';
+import {Organization, Project} from 'sentry/types';
+import {defined} from 'sentry/utils';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import {useOrganizationStats} from 'sentry/utils/useOrganizationStats';
+import {getClientSampleRates} from 'sentry/views/settings/project/server-side-sampling/utils';
+
+export const dynamicSamplingMetricsAccuracyMessage = t(
+  'The accuracy of performance metrics can be improved by adjusting your client-side sample rate.'
+);
+
+type Props = {
+  organization: Organization;
+  /**
+   * The project that the user is currently viewing.
+   * If there are more projects selected, this shall be undefined.
+   */
+  selectedProject?: Project;
+};
+
+// This alert will be shown if there is:
+// - all the required feature flags are enabled
+// - only one project selected
+// - the use viewing this has project:write permission
+// - if the diff between the current and recommended sample rates is equal or greater than 50%
+// - if we don't display other dynamic sampling alerts. According to the code this cans till be shown toegther with global sdk updates alert
+export function DynamicSamplingMetricsAccuracyAlert({
+  organization,
+  selectedProject,
+}: Props) {
+  const requiredFeatureFlagsEnabled =
+    organization.features.includes('server-side-sampling') &&
+    organization.features.includes('server-side-sampling-ui') &&
+    organization.features.includes('dynamic-sampling-performance-cta');
+
+  const organizationStats = useOrganizationStats({
+    organizationSlug: organization.slug,
+    queryParameters: {
+      project: selectedProject?.id,
+      category: 'transaction',
+      field: 'sum(quantity)',
+      interval: '1h',
+      statsPeriod: '48h',
+      groupBy: 'outcome',
+    },
+    queryOptions: {
+      enabled:
+        // Only show if all required feature flags are enabled  and a project is selected
+        requiredFeatureFlagsEnabled && !!selectedProject?.id,
+      staleTime: 1000 * 60 * 60, // a request will be considered fresh (or not stale) for 1 hour, dismissing the need for a new request
+    },
+  });
+
+  const {diff: clientSamplingDiff} = getClientSampleRates(organizationStats.data);
+
+  const recommendChangingClientSdk =
+    defined(clientSamplingDiff) && clientSamplingDiff >= 50;
+
+  const showAlert =
+    requiredFeatureFlagsEnabled && !!selectedProject && recommendChangingClientSdk;
+
+  useEffect(() => {
+    if (!showAlert) {
+      return;
+    }
+
+    trackAdvancedAnalyticsEvent('sampling.performance.metrics.accuracy.alert', {
+      organization,
+      project_id: selectedProject.id,
+    });
+  }, [showAlert, selectedProject?.id, organization]);
+
+  if (!showAlert) {
+    return null;
+  }
+
+  return (
+    <Alert
+      type="warning"
+      showIcon
+      trailingItems={
+        <Button
+          priority="link"
+          borderless
+          href={`/settings/${organization.slug}/projects/${selectedProject.slug}/dynamic-sampling/rules/uniform/?referrer=performance.rate-alert`}
+        >
+          {t('Adjust Sample Rates')}
+        </Button>
+      }
+    >
+      {dynamicSamplingMetricsAccuracyMessage}
+    </Alert>
+  );
+}

+ 97 - 19
static/app/views/performance/landing/index.spec.tsx

@@ -1,4 +1,5 @@
 import {browserHistory} from 'react-router';
+import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
 
 import {addMetricsDataMock} from 'sentry-test/performance/addMetricsDataMock';
 import {initializeData} from 'sentry-test/performance/initializePerformanceData';
@@ -12,32 +13,38 @@ 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 {dynamicSamplingMetricsAccuracyMessage} from './dynamicSamplingMetricsAccuracyAlert';
+
 const WrappedComponent = ({data, withStaticFilters = false}) => {
   const eventView = generatePerformanceEventView(data.router.location, data.projects, {
     withStaticFilters,
   });
 
+  const client = new QueryClient();
+
   return (
-    <OrganizationContext.Provider value={data.organization}>
-      <MetricsCardinalityProvider
-        location={data.router.location}
-        organization={data.organization}
-      >
-        <PerformanceLanding
-          router={data.router}
-          organization={data.organization}
+    <QueryClientProvider client={client}>
+      <OrganizationContext.Provider value={data.organization}>
+        <MetricsCardinalityProvider
           location={data.router.location}
-          eventView={eventView}
-          projects={data.projects}
-          selection={eventView.getPageFilters()}
-          onboardingProject={undefined}
-          handleSearch={() => {}}
-          handleTrendsClick={() => {}}
-          setError={() => {}}
-          withStaticFilters={withStaticFilters}
-        />
-      </MetricsCardinalityProvider>
-    </OrganizationContext.Provider>
+          organization={data.organization}
+        >
+          <PerformanceLanding
+            router={data.router}
+            organization={data.organization}
+            location={data.router.location}
+            eventView={eventView}
+            projects={data.projects}
+            selection={eventView.getPageFilters()}
+            onboardingProject={undefined}
+            handleSearch={() => {}}
+            handleTrendsClick={() => {}}
+            setError={() => {}}
+            withStaticFilters={withStaticFilters}
+          />
+        </MetricsCardinalityProvider>
+      </OrganizationContext.Provider>
+    </QueryClientProvider>
   );
 };
 
@@ -289,6 +296,77 @@ describe('Performance > Landing > Index', function () {
     expect(screen.getByTestId('frontend-pageload-view')).toBeInTheDocument();
   });
 
+  it('renders DynamicSamplingMetricsAccuracyAlert', async function () {
+    const project = TestStubs.Project({id: 99, platform: 'javascript-react'});
+
+    const data = initializeData({
+      projects: [project],
+      selectedProject: 99,
+      features: [
+        'server-side-sampling',
+        'server-side-sampling-ui',
+        'dynamic-sampling-performance-cta',
+      ],
+    });
+
+    MockApiClient.addMockResponse({
+      url: `/organizations/${data.organization.slug}/stats_v2/`,
+      method: 'GET',
+      body: TestStubs.OutcomesWithLowProcessedEvents(),
+    });
+
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/projects/',
+      body: [project],
+    });
+
+    wrapper = render(<WrappedComponent data={data} />, data.routerContext);
+
+    expect(
+      await screen.findByText(dynamicSamplingMetricsAccuracyMessage)
+    ).toBeInTheDocument();
+  });
+
+  it('does not render DynamicSamplingMetricsAccuracyAlert if there are other Dynamic Sampling alerts being rendered', async function () {
+    const project = TestStubs.Project({id: 99, platform: 'javascript-react'});
+
+    const data = initializeData({
+      projects: [project],
+      selectedProject: 99,
+      features: [
+        'server-side-sampling',
+        'server-side-sampling-ui',
+        'dynamic-sampling-performance-cta',
+        'performance-transaction-name-only-search',
+      ],
+    });
+
+    addMetricsDataMock();
+
+    MockApiClient.addMockResponse({
+      method: 'GET',
+      url: '/organizations/org-slug/metrics/data/',
+      body: TestStubs.MetricsField(),
+    });
+
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/projects/',
+      body: [project],
+    });
+
+    MockApiClient.addMockResponse({
+      url: `/organizations/${data.organization.slug}/stats_v2/`,
+      method: 'GET',
+      body: TestStubs.OutcomesWithLowProcessedEvents(),
+    });
+
+    wrapper = render(<WrappedComponent data={data} />, data.routerContext);
+
+    expect(
+      await screen.findByText(dynamicSamplingMetricsAccuracyMessage)
+    ).toBeInTheDocument();
+  });
+
   describe('with transaction search feature', function () {
     it('renders the search bar', async function () {
       addMetricsDataMock();

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