modulesOnboarding.tsx 15 KB

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