Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ecf4694
feat(usage overview): Introduce util functions and hook
isabellaenriquez Nov 28, 2025
a9fdc2d
fix test + knip
isabellaenriquez Nov 28, 2025
28f2d3e
add this too
isabellaenriquez Nov 28, 2025
6b0f5aa
feat(usage overview): Update buttons
isabellaenriquez Nov 28, 2025
a6efc5d
update tests + tweak style
isabellaenriquez Nov 28, 2025
f7623fe
also add this
isabellaenriquez Nov 28, 2025
485be78
feat(usage overview): Extract drawer charts into component
isabellaenriquez Nov 28, 2025
145d051
rm unused exported type
isabellaenriquez Nov 28, 2025
ab127d3
update tests
isabellaenriquez Nov 28, 2025
87a9f70
feat(usage overview): Introduce new panel
isabellaenriquez Nov 28, 2025
eec3fb6
split out breakdownInfo
isabellaenriquez Nov 28, 2025
bd9804f
feat(usage overview): Add breakdown into panel
isabellaenriquez Nov 28, 2025
eb07afa
fix types
isabellaenriquez Nov 28, 2025
6d701b6
add padding
isabellaenriquez Nov 28, 2025
f8b12e6
feat(usage overview): Introduce new table
isabellaenriquez Dec 1, 2025
800b057
feat(usage overview): Release new Usage Overview
isabellaenriquez Dec 1, 2025
eb1e712
fix tests
isabellaenriquez Dec 1, 2025
39e1d46
Merge branch 'isabella/uo-v2-pt-6' into isabella/uo-v2-pt-7
isabellaenriquez Dec 1, 2025
99543b4
fix mocks
isabellaenriquez Dec 1, 2025
ad8f5ac
Merge branch 'master' into isabella/uo-v2-pt-7
isabellaenriquez Dec 2, 2025
1794e53
small cosmetic tweaks + hide irrelevant breakdown info
isabellaenriquez Dec 3, 2025
8839526
add lock to header
isabellaenriquez Dec 3, 2025
b9ea509
alter cta
isabellaenriquez Dec 4, 2025
8361ee1
fix translation + inner table bug
isabellaenriquez Dec 4, 2025
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
116 changes: 116 additions & 0 deletions static/gsApp/hooks/useProductBillingMetadata.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type {DataCategory} from 'sentry/types/core';
import {toTitleCase} from 'sentry/utils/string/toTitleCase';

import type {AddOn, AddOnCategory, ProductTrial, Subscription} from 'getsentry/types';
import {
checkIsAddOn,
getActiveProductTrial,
getBilledCategory,
getPotentialProductTrial,
productIsEnabled,
} from 'getsentry/utils/billing';
import {getPlanCategoryName} from 'getsentry/utils/dataCategory';

interface ProductBillingMetadata {
/**
* The active product trial for the given product, if any.
* Always null when excludeProductTrials is true.
*/
activeProductTrial: ProductTrial | null;
/**
* The billed category for the given product.
* When product is a DataCategory, we just return product.
* When product is an AddOnCategory, we return the billed category for the
* add-on.
*/
billedCategory: DataCategory | null;
/**
* The display name for the given product in title case.
*/
displayName: string;
/**
* Whether the product is an add-on.
*/
isAddOn: boolean;
/**
* Whether the product is enabled for the subscription.
*/
isEnabled: boolean;
/**
* The longest available product trial for the given product, if any.
* Always null when excludeProductTrials is true.
*/
potentialProductTrial: ProductTrial | null;
/**
* Whether the usage for the given product has exceeded the limit.
*/
usageExceeded: boolean;
/**
* The add-on information for the given product from the subscription,
* if any.
*/
addOnInfo?: AddOn;
}

const EMPTY_PRODUCT_BILLING_METADATA: ProductBillingMetadata = {
billedCategory: null,
displayName: '',
isAddOn: false,
isEnabled: false,
usageExceeded: false,
activeProductTrial: null,
potentialProductTrial: null,
};

