Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dc25142
fix(usage overview): Display add-on categories with reserved volume
isabellaenriquez Dec 5, 2025
d3059d8
feat(seer): Add Seer-specific content to Subscription Settings
isabellaenriquez Dec 5, 2025
99bd800
comments + additional spend
isabellaenriquez Dec 5, 2025
05b24d0
hide billedSeats when not enabled
isabellaenriquez Dec 5, 2025
fa2a4fd
fix tests
isabellaenriquez Dec 5, 2025
5018c1f
move to other pr
isabellaenriquez Dec 6, 2025
136a771
document assumption
isabellaenriquez Dec 6, 2025
ce02002
tests
isabellaenriquez Dec 6, 2025
3eca442
Merge branch 'isabella/bil-1842' into isabella/bil-1818
isabellaenriquez Dec 8, 2025
80d9091
some fixes
isabellaenriquez Dec 8, 2025
66ff094
fix these tests
isabellaenriquez Dec 8, 2025
a1db40f
undo some things
isabellaenriquez Dec 8, 2025
bf267c4
this too
isabellaenriquez Dec 8, 2025
001facb
Merge branch 'isabella/bil-1842' into isabella/bil-1818
isabellaenriquez Dec 8, 2025
73ae047
rm unused export
isabellaenriquez Dec 8, 2025
85ef250
Merge branch 'isabella/bil-1842' into isabella/bil-1818
isabellaenriquez Dec 8, 2025
aa83595
add formatting for profiling + seer
isabellaenriquez Dec 8, 2025
90e86f0
add tests + setting btn
isabellaenriquez Dec 8, 2025
88cbfcf
address pr feedback
isabellaenriquez Dec 8, 2025
c0ef83d
pr feedback
isabellaenriquez Dec 9, 2025
cc67673
Merge branch 'master' into isabella/bil-1818
isabellaenriquez Dec 9, 2025
4a8a7b7
minor styling tweaks
isabellaenriquez Dec 9, 2025
7bb7842
fix border styling
isabellaenriquez Dec 9, 2025
3a3859b
fix interpolation issue
isabellaenriquez Dec 9, 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
8 changes: 5 additions & 3 deletions static/app/constants/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {t} from 'sentry/locale';
import type {DataCategoryInfo, Scope} from 'sentry/types/core';
import {DataCategory, DataCategoryExact} from 'sentry/types/core';
import type {PermissionResource} from 'sentry/types/integrations';
import type {OrgRole} from 'sentry/types/organization';
import type {Organization, OrgRole} from 'sentry/types/organization';

