Browse Source

feat(sampling): Add recommended steps modal (#36332)

Priscila Oliveira 2 years ago
parent
commit
9a4e57dacf

+ 10 - 8
static/app/components/sidebar/broadcastSdkUpdates.tsx

@@ -75,7 +75,7 @@ function BroadcastSdkUpdates({projects, sdkUpdates, organization}: Props) {
           return (
             <div key={sdkName}>
               <Header>
-                <SdkProjectBadge project={project} />
+                <SdkProjectBadge project={project} organization={organization} />
                 {isDeprecated && <Tag type="warning">{t('Deprecated')}</Tag>}
               </Header>
               <SdkOutdatedVersion>
@@ -85,7 +85,7 @@ function BroadcastSdkUpdates({projects, sdkUpdates, organization}: Props) {
                   ),
                 })}
               </SdkOutdatedVersion>
-              <StyledList>
+              <UpdateSuggestions>
                 {suggestions.map((suggestion, i) => (
                   <ListItem key={i}>
                     {getSdkUpdateSuggestion({
@@ -99,7 +99,7 @@ function BroadcastSdkUpdates({projects, sdkUpdates, organization}: Props) {
                     })}
                   </ListItem>
                 ))}
-              </StyledList>
+              </UpdateSuggestions>
             </div>
           );
         });
@@ -148,7 +148,7 @@ function BroadcastSdkUpdates({projects, sdkUpdates, organization}: Props) {
 
 export default withSdkUpdates(withProjects(withOrganization(BroadcastSdkUpdates)));
 
-const UpdatesList = styled('div')`
+export const UpdatesList = styled('div')`
   margin-top: ${space(3)};
   display: grid;
   grid-auto-flow: row;
@@ -163,16 +163,16 @@ const Header = styled('div')`
   align-items: center;
 `;
 
-const SdkOutdatedVersion = styled('div')`
+export const SdkOutdatedVersion = styled('div')`
   /* 24px + 8px to be aligned with the SdkProjectBadge data */
   padding-left: calc(24px + ${space(1)});
 `;
 
-const OutdatedVersion = styled('span')`
+export const OutdatedVersion = styled('span')`
   color: ${p => p.theme.gray400};
 `;
 
-const SdkProjectBadge = styled(ProjectBadge)`
+export const SdkProjectBadge = styled(ProjectBadge)`
   font-size: ${p => p.theme.fontSizeExtraLarge};
   line-height: 1;
 `;
@@ -181,8 +181,10 @@ const StyledAlert = styled(Alert)`
   margin-top: ${space(2)};
 `;
 
-const StyledList = styled(List)`
+export const UpdateSuggestions = styled(List)`
   /* 24px + 8px to be aligned with the project name
   * displayed by the SdkProjectBadge component */
   padding-left: calc(24px + ${space(1)});
 `;
+
+export const UpdateSuggestion = styled(ListItem)``;

+ 28 - 0
static/app/types/sampling.tsx

@@ -178,3 +178,31 @@ export type SamplingRule = {
 };
 
 export type SamplingRules = Array<SamplingRule>;
+
+export type SamplingDistribution = {
+  null_sample_rate_percentage: null | number;
+  project_breakdown:
+    | null
+    | {
+        'count()': number;
+        project: string;
+        project_id: number;
+      }[];
+  sample_rate_distributions: null | {
+    avg: null | number;
+    max: null | number;
+    min: null | number;
+    p50: null | number;
+    p90: null | number;
+    p95: null | number;
+    p99: null | number;
+  };
+  sample_size: number;
+};
+
+export type SamplingSdkVersion = {
+  isSendingSampleRate: boolean;
+  latestSDKName: string;
+  latestSDKVersion: string;
+  project: string;
+};

+ 1 - 1
static/app/views/settings/organizationRepositories/organizationRepositories.tsx

@@ -75,7 +75,7 @@ function OrganizationRepositories({itemList, onRepositoryChange, params}: Props)
               'Adding one or more repositories will enable enhanced releases and the ability to resolve Sentry Issues via git message.'
             )}
             action={
-              <Button href="https://docs.sentry.io/learn/releases/">
+              <Button external href="https://docs.sentry.io/learn/releases/">
                 {t('Learn more')}
               </Button>
             }

