dataCategory.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import upperFirst from 'lodash/upperFirst';
  2. import {DataCategory} from 'sentry/types/core';
  3. import type {Organization} from 'sentry/types/organization';
  4. import oxfordizeArray from 'sentry/utils/oxfordizeArray';
  5. import type {
  6. BillingMetricHistory,
  7. Plan,
  8. RecurringCredit,
  9. Subscription,
  10. } from 'getsentry/types';
  11. import {CreditType} from 'getsentry/types';
  12. import titleCase from 'getsentry/utils/titleCase';
  13. export const GIFT_CATEGORIES: string[] = [
  14. DataCategory.ERRORS,
  15. DataCategory.TRANSACTIONS,
  16. DataCategory.REPLAYS,
  17. DataCategory.ATTACHMENTS,
  18. DataCategory.MONITOR_SEATS,
  19. DataCategory.SPANS,
  20. DataCategory.SPANS_INDEXED,
  21. DataCategory.PROFILE_DURATION,
  22. DataCategory.UPTIME,
  23. ];
  24. const DATA_CATEGORY_FEATURES: {[key: string]: string | null} = {
  25. [DataCategory.ERRORS]: null, // All plans have access to errors
  26. [DataCategory.TRANSACTIONS]: 'performance-view',
  27. [DataCategory.REPLAYS]: 'session-replay',
  28. [DataCategory.ATTACHMENTS]: 'event-attachments',
  29. [DataCategory.MONITOR_SEATS]: 'monitor-seat-billing',
  30. [DataCategory.SPANS]: 'spans-usage-tracking',
  31. [DataCategory.UPTIME]: 'uptime',
  32. };
  33. const CREDIT_TYPE_TO_DATA_CATEGORY = {
  34. [CreditType.ERROR]: DataCategory.ERRORS,
  35. [CreditType.TRANSACTION]: DataCategory.TRANSACTIONS,
  36. [CreditType.SPAN]: DataCategory.SPANS,
  37. [CreditType.PROFILE_DURATION]: DataCategory.PROFILE_DURATION,
  38. [CreditType.ATTACHMENT]: DataCategory.ATTACHMENTS,
  39. [CreditType.REPLAY]: DataCategory.REPLAYS,
  40. [CreditType.MONITOR_SEAT]: DataCategory.MONITOR_SEATS,
  41. [CreditType.UPTIME]: DataCategory.UPTIME,
  42. };
  43. export const SINGULAR_DATA_CATEGORY = {
  44. default: 'default',
  45. errors: 'error',
  46. transactions: 'transaction',
  47. profiles: 'profile',
  48. attachments: 'attachment',
  49. replays: 'replay',
  50. monitorSeats: 'monitorSeat',
  51. spans: 'span',
  52. uptime: 'uptime',
  53. };
  54. /**
  55. *
  56. * Get the data category for a recurring credit type
  57. */
  58. export function getCreditDataCategory(credit: RecurringCredit) {
  59. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  60. return CREDIT_TYPE_TO_DATA_CATEGORY[credit.type];
  61. }
  62. type CategoryNameProps = {
  63. category: string;
  64. capitalize?: boolean;
  65. hadCustomDynamicSampling?: boolean;
  66. plan?: Plan;
  67. };
  68. /**
  69. * Convert a billed category to a display name.
  70. */
  71. export function getPlanCategoryName({
  72. plan,
  73. category,
  74. hadCustomDynamicSampling = false,
  75. capitalize = true,
  76. }: CategoryNameProps) {
  77. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  78. const displayNames = plan?.categoryDisplayNames?.[category];
  79. const categoryName =
  80. category === DataCategory.SPANS && hadCustomDynamicSampling
  81. ? 'accepted spans'
  82. : displayNames
  83. ? displayNames.plural
  84. : category;
  85. return capitalize ? upperFirst(categoryName) : categoryName;
  86. }
  87. /**
  88. * Convert a billed category to a singular display name.
  89. */
  90. export function getSingularCategoryName({
  91. plan,
  92. category,
  93. hadCustomDynamicSampling = false,
  94. capitalize = true,
  95. }: CategoryNameProps) {
  96. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  97. const displayNames = plan?.categoryDisplayNames?.[category];
  98. const categoryName =
  99. category === DataCategory.SPANS && hadCustomDynamicSampling
  100. ? 'accepted span'
  101. : displayNames
  102. ? displayNames.singular
  103. : category.substring(0, category.length - 1);
  104. return capitalize ? upperFirst(categoryName) : categoryName;
  105. }
  106. /**
  107. * Convert a list of reserved budget categories to a display name for the budget
  108. */
  109. export function getReservedBudgetDisplayName({
  110. plan,
  111. categories,
  112. hadCustomDynamicSampling = false,
  113. shouldTitleCase = false,
  114. }: Omit<CategoryNameProps, 'category' | 'capitalize'> & {
  115. categories: string[];
  116. shouldTitleCase?: boolean;
  117. }) {
  118. return oxfordizeArray(
  119. categories
  120. .map(category => {
  121. const name = getPlanCategoryName({
  122. plan,
  123. category,
  124. hadCustomDynamicSampling,
  125. capitalize: false,
  126. });
  127. return shouldTitleCase ? titleCase(name) : name;
  128. })
  129. .sort()
  130. );
  131. }
  132. /**
  133. * Get a string of display names.
  134. *
  135. * Ex: errors, transctions, and attachments.
  136. */
  137. export function listDisplayNames({
  138. plan,
  139. categories,
  140. hadCustomDynamicSampling = false,
  141. }: {
  142. categories: string[];
  143. plan: Plan;
  144. hadCustomDynamicSampling?: boolean;
  145. }) {
  146. const categoryNames = categories
  147. .filter(
  148. category => category !== DataCategory.SPANS_INDEXED || hadCustomDynamicSampling // filter out stored spans if no DS
  149. )
  150. .map(category =>
  151. getPlanCategoryName({plan, category, capitalize: false, hadCustomDynamicSampling})
  152. );
  153. return oxfordizeArray(categoryNames);
  154. }
  155. /**
  156. * Sort data categories in order.
  157. */
  158. export function sortCategories(categories?: {
  159. [key: string]: BillingMetricHistory;
  160. }): BillingMetricHistory[] {
  161. return Object.values(categories || {}).sort((a, b) => (a.order > b.order ? 1 : -1));
  162. }
  163. export function sortCategoriesWithKeys(categories?: {
  164. [key: string]: BillingMetricHistory;
  165. }): Array<[string, BillingMetricHistory]> {
  166. return Object.entries(categories || {}).sort((a, b) =>
  167. a[1].order > b[1].order ? 1 : -1
  168. );
  169. }
  170. /**
  171. * Whether the subscription plan includes a data category.
  172. */
  173. function hasCategory(subscription: Subscription, category: string) {
  174. return hasPlanCategory(subscription.planDetails, category);
  175. }
  176. function hasPlanCategory(plan: Plan, category: string) {
  177. return plan.categories.includes(category);
  178. }
  179. /**
  180. * Whether an organization has access to a data category.
  181. *
  182. * NOTE: Includes accounts that have free access to a data category through
  183. * custom feature handlers and plan trial. Used for usage UI.
  184. */
  185. export function hasCategoryFeature(
  186. category: string,
  187. subscription: Subscription,
  188. organization: Organization
  189. ) {
  190. if (hasCategory(subscription, category)) {
  191. return true;
  192. }
  193. const feature = DATA_CATEGORY_FEATURES[category];
  194. if (typeof feature === 'undefined') {
  195. return false;
  196. }
  197. return feature ? organization.features.includes(feature) : true;
  198. }