/**
* Common constants here
Expand Down Expand Up @@ -611,15 +611,17 @@ export const DATA_CATEGORY_INFO = {
name: DataCategoryExact.SEER_USER,
plural: DataCategory.SEER_USER,
singular: 'seerUser',
displayName: 'seer user',
titleName: t('Seer'),
displayName: 'active contributor',
titleName: t('Active Contributors'),
productName: t('Seer'),
uid: 34,
isBilledCategory: true,
statsInfo: {
...DEFAULT_STATS_INFO,
showExternalStats: false, // TODO(seer): add external stats when ready
},
getProductLink: (organization: Organization) =>
`/settings/${organization.slug}/seer/`,
},
} as const satisfies Record<DataCategoryExact, DataCategoryInfo>;

Expand Down
2 changes: 2 additions & 0 deletions static/app/types/core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type {getInterval} from 'sentry/components/charts/utils';
import type {MenuListItemProps} from 'sentry/components/core/menuListItem';
import type {ALLOWED_SCOPES} from 'sentry/constants';
import type {Organization} from 'sentry/types/organization';

/**
* Visual representation of a project/team/organization/user
Expand Down Expand Up @@ -149,6 +150,7 @@ export interface DataCategoryInfo {
titleName: string;
uid: number;
docsUrl?: string;
getProductLink?: (organization: Organization) => string;
}

export enum Outcome {
Expand Down
20 changes: 11 additions & 9 deletions static/gsAdmin/components/changePlanAction.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ describe('ChangePlanAction', () => {
);
await selectEvent.select(screen.getByRole('textbox', {name: 'Logs (GB)'}), '5');

expect(screen.queryByText('Available Products')).not.toBeInTheDocument();
expect(screen.getByText('Available Products')).toBeInTheDocument(); // will always show if any product is launched and available for an org

expect(screen.getByRole('button', {name: 'Change Plan'})).toBeEnabled();
await userEvent.click(screen.getByRole('button', {name: 'Change Plan'}));
Expand All @@ -241,7 +241,6 @@ describe('ChangePlanAction', () => {
});

it('completes form with addOns', async () => {
mockOrg.features = ['seer-billing', 'seer-user-billing']; // this won't happen IRL, but doing this for testing multiple addons
const putMock = MockApiClient.addMockResponse({
url: `/customers/${mockOrg.slug}/subscription/`,
method: 'PUT',
Expand All @@ -267,11 +266,14 @@ describe('ChangePlanAction', () => {
);
await selectEvent.select(screen.getByRole('textbox', {name: 'Logs (GB)'}), '5');

// XXX: irl we would not have both versions of Seer available, but doing this for testing multiple addons
expect(screen.getByText('Available Products')).toBeInTheDocument();
const seerSelections = screen.getAllByText('Seer');
expect(seerSelections).toHaveLength(2);
await userEvent.click(seerSelections[0]!);
await userEvent.click(seerSelections[1]!);
const seerSelection = screen.getByText('Seer');
const legacySeerSelection = screen.getByText('Seer (Legacy)');
expect(seerSelection).toBeInTheDocument();
expect(legacySeerSelection).toBeInTheDocument();
await userEvent.click(seerSelection);
await userEvent.click(legacySeerSelection);

expect(screen.getByRole('button', {name: 'Change Plan'})).toBeEnabled();
await userEvent.click(screen.getByRole('button', {name: 'Change Plan'}));
Expand Down Expand Up @@ -473,7 +475,7 @@ describe('ChangePlanAction', () => {

// Verify Seer budget checkbox is checked when subscription has Seer budget
const seerCheckbox = screen.getByRole('checkbox', {
name: 'Seer',
name: 'Seer (Legacy)',
});
expect(seerCheckbox).toBeChecked();
});
Expand Down Expand Up @@ -516,7 +518,7 @@ describe('ChangePlanAction', () => {

// Check the Seer budget checkbox
const seerCheckbox = screen.getByRole('checkbox', {
name: 'Seer',
name: 'Seer (Legacy)',
});
await userEvent.click(seerCheckbox);

Expand Down Expand Up @@ -568,7 +570,7 @@ describe('ChangePlanAction', () => {

// Verify Seer budget checkbox is unchecked (default state)
const seerCheckbox = screen.getByRole('checkbox', {
name: 'Seer',
name: 'Seer (Legacy)',
});
expect(seerCheckbox).not.toBeChecked();

Expand Down
1 change: 0 additions & 1 deletion static/gsAdmin/components/changePlanAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,6 @@ function ChangePlanAction({
formModel={formModel}
activePlan={activePlan}
subscription={subscription}
organization={organization}
onSubmit={handleSubmit}
onCancel={closeModal}
onSubmitSuccess={(data: Data) => {
Expand Down
35 changes: 21 additions & 14 deletions static/gsAdmin/components/planList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import type FormModel from 'sentry/components/forms/model';
import type {Data, OnSubmitCallback} from 'sentry/components/forms/types';
import {space} from 'sentry/styles/space';
import type {DataCategory} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import {toTitleCase} from 'sentry/utils/string/toTitleCase';

import {ANNUAL} from 'getsentry/constants';
import type {BillingConfig, Plan, Subscription} from 'getsentry/types';
import {
AddOnCategory,
type BillingConfig,
type Plan,
type Subscription,
} from 'getsentry/types';
import {getPlanCategoryName, isByteCategory} from 'getsentry/utils/dataCategory';
import formatCurrency from 'getsentry/utils/formatCurrency';

Expand All @@ -27,15 +31,13 @@ type Props = {
onSubmit: OnSubmitCallback;
onSubmitError: (error: any) => void;
onSubmitSuccess: (data: Data) => void;
organization: Organization;
subscription: Subscription;
tierPlans: BillingConfig['planList'];
};

function PlanList({
activePlan,
subscription,
organization,
onSubmit,
onCancel,
onSubmitSuccess,
Expand Down Expand Up @@ -79,27 +81,25 @@ function PlanList({
100000: '100K',
};

const availableProducts = useMemo(
const availableAddOns = useMemo(
() =>
Object.values(activePlan?.addOnCategories || {})
.filter(
productInfo =>
productInfo.billingFlag &&
organization.features.includes(productInfo.billingFlag)
productInfo => subscription.addOns?.[productInfo.apiName]?.isAvailable ?? false
)
.map(productInfo => {
return productInfo;
}),
[activePlan?.addOnCategories, organization.features]
[activePlan?.addOnCategories, subscription.addOns]
);

useEffect(() => {
availableProducts.forEach(productInfo => {
availableAddOns.forEach(productInfo => {
const addOnKey = `addOn${toTitleCase(productInfo.apiName, {allowInnerUpperCase: true})}`;
const enabled = subscription.addOns?.[productInfo.apiName]?.enabled;
formModel.setValue(addOnKey, enabled);
});
}, [availableProducts, subscription.addOns, formModel]);
}, [availableAddOns, subscription.addOns, formModel]);

return (
<Form
Expand Down Expand Up @@ -187,16 +187,23 @@ function PlanList({
})}
</StyledFormSection>
)}
{availableProducts.length > 0 && (
{availableAddOns.length > 0 && (
<StyledFormSection>
<h4>Available Products</h4>
{availableProducts.map(productInfo => {
{availableAddOns.map(productInfo => {
const addOnKey = `addOn${toTitleCase(productInfo.apiName, {allowInnerUpperCase: true})}`;
const titleCaseName = toTitleCase(productInfo.productName, {
allowInnerUpperCase: true,
});
const label =
productInfo.apiName === AddOnCategory.LEGACY_SEER
? `${titleCaseName} (Legacy)`
: titleCaseName;
return (
<CheckboxField
key={productInfo.apiName}
data-test-id={`checkbox-${productInfo.productName}`}
label={toTitleCase(productInfo.productName)}
label={label}
name={addOnKey}
onChange={(value: any) => {
formModel.setValue(addOnKey, value.target.checked);
Expand Down
1 change: 1 addition & 0 deletions static/gsApp/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,6 @@ export const BILLED_DATA_CATEGORY_INFO = {
canProductTrial: true,
maxAdminGift: 10_000, // TODO(seer): Update this to the actual max admin gift
tallyType: 'seat',
shortenedUnitName: t('contributor'),
},
} as const satisfies Record<DataCategoryExact, BilledDataCategoryInfo>;
17 changes: 16 additions & 1 deletion static/gsApp/hooks/useProductBillingMetadata.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {DataCategory} from 'sentry/types/core';
import {toTitleCase} from 'sentry/utils/string/toTitleCase';
import useOrganization from 'sentry/utils/useOrganization';

import type {AddOn, AddOnCategory, ProductTrial, Subscription} from 'getsentry/types';
import {
Expand All @@ -9,7 +10,10 @@ import {
getPotentialProductTrial,
productIsEnabled,
} from 'getsentry/utils/billing';
import {getPlanCategoryName} from 'getsentry/utils/dataCategory';
import {
getCategoryInfoFromPlural,
getPlanCategoryName,
} from 'getsentry/utils/dataCategory';

interface ProductBillingMetadata {
/**
Expand Down Expand Up @@ -50,6 +54,10 @@ interface ProductBillingMetadata {
* if any.
*/
addOnInfo?: AddOn;
/**
* Link to the in-app page for the given product, if any.
*/
productLink?: string;
}