export function useProductBillingMetadata(
subscription: Subscription,
product: DataCategory | AddOnCategory,
parentProduct?: DataCategory | AddOnCategory,
excludeProductTrials?: boolean
): ProductBillingMetadata {
const isAddOn = checkIsAddOn(parentProduct ?? product);
const billedCategory = getBilledCategory(subscription, product);

if (!billedCategory) {
return EMPTY_PRODUCT_BILLING_METADATA;
}

let displayName = '';
let addOnInfo = undefined;

if (isAddOn) {
const category = (parentProduct ?? product) as AddOnCategory;
addOnInfo = subscription.addOns?.[category];
if (!addOnInfo) {
return EMPTY_PRODUCT_BILLING_METADATA;
}
displayName = parentProduct
? getPlanCategoryName({
plan: subscription.planDetails,
category: billedCategory,
title: true,
})
: toTitleCase(addOnInfo.productName, {allowInnerUpperCase: true});
} else {
displayName = getPlanCategoryName({
plan: subscription.planDetails,
category: billedCategory,
title: true,
});
}

return {
displayName,
billedCategory,
isAddOn,
isEnabled: productIsEnabled(subscription, parentProduct ?? product),
addOnInfo,
usageExceeded: subscription.categories[billedCategory]?.usageExceeded ?? false,
activeProductTrial: excludeProductTrials
? null
: getActiveProductTrial(subscription.productTrials ?? null, billedCategory),
potentialProductTrial: excludeProductTrials
? null
: getPotentialProductTrial(subscription.productTrials ?? null, billedCategory),
};
}
2 changes: 1 addition & 1 deletion static/gsApp/types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export type AddOnCategoryInfo = {
productName: string;
};

type AddOn = AddOnCategoryInfo & {
export type AddOn = AddOnCategoryInfo & {
enabled: boolean;
};

Expand Down
140 changes: 138 additions & 2 deletions static/gsApp/utils/billing.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';
import {DataCategory} from 'sentry/types/core';

import {BILLION, GIGABYTE, MILLION, UNLIMITED} from 'getsentry/constants';
import {OnDemandBudgetMode} from 'getsentry/types';
import type {ProductTrial} from 'getsentry/types';
import {AddOnCategory, OnDemandBudgetMode} from 'getsentry/types';
import type {ProductTrial, Subscription} from 'getsentry/types';
import {
checkIsAddOn,
convertUsageToReservedUnit,
formatReservedWithUnits,
formatUsageWithUnits,
getActiveProductTrial,
getBestActionToIncreaseEventLimits,
getBilledCategory,
getCreditApplied,
getOnDemandCategories,
getProductTrial,
Expand All @@ -27,6 +29,7 @@ import {
isNewPayingCustomer,
isTeamPlanFamily,
MILLISECONDS_IN_HOUR,
productIsEnabled,
trialPromptIsDismissed,
UsageAction,
} from 'getsentry/utils/billing';
Expand Down Expand Up @@ -185,6 +188,11 @@ describe('formatReservedWithUnits', () => {
useUnitScaling: true,
})
).toBe(UNLIMITED);
expect(
formatReservedWithUnits(-1, DataCategory.ATTACHMENTS, {
useUnitScaling: true,
})
).toBe(UNLIMITED);
});

