import React, {Component, Fragment} from 'react'; import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import Cookies from 'js-cookie'; import moment from 'moment-timezone'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import {openModal} from 'sentry/actionCreators/modal'; import {fetchOrganizationDetails} from 'sentry/actionCreators/organization'; import type {PromptData} from 'sentry/actionCreators/prompts'; import { batchedPromptsCheck, promptsCheck, promptsUpdate, } from 'sentry/actionCreators/prompts'; import type {Client} from 'sentry/api'; import {Button, LinkButton} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import {Alert} from 'sentry/components/core/alert'; import {Badge} from 'sentry/components/core/badge'; import ExternalLink from 'sentry/components/links/externalLink'; import {DATA_CATEGORY_INFO} from 'sentry/constants'; import {IconClose} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import ConfigStore from 'sentry/stores/configStore'; import GuideStore from 'sentry/stores/guideStore'; import {space} from 'sentry/styles/space'; import {DataCategory, DataCategoryExact} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import {browserHistory} from 'sentry/utils/browserHistory'; import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser'; import {Oxfordize} from 'sentry/utils/oxfordizeArray'; import {promptIsDismissed} from 'sentry/utils/promptIsDismissed'; import normalizeUrl from 'sentry/utils/url/normalizeUrl'; import withApi from 'sentry/utils/withApi'; import {getDocsLinkForEventType} from 'sentry/views/settings/account/notifications/utils'; import { openForcedTrialModal, openPartnerPlanEndingModal, openTrialEndingModal, } from 'getsentry/actionCreators/modal'; import type {EventType} from 'getsentry/components/addEventsCTA'; import AddEventsCTA from 'getsentry/components/addEventsCTA'; import ProductTrialAlert from 'getsentry/components/productTrial/productTrialAlert'; import {makeLinkToOwnersAndBillingMembers} from 'getsentry/components/profiling/alerts'; import withSubscription from 'getsentry/components/withSubscription'; import ZendeskLink from 'getsentry/components/zendeskLink'; import SubscriptionStore from 'getsentry/stores/subscriptionStore'; import { PlanTier, type Promotion, type PromotionClaimed, type Subscription, } from 'getsentry/types'; import { getActiveProductTrial, getContractDaysLeft, getProductTrial, getTrialLength, hasPerformance, isBusinessTrial, partnerPlanEndingModalIsDismissed, trialPromptIsDismissed, } from 'getsentry/utils/billing'; import {getSingularCategoryName} from 'getsentry/utils/dataCategory'; import {getPendoAccountFields} from 'getsentry/utils/pendo'; import {claimAvailablePromotion} from 'getsentry/utils/promotionUtils'; import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics'; import trackMarketingEvent from 'getsentry/utils/trackMarketingEvent'; import withPromotions from 'getsentry/utils/withPromotions'; enum ModalType { USAGE_EXCEEDED = 'usage-exceeded', GRACE_PERIOD = 'grace-period', PAST_DUE = 'past-due', MEMBER_LIMIT = 'member-limit', } /** * how many days before the trial ends should we show the trial ending modal? */ const TRIAL_ENDING_DAY_WINDOW = 3; const ALERTS_OFF: Record = { error: false, transaction: false, replay: false, attachment: false, monitorSeat: false, span: false, profileDuration: false, uptime: false, }; type SuspensionModalProps = ModalRenderProps & { subscription: Subscription; }; function SuspensionModal({Header, Body, Footer, subscription}: SuspensionModalProps) { return (
{'Action Required'}
{t('Your account has been suspended')}

{t('Your account has been suspended with the following reason:')}

{t( 'Until this situation is resolved you will not be able to send events to Sentry.' )}

{t('Contact Support')}
); } type NoticeModalProps = ModalRenderProps & { billingPermissions: boolean; organization: Organization; subscription: Subscription; whichModal: ModalType; }; function NoticeModal({ Header, Body, Footer, closeModal, subscription, organization, whichModal, billingPermissions, }: NoticeModalProps) { const closeModalAndContinue = (link: string) => { closeModal(); if (whichModal === ModalType.PAST_DUE) { trackGetsentryAnalytics('billing_failure.button_clicked', { organization, has_link: true, has_permissions: billingPermissions, referrer: 'modal-billing-failure', }); } if (link === window.location.pathname) { return; } browserHistory.push(link); }; const closeModalDoNotContinue = () => { closeModal(); if (whichModal === ModalType.PAST_DUE) { trackGetsentryAnalytics('billing_failure.button_clicked', { organization, has_link: false, has_permissions: billingPermissions, referrer: 'modal-billing-failure', }); } }; const alertType = whichModal === ModalType.PAST_DUE ? 'error' : 'warning'; let subText: React.ReactNode; let body: React.ReactNode; let title: React.ReactNode; let link: string; let primaryButtonMessage: React.ReactNode; switch (whichModal) { case ModalType.GRACE_PERIOD: title = t('Grace period started'); body = tct( `Your organization has depleted its error capacity for the current usage period. We've put your account into a one time grace period, which will continue to accept errors at a limited rate. This grace period ends on [gracePeriodEnd].`, {gracePeriodEnd: moment(subscription.gracePeriodEnd).format('ll')} ); link = normalizeUrl(`/settings/${organization.slug}/billing/overview/`); primaryButtonMessage = t('Continue'); break; case ModalType.USAGE_EXCEEDED: title = t('Usage exceeded'); body = t( `Your organization has depleted its event capacity for the current usage period and is currently not receiving new events.` ); link = normalizeUrl(`/settings/${organization.slug}/billing/overview/`); primaryButtonMessage = t('Continue'); break; case ModalType.PAST_DUE: title = t('Unable to bill your account'); body = billingPermissions ? t( `There was an issue with your payment. Update your payment information to ensure uniterrupted access to Sentry.` ) : t( `There was an issue with your payment. Please have the Org Owner or Billing Member update your payment information to ensure continued access to Sentry.` ); link = billingPermissions ? normalizeUrl( `/settings/${organization.slug}/billing/details/?referrer=banner-billing-failure` ) : makeLinkToOwnersAndBillingMembers(organization, 'past_due_modal-alert'); primaryButtonMessage = billingPermissions ? t('Update Billing Details') : t('See Who Can Update'); break; case ModalType.MEMBER_LIMIT: title = t('Member limit exceeded'); body = t( `You organization has more members than your current subscription allows. You will need to upgrade your subscription to ensure everyone has access to Sentry.` ); link = normalizeUrl(`/settings/${organization.slug}/billing/overview/`); primaryButtonMessage = t('Continue'); break; default: } if (subscription.usageExceeded || subscription.isGracePeriod) { if (subscription.isFree) { subText = subscription.canTrial ? t( `Not yet ready to upgrade? You can start a free %s-day trial with unlimited events to better understand your usage.`, getTrialLength(organization) ) : t('To ensure uninterrupted service, upgrade your subscription.'); } else { if (subscription.planTier === PlanTier.AM3) { subText = t( `To ensure uninterrupted service, upgrade your subscription or increase your pay-as-you-go spend limit.` ); } else { subText = t( `To ensure uninterrupted service, upgrade your subscription or increase your on-demand spend limit.` ); } } } return (

{t('Action Required')}

{title}

{body}

{subText &&

{subText}

}
); } type Props = { api: Client; isLoading: boolean; organization: Organization; promotionData: { activePromotions: PromotionClaimed[]; availablePromotions: Promotion[]; completedPromotions: PromotionClaimed[]; }; subscription: Subscription; }; type State = { deactivatedMemberDismissed: boolean; overageAlertDismissed: {[key in EventType]: boolean}; overageWarningDismissed: {[key in EventType]: boolean}; productTrialDismissed: {[key in EventType]: boolean}; }; class GSBanner extends Component { // assume dismissed until we've checked the backend state: State = { deactivatedMemberDismissed: true, overageAlertDismissed: { error: true, transaction: true, replay: true, attachment: true, monitorSeat: true, span: true, profileDuration: true, uptime: true, }, overageWarningDismissed: { error: true, transaction: true, replay: true, attachment: true, monitorSeat: true, span: true, profileDuration: true, uptime: true, }, productTrialDismissed: { error: true, transaction: true, replay: true, attachment: true, monitorSeat: true, span: true, profileDuration: true, uptime: true, }, }; async componentDidMount() { if (this.props.promotionData) { this.activateFirstAvailablePromo() .then(() => this.initializePendo()) .catch(Sentry.captureException); } if (this.props.organization.access.length > 0) { this.tryTriggerTrialEndingModal(); this.tryTriggerSuspendedModal(); this.tryTriggerNoticeModal(); this.tryTriggerForcedTrial(); this.tryTriggerForcedTrialModal(); this.tryTriggerPartnerPlanEndingModal(); } await this.checkPrompts(); // must happen after prompts check if (this.overageAlertType !== null) { const {organization, subscription} = this.props; const isWarning = this.overageAlertType === 'warning'; const eventTypes = Object.entries( isWarning ? this.overageWarningActive : this.overageAlertActive ) .filter(([_, value]) => value) .map(([key, _]) => key as EventType); trackGetsentryAnalytics('quota_alert.alert_displayed', { organization, subscription, event_types: eventTypes.sort().join(','), is_warning: isWarning, }); } } componentDidUpdate(prevProps: Props) { if (this.props.promotionData !== prevProps.promotionData) { this.activateFirstAvailablePromo() .then(() => this.initializePendo()) .catch(Sentry.captureException); } } get trialEndMoment() { return moment().add(TRIAL_ENDING_DAY_WINDOW, 'days'); } get hasBillingPerms() { return this.props.organization?.access?.includes('org:billing'); } async activateFirstAvailablePromo() { const {organization, promotionData, isLoading} = this.props; if (!isLoading && promotionData) { if (isActiveSuperuser()) { return; } await claimAvailablePromotion({ promotionData, organization, }); } } async initializePendo() { const {organization, subscription} = this.props; if (!window.pendo || typeof window.pendo.initialize !== 'function') { return; } try { const data = await this.props.api.requestPromise( `/organizations/${organization.slug}/pendo-details/` ); const activePromotions = this.props.promotionData?.activePromotions; const completedPromotions = this.props.promotionData?.completedPromotions; const user = ConfigStore.get('user'); // if there is a current guide active, delay Pendo until it's done // if no current active guide, can just start Pendo // TODO: should delay Pendo if there is any popup at all that's blocking and not just guides const guideIsActive = !!GuideStore.state.currentGuide; window.pendo.initialize({ guides: { delay: guideIsActive, }, visitor: { id: `${organization.id}.${user.id}`, // need uniqueness per org per user userId: user.id, role: organization.orgRole, isDarkMode: ConfigStore.get('theme') === 'dark', ...data.userDetails, }, account: { id: organization.id, ...getPendoAccountFields(subscription, organization, { activePromotions, completedPromotions, }), ...data.organizationDetails, }, }); } catch (err) { // server will catch any 500 errors that need attention return; } } tryTriggerTrialEndingModal() { const {organization, subscription} = this.props; const trialEndingWindow = [moment(), this.trialEndMoment] as const; // Only show the trial notice if the user is on a business plan trial // Performance trials would require different content not currently supported const showTrialEndedNotice = !subscription.hasDismissedTrialEndingNotice && subscription.canSelfServe && isBusinessTrial(subscription) && moment(subscription.trialEnd).isBetween(...trialEndingWindow); if (!showTrialEndedNotice) { return; } openTrialEndingModal({organization}); } async tryTriggerPartnerPlanEndingModal() { const {organization, subscription, api} = this.props; const hasPartnerMigrationFeature = organization.features.includes( 'partner-billing-migration' ); const hasPendingUpgrade = subscription.pendingChanges !== null && subscription.pendingChanges?.planDetails.price > 0; const daysLeft = getContractDaysLeft(subscription); const showPartnerPlanEndingNotice = subscription.partner !== null && !hasPendingUpgrade && daysLeft >= 0 && daysLeft <= 30 && subscription.partner.isActive && hasPartnerMigrationFeature; if (!showPartnerPlanEndingNotice) { return; } let hasDismissed = true; const prompt = await promptsCheck(api, { organization, feature: 'partner_plan_ending_modal', }); if (daysLeft > 7) { hasDismissed = partnerPlanEndingModalIsDismissed(prompt, subscription, 'month'); } else if (daysLeft > 2) { hasDismissed = partnerPlanEndingModalIsDismissed(prompt, subscription, 'week'); } else if (daysLeft > 0) { hasDismissed = partnerPlanEndingModalIsDismissed(prompt, subscription, 'two'); } else if (daysLeft === 0) { hasDismissed = partnerPlanEndingModalIsDismissed(prompt, subscription, 'zero'); } if (!hasDismissed) { openPartnerPlanEndingModal({organization, subscription}); } } tryTriggerSuspendedModal() { const {subscription} = this.props; if (!subscription.isSuspended) { return; } openModal(props => ); } tryTriggerNoticeModal() { const {organization, subscription} = this.props; const whichModal = subscription.isGracePeriod ? ModalType.GRACE_PERIOD : subscription.usageExceeded ? ModalType.USAGE_EXCEEDED : subscription.isPastDue && subscription.canSelfServe ? ModalType.PAST_DUE : null; if (whichModal === null) { return; } // Only show USAGE_EXCEEDED or PAST_DUE for members if ( !this.hasBillingPerms && !(ModalType.USAGE_EXCEEDED || whichModal === ModalType.PAST_DUE) ) { return; } const cookie = Cookies.get('gsb'); // Did they already see the modal? if (cookie?.split(',').includes(subscription.slug)) { return; } const modalAnalytics = { [ModalType.GRACE_PERIOD]: 'grace_period_modal.seen', [ModalType.USAGE_EXCEEDED]: 'usage_exceeded_modal.seen', [ModalType.PAST_DUE]: 'past_due_modal.seen', } as const; const eventKey = modalAnalytics[whichModal]; const billingPermissions = this.hasBillingPerms; if (eventKey) { trackGetsentryAnalytics(eventKey, {organization, subscription}); } if (eventKey === 'past_due_modal.seen') { trackGetsentryAnalytics('billing_failure.displayed_banner', { organization, has_permissions: billingPermissions, referrer: 'banner-billing-failure', }); } const onClose = () => { let value = subscription.slug; if (cookie && !cookie.includes(value)) { value = `${cookie},${value}`; } const expires = new Date(); expires.setDate(expires.getDate() + 1); document.cookie = `gsb=${value}; expires=${expires.toUTCString()}; path=/`; }; openModal( props => ( ), {onClose} ); } async tryTriggerForcedTrial() { const {organization, subscription, api} = this.props; const user = ConfigStore.get('user'); // check for required conditions of triggering a forced trial of any type const considerTrigger = subscription.canSelfServe && // must be self serve subscription.isFree && hasPerformance(subscription.planDetails) && !subscription.isExemptFromForcedTrial && // orgs who ever did enterprise trials are exempt !user?.isSuperuser; // never trigger for superusers if (!considerTrigger) { return; } // mutliple possible trial endpoints depending on the situation let endpoint: string; // check for restricted integration if (subscription.hasRestrictedIntegration) { endpoint = `/organizations/${organization.slug}/restricted-integration-trial/`; // only trigger if member limit is 1 and we have multiple licenses used } else if (subscription.totalLicenses === 1 && subscription.usedLicenses > 1) { endpoint = `/organizations/${organization.slug}/over-member-limit-trial/`; } else { return; } try { await api.requestPromise(endpoint, { method: 'POST', }); trackMarketingEvent('Start Trial'); // Refresh organization and subscription state // do not mark the trial since we have this modal SubscriptionStore.loadData(organization.slug, null); fetchOrganizationDetails(api, organization.slug); openForcedTrialModal({organization}); } catch (error) { // let check fail but capture exception Sentry.captureException(error); } } tryTriggerForcedTrialModal() { const {subscription, organization} = this.props; if ( subscription.isTrial && subscription.isForcedTrial && !subscription.hasDismissedForcedTrialNotice ) { openForcedTrialModal({organization}); } } async checkPrompts() { const {api, organization, subscription} = this.props; try { const checkResults = await batchedPromptsCheck( api, [ 'deactivated_member_alert', // overage alerts 'errors_overage_alert', 'attachments_overage_alert', 'transactions_overage_alert', 'replays_overage_alert', 'monitor_seats_overage_alert', 'spans_overage_alert', 'profile_duration_overage_alert', 'uptime_overage_alert', // warning alerts 'errors_warning_alert', 'attachments_warning_alert', 'transactions_warning_alert', 'replays_warning_alert', 'monitor_seats_warning_alert', 'spans_warning_alert', 'profile_duration_warning_alert', 'uptime_warning_alert', // product trial alerts 'errors_product_trial_alert', 'attachments_product_trial_alert', 'transactions_product_trial_alert', 'replays_product_trial_alert', 'monitor_seats_product_trial_alert', 'spans_product_trial_alert', 'profile_duration_product_trial_alert', 'uptime_product_trial_alert', ], { organization, } ); // overage notifications should get reset when ondemand period ends const promptIsDismissedForBillingPeriod = (prompt: PromptData) => { const {snoozedTime, dismissedTime} = prompt || {}; // TODO: dismissed prompt should always return false const time = snoozedTime || dismissedTime; if (!time) { return false; } const onDemandPeriodEnd = new Date(subscription.onDemandPeriodEnd); onDemandPeriodEnd.setHours(23, 59, 59); return time <= onDemandPeriodEnd.getTime() / 1000; }; this.setState({ // not billing related prompt checks deactivatedMemberDismissed: promptIsDismissed( checkResults.deactivated_member_alert! ), // billing period related prompt checks overageAlertDismissed: { error: promptIsDismissedForBillingPeriod(checkResults.errors_overage_alert!), transaction: promptIsDismissedForBillingPeriod( checkResults.transactions_overage_alert! ), replay: promptIsDismissedForBillingPeriod(checkResults.replays_overage_alert!), attachment: promptIsDismissedForBillingPeriod( checkResults.attachments_overage_alert! ), monitorSeat: promptIsDismissedForBillingPeriod( checkResults.monitor_seats_overage_alert! ), span: promptIsDismissedForBillingPeriod(checkResults.spans_overage_alert!), profileDuration: promptIsDismissedForBillingPeriod( checkResults.profile_duration_overage_alert! ), uptime: promptIsDismissedForBillingPeriod(checkResults.uptime_overage_alert!), }, overageWarningDismissed: { error: promptIsDismissedForBillingPeriod(checkResults.errors_warning_alert!), transaction: promptIsDismissedForBillingPeriod( checkResults.transactions_warning_alert! ), replay: promptIsDismissedForBillingPeriod(checkResults.replays_warning_alert!), attachment: promptIsDismissedForBillingPeriod( checkResults.attachments_warning_alert! ), monitorSeat: promptIsDismissedForBillingPeriod( checkResults.monitor_seats_warning_alert! ), span: promptIsDismissedForBillingPeriod(checkResults.spans_warning_alert!), profileDuration: promptIsDismissedForBillingPeriod( checkResults.profile_duration_warning_alert! ), uptime: promptIsDismissedForBillingPeriod(checkResults.uptime_warning_alert!), }, productTrialDismissed: { error: trialPromptIsDismissed( checkResults.errors_product_trial_alert!, subscription ), transaction: trialPromptIsDismissed( checkResults.transactions_product_trial_alert!, subscription ), replay: trialPromptIsDismissed( checkResults.replays_product_trial_alert!, subscription ), attachment: trialPromptIsDismissed( checkResults.attachments_product_trial_alert!, subscription ), monitorSeat: trialPromptIsDismissed( checkResults.monitor_seats_product_trial_alert!, subscription ), span: trialPromptIsDismissed( checkResults.spans_product_trial_alert!, subscription ), profileDuration: trialPromptIsDismissed( checkResults.profile_duration_product_trial_alert!, subscription ), uptime: trialPromptIsDismissed( checkResults.uptime_product_trial_alert!, subscription ), }, }); } catch (error) { // let check fail but capture exception Sentry.captureException(error); } } get overageAlertActive(): {[key in EventType]: boolean} { const {subscription} = this.props; if (subscription.hasOverageNotificationsDisabled) { return ALERTS_OFF; } return { error: !this.state.overageAlertDismissed.error && !!subscription.categories.errors?.usageExceeded, transaction: !this.state.overageAlertDismissed.transaction && !!subscription.categories.transactions?.usageExceeded, replay: !this.state.overageAlertDismissed.replay && !!subscription.categories.replays?.usageExceeded, attachment: !this.state.overageAlertDismissed.attachment && !!subscription.categories.attachments?.usageExceeded, monitorSeat: !this.state.overageAlertDismissed.monitorSeat && !!subscription.categories.monitorSeats?.usageExceeded, span: !this.state.overageAlertDismissed.span && !!subscription.categories.spans?.usageExceeded, profileDuration: !this.state.overageAlertDismissed.profileDuration && !!subscription.categories.profileDuration?.usageExceeded, uptime: !this.state.overageAlertDismissed.uptime && !!subscription.categories.uptime?.usageExceeded, }; } get overageWarningActive(): {[key in EventType]: boolean} { const {subscription} = this.props; // disable warnings if org has on-demand if ( subscription.hasOverageNotificationsDisabled || subscription.onDemandMaxSpend > 0 ) { return ALERTS_OFF; } return { error: !this.state.overageWarningDismissed.error && !!subscription.categories.errors?.sentUsageWarning, transaction: !this.state.overageWarningDismissed.transaction && !!subscription.categories.transactions?.sentUsageWarning, replay: !this.state.overageWarningDismissed.replay && !!subscription.categories.replays?.sentUsageWarning, attachment: !this.state.overageWarningDismissed.attachment && !!subscription.categories.attachments?.sentUsageWarning, monitorSeat: !this.state.overageWarningDismissed.monitorSeat && !!subscription.categories.monitorSeats?.sentUsageWarning, span: !this.state.overageWarningDismissed.span && !!subscription.categories.spans?.sentUsageWarning, profileDuration: !this.state.overageWarningDismissed.profileDuration && !!subscription.categories.profileDuration?.sentUsageWarning, uptime: !this.state.overageWarningDismissed.uptime && !!subscription.categories.uptime?.sentUsageWarning, }; } // Returns true for overage alert, false for overage warning, and null if we don't show anything. get overageAlertType(): 'critical' | 'warning' | null { const {subscription} = this.props; if (!hasPerformance(subscription.planDetails)) { return null; } if (!subscription.canSelfServe) { return null; } if (Object.values(this.overageAlertActive).some(a => a)) { return 'critical'; } if (Object.values(this.overageWarningActive).some(a => a)) { return 'warning'; } return null; } renderOverageAlertPrimaryCTA(eventTypes: EventType[], isWarning: boolean) { const {subscription, organization} = this.props; // can't use as const with ternary const notificationType: 'overage_warning' | 'overage_critical' = isWarning ? 'overage_warning' : 'overage_critical'; const props = { organization, subscription, eventTypes, notificationType, referrer: `overage-alert-${eventTypes.join('-')}`, source: isWarning ? 'quota-warning' : 'quota-overage', handleRequestSent: () => this.handleOverageSnooze(eventTypes, isWarning), }; return ; } handleOverageSnooze(eventTypes: EventType[], isWarning: boolean) { const {organization, api} = this.props; const dismissState: {[key in EventType]: boolean} = isWarning ? this.state.overageWarningDismissed : this.state.overageAlertDismissed; for (const eventType of eventTypes) { if (dismissState[eventType]) { // This type of event is already dismissed. Skip. continue; } const key = isWarning ? 'warning' : 'overage'; const featureMap: Record = { error: `errors_${key}_alert`, transaction: `transactions_${key}_alert`, replay: `replays_${key}_alert`, attachment: `attachments_${key}_alert`, monitorSeat: `monitor_seats_${key}_alert`, span: `spans_${key}_alert`, profileDuration: `profile_duration_${key}_alert`, uptime: `uptime_${key}_alert`, }; promptsUpdate(api, { organization, feature: featureMap[eventType], status: 'snoozed', }); } const dismissedState: {[key in EventType]: boolean} = { error: true, attachment: true, replay: true, transaction: true, monitorSeat: true, span: true, profileDuration: true, uptime: true, }; // Suppress all warnings and alerts this.setState({ overageAlertDismissed: dismissedState, overageWarningDismissed: dismissedState, }); } renderOverageAlert(isWarning: boolean) { const {organization, subscription} = this.props; const plan = subscription.planDetails; let overquotaPrompt: React.ReactNode; let eventTypes: EventType[] = []; const eventTypeToElement = (eventType: EventType): JSX.Element => { const onClick = () => { trackGetsentryAnalytics('quota_alert.clicked_link', { organization, subscription, event_types: eventTypes.sort().join(','), is_warning: isWarning, clicked_event: eventType, }); }; // @ts-expect-error TS(2339): Property 'profileDuration' does not exist on type ... Remove this comment to see the full error message return { error: ( {getSingularCategoryName({ plan, category: DataCategory.ERRORS, capitalize: false, })} ), transaction: ( {getSingularCategoryName({ plan, category: DataCategory.TRANSACTIONS, capitalize: false, })} ), replay: ( {getSingularCategoryName({ plan, category: DataCategory.REPLAYS, capitalize: false, })} ), attachment: ( {getSingularCategoryName({ plan, category: DataCategory.ATTACHMENTS, capitalize: false, })} ), monitorSeat: ( {getSingularCategoryName({ plan, category: DataCategory.MONITOR_SEATS, capitalize: false, })} ), span: ( {getSingularCategoryName({ plan, category: DataCategory.SPANS, capitalize: false, })} ), uptime: ( {getSingularCategoryName({ plan, category: DataCategory.UPTIME, capitalize: false, })} ), // TODO: Uncomment when we have a continuous profile doc link // profile: ( // // {getSingularCategoryName({ // plan, // category: DataCategory.PROFILES, // capitalize: false, // })} // // ), }[eventType]!; }; let strictlyCronsOverage = false; if (isWarning) { eventTypes = Object.entries(this.overageWarningActive) .filter( ([key, value]) => value && getActiveProductTrial( subscription.productTrials ?? null, // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message DATA_CATEGORY_INFO[key].plural ) === null ) .map(([key, _]) => key as EventType); // Make an exception for when only crons has an overage to disable the See Usage button strictlyCronsOverage = eventTypes.length === 1 && eventTypes[0] === 'monitorSeat'; overquotaPrompt = tct( 'You are about to exceed your [eventTypes] limit and we will drop any excess events.', { eventTypes: ( {eventTypes.map(eventTypeToElement)} ), } ); } else { eventTypes = Object.entries(this.overageAlertActive) .filter( ([key, value]) => value && getActiveProductTrial( subscription.productTrials ?? null, // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message DATA_CATEGORY_INFO[key].plural ) === null ) .map(([key, _]) => key as EventType); // Make an exception for when only crons has an overage to change the language to be more fitting and hide See Usage if ( eventTypes.length === 1 && (eventTypes[0] === 'monitorSeat' || eventTypes[0] === 'uptime') ) { overquotaPrompt = tct( `We can't enable additional [monitorTitle] because you don't have a sufficient [budgetType] budget.`, { monitorTitle: eventTypes[0] === 'monitorSeat' ? 'Cron Monitors' : 'Uptime Monitors', budgetType: subscription.planTier === PlanTier.AM3 ? 'pay-as-you-go' : 'on-demand', } ); } else { overquotaPrompt = tct( 'You have exceeded your [eventTypes] limit. We are dropping any excess events until [periodEnd].', { eventTypes: ( {eventTypes.map(eventTypeToElement)} ), periodEnd: moment(subscription.onDemandPeriodEnd).add(1, 'days').format('ll'), } ); } } if (eventTypes.length === 0) { return null; } return ( {!strictlyCronsOverage && ( { trackGetsentryAnalytics('quota_alert.clicked_see_usage', { organization, subscription, event_types: eventTypes.sort().join(','), is_warning: isWarning, }); }} > {t('See Usage')} )} {this.renderOverageAlertPrimaryCTA(eventTypes, isWarning)} } > {tct( `[firstSentence] [middleSentence] Upgrade your plan to increase your limit.`, { firstSentence: subscription.totalLicenses === 1 ? t('Your plan is limited to one user.') : tct('Your plan is limited to [totalLicenses] users.', { totalLicenses: subscription.totalLicenses, }), middleSentence: membersDeactivatedFromLimit === 1 ? tct('[wrappedNumber] member has been deactivated.', { wrappedNumber, }) : tct('[wrappedNumber] members have been deactivated.', { wrappedNumber, }), } )} ); } return productTrialAlerts ?? null; } } export default withPromotions(withApi(withSubscription(GSBanner, {noLoader: true}))); // XXX: We have no alert types with this styling, but for now we would like for // it to be differentiated. const BannerAlert = styled(Alert)` color: ${p => p.theme.headerBackground}; background-color: ${p => p.theme.bannerBackground}; border: none; `;