const EMPTY_PRODUCT_BILLING_METADATA: ProductBillingMetadata = {
Expand All @@ -68,13 +76,16 @@ export function useProductBillingMetadata(
parentProduct?: DataCategory | AddOnCategory,
excludeProductTrials?: boolean
): ProductBillingMetadata {
const organization = useOrganization();
const isAddOn = checkIsAddOn(parentProduct ?? product);
const billedCategory = getBilledCategory(subscription, product);

if (!billedCategory) {
return EMPTY_PRODUCT_BILLING_METADATA;
}

const billedCategoryInfo = getCategoryInfoFromPlural(billedCategory);

let displayName = '';
let addOnInfo = undefined;

Expand Down Expand Up @@ -112,5 +123,9 @@ export function useProductBillingMetadata(
potentialProductTrial: excludeProductTrials
? null
: getPotentialProductTrial(subscription.productTrials ?? null, billedCategory),
productLink:
organization && billedCategoryInfo
? billedCategoryInfo.getProductLink?.(organization)
: undefined,
};
}
19 changes: 19 additions & 0 deletions static/gsApp/types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1202,3 +1202,22 @@ export interface BilledDataCategoryInfo extends DataCategoryInfo {
*/
shortenedUnitName?: string;
}

type SeatStatus =
| 'UNKNOWN'
| 'ASSIGNED'
| 'OVER_QUOTA'
| 'DISABLED_FOR_BILLING'
| 'REMOVED'
| 'REALLOCATED';

export type BillingSeatAssignment = {
billingMetric: DataCategory;
created: string;
displayName: string;
id: number;
isTrialSeat: boolean;
projectId: number;
seatIdentifier: string;
status: SeatStatus;
};
Loading
Loading