samplingModeSwitchModal.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import {useId} from 'react';
  2. import styled from '@emotion/styled';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. addSuccessMessage,
  7. } from 'sentry/actionCreators/indicator';
  8. import {type ModalRenderProps, openModal} from 'sentry/actionCreators/modal';
  9. import {Button} from 'sentry/components/button';
  10. import FieldGroup from 'sentry/components/forms/fieldGroup';
  11. import ExternalLink from 'sentry/components/links/externalLink';
  12. import {t, tct} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {Organization} from 'sentry/types/organization';
  15. import {formatNumberWithDynamicDecimalPoints} from 'sentry/utils/number/formatNumberWithDynamicDecimalPoints';
  16. import {PercentInput} from 'sentry/views/settings/dynamicSampling/percentInput';
  17. import {organizationSamplingForm} from 'sentry/views/settings/dynamicSampling/utils/organizationSamplingForm';
  18. import {useUpdateOrganization} from 'sentry/views/settings/dynamicSampling/utils/useUpdateOrganization';
  19. interface Props {
  20. /**
  21. * The sampling mode to switch to.
  22. */
  23. samplingMode: Organization['samplingMode'];
  24. /**
  25. * The initial target rate for the automatic sampling mode.
  26. * Required if `samplingMode` is 'automatic'.
  27. */
  28. initialTargetRate?: number;
  29. }
  30. const {FormProvider, useFormState, useFormField} = organizationSamplingForm;
  31. function SamplingModeSwitchModal({
  32. Header,
  33. Body,
  34. Footer,
  35. closeModal,
  36. samplingMode,
  37. initialTargetRate = 1,
  38. }: Props & ModalRenderProps) {
  39. const formState = useFormState({
  40. initialValues: {
  41. targetSampleRate: formatNumberWithDynamicDecimalPoints(initialTargetRate * 100, 2),
  42. },
  43. });
  44. const {mutate: updateOrganization, isPending} = useUpdateOrganization({
  45. onMutate: () => {
  46. addLoadingMessage(t('Switching sampling mode...'));
  47. },
  48. onSuccess: () => {
  49. addSuccessMessage(t('Changes applied.'));
  50. closeModal();
  51. },
  52. onError: () => {
  53. addErrorMessage(t('Unable to save changes. Please try again.'));
  54. },
  55. });
  56. const handleSubmit = () => {
  57. if (!formState.isValid) {
  58. return;
  59. }
  60. const changes: Parameters<typeof updateOrganization>[0] = {
  61. samplingMode,
  62. };
  63. if (samplingMode === 'organization') {
  64. changes.targetSampleRate = Number(formState.fields.targetSampleRate.value) / 100;
  65. }
  66. updateOrganization(changes);
  67. };
  68. return (
  69. <FormProvider formState={formState}>
  70. <form
  71. onSubmit={event => {
  72. event.preventDefault();
  73. handleSubmit();
  74. }}
  75. noValidate
  76. >
  77. <Header>
  78. <h5>
  79. {samplingMode === 'organization'
  80. ? t('Switch to Automatic Mode')
  81. : t('Switch to Manual Mode')}
  82. </h5>
  83. </Header>
  84. <Body>
  85. <p>
  86. {samplingMode === 'organization'
  87. ? tct(
  88. '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]',
  89. {
  90. link: (
  91. <ExternalLink href="https://docs.sentry.io/product/performance/retention-priorities/" />
  92. ),
  93. }
  94. )
  95. : tct(
  96. '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]',
  97. {
  98. link: (
  99. <ExternalLink href="https://docs.sentry.io/product/performance/retention-priorities/" />
  100. ),
  101. }
  102. )}
  103. </p>
  104. {samplingMode === 'organization' && <TargetRateInput disabled={isPending} />}
  105. <p>
  106. {samplingMode === 'organization'
  107. ? tct(
  108. 'By switching [strong:you will lose your manually configured sample rates].',
  109. {
  110. strong: <strong />,
  111. }
  112. )
  113. : t('You can switch back to automatic mode at any time.')}
  114. </p>
  115. </Body>
  116. <Footer>
  117. <ButtonWrapper>
  118. <Button disabled={isPending} onClick={closeModal}>
  119. {t('Cancel')}
  120. </Button>
  121. <Button
  122. priority="primary"
  123. disabled={isPending || !formState.isValid}
  124. onClick={handleSubmit}
  125. >
  126. {t('Switch Mode')}
  127. </Button>
  128. </ButtonWrapper>
  129. </Footer>
  130. </form>
  131. </FormProvider>
  132. );
  133. }
  134. function TargetRateInput({disabled}: {disabled?: boolean}) {
  135. const id = useId();
  136. const {value, onChange, error} = useFormField('targetSampleRate');
  137. return (
  138. <FieldGroup
  139. label={t('Global Target Sample Rate')}
  140. css={{paddingBottom: space(0.5)}}
  141. inline={false}
  142. showHelpInTooltip
  143. flexibleControlStateSize
  144. stacked
  145. required
  146. >
  147. <InputWrapper>
  148. <PercentInput
  149. id={id}
  150. aria-label={t('Global Target Sample Rate')}
  151. value={value}
  152. onChange={event => onChange(event.target.value)}
  153. disabled={disabled}
  154. />
  155. <ErrorMessage>
  156. {error
  157. ? error
  158. : // Placholder character to keep the space occupied
  159. '\u200b'}
  160. </ErrorMessage>
  161. </InputWrapper>
  162. </FieldGroup>
  163. );
  164. }
  165. const InputWrapper = styled('div')`
  166. display: flex;
  167. flex-direction: column;
  168. gap: ${space(0.5)};
  169. `;
  170. const ErrorMessage = styled('div')`
  171. color: ${p => p.theme.red300};
  172. font-size: ${p => p.theme.fontSizeExtraSmall};
  173. `;
  174. const ButtonWrapper = styled('div')`
  175. display: flex;
  176. gap: ${space(2)};
  177. `;
  178. export function openSamplingModeSwitchModal(props: Props) {
  179. openModal(dialogProps => <SamplingModeSwitchModal {...dialogProps} {...props} />);
  180. }