Browse Source

feat(sampling): Add parent projects alert [TET-338] (#39598)

Co-authored-by: Ahmed Etefy <ahmed.etefy@sentry.io>
Matej Minar 2 years ago
parent
commit
5729c96876

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

@@ -102,6 +102,13 @@ export type SamplingRule = {
 
 export type SamplingDistribution = {
   endTimestamp: string | null;
+  parentProjectBreakdown:
+    | null
+    | {
+        percentage: number;
+        project: string;
+        projectId: number;
+      }[];
   projectBreakdown:
     | null
     | {

+ 1 - 5
static/app/views/settings/project/server-side-sampling/samplingBreakdown.spec.tsx

@@ -18,11 +18,7 @@ describe('Server-Side Sampling - SamplingBreakdown', function () {
 
     render(<SamplingBreakdown orgSlug={organization.slug} />);
 
-    expect(
-      screen.getByText(
-        'There were no traces initiated from this project in the last 30 days.'
-      )
-    ).toBeInTheDocument();
+    expect(screen.getByText(/This project made no/)).toBeInTheDocument();
   });
 
   it('renders project breakdown', function () {

+ 23 - 3
static/app/views/settings/project/server-side-sampling/samplingBreakdown.tsx

@@ -63,7 +63,7 @@ export function SamplingBreakdown({orgSlug}: Props) {
           <HeaderTitle>{t('Transaction Breakdown')}</HeaderTitle>
           <QuestionTooltip
             title={tct(
-              'Sampling rules defined here can also affect other projects. [learnMore: Learn more]',
+              'Shows which projects are affected by the sampling decisions this project makes. [learnMore: Learn more]',
               {
                 learnMore: (
                   <ExternalLink
@@ -110,8 +110,28 @@ export function SamplingBreakdown({orgSlug}: Props) {
               </Projects>
             ) : (
               <EmptyMessage>
-                {t(
-                  'There were no traces initiated from this project in the last 30 days.'
+                {tct(
+                  'This project made no [samplingDecisions] within the last 30 days.',
+                  {
+                    samplingDecisions: (
+                      <Tooltip
+                        title={tct(
+                          'The first transaction in a trace makes the sampling decision for all following transactions. [learnMore: Learn more]',
+                          {
+                            learnMore: (
+                              <ExternalLink
+                                href={`${SERVER_SIDE_SAMPLING_DOC_LINK}#traces--propagation-of-sampling-decisions`}
+                              />
+                            ),
+                          }
+                        )}
+                        showUnderline
+                        isHoverable
+                      >
+                        {t('sampling decisions')}
+                      </Tooltip>
+                    ),
+                  }
                 )}
               </EmptyMessage>
             )}

+ 63 - 0
static/app/views/settings/project/server-side-sampling/samplingFromOtherProject.spec.tsx

@@ -0,0 +1,63 @@
+import {act, render, screen} from 'sentry-test/reactTestingLibrary';
+
+import ProjectsStore from 'sentry/stores/projectsStore';
+import {ServerSideSamplingStore} from 'sentry/stores/serverSideSamplingStore';
+
+import {SamplingFromOtherProject} from './samplingFromOtherProject';
+import {getMockData, mockedSamplingDistribution} from './testUtils';
+
+export const samplingBreakdownTitle = 'Transaction Breakdown';
+
+describe('Server-Side Sampling - SamplingFromOtherProject', function () {
+  afterEach(function () {
+    act(() => ProjectsStore.reset());
+    act(() => ServerSideSamplingStore.reset());
+  });
+
+  it('renders the parent projects', function () {
+    const {organization} = getMockData();
+    const parentProjectBreakdown = mockedSamplingDistribution.parentProjectBreakdown;
+
+    ProjectsStore.loadInitialData(
+      parentProjectBreakdown!.map(p =>
+        TestStubs.Project({id: p.projectId, slug: p.project})
+      )
+    );
+
+    ServerSideSamplingStore.distributionRequestSuccess(mockedSamplingDistribution);
+
+    render(<SamplingFromOtherProject orgSlug={organization.slug} projectSlug="abc" />);
+
+    expect(screen.getByText('parent-project')).toBeInTheDocument();
+    expect(
+      screen.getByText(
+        'The following project made sampling decisions for this project. You might want to set up rules there.'
+      )
+    ).toBeInTheDocument();
+  });
+
+  it('does not render if there are no parent projects', function () {
+    const {organization} = getMockData();
+    const parentProjectBreakdown = mockedSamplingDistribution.parentProjectBreakdown;
+
+    ProjectsStore.loadInitialData(
+      parentProjectBreakdown!.map(p =>
+        TestStubs.Project({id: p.projectId, slug: p.project})
+      )
+    );
+
+    ServerSideSamplingStore.distributionRequestSuccess({
+      ...mockedSamplingDistribution,
+      parentProjectBreakdown: [],
+    });
+
+    render(<SamplingFromOtherProject orgSlug={organization.slug} projectSlug="abc" />);
+
+    expect(screen.queryByText('parent-project')).not.toBeInTheDocument();
+    expect(
+      screen.queryByText(
+        'The following project made sampling decisions for this project. You might want to set up rules there.'
+      )
+    ).not.toBeInTheDocument();
+  });
+});

+ 74 - 0
static/app/views/settings/project/server-side-sampling/samplingFromOtherProject.tsx

@@ -0,0 +1,74 @@
+import styled from '@emotion/styled';
+
+import Alert from 'sentry/components/alert';
+import Button from 'sentry/components/button';
+import ProjectBadge from 'sentry/components/idBadge/projectBadge';
+import {t, tn} from 'sentry/locale';
+import space from 'sentry/styles/space';
+import {Organization, Project} from 'sentry/types';
+import useProjects from 'sentry/utils/useProjects';
+
+import {useDistribution} from './utils/useDistribution';
+import {SERVER_SIDE_SAMPLING_DOC_LINK} from './utils';
+
+type Props = {
+  orgSlug: Organization['slug'];
+  projectSlug: Project['slug'];
+};
+
+export function SamplingFromOtherProject({orgSlug, projectSlug}: Props) {
+  const {distribution, loading} = useDistribution();
+
+  const {projects} = useProjects({
+    slugs: distribution?.parentProjectBreakdown?.map(({project}) => project) ?? [],
+    orgId: orgSlug,
+  });
+
+  const otherProjects = projects.filter(project => project.slug !== projectSlug);
+
+  if (loading || otherProjects.length === 0) {
+    return null;
+  }
+
+  return (
+    <Alert
+      type="info"
+      showIcon
+      trailingItems={
+        <Button
+          href={`${SERVER_SIDE_SAMPLING_DOC_LINK}#traces--propagation-of-sampling-decisions`}
+          priority="link"
+          borderless
+          external
+        >
+          {t('Learn More')}
+        </Button>
+      }
+    >
+      {tn(
+        'The following project made sampling decisions for this project. You might want to set up rules there.',
+        'The following projects made sampling decisions for this project. You might want to set up rules there.',
+        otherProjects.length
+      )}
+      <Projects>
+        {otherProjects.map(project => (
+          <ProjectBadge
+            key={project.slug}
+            project={project}
+            avatarSize={16}
+            to={`/settings/${orgSlug}/projects/${project.slug}/dynamic-sampling/`}
+          />
+        ))}
+      </Projects>
+    </Alert>
+  );
+}
+
+const Projects = styled('div')`
+  display: flex;
+  flex-wrap: wrap;
+  gap: ${space(1.5)};
+  justify-content: flex-start;
+  align-items: center;
+  margin-top: ${space(1)};
+`;

+ 6 - 0
static/app/views/settings/project/server-side-sampling/serverSideSampling.tsx

@@ -63,6 +63,7 @@ import {
 } from './rule';
 import {SamplingBreakdown} from './samplingBreakdown';
 import {SamplingFeedback} from './samplingFeedback';
+import {SamplingFromOtherProject} from './samplingFromOtherProject';
 import {SamplingProjectIncompatibleAlert} from './samplingProjectIncompatibleAlert';
 import {SamplingPromo} from './samplingPromo';
 import {SamplingSDKClientRateChangeAlert} from './samplingSDKClientRateChangeAlert';
@@ -541,6 +542,11 @@ export function ServerSideSampling({project}: Props) {
           />
         )}
 
+        <SamplingFromOtherProject
+          orgSlug={organization.slug}
+          projectSlug={project.slug}
+        />
+
         {hasAccess && <SamplingBreakdown orgSlug={organization.slug} />}
         {!rules.length ? (
           <SamplingPromo

+ 7 - 0
static/app/views/settings/project/server-side-sampling/testUtils.tsx

@@ -141,6 +141,13 @@ export const mockedSamplingDistribution: SamplingDistribution = {
       'count()': 100,
     },
   ],
+  parentProjectBreakdown: [
+    {
+      percentage: 10,
+      project: 'parent-project',
+      projectId: 10,
+    },
+  ],
   sampleSize: 100,
   startTimestamp: '2017-08-04T07:52:11Z',
   endTimestamp: '2017-08-05T07:52:11Z',