123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- import {Fragment, useEffect, useState} from 'react';
- import styled from '@emotion/styled';
- import isEqual from 'lodash/isEqual';
- import Alert from 'sentry/components/alert';
- import Button from 'sentry/components/button';
- import ButtonBar from 'sentry/components/buttonBar';
- import {NumberField} from 'sentry/components/forms';
- import ExternalLink from 'sentry/components/links/externalLink';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import {PanelTable} from 'sentry/components/panels';
- import QuestionTooltip from 'sentry/components/questionTooltip';
- import Radio from 'sentry/components/radio';
- import {IconRefresh} from 'sentry/icons';
- import {t, tct} from 'sentry/locale';
- import ModalStore from 'sentry/stores/modalStore';
- import {useLegacyStore} from 'sentry/stores/useLegacyStore';
- import space from 'sentry/styles/space';
- import {Project, SeriesApi} from 'sentry/types';
- import {SamplingRule, UniformModalsSubmit} from 'sentry/types/sampling';
- import {defined} from 'sentry/utils';
- import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
- import {formatPercentage} from 'sentry/utils/formatters';
- import TextBlock from 'sentry/views/settings/components/text/textBlock';
- import {SamplingSDKAlert} from '../samplingSDKAlert';
- import {
- isValidSampleRate,
- percentageToRate,
- rateToPercentage,
- SERVER_SIDE_SAMPLING_DOC_LINK,
- } from '../utils';
- import {hasFirstBucketsEmpty} from '../utils/hasFirstBucketsEmpty';
- import {projectStatsToPredictedSeries} from '../utils/projectStatsToPredictedSeries';
- import {projectStatsToSampleRates} from '../utils/projectStatsToSampleRates';
- import {projectStatsToSeries} from '../utils/projectStatsToSeries';
- import useProjectStats from '../utils/useProjectStats';
- import {useRecommendedSdkUpgrades} from '../utils/useRecommendedSdkUpgrades';
- import {RecommendedStepsModal, RecommendedStepsModalProps} from './recommendedStepsModal';
- import {UniformRateChart} from './uniformRateChart';
- const CONSERVATIVE_SAMPLE_RATE = 0.1;
- enum Strategy {
- CURRENT = 'current',
- RECOMMENDED = 'recommended',
- }
- enum Step {
- SET_UNIFORM_SAMPLE_RATE = 'set_uniform_sample_rate',
- RECOMMENDED_STEPS = 'recommended_steps',
- }
- type Props = Omit<
- RecommendedStepsModalProps,
- 'onSubmit' | 'recommendedSdkUpgrades' | 'projectId' | 'recommendedSampleRate'
- > & {
- onSubmit: UniformModalsSubmit;
- project: Project;
- rules: SamplingRule[];
- projectStats?: SeriesApi;
- };
- function UniformRateModal({
- Header,
- Body,
- Footer,
- closeModal,
- organization,
- projectStats,
- project,
- uniformRule,
- onSubmit,
- onReadDocs,
- ...props
- }: Props) {
- const [rules, setRules] = useState(props.rules);
- const modalStore = useLegacyStore(ModalStore);
- const {projectStats: projectStats30d, loading: loading30d} = useProjectStats({
- orgSlug: organization.slug,
- projectId: project.id,
- interval: '1d',
- statsPeriod: '30d',
- });
- const {recommendedSdkUpgrades, fetching: fetchingRecommendedSdkUpgrades} =
- useRecommendedSdkUpgrades({
- orgSlug: organization.slug,
- });
- const loading = loading30d || !projectStats || fetchingRecommendedSdkUpgrades;
- const [activeStep, setActiveStep] = useState<Step>(Step.SET_UNIFORM_SAMPLE_RATE);
- const shouldUseConservativeSampleRate =
- recommendedSdkUpgrades.length === 0 &&
- hasFirstBucketsEmpty(projectStats30d, 27) &&
- hasFirstBucketsEmpty(projectStats, 3);
- useEffect(() => {
- // updated or created rules will always have a new id,
- // therefore the isEqual will always work in this case
- if (modalStore.renderer === null && isEqual(rules, props.rules)) {
- trackAdvancedAnalyticsEvent(
- activeStep === Step.RECOMMENDED_STEPS
- ? 'sampling.settings.modal.recommended.next.steps_cancel'
- : 'sampling.settings.modal.uniform.rate_cancel',
- {
- organization,
- project_id: project.id,
- }
- );
- }
- }, [activeStep, modalStore.renderer, organization, project.id, rules, props.rules]);
- const uniformSampleRate = uniformRule?.sampleRate;
- const {trueSampleRate, maxSafeSampleRate} = projectStatsToSampleRates(projectStats);
- const currentClientSampling =
- defined(trueSampleRate) && !isNaN(trueSampleRate) ? trueSampleRate : undefined;
- const currentServerSampling =
- defined(uniformSampleRate) && !isNaN(uniformSampleRate)
- ? uniformSampleRate
- : undefined;
- const recommendedClientSampling =
- defined(maxSafeSampleRate) && !isNaN(maxSafeSampleRate)
- ? maxSafeSampleRate
- : undefined;
- const recommendedServerSampling = shouldUseConservativeSampleRate
- ? CONSERVATIVE_SAMPLE_RATE
- : currentClientSampling;
- const [selectedStrategy, setSelectedStrategy] = useState<Strategy>(Strategy.CURRENT);
- const [clientInput, setClientInput] = useState(
- rateToPercentage(recommendedClientSampling)
- );
- const [serverInput, setServerInput] = useState(
- rateToPercentage(recommendedServerSampling)
- );
- // ^^^ We use clientInput and serverInput variables just for the text fields, everywhere else we should use client and server variables vvv
- const client = percentageToRate(clientInput);
- const server = percentageToRate(serverInput);
- const [saving, setSaving] = useState(false);
- const shouldHaveNextStep =
- client !== currentClientSampling || recommendedSdkUpgrades.length > 0;
- useEffect(() => {
- setClientInput(rateToPercentage(recommendedClientSampling));
- setServerInput(rateToPercentage(recommendedServerSampling));
- }, [recommendedClientSampling, recommendedServerSampling]);
- useEffect(() => {
- trackAdvancedAnalyticsEvent(
- selectedStrategy === Strategy.RECOMMENDED
- ? 'sampling.settings.modal.uniform.rate_switch_recommended'
- : 'sampling.settings.modal.uniform.rate_switch_current',
- {
- organization,
- project_id: project.id,
- }
- );
- }, [selectedStrategy, organization, project.id]);
- const isEdited =
- client !== recommendedClientSampling || server !== recommendedServerSampling;
- const isValid = isValidSampleRate(client) && isValidSampleRate(server);
- function handlePrimaryButtonClick() {
- // this can either be "Next" or "Done"
- if (!isValid) {
- return;
- }
- if (shouldHaveNextStep) {
- trackAdvancedAnalyticsEvent('sampling.settings.modal.uniform.rate_next', {
- organization,
- project_id: project.id,
- });
- setActiveStep(Step.RECOMMENDED_STEPS);
- return;
- }
- setSaving(true);
- onSubmit({
- recommendedSampleRate: !isEdited,
- uniformRateModalOrigin: true,
- sampleRate: server!,
- rule: uniformRule,
- onSuccess: newRules => {
- setSaving(false);
- setRules(newRules);
- closeModal();
- },
- onError: () => {
- setSaving(false);
- },
- });
- }
- function handleReadDocs() {
- trackAdvancedAnalyticsEvent('sampling.settings.modal.uniform.rate_read_docs', {
- organization,
- project_id: project.id,
- });
- onReadDocs();
- }
- 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={onSubmit}
- onReadDocs={onReadDocs}
- clientSampleRate={client}
- serverSampleRate={server}
- uniformRule={uniformRule}
- projectId={project.id}
- recommendedSampleRate={!isEdited}
- onSetRules={setRules}
- />
- );
- }
- return (
- <Fragment>
- <Header closeButton>
- <h4>{t('Set a global sample rate')}</h4>
- </Header>
- <Body>
- <TextBlock>
- {tct(
- 'Set a server-side sample rate for all transactions using our suggestion as a starting point. To accurately monitor overall performance, we also suggest changing your client(SDK) sample rate to allow more metrics to be processed. [learnMoreLink: Learn more about quota management].',
- {
- learnMoreLink: <ExternalLink href={SERVER_SIDE_SAMPLING_DOC_LINK} />,
- }
- )}
- </TextBlock>
- {loading ? (
- <LoadingIndicator />
- ) : (
- <Fragment>
- <UniformRateChart
- series={
- selectedStrategy === Strategy.CURRENT
- ? projectStatsToSeries(projectStats30d)
- : projectStatsToPredictedSeries(projectStats30d, client, server)
- }
- isLoading={loading30d}
- />
- <StyledPanelTable
- headers={[
- t('Sampling Values'),
- <RightAligned key="client">{t('Client')}</RightAligned>,
- <RightAligned key="server">{t('Server')}</RightAligned>,
- '',
- ]}
- >
- <Fragment>
- <Label htmlFor="sampling-current">
- <Radio
- id="sampling-current"
- checked={selectedStrategy === Strategy.CURRENT}
- onChange={() => {
- setSelectedStrategy(Strategy.CURRENT);
- }}
- />
- {t('Current')}
- </Label>
- <RightAligned>
- {defined(currentClientSampling)
- ? formatPercentage(currentClientSampling)
- : 'N/A'}
- </RightAligned>
- <RightAligned>
- {defined(currentServerSampling)
- ? formatPercentage(currentServerSampling)
- : 'N/A'}
- </RightAligned>
- <div />
- </Fragment>
- <Fragment>
- <Label htmlFor="sampling-recommended">
- <Radio
- id="sampling-recommended"
- checked={selectedStrategy === Strategy.RECOMMENDED}
- onChange={() => {
- setSelectedStrategy(Strategy.RECOMMENDED);
- }}
- />
- {isEdited ? t('New') : t('Suggested')}
- {!isEdited && (
- <QuestionTooltip
- title={t(
- 'Optimal sample rates based on your organization’s usage and quota.'
- )}
- size="sm"
- />
- )}
- </Label>
- <RightAligned>
- <StyledNumberField
- name="recommended-client-sampling"
- placeholder="%"
- value={clientInput ?? null}
- onChange={value => {
- setClientInput(value === '' ? undefined : value);
- }}
- onFocus={() => setSelectedStrategy(Strategy.RECOMMENDED)}
- stacked
- flexibleControlStateSize
- inline={false}
- />
- </RightAligned>
- <RightAligned>
- <StyledNumberField
- name="recommended-server-sampling"
- placeholder="%"
- value={serverInput ?? null}
- onChange={value => {
- setServerInput(value === '' ? undefined : value);
- }}
- onFocus={() => setSelectedStrategy(Strategy.RECOMMENDED)}
- stacked
- flexibleControlStateSize
- inline={false}
- />
- </RightAligned>
- <ResetButton>
- {isEdited && (
- <Button
- icon={<IconRefresh size="sm" />}
- aria-label={t('Reset to suggested values')}
- onClick={() => {
- setClientInput(rateToPercentage(recommendedClientSampling));
- setServerInput(rateToPercentage(recommendedServerSampling));
- }}
- borderless
- size="zero"
- />
- )}
- </ResetButton>
- </Fragment>
- </StyledPanelTable>
- <SamplingSDKAlert
- organization={organization}
- projectId={project.id}
- rules={rules}
- recommendedSdkUpgrades={recommendedSdkUpgrades}
- showLinkToTheModal={false}
- onReadDocs={onReadDocs}
- />
- {shouldUseConservativeSampleRate && (
- <Alert type="info" showIcon>
- {t(
- "For accurate suggestions, we need at least 48hrs to ingest transactions. Meanwhile, here's a conservative server-side sampling rate which can be changed later on."
- )}
- </Alert>
- )}
- </Fragment>
- )}
- </Body>
- <Footer>
- <FooterActions>
- <Button href={SERVER_SIDE_SAMPLING_DOC_LINK} onClick={handleReadDocs} external>
- {t('Read Docs')}
- </Button>
- <ButtonBar gap={1}>
- {shouldHaveNextStep && <Stepper>{t('Step 1 of 2')}</Stepper>}
- <Button onClick={closeModal}>{t('Cancel')}</Button>
- <Button
- priority="primary"
- onClick={handlePrimaryButtonClick}
- disabled={saving || !isValid || selectedStrategy === Strategy.CURRENT}
- title={
- selectedStrategy === Strategy.CURRENT
- ? t('Current sampling values selected')
- : !isValid
- ? t('Sample rate is not valid')
- : undefined
- }
- >
- {shouldHaveNextStep ? t('Next') : t('Done')}
- </Button>
- </ButtonBar>
- </FooterActions>
- </Footer>
- </Fragment>
- );
- }
- const StyledPanelTable = styled(PanelTable)`
- grid-template-columns: 1fr 115px 115px 35px;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- `;
- const RightAligned = styled('div')`
- text-align: right;
- `;
- const ResetButton = styled('div')`
- padding-left: 0;
- display: inline-flex;
- `;
- const Label = styled('label')`
- font-weight: 400;
- display: inline-flex;
- align-items: center;
- gap: ${space(1)};
- margin-bottom: 0;
- `;
- const StyledNumberField = styled(NumberField)`
- width: 100%;
- `;
- export const FooterActions = styled('div')`
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex: 1;
- gap: ${space(1)};
- `;
- export const Stepper = styled('span')`
- font-size: ${p => p.theme.fontSizeMedium};
- color: ${p => p.theme.subText};
- `;
- export {UniformRateModal};
|