it('returns correct string for Profile Duration', () => {
Expand Down Expand Up @@ -253,6 +261,11 @@ describe('formatReservedWithUnits', () => {
useUnitScaling: true,
})
).toBe(UNLIMITED);
expect(
formatReservedWithUnits(-1, DataCategory.LOG_BYTE, {
useUnitScaling: true,
})
).toBe(UNLIMITED);

expect(
formatReservedWithUnits(1234, DataCategory.LOG_BYTE, {
Expand Down Expand Up @@ -1129,3 +1142,126 @@ describe('getCreditApplied', () => {
).toBe(0);
});
});

describe('checkIsAddOn', () => {
it('returns true for add-on', () => {
expect(checkIsAddOn(AddOnCategory.LEGACY_SEER)).toBe(true);
expect(checkIsAddOn(AddOnCategory.SEER)).toBe(true);
});

it('returns false for data category', () => {
expect(checkIsAddOn(DataCategory.ERRORS)).toBe(false);
expect(checkIsAddOn(DataCategory.SEER_AUTOFIX)).toBe(false);
});
});

describe('getBilledCategory', () => {
const organization = OrganizationFixture();
const subscription = SubscriptionFixture({organization, plan: 'am3_team'});

it('returns correct billed category for data category', () => {
subscription.planDetails.categories.forEach(category => {
expect(getBilledCategory(subscription, category)).toBe(category);
});
});

it('returns correct billed category for add-on', () => {
expect(getBilledCategory(subscription, AddOnCategory.LEGACY_SEER)).toBe(
DataCategory.SEER_AUTOFIX
);
expect(getBilledCategory(subscription, AddOnCategory.SEER)).toBe(
DataCategory.SEER_USER
);
});
});

describe('productIsEnabled', () => {
const organization = OrganizationFixture();
let subscription: Subscription;

beforeEach(() => {
subscription = SubscriptionFixture({organization, plan: 'am3_team'});
});

it('returns true for active product trial', () => {
subscription.productTrials = [
{
// not started
category: DataCategory.PROFILE_DURATION,
isStarted: false,
reasonCode: 1001,
startDate: undefined,
endDate: moment().utc().add(20, 'years').format(),
},
{
// started
category: DataCategory.REPLAYS,
isStarted: true,
reasonCode: 1001,
startDate: moment().utc().subtract(10, 'days').format(),
endDate: moment().utc().add(20, 'days').format(),
},
{
// started
category: DataCategory.SEER_AUTOFIX,
isStarted: true,
reasonCode: 1001,
startDate: moment().utc().subtract(10, 'days').format(),
endDate: moment().utc().add(20, 'days').format(),
},
];

expect(productIsEnabled(subscription, DataCategory.PROFILE_DURATION)).toBe(false);
expect(productIsEnabled(subscription, DataCategory.REPLAYS)).toBe(true);
expect(productIsEnabled(subscription, DataCategory.SEER_AUTOFIX)).toBe(true);
expect(productIsEnabled(subscription, AddOnCategory.LEGACY_SEER)).toBe(true); // because there is a product trial for the billed category
});

it('uses subscription add-on info for add-on', () => {
subscription.addOns!.seer = {
...subscription.addOns?.seer!,
enabled: true,
};

expect(productIsEnabled(subscription, AddOnCategory.SEER)).toBe(true);
expect(productIsEnabled(subscription, AddOnCategory.LEGACY_SEER)).toBe(false);
});

it('returns true for non-PAYG-only data categories', () => {
expect(productIsEnabled(subscription, DataCategory.ERRORS)).toBe(true);
});

it('uses PAYG budgets for PAYG-only data categories', () => {
expect(productIsEnabled(subscription, DataCategory.PROFILE_DURATION)).toBe(false);

// shared PAYG
subscription.onDemandBudgets = {
budgetMode: OnDemandBudgetMode.SHARED,
sharedMaxBudget: 1000,
enabled: true,
onDemandSpendUsed: 0,
};
expect(productIsEnabled(subscription, DataCategory.PROFILE_DURATION)).toBe(true);

// per-category PAYG
subscription.onDemandBudgets = {
budgetMode: OnDemandBudgetMode.PER_CATEGORY,
enabled: true,
budgets: {
errors: 1000,
},
usedSpends: {},
};
expect(productIsEnabled(subscription, DataCategory.PROFILE_DURATION)).toBe(false);

subscription.onDemandBudgets.budgets = {
...subscription.onDemandBudgets.budgets,
profileDuration: 1000,
};
subscription.categories.profileDuration = {
...subscription.categories.profileDuration!,
onDemandBudget: 1000,
};
expect(productIsEnabled(subscription, DataCategory.PROFILE_DURATION)).toBe(true);
});
});
Loading
Loading