import {Fragment, useEffect, useState} from 'react'; import styled from '@emotion/styled'; import startCase from 'lodash/startCase'; import {PlatformIcon} from 'platformicons'; import appStartPreviewImg from 'sentry-images/insights/module-upsells/insights-app-starts-module-charts.svg'; import assetsPreviewImg from 'sentry-images/insights/module-upsells/insights-assets-module-charts.svg'; import cachesPreviewImg from 'sentry-images/insights/module-upsells/insights-caches-module-charts.svg'; import llmPreviewImg from 'sentry-images/insights/module-upsells/insights-llm-module-charts.svg'; import queriesPreviewImg from 'sentry-images/insights/module-upsells/insights-queries-module-charts.svg'; import queuesPreviewImg from 'sentry-images/insights/module-upsells/insights-queues-module-charts.svg'; import requestPreviewImg from 'sentry-images/insights/module-upsells/insights-requests-module-charts.svg'; import screenLoadsPreviewImg from 'sentry-images/insights/module-upsells/insights-screen-loads-module-charts.svg'; import screenRenderingPreviewImg from 'sentry-images/insights/module-upsells/insights-screen-rendering-module-charts.svg'; import webVitalsPreviewImg from 'sentry-images/insights/module-upsells/insights-web-vitals-module-charts.svg'; import emptyStateImg from 'sentry-images/spot/performance-waiting-for-span.svg'; import {LinkButton} from 'sentry/components/button'; import Panel from 'sentry/components/panels/panel'; import {Tooltip} from 'sentry/components/tooltip'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {PlatformKey} from 'sentry/types/project'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout'; import type {TitleableModuleNames} from 'sentry/views/insights/common/components/modulePageProviders'; import {useHasFirstSpan} from 'sentry/views/insights/common/queries/useHasFirstSpan'; import {useOnboardingProject} from 'sentry/views/insights/common/queries/useOnboardingProject'; import { MODULE_DATA_TYPES, MODULE_DATA_TYPES_PLURAL, MODULE_PRODUCT_DOC_LINKS, MODULE_TITLES, } from 'sentry/views/insights/settings'; import {ModuleName} from 'sentry/views/insights/types'; import PerformanceOnboarding from 'sentry/views/performance/onboarding'; export function ModulesOnboarding({ children, moduleName, }: { children: React.ReactNode; moduleName: ModuleName; }) { const organization = useOrganization(); const onboardingProject = useOnboardingProject(); const {reloadProjects} = useProjects(); const hasData = useHasFirstSpan(moduleName); // Refetch the project metadata if the selected project does not have insights data, because // we may have received insight data (and subsequently updated `Project.hasInsightxx`) // after the initial project fetch. useEffect(() => { if (!hasData) { reloadProjects(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasData]); if (onboardingProject) { return ( ); } if (!hasData) { return ( ); } return children; } function ModulesOnboardingPanel({moduleName}: {moduleName: ModuleName}) { const emptyStateContent = EMPTY_STATE_CONTENT[moduleName]; return (
{emptyStateContent.heading}

{emptyStateContent.description}