+ 154 - 0
static/app/views/settings/project/server-side-sampling/modals/recommendedStepsModal.tsx

@@ -0,0 +1,154 @@
+import 'prism-sentry/index.css';
+
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import {ModalRenderProps} from 'sentry/actionCreators/modal';
+import Button from 'sentry/components/button';
+import ButtonBar from 'sentry/components/buttonBar';
+import ExternalLink from 'sentry/components/links/externalLink';
+import List from 'sentry/components/list';
+import ListItem from 'sentry/components/list/listItem';
+import {
+  OutdatedVersion,
+  SdkOutdatedVersion,
+  SdkProjectBadge,
+  UpdatesList,
+} from 'sentry/components/sidebar/broadcastSdkUpdates';
+import {t, tct} from 'sentry/locale';
+import space from 'sentry/styles/space';
+import {Organization, Project} from 'sentry/types';
+import {SamplingSdkVersion} from 'sentry/types/sampling';
+import TextBlock from 'sentry/views/settings/components/text/textBlock';
+
+import {SERVER_SIDE_SAMPLING_DOC_LINK} from '../utils';
+
+import {FooterActions, Stepper} from './uniformRateModal';
+
+type RecommendedSdkUpgrade = {
+  latestSDKName: SamplingSdkVersion['latestSDKName'];
+  latestSDKVersion: SamplingSdkVersion['latestSDKVersion'];
+  project: Project;
+};
+
+export type RecommendedStepsModalProps = ModalRenderProps & {
+  organization: Organization;
+  recommendedSdkUpgrades: RecommendedSdkUpgrade[];
+  onGoBack?: () => void;
+  onSubmit?: () => void;
+  project?: Project;
+};
+
+export function RecommendedStepsModal({
+  Header,
+  Body,
+  Footer,
+  closeModal,
+  organization,
+  recommendedSdkUpgrades,
+  onGoBack,
+  onSubmit,
+}: RecommendedStepsModalProps) {
+  return (
+    <Fragment>
+      <Header closeButton>
+        <h4>{t('Recommended next steps\u2026')}</h4>
+      </Header>
+      <Body>
+        <List symbol="colored-numeric">
+          {!!recommendedSdkUpgrades.length && (
+            <ListItem>
+              <h5>{t('Update the following SDK versions')}</h5>
+              <TextBlock>
+                {tct(
+                  "I know what you're thinking, [italic:“[strong:It's already working, why should I?]”]. By updating the following SDK's before activating any server sampling rules, you're avoiding situations when our servers aren't accepting enough transactions ([doubleSamplingLink:double sampling]) or our servers are accepting too many transactions ([exceededQuotaLink:exceeded quota]).",
+                  {
+                    strong: <strong />,
+                    italic: <i />,
+                    doubleSamplingLink: <ExternalLink href="" />,
+                    exceededQuotaLink: <ExternalLink href="" />,
+                  }
+                )}
+              </TextBlock>
+              <UpgradeSDKfromProjects>
+                {recommendedSdkUpgrades.map(
+                  ({project, latestSDKName, latestSDKVersion}) => {
+                    return (
+                      <div key={project.id}>
+                        <SdkProjectBadge project={project} organization={organization} />
+                        <SdkOutdatedVersion>
+                          {tct('This project is on [current-version]', {
+                            ['current-version']: (
+                              <OutdatedVersion>{`${latestSDKName}@v${latestSDKVersion}`}</OutdatedVersion>
+                            ),
+                          })}
+                        </SdkOutdatedVersion>
+                      </div>
+                    );
+                  }
+                )}
+              </UpgradeSDKfromProjects>
+            </ListItem>
+          )}
+          <ListItem>
+            <h5>{t('Increase your SDK Transaction sample rate')}</h5>
+            <TextBlock>
+              {t(
+                'This comes in handy when server-side sampling target the transactions you want to accept, but you need more of those transactions being sent by your client. Here we  already suggest a value based on your quota and throughput.'
+              )}
+            </TextBlock>
+            <div>
+              <pre className="language-javascript highlight">
+                <code className="language-javascript">
+                  Sentry
+                  <span className="token punctuation">.</span>
+                  <span className="token function">init</span>
+                  <span className="token punctuation">(</span>
+                  <span className="token punctuation">{'{'}</span>
+                  <br />
+                  <span className="token punctuation">{'  ...'}</span>
+                  <br />
+                  <span className="token literal-property property">
+                    {'  traceSampleRate'}
+                  </span>
+                  <span className="token operator">:</span>{' '}
+                  <span className="token string">1.0</span>
+                  <span className="token punctuation">,</span>{' '}
+                  <span className="token comment">// 100%</span>
+                  <br />
+                  <span className="token punctuation">{'}'}</span>
+                  <span className="token punctuation">)</span>
+                  <span className="token punctuation">;</span>
+                </code>
+              </pre>
+            </div>
+          </ListItem>
+        </List>
+      </Body>
+      <Footer>
+        <FooterActions>
+          <Button href={SERVER_SIDE_SAMPLING_DOC_LINK} external>
+            {t('Read Docs')}
+          </Button>
+          <ButtonBar gap={1}>
+            {onGoBack && (
+              <Fragment>
+                <Stepper>{t('Step 2 of 2')}</Stepper>
+                <Button onClick={onGoBack}>{t('Back')}</Button>
+              </Fragment>
+            )}
+            {!onGoBack && <Button onClick={closeModal}>{t('Cancel')}</Button>}
+            <Button priority="primary" onClick={onSubmit}>
+              {t('Done')}
+            </Button>
+          </ButtonBar>
+        </FooterActions>
+      </Footer>
+    </Fragment>
+  );
+}
+
+const UpgradeSDKfromProjects = styled(UpdatesList)`
+  margin-top: 0;
+  margin-bottom: ${space(3)};
+`;

