Add Explore Premium request modal to the profile menu
What does this MR do and why?
This implements a painted door test for a Premium upgrade request in the profile menu, per https://gitlab.com/gitlab-org/gitlab/-/work_items/617948. The goal is to measure whether Free-group members who aren't Owners will actually ask for an upgrade, before we build a real request flow. Nothing is sent to Owners; this only records that a request happened.
A new "Explore Premium" item under "What's new for you" in the profile menu opens a Free vs Premium comparison modal (a variant of the end-of-trial modal) with "Learn more" and "Request Premium features" buttons. There are three states: not requested (both buttons shown), requested (after clicking the request button, the state updates), and later session (only "Learn more" remains, as the primary button, with the intro sentence changed). A feature spec covers the three-state transition: not requested to requested, persisting across a reload, and previously requested after signing out and back in.
This MR is stacked on !258282 (merged), which adds the shared eligibility class, the two callout IDs, the feature flag, the sidebar context, and the frontend helper. Eligibility: GitLab.com only, root namespace is a Free top-level group, the user is a member of the group or a subgroup but can't edit billing (i.e. not an Owner), and isn't a GitLab team member. It's behind the painted_door_request_ctas feature flag, and a request is stored as a user group callout — one per user per root group, with the time it was made — no migration needed. "Same session" is tracked with a cookie set when the request button is clicked; it expires after one day or on sign-out, so the requested state carries across tabs and reloads until then.
Five new internal events (tier: free, product_group: acquisition) cover the menu item view and click, the modal's request and Learn more clicks, and modal dismissal: view_explore_premium_user_menu_item, click_explore_premium_user_menu_item, click_request_premium_features_explore_premium_modal, click_learn_more_explore_premium_modal, dismiss_explore_premium_modal. label carries the state (not_requested / requested / previously_requested) and property carries the root group id.
The Free vs Premium comparison table used by the end-of-trial modal is extracted into a shared free_vs_premium_comparison_table.vue component (with screen-reader text added to the check/cross icons) so the new modal can reuse it. The end-of-trial modal behaves the same as before; its logo now uses gl-ml-6 so it's actually centered (it was ~4px right of center).
The chat panel MR !257330 (merged) is stacked on !258282 (merged), not on this MR, and does not depend on anything here.
References
- Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/617948
- Depends on !258282 (merged)
- Companion MR: !257330 (merged)
Screenshots or screen recordings
| State | Profile menu modal |
|---|---|
| Not requested | ![]() |
| Requested | ![]() |
| Later session | ![]() |
How to set up and validate locally
- Run GDK in SaaS simulation mode:
export GITLAB_SIMULATE_SAAS=1(or set the equivalentgdk configvalue) and restart the relevant services. - In a rails console, enable the flag:
Feature.enable(:painted_door_request_ctas). - Sign in as a Developer (not Owner) of a Free top-level group.
- Open a project or group page under that namespace.
- Open the profile menu, click "Explore Premium" under "What's new for you", and confirm the comparison modal shows "Learn more" and "Request Premium features"; click the request button and confirm the state updates.
- Reload the page and confirm the modal still shows the "Requested" state.
- Sign out and sign back in, then confirm the modal falls back to the later-session state: only "Learn more" as primary, with the changed intro sentence.
- To reset and retest, delete the
painted_door_*cookie (or sign out) and delete the correspondingUsers::GroupCalloutrow for your user/group.
Snowplow events
Events captured in local Snowplow Micro (0 bad events)
Recorded against GDK in SaaS mode: a Maintainer on a Free top-level group (id 242) with the flag enabled for that group. Events are listed in the order they fired and trimmed to the relevant fields. Every event also carries the gitlab_service_ping internal-event context.
- Events 1–4: first open (
not_requested), then Learn more, then request. The request event has nolabel, which matches its definition. - Events 5–7: menu reopened in the same tab (
requested), then the modal closed without requesting. - Event 8:
previously_requested, recorded with an earlier build where a new tab started a new session. The label logic is unchanged.
[
{ "se_action": "view_explore_premium_user_menu_item", "se_label": "not_requested", "se_property": "242" },
{ "se_action": "click_explore_premium_user_menu_item", "se_label": "not_requested", "se_property": "242" },
{ "se_action": "click_learn_more_explore_premium_modal", "se_label": "not_requested", "se_property": "242" },
{ "se_action": "click_request_premium_features_explore_premium_modal", "se_label": null, "se_property": "242" },
{ "se_action": "view_explore_premium_user_menu_item", "se_label": "requested", "se_property": "242" },
{ "se_action": "click_explore_premium_user_menu_item", "se_label": "requested", "se_property": "242" },
{ "se_action": "dismiss_explore_premium_modal", "se_label": "requested", "se_property": "242" },
{ "se_action": "view_explore_premium_user_menu_item", "se_label": "previously_requested", "se_property": "242" }
]Fields shared by every event:
{
"se_category": "groups:show",
"gitlab_standard_context": {
"user_id": 135,
"namespace_id": 242,
"ultimate_parent_namespace_id": 242,
"project_id": null,
"plan": "free",
"realm": "saas",
"is_gitlab_team_member": false,
"extra": {}
}
}MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.