{emptyStateContent.valuePropDescription}
    {emptyStateContent.valuePropPoints.map(point => (
  • {point}
  • ))}
{t('Read the docs')}
); } type ModulePreviewProps = {moduleName: ModuleName}; function ModulePreview({moduleName}: ModulePreviewProps) { const emptyStateContent = EMPTY_STATE_CONTENT[moduleName]; const [hoveredIcon, setHoveredIcon] = useState(null); return ( {emptyStateContent.supportedSdks && (
{t('Supported Today: ')}
{emptyStateContent.supportedSdks.map((sdk: PlatformKey) => ( setHoveredIcon(sdk)} onMouseOut={() => setHoveredIcon(null)} > ))}
)}
); } const Sidebar = styled('div')` position: relative; flex: 3; `; const PerfImage = styled('img')` max-width: 100%; min-width: 200px; `; const Container = styled('div')` position: relative; overflow: hidden; min-height: 160px; padding: ${space(4)}; `; const SplitMainContent = styled('div')` display: flex; align-items: stretch; flex-wrap: wrap-reverse; gap: ${space(4)}; `; const Header = styled('h3')` margin-bottom: ${space(1)}; `; const SplitContainer = styled(Panel)` display: flex; justify-content: center; overflow: hidden; `; const ModuleInfo = styled('div')` flex: 5; width: 100%; `; const ModulePreviewImage = styled('img')` max-width: 100%; display: block; margin: auto; margin-bottom: ${space(2)}; object-fit: contain; `; const ModulePreviewContainer = styled('div')` flex: 2; width: 100%; padding: ${space(3)}; background-color: ${p => p.theme.backgroundSecondary}; `; const SupportedSdkContainer = styled('div')` display: flex; flex-direction: column; gap: ${space(1)}; align-items: center; color: ${p => p.theme.gray300}; `; const SupportedSdkList = styled('div')` display: flex; flex-wrap: wrap; gap: ${space(0.5)}; justify-content: center; `; const SupportedSdkIconContainer = styled('div')` display: flex; justify-content: center; align-items: center; background-color: ${p => p.theme.gray100}; width: 42px; height: 42px; border-radius: 3px; &:hover { box-shadow: 0 0 0 1px ${p => p.theme.gray200}; } `; const ValueProp = styled('div')` flex: 1; padding: ${space(3)}; ul { margin-top: ${space(1)}; } `; type EmptyStateContent = { description: React.ReactNode; heading: React.ReactNode; imageSrc: any; valuePropDescription: React.ReactNode; valuePropPoints: React.ReactNode[]; supportedSdks?: PlatformKey[]; }; const EMPTY_STATE_CONTENT: Record = { app_start: { heading: t(`Don't lose your user's attention before your app loads`), description: tct( 'Monitor cold and warm [dataTypePlural] and track down the operations and releases contributing to regressions.', { dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.APP_START].toLocaleLowerCase(), } ), valuePropDescription: tct(`Mobile [dataType] insights give you visibility into:`, { dataType: MODULE_DATA_TYPES[ModuleName.APP_START], }), valuePropPoints: [ t('Application start duration broken down by release.'), t('Performance by device class.'), t('Real user performance metrics.'), ], imageSrc: appStartPreviewImg, supportedSdks: ['android', 'flutter', 'apple-ios', 'react-native'], }, ai: { heading: t('Find out what your LLM model is actually saying'), description: tct( 'Get insights into critical [dataType] metrics, like token usage, to monitor and fix issues with AI pipelines.', { dataType: MODULE_DATA_TYPES[ModuleName.AI], } ), valuePropDescription: tct( 'See what your [dataTypePlural] are doing in production by monitoring:', { dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.AI], } ), valuePropPoints: [ t('Token cost and usage per-provider and per-pipeline.'), tct('The inputs and outputs of [dataType] calls.', { dataType: MODULE_DATA_TYPES[ModuleName.AI], }), tct('Performance and timing information about [dataTypePlural] in production.', { dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.AI], }), ], imageSrc: llmPreviewImg, supportedSdks: ['python'], }, // Mobile UI is not released yet 'mobile-ui': { heading: t('TODO'), description: t('TODO'), valuePropDescription: t('Mobile UI load insights include:'), valuePropPoints: [], imageSrc: screenLoadsPreviewImg, }, // Mobile Screens is not released yet 'mobile-screens': { heading: t('Mobile Screens'), description: t('Explore mobile app metrics.'), valuePropDescription: '', valuePropPoints: [], imageSrc: screenLoadsPreviewImg, }, cache: { heading: t('Bringing you one less hard problem in computer science'), description: t( '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.' ), valuePropDescription: tct('[dataType] insights include:', { dataType: MODULE_DATA_TYPES[ModuleName.CACHE], }), valuePropPoints: [ t('Throughput of your cached endpoints.'), tct('Average [dataType] hit and miss duration.', { dataType: MODULE_DATA_TYPES[ModuleName.CACHE].toLocaleLowerCase(), }), t('Hit / miss ratio of keys accessed by your application.'), ], imageSrc: cachesPreviewImg, supportedSdks: ['python', 'javascript', 'php', 'java', 'ruby', 'dotnet'], }, db: { heading: tct( 'Fix the slow [dataTypePlural] you honestly intended to get back to later', {dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.DB].toLocaleLowerCase()} ), description: tct( 'Investigate the performance of database [dataTypePlural] and get the information necessary to improve them.', {dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.DB].toLocaleLowerCase()} ), valuePropDescription: tct('[dataType] insights give you visibility into:', { dataType: MODULE_DATA_TYPES[ModuleName.DB], }), valuePropPoints: [ tct('Slow [dataTypePlural].', { dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.DB].toLocaleLowerCase(), }), tct('High volume [dataTypePlural].', { dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.DB].toLocaleLowerCase(), }), t('One off slow queries, vs. trends'), ], imageSrc: queriesPreviewImg, }, http: { heading: t( 'Are your API dependencies working as well as their landing page promised? ' ), description: t( 'See the outbound HTTP requests being made to internal and external APIs, allowing you to understand trends in status codes, latency, and throughput.' ), valuePropDescription: tct('[dataType] insights give you visibility into:', { dataType: MODULE_DATA_TYPES[ModuleName.HTTP], }), valuePropPoints: [ t('Anomalies in status codes by domain.'), t('Request throughput by domain.'), t('Average duration of requests.'), ], imageSrc: requestPreviewImg, }, resource: { heading: t('Is your favorite animated gif worth the time it takes to load?'), description: tct( 'Find large and slow-to-load [dataTypePlurl] used by your application and understand their impact on page performance.', {dataTypePlurl: MODULE_DATA_TYPES_PLURAL[ModuleName.RESOURCE].toLocaleLowerCase()} ), valuePropDescription: tct('[dataType] insights give you visibility into:', { dataType: MODULE_DATA_TYPES[ModuleName.RESOURCE], }), valuePropPoints: [ tct('[dataType] performance broken down by category and domain.', { dataType: MODULE_DATA_TYPES[ModuleName.RESOURCE], }), tct('Whether [dataTypePlural] are blocking page rendering.', { dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.RESOURCE].toLocaleLowerCase(), }), tct('[dataType] size and whether it’s growing over time.', { dataType: MODULE_DATA_TYPES[ModuleName.RESOURCE], }), ], imageSrc: assetsPreviewImg, // 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 supportedSdks: [ 'javascript', 'javascript-angular', 'javascript-astro', 'javascript-ember', 'javascript-gatsby', 'javascript-nextjs', 'javascript-react', 'javascript-remix', 'javascript-solid', 'javascript-svelte', 'javascript-sveltekit', 'javascript-vue', ], }, vital: { heading: t('Finally answer, is this page slow for everyone or just me?'), description: t( 'Get industry standard metrics telling you the quality of user experience on a web page and see what needs improving.' ), valuePropDescription: tct('[dataType] insights give you visibility into:', { dataType: MODULE_DATA_TYPES[ModuleName.VITAL], }), valuePropPoints: [ t('Performance scores broken down by page.'), t('Performance metrics for individual operations that affect page performance.'), t('Drill down to real user sessions.'), ], imageSrc: webVitalsPreviewImg, }, queue: { heading: t('Ensure your background jobs aren’t being sent to /dev/null'), description: tct( 'Understand the health and performance impact that [dataTypePlural] have on your application and diagnose errors tied to jobs.', { dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.QUEUE].toLocaleLowerCase(), } ), valuePropDescription: tct('[dataType] insights give you visibility into:', { dataType: MODULE_DATA_TYPES[ModuleName.QUEUE], }), valuePropPoints: [ t('Metrics for how long jobs spend processing and waiting in queue.'), t('Job error rates and retry counts.'), t('Published vs., processed job volume.'), ], imageSrc: queuesPreviewImg, supportedSdks: ['python', 'javascript', 'php', 'java', 'ruby', 'dotnet'], }, screen_load: { heading: t(`Don’t lose your user's attention once your app loads`), description: tct( 'View the most active [dataTypePlural] in your mobile application and monitor your releases for screen load performance.', { dataTypePlural: MODULE_DATA_TYPES_PLURAL[ModuleName.SCREEN_LOAD].toLocaleLowerCase(), } ), valuePropDescription: tct('[dataType] insights include:', { dataType: MODULE_DATA_TYPES[ModuleName.SCREEN_LOAD], }), valuePropPoints: [ t('Compare metrics across releases, root causing performance degradations.'), t('See performance by device class.'), t('Drill down to real user sessions.'), ], imageSrc: screenLoadsPreviewImg, supportedSdks: ['android', 'flutter', 'apple-ios', 'react-native'], }, 'screen-rendering': { description: t( 'Screen Rendering identifies slow and frozen interactions, helping you find and fix problems that might cause users to complain, or uninstall.' ), heading: t('Fast-loading apps can still be janky'), imageSrc: screenRenderingPreviewImg, valuePropDescription: tct('With [moduleTitle]:', { moduleTitle: MODULE_TITLES[ModuleName.SCREEN_RENDERING], }), valuePropPoints: [ tct('Find and debug slow rendering interactions.', { dataType: MODULE_DATA_TYPES[ModuleName.SCREEN_RENDERING].toLowerCase(), }), t('Compare render performance between releases.'), tct('Correlate [dataType] performance with real-user metrics.', { dataType: MODULE_DATA_TYPES[ModuleName.SCREEN_RENDERING].toLowerCase(), }), ], supportedSdks: ['android', 'flutter', 'apple-ios', 'react-native'], }, };