alerts.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import {Fragment, useCallback, useEffect} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {ButtonProps} from 'sentry/components/button';
  4. import {Button} from 'sentry/components/button';
  5. import {Alert} from 'sentry/components/core/alert';
  6. import {IconClose, IconInfo, IconWarning} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import type {Organization} from 'sentry/types/organization';
  9. import useDismissAlert from 'sentry/utils/useDismissAlert';
  10. import {openAM2ProfilingUpsellModal} from 'getsentry/actionCreators/modal';
  11. import withSubscription from 'getsentry/components/withSubscription';
  12. import type {Subscription} from 'getsentry/types';
  13. import {PlanTier} from 'getsentry/types';
  14. import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
  15. export function makeLinkToOwnersAndBillingMembers(
  16. organization: Organization,
  17. referrer: string
  18. ) {
  19. return `/settings/${organization.slug}/members/?referrer=${referrer}&query=role%3Abilling+role%3Aowner`;
  20. }
  21. export function makeLinkToManageSubscription(
  22. organization: Organization,
  23. referrer: string
  24. ) {
  25. return `/settings/${organization.slug}/billing/overview/?referrer=${referrer}`;
  26. }
  27. function makeAnalyticsProps(
  28. organization: AlertProps['organization'],
  29. subscription: AlertProps['subscription']
  30. ) {
  31. return {
  32. organization,
  33. surface: 'profiling' as const,
  34. planTier: subscription.planTier,
  35. canSelfServe: subscription.canSelfServe,
  36. channel: subscription.channel,
  37. has_billing_scope: organization.access?.includes('org:billing'),
  38. };
  39. }
  40. function trackOpenModal({organization, subscription}: AlertProps) {
  41. trackGetsentryAnalytics(
  42. 'upgrade_now.alert.open_modal',
  43. makeAnalyticsProps(organization, subscription)
  44. );
  45. }
  46. function trackPageView({organization, subscription}: AlertProps) {
  47. trackGetsentryAnalytics(
  48. 'upgrade_now.alert.viewed',
  49. makeAnalyticsProps(organization, subscription)
  50. );
  51. }
  52. function trackDismiss({organization, subscription}: AlertProps) {
  53. trackGetsentryAnalytics(
  54. 'upgrade_now.alert.dismiss',
  55. makeAnalyticsProps(organization, subscription)
  56. );
  57. }
  58. function trackManageSubscriptionClicked({organization, subscription}: AlertProps) {
  59. trackGetsentryAnalytics(
  60. 'upgrade_now.alert.manage_sub',
  61. makeAnalyticsProps(organization, subscription)
  62. );
  63. }
  64. interface AlertProps {
  65. organization: Organization;
  66. subscription: Subscription;
  67. }
  68. interface GraceAlertProps {
  69. action: {
  70. label: string;
  71. onClick?: () => void;
  72. to?: string | Record<PropertyKey, unknown>;
  73. };
  74. children: React.ReactNode;
  75. dismiss: undefined | (() => void);
  76. disableAction?: boolean;
  77. type?: 'error';
  78. }
  79. function GraceAlert({children, action, dismiss, type, disableAction}: GraceAlertProps) {
  80. const trailingItems = (
  81. <Fragment>
  82. <StyledButton
  83. size="xs"
  84. onClick={action.onClick}
  85. to={action.to}
  86. disabled={disableAction}
  87. >
  88. {action.label}
  89. </StyledButton>
  90. {dismiss ? (
  91. <StyledButton priority="link" size="sm" onClick={dismiss}>
  92. <IconClose color="gray500" size="sm" />
  93. </StyledButton>
  94. ) : null}
  95. </Fragment>
  96. );
  97. return (
  98. <Alert
  99. icon={type ? <IconWarning /> : dismiss ? <IconInfo /> : <IconWarning />}
  100. showIcon
  101. system
  102. trailingItems={trailingItems}
  103. type={type ? type : dismiss ? 'info' : 'error'}
  104. >
  105. {children}
  106. </Alert>
  107. );
  108. }
  109. const StyledButton = styled(Button)`
  110. color: inherit;
  111. `;
  112. interface GenericGraceAlertProps extends AlertProps {
  113. action: (props: any) => {
  114. label: string;
  115. onClick?: () => void;
  116. to?: string | Record<PropertyKey, unknown>;
  117. };
  118. children: React.ReactNode;
  119. dismissKey: string;
  120. dismissable: boolean;
  121. disableAction?: boolean;
  122. }
  123. function GenericGraceAlert(props: GenericGraceAlertProps) {
  124. const {dismiss, isDismissed} = useDismissAlert({
  125. key: props.dismissKey,
  126. expirationDays: 14,
  127. });
  128. const onDismiss = useCallback(() => {
  129. dismiss();
  130. trackDismiss(props);
  131. }, [dismiss, props]);
  132. useEffect(() => {
  133. if (!isDismissed) {
  134. trackPageView(props);
  135. }
  136. }, [props, isDismissed]);
  137. if (isDismissed && props.dismissable) {
  138. return null;
  139. }
  140. return (
  141. <GraceAlert
  142. action={props.action({dismiss: onDismiss})}
  143. dismiss={props.dismissable ? onDismiss : undefined}
  144. disableAction={props.disableAction}
  145. >
  146. {props.children}
  147. </GraceAlert>
  148. );
  149. }
  150. // Beta users on AM1 - explain that free ingestion will stop
  151. // Users to be told that free profiling ingestion will end post-launch;
  152. // and they will need to upgrade to AM2 to continue using profiling.
  153. function ProfilingAM1BetaUserGraceAlert({organization, subscription}: AlertProps) {
  154. const userCanUpgrade = organization.access?.includes('org:billing');
  155. return (
  156. <GenericGraceAlert
  157. // If ingestion has not been stopped, the alert can be dismissed
  158. dismissable={false}
  159. dismissKey={`${organization.id}:dismiss-profiling-beta-am1-stopped-ingestion`}
  160. action={({dismiss}) => {
  161. if (subscription.canSelfServe) {
  162. if (userCanUpgrade) {
  163. return {
  164. label: t('Update Plan'),
  165. onClick: () => {
  166. openAM2ProfilingUpsellModal({
  167. organization,
  168. subscription,
  169. onComplete: dismiss,
  170. });
  171. trackOpenModal({organization, subscription});
  172. },
  173. };
  174. }
  175. return {
  176. label: t('See who can update'),
  177. to: makeLinkToOwnersAndBillingMembers(
  178. organization,
  179. 'profiling_onboard_am1-alert'
  180. ),
  181. onClick: () => trackManageSubscriptionClicked({organization, subscription}),
  182. };
  183. }
  184. return {
  185. label: t('Manage subscription'),
  186. to: makeLinkToManageSubscription(organization, 'profiling_onboard_am1-alert'),
  187. onClick: () => trackManageSubscriptionClicked({organization, subscription}),
  188. };
  189. }}
  190. organization={organization}
  191. subscription={subscription}
  192. >
  193. {t(
  194. 'The profiling beta has now ended, and this organization is unable to ingest new profiles. Existing data will expire per our retention policy. To continue using profiling, update to the latest version of your plan.'
  195. )}
  196. </GenericGraceAlert>
  197. );
  198. }
  199. // Wrappers switch the different alerts to show for users on the AM1 plan.
  200. function ProfilingAM1Alerts({organization, subscription}: AlertProps) {
  201. if (organization.features.includes('profiling-beta')) {
  202. return (
  203. <ProfilingAM1BetaUserGraceAlert
  204. organization={organization}
  205. subscription={subscription}
  206. />
  207. );
  208. }
  209. return null;
  210. }
  211. interface ProfilingBetaAlertBannerProps {
  212. organization: Organization;
  213. subscription: Subscription;
  214. }
  215. function ProfilingBetaAlertBannerComponent(props: ProfilingBetaAlertBannerProps) {
  216. const ComponentToShow =
  217. props.subscription.planTier === PlanTier.AM1 ? ProfilingAM1Alerts : null;
  218. return ComponentToShow ? (
  219. <ComponentToShow
  220. organization={props.organization}
  221. subscription={props.subscription}
  222. />
  223. ) : null;
  224. }
  225. export const ProfilingBetaAlertBanner = withSubscription(
  226. ProfilingBetaAlertBannerComponent,
  227. {noLoader: true}
  228. );
  229. type UpgradePlanButtonProps = ButtonProps & {
  230. children: React.ReactNode;
  231. fallback: React.ReactNode;
  232. organization: Organization;
  233. subscription: Subscription;
  234. };
  235. const hidePromptTiers: string[] = [PlanTier.AM2, PlanTier.AM3];
  236. function UpgradePlanButton(props: UpgradePlanButtonProps) {
  237. const {subscription, organization, ...buttonProps} = props;
  238. if (hidePromptTiers.includes(subscription.planTier)) {
  239. return <Fragment>{props.fallback}</Fragment>;
  240. }
  241. const userCanUpgradePlan = organization.access?.includes('org:billing');
  242. if (subscription.canSelfServe) {
  243. if (userCanUpgradePlan) {
  244. return (
  245. <Button
  246. {...buttonProps}
  247. onClick={evt => {
  248. openAM2ProfilingUpsellModal({
  249. organization,
  250. subscription,
  251. });
  252. trackOpenModal({organization, subscription});
  253. props.onClick?.(evt);
  254. }}
  255. >
  256. {t('Update Plan')}
  257. </Button>
  258. );
  259. }
  260. return (
  261. <Button
  262. {...buttonProps}
  263. to={makeLinkToOwnersAndBillingMembers(
  264. organization,
  265. `profiling_onboard_${
  266. subscription.planTier === PlanTier.AM1 ? 'am1' : 'mmx'
  267. }-alert`
  268. )}
  269. onClick={() => trackManageSubscriptionClicked({organization, subscription})}
  270. >
  271. {t('See who can update')}
  272. </Button>
  273. );
  274. }
  275. return (
  276. <Button
  277. {...buttonProps}
  278. to={`/settings/${organization.slug}/billing/overview/?referrer=profiling_onboard_${
  279. subscription.planTier === PlanTier.AM1 ? 'am1' : 'mmx'
  280. }-alert`}
  281. onClick={() => trackManageSubscriptionClicked({organization, subscription})}
  282. >
  283. {t('Manage subscription')}
  284. </Button>
  285. );
  286. }
  287. export const ProfilingUpgradePlanButton = withSubscription(UpgradePlanButton, {
  288. noLoader: true,
  289. });
  290. interface ProfilingAM1OrMMXUpgradeProps {
  291. fallback: React.ReactNode;
  292. organization: Organization;
  293. subscription: Subscription;
  294. }
  295. function ProfilingAM1OrMMXUpgradeComponent({
  296. organization,
  297. subscription,
  298. fallback,
  299. }: ProfilingAM1OrMMXUpgradeProps) {
  300. if (hidePromptTiers.includes(subscription.planTier)) {
  301. return <Fragment>{fallback}</Fragment>;
  302. }
  303. const userCanUpgradePlan = organization.access?.includes('org:billing');
  304. return (
  305. <Fragment>
  306. <h3>{t('Function level insights')}</h3>
  307. <p>
  308. {userCanUpgradePlan
  309. ? t(
  310. 'Discover slow-to-execute or resource intensive functions within your application. To access profiling, please update to the latest version of your plan.'
  311. )
  312. : t(
  313. 'Discover slow-to-execute or resource intensive functions within your application. To access profiling, please request your account owner to update to the latest version of your plan.'
  314. )}
  315. </p>
  316. </Fragment>
  317. );
  318. }
  319. export const ProfilingAM1OrMMXUpgrade = withSubscription(
  320. ProfilingAM1OrMMXUpgradeComponent,
  321. {
  322. noLoader: true,
  323. }
  324. );