insightsUpsellPage.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import upsellImage from 'getsentry-images/features/insights/module-upsells/insights-module-upsell.svg';
  4. import appStartPreviewImg from 'sentry-images/insights/module-upsells/insights-app-starts-module-charts.svg';
  5. import assetsPreviewImg from 'sentry-images/insights/module-upsells/insights-assets-module-charts.svg';
  6. import cachesPreviewImg from 'sentry-images/insights/module-upsells/insights-caches-module-charts.svg';
  7. import llmPreviewImg from 'sentry-images/insights/module-upsells/insights-llm-module-charts.svg';
  8. import queriesPreviewImg from 'sentry-images/insights/module-upsells/insights-queries-module-charts.svg';
  9. import queuesPreviewImg from 'sentry-images/insights/module-upsells/insights-queues-module-charts.svg';
  10. import requestPreviewImg from 'sentry-images/insights/module-upsells/insights-requests-module-charts.svg';
  11. import screenLoadsPreviewImg from 'sentry-images/insights/module-upsells/insights-screen-loads-module-charts.svg';
  12. import screenRenderingPreviewImg from 'sentry-images/insights/module-upsells/insights-screen-rendering-module-charts.svg';
  13. import webVitalsPreviewImg from 'sentry-images/insights/module-upsells/insights-web-vitals-module-charts.svg';
  14. import {Button, LinkButton} from 'sentry/components/button';
  15. import Panel from 'sentry/components/panels/panel';
  16. import {IconBusiness, IconCheckmark} from 'sentry/icons';
  17. import type {SVGIconProps} from 'sentry/icons/svgIcon';
  18. import {t} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import type {Organization} from 'sentry/types/organization';
  21. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import withOrganization from 'sentry/utils/withOrganization';
  24. import type {TitleableModuleNames} from 'sentry/views/insights/common/components/modulePageProviders';
  25. import {MODULE_TITLES} from 'sentry/views/insights/settings';
  26. import {openUpsellModal} from 'getsentry/actionCreators/modal';
  27. import {
  28. type InsightSidebarId,
  29. InsightsItemAccessRule,
  30. } from 'getsentry/components/sidebarNavigationItem';
  31. import UpgradeOrTrialButton from 'getsentry/components/upgradeOrTrialButton';
  32. import {SidebarFooter} from 'getsentry/components/upsellModal/footer';
  33. import withSubscription from 'getsentry/components/withSubscription';
  34. import {useBillingConfig} from 'getsentry/hooks/useBillingConfig';
  35. import type {Subscription} from 'getsentry/types';
  36. import {getFriendlyPlanName} from 'getsentry/utils/billing';
  37. const SUBTITLE = t(
  38. 'Insights give you a deeper understanding of your application’s frontend and backend dependencies so you can easily create software that’s performant, reliable, and that people want to use.'
  39. );
  40. const TITLE = t('Find out why your application is mad at you');
  41. interface Props {
  42. children: React.ReactNode;
  43. moduleName: TitleableModuleNames;
  44. organization: Organization;
  45. subscription: Subscription;
  46. fullPage?: boolean; // This prop is temporary while we transition to domain views for performance
  47. }
  48. type ModuleNameClickHandler = (module: TitleableModuleNames) => void;
  49. /** @internal exported for tests only */
  50. export function InsightsUpsellPage({
  51. moduleName,
  52. fullPage,
  53. subscription,
  54. children,
  55. }: Props) {
  56. const hasRequiredFeatures = useHasRequiredInsightFeatures(moduleName, subscription);
  57. if (hasRequiredFeatures) {
  58. return children;
  59. }
  60. return (
  61. <UpsellPage
  62. defaultModule={moduleName}
  63. subscription={subscription}
  64. fullPage={fullPage ?? true}
  65. />
  66. );
  67. }
  68. const useHasRequiredInsightFeatures = (
  69. moduleName: TitleableModuleNames,
  70. subscription: Subscription
  71. ) => {
  72. const id = sidebarIdMap[moduleName];
  73. const organization = useOrganization();
  74. const {data: billingConfig} = useBillingConfig({organization, subscription});
  75. // if there's no sidebar id mapping, the module isn't bound to any feature,
  76. // so let's just show it
  77. if (id === undefined) {
  78. return true;
  79. }
  80. const subscriptionPlan = subscription.planDetails;
  81. const subscriptionPlanFeatures = subscriptionPlan?.features ?? [];
  82. const trialPlan = subscription.trialPlan
  83. ? billingConfig?.planList?.find(plan => plan.id === subscription.trialPlan)
  84. : undefined;
  85. const trialPlanFeatures = trialPlan?.features ?? [];
  86. const planFeatures = [...new Set([...subscriptionPlanFeatures, ...trialPlanFeatures])];
  87. const rule = new InsightsItemAccessRule(id, organization, planFeatures);
  88. return rule.hasRequiredFeatures;
  89. };
  90. function UpsellPage({
  91. defaultModule,
  92. subscription,
  93. fullPage,
  94. }: {
  95. defaultModule: TitleableModuleNames;
  96. fullPage: boolean;
  97. subscription: Subscription;
  98. }) {
  99. if (fullPage) {
  100. return (
  101. <FullPageContainer>
  102. <Content defaultModule={defaultModule} subscription={subscription} />
  103. </FullPageContainer>
  104. );
  105. }
  106. return (
  107. <Background>
  108. <StyledPanel>
  109. <ContentContainer>
  110. <Content defaultModule={defaultModule} subscription={subscription} />
  111. </ContentContainer>
  112. </StyledPanel>
  113. </Background>
  114. );
  115. }
  116. function Content({
  117. defaultModule,
  118. subscription,
  119. }: {
  120. defaultModule: TitleableModuleNames;
  121. subscription: Subscription;
  122. }) {
  123. const organization = useOrganization();
  124. const [selectedModule, setSelectedModule] =
  125. useState<TitleableModuleNames>(defaultModule);
  126. const modulePreviewContent = MODULE_PREVIEW_CONTENT[selectedModule];
  127. const checkoutUrl = normalizeUrl(
  128. `/settings/${organization.slug}/billing/checkout/?referrer=upsell-insights-${selectedModule}`
  129. );
  130. const source = 'insight-product-trial';
  131. const canTrial = subscription.canTrial;
  132. return (
  133. <Fragment>
  134. <PageLayout>
  135. <MainContent>
  136. <Title>{TITLE}</Title>
  137. {SUBTITLE}
  138. <SplitMainContent>
  139. <FeatureListContainer>
  140. <ModuleNameList
  141. selectedModule={selectedModule}
  142. subscription={subscription}
  143. onModuleNameClick={moduleName => setSelectedModule(moduleName)}
  144. />
  145. </FeatureListContainer>
  146. <ModulePreviewContainer>
  147. {modulePreviewContent?.description}
  148. {modulePreviewContent && (
  149. <PreviewImage src={modulePreviewContent.imageSrc} />
  150. )}
  151. </ModulePreviewContainer>
  152. </SplitMainContent>
  153. </MainContent>
  154. <Sidebar>
  155. <UpsellImage src={upsellImage} />
  156. <StyledSidebarFooter>
  157. <h1>{t('Current Plan')}</h1>
  158. <h2>{getFriendlyPlanName(subscription)}</h2>
  159. <a href="https://sentry.io/pricing" target="_blank" rel="noopener noreferrer">
  160. {t('Learn more and compare plans')}
  161. </a>
  162. </StyledSidebarFooter>
  163. </Sidebar>
  164. </PageLayout>
  165. <ButtonContainer>
  166. <UpgradeOrTrialButton
  167. subscription={subscription}
  168. priority="primary"
  169. organization={organization}
  170. source={source}
  171. aria-label="Start Trial"
  172. />
  173. {canTrial && <LinkButton to={checkoutUrl}>Upgrade Now</LinkButton>}
  174. {!canTrial && (
  175. <Button
  176. onClick={() =>
  177. openUpsellModal({
  178. organization,
  179. source,
  180. defaultSelection: 'insights-modules',
  181. })
  182. }
  183. >
  184. {t('Learn More')}
  185. </Button>
  186. )}
  187. </ButtonContainer>
  188. </Fragment>
  189. );
  190. }
  191. function ModuleNameList({
  192. selectedModule,
  193. subscription,
  194. onModuleNameClick,
  195. }: {
  196. onModuleNameClick: ModuleNameClickHandler;
  197. selectedModule: TitleableModuleNames;
  198. subscription: Subscription;
  199. }) {
  200. // TODO - it would be nice if this list was dynamic based on the sidebar items
  201. const commonProps = {selectedModule, subscription, onModuleNameClick};
  202. return (
  203. <FeatureList>
  204. <ModuleNameListItem moduleName="http" {...commonProps} />
  205. <ModuleNameListItem moduleName="db" {...commonProps} />
  206. <ModuleNameListItem moduleName="resource" {...commonProps} />
  207. <ModuleNameListItem moduleName="app_start" {...commonProps} />
  208. <ModuleNameListItem moduleName="screen_load" {...commonProps} />
  209. <ModuleNameListItem moduleName="vital" {...commonProps} />
  210. <ModuleNameListItem moduleName="cache" {...commonProps} />
  211. <ModuleNameListItem moduleName="queue" {...commonProps} />
  212. <ModuleNameListItem moduleName="ai" {...commonProps} />
  213. <ModuleNameListItem moduleName="screen-rendering" {...commonProps} />
  214. </FeatureList>
  215. );
  216. }
  217. function ModuleNameListItem({
  218. moduleName,
  219. selectedModule,
  220. subscription,
  221. onModuleNameClick,
  222. }: {
  223. moduleName: TitleableModuleNames;
  224. onModuleNameClick: ModuleNameClickHandler;
  225. selectedModule: TitleableModuleNames;
  226. subscription: Subscription;
  227. }) {
  228. const moduleTitle = MODULE_TITLES[moduleName];
  229. const hasRequiredFeatures = useHasRequiredInsightFeatures(moduleName, subscription);
  230. const isSelected = selectedModule === moduleName;
  231. const iconProps: SVGIconProps = {
  232. size: 'md',
  233. color: isSelected ? undefined : 'gray200',
  234. };
  235. return (
  236. <FeatureListItem
  237. isSelected={isSelected}
  238. onClick={() => onModuleNameClick(moduleName)}
  239. >
  240. {hasRequiredFeatures ? (
  241. <IconCheckmark {...iconProps} />
  242. ) : (
  243. <IconBusiness {...iconProps} />
  244. )}{' '}
  245. {moduleTitle}
  246. </FeatureListItem>
  247. );
  248. }
  249. const PageLayout = styled('div')`
  250. display: flex;
  251. align-items: stretch;
  252. gap: ${space(4)};
  253. padding-bottom: ${space(4)};
  254. `;
  255. const MainContent = styled('div')`
  256. flex: 5;
  257. `;
  258. const Title = styled('h2')`
  259. font-weight: ${p => p.theme.fontWeightNormal};
  260. margin-bottom: ${space(1)};
  261. `;
  262. const Sidebar = styled('div')`
  263. position: relative;
  264. flex: 3;
  265. `;
  266. const flexGap = space(2);
  267. const SplitMainContent = styled('div')`
  268. display: flex;
  269. border-radius: 10px;
  270. padding: ${space(4)};
  271. margin-top: ${space(2)};
  272. gap: ${flexGap};
  273. justify-content: space-between;
  274. background-color: ${p => p.theme.backgroundElevated};
  275. width: 100%;
  276. `;
  277. const FeatureListContainer = styled('div')`
  278. width: 100%;
  279. white-space: nowrap;
  280. flex: 1;
  281. `;
  282. const ModulePreviewContainer = styled('div')`
  283. border-left: 1px solid ${p => p.theme.border};
  284. padding-left: ${flexGap};
  285. `;
  286. const FeatureList = styled('ul')`
  287. display: flex;
  288. row-gap: ${space(1.5)};
  289. flex-direction: column;
  290. list-style-type: none;
  291. margin: 0;
  292. padding: 0;
  293. `;
  294. const FeatureListItem = styled('li')<{isSelected: boolean}>`
  295. display: flex;
  296. align-items: center;
  297. gap: ${space(2)};
  298. color: ${p => (p.isSelected ? p.theme.gray500 : p.theme.gray300)};
  299. ${p => p.isSelected && `font-weight: ${p.theme.fontWeightBold};`}
  300. cursor: pointer;
  301. :hover {
  302. color: ${p => p.theme.gray500};
  303. }
  304. `;
  305. const PreviewImage = styled('img')`
  306. max-width: 70%;
  307. display: block;
  308. margin: auto;
  309. `;
  310. const UpsellImage = styled('img')`
  311. width: 100%;
  312. `;
  313. const Background = styled('div')`
  314. background-color: ${p => p.theme.background};
  315. height: 100%;
  316. `;
  317. const ContentContainer = styled('div')`
  318. max-width: 1800px;
  319. margin: 0 auto;
  320. height: 100%;
  321. width: 100%;
  322. padding: ${space(4)};
  323. `;
  324. const StyledPanel = styled(Panel)`
  325. margin: ${space(3)} ${space(4)};
  326. `;
  327. const FullPageContainer = styled('div')`
  328. max-width: 1800px;
  329. margin: 0 auto;
  330. height: 100%;
  331. width: 100%;
  332. padding: 100px;
  333. `;
  334. const StyledSidebarFooter = styled(SidebarFooter)`
  335. position: absolute;
  336. border-left: 8px solid ${p => p.theme.border};
  337. padding-left: ${space(2)};
  338. bottom: 0;
  339. `;
  340. const ButtonContainer = styled('div')`
  341. display: flex;
  342. gap: ${space(1)};
  343. `;
  344. const MODULE_PREVIEW_CONTENT: Partial<
  345. Record<TitleableModuleNames, {description: string; imageSrc: any}>
  346. > = {
  347. app_start: {
  348. description: t('Improve the latency associated with your application starting up.'),
  349. imageSrc: appStartPreviewImg,
  350. },
  351. ai: {
  352. description: t(
  353. 'Get insights into critical metrics, like token usage, to monitor and fix issues with AI pipelines.'
  354. ),
  355. imageSrc: llmPreviewImg,
  356. },
  357. 'mobile-ui': {
  358. description: t(
  359. 'View the most active screens in your mobile application and monitor your releases for TTID and TTFD regressions.'
  360. ),
  361. imageSrc: screenLoadsPreviewImg,
  362. },
  363. cache: {
  364. description: t(
  365. 'Discover whether your application is utilizing caching effectively and understand the latency associated with cache misses.'
  366. ),
  367. imageSrc: cachesPreviewImg,
  368. },
  369. db: {
  370. description: t(
  371. 'Investigate the performance of database queries and get the information necessary to improve them.'
  372. ),
  373. imageSrc: queriesPreviewImg,
  374. },
  375. http: {
  376. description: t(
  377. 'Monitor outgoing HTTP requests and investigate errors and performance bottlenecks tied to domains.'
  378. ),
  379. imageSrc: requestPreviewImg,
  380. },
  381. resource: {
  382. description: t(
  383. 'Find large and slow-to-load resources used by your application and understand their impact on page performance.'
  384. ),
  385. imageSrc: assetsPreviewImg,
  386. },
  387. vital: {
  388. description: t(
  389. 'Get a set of metrics telling you the quality of user experience on a web page and see what needs improving.'
  390. ),
  391. imageSrc: webVitalsPreviewImg,
  392. },
  393. queue: {
  394. description: t(
  395. 'Understand the health and performance impact that queues have on your application and diagnose errors tied to jobs.'
  396. ),
  397. imageSrc: queuesPreviewImg,
  398. },
  399. screen_load: {
  400. description: t(
  401. 'View the most active screens in your mobile application and monitor your releases for TTID and TTFD regressions.'
  402. ),
  403. imageSrc: screenLoadsPreviewImg,
  404. },
  405. 'screen-rendering': {
  406. description: t(
  407. 'Screen Rendering identifies slow and frozen interactions, helping you find and fix problems that might cause users to complain, or uninstall.'
  408. ),
  409. imageSrc: screenRenderingPreviewImg,
  410. },
  411. };
  412. // This matches ids in the sidebar items and in the hook in getsentry
  413. const sidebarIdMap: Partial<Record<TitleableModuleNames, InsightSidebarId>> = {
  414. ai: 'llm-monitoring',
  415. 'mobile-ui': 'performance-mobile-ui',
  416. cache: 'performance-cache',
  417. db: 'performance-database',
  418. http: 'performance-http',
  419. resource: 'performance-browser-resources',
  420. screen_load: 'performance-mobile-screens',
  421. app_start: 'performance-mobile-app-startup',
  422. vital: 'performance-webvitals',
  423. queue: 'performance-queues',
  424. 'screen-rendering': 'performance-screen-rendering',
  425. };
  426. export default withOrganization(withSubscription(InsightsUpsellPage, {noLoader: true}));