Skip to content

Commit

Permalink
feat(dynamic-sampling): Add sample rate input to mode switch (#80653)
Browse files Browse the repository at this point in the history
Add an input for setting the org wide sample rate to the confirm dialog
when switching from manual to automatic mode.

Closes getsentry/projects#384
  • Loading branch information
ArthurKnaus authored Nov 13, 2024
1 parent 914c741 commit 981b4a0
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 88 deletions.
25 changes: 24 additions & 1 deletion static/app/views/settings/dynamicSampling/projectSampling.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,29 @@ export function ProjectSampling() {
});
};

// TODO(aknaus): This calculation + stiching of the two requests is repeated in a few places
// and should be moved to a shared utility function.
const initialTargetRate = useMemo(() => {
const sampleRates = sampleRatesQuery.data ?? [];
const spanCounts = sampleCountsQuery.data ?? [];
const totalSpanCount = spanCounts.reduce((acc, item) => acc + item.count, 0);

const spanCountsById = spanCounts.reduce(
(acc, item) => {
acc[item.project.id] = item.count;
return acc;
},
{} as Record<string, number>
);

return (
sampleRates.reduce((acc, item) => {
const count = spanCountsById[item.id] ?? 0;
return acc + count * item.sampleRate;
}, 0) / totalSpanCount
);
}, [sampleRatesQuery.data, sampleCountsQuery.data]);

const isFormActionDisabled =
!hasAccess ||
sampleRatesQuery.isPending ||
Expand All @@ -105,7 +128,7 @@ export function ProjectSampling() {
<Panel>
<PanelHeader>{t('General Settings')}</PanelHeader>
<PanelBody>
<SamplingModeField />
<SamplingModeField initialTargetRate={initialTargetRate} />
</PanelBody>
</Panel>
<HeadingRow>
Expand Down
25 changes: 13 additions & 12 deletions static/app/views/settings/dynamicSampling/projectsEditTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,26 +132,29 @@ export function ProjectsEditTable({
);
const [activeItems, inactiveItems] = partition(items, item => item.count > 0);

const totalSpanCount = useMemo(
() => items.reduce((acc, item) => acc + item.count, 0),
[items]
);

const projectedOrgRate = useMemo(() => {
if (editMode === 'bulk') {
return orgRate;
}
const totalSpans = items.reduce((acc, item) => acc + item.count, 0);
const totalSampledSpans = items.reduce(
(acc, item) => acc + item.count * Number(value[item.project.id] ?? 100),
0
);
return formatNumberWithDynamicDecimalPoints(totalSampledSpans / totalSpans, 2);
}, [editMode, items, orgRate, value]);
return formatNumberWithDynamicDecimalPoints(totalSampledSpans / totalSpanCount, 2);
}, [editMode, items, orgRate, totalSpanCount, value]);

const initialOrgRate = useMemo(() => {
const totalSpans = items.reduce((acc, item) => acc + item.count, 0);
const totalSampledSpans = items.reduce(
(acc, item) => acc + item.count * Number(initialValue[item.project.id] ?? 100),
0
);
return formatNumberWithDynamicDecimalPoints(totalSampledSpans / totalSpans, 2);
}, [initialValue, items]);
return formatNumberWithDynamicDecimalPoints(totalSampledSpans / totalSpanCount, 2);
}, [initialValue, items, totalSpanCount]);

