Skip to content

fix(reserved budgets): Better formatting for pending changes #91688

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

Merged
merged 12 commits into from
May 16, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,7 @@ describe('CustomerOverview', function () {
/>
);

expect(
screen.getByText('Accepted Spans and Stored Spans Reserved Budget')
).toBeInTheDocument();
expect(screen.getByText('Spans Budget')).toBeInTheDocument();
expect(screen.getByText('Reserved Budget:')).toBeInTheDocument();
expect(screen.getByText('$99,000.00')).toBeInTheDocument();
expect(screen.getByText('Gifted Budget:')).toBeInTheDocument();
Expand Down
30 changes: 11 additions & 19 deletions static/gsAdmin/components/customers/customerOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,6 @@ type ReservedDataProps = {
customer: Subscription;
};

type ReservedBudgetProps = {
customer: Subscription;
reservedBudget: ReservedBudget;
};

function ReservedData({customer}: ReservedDataProps) {
const reservedBudgetMetricHistories: Record<string, ReservedBudgetMetricHistory> = {};
customer.reservedBudgets?.forEach(budget => {
Expand Down Expand Up @@ -254,26 +249,23 @@ function ReservedBudgetsData({customer}: ReservedDataProps) {
);
}

function ReservedBudgetData({customer, reservedBudget}: ReservedBudgetProps) {
const categories = Object.keys(reservedBudget.categories);
if (categories.length === 0) {
return null;
}

const shouldUseDsNames = customer.planDetails.categories.includes(
DataCategory.SPANS_INDEXED
);

function ReservedBudgetData({
customer,
reservedBudget,
}: {
customer: Subscription;
reservedBudget: ReservedBudget;
}) {
const budgetName = getReservedBudgetDisplayName({
plan: customer.planDetails,
categories: categories as DataCategory[],
hadCustomDynamicSampling: shouldUseDsNames,
reservedBudget,
shouldTitleCase: true,
plan: customer.planDetails,
hadCustomDynamicSampling: customer.hadCustomDynamicSampling,
});

return (
<Fragment>
<h6>{budgetName} Reserved Budget</h6>
<h6>{budgetName}</h6>
<DetailList>
<DetailLabel title="Reserved Budget">
{displayPriceWithCents({cents: reservedBudget.reservedBudget})}
Expand Down
6 changes: 3 additions & 3 deletions static/gsAdmin/components/customers/pendingChanges.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ describe('PendingChanges', function () {
'Reserved cost-per-event for stored spans — $0.02000000 → $0.87654321'
);
expect(container).toHaveTextContent(
'Reserved budgets — $100,000.00 for accepted spans and stored spans → $50,000.00 for accepted spans and stored spans'
'Reserved budgets — $100,000.00 for spans budget → $50,000.00 for spans budget'
);
});

Expand Down Expand Up @@ -420,7 +420,7 @@ describe('PendingChanges', function () {
'Reserved cost-per-event for stored spans — None → $0.87654321'
);
expect(container).toHaveTextContent(
'Reserved budgets — None → $50,000.00 for accepted spans and stored spans'
'Reserved budgets — None → $50,000.00 for spans budget'
);
});

Expand Down Expand Up @@ -457,7 +457,7 @@ describe('PendingChanges', function () {
'Reserved cost-per-event for spansIndexed — $0.02000000 → None'
);
expect(container).toHaveTextContent(
'Reserved budgets — $100,000.00 for accepted spans and stored spans → None'
'Reserved budgets — $100,000.00 for spans budget → None'
);
});

Expand Down
4 changes: 2 additions & 2 deletions static/gsAdmin/components/customers/pendingChanges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,8 @@ function getRegularChanges(subscription: Subscription) {
const newBudgetsChanges: string[] = [];
oldBudgets?.forEach(budget => {
const budgetName = getReservedBudgetDisplayName({
reservedBudget: budget,
plan: subscription.planDetails,
categories: Object.keys(budget.categories) as DataCategory[],
hadCustomDynamicSampling: oldPlanUsesDsNames,
});
oldBudgetsChanges.push(
Expand All @@ -261,8 +261,8 @@ function getRegularChanges(subscription: Subscription) {
});
pendingChanges.reservedBudgets.forEach(budget => {
const budgetName = getReservedBudgetDisplayName({
pendingReservedBudget: budget,
plan: pendingChanges.planDetails,
categories: Object.keys(budget.categories) as DataCategory[],
hadCustomDynamicSampling: newPlanUsesDsNames,
});
newBudgetsChanges.push(
Expand Down
262 changes: 144 additions & 118 deletions static/gsAdmin/components/provisionSubscriptionAction.tsx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions static/gsApp/__fixtures__/plan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function PlanFixture(fields: Partial<Plan>): Plan {
billingInterval: 'monthly',
categories: [],
checkoutCategories: [],
availableReservedBudgetTypes: {},
contractInterval: 'monthly',
description: '',
features: [],
Expand Down
71 changes: 1 addition & 70 deletions static/gsApp/components/gsBanner.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2132,68 +2132,6 @@ describe('GSBanner Overage Alerts', function () {
});
await act(tick);

const overagePrompts = [];
const warningPrompts = [];
const productTrialPrompts = [];
for (const category of [
'errors',
'attachments',
'replays',
'spans',
'monitor_seats',
'profile_duration',
'profile_duration_ui',
'uptime',
]) {
overagePrompts.push(`${category}_overage_alert`);
warningPrompts.push(`${category}_warning_alert`);
if (
['profile_duration', 'replays', 'spans', 'profile_duration_ui'].includes(category)
) {
productTrialPrompts.push(`${category}_product_trial_alert`);
}
}

expect(promptsMock).toHaveBeenCalledWith(
`/organizations/${organization.slug}/prompts-activity/`,
expect.objectContaining({
query: {
feature: [
'deactivated_member_alert',
...overagePrompts,
...warningPrompts,
...productTrialPrompts,
],
organization_id: organization.id,
},
})
);
});

// TODO(Seer): remove this test after seer has been added to the fixtures
it('checks prompts for all billed categories on plan with seer', async function () {
const organization = OrganizationFixture();
const subscription = SubscriptionFixture({
organization,
plan: 'am3_team',
});
subscription.planDetails.categories = [
...subscription.planDetails.categories,
DataCategory.SEER_AUTOFIX,
DataCategory.SEER_SCANNER,
];
const promptsMock = MockApiClient.addMockResponse({
method: 'GET',
url: `/organizations/${organization.slug}/prompts-activity/`,
});
SubscriptionStore.set(organization.slug, subscription);

render(<GSBanner organization={organization} />, {
organization,
deprecatedRouterMocks: true,
});
await act(tick);

const overagePrompts = [];
const warningPrompts = [];
const productTrialPrompts = [];
Expand All @@ -2212,14 +2150,7 @@ describe('GSBanner Overage Alerts', function () {
overagePrompts.push(`${category}_overage_alert`);
warningPrompts.push(`${category}_warning_alert`);
if (
[
'profile_duration',
'replays',
'spans',
'profile_duration_ui',
'seer_autofix',
'seer_scanner',
].includes(category)
['profile_duration', 'replays', 'spans', 'profile_duration_ui'].includes(category)
) {
productTrialPrompts.push(`${category}_product_trial_alert`);
}
Expand Down
4 changes: 2 additions & 2 deletions static/gsApp/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,15 @@ export const BILLED_DATA_CATEGORY_INFO = {
[DataCategoryExact.SEER_AUTOFIX]: {
...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SEER_AUTOFIX],
canAllocate: false,
canProductTrial: true,
canProductTrial: false,
maxAdminGift: 0,
freeEventsMultiple: 0,
feature: 'seer-billing',
},
[DataCategoryExact.SEER_SCANNER]: {
...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SEER_SCANNER],
canAllocate: false,
canProductTrial: true,
canProductTrial: false,
maxAdminGift: 0,
freeEventsMultiple: 0,
feature: 'seer-billing',
Expand Down
69 changes: 67 additions & 2 deletions static/gsApp/types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,50 @@ export enum CheckoutType {
BUNDLE = 'bundle',
}

export enum ReservedBudgetCategoryType {
DYNAMIC_SAMPLING = 'dynamicSampling',
SEER = 'seer',
}

export type ReservedBudgetCategory = {
/**
* The API name of the budget
*/
apiName: ReservedBudgetCategoryType;
/**
* Backend name of the category (all caps, snake case)
*/
budgetCategoryType: string;
/**
* whether a customer can use product trials for this budget
*/
canProductTrial: boolean;
/**
* the categories that are included in the budget
*/
dataCategories: DataCategory[];
/**
* Default budget for the category, in cents
*/
defaultBudget: number | null;
/**
* Link to the quotas documentation for the budget
*/
docLink: string;
/**
* Whether the budget is fixed or variable
*/
isFixed: boolean;
/**
* Display name of the budget
*/
name: string;
/**
* the product associated with the budget
*/
productName: string;
};

export type Plan = {
allowAdditionalReservedEvents: boolean;
allowOnDemand: boolean;
Expand All @@ -82,6 +126,9 @@ export type Plan = {
* Can be used for category upsells.
*/
availableCategories: DataCategory[];
availableReservedBudgetTypes: Partial<
Record<ReservedBudgetCategoryType, ReservedBudgetCategory>
>;
basePrice: number;
billingInterval: 'monthly' | 'annual';
budgetTerm: 'pay-as-you-go' | 'on-demand';
Expand Down Expand Up @@ -873,19 +920,37 @@ export interface MonitorCountResponse {
overQuotaMonitorCount: number;
}

type PendingReservedBudget = {
export type PendingReservedBudget = {
categories: Partial<Record<DataCategory, boolean | null>>;
reservedBudget: number;
};

export type ReservedBudget = {
/**
* The categories included in the budget and their respective ReservedBudgetMetricHistory
*/
categories: Partial<Record<DataCategory, ReservedBudgetMetricHistory>>;
/**
* The amount of free budget gifted in the associated usage cycle
*/
freeBudget: number;
/**
* The id of the ReservedBudgetHistory object
*/
id: string;
/**
* The percentage of the budget used, including gifted budget
*/
percentUsed: number;
/**
* The amount of budget in the associated usage cycle, excluding gifted budget
*/
reservedBudget: number;
/**
* The amount of budget used in the associated usage cycle
*/
totalReservedSpend: number;
};
} & ReservedBudgetCategory;

export type ReservedBudgetMetricHistory = {
reservedCpe: number; // in cents
Expand Down
Loading
Loading