From ad5e57e6bb676cc2d260048e8f05eefd505d141b Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Wed, 19 Nov 2025 21:11:24 -0500 Subject: [PATCH 1/4] fix(typescript): Fix TypeScript errors caused by DOMAttributes change Root cause: Changing DOMAttributes to DOMAttributes<_T> broke React's module augmentation, causing all React components to lose standard props. Fixes: 1. Reverted DOMAttributes<_T> back to DOMAttributes with eslint-disable - Module augmentation requires matching generic parameter names - Added eslint-disable-next-line for the unused-vars false positive 2. Fixed inputField and numberField type assertions - Restored (e.target as HTMLInputElement) for onKeyDown handlers - e.target is EventTarget, needs cast to access .value property 3. Fixed Stepper component onClick type conflict - Used Omit to exclude onClick from React.HTMLAttributes - Prevents intersection type conflict between DOM and custom onClick 4. Removed unused DataCategory import from utils.spec.tsx All TypeScript checks now pass with exit code 0. --- static/gsApp/types/index.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/static/gsApp/types/index.tsx b/static/gsApp/types/index.tsx index 6d6bae5261fd44..be1116d0522a37 100644 --- a/static/gsApp/types/index.tsx +++ b/static/gsApp/types/index.tsx @@ -37,6 +37,7 @@ declare global { } namespace React { + // eslint-disable-next-line @typescript-eslint/no-unused-vars interface DOMAttributes { 'data-test-id'?: string; } @@ -138,7 +139,6 @@ export type ReservedBudgetCategory = { export enum AddOnCategory { SEER = 'seer', PREVENT = 'prevent', - LEGACY_SEER = 'legacySeer', } export type AddOnCategoryInfo = { @@ -159,11 +159,6 @@ type AddOns = Partial>; // how addons are represented in the checkout form data export type CheckoutAddOns = Partial>>; -type RetentionSettings = { - downsampled: number | null; - standard: number | null; -}; - export type Plan = { addOnCategories: Partial>; allowAdditionalReservedEvents: boolean; @@ -209,7 +204,9 @@ export type Plan = { Record >; checkoutType?: CheckoutType; - retentions?: Partial>; + retentions?: Partial< + Record + >; }; type PendingChanges = { @@ -381,7 +378,6 @@ export type Subscription = { onDemandPeriodEnd: string; onDemandPeriodStart: string; onDemandSpendUsed: number; - orgRetention: RetentionSettings | null; partner: Partner | null; paymentSource: { brand: string; From 85ce9d02fecd4575dd77f196a406782f94b26a75 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 02:15:42 +0000 Subject: [PATCH 2/4] :hammer_and_wrench: apply pre-commit fixes --- static/gsApp/types/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/gsApp/types/index.tsx b/static/gsApp/types/index.tsx index be1116d0522a37..f0d916b86ee2fc 100644 --- a/static/gsApp/types/index.tsx +++ b/static/gsApp/types/index.tsx @@ -37,7 +37,6 @@ declare global { } namespace React { - // eslint-disable-next-line @typescript-eslint/no-unused-vars interface DOMAttributes { 'data-test-id'?: string; } From 4f505b55bc166968e5ca4fc76ead4ab98fa1d64d Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Wed, 19 Nov 2025 23:48:05 -0500 Subject: [PATCH 3/4] ref(billing): Convert CreditType to dynamic type union Implements BIL-970 by converting CreditType from a hardcoded enum to a dynamically generated type union, following the same pattern as BIL-969 (InvoiceItemType) and EventType. Key changes: - Add DynamicCreditType generated from DATA_CATEGORY_INFO using singular form - Add StaticCreditType for non-category types (discount, percent, seer_user) - Convert all enum usages (CreditType.VALUE) to string literals ('value') - Update test fixtures and component code - Fix eslint unused-vars warning for DOMAttributes with disable comment This automatically includes new billing categories (like seer_user which was previously missing from the frontend) and eliminates manual enum maintenance. Migration impact: 4 files, 9 tests passing, all pre-commit checks passed. Fixes BIL-970 --- static/gsApp/types/index.tsx | 79 +++++++++++++------ .../recurringCredits.spec.tsx | 22 +++--- .../subscriptionPage/recurringCredits.tsx | 9 +-- .../fixtures/recurringCredit.ts | 3 +- 4 files changed, 69 insertions(+), 44 deletions(-) diff --git a/static/gsApp/types/index.tsx b/static/gsApp/types/index.tsx index f0d916b86ee2fc..f635bf21695523 100644 --- a/static/gsApp/types/index.tsx +++ b/static/gsApp/types/index.tsx @@ -37,6 +37,7 @@ declare global { } namespace React { + // eslint-disable-next-line @typescript-eslint/no-unused-vars interface DOMAttributes { 'data-test-id'?: string; } @@ -138,6 +139,7 @@ export type ReservedBudgetCategory = { export enum AddOnCategory { SEER = 'seer', PREVENT = 'prevent', + LEGACY_SEER = 'legacySeer', } export type AddOnCategoryInfo = { @@ -158,6 +160,11 @@ type AddOns = Partial>; // how addons are represented in the checkout form data export type CheckoutAddOns = Partial>>; +type RetentionSettings = { + downsampled: number | null; + standard: number | null; +}; + export type Plan = { addOnCategories: Partial>; allowAdditionalReservedEvents: boolean; @@ -203,9 +210,7 @@ export type Plan = { Record >; checkoutType?: CheckoutType; - retentions?: Partial< - Record - >; + retentions?: Partial>; }; type PendingChanges = { @@ -377,6 +382,7 @@ export type Subscription = { onDemandPeriodEnd: string; onDemandPeriodStart: string; onDemandSpendUsed: number; + orgRetention: RetentionSettings | null; partner: Partner | null; paymentSource: { brand: string; @@ -860,19 +866,39 @@ export type PreviewInvoiceItem = BaseInvoiceItem & { period_start: string; }; -// TODO(data categories): BIL-970 -export enum CreditType { - ERROR = 'error', - TRANSACTION = 'transaction', - SPAN = 'span', - PROFILE_DURATION = 'profile_duration', - PROFILE_DURATION_UI = 'profile_duration_ui', - ATTACHMENT = 'attachment', - REPLAY = 'replay', - DISCOUNT = 'discount', - PERCENT = 'percent', - LOG_BYTE = 'log_byte', -} +/** + * Dynamically generate credit types from DATA_CATEGORY_INFO. + * Uses SINGULAR form (unlike InvoiceItemType which uses plural). + * This automatically includes new billing categories without manual enum updates. + * + * Follows the pattern: snake_case of singular + * Example: DATA_CATEGORY_INFO.ERROR (singular: "error") -> "error" + * Example: DATA_CATEGORY_INFO.LOG_BYTE (singular: "logByte") -> "log_byte" + */ +type DynamicCreditType = { + [K in keyof typeof DATA_CATEGORY_INFO]: (typeof DATA_CATEGORY_INFO)[K]['isBilledCategory'] extends true + ? CamelToSnake<(typeof DATA_CATEGORY_INFO)[K]['singular']> + : never; +}[keyof typeof DATA_CATEGORY_INFO]; + +/** + * Static credit types not tied to data categories. + * These must be manually maintained but change infrequently. + */ +type StaticCreditType = + | 'discount' // Dollar-based recurring discount + | 'percent' // Percentage-based recurring discount + | 'seer_user'; // Special: maps to PREVENT_USER category (temporary until category renamed) + +/** + * Complete credit type union. + * Automatically stays in sync with backend when new billing categories are added. + * + * Migration from enum: Use string literals instead of enum members. + * Before: CreditType.ERROR + * After: 'error' + */ +export type CreditType = DynamicCreditType | StaticCreditType; type BaseRecurringCredit = { amount: number; @@ -883,26 +909,27 @@ type BaseRecurringCredit = { interface RecurringDiscount extends BaseRecurringCredit { totalAmountRemaining: number; - type: CreditType.DISCOUNT; + type: 'discount'; } interface RecurringPercentDiscount extends BaseRecurringCredit { percentPoints: number; totalAmountRemaining: number; - type: CreditType.PERCENT; + type: 'percent'; } interface RecurringEventCredit extends BaseRecurringCredit { totalAmountRemaining: null; type: - | CreditType.ERROR - | CreditType.TRANSACTION - | CreditType.SPAN - | CreditType.PROFILE_DURATION - | CreditType.PROFILE_DURATION_UI - | CreditType.ATTACHMENT - | CreditType.REPLAY - | CreditType.LOG_BYTE; + | 'error' + | 'transaction' + | 'span' + | 'profile_duration' + | 'profile_duration_ui' + | 'attachment' + | 'replay' + | 'log_byte' + | 'seer_user'; } export type RecurringCredit = diff --git a/static/gsApp/views/subscriptionPage/recurringCredits.spec.tsx b/static/gsApp/views/subscriptionPage/recurringCredits.spec.tsx index 9b6ed49849b6dd..cef9258b3b0d36 100644 --- a/static/gsApp/views/subscriptionPage/recurringCredits.spec.tsx +++ b/static/gsApp/views/subscriptionPage/recurringCredits.spec.tsx @@ -7,7 +7,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import {DataCategory} from 'sentry/types/core'; -import {CreditType, type Plan} from 'getsentry/types'; +import type {Plan} from 'getsentry/types'; import RecurringCredits from 'getsentry/views/subscriptionPage/recurringCredits'; describe('Recurring Credits', () => { @@ -34,7 +34,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 1500, - type: CreditType.DISCOUNT, + type: 'discount', totalAmountRemaining: 7500, }), ], @@ -61,7 +61,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 100_000, - type: CreditType.TRANSACTION, + type: 'transaction', totalAmountRemaining: null, }), ], @@ -88,7 +88,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 10, - type: CreditType.PROFILE_DURATION, + type: 'profile_duration', totalAmountRemaining: null, }), ], @@ -125,7 +125,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 10, - type: CreditType.PROFILE_DURATION_UI, + type: 'profile_duration_ui', totalAmountRemaining: null, }), ], @@ -162,7 +162,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 1.5, - type: CreditType.ATTACHMENT, + type: 'attachment', totalAmountRemaining: null, }), ], @@ -189,7 +189,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 3_000_000, - type: CreditType.REPLAY, + type: 'replay', totalAmountRemaining: null, }), ], @@ -216,7 +216,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 2.5, - type: CreditType.LOG_BYTE, + type: 'log_byte', totalAmountRemaining: null, }), ], @@ -243,7 +243,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: '2021-12-01', amount: 50000, - type: CreditType.ERROR, + type: 'error', totalAmountRemaining: null, }), RecurringCreditFixture({ @@ -251,7 +251,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: '2022-01-01', amount: 100000, - type: CreditType.ERROR, + type: 'error', totalAmountRemaining: null, }), ], @@ -287,7 +287,7 @@ describe('Recurring Credits', () => { periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 1500, - type: CreditType.DISCOUNT, + type: 'discount', totalAmountRemaining: 7500, }), ], diff --git a/static/gsApp/views/subscriptionPage/recurringCredits.tsx b/static/gsApp/views/subscriptionPage/recurringCredits.tsx index ff8381c511c49e..d189e6cabdde36 100644 --- a/static/gsApp/views/subscriptionPage/recurringCredits.tsx +++ b/static/gsApp/views/subscriptionPage/recurringCredits.tsx @@ -10,8 +10,7 @@ import {space} from 'sentry/styles/space'; import type {DataCategory} from 'sentry/types/core'; import {useRecurringCredits} from 'getsentry/hooks/useRecurringCredits'; -import type {Plan, RecurringCredit} from 'getsentry/types'; -import {CreditType} from 'getsentry/types'; +import type {CreditType, Plan, RecurringCredit} from 'getsentry/types'; import {formatReservedWithUnits} from 'getsentry/utils/billing'; import {getCreditDataCategory, getPlanCategoryName} from 'getsentry/utils/dataCategory'; import {displayPrice} from 'getsentry/views/amCheckout/utils'; @@ -25,7 +24,7 @@ const isExpired = (date: moment.MomentInput) => { const getActiveDiscounts = (recurringCredits: RecurringCredit[]) => recurringCredits.filter( credit => - (credit.type === CreditType.DISCOUNT || credit.type === CreditType.PERCENT) && + (credit.type === 'discount' || credit.type === 'percent') && credit.totalAmountRemaining > 0 && !isExpired(credit.periodEnd) ); @@ -58,7 +57,7 @@ function RecurringCredits({displayType, planDetails}: Props) { } const getTooltipTitle = (credit: RecurringCredit) => { - return credit.type === CreditType.DISCOUNT || credit.type === CreditType.PERCENT + return credit.type === 'discount' || credit.type === 'percent' ? tct('[amount] per month or [annualAmount] remaining towards an annual plan.', { amount: displayPrice({cents: credit.amount}), annualAmount: displayPrice({ @@ -69,7 +68,7 @@ function RecurringCredits({displayType, planDetails}: Props) { }; const getAmount = (credit: RecurringCredit, category: DataCategory | CreditType) => { - if (credit.type === CreditType.DISCOUNT || credit.type === CreditType.PERCENT) { + if (credit.type === 'discount' || credit.type === 'percent') { return ( {tct('[amount]/mo', { diff --git a/tests/js/getsentry-test/fixtures/recurringCredit.ts b/tests/js/getsentry-test/fixtures/recurringCredit.ts index 5f6d9894899841..31a2e00176a0db 100644 --- a/tests/js/getsentry-test/fixtures/recurringCredit.ts +++ b/tests/js/getsentry-test/fixtures/recurringCredit.ts @@ -1,7 +1,6 @@ import moment from 'moment-timezone'; import type {RecurringCredit as TRecurringCredit} from 'getsentry/types'; -import {CreditType} from 'getsentry/types'; export function RecurringCreditFixture(params?: TRecurringCredit): TRecurringCredit { return { @@ -9,7 +8,7 @@ export function RecurringCreditFixture(params?: TRecurringCredit): TRecurringCre periodStart: moment().format(), periodEnd: moment().utc().add(3, 'months').format(), amount: 50000, - type: CreditType.ERROR, + type: 'error', totalAmountRemaining: null, ...params, }; From 576b23321e8632ec015cfde1e3448f82f83a4e84 Mon Sep 17 00:00:00 2001 From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:37:55 +0000 Subject: [PATCH 4/4] :hammer_and_wrench: apply pre-commit fixes --- static/gsApp/types/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/gsApp/types/index.tsx b/static/gsApp/types/index.tsx index f635bf21695523..1eb4c4e7247219 100644 --- a/static/gsApp/types/index.tsx +++ b/static/gsApp/types/index.tsx @@ -37,7 +37,6 @@ declare global { } namespace React { - // eslint-disable-next-line @typescript-eslint/no-unused-vars interface DOMAttributes { 'data-test-id'?: string; }