const breakdownSampleRates = useMemo(
() =>
Expand Down Expand Up @@ -203,12 +206,10 @@ export function ProjectsEditTable({
/>
<FlexRow>
<PreviousValue>
{initialOrgRate !== projectedOrgRate ? (
t('previous: %f%%', initialOrgRate)
) : (
// Placeholder char to prevent the line from collapsing
<Fragment>&#x200b;</Fragment>
)}
{initialOrgRate !== projectedOrgRate
? t('previous: %f%%', initialOrgRate)
: // Placeholder char to prevent the line from collapsing
'\u200b'}
</PreviousValue>
{hasAccess && !isBulkEditEnabled && (
<BulkEditButton
Expand Down
87 changes: 12 additions & 75 deletions static/app/views/settings/dynamicSampling/samplingModeField.tsx
Original file line number Diff line number Diff line change
@@ -1,93 +1,30 @@
import {Fragment} from 'react';
import styled from '@emotion/styled';

import {
addErrorMessage,
addLoadingMessage,
addSuccessMessage,
} from 'sentry/actionCreators/indicator';
import {openConfirmModal} from 'sentry/components/confirm';
import FieldGroup from 'sentry/components/forms/fieldGroup';
import ExternalLink from 'sentry/components/links/externalLink';
import QuestionTooltip from 'sentry/components/questionTooltip';
import {SegmentedControl} from 'sentry/components/segmentedControl';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import useOrganization from 'sentry/utils/useOrganization';
import {openSamplingModeSwitchModal} from 'sentry/views/settings/dynamicSampling/samplingModeSwitchModal';
import {useHasDynamicSamplingWriteAccess} from 'sentry/views/settings/dynamicSampling/utils/access';
import {useUpdateOrganization} from 'sentry/views/settings/dynamicSampling/utils/useUpdateOrganization';

const switchToManualMessage = tct(
'Switching to manual mode disables automatic adjustments. After the switch, you can configure individual sample rates for each project. Dynamic sampling priorities continue to apply within the projects. [link:Learn more]',
{
link: (
<ExternalLink href="https://docs.sentry.io/product/performance/retention-priorities/" />
),
}
);

const switchToAutoMessage = tct(
'Switching to automatic mode enables continuous adjustments for your projects based on a global target sample rate. Sentry boosts the sample rates of small projects and ensures equal visibility. [link:Learn more]',
{
link: (
<ExternalLink href="https://docs.sentry.io/product/performance/retention-priorities/" />
),
}
);
interface Props {
/**
* The initial target rate for the automatic sampling mode.
*/
initialTargetRate?: number;
}

export function SamplingModeField() {
export function SamplingModeField({initialTargetRate}: Props) {
const {samplingMode} = useOrganization();
const hasAccess = useHasDynamicSamplingWriteAccess();

const {mutate: updateOrganization, isPending} = useUpdateOrganization({
onMutate: () => {
addLoadingMessage(t('Switching sampling mode...'));
},
onSuccess: () => {
addSuccessMessage(t('Changes applied.'));
},
onError: () => {
addErrorMessage(t('Unable to save changes. Please try again.'));
},
});

const handleSwitchMode = () => {
openConfirmModal({
confirmText: t('Switch Mode'),
cancelText: t('Cancel'),
header: (
<h5>
{samplingMode === 'organization'
? t('Switch to Manual Mode')
: t('Switch to Automatic Mode')}
</h5>
),
message: (
<Fragment>
<p>
{samplingMode === 'organization'
? switchToManualMessage
: switchToAutoMessage}
</p>
{samplingMode === 'organization' ? (
<p>{t('You can switch back to automatic mode at any time.')}</p>
) : (
<p>
{tct(
'By switching [strong:you will lose your manually configured sample rates].',
{
strong: <strong />,
}
)}
</p>
)}
</Fragment>
),
onConfirm: () => {
updateOrganization({
samplingMode: samplingMode === 'organization' ? 'project' : 'organization',
});
},
openSamplingModeSwitchModal({
samplingMode: samplingMode === 'organization' ? 'project' : 'organization',
initialTargetRate,
});
};

Expand All @@ -99,7 +36,7 @@ export function SamplingModeField() {
>
<ControlWrapper>
<SegmentedControl
disabled={!hasAccess || isPending}
disabled={!hasAccess}
label={t('Sampling mode')}
value={samplingMode}
onChange={handleSwitchMode}
Expand Down
193 changes: 193 additions & 0 deletions static/app/views/settings/dynamicSampling/samplingModeSwitchModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import {useId} from 'react';
import styled from '@emotion/styled';

import {
addErrorMessage,
addLoadingMessage,
addSuccessMessage,
} from 'sentry/actionCreators/indicator';
import {type ModalRenderProps, openModal} from 'sentry/actionCreators/modal';
import {Button} from 'sentry/components/button';
import FieldGroup from 'sentry/components/forms/fieldGroup';
import ExternalLink from 'sentry/components/links/externalLink';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Organization} from 'sentry/types/organization';
import {formatNumberWithDynamicDecimalPoints} from 'sentry/utils/number/formatNumberWithDynamicDecimalPoints';
import {PercentInput} from 'sentry/views/settings/dynamicSampling/percentInput';
import {organizationSamplingForm} from 'sentry/views/settings/dynamicSampling/utils/organizationSamplingForm';
import {useUpdateOrganization} from 'sentry/views/settings/dynamicSampling/utils/useUpdateOrganization';

interface Props {
/**
* The sampling mode to switch to.
*/
samplingMode: Organization['samplingMode'];
/**
* The initial target rate for the automatic sampling mode.
* Required if `samplingMode` is 'automatic'.
*/
initialTargetRate?: number;
}

const {FormProvider, useFormState, useFormField} = organizationSamplingForm;

function SamplingModeSwitchModal({
Header,
Body,
Footer,
closeModal,
samplingMode,
initialTargetRate = 1,
}: Props & ModalRenderProps) {
const formState = useFormState({
initialValues: {
targetSampleRate: formatNumberWithDynamicDecimalPoints(initialTargetRate * 100, 2),
},
});

const {mutate: updateOrganization, isPending} = useUpdateOrganization({
onMutate: () => {
addLoadingMessage(t('Switching sampling mode...'));
},
onSuccess: () => {
addSuccessMessage(t('Changes applied.'));
closeModal();
},
onError: () => {
addErrorMessage(t('Unable to save changes. Please try again.'));
},
});

const handleSubmit = () => {
if (!formState.isValid) {
return;
}
const changes: Parameters<typeof updateOrganization>[0] = {
samplingMode,
};
if (samplingMode === 'organization') {
changes.targetSampleRate = Number(formState.fields.targetSampleRate.value) / 100;
}
updateOrganization(changes);
};

return (
<FormProvider formState={formState}>
<form
onSubmit={event => {
event.preventDefault();
handleSubmit();
}}
noValidate
>
<Header>
<h5>
{samplingMode === 'organization'
? t('Switch to Automatic Mode')
: t('Switch to Manual Mode')}
</h5>
</Header>
<Body>
<p>
{samplingMode === 'organization'
? tct(
'Switching to automatic mode enables continuous adjustments for your projects based on a global target sample rate. Sentry boosts the sample rates of small projects and ensures equal visibility. [link:Learn more]',
{
link: (
<ExternalLink href="https://docs.sentry.io/product/performance/retention-priorities/" />
),
}
)
: tct(
'Switching to manual mode disables automatic adjustments. After the switch, you can configure individual sample rates for each project. Dynamic sampling priorities continue to apply within the projects. [link:Learn more]',
{
link: (
<ExternalLink href="https://docs.sentry.io/product/performance/retention-priorities/" />
),
}
)}
</p>
{samplingMode === 'organization' && <TargetRateInput disabled={isPending} />}
<p>
{samplingMode === 'organization'
? tct(
'By switching [strong:you will lose your manually configured sample rates].',
{
strong: <strong />,
}
)
: t('You can switch back to automatic mode at any time.')}
</p>
</Body>
<Footer>
<ButtonWrapper>
<Button disabled={isPending} onClick={closeModal}>
{t('Cancel')}
</Button>
<Button
priority="primary"
disabled={isPending || !formState.isValid}
onClick={handleSubmit}
>
{t('Switch Mode')}
</Button>
</ButtonWrapper>
</Footer>
</form>
</FormProvider>
);
}

function TargetRateInput({disabled}: {disabled?: boolean}) {
const id = useId();
const {value, onChange, error} = useFormField('targetSampleRate');

return (
<FieldGroup
label={t('Global Target Sample Rate')}
css={{paddingBottom: space(0.5)}}
inline={false}
showHelpInTooltip
flexibleControlStateSize
stacked
required
>
<InputWrapper>
<PercentInput
id={id}
aria-label={t('Global Target Sample Rate')}
value={value}
onChange={event => onChange(event.target.value)}
disabled={disabled}
/>
<ErrorMessage>
{error
? error
: // Placholder character to keep the space occupied
'\u200b'}
</ErrorMessage>
</InputWrapper>
</FieldGroup>
);
}

const InputWrapper = styled('div')`
display: flex;
flex-direction: column;
gap: ${space(0.5)};
`;

const ErrorMessage = styled('div')`
color: ${p => p.theme.red300};
font-size: ${p => p.theme.fontSizeExtraSmall};
`;

const ButtonWrapper = styled('div')`
display: flex;
gap: ${space(2)};
`;

export function openSamplingModeSwitchModal(props: Props) {
openModal(dialogProps => <SamplingModeSwitchModal {...dialogProps} {...props} />);
}

0 comments on commit 981b4a0

Please sign in to comment.