profilingUpgradeModal.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import type {ComponentProps} from 'react';
  2. import {useCallback, useEffect} from 'react';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import * as Sentry from '@sentry/react';
  6. import {addSuccessMessage} from 'sentry/actionCreators/indicator';
  7. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  8. import {closeModal} from 'sentry/actionCreators/modal';
  9. import {Button} from 'sentry/components/button';
  10. import ErrorBoundary from 'sentry/components/errorBoundary';
  11. import HighlightModalContainer from 'sentry/components/highlightModalContainer';
  12. import ExternalLink from 'sentry/components/links/externalLink';
  13. import List from 'sentry/components/list';
  14. import ListItem from 'sentry/components/list/listItem';
  15. import Placeholder from 'sentry/components/placeholder';
  16. import {SidebarPanelKey} from 'sentry/components/sidebar/types';
  17. import {t} from 'sentry/locale';
  18. import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
  19. import {space} from 'sentry/styles/space';
  20. import type {Organization} from 'sentry/types/organization';
  21. import useApi from 'sentry/utils/useApi';
  22. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  23. import type {Subscription} from 'getsentry/types';
  24. import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
  25. import PlanTable from '../upgradeNowModal/planTable';
  26. import usePreviewData from '../upgradeNowModal/usePreviewData';
  27. import useUpgradeNowParams from '../upgradeNowModal/useUpgradeNowParams';
  28. import {redirectToManage} from '../upgradeNowModal/utils';
  29. type Props = ModalRenderProps &
  30. Omit<ComponentProps<typeof ActionButtons>, 'hasPriceChange'> & {
  31. organization: Organization;
  32. subscription: Subscription;
  33. };
  34. function UpsellModal(props: Props) {
  35. const {organization, subscription} = props;
  36. const hasBillingAccess = organization.access?.includes('org:billing');
  37. const {loading, reservations, previewData, error} = usePreviewData(props);
  38. useEffect(() => {
  39. if (error && hasBillingAccess) {
  40. // Redirect the user to the subscriptions page, where they will find important information.
  41. // If they wish to update their plan, we ask them to contact our sales/support team.
  42. redirectToManage(organization);
  43. }
  44. }, [error, hasBillingAccess, organization]);
  45. useEffect(() => {
  46. trackGetsentryAnalytics('upgrade_now.modal.viewed', {
  47. organization,
  48. planTier: subscription.planTier,
  49. canSelfServe: subscription.canSelfServe,
  50. channel: subscription.channel,
  51. has_billing_scope: organization.access?.includes('org:billing'),
  52. surface: 'profiling',
  53. // Disable `react-hooks/exhaustive-deps` because of this next line...
  54. // We want to track analytics right away, cannot wait for the network.
  55. has_price_change: loading ? undefined : previewData?.billedAmount !== 0,
  56. });
  57. }, [organization, subscription]); // eslint-disable-line react-hooks/exhaustive-deps
  58. return (
  59. <HighlightModalContainer>
  60. <ModalLayout>
  61. <UpsellContent>
  62. <SubheaderPrimary>{t('Updates to Sentry')}</SubheaderPrimary>
  63. <Header>{t('Performance Monitoring and Profiling that scales')}</Header>
  64. <p>
  65. {t(
  66. 'Get full visibility into the performance and stability of your application'
  67. )}
  68. </p>
  69. <List symbol="bullet">
  70. <ListItem>
  71. {t('Identify hot code paths to optimize resource consumption')}
  72. </ListItem>
  73. <ListItem>{t('See the exact functions causing performance issues')}</ListItem>
  74. <ListItem>
  75. <ExternalLink href="https://docs.sentry.io/product/data-management-settings/dynamic-sampling/">
  76. {t('Dynamically sample performance events at scale*')}
  77. </ExternalLink>
  78. </ListItem>
  79. </List>
  80. {loading || error ? (
  81. <Placeholder height="40px" />
  82. ) : (
  83. <ActionButtons {...props} hasPriceChange={previewData.billedAmount !== 0} />
  84. )}
  85. <Note>
  86. {t(
  87. '* Dynamic sampling kicks in for customers reserving 1M or more performance units a month'
  88. )}
  89. </Note>
  90. </UpsellContent>
  91. <div>
  92. <Subheader>{t('Plan Volume')}</Subheader>
  93. <ErrorBoundary mini>
  94. {loading || error ? (
  95. <Placeholder height="100%" />
  96. ) : (
  97. <PlanTable
  98. organization={organization}
  99. subscription={subscription}
  100. reservations={reservations}
  101. previewData={previewData}
  102. />
  103. )}
  104. </ErrorBoundary>
  105. </div>
  106. </ModalLayout>
  107. </HighlightModalContainer>
  108. );
  109. }
  110. const Subheader = styled('h2')`
  111. text-transform: uppercase;
  112. font-weight: bold;
  113. font-size: ${p => p.theme.fontSizeSmall};
  114. margin-bottom: ${space(1)};
  115. `;
  116. const SubheaderPrimary = styled(Subheader)`
  117. color: ${p => p.theme.purple300};
  118. `;
  119. const Header = styled('h1')`
  120. font-size: ${p => p.theme.headerFontSize};
  121. font-weight: bold;
  122. margin: ${space(1)} 0;
  123. `;
  124. const ModalLayout = styled('div')`
  125. display: grid;
  126. font-size: ${p => p.theme.fontSizeMedium};
  127. margin-bottom: ${space(2)};
  128. @media (min-width: ${p => p.theme.breakpoints.small}) {
  129. grid-template-columns: 1fr auto;
  130. gap: ${space(3)};
  131. }
  132. `;
  133. const UpsellContent = styled('div')`
  134. grid-column: 1;
  135. grid-row: 1;
  136. font-size: ${p => p.theme.fontSizeLarge};
  137. `;
  138. const Note = styled('p')`
  139. color: ${p => p.theme.gray300};
  140. font-size: ${p => p.theme.fontSizeExtraSmall};
  141. `;
  142. export const modalCss = css`
  143. width: 100%;
  144. max-width: 980px;
  145. [role='document'] {
  146. position: relative;
  147. padding: 80px;
  148. overflow: hidden;
  149. }
  150. `;
  151. export default UpsellModal;
  152. type ActionButtonsProps = {
  153. hasPriceChange: boolean;
  154. organization: Organization;
  155. subscription: Subscription;
  156. isActionDisabled?: boolean;
  157. onComplete?: () => void;
  158. };
  159. function ActionButtons({
  160. hasPriceChange,
  161. isActionDisabled,
  162. onComplete,
  163. organization,
  164. subscription,
  165. }: ActionButtonsProps) {
  166. const api = useApi();
  167. const {plan, reservations} = useUpgradeNowParams({organization, subscription});
  168. const onUpdatePlan = useCallback(async () => {
  169. try {
  170. await api.requestPromise(`/customers/${organization.slug}/subscription/`, {
  171. method: 'PUT',
  172. data: {
  173. ...reservations,
  174. plan: plan?.id,
  175. referrer: 'profiling-am2-update-modal',
  176. },
  177. });
  178. SubscriptionStore.loadData(organization.slug, () => {
  179. if (onComplete) {
  180. onComplete();
  181. }
  182. closeModal();
  183. addSuccessMessage(t('Subscription Updated!'));
  184. SidebarPanelStore.activatePanel(SidebarPanelKey.PROFILING_ONBOARDING);
  185. trackGetsentryAnalytics('upgrade_now.modal.update_now', {
  186. organization,
  187. planTier: subscription.planTier,
  188. canSelfServe: subscription.canSelfServe,
  189. channel: subscription.channel,
  190. has_billing_scope: organization.access?.includes('org:billing'),
  191. surface: 'profiling',
  192. has_price_change: hasPriceChange,
  193. });
  194. });
  195. } catch (err) {
  196. Sentry.captureException(err);
  197. redirectToManage(organization);
  198. }
  199. }, [api, organization, subscription, plan, reservations, onComplete, hasPriceChange]);
  200. const onClickManageSubscription = useCallback(() => {
  201. trackGetsentryAnalytics('upgrade_now.modal.manage_sub', {
  202. organization,
  203. surface: 'profiling',
  204. planTier: subscription.planTier,
  205. canSelfServe: subscription.canSelfServe,
  206. channel: subscription.channel,
  207. has_billing_scope: organization.access?.includes('org:billing'),
  208. });
  209. }, [organization, subscription]);
  210. const hasBillingAccess = organization.access?.includes('org:billing');
  211. return hasBillingAccess ? (
  212. <ButtonRow>
  213. <Button
  214. priority="primary"
  215. onClick={onUpdatePlan}
  216. disabled={isActionDisabled === true}
  217. >
  218. {t('Update Now')}
  219. </Button>
  220. <Button
  221. to={`/settings/${organization.slug}/billing/checkout/?referrer=profiling_onboard_modal-owner-modal`}
  222. onClick={onClickManageSubscription}
  223. >
  224. {t('Manage Subscription')}
  225. </Button>
  226. </ButtonRow>
  227. ) : (
  228. <ButtonRow>
  229. <Button
  230. disabled
  231. title={t(
  232. 'Only members with the role “Owner” or “Billing” can manage subscriptions'
  233. )}
  234. >
  235. {t('Manage Subscription')}
  236. </Button>
  237. </ButtonRow>
  238. );
  239. }
  240. const ButtonRow = styled('p')`
  241. display: flex;
  242. gap: ${space(1.5)};
  243. margin-top: ${space(3)};
  244. margin-bottom: ${space(2)};
  245. `;