import {Fragment, useState} from 'react';
import styled from '@emotion/styled';
import upsellImage from 'getsentry-images/features/insights/module-upsells/insights-module-upsell.svg';
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 {Button, LinkButton} from 'sentry/components/button';
import Panel from 'sentry/components/panels/panel';
import {IconBusiness, IconCheckmark} from 'sentry/icons';
import type {SVGIconProps} from 'sentry/icons/svgIcon';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Organization} from 'sentry/types/organization';
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
import useOrganization from 'sentry/utils/useOrganization';
import withOrganization from 'sentry/utils/withOrganization';
import type {TitleableModuleNames} from 'sentry/views/insights/common/components/modulePageProviders';
import {MODULE_TITLES} from 'sentry/views/insights/settings';
import {openUpsellModal} from 'getsentry/actionCreators/modal';
import {
type InsightSidebarId,
InsightsItemAccessRule,
} from 'getsentry/components/sidebarNavigationItem';
import UpgradeOrTrialButton from 'getsentry/components/upgradeOrTrialButton';
import {SidebarFooter} from 'getsentry/components/upsellModal/footer';
import withSubscription from 'getsentry/components/withSubscription';
import {useBillingConfig} from 'getsentry/hooks/useBillingConfig';
import type {Subscription} from 'getsentry/types';
import {getFriendlyPlanName} from 'getsentry/utils/billing';
const SUBTITLE = t(
'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.'
);
const TITLE = t('Find out why your application is mad at you');
interface Props {
children: React.ReactNode;
moduleName: TitleableModuleNames;
organization: Organization;
subscription: Subscription;
fullPage?: boolean; // This prop is temporary while we transition to domain views for performance
}
type ModuleNameClickHandler = (module: TitleableModuleNames) => void;
/** @internal exported for tests only */
export function InsightsUpsellPage({
moduleName,
fullPage,
subscription,
children,
}: Props) {
const hasRequiredFeatures = useHasRequiredInsightFeatures(moduleName, subscription);
if (hasRequiredFeatures) {
return children;
}
return (
);
}
const useHasRequiredInsightFeatures = (
moduleName: TitleableModuleNames,
subscription: Subscription
) => {
const id = sidebarIdMap[moduleName];
const organization = useOrganization();
const {data: billingConfig} = useBillingConfig({organization, subscription});
// if there's no sidebar id mapping, the module isn't bound to any feature,
// so let's just show it
if (id === undefined) {
return true;
}
const subscriptionPlan = subscription.planDetails;
const subscriptionPlanFeatures = subscriptionPlan?.features ?? [];
const trialPlan = subscription.trialPlan
? billingConfig?.planList?.find(plan => plan.id === subscription.trialPlan)
: undefined;
const trialPlanFeatures = trialPlan?.features ?? [];
const planFeatures = [...new Set([...subscriptionPlanFeatures, ...trialPlanFeatures])];
const rule = new InsightsItemAccessRule(id, organization, planFeatures);
return rule.hasRequiredFeatures;
};
function UpsellPage({
defaultModule,
subscription,
fullPage,
}: {
defaultModule: TitleableModuleNames;
fullPage: boolean;
subscription: Subscription;
}) {
if (fullPage) {
return (
);
}
return (
);
}
function Content({
defaultModule,
subscription,
}: {
defaultModule: TitleableModuleNames;
subscription: Subscription;
}) {
const organization = useOrganization();
const [selectedModule, setSelectedModule] =
useState(defaultModule);
const modulePreviewContent = MODULE_PREVIEW_CONTENT[selectedModule];
const checkoutUrl = normalizeUrl(
`/settings/${organization.slug}/billing/checkout/?referrer=upsell-insights-${selectedModule}`
);
const source = 'insight-product-trial';
const canTrial = subscription.canTrial;
return (
{TITLE}
{SUBTITLE}
setSelectedModule(moduleName)}
/>
{modulePreviewContent?.description}
{modulePreviewContent && (
)}
{t('Current Plan')}
{getFriendlyPlanName(subscription)}
{t('Learn more and compare plans')}
{canTrial && Upgrade Now}
{!canTrial && (
)}
);
}
function ModuleNameList({
selectedModule,
subscription,
onModuleNameClick,
}: {
onModuleNameClick: ModuleNameClickHandler;
selectedModule: TitleableModuleNames;
subscription: Subscription;
}) {
// TODO - it would be nice if this list was dynamic based on the sidebar items
const commonProps = {selectedModule, subscription, onModuleNameClick};
return (
);
}
function ModuleNameListItem({
moduleName,
selectedModule,
subscription,
onModuleNameClick,
}: {
moduleName: TitleableModuleNames;
onModuleNameClick: ModuleNameClickHandler;
selectedModule: TitleableModuleNames;
subscription: Subscription;
}) {
const moduleTitle = MODULE_TITLES[moduleName];
const hasRequiredFeatures = useHasRequiredInsightFeatures(moduleName, subscription);
const isSelected = selectedModule === moduleName;
const iconProps: SVGIconProps = {
size: 'md',
color: isSelected ? undefined : 'gray200',
};
return (
onModuleNameClick(moduleName)}
>
{hasRequiredFeatures ? (
) : (
)}{' '}
{moduleTitle}
);
}
const PageLayout = styled('div')`
display: flex;
align-items: stretch;
gap: ${space(4)};
padding-bottom: ${space(4)};
`;
const MainContent = styled('div')`
flex: 5;
`;
const Title = styled('h2')`
font-weight: ${p => p.theme.fontWeightNormal};
margin-bottom: ${space(1)};
`;
const Sidebar = styled('div')`
position: relative;
flex: 3;
`;
const flexGap = space(2);
const SplitMainContent = styled('div')`
display: flex;
border-radius: 10px;
padding: ${space(4)};
margin-top: ${space(2)};
gap: ${flexGap};
justify-content: space-between;
background-color: ${p => p.theme.backgroundElevated};
width: 100%;
`;
const FeatureListContainer = styled('div')`
width: 100%;
white-space: nowrap;
flex: 1;
`;
const ModulePreviewContainer = styled('div')`
border-left: 1px solid ${p => p.theme.border};
padding-left: ${flexGap};
`;
const FeatureList = styled('ul')`
display: flex;
row-gap: ${space(1.5)};
flex-direction: column;
list-style-type: none;
margin: 0;
padding: 0;
`;
const FeatureListItem = styled('li')<{isSelected: boolean}>`
display: flex;
align-items: center;
gap: ${space(2)};
color: ${p => (p.isSelected ? p.theme.gray500 : p.theme.gray300)};
${p => p.isSelected && `font-weight: ${p.theme.fontWeightBold};`}
cursor: pointer;
:hover {
color: ${p => p.theme.gray500};
}
`;
const PreviewImage = styled('img')`
max-width: 70%;
display: block;
margin: auto;
`;
const UpsellImage = styled('img')`
width: 100%;
`;
const Background = styled('div')`
background-color: ${p => p.theme.background};
height: 100%;
`;
const ContentContainer = styled('div')`
max-width: 1800px;
margin: 0 auto;
height: 100%;
width: 100%;
padding: ${space(4)};
`;
const StyledPanel = styled(Panel)`
margin: ${space(3)} ${space(4)};
`;
const FullPageContainer = styled('div')`
max-width: 1800px;
margin: 0 auto;
height: 100%;
width: 100%;
padding: 100px;
`;
const StyledSidebarFooter = styled(SidebarFooter)`
position: absolute;
border-left: 8px solid ${p => p.theme.border};
padding-left: ${space(2)};
bottom: 0;
`;
const ButtonContainer = styled('div')`
display: flex;
gap: ${space(1)};
`;
const MODULE_PREVIEW_CONTENT: Partial<
Record
> = {
app_start: {
description: t('Improve the latency associated with your application starting up.'),
imageSrc: appStartPreviewImg,
},
ai: {
description: t(
'Get insights into critical metrics, like token usage, to monitor and fix issues with AI pipelines.'
),
imageSrc: llmPreviewImg,
},
'mobile-ui': {
description: t(
'View the most active screens in your mobile application and monitor your releases for TTID and TTFD regressions.'
),
imageSrc: screenLoadsPreviewImg,
},
cache: {
description: t(
'Discover whether your application is utilizing caching effectively and understand the latency associated with cache misses.'
),
imageSrc: cachesPreviewImg,
},
db: {
description: t(
'Investigate the performance of database queries and get the information necessary to improve them.'
),
imageSrc: queriesPreviewImg,
},
http: {
description: t(
'Monitor outgoing HTTP requests and investigate errors and performance bottlenecks tied to domains.'
),
imageSrc: requestPreviewImg,
},
resource: {
description: t(
'Find large and slow-to-load resources used by your application and understand their impact on page performance.'
),
imageSrc: assetsPreviewImg,
},
vital: {
description: t(
'Get a set of metrics telling you the quality of user experience on a web page and see what needs improving.'
),
imageSrc: webVitalsPreviewImg,
},
queue: {
description: t(
'Understand the health and performance impact that queues have on your application and diagnose errors tied to jobs.'
),
imageSrc: queuesPreviewImg,
},
screen_load: {
description: t(
'View the most active screens in your mobile application and monitor your releases for TTID and TTFD regressions.'
),
imageSrc: screenLoadsPreviewImg,
},
'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.'
),
imageSrc: screenRenderingPreviewImg,
},
};
// This matches ids in the sidebar items and in the hook in getsentry
const sidebarIdMap: Partial> = {
ai: 'llm-monitoring',
'mobile-ui': 'performance-mobile-ui',
cache: 'performance-cache',
db: 'performance-database',
http: 'performance-http',
resource: 'performance-browser-resources',
screen_load: 'performance-mobile-screens',
app_start: 'performance-mobile-app-startup',
vital: 'performance-webvitals',
queue: 'performance-queues',
'screen-rendering': 'performance-screen-rendering',
};
export default withOrganization(withSubscription(InsightsUpsellPage, {noLoader: true}));