+ 39 - 11
static/app/views/settings/project/server-side-sampling/modals/uniformRateModal.tsx

@@ -1,7 +1,6 @@
 import {Fragment, useEffect, useState} from 'react';
 import styled from '@emotion/styled';
 
-import {ModalRenderProps} from 'sentry/actionCreators/modal';
 import Alert from 'sentry/components/alert';
 import Button from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
@@ -12,7 +11,7 @@ import Radio from 'sentry/components/radio';
 import {IconRefresh} from 'sentry/icons';
 import {t, tct} from 'sentry/locale';
 import space from 'sentry/styles/space';
-import {Organization, Project, SeriesApi} from 'sentry/types';
+import {Project, SeriesApi} from 'sentry/types';
 import {SamplingRules} from 'sentry/types/sampling';
 import {defined} from 'sentry/utils';
 import {formatPercentage} from 'sentry/utils/formatters';
@@ -24,6 +23,7 @@ import {projectStatsToSampleRates} from '../utils/projectStatsToSampleRates';
 import {projectStatsToSeries} from '../utils/projectStatsToSeries';
 import useProjectStats from '../utils/useProjectStats';
 
+import {RecommendedStepsModal, RecommendedStepsModalProps} from './recommendedStepsModal';
 import {UniformRateChart} from './uniformRateChart';
 
 enum Strategy {
@@ -31,22 +31,28 @@ enum Strategy {
   RECOMMENDED = 'recommended',
 }
 
-type Props = ModalRenderProps & {
-  organization: Organization;
+enum Step {
+  SET_UNIFORM_SAMPLE_RATE = 'set_uniform_sample_rate',
+  RECOMMENDED_STEPS = 'recommended_steps',
+}
+
+type Props = RecommendedStepsModalProps & {
   rules: SamplingRules;
   project?: Project;
   projectStats?: SeriesApi;
 };
 
 function UniformRateModal({
-  organization,
-  project,
-  projectStats,
-  rules,
   Header,
   Body,
   Footer,
   closeModal,
+  organization,
+  recommendedSdkUpgrades,
+  projectStats,
+  project,
+  rules,
+  ...props
 }: Props) {
   const {projectStats: projectStats30d, loading: loading30d} = useProjectStats({
     orgSlug: organization.slug,
@@ -59,6 +65,7 @@ function UniformRateModal({
 
   // TODO(sampling): fetch from API
   const affectedProjects = ['ProjectA', 'ProjectB', 'ProjectC'];
+  const [activeStep, setActiveStep] = useState<Step>(Step.SET_UNIFORM_SAMPLE_RATE);
 
   const uniformSampleRate = rules.find(isUniformRule)?.sampleRate;
 
@@ -88,6 +95,22 @@ function UniformRateModal({
   const isEdited =
     client !== recommendedClientSampling || server !== recommendedServerSampling;
 
+  if (activeStep === Step.RECOMMENDED_STEPS) {
+    return (
+      <RecommendedStepsModal
+        {...props}
+        Header={Header}
+        Body={Body}
+        Footer={Footer}
+        closeModal={closeModal}
+        organization={organization}
+        recommendedSdkUpgrades={recommendedSdkUpgrades}
+        onGoBack={() => setActiveStep(Step.SET_UNIFORM_SAMPLE_RATE)}
+        onSubmit={() => {}}
+      />
+    );
+  }
+
   return (
     <Fragment>
       <Header closeButton>
@@ -231,7 +254,12 @@ function UniformRateModal({
           <ButtonBar gap={1}>
             <Stepper>{t('Step 1 of 2')}</Stepper>
             <Button onClick={closeModal}>{t('Cancel')}</Button>
-            <Button priority="primary">{t('Next')}</Button>
+            <Button
+              priority="primary"
+              onClick={() => setActiveStep(Step.RECOMMENDED_STEPS)}
+            >
+              {t('Next')}
+            </Button>
           </ButtonBar>
         </FooterActions>
       </Footer>
@@ -266,7 +294,7 @@ const StyledNumberField = styled(NumberField)`
   width: 100%;
 `;
 
-const FooterActions = styled('div')`
+export const FooterActions = styled('div')`
   display: flex;
   justify-content: space-between;
   align-items: center;
@@ -274,7 +302,7 @@ const FooterActions = styled('div')`
   gap: ${space(1)};
 `;
 
-const Stepper = styled('span')`
+export const Stepper = styled('span')`
   font-size: ${p => p.theme.fontSizeMedium};
   color: ${p => p.theme.subText};
 `;

+ 4 - 0
static/app/views/settings/project/server-side-sampling/promo.tsx

@@ -87,4 +87,8 @@ const Description = styled('div')`
     padding: ${space(4)};
     justify-content: flex-start;
   }
+
+  p {
+    font-size: ${p => p.theme.fontSizeLarge};
+  }
 `;

+ 108 - 8
static/app/views/settings/project/server-side-sampling/serverSideSampling.tsx

@@ -5,8 +5,10 @@ import isEqual from 'lodash/isEqual';
 
 import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
 import {openModal} from 'sentry/actionCreators/modal';
+import Alert from 'sentry/components/alert';
 import Button from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
+import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import {Panel, PanelFooter, PanelHeader} from 'sentry/components/panels';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {IconAdd} from 'sentry/icons';
@@ -15,10 +17,12 @@ import ProjectStore from 'sentry/stores/projectsStore';
 import space from 'sentry/styles/space';
 import {Project} from 'sentry/types';
 import {SamplingRule, SamplingRuleOperator, SamplingRules} from 'sentry/types/sampling';
+import {defined} from 'sentry/utils';
 import handleXhrErrorResponse from 'sentry/utils/handleXhrErrorResponse';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePrevious from 'sentry/utils/usePrevious';
+import useProjects from 'sentry/utils/useProjects';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
 import PermissionAlert from 'sentry/views/settings/organization/permissionAlert';
@@ -26,10 +30,13 @@ import PermissionAlert from 'sentry/views/settings/organization/permissionAlert'
 import {DraggableList, UpdateItemsProps} from '../sampling/rules/draggableList';
 
 import {ActivateModal} from './modals/activateModal';
+import {RecommendedStepsModal} from './modals/recommendedStepsModal';
 import {SpecificConditionsModal} from './modals/specificConditionsModal';
 import {responsiveModal} from './modals/styles';
 import {UniformRateModal} from './modals/uniformRateModal';
 import useProjectStats from './utils/useProjectStats';
+import useSamplingDistribution from './utils/useSamplingDistribution';
+import useSdkVersions from './utils/useSdkVersions';
 import {Promo} from './promo';
 import {
   ActiveColumn,
@@ -54,7 +61,9 @@ export function ServerSideSampling({project}: Props) {
 
   const currentRules = project.dynamicSampling?.rules;
   const previousRules = usePrevious(currentRules);
+
   const [rules, setRules] = useState<SamplingRules>(currentRules ?? []);
+
   const {projectStats} = useProjectStats({
     orgSlug: organization.slug,
     projectId: project?.id,
@@ -62,6 +71,56 @@ export function ServerSideSampling({project}: Props) {
     statsPeriod: '48h',
   });
 
+  const {samplingDistribution} = useSamplingDistribution({
+    orgSlug: organization.slug,
+    projSlug: project.slug,
+  });
+
+  const {samplingSdkVersions} = useSdkVersions({
+    orgSlug: organization.slug,
+    projSlug: project.slug,
+    projectIds: samplingDistribution?.project_breakdown?.map(
+      projectBreakdown => projectBreakdown.project_id
+    ),
+  });
+
+  const notSendingSampleRateSdkUpgrades =
+    samplingSdkVersions?.filter(
+      samplingSdkVersion => !samplingSdkVersion.isSendingSampleRate
+    ) ?? [];
+
+  const {projects} = useProjects({
+    slugs: notSendingSampleRateSdkUpgrades.map(sdkUpgrade => sdkUpgrade.project),
+    orgId: organization.slug,
+  });
+
+  // Rules without a condition (Else case) always have to be 'pinned' to the bottom of the list
+  // and cannot be sorted
+  const items = rules.map(rule => ({
+    ...rule,
+    id: String(rule.id),
+    bottomPinned: !rule.condition.inner.length,
+  }));
+
+  const recommendedSdkUpgrades = projects
+    .map(upgradeSDKfromProject => {
+      const sdkInfo = notSendingSampleRateSdkUpgrades.find(
+        notSendingSampleRateSdkUpgrade =>
+          notSendingSampleRateSdkUpgrade.project === upgradeSDKfromProject.slug
+      );
+
+      if (!sdkInfo) {
+        return undefined;
+      }
+
+      return {
+        project: upgradeSDKfromProject,
+        latestSDKName: sdkInfo.latestSDKName,
+        latestSDKVersion: sdkInfo.latestSDKVersion,
+      };
+    })
+    .filter(defined);
+
   useEffect(() => {
     if (!isEqual(previousRules, currentRules)) {
       setRules(currentRules ?? []);
@@ -89,6 +148,7 @@ export function ServerSideSampling({project}: Props) {
           project={project}
           projectStats={projectStats}
           rules={rules}
+          recommendedSdkUpgrades={recommendedSdkUpgrades}
         />
       ),
       {
@@ -97,6 +157,21 @@ export function ServerSideSampling({project}: Props) {
     );
   }
 
+  function handleOpenRecommendedSteps() {
+    if (!recommendedSdkUpgrades.length) {
+      return;
+    }
+
+    openModal(modalProps => (
+      <RecommendedStepsModal
+        {...modalProps}
+        organization={organization}
+        project={project}
+        recommendedSdkUpgrades={recommendedSdkUpgrades}
+      />
+    ));
+  }
+
   async function handleSortRules({overIndex, reorderedItems: ruleIds}: UpdateItemsProps) {
     if (!rules[overIndex].condition.inner.length) {
       addErrorMessage(
@@ -170,14 +245,6 @@ export function ServerSideSampling({project}: Props) {
     }
   }
 
-  // Rules without a condition (Else case) always have to be 'pinned' to the bottom of the list
-  // and cannot be sorted
-  const items = rules.map(rule => ({
-    ...rule,
-    id: String(rule.id),
-    bottomPinned: !rule.condition.inner.length,
-  }));
-
   return (
     <SentryDocumentTitle title={t('Server-side Sampling')}>
       <Fragment>
@@ -193,6 +260,31 @@ export function ServerSideSampling({project}: Props) {
             'These settings can only be edited by users with the organization owner, manager, or admin role.'
           )}
         />
+        {!!recommendedSdkUpgrades.length && !!rules.length && (
+          <Alert
+            data-test-id="recommended-sdk-upgrades-alert"
+            type="info"
+            showIcon
+            trailingItems={
+              <Button onClick={handleOpenRecommendedSteps} priority="link" borderless>
+                {t('Learn More')}
+              </Button>
+            }
+          >
+            {t(
+              'To keep a consistent amount of transactions across your applications multiple services, we recommend you update the SDK versions for the following projects:'
+            )}
+            <Projects>
+              {recommendedSdkUpgrades.map(recommendedSdkUpgrade => (
+                <ProjectBadge
+                  key={recommendedSdkUpgrade.project.id}
+                  project={recommendedSdkUpgrade.project}
+                  avatarSize={16}
+                />
+              ))}
+            </Projects>
+          </Alert>
+        )}
         <RulesPanel>
           <RulesPanelHeader lightText>
             <RulesPanelLayout>
@@ -347,3 +439,11 @@ const AddRuleButton = styled(Button)`
     width: 100%;
   }
 `;
+
+const Projects = styled('div')`
+  display: flex;
+  flex-wrap: wrap;
+  gap: ${space(1.5)};
+  justify-content: flex-start;
+  margin-top: ${space(1)};
+`;

+ 40 - 0
static/app/views/settings/project/server-side-sampling/utils/useSamplingDistribution.tsx

@@ -0,0 +1,40 @@
+import {useEffect, useState} from 'react';
+
+import {t} from 'sentry/locale';
+import {Organization, Project} from 'sentry/types';
+import {SamplingDistribution} from 'sentry/types/sampling';
+import handleXhrErrorResponse from 'sentry/utils/handleXhrErrorResponse';
+import useApi from 'sentry/utils/useApi';
+
+type Props = {
+  orgSlug: Organization['slug'];
+  projSlug: Project['slug'];
+};
+
+function useSamplingDistribution({orgSlug, projSlug}: Props) {
+  const api = useApi();
+  const [samplingDistribution, setSamplingDistribution] = useState<
+    SamplingDistribution | undefined
+  >(undefined);
+
+  useEffect(() => {
+    async function fetchSamplingDistribution() {
+      try {
+        const response = await api.requestPromise(
+          `/projects/${orgSlug}/${projSlug}/dynamic-sampling/distribution/`
+        );
+        setSamplingDistribution(response);
+      } catch (error) {
+        const errorMessage = t('Unable to fetch sampling distribution');
+        handleXhrErrorResponse(errorMessage)(error);
+      }
+    }
+    fetchSamplingDistribution();
+  }, [api, projSlug, orgSlug]);
+
+  return {
+    samplingDistribution,
+  };
+}
+
+export default useSamplingDistribution;

+ 51 - 0
static/app/views/settings/project/server-side-sampling/utils/useSdkVersions.tsx

@@ -0,0 +1,51 @@
+import {useEffect, useState} from 'react';
+
+import {t} from 'sentry/locale';
+import {Organization, Project} from 'sentry/types';
+import {SamplingSdkVersion} from 'sentry/types/sampling';
+import handleXhrErrorResponse from 'sentry/utils/handleXhrErrorResponse';
+import useApi from 'sentry/utils/useApi';
+
+type Props = {
+  orgSlug: Organization['slug'];
+  projSlug: Project['slug'];
+  projectIds?: number[];
+};
+
+function useSdkVersions({orgSlug, projSlug, projectIds = []}: Props) {
+  const api = useApi();
+  const [samplingSdkVersions, setSamplingSdkVersions] = useState<
+    SamplingSdkVersion[] | undefined
+  >(undefined);
+
+  useEffect(() => {
+    async function fetchSamplingSdkVersions() {
+      if (!projectIds.length) {
+        return;
+      }
+
+      try {
+        const response = await api.requestPromise(
+          `/organizations/${orgSlug}/dynamic-sampling/sdk-versions/`,
+          {
+            method: 'GET',
+            query: {
+              projects: projectIds,
+            },
+          }
+        );
+        setSamplingSdkVersions(response);
+      } catch (error) {
+        const message = t('Unable to fetch sampling sdk versions');
+        handleXhrErrorResponse(message)(error);
+      }
+    }
+    fetchSamplingSdkVersions();
+  }, [api, projSlug, orgSlug, projectIds]);
+
+  return {
+    samplingSdkVersions,
+  };
+}
+
+export default useSdkVersions;

+ 24 - 5
tests/js/spec/views/settings/project/server-side-sampling/activateModal.spec.tsx

@@ -14,9 +14,26 @@ import {RouteContext} from 'sentry/views/routeContext';
 import ServerSideSampling from 'sentry/views/settings/project/server-side-sampling';
 import {SERVER_SIDE_SAMPLING_DOC_LINK} from 'sentry/views/settings/project/server-side-sampling/utils';
 import importedUseProjectStats from 'sentry/views/settings/project/server-side-sampling/utils/useProjectStats';
+import importedUseSamplingDistribution from 'sentry/views/settings/project/server-side-sampling/utils/useSamplingDistribution';
 
 import {getMockData} from './index.spec';
 
+jest.mock(
+  'sentry/views/settings/project/server-side-sampling/utils/useSamplingDistribution'
+);
+const useSamplingDistribution = importedUseSamplingDistribution as jest.MockedFunction<
+  typeof importedUseSamplingDistribution
+>;
+
+useSamplingDistribution.mockImplementation(() => ({
+  samplingDistribution: {
+    project_breakdown: null,
+    sample_size: 0,
+    null_sample_rate_percentage: null,
+    sample_rate_distributions: null,
+  },
+}));
+
 jest.mock('sentry/views/settings/project/server-side-sampling/utils/useProjectStats');
 const useProjectStats = importedUseProjectStats as jest.MockedFunction<
   typeof importedUseProjectStats
@@ -48,11 +65,13 @@ describe('Server-side Sampling - Activate Modal', function () {
     };
 
     const {router, project, organization} = getMockData({
-      project: TestStubs.Project({
-        dynamicSampling: {
-          rules: [uniformRule],
-        },
-      }),
+      projects: [
+        TestStubs.Project({
+          dynamicSampling: {
+            rules: [uniformRule],
+          },
+        }),
+      ],
     });
 
     const saveMock = MockApiClient.addMockResponse({

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