Hide Ultimate trial link from Ultimate tier owners
What does this MR do and why?
Owners of top-level namespaces on a community program plan (Open Source, Education, Non-Profit — all stored as the opensource plan) see "Start an Ultimate trial" in the profile dropdown, even though the namespace is already on an Ultimate-tier plan. A customer reported this.
The dropdown item is built by GlobalMediator in ee/app/presenters/gitlab_subscriptions/upgrade_presenter.rb, for the case where there is no group or project in context:
can_upgrade_subscription?returnsfalse. It relies onUser#owned_free_or_trial_groups_with_limit, which uses thefree_or_trialnamespace scope. That scope only matches the free plan or an active trial, so anopensourcenamespace does not match.can_start_trial?then returnstrue. It callsGitlabSubscriptions::Trials.no_eligible_namespaces_for_user?, which istruewhen the user owns no namespace a trial could be applied to.Namespaces::TrialEligibleFinderfilters onPlan::PLANS_ELIGIBLE_FOR_TRIAL(default,free,premium), which excludesopensource.- With the first branch skipped and the second one true, the mediator falls through to the trial link.
The fix adds a check in can_start_trial?: it also requires that the user does not own any Ultimate-tier namespace, using the existing User#owns_paid_namespace? helper with the existing Plan::ULTIMATE_TIER_PLANS constant (gold, ultimate, opensource, ultimate_trial, ultimate_trial_paid_customer). Using the tier constant instead of hardcoding opensource means any future plan added to the Ultimate tier is covered automatically, and it also fixes the same symptom for owners of paid Ultimate and Gold namespaces, who hit the identical fall-through today.
What this MR deliberately does not change: Namespaces::TrialEligibleFinder and Plan::PLANS_ELIGIBLE_FOR_TRIAL. That finder is shared with the real trial-start flow (GitlabSubscriptions::TrialsController, GitlabSubscriptions::Trials::UltimateCreateService, ApplyTrialService). It already correctly excludes opensource, because a namespace already on the Ultimate tier should not be trial-eligible. Adding opensource there would make those namespaces genuinely eligible to start a trial, which is wrong.
No frontend change is needed. app/assets/javascripts/super_sidebar/components/user_menu.vue renders the item from data.upgrade_link with no client-side eligibility logic, so the presenter is the single source of truth. When the presenter returns an empty hash, the dropdown item is not rendered at all.
Performance: the new check is one database query, inside the existing 10 minute Rails cache entry for can_start_trial, and it only runs for users who reach the fallback branch at all. It is evaluated first, so owners of an Ultimate-tier namespace short-circuit before Namespaces::TrialEligibleFinder runs its own query. The reverse order would instead save that query for owners of a Premium or Bronze namespace. Neither order dominates, and for users who own no groups both queries run either way.
Behavior after this change:
- Owner of an
opensourcenamespace: no dropdown item. - Owner of a free namespace: unchanged, still "Upgrade subscription" pointing at the group billing page.
- Owner of both a free and an
opensourcenamespace: unchanged, still "Upgrade subscription" for the free group, because that branch is evaluated first. - User who owns no groups: unchanged, still "Start an Ultimate trial".
- Owner of a namespace whose Ultimate trial ended today or earlier: no dropdown item, where before there was a trial link.
in_active_trialrequirestrial_ends_onto be tomorrow or later, so those namespaces miss the first branch, and their plan name is still inPlan::ULTIMATE_TIER_PLANS. Suppressing the link looks right, since that namespace has already had its trial, but calling it out as a deliberate side effect.
Tests: ee/spec/presenters/gitlab_subscriptions/upgrade_presenter_spec.rb gains a case for an owner of an Ultimate-tier namespace (no dropdown item) and one for an owner of both a free and an Ultimate-tier namespace (still "Upgrade subscription"). ee/spec/models/ee/user_spec.rb covers owns_paid_namespace? scoped to Plan::ULTIMATE_TIER_PLANS: it matches a namespace on the opensource plan and does not match bronze or free namespaces. The plan-name coverage lives in the model spec because RSpec/FactoryBot/AvoidCreate applies to presenter specs.
Feature flag: none. The change is a single conditional in a presenter, is trivially revertable, and gating it would add more risk surface than it removes.
References
- Issue: #628891
Screenshots or screen recordings
| Before | After |
|---|---|
| Screenshot attached to the issue (#628891): profile dropdown shows "Start an Ultimate trial" for a community program namespace owner | Dropdown item is absent |
How to set up and validate locally
-
Simulate a SaaS instance: set
GITLAB_SIMULATE_SAAS=1in the GDK environment and restart Rails. -
In the Rails console, put a group you own on the
opensourceplan:user = User.find_by_username('root') group = Group.find_by_full_path('your-group') plan = Plan.find_or_create_by!(name: 'opensource') { |p| p.title = 'Open Source' } group.gitlab_subscription&.destroy! GitlabSubscription.create!(namespace: group, hosted_plan: plan, seats: 10, start_date: Date.current, end_date: 1.year.from_now) -
Clear the cached dropdown state for that user, because both branches are cached for 10 minutes:
Rails.cache.delete(['users', user.id, 'can_start_trial']) Rails.cache.delete(['users', user.id, 'owned_groups_url']) -
Sign in as that user and open the profile dropdown in the top right. On master it shows "Start an Ultimate trial". On this branch there is no subscription item.
-
Optionally confirm the free-plan path is unaffected: repeat with plan name
freeand confirm the dropdown shows "Upgrade subscription".
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.