import upperFirst from 'lodash/upperFirst'; import {DataCategory} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import oxfordizeArray from 'sentry/utils/oxfordizeArray'; import type { BillingMetricHistory, Plan, RecurringCredit, Subscription, } from 'getsentry/types'; import {CreditType} from 'getsentry/types'; import titleCase from 'getsentry/utils/titleCase'; export const GIFT_CATEGORIES: string[] = [ DataCategory.ERRORS, DataCategory.TRANSACTIONS, DataCategory.REPLAYS, DataCategory.ATTACHMENTS, DataCategory.MONITOR_SEATS, DataCategory.SPANS, DataCategory.SPANS_INDEXED, DataCategory.PROFILE_DURATION, DataCategory.UPTIME, ]; const DATA_CATEGORY_FEATURES: {[key: string]: string | null} = { [DataCategory.ERRORS]: null, // All plans have access to errors [DataCategory.TRANSACTIONS]: 'performance-view', [DataCategory.REPLAYS]: 'session-replay', [DataCategory.ATTACHMENTS]: 'event-attachments', [DataCategory.MONITOR_SEATS]: 'monitor-seat-billing', [DataCategory.SPANS]: 'spans-usage-tracking', [DataCategory.UPTIME]: 'uptime', }; const CREDIT_TYPE_TO_DATA_CATEGORY = { [CreditType.ERROR]: DataCategory.ERRORS, [CreditType.TRANSACTION]: DataCategory.TRANSACTIONS, [CreditType.SPAN]: DataCategory.SPANS, [CreditType.PROFILE_DURATION]: DataCategory.PROFILE_DURATION, [CreditType.ATTACHMENT]: DataCategory.ATTACHMENTS, [CreditType.REPLAY]: DataCategory.REPLAYS, [CreditType.MONITOR_SEAT]: DataCategory.MONITOR_SEATS, [CreditType.UPTIME]: DataCategory.UPTIME, }; export const SINGULAR_DATA_CATEGORY = { default: 'default', errors: 'error', transactions: 'transaction', profiles: 'profile', attachments: 'attachment', replays: 'replay', monitorSeats: 'monitorSeat', spans: 'span', uptime: 'uptime', }; /** * * Get the data category for a recurring credit type */ export function getCreditDataCategory(credit: RecurringCredit) { // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message return CREDIT_TYPE_TO_DATA_CATEGORY[credit.type]; } type CategoryNameProps = { category: string; capitalize?: boolean; hadCustomDynamicSampling?: boolean; plan?: Plan; }; /** * Convert a billed category to a display name. */ export function getPlanCategoryName({ plan, category, hadCustomDynamicSampling = false, capitalize = true, }: CategoryNameProps) { // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message const displayNames = plan?.categoryDisplayNames?.[category]; const categoryName = category === DataCategory.SPANS && hadCustomDynamicSampling ? 'accepted spans' : displayNames ? displayNames.plural : category; return capitalize ? upperFirst(categoryName) : categoryName; } /** * Convert a billed category to a singular display name. */ export function getSingularCategoryName({ plan, category, hadCustomDynamicSampling = false, capitalize = true, }: CategoryNameProps) { // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message const displayNames = plan?.categoryDisplayNames?.[category]; const categoryName = category === DataCategory.SPANS && hadCustomDynamicSampling ? 'accepted span' : displayNames ? displayNames.singular : category.substring(0, category.length - 1); return capitalize ? upperFirst(categoryName) : categoryName; } /** * Convert a list of reserved budget categories to a display name for the budget */ export function getReservedBudgetDisplayName({ plan, categories, hadCustomDynamicSampling = false, shouldTitleCase = false, }: Omit & { categories: string[]; shouldTitleCase?: boolean; }) { return oxfordizeArray( categories .map(category => { const name = getPlanCategoryName({ plan, category, hadCustomDynamicSampling, capitalize: false, }); return shouldTitleCase ? titleCase(name) : name; }) .sort() ); } /** * Get a string of display names. * * Ex: errors, transctions, and attachments. */ export function listDisplayNames({ plan, categories, hadCustomDynamicSampling = false, }: { categories: string[]; plan: Plan; hadCustomDynamicSampling?: boolean; }) { const categoryNames = categories .filter( category => category !== DataCategory.SPANS_INDEXED || hadCustomDynamicSampling // filter out stored spans if no DS ) .map(category => getPlanCategoryName({plan, category, capitalize: false, hadCustomDynamicSampling}) ); return oxfordizeArray(categoryNames); } /** * Sort data categories in order. */ export function sortCategories(categories?: { [key: string]: BillingMetricHistory; }): BillingMetricHistory[] { return Object.values(categories || {}).sort((a, b) => (a.order > b.order ? 1 : -1)); } export function sortCategoriesWithKeys(categories?: { [key: string]: BillingMetricHistory; }): Array<[string, BillingMetricHistory]> { return Object.entries(categories || {}).sort((a, b) => a[1].order > b[1].order ? 1 : -1 ); } /** * Whether the subscription plan includes a data category. */ function hasCategory(subscription: Subscription, category: string) { return hasPlanCategory(subscription.planDetails, category); } function hasPlanCategory(plan: Plan, category: string) { return plan.categories.includes(category); } /** * Whether an organization has access to a data category. * * NOTE: Includes accounts that have free access to a data category through * custom feature handlers and plan trial. Used for usage UI. */ export function hasCategoryFeature( category: string, subscription: Subscription, organization: Organization ) { if (hasCategory(subscription, category)) { return true; } const feature = DATA_CATEGORY_FEATURES[category]; if (typeof feature === 'undefined') { return false; } return feature ? organization.features.includes(feature) : true; }