Skip to content

Commit 239850c

Browse files
authored
feat(billing): Add gift formatting for Logs (#95482)
Closes: https://linear.app/getsentry/issue/BIL-1092/add-formatting-for-gifting Adds specific handling for `DataCategory.LOG_BYTE` with GB formatting
1 parent a1f46a3 commit 239850c

File tree

5 files changed

+97
-23
lines changed

5 files changed

+97
-23
lines changed

static/gsAdmin/components/addGiftEventsAction.spec.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,67 @@ describe('Gift', function () {
327327
});
328328
});
329329

330+
describe('Log Bytes', function () {
331+
const triggerGiftModal = () => {
332+
openAdminConfirmModal({
333+
renderModalSpecificContent: deps => (
334+
<AddGiftEventsAction
335+
subscription={mockSub}
336+
dataCategory={DataCategory.LOG_BYTE}
337+
billedCategoryInfo={BILLED_DATA_CATEGORY_INFO[DataCategoryExact.LOG_BYTE]}
338+
{...deps}
339+
/>
340+
),
341+
});
342+
};
343+
344+
function getLogBytesInput() {
345+
return screen.getByRole('textbox', {
346+
name: 'How many log bytes in GB?',
347+
});
348+
}
349+
350+
async function setNumLogBytes(numLogBytes: string) {
351+
await userEvent.clear(getLogBytesInput());
352+
await userEvent.type(getLogBytesInput(), numLogBytes);
353+
}
354+
355+
it('has valid log bytes input', async function () {
356+
const maxValue = BILLED_DATA_CATEGORY_INFO[DataCategoryExact.LOG_BYTE].maxAdminGift;
357+
triggerGiftModal();
358+
renderGlobalModal();
359+
360+
const logBytesInput = getLogBytesInput();
361+
362+
await setNumLogBytes('1');
363+
expect(logBytesInput).toHaveValue('1');
364+
expect(logBytesInput).toHaveAccessibleDescription('Total: 1 GB');
365+
366+
await setNumLogBytes('-50');
367+
expect(logBytesInput).toHaveValue('50');
368+
expect(logBytesInput).toHaveAccessibleDescription('Total: 50 GB');
369+
370+
await setNumLogBytes(`${maxValue + 5}`);
371+
expect(logBytesInput).toHaveValue('10000');
372+
expect(logBytesInput).toHaveAccessibleDescription('Total: 10,000 GB');
373+
374+
await setNumLogBytes('10,');
375+
expect(logBytesInput).toHaveValue('10');
376+
expect(logBytesInput).toHaveAccessibleDescription('Total: 10 GB');
377+
378+
await setNumLogBytes('5.');
379+
expect(logBytesInput).toHaveValue('5');
380+
expect(logBytesInput).toHaveAccessibleDescription('Total: 5 GB');
381+
});
382+
383+
it('disables confirm button when no number is entered', function () {
384+
triggerGiftModal();
385+
386+
renderGlobalModal();
387+
expect(screen.getByTestId('confirm-button')).toBeDisabled();
388+
});
389+
});
390+
330391
describe('Uptime Monitors', function () {
331392
const triggerGiftModal = () => {
332393
openAdminConfirmModal({

static/gsAdmin/components/addGiftEventsAction.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import type {
99
AdminConfirmRenderProps,
1010
} from 'admin/components/adminConfirmationModal';
1111
import type {BilledDataCategoryInfo, Subscription} from 'getsentry/types';
12-
import {getPlanCategoryName} from 'getsentry/utils/dataCategory';
12+
import {
13+
getPlanCategoryName,
14+
isByteCategory,
15+
isContinuousProfiling,
16+
} from 'getsentry/utils/dataCategory';
1317

1418
/** @internal exported for tests only */
1519
export function getFreeEventsKey(dataCategory: DataCategory) {
@@ -90,20 +94,19 @@ class AddGiftEventsAction extends Component<Props, State> {
9094
const {freeEvents} = this.state;
9195

9296
function getlabel() {
93-
if (dataCategory === DataCategory.ATTACHMENTS) {
94-
return 'How many attachments in GB?';
95-
}
96-
if (
97-
dataCategory === DataCategory.PROFILE_DURATION ||
98-
dataCategory === DataCategory.PROFILE_DURATION_UI
99-
) {
100-
return 'How many profile hours?';
101-
}
10297
const categoryName = getPlanCategoryName({
10398
plan: subscription.planDetails,
10499
category: dataCategory,
105100
capitalize: false,
106101
});
102+
103+
if (isByteCategory(dataCategory)) {
104+
return `How many ${categoryName} in GB?`;
105+
}
106+
if (isContinuousProfiling(dataCategory)) {
107+
return 'How many profile hours?';
108+
}
109+
107110
const multiplier = billedCategoryInfo?.freeEventsMultiple ?? 0;
108111
const addToMessage =
109112
multiplier > 1
@@ -115,17 +118,14 @@ class AddGiftEventsAction extends Component<Props, State> {
115118
const total = this.calculatedTotal.toLocaleString();
116119
function getHelp() {
117120
let postFix = '';
118-
if (
119-
dataCategory === DataCategory.PROFILE_DURATION ||
120-
dataCategory === DataCategory.PROFILE_DURATION_UI
121-
) {
121+
if (isContinuousProfiling(dataCategory)) {
122122
if (total === '1') {
123123
postFix = ' hour';
124124
} else {
125125
postFix = ' hours';
126126
}
127127
}
128-
if (dataCategory === DataCategory.ATTACHMENTS) {
128+
if (isByteCategory(dataCategory)) {
129129
postFix = ' GB';
130130
}
131131
return `Total: ${total}${postFix}`;

static/gsApp/constants.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,16 @@ export const BILLED_DATA_CATEGORY_INFO = {
167167
...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.SEER_SCANNER],
168168
feature: 'seer-billing',
169169
},
170+
[DataCategoryExact.LOG_BYTE]: {
171+
...DEFAULT_BILLED_DATA_CATEGORY_INFO[DataCategoryExact.LOG_BYTE],
172+
canAllocate: false,
173+
canProductTrial: true,
174+
maxAdminGift: 10_000,
175+
freeEventsMultiple: 1,
176+
hasSpikeProtection: false,
177+
feature: 'logs-billing',
178+
reservedVolumeTooltip: t(
179+
'Log bytes represent the amount of log data ingested and stored.'
180+
),
181+
},
170182
} as const satisfies Record<DataCategoryExact, BilledDataCategoryInfo>;

static/gsApp/utils/dataCategory.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import upperFirst from 'lodash/upperFirst';
22

33
import {DATA_CATEGORY_INFO} from 'sentry/constants';
4-
import type {DataCategoryExact} from 'sentry/types/core';
5-
import {DataCategory} from 'sentry/types/core';
4+
import {DataCategory, DataCategoryExact} from 'sentry/types/core';
65
import type {Organization} from 'sentry/types/organization';
76
import oxfordizeArray from 'sentry/utils/oxfordizeArray';
87
import {toTitleCase} from 'sentry/utils/string/toTitleCase';
@@ -69,11 +68,13 @@ export function getPlanCategoryName({
6968
}: CategoryNameProps) {
7069
const displayNames = plan?.categoryDisplayNames?.[category];
7170
const categoryName =
72-
category === DataCategory.SPANS && hadCustomDynamicSampling
73-
? 'accepted spans'
74-
: displayNames
75-
? displayNames.plural
76-
: category;
71+
category === DataCategory.LOG_BYTE
72+
? 'log bytes'
73+
: category === DataCategory.SPANS && hadCustomDynamicSampling
74+
? 'accepted spans'
75+
: displayNames
76+
? displayNames.plural
77+
: category;
7778
return title
7879
? toTitleCase(categoryName, {allowInnerUpperCase: true})
7980
: capitalize

static/gsApp/views/subscriptionPage/recurringCredits.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ describe('Recurring Credits', function () {
229229

230230
await screen.findByRole('heading', {name: /recurring credits/i});
231231

232-
expect(screen.getByText('logBytes')).toBeInTheDocument();
232+
expect(screen.getByText('log bytes')).toBeInTheDocument();
233233
expect(screen.getByTestId('amount')).toHaveTextContent('+2.5 GB/mo');
234234
});
235235

0 commit comments

Comments
 (0)