fix(billing): Account for gifted quantities in productIsEnabled check#113142
Merged
fix(billing): Account for gifted quantities in productIsEnabled check#113142
Conversation
productIsEnabled() checked only `reserved === 0` to determine if a product was PAYG-only, ignoring the `free` (gifted) field. For orgs on am3_t_ent with legacy soft cap and gifted monitors, this caused Cron Monitors and Uptime Monitors to incorrectly show "Upgrade required" even though they had available quota via gifted quantities. Use `prepaid` (which equals `reserved + free`) instead of `reserved` alone, consistent with how the backend quota logic already works. Also fix the same issue in URL-based product selection on the subscription usage overview page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Legacy soft cap orgs with TRUE_FORWARD or ON_DEMAND billing on a category can use that product without prepaid quota. Add softCapType to the productIsEnabled check and URL-based product selection so these orgs don't see "Upgrade required" on the subscription page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
krithikravi
approved these changes
Apr 16, 2026
Member
krithikravi
left a comment
There was a problem hiding this comment.
not ultra familiar with react but the code changes look good to me
subscription.soft_cap (legacy flag, serialized as hasSoftCap) and
metric_history.soft_cap_type (per-category enum) are orthogonal
fields in getsentry that are not kept in sync. Legacy soft cap orgs
can have hasSoftCap=true with soft_cap_type=null on newer categories
like MONITOR_SEAT and UPTIME, because create_new_category_histories
does not inherit soft_cap_type from siblings or from the subscription
flag.
This matches the backend pattern used in getsentry quotas.py,
tally_usage.py, seat_policy/monitor.py, and billingseatassignment.py:
subscription.soft_cap OR metric_history.soft_cap_type is not None
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pull the repeated subscription.categories[productFromQuery as DataCategory] lookup and the productFromQuery as DataCategory cast into locals. No behavior change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The isPaygOnly name read awkwardly through a double negation (!isPaygOnly) and was semantically misleading for the per-category soft cap case (the category is billed via soft cap, not PAYG). Split the condition into two positively-named tiers that mirror the actual access model: - hasNonPaygAccess: prepaid quota, per-category soft cap billing, or subscription-level legacy soft cap - hasPaygBudget: per-category or shared on-demand budget A category is enabled if either tier grants access. Add a regression test for UNLIMITED_RESERVED (-1) prepaid, since the inversion from === 0 to > 0 would silently exclude unlimited (the correct inversion is !== 0). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c8d7ebe. Configure here.
… prepaid The prepaid check in index.tsx used `> 0`, which treats the UNLIMITED_RESERVED=-1 sentinel as "no quota" and silently rewrites the URL back to the current selectedProduct. Real paying customers have reserved=-1 on categories like INSTALLABLE_BUILD and SIZE_ANALYSIS (via correct_enterprise_reserved in getsentry/billing/plans/__init__.py), so deep-linking to those products on the subscription page didn't work. Change to `!== 0` to match productIsEnabled in billing.tsx. Add a regression test that fails without the fix. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dashed
added a commit
that referenced
this pull request
Apr 16, 2026
checkBudgetUsageFor returned UNAVAILABLE for orgs whose profileDuration.reserved is UNLIMITED_RESERVED (-1) because the existing guard required `reserved > 0`. That misclassification caused ContinuousProfilingBillingRequirementBanner to render a misleading "profiling unavailable" banner to customers with unlimited quota — including am3_t_ent / am3_t trial cohorts and enterprise orgs promoted to unlimited reserved via correct_enterprise_reserved(). Replace the `reserved > 0` clause with `(reserved ?? 0) !== 0`, matching the pattern #113142 landed in productIsEnabled (static/gsApp/utils/billing.tsx:1044) and the usage-overview URL selector (static/gsApp/views/subscriptionPage/usageOverview/index.tsx:64). Add unit tests covering every branch of checkBudgetUsageFor and component tests for ContinuousProfilingBillingRequirementBanner pinning the regression case. Refs #113142 Co-Authored-By: Claude <noreply@anthropic.com>
dashed
added a commit
that referenced
this pull request
Apr 16, 2026
`usagePercentage` in `static/gsApp/views/subscriptionPage/usageHistory.tsx` returned `>100%` for historical billing-period records where `prepaid` is `UNLIMITED_RESERVED` (-1), because `usage > -1` is always true for non-negative usage. The call site at the "Used (%)" cell special-cased `RESERVED_BUDGET_QUOTA` (-2) but not `UNLIMITED_RESERVED`, so unlimited categories rendered as ">100%" — the opposite of the truth — on every past-period row in the usage history table. Apply the fix at both layers: - Call site: add an `isUnlimitedReserved(metricHistory.reserved) → UNLIMITED` branch parallel to the existing `RESERVED_BUDGET_QUOTA → 'N/A'` guard. - Helper (`usagePercentage`): early-return `UNLIMITED` when `isUnlimitedReserved(prepaid)`, as defense in depth. Export the helper so the unit-test layer can pin this contract. Affected cohort: enterprise orgs promoted to unlimited via `correct_enterprise_reserved()`, and any billing-history row from a past `am3_t` / `am3_t_ent` trial period. Same cohort as #113142 and #113254. Add unit tests for the now-exported `usagePercentage` (7 cases: null, 0, unlimited, overage, under-quota, zero usage, exact match) and component regression tests for both the fully-unlimited row and the defense-in-depth `reserved > 0, prepaid = -1` case. Refs #113142 Refs #113254 Co-Authored-By: Claude <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Problem
On the subscription page, orgs on
am3_t_entwith legacy soft cap see "Upgrade required — You currently do not have access to this feature" for Cron Monitors and Uptime Monitors, even when those monitors are gifted.Root Cause
productIsEnabled()(frontend) decides whether a billing category is active or locked. Three bugs compounded:Bug 1: Gifted quantities ignored
Only checks
reserved(plan-included quota), ignoringfree(gifted quota).Bug 2: Per-category soft cap type ignored
A category with
softCapType = 'TRUE_FORWARD'or'ON_DEMAND'represents soft cap billing — the org can use the product via forward/ondemand billing. But the function didn't checksoftCapType.Bug 3: Subscription-level legacy soft cap flag ignored (the main miss)
subscription.soft_cap(legacy boolean, serialized ashasSoftCap) andmetric_history.soft_cap_type(per-category enum) are orthogonal fields in getsentry that are not kept in sync. Verified across the getsentry codebase:change_soft_cap()(getsentry/billing/soft_cap.py:17-54) togglessubscription.soft_capbut does NOT touch any metric history'ssoft_cap_type.subscription.soft_cap = "enable soft-cap" in deal_termsindependently from per-categorysoft_cap_type_*fields.create_new_category_histories(getsentry/models/billingmetrichistory.py:301) creates rows for backfilled categories (like MONITOR_SEAT, UPTIME) withsoft_cap_type = None— it does NOT inherit from siblings or from the subscription-levelsoft_capflag.So legacy soft cap orgs whose Cron/Uptime categories were backfilled after original provisioning have
hasSoftCap = truebutsoftCapType = nullon those categories. This is exactly the am3_t_ent scenario reported.Data for the affected orgs
hasSoftCaptruereserved0free0prepaid0softCapTypenullonDemandBudget0Fix
billing.tsx:1043prepaid=reserved + free— accounts for gifted quantities!softCapType— treats soft cap categories as enabled!subscription.hasSoftCap— treats legacy soft cap subscriptions as enabledThis matches the backend-wide pattern:
subscription.soft_cap OR metric_history.soft_cap_type is not Noneused in:getsentry/quotas.pygetsentry/billing/seat_policy/monitor.pygetsentry/billing/tally_usage.pygetsentry/models/billingseatassignment.pyindex.tsx(URL product selection)Parallel changes to the selectability check: use
prepaid, addsoftCapTypecheck, addhasSoftCapcheck (guarded on the category existing on the subscription).Tests Added (16 new across 4 files)
billing.spec.tsx(6 new inproductIsEnableddescribe):index.spec.tsx(4 new):panel.spec.tsx(4 new):table.spec.tsx(2 new):Why this matches the backend
The backend serializer (
getsentry/api/serializers/customer.py:499) sendshasSoftCap: sub.soft_capdirectly. Every quota/seat check in the backend uses theORpattern. The frontend now matches that invariant.