Default registration country from Cloudflare IP header

What does this MR do and why?

The registration welcome form's country/region selector starts empty, so every new user has to open the dropdown and pick a country. This is a required interaction on the highest-traffic step of signup.

GitLab.com already receives the visitor's country on every request. Two independent Cloudflare settings each send Cf-Ipcountry, and either alone is sufficient: the add_visitor_location_headers managed transform, which is enabled for the gitlab.com zone in gitlab-com/gl-infra/config-mgmt, and the zone's IP Geolocation network setting, which Cloudflare documents as sending the header "even when Add visitor location headers is turned off".

GitLab already consumes this header for the unknown-sign-in notification email and for PIPL compliance checks, and logs it as cf_ipcountry on every controller request.

This MR uses that header to pre-select the country in the welcome form. The field stays editable, so a wrong guess only costs the user a correction rather than producing bad data.

Behind the default_registration_country_from_ip feature flag, default disabled.

Implementation notes

  • Adds a country_code method as an EE extension in ee/lib/ee/gitlab/auth/visitor_location.rb, injected via prepend_mod on lib/gitlab/auth/visitor_location.rb. It's deliberately EE-only because reading visitor location is tracking-adjacent. The CE-side change is a single prepend_mod line.
  • country_code returns the raw ISO 3166-1 alpha-2 code, and nil when the header is absent, blank, or one of Cloudflare's unresolved sentinels: XX (could not resolve the IP) and T1 (Tor exit node).
  • ee/app/components/registrations/welcome/form_component.rb gains a default_country method used for the country key in the view model. Precedence: a submitted params[:country] wins (so a failed submit round-trips the user's own choice), then the detected code, then an empty string.
  • The detected code is filtered through World.countries_for_select. The selector omits ten countries (World::COUNTRY_DENYLIST plus the JH_MARKET entries), so a detected country with no matching option falls back to empty rather than an unselectable value.
  • Self-managed instances have no Cloudflare in front of them, so the header is absent and the field behaves exactly as it does today.
  • No frontend or controller changes needed: ViewComponent::Base already exposes request, and free_welcome_form.vue already reads userData.country into its form values.
  • user_data was already at the Metrics/CyclomaticComplexity limit, which is why this logic is extracted into default_country rather than inlined.

How to set up and validate locally

The header isn't present in GDK, so it has to be supplied manually.

  1. Enable the flag in the Rails console: Feature.enable(:default_registration_country_from_ip)
  2. Request the welcome page with the header set, with a signed-in session, e.g. curl -H 'Cf-Ipcountry: NL' <gdk-url>/users/sign_up/welcome (or add the header via a browser extension)
  3. The country selector should render with the matching country pre-selected.
  4. Try XX and CN to confirm both fall back to an empty selector.

Measuring how often the header resolves to a usable country, and how often users correct the pre-filled value, is not covered by this MR and needs separate instrumentation. On GitLab.com the submitted country is only passed through to CustomersDot rather than persisted on the user.

Edited by David Hamp-Gonsalves

Merge request reports

Loading
Loading