modulesOnboarding.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import {Fragment, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import startCase from 'lodash/startCase';
  4. import {PlatformIcon} from 'platformicons';
  5. import appStartPreviewImg from 'sentry-images/insights/module-upsells/insights-app-starts-module-charts.svg';
  6. import assetsPreviewImg from 'sentry-images/insights/module-upsells/insights-assets-module-charts.svg';
  7. import cachesPreviewImg from 'sentry-images/insights/module-upsells/insights-caches-module-charts.svg';
  8. import llmPreviewImg from 'sentry-images/insights/module-upsells/insights-llm-module-charts.svg';
  9. import queriesPreviewImg from 'sentry-images/insights/module-upsells/insights-queries-module-charts.svg';
  10. import queuesPreviewImg from 'sentry-images/insights/module-upsells/insights-queues-module-charts.svg';
  11. import requestPreviewImg from 'sentry-images/insights/module-upsells/insights-requests-module-charts.svg';
  12. import screenLoadsPreviewImg from 'sentry-images/insights/module-upsells/insights-screen-loads-module-charts.svg';
  13. import webVitalsPreviewImg from 'sentry-images/insights/module-upsells/insights-web-vitals-module-charts.svg';
  14. import emptyStateImg from 'sentry-images/spot/performance-waiting-for-span.svg';
  15. import {LinkButton} from 'sentry/components/button';
  16. import Panel from 'sentry/components/panels/panel';
  17. import {Tooltip} from 'sentry/components/tooltip';
  18. import {t, tct} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import type {PlatformKey} from 'sentry/types/project';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. import useProjects from 'sentry/utils/useProjects';
  23. import * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout';
  24. import type {TitleableModuleNames} from 'sentry/views/insights/common/components/modulePageProviders';
  25. import {useHasFirstSpan} from 'sentry/views/insights/common/queries/useHasFirstSpan';
  26. import {useOnboardingProject} from 'sentry/views/insights/common/queries/useOnboardingProject';
  27. import {
  28. MODULE_DATA_TYPES,
  29. MODULE_DATA_TYPES_PLURAL,
  30. MODULE_PRODUCT_DOC_LINKS,
  31. } from 'sentry/views/insights/settings';
  32. import {ModuleName} from 'sentry/views/insights/types';
  33. import PerformanceOnboarding from 'sentry/views/performance/onboarding';
  34. export function ModulesOnboarding({
  35. children,
  36. moduleName,
  37. }: {
  38. children: React.ReactNode;
  39. moduleName: ModuleName;
  40. }) {
  41. const organization = useOrganization();
  42. const onboardingProject = useOnboardingProject();
  43. const {reloadProjects} = useProjects();
  44. const hasData = useHasFirstSpan(moduleName);
  45. // Refetch the project metadata if the selected project does not have insights data, because
  46. // we may have received insight data (and subsequently updated `Project.hasInsightxx`)
  47. // after the initial project fetch.
  48. useEffect(() => {
  49. if (!hasData) {
  50. reloadProjects();
  51. }
  52. // eslint-disable-next-line react-hooks/exhaustive-deps
  53. }, [hasData]);
  54. if (onboardingProject) {
  55. return (
  56. <ModuleLayout.Full>
  57. <PerformanceOnboarding organization={organization} project={onboardingProject} />
  58. </ModuleLayout.Full>
  59. );
  60. }
  61. if (!hasData) {
  62. return (
  63. <ModuleLayout.Full>
  64. <ModulesOnboardingPanel moduleName={moduleName} />
  65. </ModuleLayout.Full>
  66. );
  67. }
  68. return children;
  69. }
  70. function ModulesOnboardingPanel({moduleName}: {moduleName: ModuleName}) {
  71. const emptyStateContent = EMPTY_STATE_CONTENT[moduleName];
  72. return (
  73. <Panel>
  74. <Container>
  75. <SplitMainContent>
  76. <ModuleInfo>
  77. <Fragment>
  78. <Header>{emptyStateContent.heading}</Header>
  79. <p>{emptyStateContent.description}</p>
  80. </Fragment>
  81. <SplitContainer>
  82. <ModulePreview moduleName={moduleName} />
  83. <ValueProp>
  84. {emptyStateContent.valuePropDescription}
  85. <ul>
  86. {emptyStateContent.valuePropPoints.map(point => (
  87. <li key={point?.toString()}>{point}</li>
  88. ))}
  89. </ul>
  90. </ValueProp>
  91. </SplitContainer>
  92. </ModuleInfo>
  93. <Sidebar>
  94. <PerfImage src={emptyStateImg} />
  95. </Sidebar>
  96. </SplitMainContent>
  97. <LinkButton
  98. priority="primary"
  99. external
  100. href={MODULE_PRODUCT_DOC_LINKS[moduleName]}
  101. >
  102. {t('Read the docs')}
  103. </LinkButton>
  104. </Container>
  105. </Panel>
  106. );
  107. }
  108. type ModulePreviewProps = {moduleName: ModuleName};
  109. function ModulePreview({moduleName}: ModulePreviewProps) {
  110. const emptyStateContent = EMPTY_STATE_CONTENT[moduleName];
  111. const [hoveredIcon, setHoveredIcon] = useState<PlatformKey | null>(null);
  112. return (
  113. <ModulePreviewContainer>
  114. <ModulePreviewImage src={emptyStateContent.imageSrc} />
  115. {emptyStateContent.supportedSdks && (
  116. <SupportedSdkContainer>
  117. <div>{t('Supported Today: ')}</div>
  118. <SupportedSdkList>
  119. {emptyStateContent.supportedSdks.map((sdk: PlatformKey) => (
  120. <Tooltip title={startCase(sdk)} key={sdk} position="top">
  121. <SupportedSdkIconContainer
  122. onMouseOver={() => setHoveredIcon(sdk)}
  123. onMouseOut={() => setHoveredIcon(null)}
  124. >
  125. <PlatformIcon
  126. platform={sdk}
  127. size={hoveredIcon === sdk ? '30px' : '25px'}
  128. />
  129. </SupportedSdkIconContainer>
  130. </Tooltip>
  131. ))}
  132. </SupportedSdkList>
  133. </SupportedSdkContainer>
  134. )}
  135. </ModulePreviewContainer>
  136. );
  137. }
  138. const Sidebar = styled('div')`
  139. position: relative;
  140. flex: 3;
  141. `;
  142. const PerfImage = styled('img')`
  143. max-width: 100%;
  144. min-width: 200px;
  145. `;
  146. const Container = styled('div')`
  147. position: relative;
  148. overflow: hidden;
  149. min-height: 160px;
  150. padding: ${space(4)};
  151. `;
  152. const SplitMainContent = styled('div')`
  153. display: flex;
  154. align-items: stretch;
  155. flex-wrap: wrap-reverse;
  156. gap: ${space(4)};
  157. `;
  158. const Header = styled('h3')`
  159. margin-bottom: ${space(1)};
  160. `;
  161. const SplitContainer = styled(Panel)`
  162. display: flex;
  163. justify-content: center;
  164. `;
  165. const ModuleInfo = styled('div')`
  166. flex: 5;
  167. width: 100%;
  168. `;
  169. const ModulePreviewImage = styled('img')`
  170. max-width: 100%;
  171. display: block;
  172. margin: auto;
  173. margin-bottom: ${space(2)};
  174. object-fit: contain;
  175. `;
  176. const ModulePreviewContainer = styled('div')`
  177. flex: 2;
  178. width: 100%;
  179. padding: ${space(3)};
  180. background-color: ${p => p.theme.backgroundSecondary};
  181. `;
  182. const SupportedSdkContainer = styled('div')`
  183. display: flex;
  184. flex-direction: column;
  185. gap: ${space(1)};
  186. align-items: center;
  187. color: ${p => p.theme.gray300};
  188. `;
  189. const SupportedSdkList = styled('div')`
  190. display: flex;
  191. flex-wrap: wrap;
  192. gap: ${space(0.5)};
  193. justify-content: center;
  194. `;
  195. const SupportedSdkIconContainer = styled('div')`
  196. display: flex;
  197. justify-content: center;
  198. align-items: center;
  199. background-color: ${p => p.theme.gray100};
  200. width: 42px;
  201. height: 42px;
  202. border-radius: 3px;
  203. &:hover {
  204. box-shadow: 0 0 0 1px ${p => p.theme.gray200};
  205. }
  206. `;
  207. const ValueProp = styled('div')`
  208. flex: 1;
  209. padding: ${space(3)};
  210. ul {
  211. margin-top: ${space(1)};
  212. }
  213. `;
  214. type EmptyStateContent = {
  215. description: React.ReactNode;
  216. heading: React.ReactNode;
  217. imageSrc: any;
  218. valuePropDescription: React.ReactNode;
  219. valuePropPoints: React.ReactNode[];
  220. supportedSdks?: PlatformKey[];
  221. };
  222. const EMPTY_STATE_CONTENT: Record<TitleableModuleNames, EmptyStateContent> = {
  223. app_start: {
  224. heading: t(`Don't lose your user's attention before your app loads`),
  225. description: tct(
  226. 'Monitor cold and warm [dataTypePlural] and track down the operations and releases contributing to regressions.',
  227. {
  228. dataTypePlural:
  229. MODULE_DATA_TYPES_PLURAL[ModuleName.APP_START].toLocaleLowerCase(),
  230. }
  231. ),
  232. valuePropDescription: tct(`Mobile [dataType] insights give you visibility into:`, {
  233. dataType: MODULE_DATA_TYPES[ModuleName.APP_START],
  234. }),
  235. valuePropPoints: [
  236. t('Application start duration broken down by release.'),
  237. t('Performance by device class.'),
  238. t('Real user performance metrics.'),
  239. ],
  240. imageSrc: appStartPreviewImg,
  241. supportedSdks: ['android', 'flutter', 'apple-ios', 'react-native'],
  242. },
  243. ai: {
  244. heading: t('Find out what your LLM model is actually saying'),
  245. description: tct(
  246. 'Get insights into critical [dataType] metrics, like token usage, to monitor and fix issues with AI pipelines.',
  247. {
  248. dataType: MODULE_DATA_TYPES[ModuleName.AI],
  249. }
  250. ),
  251. valuePropDescription: tct(
  252. 'See what your [dataTypePlural] are doing in production by monitoring:',
  253. {
  254. dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.AI],
  255. }
  256. ),
  257. valuePropPoints: [
  258. t('Token cost and usage per-provider and per-pipeline.'),
  259. tct('The inputs and outputs of [dataType] calls.', {
  260. dataType: MODULE_DATA_TYPES[ModuleName.AI],
  261. }),
  262. tct('Performance and timing information about [dataTypePlural] in production.', {
  263. dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.AI],
  264. }),
  265. ],
  266. imageSrc: llmPreviewImg,
  267. supportedSdks: ['python'],
  268. },
  269. // Mobile UI is not released yet
  270. 'mobile-ui': {
  271. heading: t('TODO'),
  272. description: t('TODO'),
  273. valuePropDescription: t('Mobile UI load insights include:'),
  274. valuePropPoints: [],
  275. imageSrc: screenLoadsPreviewImg,
  276. },
  277. // Mobile Screens is not released yet
  278. 'mobile-screens': {
  279. heading: t('Mobile Screens'),
  280. description: t('Explore mobile app metrics.'),
  281. valuePropDescription: '',
  282. valuePropPoints: [],
  283. imageSrc: screenLoadsPreviewImg,
  284. },
  285. cache: {
  286. heading: t('Bringing you one less hard problem in computer science'),
  287. description: t(
  288. 'We’ll tell you if the parts of your application that interact with caches are hitting cache as often as intended, and whether caching is providing the performance improvements expected.'
  289. ),
  290. valuePropDescription: tct('[dataType] insights include:', {
  291. dataType: MODULE_DATA_TYPES[ModuleName.CACHE],
  292. }),
  293. valuePropPoints: [
  294. t('Throughput of your cached endpoints.'),
  295. tct('Average [dataType] hit and miss duration.', {
  296. dataType: MODULE_DATA_TYPES[ModuleName.CACHE].toLocaleLowerCase(),
  297. }),
  298. t('Hit / miss ratio of keys accessed by your application.'),
  299. ],
  300. imageSrc: cachesPreviewImg,
  301. supportedSdks: ['python', 'javascript', 'php', 'java', 'ruby', 'dotnet'],
  302. },
  303. db: {
  304. heading: tct(
  305. 'Fix the slow [dataTypePlural] you honestly intended to get back to later',
  306. {dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.DB].toLocaleLowerCase()}
  307. ),
  308. description: tct(
  309. 'Investigate the performance of database [dataTypePlural] and get the information necessary to improve them.',
  310. {dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.DB].toLocaleLowerCase()}
  311. ),
  312. valuePropDescription: tct('[dataType] insights give you visibility into:', {
  313. dataType: MODULE_DATA_TYPES[ModuleName.DB],
  314. }),
  315. valuePropPoints: [
  316. tct('Slow [dataTypePlural].', {
  317. dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.DB].toLocaleLowerCase(),
  318. }),
  319. tct('High volume [dataTypePlural].', {
  320. dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.DB].toLocaleLowerCase(),
  321. }),
  322. t('One off slow queries, vs. trends'),
  323. ],
  324. imageSrc: queriesPreviewImg,
  325. },
  326. http: {
  327. heading: t(
  328. 'Are your API dependencies working as well as their landing page promised? '
  329. ),
  330. description: t(
  331. 'See the outbound HTTP requests being made to internal and external APIs, allowing you to understand trends in status codes, latency, and throughput.'
  332. ),
  333. valuePropDescription: tct('[dataType] insights give you visibility into:', {
  334. dataType: MODULE_DATA_TYPES[ModuleName.HTTP],
  335. }),
  336. valuePropPoints: [
  337. t('Anomalies in status codes by domain.'),
  338. t('Request throughput by domain.'),
  339. t('Average duration of requests.'),
  340. ],
  341. imageSrc: requestPreviewImg,
  342. },
  343. resource: {
  344. heading: t('Is your favourite animated gif worth the time it takes to load?'),
  345. description: tct(
  346. 'Find large and slow-to-load [dataTypePlurl] used by your application and understand their impact on page performance.',
  347. {dataTypePlurl: MODULE_DATA_TYPES_PLURAL[ModuleName.RESOURCE].toLocaleLowerCase()}
  348. ),
  349. valuePropDescription: tct('[dataType] insights give you visibility into:', {
  350. dataType: MODULE_DATA_TYPES[ModuleName.RESOURCE],
  351. }),
  352. valuePropPoints: [
  353. tct('[dataType] performance broken down by category and domain.', {
  354. dataType: MODULE_DATA_TYPES[ModuleName.RESOURCE],
  355. }),
  356. tct('Whether [dataTypePlural] are blocking page rendering.', {
  357. dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.RESOURCE].toLocaleLowerCase(),
  358. }),
  359. tct('[dataType] size and whether it’s growing over time.', {
  360. dataType: MODULE_DATA_TYPES[ModuleName.RESOURCE],
  361. }),
  362. ],
  363. imageSrc: assetsPreviewImg,
  364. // TODO - this is a lot of manual work, and its duplicated between here and our docs, it would great if there's a single source of truth
  365. supportedSdks: [
  366. 'javascript',
  367. 'javascript-angular',
  368. 'javascript-astro',
  369. 'javascript-ember',
  370. 'javascript-gatsby',
  371. 'javascript-nextjs',
  372. 'javascript-react',
  373. 'javascript-remix',
  374. 'javascript-solid',
  375. 'javascript-svelte',
  376. 'javascript-sveltekit',
  377. 'javascript-vue',
  378. ],
  379. },
  380. vital: {
  381. heading: t('Finally answer, is this page slow for everyone or just me?'),
  382. description: t(
  383. 'Get industry standard metrics telling you the quality of user experience on a web page and see what needs improving.'
  384. ),
  385. valuePropDescription: tct('[dataType] insights give you visibility into:', {
  386. dataType: MODULE_DATA_TYPES[ModuleName.VITAL],
  387. }),
  388. valuePropPoints: [
  389. t('Performance scores broken down by page.'),
  390. t('Performance metrics for individual operations that affect page performance.'),
  391. t('Drill down to real user sessions.'),
  392. ],
  393. imageSrc: webVitalsPreviewImg,
  394. },
  395. queue: {
  396. heading: t('Ensure your background jobs aren’t being sent to /dev/null'),
  397. description: tct(
  398. 'Understand the health and performance impact that [dataTypePlural] have on your application and diagnose errors tied to jobs.',
  399. {
  400. dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.QUEUE].toLocaleLowerCase(),
  401. }
  402. ),
  403. valuePropDescription: tct('[dataType] insights give you visibility into:', {
  404. dataType: MODULE_DATA_TYPES[ModuleName.QUEUE],
  405. }),
  406. valuePropPoints: [
  407. t('Metrics for how long jobs spend processing and waiting in queue.'),
  408. t('Job error rates and retry counts.'),
  409. t('Published vs., processed job volume.'),
  410. ],
  411. imageSrc: queuesPreviewImg,
  412. supportedSdks: ['python', 'javascript', 'php', 'java', 'ruby', 'dotnet'],
  413. },
  414. screen_load: {
  415. heading: t(`Don’t lose your user's attention once your app loads`),
  416. description: tct(
  417. 'View the most active [dataTypePlural] in your mobile application and monitor your releases for screen load performance.',
  418. {
  419. dataTypePlural:
  420. MODULE_DATA_TYPES_PLURAL[ModuleName.SCREEN_LOAD].toLocaleLowerCase(),
  421. }
  422. ),
  423. valuePropDescription: tct('[dataType] insights include:', {
  424. dataType: MODULE_DATA_TYPES[ModuleName.SCREEN_LOAD],
  425. }),
  426. valuePropPoints: [
  427. t('Compare metrics across releases, root causing performance degradations.'),
  428. t('See performance by device class.'),
  429. t('Drill down to real user sessions.'),
  430. ],
  431. imageSrc: screenLoadsPreviewImg,
  432. supportedSdks: ['android', 'flutter', 'apple-ios', 'react-native'],
  433. },
  434. };