Skip to content

Commit 5a6def8

Browse files
fix(quota alert): copy change (#90204)
"Quota Exceeded" -> "[category] Quota Exceeded" if only one category is exceeded, else "Quotas Exceeded" ![Screenshot 2025-04-23 at 11 35 09 AM](https://github.com/user-attachments/assets/4726d099-a1b5-4c63-9ed0-88f35f8eb1c3) ![Screenshot 2025-04-23 at 11 35 37 AM](https://github.com/user-attachments/assets/e34bb1a1-601b-4933-950a-ed2cbae8187a) If a non-PAYG only category and any PAYG only categories (ie. crons, profiling hours) are exceeded, we still render the singular title (ie. "[non-PAYG only category] Quota Exceeded") if the customer doesn't have PAYG, otherwise we'd render "Quotas Exceeded"
1 parent 581b623 commit 5a6def8

File tree

2 files changed

+79
-22
lines changed

2 files changed

+79
-22
lines changed

static/gsApp/components/navBillingStatus.spec.tsx

+62-20
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,13 @@ describe('PrimaryNavigationQuotaExceeded', function () {
112112
).toBe('Mon Jun 06 2022');
113113
}
114114

115-
it('should render', async function () {
115+
it('should render for multiple categories', async function () {
116116
render(<PrimaryNavigationQuotaExceeded organization={organization} />);
117117

118118
// open the alert
119119
await userEvent.click(await screen.findByRole('button', {name: 'Billing Status'}));
120-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
121-
// doesn't show categories with <1 reserved tier and no PAYG
120+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
121+
// doesn't show categories with <=1 reserved tier and no PAYG
122122
expect(
123123
screen.getByText(
124124
/Youve run out of errors, replays, and spans for this billing cycle./
@@ -129,6 +129,47 @@ describe('PrimaryNavigationQuotaExceeded', function () {
129129
expect(screen.getByRole('checkbox')).not.toBeChecked();
130130
});
131131

132+
it('should render for single category', async function () {
133+
const newSub = SubscriptionFixture({
134+
organization,
135+
plan: 'am3_team',
136+
});
137+
newSub.categories.errors!.usageExceeded = true;
138+
newSub.categories.monitorSeats!.usageExceeded = true;
139+
SubscriptionStore.set(organization.slug, newSub);
140+
localStorage.setItem(
141+
`billing-status-last-shown-categories-${organization.id}`,
142+
'errors' // exceeded categories
143+
);
144+
render(<PrimaryNavigationQuotaExceeded organization={organization} />);
145+
146+
// open the alert
147+
await userEvent.click(await screen.findByRole('button', {name: 'Billing Status'}));
148+
149+
expect(await screen.findByText('Error Quota Exceeded')).toBeInTheDocument();
150+
expect(
151+
screen.getByText(/Youve run out of errors for this billing cycle./)
152+
).toBeInTheDocument(); // doesn't show categories with <=1 reserved tier and no PAYG
153+
expect(screen.getByRole('checkbox')).not.toBeChecked();
154+
});
155+
156+
it('should not render for zero categories', function () {
157+
const newSub = SubscriptionFixture({
158+
organization,
159+
plan: 'am3_team',
160+
});
161+
// these categories are PAYG categories and there is no PAYG, so they should not trigger the alert
162+
newSub.categories.monitorSeats!.usageExceeded = true;
163+
newSub.categories.profileDuration!.usageExceeded = true;
164+
SubscriptionStore.set(organization.slug, newSub);
165+
localStorage.clear();
166+
render(<PrimaryNavigationQuotaExceeded organization={organization} />);
167+
expect(
168+
screen.queryByRole('button', {name: 'Billing Status'})
169+
).not.toBeInTheDocument();
170+
expect(screen.queryByText('Quotas Exceeded')).not.toBeInTheDocument();
171+
});
172+
132173
it('should render PAYG categories when there is shared PAYG', async function () {
133174
localStorage.setItem(
134175
`billing-status-last-shown-categories-${organization.id}`,
@@ -140,7 +181,7 @@ describe('PrimaryNavigationQuotaExceeded', function () {
140181

141182
// open the alert
142183
await userEvent.click(await screen.findByRole('button', {name: 'Billing Status'}));
143-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
184+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
144185
expect(
145186
screen.getByText(
146187
/Youve run out of errors, replays, spans, cron monitors, and continuous profile hours for this billing cycle./
@@ -177,7 +218,7 @@ describe('PrimaryNavigationQuotaExceeded', function () {
177218

178219
// open the alert
179220
await userEvent.click(await screen.findByRole('button', {name: 'Billing Status'}));
180-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
221+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
181222
expect(
182223
screen.getByText(
183224
/Youve run out of errors, replays, spans, and cron monitors for this billing cycle./
@@ -197,7 +238,7 @@ describe('PrimaryNavigationQuotaExceeded', function () {
197238
expect(
198239
screen.queryByRole('button', {name: 'Billing Status'})
199240
).not.toBeInTheDocument();
200-
expect(screen.queryByText('Quota Exceeded')).not.toBeInTheDocument();
241+
expect(screen.queryByText('Quotas Exceeded')).not.toBeInTheDocument();
201242

202243
// reset
203244
subscription.canSelfServe = true;
@@ -208,7 +249,7 @@ describe('PrimaryNavigationQuotaExceeded', function () {
208249

209250
// open the alert
210251
await userEvent.click(await screen.findByRole('button', {name: 'Billing Status'}));
211-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
252+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
212253

213254
// stop the alert from animating
214255
await userEvent.click(screen.getByRole('checkbox'));
@@ -220,7 +261,7 @@ describe('PrimaryNavigationQuotaExceeded', function () {
220261

221262
// open the alert
222263
await userEvent.click(await screen.findByRole('button', {name: 'Billing Status'}));
223-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
264+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
224265
expect(screen.getByText('Request Additional Quota')).toBeInTheDocument();
225266

226267
// click the button
@@ -235,11 +276,12 @@ describe('PrimaryNavigationQuotaExceeded', function () {
235276
organization,
236277
plan: 'am3_f',
237278
});
279+
freeSub.categories.errors!.usageExceeded = true;
238280
freeSub.categories.replays!.usageExceeded = true;
239281
SubscriptionStore.set(organization.slug, freeSub);
240282
localStorage.setItem(
241283
`billing-status-last-shown-categories-${organization.id}`,
242-
'replays'
284+
'errors-replays'
243285
);
244286
MockApiClient.addMockResponse({
245287
method: 'GET',
@@ -251,12 +293,12 @@ describe('PrimaryNavigationQuotaExceeded', function () {
251293

252294
// open the alert
253295
await userEvent.click(await screen.findByRole('button', {name: 'Billing Status'}));
254-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
296+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
255297
expect(screen.getByText('Start Trial')).toBeInTheDocument();
256298

257299
// click the button
258300
await userEvent.click(screen.getByText('Start Trial'));
259-
expect(promptMock).toHaveBeenCalledTimes(1);
301+
expect(promptMock).toHaveBeenCalledTimes(2);
260302
expect(customerPutMock).toHaveBeenCalled();
261303
});
262304

@@ -265,14 +307,14 @@ describe('PrimaryNavigationQuotaExceeded', function () {
265307
expect(
266308
await screen.findByRole('button', {name: 'Billing Status'})
267309
).toBeInTheDocument();
268-
expect(screen.queryByText('Quota Exceeded')).not.toBeInTheDocument();
310+
expect(screen.queryByText('Quotas Exceeded')).not.toBeInTheDocument();
269311

270312
localStorage.clear();
271313
render(<PrimaryNavigationQuotaExceeded organization={organization} />);
272314
expect(
273315
await screen.findByRole('button', {name: 'Billing Status'})
274316
).toBeInTheDocument();
275-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
317+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
276318
assertLocalStorageStateAfterAutoOpen();
277319
});
278320

@@ -298,15 +340,15 @@ describe('PrimaryNavigationQuotaExceeded', function () {
298340
expect(
299341
await screen.findByRole('button', {name: 'Billing Status'})
300342
).toBeInTheDocument();
301-
expect(screen.queryByText('Quota Exceeded')).not.toBeInTheDocument();
343+
expect(screen.queryByText('Quotas Exceeded')).not.toBeInTheDocument();
302344

303345
// even when localStorage is cleared, the alert should not show
304346
localStorage.clear();
305347
render(<PrimaryNavigationQuotaExceeded organization={organization} />);
306348
expect(
307349
await screen.findByRole('button', {name: 'Billing Status'})
308350
).toBeInTheDocument();
309-
expect(screen.queryByText('Quota Exceeded')).not.toBeInTheDocument();
351+
expect(screen.queryByText('Quotas Exceeded')).not.toBeInTheDocument();
310352
expect(localStorage).toHaveLength(0);
311353
});
312354

@@ -330,19 +372,19 @@ describe('PrimaryNavigationQuotaExceeded', function () {
330372
});
331373
localStorage.clear();
332374
render(<PrimaryNavigationQuotaExceeded organization={organization} />);
333-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
375+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
334376
});
335377

336378
it('should auto open the alert when categories have changed', async function () {
337379
render(<PrimaryNavigationQuotaExceeded organization={organization} />);
338-
expect(screen.queryByText('Quota Exceeded')).not.toBeInTheDocument();
380+
expect(screen.queryByText('Quotas Exceeded')).not.toBeInTheDocument();
339381

340382
localStorage.setItem(
341383
`billing-status-last-shown-categories-${organization.id}`,
342384
'errors-replays'
343385
); // spans not included, so alert should show even though last opened "today"
344386
render(<PrimaryNavigationQuotaExceeded organization={organization} />);
345-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
387+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
346388
assertLocalStorageStateAfterAutoOpen();
347389
});
348390

@@ -352,7 +394,7 @@ describe('PrimaryNavigationQuotaExceeded', function () {
352394
'Sun Jun 05 2022'
353395
); // more than a day, so alert should show even though categories haven't changed
354396
render(<PrimaryNavigationQuotaExceeded organization={organization} />);
355-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
397+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
356398
assertLocalStorageStateAfterAutoOpen();
357399
});
358400

@@ -362,7 +404,7 @@ describe('PrimaryNavigationQuotaExceeded', function () {
362404
'Sun May 29 2022'
363405
); // last seen before current usage cycle started, so alert should show even though categories haven't changed
364406
render(<PrimaryNavigationQuotaExceeded organization={organization} />);
365-
expect(await screen.findByText('Quota Exceeded')).toBeInTheDocument();
407+
expect(await screen.findByText('Quotas Exceeded')).toBeInTheDocument();
366408
assertLocalStorageStateAfterAutoOpen();
367409
});
368410
});

static/gsApp/components/navBillingStatus.tsx

+17-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ import {
3131
type Subscription,
3232
} from 'getsentry/types';
3333
import {getCategoryInfoFromPlural} from 'getsentry/utils/billing';
34-
import {listDisplayNames, sortCategoriesWithKeys} from 'getsentry/utils/dataCategory';
34+
import {
35+
getSingularCategoryName,
36+
listDisplayNames,
37+
sortCategoriesWithKeys,
38+
} from 'getsentry/utils/dataCategory';
3539
import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
3640

3741
const ANIMATE_PROPS: MotionProps = {
@@ -80,7 +84,18 @@ function QuotaExceededContent({
8084
<HeaderTitle>{t('Billing Status')}</HeaderTitle>
8185
</Header>
8286
<Body>
83-
<Title>{t('Quota Exceeded')}</Title>
87+
<Title>
88+
{exceededCategories.length === 1
89+
? tct('[category] Quota Exceeded', {
90+
category: getSingularCategoryName({
91+
plan: subscription.planDetails,
92+
category: exceededCategories[0] as DataCategory,
93+
hadCustomDynamicSampling: subscription.hadCustomDynamicSampling,
94+
title: true,
95+
}),
96+
})
97+
: t('Quotas Exceeded')}
98+
</Title>
8499
<Description>
85100
{tct(
86101
'You’ve run out of [exceededCategories] for this billing cycle. This means we are no longer monitoring or ingesting events and showing them in Sentry.',

0 commit comments

Comments
 (0)