Skip to content

Commit

Permalink
feat(dynamic-sampling): Sampling breakdown (#80304)
Browse files Browse the repository at this point in the history
Display a breakdown visualization of the whole org in manual sampling
mode.

Closes getsentry/projects#355
  • Loading branch information
ArthurKnaus authored Nov 6, 2024
1 parent 1c9225f commit 25f1e04
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {css} from '@emotion/react';
import styled from '@emotion/styled';

import FieldGroup from 'sentry/components/forms/fieldGroup';
import {InputGroup} from 'sentry/components/inputGroup';
import {Tooltip} from 'sentry/components/tooltip';
import {t} from 'sentry/locale';
import {PercentInput} from 'sentry/views/settings/dynamicSampling/percentInput';
import {organizationSamplingForm} from 'sentry/views/settings/dynamicSampling/utils/organizationSamplingForm';
import {useAccess} from 'sentry/views/settings/projectMetrics/access';

Expand Down Expand Up @@ -32,20 +32,15 @@ export function OrganizationSampleRateField({}) {
disabled={hasAccess}
title={t('You do not have permission to change the sample rate.')}
>
<InputGroup>
<InputGroup.Input
width={100}
type="number"
min={0}
max={100}
disabled={!hasAccess}
value={field.value}
onChange={event => field.onChange(event.target.value)}
/>
<InputGroup.TrailingItems>
<TrailingPercent>%</TrailingPercent>
</InputGroup.TrailingItems>
</InputGroup>
<PercentInput
width={100}
type="number"
min={0}
max={100}
disabled={!hasAccess}
value={field.value}
onChange={event => field.onChange(event.target.value)}
/>
</Tooltip>
{field.error ? (
<ErrorMessage>{field.error}</ErrorMessage>
Expand Down Expand Up @@ -74,7 +69,3 @@ const InputWrapper = styled('div')`
flex-direction: column;
gap: 4px;
`;

const TrailingPercent = styled('strong')`
padding: 0 2px;
`;
27 changes: 27 additions & 0 deletions static/app/views/settings/dynamicSampling/percentInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type React from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';

import {InputGroup} from 'sentry/components/inputGroup';
import {space} from 'sentry/styles/space';

interface Props extends React.ComponentProps<typeof InputGroup.Input> {}

export function PercentInput(props: Props) {
return (
<InputGroup
css={css`
width: 160px;
`}
>
<InputGroup.Input type="number" {...props} />
<InputGroup.TrailingItems>
<TrailingPercent>%</TrailingPercent>
</InputGroup.TrailingItems>
</InputGroup>
);
}

const TrailingPercent = styled('strong')`
padding: 0 ${space(0.25)};
`;
101 changes: 90 additions & 11 deletions static/app/views/settings/dynamicSampling/projectsEditTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import {useCallback, useMemo} from 'react';
import {Fragment, useCallback, useMemo} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
import partition from 'lodash/partition';

import LoadingError from 'sentry/components/loadingError';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import Panel from 'sentry/components/panels/panel';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {formatNumberWithDynamicDecimalPoints} from 'sentry/utils/number/formatNumberWithDynamicDecimalPoints';
import useProjects from 'sentry/utils/useProjects';
import {PercentInput} from 'sentry/views/settings/dynamicSampling/percentInput';
import {ProjectsTable} from 'sentry/views/settings/dynamicSampling/projectsTable';
import {SamplingBreakdown} from 'sentry/views/settings/dynamicSampling/samplingBreakdown';
import {projectSamplingForm} from 'sentry/views/settings/dynamicSampling/utils/projectSamplingForm';
import {useProjectSampleCounts} from 'sentry/views/settings/dynamicSampling/utils/useProjectSampleCounts';

Expand All @@ -16,7 +24,7 @@ interface Props {
const {useFormField} = projectSamplingForm;
const EMPTY_ARRAY = [];

export function ProjectsEditTable({isLoading, period}: Props) {
export function ProjectsEditTable({isLoading: isLoadingProp, period}: Props) {
const {projects, fetching} = useProjects();

const {value, initialValue, error, onChange} = useFormField('projectRates');
Expand Down Expand Up @@ -64,19 +72,90 @@ export function ProjectsEditTable({isLoading, period}: Props) {
[onChange]
);

// weighted average of all projects' sample rates
const totalSpans = items.reduce((acc, item) => acc + item.count, 0);
const projectedOrgRate = useMemo(() => {
const totalSampledSpans = items.reduce(
(acc, item) => acc + item.count * Number(value[item.project.id] ?? 100),
0
);
return totalSampledSpans / totalSpans;
}, [items, value, totalSpans]);

const breakdownSampleRates = useMemo(
() =>
Object.entries(value).reduce(
(acc, [projectId, rate]) => {
acc[projectId] = Number(rate) / 100;
return acc;
},
{} as Record<string, number>
),
[value]
);

if (isError) {
return <LoadingError onRetry={refetch} />;
}

const isLoading = fetching || isPending || isLoadingProp;

return (
<ProjectsTable
canEdit
onChange={handleChange}
emptyMessage={t('No active projects found in the selected period.')}
isEmpty={!data.length}
isLoading={fetching || isPending || isLoading}
items={activeItems}
inactiveItems={inactiveItems}
/>
<Fragment>
<BreakdownPanel>
{isLoading ? (
<LoadingIndicator
css={css`
margin: ${space(4)} 0;
`}
/>
) : (
<Fragment>
<ProjectedOrgRateWrapper>
{t('Projected Organization Rate')}
<PercentInput
type="number"
disabled
min={0}
max={100}
size="sm"
value={formatNumberWithDynamicDecimalPoints(projectedOrgRate, 2)}
/>
</ProjectedOrgRateWrapper>
<Divider />
<SamplingBreakdown period={period} sampleRates={breakdownSampleRates} />
</Fragment>
)}
</BreakdownPanel>

<ProjectsTable
canEdit
onChange={handleChange}
emptyMessage={t('No active projects found in the selected period.')}
isEmpty={!data.length}
isLoading={isLoading}
items={activeItems}
inactiveItems={inactiveItems}
/>
</Fragment>
);
}

const BreakdownPanel = styled(Panel)`
margin-bottom: ${space(3)};
padding: ${space(2)};
`;
const ProjectedOrgRateWrapper = styled('label')`
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: ${space(1)};
font-weight: ${p => p.theme.fontWeightNormal};
`;

const Divider = styled('hr')`
margin: ${space(2)} -${space(2)};
border: none;
border-top: 1px solid ${p => p.theme.innerBorder};
`;
34 changes: 10 additions & 24 deletions static/app/views/settings/dynamicSampling/projectsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {Fragment, memo, useCallback, useState} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';

import {hasEveryAccess} from 'sentry/components/acl/access';
import {LinkButton} from 'sentry/components/button';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import {InputGroup} from 'sentry/components/inputGroup';
import {PanelTable} from 'sentry/components/panels/panelTable';
import {Tooltip} from 'sentry/components/tooltip';
import {IconArrow, IconChevron, IconSettings} from 'sentry/icons';
Expand All @@ -15,6 +13,7 @@ import type {Project} from 'sentry/types/project';
import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
import oxfordizeArray from 'sentry/utils/oxfordizeArray';
import useOrganization from 'sentry/utils/useOrganization';
import {PercentInput} from 'sentry/views/settings/dynamicSampling/percentInput';

interface ProjectItem {
count: number;
Expand Down Expand Up @@ -281,24 +280,15 @@ const TableRow = memo(function TableRow({
disabled={canEdit}
title={t('To edit project sample rates, switch to manual sampling mode.')}
>
<InputGroup
css={css`
width: 160px;
`}
>
<InputGroup.Input
type="number"
disabled={!canEdit}
onChange={handleChange}
min={0}
max={100}
size="sm"
value={sampleRate}
/>
<InputGroup.TrailingItems>
<TrailingPercent>%</TrailingPercent>
</InputGroup.TrailingItems>
</InputGroup>
<PercentInput
type="number"
disabled={!canEdit}
onChange={handleChange}
min={0}
max={100}
size="sm"
value={sampleRate}
/>
</Tooltip>
</FirstCellLine>
{error ? (
Expand Down Expand Up @@ -439,7 +429,3 @@ const SettingsButton = styled(LinkButton)`
visibility: visible;
}
`;

const TrailingPercent = styled('strong')`
padding: 0 ${space(0.25)};
`;
Loading

0 comments on commit 25f1e04

Please sign in to comment.