usageTotals.spec.tsx 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259
  1. import moment from 'moment-timezone';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {MetricHistoryFixture} from 'getsentry-test/fixtures/metricHistory';
  4. import {
  5. Am3DsEnterpriseSubscriptionFixture,
  6. SubscriptionFixture,
  7. } from 'getsentry-test/fixtures/subscription';
  8. import {UsageTotalFixture} from 'getsentry-test/fixtures/usageTotal';
  9. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  10. import {DataCategory} from 'sentry/types/core';
  11. import {GIGABYTE, RESERVED_BUDGET_QUOTA, UNLIMITED_RESERVED} from 'getsentry/constants';
  12. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  13. import {OnDemandBudgetMode, type Subscription} from 'getsentry/types';
  14. import UsageTotals, {
  15. calculateCategoryOnDemandUsage,
  16. calculateCategoryPrepaidUsage,
  17. } from 'getsentry/views/subscriptionPage/usageTotals';
  18. describe('Subscription > UsageTotals', function () {
  19. const totals = UsageTotalFixture({
  20. accepted: 26,
  21. dropped: 10,
  22. droppedOverQuota: 7,
  23. droppedSpikeProtection: 1,
  24. droppedOther: 2,
  25. });
  26. const organization = OrganizationFixture();
  27. let subscription!: Subscription;
  28. beforeEach(() => {
  29. subscription = SubscriptionFixture({
  30. organization,
  31. plan: 'am3_business',
  32. });
  33. SubscriptionStore.set(organization.slug, subscription);
  34. });
  35. afterEach(() => {
  36. SubscriptionStore.init();
  37. });
  38. it('calculates error totals and renders them', async function () {
  39. render(
  40. <UsageTotals
  41. category="errors"
  42. totals={totals}
  43. reservedUnits={100_000}
  44. prepaidUnits={100_000}
  45. subscription={subscription}
  46. organization={organization}
  47. displayMode="usage"
  48. />
  49. );
  50. expect(screen.getByText('Errors usage this period')).toBeInTheDocument();
  51. expect(screen.getByTestId('reserved-errors')).toHaveTextContent('100K Reserved');
  52. expect(screen.getByText('26')).toBeInTheDocument();
  53. // Expand usage table
  54. await userEvent.click(screen.getByRole('button'));
  55. expect(
  56. screen.getByRole('row', {name: 'Errors Quantity % of Errors'})
  57. ).toBeInTheDocument();
  58. expect(screen.getByRole('row', {name: 'Accepted 26 72%'})).toBeInTheDocument();
  59. expect(screen.getByRole('row', {name: 'Total Dropped 10 28%'})).toBeInTheDocument();
  60. expect(screen.getByRole('row', {name: 'Over Quota 7 19%'})).toBeInTheDocument();
  61. expect(screen.getByRole('row', {name: 'Spike Protection 1 3%'})).toBeInTheDocument();
  62. expect(screen.getByRole('row', {name: 'Other 2 6%'})).toBeInTheDocument();
  63. });
  64. it('renders transaction event totals with feature', async function () {
  65. const am2Subscription = SubscriptionFixture({
  66. organization,
  67. plan: 'am2_business',
  68. });
  69. SubscriptionStore.set(organization.slug, am2Subscription);
  70. render(
  71. <UsageTotals
  72. showEventBreakdown
  73. category="transactions"
  74. totals={totals}
  75. eventTotals={{transactions: totals, profiles: totals}}
  76. reservedUnits={100_000}
  77. prepaidUnits={100_000}
  78. subscription={am2Subscription}
  79. organization={organization}
  80. displayMode="usage"
  81. />
  82. );
  83. expect(screen.getByTestId('reserved-transactions')).toHaveTextContent(
  84. '100K Reserved'
  85. );
  86. expect(screen.getByText('26')).toBeInTheDocument();
  87. // Expand usage table
  88. await userEvent.click(screen.getByRole('button'));
  89. expect(
  90. screen.getByRole('columnheader', {name: 'Transaction Events'})
  91. ).toBeInTheDocument();
  92. expect(
  93. screen.getByRole('columnheader', {name: 'Profile Events'})
  94. ).toBeInTheDocument();
  95. });
  96. it('does not render transaction event totals without feature', async function () {
  97. const am2Subscription = SubscriptionFixture({
  98. organization,
  99. plan: 'am2_business',
  100. });
  101. SubscriptionStore.set(organization.slug, am2Subscription);
  102. render(
  103. <UsageTotals
  104. category="transactions"
  105. totals={totals}
  106. eventTotals={{transactions: totals, profiles: totals}}
  107. reservedUnits={100_000}
  108. prepaidUnits={100_000}
  109. subscription={am2Subscription}
  110. organization={organization}
  111. displayMode="usage"
  112. />
  113. );
  114. expect(screen.getByTestId('reserved-transactions')).toHaveTextContent(
  115. '100K Reserved'
  116. );
  117. expect(screen.getByText('26')).toBeInTheDocument();
  118. // Expand usage table
  119. await userEvent.click(screen.getByRole('button'));
  120. expect(
  121. screen.queryByRole('columnheader', {name: 'Transaction Events'})
  122. ).not.toBeInTheDocument();
  123. expect(
  124. screen.queryByRole('columnheader', {name: 'Profile Events'})
  125. ).not.toBeInTheDocument();
  126. });
  127. it('renders accepted spans in spend mode with reserved budgets and dynamic sampling', async function () {
  128. const dsSubscription = Am3DsEnterpriseSubscriptionFixture({
  129. organization,
  130. hadCustomDynamicSampling: true,
  131. });
  132. render(
  133. <UsageTotals
  134. showEventBreakdown
  135. category="spans"
  136. totals={totals}
  137. eventTotals={{spans: totals}}
  138. reservedUnits={RESERVED_BUDGET_QUOTA}
  139. prepaidUnits={RESERVED_BUDGET_QUOTA}
  140. prepaidBudget={100_000_00}
  141. reservedBudget={100_000_00}
  142. reservedSpend={40_000_00}
  143. subscription={dsSubscription}
  144. organization={organization}
  145. displayMode="usage"
  146. />
  147. );
  148. expect(screen.getByText('Accepted spans spend this period')).toBeInTheDocument();
  149. expect(screen.getByTestId('reserved-spans')).toHaveTextContent(
  150. '$100,000.00 Reserved'
  151. );
  152. expect(screen.getByText('$40,000')).toBeInTheDocument();
  153. expect(screen.getByText('40% of $100,000')).toBeInTheDocument();
  154. // Expand usage table
  155. await userEvent.click(screen.getByRole('button'));
  156. expect(
  157. screen.getByRole('row', {name: 'Accepted Spans Quantity % of Accepted Spans'})
  158. ).toBeInTheDocument();
  159. expect(
  160. screen.getByRole('columnheader', {name: 'Accepted Spans'})
  161. ).toBeInTheDocument();
  162. });
  163. it('renders spans with reserved budgets without dynamic sampling', async function () {
  164. const dsSubscription = Am3DsEnterpriseSubscriptionFixture({
  165. organization,
  166. hadCustomDynamicSampling: false,
  167. });
  168. render(
  169. <UsageTotals
  170. showEventBreakdown
  171. category="spans"
  172. totals={totals}
  173. eventTotals={{spans: totals}}
  174. reservedUnits={RESERVED_BUDGET_QUOTA}
  175. prepaidUnits={RESERVED_BUDGET_QUOTA}
  176. prepaidBudget={100_000_00}
  177. reservedBudget={100_000_00}
  178. reservedSpend={60_000_00}
  179. subscription={dsSubscription}
  180. organization={organization}
  181. displayMode="usage"
  182. />
  183. );
  184. expect(screen.getByText('Spans spend this period')).toBeInTheDocument();
  185. expect(screen.getByTestId('reserved-spans')).toHaveTextContent(
  186. '$100,000.00 Reserved'
  187. );
  188. expect(screen.getByText('$60,000')).toBeInTheDocument();
  189. expect(screen.getByText('60% of $100,000')).toBeInTheDocument();
  190. // Expand usage table
  191. await userEvent.click(screen.getByRole('button'));
  192. expect(
  193. screen.getByRole('row', {name: 'Spans Quantity % of Spans'})
  194. ).toBeInTheDocument();
  195. expect(screen.getByRole('columnheader', {name: 'Spans'})).toBeInTheDocument();
  196. });
  197. it('renders reserved budget categories with gifted budget', function () {
  198. const dsSubscription = Am3DsEnterpriseSubscriptionFixture({
  199. organization,
  200. hadCustomDynamicSampling: true,
  201. });
  202. render(
  203. <UsageTotals
  204. category="spans"
  205. totals={totals}
  206. eventTotals={{spans: totals}}
  207. reservedUnits={RESERVED_BUDGET_QUOTA}
  208. prepaidUnits={RESERVED_BUDGET_QUOTA}
  209. prepaidBudget={110_000_00}
  210. reservedBudget={100_000_00}
  211. reservedSpend={60_000_00}
  212. freeBudget={10_000_00}
  213. subscription={dsSubscription}
  214. organization={organization}
  215. displayMode="usage"
  216. />
  217. );
  218. expect(screen.getByTestId('gifted-spans')).toHaveTextContent(
  219. '$100,000.00 Reserved + $10,000.00 Gifted'
  220. );
  221. expect(screen.getByText('55% of $110,000')).toBeInTheDocument();
  222. });
  223. it('renders reserved budget categories with soft cap', function () {
  224. const dsSubscription = Am3DsEnterpriseSubscriptionFixture({
  225. organization,
  226. hadCustomDynamicSampling: true,
  227. });
  228. render(
  229. <UsageTotals
  230. category="spans"
  231. totals={totals}
  232. eventTotals={{spans: totals}}
  233. reservedUnits={RESERVED_BUDGET_QUOTA}
  234. prepaidUnits={RESERVED_BUDGET_QUOTA}
  235. prepaidBudget={100_000_00}
  236. reservedBudget={100_000_00}
  237. reservedSpend={60_000_00}
  238. softCapType="ON_DEMAND"
  239. subscription={dsSubscription}
  240. organization={organization}
  241. displayMode="usage"
  242. />
  243. );
  244. expect(screen.getByTestId('reserved-spans')).toHaveTextContent(
  245. '$100,000.00 Reserved (On Demand)'
  246. );
  247. });
  248. it('formats units', async function () {
  249. render(
  250. <UsageTotals
  251. category="attachments"
  252. totals={totals}
  253. reservedUnits={100}
  254. prepaidUnits={100}
  255. subscription={subscription}
  256. organization={organization}
  257. displayMode="usage"
  258. />
  259. );
  260. expect(screen.getByTestId('reserved-attachments')).toHaveTextContent(
  261. '100 GB Reserved'
  262. );
  263. // Expand usage table
  264. await userEvent.click(screen.getByRole('button'));
  265. expect(
  266. screen.getByRole('row', {name: 'Attachments Quantity % of Attachments'})
  267. ).toBeInTheDocument();
  268. expect(screen.getByRole('row', {name: 'Accepted 26 B 72%'})).toBeInTheDocument();
  269. expect(screen.getByRole('row', {name: 'Total Dropped 10 B 28%'})).toBeInTheDocument();
  270. expect(screen.getByRole('row', {name: 'Over Quota 7 B 19%'})).toBeInTheDocument();
  271. expect(
  272. screen.getByRole('row', {name: 'Spike Protection 1 B 3%'})
  273. ).toBeInTheDocument();
  274. expect(screen.getByRole('row', {name: 'Other 2 B 6%'})).toBeInTheDocument();
  275. });
  276. it('renders default stats with no billing history', function () {
  277. render(
  278. <UsageTotals
  279. category="transactions"
  280. subscription={subscription}
  281. organization={organization}
  282. displayMode="usage"
  283. />
  284. );
  285. expect(screen.getByText('Transactions usage this period')).toBeInTheDocument();
  286. expect(screen.getByText('0')).toBeInTheDocument();
  287. });
  288. it('renders gifted errors', function () {
  289. render(
  290. <UsageTotals
  291. category="errors"
  292. totals={UsageTotalFixture({accepted: 175_000})}
  293. reservedUnits={50_000}
  294. freeUnits={150_000}
  295. prepaidUnits={200_000}
  296. subscription={subscription}
  297. organization={organization}
  298. displayMode="usage"
  299. />
  300. );
  301. expect(screen.getByText('175,000')).toBeInTheDocument();
  302. expect(screen.getByTestId('gifted-errors')).toHaveTextContent(
  303. '50K Reserved + 150K Gifted'
  304. );
  305. });
  306. it('renders gifted transactions', function () {
  307. render(
  308. <UsageTotals
  309. category="transactions"
  310. totals={totals}
  311. reservedUnits={100_000}
  312. freeUnits={200_000}
  313. prepaidUnits={300_000}
  314. subscription={subscription}
  315. organization={organization}
  316. displayMode="usage"
  317. />
  318. );
  319. expect(screen.getByTestId('gifted-transactions')).toHaveTextContent(
  320. '100K Reserved + 200K Gifted'
  321. );
  322. });
  323. it('does not render gifted transactions with unlimited quota', function () {
  324. render(
  325. <UsageTotals
  326. category="transactions"
  327. totals={totals}
  328. reservedUnits={UNLIMITED_RESERVED}
  329. freeUnits={200_000}
  330. prepaidUnits={UNLIMITED_RESERVED}
  331. subscription={subscription}
  332. organization={organization}
  333. displayMode="usage"
  334. />
  335. );
  336. expect(screen.getByTestId('reserved-transactions')).toHaveTextContent('∞ Reserved');
  337. });
  338. it('renders gifted attachments', function () {
  339. render(
  340. <UsageTotals
  341. category="attachments"
  342. totals={totals}
  343. freeUnits={2}
  344. reservedUnits={1}
  345. prepaidUnits={1}
  346. subscription={subscription}
  347. organization={organization}
  348. displayMode="usage"
  349. />
  350. );
  351. expect(screen.getByTestId('gifted-attachments')).toHaveTextContent(
  352. '1 GB Reserved + 2 GB Gifted'
  353. );
  354. });
  355. it('renders accepted percentage for attachments', function () {
  356. const attachments = UsageTotalFixture({accepted: GIGABYTE * 0.6});
  357. render(
  358. <UsageTotals
  359. category="attachments"
  360. totals={attachments}
  361. reservedUnits={1}
  362. prepaidUnits={1}
  363. subscription={subscription}
  364. organization={organization}
  365. displayMode="usage"
  366. />
  367. );
  368. expect(screen.getByText('600 MB')).toBeInTheDocument();
  369. });
  370. it('renders accepted percentage for errors', function () {
  371. const errors = UsageTotalFixture({accepted: 92_400});
  372. render(
  373. <UsageTotals
  374. category={DataCategory.ERRORS}
  375. totals={errors}
  376. reservedUnits={100_000}
  377. prepaidUnits={100_000}
  378. subscription={subscription}
  379. organization={organization}
  380. displayMode="usage"
  381. />
  382. );
  383. expect(screen.getByText('92,400')).toBeInTheDocument();
  384. });
  385. it('renders accepted percentage for transactions', function () {
  386. const transactions = UsageTotalFixture({accepted: 200_000});
  387. render(
  388. <UsageTotals
  389. category={DataCategory.TRANSACTIONS}
  390. totals={transactions}
  391. reservedUnits={100_000}
  392. prepaidUnits={100_000}
  393. subscription={subscription}
  394. organization={organization}
  395. displayMode="usage"
  396. />
  397. );
  398. expect(screen.getByText('200,000')).toBeInTheDocument();
  399. });
  400. it('renders true forward', function () {
  401. render(
  402. <UsageTotals
  403. trueForward
  404. category="errors"
  405. totals={totals}
  406. reservedUnits={100_000}
  407. prepaidUnits={100_000}
  408. subscription={subscription}
  409. organization={organization}
  410. displayMode="usage"
  411. />
  412. );
  413. expect(screen.getByTestId('reserved-errors')).toHaveTextContent(
  414. '100K Reserved (True Forward)'
  415. );
  416. });
  417. it('renders soft cap type on demand', function () {
  418. render(
  419. <UsageTotals
  420. softCapType="ON_DEMAND"
  421. category="errors"
  422. totals={totals}
  423. reservedUnits={100_000}
  424. prepaidUnits={100_000}
  425. subscription={subscription}
  426. organization={organization}
  427. displayMode="usage"
  428. />
  429. );
  430. expect(screen.getByTestId('reserved-errors')).toHaveTextContent(
  431. '100K Reserved (On Demand)'
  432. );
  433. });
  434. it('renders soft cap type true forward', function () {
  435. render(
  436. <UsageTotals
  437. softCapType="TRUE_FORWARD"
  438. category="errors"
  439. totals={totals}
  440. reservedUnits={100_000}
  441. prepaidUnits={100_000}
  442. subscription={subscription}
  443. organization={organization}
  444. displayMode="usage"
  445. />
  446. );
  447. expect(screen.getByTestId('reserved-errors')).toHaveTextContent(
  448. '100K Reserved (True Forward)'
  449. );
  450. });
  451. it('renders true forward with gifted amount', function () {
  452. render(
  453. <UsageTotals
  454. trueForward
  455. category="errors"
  456. totals={totals}
  457. reservedUnits={100_000}
  458. prepaidUnits={100_000}
  459. freeUnits={50_000}
  460. subscription={subscription}
  461. organization={organization}
  462. displayMode="usage"
  463. />
  464. );
  465. expect(screen.getByTestId('gifted-errors')).toHaveTextContent(
  466. '100K Reserved (True Forward) + 50K Gifted'
  467. );
  468. });
  469. it('renders soft cap type on demand with gifted amount', function () {
  470. render(
  471. <UsageTotals
  472. softCapType="ON_DEMAND"
  473. category="errors"
  474. totals={totals}
  475. reservedUnits={100_000}
  476. prepaidUnits={100_000}
  477. freeUnits={50_000}
  478. subscription={subscription}
  479. organization={organization}
  480. displayMode="usage"
  481. />
  482. );
  483. expect(screen.getByTestId('gifted-errors')).toHaveTextContent(
  484. '100K Reserved (On Demand) + 50K Gifted'
  485. );
  486. });
  487. it('renders soft cap type true forward with gifted amount', function () {
  488. render(
  489. <UsageTotals
  490. softCapType="TRUE_FORWARD"
  491. category="errors"
  492. totals={totals}
  493. reservedUnits={100_000}
  494. prepaidUnits={100_000}
  495. freeUnits={50_000}
  496. subscription={subscription}
  497. organization={organization}
  498. displayMode="usage"
  499. />
  500. );
  501. expect(screen.getByTestId('gifted-errors')).toHaveTextContent(
  502. '100K Reserved (True Forward) + 50K Gifted'
  503. );
  504. });
  505. it('renders usage card with Trial if an active trial exists', function () {
  506. subscription.productTrials = [
  507. {
  508. category: DataCategory.REPLAYS,
  509. isStarted: true,
  510. reasonCode: 1001,
  511. startDate: moment().utc().subtract(10, 'days').format(),
  512. endDate: moment().utc().add(20, 'days').format(),
  513. },
  514. ];
  515. render(
  516. <UsageTotals
  517. category="replays"
  518. totals={totals}
  519. reservedUnits={500}
  520. prepaidUnits={500}
  521. subscription={subscription}
  522. organization={organization}
  523. displayMode="usage"
  524. />
  525. );
  526. expect(screen.getByText('Replays trial usage this period')).toBeInTheDocument();
  527. expect(screen.queryByText('Start trial')).not.toBeInTheDocument();
  528. });
  529. it('renders usage card with Start trial button if a potential trial exists', function () {
  530. subscription.productTrials = [
  531. {
  532. category: DataCategory.REPLAYS,
  533. isStarted: false,
  534. reasonCode: 1001,
  535. startDate: undefined,
  536. endDate: moment().utc().add(20, 'years').format(),
  537. },
  538. ];
  539. render(
  540. <UsageTotals
  541. category="replays"
  542. totals={totals}
  543. reservedUnits={500}
  544. prepaidUnits={500}
  545. subscription={subscription}
  546. organization={organization}
  547. displayMode="usage"
  548. />
  549. );
  550. expect(screen.getByText('Replays usage this period')).toBeInTheDocument();
  551. expect(screen.queryByText('Replays Trial')).not.toBeInTheDocument();
  552. expect(screen.getByText('Start trial')).toBeInTheDocument();
  553. });
  554. it('renders usage card as normal if trial has ended', function () {
  555. subscription.productTrials = [
  556. {
  557. category: DataCategory.REPLAYS,
  558. isStarted: true,
  559. reasonCode: 1001,
  560. startDate: moment().utc().subtract(10, 'days').format(),
  561. endDate: moment().utc().subtract(1, 'days').format(),
  562. },
  563. ];
  564. render(
  565. <UsageTotals
  566. category="replays"
  567. totals={totals}
  568. reservedUnits={500}
  569. prepaidUnits={500}
  570. subscription={subscription}
  571. organization={organization}
  572. displayMode="usage"
  573. />
  574. );
  575. expect(screen.getByText('Replays usage this period')).toBeInTheDocument();
  576. expect(screen.queryByText('Replays Trial')).not.toBeInTheDocument();
  577. expect(screen.queryByText('Start trial')).not.toBeInTheDocument();
  578. });
  579. it('renders total usage in dollars', () => {
  580. const spendSubscription = SubscriptionFixture({
  581. organization,
  582. plan: 'am2_team',
  583. planTier: 'am2',
  584. });
  585. const prepaidUsage = 100_000;
  586. const prepaid = prepaidUsage * 2;
  587. spendSubscription.categories.errors = MetricHistoryFixture({
  588. prepaid,
  589. reserved: prepaid,
  590. });
  591. const spendTotals = UsageTotalFixture({
  592. accepted: prepaidUsage,
  593. dropped: 100_000,
  594. });
  595. render(
  596. <UsageTotals
  597. category="errors"
  598. totals={spendTotals}
  599. reservedUnits={200_000}
  600. prepaidUnits={200_000}
  601. subscription={spendSubscription}
  602. organization={organization}
  603. displayMode="cost"
  604. />
  605. );
  606. expect(screen.getByText('Errors spend this period')).toBeInTheDocument();
  607. expect(screen.getByText('$32 Included in Subscription')).toBeInTheDocument();
  608. expect(screen.getByText('50% of $32')).toBeInTheDocument();
  609. });
  610. it('renders total usage', () => {
  611. const spendSubscription = SubscriptionFixture({
  612. organization,
  613. plan: 'am2_team',
  614. planTier: 'am2',
  615. });
  616. const prepaidUsage = 100_000;
  617. const prepaid = prepaidUsage * 2;
  618. spendSubscription.categories.errors = MetricHistoryFixture({
  619. prepaid,
  620. reserved: prepaid,
  621. });
  622. const spendTotals = UsageTotalFixture({
  623. accepted: prepaidUsage,
  624. dropped: 100_000,
  625. });
  626. render(
  627. <UsageTotals
  628. category="errors"
  629. totals={spendTotals}
  630. reservedUnits={200_000}
  631. prepaidUnits={200_000}
  632. subscription={spendSubscription}
  633. organization={organization}
  634. displayMode="usage"
  635. />
  636. );
  637. expect(screen.getByText('Errors usage this period')).toBeInTheDocument();
  638. const totalUsageContainer = screen.getByText('Total Usage');
  639. expect(totalUsageContainer).toBeInTheDocument();
  640. expect(totalUsageContainer.parentElement).toHaveTextContent('100,000Total Usage');
  641. expect(screen.getByText('100K of 200K')).toBeInTheDocument();
  642. });
  643. it('displays plan usage when there is no spend', () => {
  644. render(
  645. <UsageTotals
  646. category="errors"
  647. totals={totals}
  648. reservedUnits={100_000}
  649. prepaidUnits={100_000}
  650. subscription={subscription}
  651. organization={organization}
  652. displayMode="cost"
  653. />
  654. );
  655. // Spend is 0, so we should display included plan usage
  656. expect(screen.getByText('Included in Subscription')).toBeInTheDocument();
  657. expect(screen.getByText('26 of 100K')).toBeInTheDocument();
  658. });
  659. it('displays shared On-Demand max available when another category has used some', () => {
  660. organization.features = ['ondemand-budgets'];
  661. const onDemandCategoryMax = 8000;
  662. const onDemandSubscription = SubscriptionFixture({
  663. organization,
  664. plan: 'am2_team',
  665. planTier: 'am2',
  666. onDemandBudgets: {
  667. budgetMode: OnDemandBudgetMode.SHARED,
  668. enabled: true,
  669. onDemandSpendUsed: onDemandCategoryMax,
  670. sharedMaxBudget: onDemandCategoryMax,
  671. },
  672. });
  673. const prepaid = 200_000;
  674. onDemandSubscription.categories.errors = MetricHistoryFixture({
  675. prepaid,
  676. reserved: prepaid,
  677. // Errors uses half of the shared on demand budget
  678. onDemandSpendUsed: onDemandCategoryMax / 2,
  679. });
  680. // Performance uses some as well
  681. onDemandSubscription.categories.transactions = MetricHistoryFixture({
  682. onDemandSpendUsed: onDemandCategoryMax / 4,
  683. });
  684. render(
  685. <UsageTotals
  686. category="errors"
  687. totals={totals}
  688. reservedUnits={100_000}
  689. prepaidUnits={100_000}
  690. subscription={onDemandSubscription}
  691. organization={organization}
  692. displayMode="cost"
  693. />
  694. );
  695. // Errors has used 50% and it could spend another $20 since performance has used $20
  696. expect(screen.getByText('$40 of $60 ($80 max)')).toBeInTheDocument();
  697. expect(
  698. screen.getByText('$32 Included in Subscription + $40 On-Demand')
  699. ).toBeInTheDocument();
  700. });
  701. it('displays pay-as-you-go max available when another category has used some', () => {
  702. organization.features = ['ondemand-budgets'];
  703. const onDemandCategoryMax = 8000;
  704. const paygSubscription = SubscriptionFixture({
  705. organization,
  706. plan: 'am3_team',
  707. planTier: 'am3',
  708. onDemandMaxSpend: 8000,
  709. });
  710. const prepaid = 200_000;
  711. paygSubscription.categories.errors = MetricHistoryFixture({
  712. prepaid,
  713. reserved: prepaid,
  714. // Errors uses half of the shared on demand budget
  715. onDemandSpendUsed: onDemandCategoryMax / 2,
  716. });
  717. // Spans uses some as well
  718. paygSubscription.categories.spans = MetricHistoryFixture({
  719. onDemandSpendUsed: onDemandCategoryMax / 4,
  720. });
  721. render(
  722. <UsageTotals
  723. category="errors"
  724. totals={totals}
  725. reservedUnits={100_000}
  726. prepaidUnits={100_000}
  727. subscription={paygSubscription}
  728. organization={organization}
  729. displayMode="cost"
  730. />
  731. );
  732. // Errors has used 50% and it could spend another $20 since performance has used $20
  733. expect(screen.getByText('$40 of $60 ($80 max)')).toBeInTheDocument();
  734. expect(
  735. screen.getByText('$51 Included in Subscription + $40 Pay-as-you-go')
  736. ).toBeInTheDocument();
  737. });
  738. it('renders onDemandUsage when over prepaidTotal', function () {
  739. const usageTotals = UsageTotalFixture({
  740. accepted: 150_000,
  741. dropped: 10,
  742. droppedOverQuota: 7,
  743. droppedSpikeProtection: 1,
  744. droppedOther: 2,
  745. });
  746. organization.features = ['ondemand-budgets'];
  747. const paygSubscription = SubscriptionFixture({
  748. organization,
  749. plan: 'am3_team',
  750. planTier: 'am3',
  751. onDemandMaxSpend: 10_00, // $10.00
  752. });
  753. paygSubscription.categories.errors = MetricHistoryFixture({
  754. reserved: 100_000,
  755. onDemandQuantity: 50_000,
  756. });
  757. render(
  758. <UsageTotals
  759. category="errors"
  760. totals={usageTotals}
  761. reservedUnits={100_000}
  762. prepaidUnits={100_000}
  763. subscription={paygSubscription}
  764. organization={organization}
  765. displayMode="usage"
  766. />
  767. );
  768. // Test the header section
  769. expect(screen.getByText('Errors usage this period')).toBeInTheDocument();
  770. expect(screen.getByTestId('reserved-errors')).toHaveTextContent('100K Reserved');
  771. // Test the Pay-as-you-go section
  772. expect(screen.getByText('Pay-as-you-go')).toBeInTheDocument();
  773. expect(screen.getByText('50,000')).toBeInTheDocument(); // On-demand usage amount
  774. // Test the included subscription section
  775. const includedSection = screen.getByText('Included in Subscription').parentElement;
  776. expect(includedSection).toHaveTextContent('100K of 100K');
  777. // Test the total usage section
  778. const totalUsageSection = screen.getByText('Total Usage').parentElement;
  779. expect(totalUsageSection).toHaveTextContent('150,000');
  780. // Verify expand button exists
  781. expect(screen.getByLabelText('Expand usage totals')).toBeInTheDocument();
  782. // Test the usage bars are rendered
  783. const usageBarContainer = document.querySelector('[class*="PlanUseBarContainer"]');
  784. expect(usageBarContainer).toBeInTheDocument();
  785. // Verify both prepaid and on-demand bars exist
  786. const usageBars = document.querySelectorAll(
  787. '[class*="PlanUseBarGroup"] > [class*="PlanUseBar"]'
  788. );
  789. expect(usageBars).toHaveLength(2);
  790. });
  791. it('does not render onDemandUsage when under prepaidTotal', function () {
  792. const usageTotals = UsageTotalFixture({
  793. accepted: 100_000,
  794. dropped: 10,
  795. droppedOverQuota: 7,
  796. droppedSpikeProtection: 1,
  797. droppedOther: 2,
  798. });
  799. organization.features = ['ondemand-budgets'];
  800. const paygSubscription = SubscriptionFixture({
  801. organization,
  802. plan: 'am3_team',
  803. planTier: 'am3',
  804. onDemandMaxSpend: 10_00, // $10.00
  805. });
  806. paygSubscription.categories.errors = MetricHistoryFixture({
  807. reserved: 100_000,
  808. onDemandQuantity: 0,
  809. });
  810. render(
  811. <UsageTotals
  812. category="errors"
  813. totals={usageTotals}
  814. reservedUnits={100_000}
  815. prepaidUnits={100_000}
  816. subscription={paygSubscription}
  817. organization={organization}
  818. displayMode="usage"
  819. />
  820. );
  821. // Test the header section
  822. expect(screen.getByText('Errors usage this period')).toBeInTheDocument();
  823. expect(screen.getByTestId('reserved-errors')).toHaveTextContent('100K Reserved');
  824. // Test the Pay-as-you-go section
  825. expect(screen.getByText('Pay-as-you-go')).toBeInTheDocument();
  826. const paygUsage = screen.getByText('0', {
  827. selector: '[class*="LegendContainer"] [class*="LegendPrice"]',
  828. });
  829. expect(paygUsage).toBeInTheDocument();
  830. expect(paygUsage).toHaveTextContent(/^0$/); // On-demand usage amount
  831. // Test the included subscription section
  832. const includedSection = screen.getByText('Included in Subscription').parentElement;
  833. expect(includedSection).toHaveTextContent('100K of 100K');
  834. // Test the total usage section
  835. const totalUsageSection = screen.getByText('Total Usage').parentElement;
  836. expect(totalUsageSection).toHaveTextContent('100,000');
  837. // Verify expand button exists
  838. expect(screen.getByLabelText('Expand usage totals')).toBeInTheDocument();
  839. // Test the usage bars are rendered
  840. const usageBarContainer = document.querySelector('[class*="PlanUseBarContainer"]');
  841. expect(usageBarContainer).toBeInTheDocument();
  842. // Verify both prepaid and on-demand bars exist
  843. const usageBars = document.querySelectorAll(
  844. '[class*="PlanUseBarGroup"] > [class*="PlanUseBar"]'
  845. );
  846. expect(usageBars).toHaveLength(2);
  847. });
  848. });
  849. describe('calculateCategoryPrepaidUsage', () => {
  850. const organization = OrganizationFixture();
  851. it('calculates prepaid usage of 50%', () => {
  852. const subscription = SubscriptionFixture({
  853. organization,
  854. plan: 'am2_team',
  855. planTier: 'am2',
  856. });
  857. const prepaidUsage = 150_000;
  858. const prepaid = prepaidUsage * 2;
  859. subscription.categories.errors = MetricHistoryFixture({
  860. prepaid,
  861. reserved: prepaid,
  862. });
  863. const totals = UsageTotalFixture({
  864. accepted: prepaidUsage,
  865. dropped: 100_000,
  866. });
  867. expect(
  868. calculateCategoryPrepaidUsage('errors', subscription, totals, prepaid)
  869. ).toEqual({
  870. onDemandUsage: 0,
  871. prepaidPercentUsed: 50,
  872. prepaidPrice: 5000,
  873. prepaidSpend: 2500,
  874. prepaidUsage,
  875. });
  876. });
  877. it('converts annual prepaid price to monthly', () => {
  878. const subscription = SubscriptionFixture({
  879. organization,
  880. plan: 'am2_team',
  881. planTier: 'am2',
  882. });
  883. subscription.planDetails.billingInterval = 'annual';
  884. const prepaidPrice = 10000;
  885. subscription.planDetails.planCategories.errors = [
  886. {events: 100_000, price: prepaidPrice * 12, unitPrice: 0.1, onDemandPrice: 0.2},
  887. ];
  888. const totals = UsageTotalFixture();
  889. expect(
  890. calculateCategoryPrepaidUsage('errors', subscription, totals, prepaidPrice)
  891. ).toEqual({
  892. onDemandUsage: 0,
  893. prepaidPercentUsed: 0,
  894. prepaidPrice,
  895. prepaidSpend: 0,
  896. prepaidUsage: 0,
  897. });
  898. });
  899. it('should not error when prices are not available', () => {
  900. const subscription = SubscriptionFixture({
  901. organization,
  902. plan: 'am2_team',
  903. planTier: 'am2',
  904. });
  905. const prepaidPrice = 0;
  906. delete subscription.planDetails.planCategories.monitorSeats;
  907. const totals = UsageTotalFixture();
  908. expect(
  909. calculateCategoryPrepaidUsage('monitorSeats', subscription, totals, prepaidPrice)
  910. ).toEqual({
  911. onDemandUsage: 0,
  912. prepaidPercentUsed: 0,
  913. prepaidPrice: 0,
  914. prepaidSpend: 0,
  915. prepaidUsage: 0,
  916. });
  917. });
  918. it('on-demand usage should be zero for unlimited reserve', () => {
  919. const subscription = SubscriptionFixture({
  920. organization,
  921. plan: 'am3_t',
  922. planTier: 'am3',
  923. });
  924. const prepaidUsage = 10;
  925. const prepaid = -1;
  926. subscription.categories.monitorSeats = MetricHistoryFixture({
  927. prepaid,
  928. reserved: prepaid,
  929. });
  930. const totals = UsageTotalFixture({
  931. accepted: prepaidUsage,
  932. dropped: 0,
  933. });
  934. expect(
  935. calculateCategoryPrepaidUsage('monitorSeats', subscription, totals, prepaid)
  936. ).toEqual({
  937. onDemandUsage: 0,
  938. prepaidPercentUsed: 0,
  939. prepaidPrice: 0,
  940. prepaidSpend: 0,
  941. prepaidUsage,
  942. });
  943. });
  944. it('calculates onDemandUsage using categoryInfo.onDemandQuantity when over prepaidTotal', () => {
  945. const subscription = SubscriptionFixture({
  946. organization,
  947. plan: 'am2_team',
  948. planTier: 'am2',
  949. });
  950. const prepaid = 100_000;
  951. const totals = UsageTotalFixture({
  952. accepted: 150_000, // totals.accepted > prepaidTotal
  953. });
  954. subscription.categories.errors = MetricHistoryFixture({
  955. reserved: prepaid,
  956. onDemandQuantity: 50_000,
  957. });
  958. const result = calculateCategoryPrepaidUsage('errors', subscription, totals, prepaid);
  959. expect(result.onDemandUsage).toBe(50_000);
  960. expect(result.prepaidUsage).toBe(100_000);
  961. });
  962. it('sets onDemandUsage to zero when totals.accepted <= prepaidTotal', () => {
  963. const subscription = SubscriptionFixture({
  964. organization,
  965. plan: 'am2_team',
  966. planTier: 'am2',
  967. });
  968. const prepaid = 100_000;
  969. const totals = UsageTotalFixture({
  970. accepted: 80_000, // totals.accepted <= prepaidTotal
  971. });
  972. subscription.categories.errors = MetricHistoryFixture({
  973. reserved: prepaid,
  974. onDemandQuantity: 0,
  975. });
  976. const result = calculateCategoryPrepaidUsage('errors', subscription, totals, prepaid);
  977. expect(result.onDemandUsage).toBe(0);
  978. expect(result.prepaidUsage).toBe(80_000);
  979. });
  980. it('sets onDemandUsage to zero when isUnlimitedReserved(prepaidTotal) is true', () => {
  981. const subscription = SubscriptionFixture({
  982. organization,
  983. plan: 'am2_team',
  984. planTier: 'am2',
  985. });
  986. const prepaid = UNLIMITED_RESERVED;
  987. const totals = UsageTotalFixture({
  988. accepted: 150_000,
  989. });
  990. subscription.categories.errors = MetricHistoryFixture({
  991. reserved: prepaid,
  992. onDemandQuantity: 50_000,
  993. });
  994. const result = calculateCategoryPrepaidUsage('errors', subscription, totals, prepaid);
  995. expect(result.onDemandUsage).toBe(0);
  996. expect(result.prepaidUsage).toBe(150_000);
  997. });
  998. it('calculates for reserved budgets with reserved spend', function () {
  999. const subscription = Am3DsEnterpriseSubscriptionFixture({organization});
  1000. const prepaid = 100_000_00;
  1001. const totals = UsageTotalFixture({
  1002. accepted: 150_000,
  1003. });
  1004. subscription.categories.spans!.onDemandQuantity = 50_000;
  1005. const result = calculateCategoryPrepaidUsage(
  1006. 'spans',
  1007. subscription,
  1008. totals,
  1009. prepaid,
  1010. undefined,
  1011. 10_000_00
  1012. );
  1013. expect(result).toEqual({
  1014. onDemandUsage: 0,
  1015. prepaidPercentUsed: 10,
  1016. prepaidPrice: 100_000_00,
  1017. prepaidSpend: 10_000_00,
  1018. prepaidUsage: 150_000,
  1019. });
  1020. const result2 = calculateCategoryPrepaidUsage(
  1021. 'spans',
  1022. subscription,
  1023. totals,
  1024. prepaid,
  1025. undefined,
  1026. 100_000_00
  1027. );
  1028. expect(result2).toEqual({
  1029. onDemandUsage: 50_000,
  1030. prepaidPercentUsed: 100,
  1031. prepaidPrice: 100_000_00,
  1032. prepaidSpend: 100_000_00,
  1033. prepaidUsage: 100_000,
  1034. });
  1035. const result3 = calculateCategoryPrepaidUsage(
  1036. 'spans',
  1037. subscription,
  1038. totals,
  1039. prepaid,
  1040. undefined,
  1041. 0
  1042. );
  1043. expect(result3).toEqual({
  1044. onDemandUsage: 0,
  1045. prepaidPercentUsed: 0,
  1046. prepaidPrice: 100_000_00,
  1047. prepaidSpend: 0,
  1048. prepaidUsage: 150_000,
  1049. });
  1050. });
  1051. it('calculates for reserved budgets with reserved cpe', function () {
  1052. const subscription = Am3DsEnterpriseSubscriptionFixture({organization});
  1053. const prepaid = 100_000_00;
  1054. const totals = UsageTotalFixture({
  1055. accepted: 150_000,
  1056. });
  1057. subscription.categories.spans!.onDemandQuantity = 50_000;
  1058. const result = calculateCategoryPrepaidUsage(
  1059. 'spans',
  1060. subscription,
  1061. totals,
  1062. prepaid,
  1063. 100
  1064. );
  1065. expect(result).toEqual({
  1066. onDemandUsage: 50_000,
  1067. prepaidPercentUsed: 100,
  1068. prepaidPrice: 100_000_00,
  1069. prepaidSpend: 100_000_00,
  1070. prepaidUsage: 100_000,
  1071. });
  1072. });
  1073. });
  1074. describe('calculateCategoryOnDemandUsage', () => {
  1075. const organization = OrganizationFixture();
  1076. it('calculates on demand shared fully used by this category', () => {
  1077. const onDemandCategoryMax = 8000;
  1078. const subscription = SubscriptionFixture({
  1079. organization,
  1080. plan: 'am2_team',
  1081. planTier: 'am2',
  1082. onDemandBudgets: {
  1083. budgetMode: OnDemandBudgetMode.SHARED,
  1084. enabled: true,
  1085. onDemandSpendUsed: onDemandCategoryMax,
  1086. sharedMaxBudget: onDemandCategoryMax,
  1087. },
  1088. });
  1089. subscription.categories.errors = MetricHistoryFixture({
  1090. onDemandSpendUsed: onDemandCategoryMax,
  1091. });
  1092. expect(calculateCategoryOnDemandUsage('errors', subscription)).toEqual({
  1093. onDemandTotalAvailable: onDemandCategoryMax,
  1094. onDemandCategoryMax,
  1095. onDemandCategorySpend: onDemandCategoryMax,
  1096. ondemandPercentUsed: 100,
  1097. });
  1098. });
  1099. it('calculates on demand shared used 100% by another category', () => {
  1100. const onDemandCategoryMax = 8000;
  1101. const subscription = SubscriptionFixture({
  1102. organization,
  1103. plan: 'am2_team',
  1104. planTier: 'am2',
  1105. onDemandBudgets: {
  1106. budgetMode: OnDemandBudgetMode.SHARED,
  1107. enabled: true,
  1108. onDemandSpendUsed: onDemandCategoryMax,
  1109. sharedMaxBudget: onDemandCategoryMax,
  1110. },
  1111. });
  1112. subscription.categories.errors = MetricHistoryFixture({
  1113. onDemandSpendUsed: 0,
  1114. });
  1115. expect(calculateCategoryOnDemandUsage('errors', subscription)).toEqual({
  1116. onDemandTotalAvailable: onDemandCategoryMax,
  1117. onDemandCategoryMax,
  1118. onDemandCategorySpend: 0,
  1119. ondemandPercentUsed: 0,
  1120. });
  1121. });
  1122. it('calculates on demand shared used 50% by another category', () => {
  1123. const onDemandCategoryMax = 8000;
  1124. const subscription = SubscriptionFixture({
  1125. organization,
  1126. plan: 'am2_team',
  1127. planTier: 'am2',
  1128. onDemandBudgets: {
  1129. budgetMode: OnDemandBudgetMode.SHARED,
  1130. enabled: true,
  1131. onDemandSpendUsed: onDemandCategoryMax,
  1132. sharedMaxBudget: onDemandCategoryMax,
  1133. },
  1134. });
  1135. // Replays uses half of the shared on demand budget
  1136. subscription.categories.replays = MetricHistoryFixture({
  1137. onDemandSpendUsed: onDemandCategoryMax / 2,
  1138. });
  1139. expect(calculateCategoryOnDemandUsage('errors', subscription)).toEqual({
  1140. onDemandTotalAvailable: onDemandCategoryMax,
  1141. // Half is left for other categories
  1142. onDemandCategoryMax: onDemandCategoryMax / 2,
  1143. onDemandCategorySpend: 0,
  1144. ondemandPercentUsed: 0,
  1145. });
  1146. });
  1147. });