Skip to content

feat(profiling): Add banner to set budget for profile hours #91661

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions static/app/components/profiling/billing/alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export const ContinuousProfilingBetaAlertBanner = HookOrDefault({
export const ContinuousProfilingBetaSDKAlertBanner = HookOrDefault({
hookName: 'component:continuous-profiling-beta-sdk-banner',
});

export const ContinuousProfilingNoQuotaAlertBanner = HookOrDefault({
hookName: 'component:continuous-profiling-no-quota-banner',
});
5 changes: 5 additions & 0 deletions static/app/types/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ type ContinuousProfilingBetaAlertBannerProps = {
organization: Organization;
};

type ContinuousProfilingNoQuotaBannerProps = {
organization: Organization;
};

type CronsBillingBannerProps = {
organization: Organization;
};
Expand Down Expand Up @@ -162,6 +166,7 @@ type ComponentHooks = {
'component:confirm-account-close': () => React.ComponentType<AttemptCloseAttemptProps>;
'component:continuous-profiling-beta-banner': () => React.ComponentType<ContinuousProfilingBetaAlertBannerProps>;
'component:continuous-profiling-beta-sdk-banner': () => React.ComponentType;
'component:continuous-profiling-no-quota-banner': () => React.ComponentType<ContinuousProfilingNoQuotaBannerProps>;
'component:crons-list-page-header': () => React.ComponentType<CronsBillingBannerProps>;
'component:crons-onboarding-panel': () => React.ComponentType<CronsOnboardingPanelProps>;
'component:dashboards-header': () => React.ComponentType<DashboardHeadersProps>;
Expand Down
2 changes: 2 additions & 0 deletions static/app/views/profiling/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {TransactionSearchQueryBuilder} from 'sentry/components/performance/trans
import {
ContinuousProfilingBetaAlertBanner,
ContinuousProfilingBetaSDKAlertBanner,
ContinuousProfilingNoQuotaAlertBanner,
ProfilingBetaAlertBanner,
} from 'sentry/components/profiling/billing/alerts';
import {ProfileEventsTable} from 'sentry/components/profiling/profileEventsTable';
Expand Down Expand Up @@ -146,6 +147,7 @@ export default function ProfilingContent({location}: ProfilingContentProps) {
<Feature features="continuous-profiling-beta-ui">
<ContinuousProfilingBetaAlertBanner organization={organization} />
<ContinuousProfilingBetaSDKAlertBanner />
<ContinuousProfilingNoQuotaAlertBanner organization={organization} />
</Feature>
<ProfilingContentPageHeader />
<LayoutBody>
Expand Down
168 changes: 166 additions & 2 deletions static/gsApp/components/profiling/alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import {openAM2ProfilingUpsellModal} from 'getsentry/actionCreators/modal';
import AddEventsCTA, {type EventType} from 'getsentry/components/addEventsCTA';
import withSubscription from 'getsentry/components/withSubscription';
import type {Subscription} from 'getsentry/types';
import {PlanTier} from 'getsentry/types';
import {isAm2Plan, isEnterprise} from 'getsentry/utils/billing';
import {OnDemandBudgetMode, PlanTier} from 'getsentry/types';
import {isAm2Plan, isAm3Plan, isEnterprise} from 'getsentry/utils/billing';
import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';

export function makeLinkToOwnersAndBillingMembers(
Expand Down Expand Up @@ -372,6 +372,170 @@ export function ContinuousProfilingBetaSDKAlertBanner() {
);
}

interface ContinuousProfilingNoQuotaAlertBannerProps {
organization: Organization;
subscription: Subscription;
}

function ContinuousProfilingNoQuotaAlertAm2Banner({
organization,
subscription,
}: ContinuousProfilingNoQuotaAlertBannerProps) {
const hasBudget = hasBudgetConfiguredForContinuousOrUiProfilesHours(subscription);

// As long as they have a budget set, don't show this banner.
if (hasBudget) {
return null;
}

// For AM2 plans, if they haven't tried to send any continuous/ui profiles, don't
// show this banner because they can still use transaction profiles.
if (
!subscription.categories.profileDuration?.usageExceeded &&
!subscription.categories.profileDurationUI?.usageExceeded
Comment on lines +394 to +395
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe customers without PAYG that aren't using transaction profiles will fall under this condition too since having usage is not exceeded for profile hour categories unless they have PAYG set up and it's all used up

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure i understand what you're saying here.

) {
return null;
}

const eventTypes: EventType[] = [
DataCategoryExact.PROFILE_DURATION,
DataCategoryExact.PROFILE_DURATION_UI,
];

return (
<Alert.Container>
<Alert
system
type="warning"
showIcon
trailingItems={
<AddEventsCTA
organization={organization}
subscription={subscription}
buttonProps={{
priority: 'default',
size: 'xs',
style: {marginBlock: `-${space(0.25)}`},
}}
eventTypes={eventTypes}
notificationType="overage_critical"
referrer={`no-profiling-quota-${eventTypes.join('-')}`}
source="continuous-profiling-no-quota-banner"
/>
}
>
{t(
'Continuous Profiling and UI Profiling requires you to set your On-demand budget.'
)}
Comment on lines +427 to +429
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{t(
'Continuous Profiling and UI Profiling requires you to set your On-demand budget.'
)}
{tct(
'Continuous Profiling and UI Profiling requires you to set your [budgetTerm] budget.',
{budgetTerm: subscription.planDetails.budgetTerm}
)}

if you use this looks like you won't need to split into the different tier banners? not sure if there were any other changes

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im tempted to say let's leave it as separate components as this feels like something that we're likely to iterate on a few times before getting it right and the differences between AM2 and AM3 plans makes it annoying to handle if we have to litter isAm2Plan and isAm3Plan checks everywhere.

</Alert>
</Alert.Container>
);
}

function ContinuousProfilingNoQuotaAlertAm3Banner({
organization,
subscription,
}: ContinuousProfilingNoQuotaAlertBannerProps) {
const hasBudget = hasBudgetConfiguredForContinuousOrUiProfilesHours(subscription);

// As long as they have a budget set, don't show this banner.
if (hasBudget) {
return null;
}

const eventTypes: EventType[] = [
DataCategoryExact.PROFILE_DURATION,
DataCategoryExact.PROFILE_DURATION_UI,
];

return (
<Alert.Container>
<Alert
system
type="warning"
showIcon
trailingItems={
<AddEventsCTA
organization={organization}
subscription={subscription}
buttonProps={{
priority: 'default',
size: 'xs',
style: {marginBlock: `-${space(0.25)}`},
}}
eventTypes={eventTypes}
referrer={`no-profiling-quota-${eventTypes.join('-')}`}
source="continuous-profiling-no-quota-banner"
/>
}
>
{t(
'Continuous Profiling and UI Profiling requires you to set your Pay-as-you-go budget.'
)}
</Alert>
</Alert.Container>
);
}

function hasBudgetConfiguredForContinuousOrUiProfilesHours(
subscription: Subscription
): boolean {
if (
subscription.categories.profileDuration?.reserved ||
subscription.categories.profileDurationUI?.reserved
) {
return true;
}

if (subscription.onDemandBudgets) {
if (subscription.onDemandBudgets.budgetMode === OnDemandBudgetMode.SHARED) {
if (subscription.onDemandBudgets.sharedMaxBudget) {
return true;
}
} else if (
subscription.onDemandBudgets.budgetMode === OnDemandBudgetMode.PER_CATEGORY
) {
if (
subscription.onDemandBudgets.profileDurationBudget ||
subscription.onDemandBudgets.profileDurationUIBudget
) {
return true;
}
}
}

return false;
}

function ContinuousProfilingNoQuotaAlertBannerComponent({
organization,
subscription,
}: ContinuousProfilingNoQuotaAlertBannerProps) {
if (isAm2Plan(subscription.plan)) {
return (
<ContinuousProfilingNoQuotaAlertAm2Banner
organization={organization}
subscription={subscription}
/>
);
}

if (isAm3Plan(subscription.plan)) {
return (
<ContinuousProfilingNoQuotaAlertAm3Banner
organization={organization}
subscription={subscription}
/>
);
}

return null;
}

export const ContinuousProfilingNoQuotaAlertBanner = withSubscription(
ContinuousProfilingNoQuotaAlertBannerComponent
);

interface SDKDeprecation {
minimumVersion: string;
projectId: string;
Expand Down
3 changes: 3 additions & 0 deletions static/gsApp/registerHooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import OpenInDiscoverBtn from './components/openInDiscoverBtn';
import {
ContinuousProfilingBetaAlertBanner,
ContinuousProfilingBetaSDKAlertBanner,
ContinuousProfilingNoQuotaAlertBanner,
ProfilingBetaAlertBanner,
} from './components/profiling/alerts';
import ReplayOnboardingAlert from './components/replayOnboardingAlert';
Expand Down Expand Up @@ -208,6 +209,8 @@ const GETSENTRY_HOOKS: Partial<Hooks> = {
'component:continuous-profiling-beta-banner': () => ContinuousProfilingBetaAlertBanner,
'component:continuous-profiling-beta-sdk-banner': () =>
ContinuousProfilingBetaSDKAlertBanner,
'component:continuous-profiling-no-quota-banner': () =>
ContinuousProfilingNoQuotaAlertBanner,
'component:explore-date-range-query-limit-footer': () =>
ExploreDateRangeQueryLimitFooter,
/**
Expand Down
Loading