Files
supabase/apps/studio/data/misc/get-default-region-query.ts
Alaister YoungandAlaister Young aa3643f39d fix(studio): restrict geolocated default region to provider regions (#49141)
Follow-up to #49131. For `AWS_NIMBUS` orgs, the new-project form's
Region trigger could show a region that wasn't in the dropdown at all
(e.g. "Southeast Asia (Singapore)" while the list only offered "East US
(North Virginia)"). The geolocation-based default region
(`useDefaultRegionQuery`) picked the nearest region from **all** AWS
regions and seeded it into `dbRegion` unvalidated, ignoring the
provider's restricted region list.

**Changed:**

- `getDefaultRegionOption` now computes the nearest region only over the
provider's available regions (new `getDefaultRegionCandidateKeys`
helper). The flag-based restricted pool (`defaultRegionRestrictedPool`)
narrows within that set and is ignored if the intersection would be
empty.
- The form's default-region selection is extracted into
`resolveDefaultDbRegion` (`ProjectCreation.utils.ts`): High Availability
region first, then the recommended smart region, then the geolocated
default — used only when the provider actually offers that region —
falling back to the provider's static default.
- `getAvailableRegions` takes an injectable `environment` param (same
pattern as `getHighAvailabilityRegionCode`) so the prod-only Nimbus
region list is unit-testable.

**Added:**

- Unit tests for `getDefaultRegionCandidateKeys` (provider clamping
incl. Nimbus on prod, restricted-pool intersection, empty-intersection
fallback), `getAvailableRegions` across environments, and
`resolveDefaultDbRegion` (branch priority plus the fallback when the
geolocated region isn't offered).

## To test

- Emulate a Nimbus org locally by setting `"infra:cloud_providers":
["AWS_NIMBUS"]` in
`apps/studio/hooks/custom-content/custom-content.json`, then open the
new-project form: the Region trigger must show the same region the
dropdown offers (locally that's only Southeast Asia (Singapore)). To
reproduce the original mismatch path, stub
`https://www.cloudflare.com/cdn-cgi/trace` to return `loc=US` — the
trigger should still be clamped to the provider's region rather than
showing a US region
- Block or fail the Cloudflare trace request: the trigger should fall
back to the provider's static default region, not sit blank or loading
- Restore the normal provider list: the smart-region flow ("General
regions" + "Specific regions" with Recommended badges) is unaffected —
the geolocation request doesn't even fire on that path — and toggling
High Availability still transitions the region list cleanly

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Summary by CodeRabbit

- **Bug Fixes**
- Region suggestions now respect the selected cloud provider and
deployment environment.
- Project creation avoids unavailable geolocated regions and falls back
to a supported provider default.
- Restricted region pools now fall back reliably to available provider
regions.
- AWS Nimbus selection reflects the active environment while preserving
high-availability and smart-region behavior.

- **Tests**
- Added coverage for provider-specific, environment-specific,
restricted, and fallback region selection scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-08-21 15:51:03 +08:00

102 lines
4.0 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { useFlag } from 'common'
import type { CloudProvider } from 'shared-data'
import { AWS_REGIONS } from 'shared-data'
import { miscKeys } from './keys'
import { COUNTRY_LAT_LON } from '@/components/interfaces/ProjectCreation/ProjectCreation.constants'
import { getAvailableRegions } from '@/components/interfaces/ProjectCreation/ProjectCreation.utils'
import { AWS_REGIONS_COORDINATES } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
import { fetchHandler } from '@/data/fetchers'
import { getDistanceLatLonKM, tryParseJson } from '@/lib/helpers'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type DefaultRegionVariables = {
cloudProvider?: CloudProvider
restrictedPool?: string[]
useRestrictedPool?: boolean
}
// The geolocation-based default may only ever pick a region the selected cloud provider
// actually offers (e.g. AWS_NIMBUS is restricted to a single region). The flag-based
// restricted pool narrows within that set, and is ignored if it would leave no candidates.
export function getDefaultRegionCandidateKeys(
cloudProvider: CloudProvider,
restrictedPool?: string[],
environment = process.env.NEXT_PUBLIC_ENVIRONMENT
) {
const providerRegionKeys = Object.keys(getAvailableRegions(cloudProvider, environment))
const pooledRegionKeys = restrictedPool
? providerRegionKeys.filter((key) => restrictedPool.includes(key))
: providerRegionKeys
return pooledRegionKeys.length > 0 ? pooledRegionKeys : providerRegionKeys
}
export async function getDefaultRegionOption({
cloudProvider,
restrictedPool,
useRestrictedPool = true,
}: DefaultRegionVariables) {
if (!cloudProvider) throw new Error('Cloud provider is required')
try {
const data = await fetchHandler('https://www.cloudflare.com/cdn-cgi/trace').then((res) =>
res.text()
)
const locationCode: keyof typeof COUNTRY_LAT_LON = Object.fromEntries(
data.split('\n').map((item) => item.split('='))
)['loc']
const locLatLon = COUNTRY_LAT_LON[locationCode]
if (locLatLon === undefined) return undefined
const candidateKeys = getDefaultRegionCandidateKeys(
cloudProvider,
useRestrictedPool ? restrictedPool : undefined
)
const locations = Object.fromEntries(
Object.entries(AWS_REGIONS_COORDINATES).filter(([key]) => candidateKeys.includes(key))
)
const distances = Object.keys(locations).map((reg) => {
const region: { lat: number; lon: number } = {
lat: locations[reg][1],
lon: locations[reg][0],
}
return getDistanceLatLonKM(locLatLon.lat, locLatLon.lon, region.lat, region.lon)
})
const shortestDistance = Math.min(...distances)
const closestRegion = Object.keys(locations)[distances.indexOf(shortestDistance)]
return AWS_REGIONS[closestRegion as keyof typeof AWS_REGIONS].displayName
} catch (error) {
throw error
}
}
export type DefaultRegionData = Awaited<ReturnType<typeof getDefaultRegionOption>>
export type DefaultRegionError = ResponseError
export const useDefaultRegionQuery = <TData = DefaultRegionData>(
{ cloudProvider, useRestrictedPool }: DefaultRegionVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<DefaultRegionData, DefaultRegionError, TData> = {}
) => {
// [Joshen] Flag allows us to specify restricted regions for users based on percentage
const restrictedPoolFlag = useFlag('defaultRegionRestrictedPool')
const restrictedPool = tryParseJson(restrictedPoolFlag)
return useQuery<DefaultRegionData, DefaultRegionError, TData>({
queryKey: miscKeys.defaultRegion(cloudProvider, useRestrictedPool ?? true),
queryFn: () => getDefaultRegionOption({ cloudProvider, restrictedPool, useRestrictedPool }),
enabled:
enabled && typeof cloudProvider !== 'undefined' && typeof restrictedPool !== 'undefined',
retry(failureCount) {
return failureCount < 1
},
...options,
})
}