Skip to content
Merged
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
69 changes: 68 additions & 1 deletion static/gsApp/utils/billing.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ import {SubscriptionFixture} from 'getsentry-test/fixtures/subscription';

import {DataCategory} from 'sentry/types/core';

import {BILLION, GIGABYTE, MILLION, UNLIMITED} from 'getsentry/constants';
import {
BILLION,
GIGABYTE,
MILLION,
RESERVED_BUDGET_QUOTA,
UNLIMITED,
UNLIMITED_RESERVED,
} from 'getsentry/constants';
import {AddOnCategory, OnDemandBudgetMode} from 'getsentry/types';
import type {ProductTrial, Subscription} from 'getsentry/types';
import {
checkIsAddOn,
checkIsAddOnChildCategory,
convertUsageToReservedUnit,
formatReservedWithUnits,
formatUsageWithUnits,
Expand Down Expand Up @@ -1155,6 +1163,65 @@ describe('checkIsAddOn', () => {
});
});

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

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

it('returns false when parent add-on is unavailable', () => {
subscription.addOns!.seer = {
...subscription.addOns?.seer!,
isAvailable: false,
};
expect(checkIsAddOnChildCategory(subscription, DataCategory.SEER_USER, true)).toBe(
false
);
});

it('returns true for zero reserved volume', () => {
subscription.categories.seerUsers = {
...subscription.categories.seerUsers!,
reserved: 0,
};
expect(checkIsAddOnChildCategory(subscription, DataCategory.SEER_USER, true)).toBe(
true
);
});

it('returns true for RESERVED_BUDGET_QUOTA reserved volume', () => {
subscription.categories.seerAutofix = {
...subscription.categories.seerAutofix!,
reserved: RESERVED_BUDGET_QUOTA,
};
expect(checkIsAddOnChildCategory(subscription, DataCategory.SEER_AUTOFIX, true)).toBe(
true
);
});

it('returns true for sub-categories regardless of reserved volume if not checking', () => {
subscription.categories.seerAutofix = {
...subscription.categories.seerAutofix!,
reserved: UNLIMITED_RESERVED,
};
expect(
checkIsAddOnChildCategory(subscription, DataCategory.SEER_AUTOFIX, false)
).toBe(true);
});

it('returns false for sub-categories with non-zero reserved volume if checking', () => {
subscription.categories.seerAutofix = {
...subscription.categories.seerAutofix!,
reserved: UNLIMITED_RESERVED,
};
expect(checkIsAddOnChildCategory(subscription, DataCategory.SEER_AUTOFIX, true)).toBe(
false
);
});
});

describe('getBilledCategory', () => {
const organization = OrganizationFixture();
const subscription = SubscriptionFixture({organization, plan: 'am3_team'});
Expand Down
28 changes: 28 additions & 0 deletions static/gsApp/utils/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,34 @@ export function checkIsAddOn(
return Object.values(AddOnCategory).includes(selectedProduct as AddOnCategory);
}

/**
* Check if a data category is a child category of an add-on.
* If `checkReserved` is true, the category is only considered a child if it has a reserved volume of 0 or RESERVED_BUDGET_QUOTA.
* If `checkReserved` is false, the category is considered a child if it is included in `dataCategories` for any available add-on.
*/
export function checkIsAddOnChildCategory(
subscription: Subscription,
category: DataCategory,
checkReserved: boolean
) {
const parentAddOn = Object.values(subscription.addOns ?? {})
.filter(addOn => addOn.isAvailable)
.find(addOn => addOn.dataCategories.includes(category));
if (!parentAddOn) {
return false;
}

if (checkReserved) {
const metricHistory = subscription.categories[category];
if (!metricHistory) {
return false;
}
return [RESERVED_BUDGET_QUOTA, 0].includes(metricHistory.reserved ?? 0);
}

return true;
}

/**
* Get the billed DataCategory for an add-on or DataCategory.
*/
Expand Down
45 changes: 45 additions & 0 deletions static/gsApp/utils/dataCategory.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {DataCategory} from 'sentry/types/core';
import {
getPlanCategoryName,
getReservedBudgetDisplayName,
getSingularCategoryName,
hasCategoryFeature,
isByteCategory,
listDisplayNames,
Expand Down Expand Up @@ -259,6 +260,50 @@ describe('getPlanCategoryName', () => {
);
});

it('should title case category if specified', () => {
expect(
getPlanCategoryName({plan, category: DataCategory.MONITOR_SEATS, title: true})
).toBe('Cron Monitors');
expect(getPlanCategoryName({plan, category: DataCategory.ERRORS, title: true})).toBe(
'Errors'
);
});

it('should display spans as accepted spans for DS', () => {
expect(
getPlanCategoryName({
plan,
category: DataCategory.SPANS,
hadCustomDynamicSampling: true,
})
).toBe('Accepted spans');
});
});

describe('getSingularCategoryName', () => {
const plan = PlanDetailsLookupFixture('am3_team');

it('should capitalize category', () => {
expect(getSingularCategoryName({plan, category: DataCategory.TRANSACTIONS})).toBe(
'Transaction'
);
expect(getSingularCategoryName({plan, category: DataCategory.PROFILE_DURATION})).toBe(
'Continuous profile hour'
);
expect(getSingularCategoryName({plan, category: DataCategory.MONITOR_SEATS})).toBe(
'Cron monitor'
);
});

it('should title case category if specified', () => {
expect(
getSingularCategoryName({plan, category: DataCategory.MONITOR_SEATS, title: true})
).toBe('Cron Monitor');
expect(
getSingularCategoryName({plan, category: DataCategory.ERRORS, title: true})
).toBe('Error');
});

it('should display spans as accepted spans for DS', () => {
expect(
getPlanCategoryName({
Expand Down
15 changes: 7 additions & 8 deletions static/gsApp/utils/dataCategory.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import upperFirst from 'lodash/upperFirst';

import {DATA_CATEGORY_INFO} from 'sentry/constants';
import {t} from 'sentry/locale';
import {DataCategory, DataCategoryExact} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import oxfordizeArray from 'sentry/utils/oxfordizeArray';
Expand Down Expand Up @@ -68,13 +69,11 @@ export function getPlanCategoryName({
}: CategoryNameProps) {
const displayNames = plan?.categoryDisplayNames?.[category];
const categoryName =
category === DataCategory.LOG_BYTE
? 'logs'
: category === DataCategory.SPANS && hadCustomDynamicSampling
? 'accepted spans'
: displayNames
? displayNames.plural
: category;
category === DataCategory.SPANS && hadCustomDynamicSampling
? t('accepted spans')
: displayNames
? displayNames.plural
: category;
return title
? toTitleCase(categoryName, {allowInnerUpperCase: true})
: capitalize
Expand All @@ -95,7 +94,7 @@ export function getSingularCategoryName({
const displayNames = plan?.categoryDisplayNames?.[category];
const categoryName =
category === DataCategory.SPANS && hadCustomDynamicSampling
? 'accepted span'
? t('accepted span')
: displayNames
? displayNames.singular
: category.substring(0, category.length - 1);
Expand Down
Loading
Loading