Files
supabase/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts
Charis 22886fd304 feat(studio): add flag-gated general region selection (#51274)
## Problem

We want to be able to show free-plan organizations a simplified region
selector that only lists general regions (Americas, Europe,
Asia-Pacific), controlled per organization through ConfigCat.

## Solution

- ConfigCat flags are now evaluated with `organization_slug` and
`organization_created_at` (Unix seconds) custom attributes, so flags can
target and bucket by organization.
- `organization_created_at` is read from `GET
/platform/organizations/{slug}`, fetched only for free-plan
organizations, since the organization list response doesn't include it.
- Two flags:
- `freeTierGeneralRegionEnrollment`: the organization is enrolled
(control or test).
- `freeTierGeneralRegionSelection`: the organization sees only general
regions.
- For enrolled free organizations, the region selector stays in its
loading state until flags have been evaluated with the organization's
creation time, so specific regions aren't shown and then removed.
- In the test variant, the selector hides specific regions and shows a
footer linking to the plan upgrade panel. High Availability keeps its
own region list.
- Telemetry: new `free_tier_general_region_experiment_exposed` and
`free_tier_general_region_upgrade_clicked` events, and
`freeTierGeneralRegionExperiment` / `regionSelectionType` properties on
`project_creation_simple_version_submitted`.
- `created_at` is added to `OrganizationSlugResponse` in the generated
platform types, matching the API.

## Review instructions

1. With both flags off, open `/new/[slug]` for a free organization and
confirm the region selector is unchanged.
2. Using the dev toolbar, set `freeTierGeneralRegionEnrollment` and
`freeTierGeneralRegionSelection` to `true`. Confirm only general regions
are listed and the footer links to the billing plan panel.
3. Set `freeTierGeneralRegionSelection` to `false` and confirm the full
selector is shown.
4. Repeat with a paid organization and confirm the full selector is
always shown.
2026-10-07 09:43:28 -04:00

154 lines
5.2 KiB
TypeScript

import type { CloudProvider, Region } from 'shared-data'
import { AWS_REGIONS } from 'shared-data'
import { SMART_REGION_TO_EXACT_REGION_MAP } from 'shared-data/regions'
import type { OrganizationAvailableRegionsData } from '@/data/organizations/organization-available-regions-query'
import { DesiredInstanceSize, instanceSizeSpecs } from '@/data/projects/new-project.constants'
export function smartRegionToExactRegion(smartOrExactRegion: string) {
return SMART_REGION_TO_EXACT_REGION_MAP.get(smartOrExactRegion) ?? smartOrExactRegion
}
export function getAvailableRegions(
cloudProvider: CloudProvider,
environment = process.env.NEXT_PUBLIC_ENVIRONMENT
): Region {
switch (cloudProvider) {
case 'AWS':
case 'AWS_K8S':
return AWS_REGIONS
case 'AWS_NIMBUS':
if (environment !== 'prod') {
// Only allow Southeast Asia for Nimbus (local/staging)
return {
SOUTHEAST_ASIA: AWS_REGIONS.SOUTHEAST_ASIA,
}
}
// Only allow US East for Nimbus (prod)
return {
EAST_US: AWS_REGIONS.EAST_US,
}
default:
throw new Error('Invalid cloud provider')
}
}
type ResolveDefaultDbRegionArgs = {
cloudProvider: CloudProvider
isHighAvailabilityRestricted: boolean
highAvailabilityRegionName: string | undefined
isSmartRegionEnabled: boolean
recommendedSmartRegion: string | undefined
autoDefaultRegion: string | undefined
fixedDefaultRegion: string
environment?: string
}
export function resolveDefaultDbRegion({
cloudProvider,
isHighAvailabilityRestricted,
highAvailabilityRegionName,
isSmartRegionEnabled,
recommendedSmartRegion,
autoDefaultRegion,
fixedDefaultRegion,
environment = process.env.NEXT_PUBLIC_ENVIRONMENT,
}: ResolveDefaultDbRegionArgs): string | undefined {
if (isHighAvailabilityRestricted) return highAvailabilityRegionName
if (isSmartRegionEnabled) return recommendedSmartRegion
// The geolocated default is only usable if the provider actually offers that region
// (e.g. AWS_NIMBUS is restricted to a single region)
const isAutoDefaultRegionAvailable = Object.entries(
getAvailableRegions(cloudProvider, environment)
).some(([, region]) => region.displayName === autoDefaultRegion)
return isAutoDefaultRegionAvailable && autoDefaultRegion !== undefined
? autoDefaultRegion
: fixedDefaultRegion
}
/**
* When launching new projects, they only get assigned a compute size once successfully launched,
* this might assume wrong compute size, but only for projects being rapidly launched after one another on non-default compute sizes.
*
* Needs to be in the API in the future [kevin]
*/
export const monthlyInstancePrice = (instance: string | undefined): number => {
return instanceSizeSpecs[instance as DesiredInstanceSize]?.priceMonthly || 10
}
export const instanceLabel = (instance: string | undefined): string => {
return instanceSizeSpecs[instance as DesiredInstanceSize]?.label || 'Micro'
}
export const getHighAvailabilityRegionCode = (
environment = process.env.NEXT_PUBLIC_ENVIRONMENT
) => {
// Local dev stacks can run in any of the supported regions, so they're left unrestricted
if (environment === 'prod') return 'us-east-1'
if (environment === 'staging') return 'us-east-1'
if (environment === 'local') return 'eu-central-1'
return undefined
}
export const filterHighAvailabilityRegions = <T extends { code: string }>(
regions: T[],
highAvailability: boolean,
environment = process.env.NEXT_PUBLIC_ENVIRONMENT
) => {
const isLocal = environment === 'local'
const regionCode = getHighAvailabilityRegionCode(environment)
return highAvailability && !isLocal && regionCode !== undefined
? regions.filter((region) => region.code === regionCode)
: regions
}
export type FreeTierGeneralRegionExperimentVariant = 'test' | 'control'
type GetFreeTierGeneralRegionExperimentVariantArgs = {
isFreePlan: boolean
smartRegionEnabled: boolean
enrollmentFlag: boolean
selectionFlag: boolean
}
/**
* Org's arm in the freeTierGeneralRegionExperiment. The test arm only sees general regions.
* Returns undefined when the org isn't enrolled.
*/
export function getFreeTierGeneralRegionExperimentVariant({
isFreePlan,
smartRegionEnabled,
enrollmentFlag,
selectionFlag,
}: GetFreeTierGeneralRegionExperimentVariantArgs):
| FreeTierGeneralRegionExperimentVariant
| undefined {
const isInExperiment = isFreePlan && smartRegionEnabled && enrollmentFlag === true
if (!isInExperiment) return undefined
return selectionFlag === true ? 'test' : 'control'
}
export type AvailableRegions = NonNullable<OrganizationAvailableRegionsData>['all']
type GetRegionSelectionTypeArgs = {
dbRegion: string | undefined
availableRegions: AvailableRegions | undefined
}
/**
* Whether the selected region is a general region (e.g. Americas) or a specific one
* (e.g. us-east-1). Returns undefined if it matches neither list.
*/
export function getRegionSelectionType({
dbRegion,
availableRegions,
}: GetRegionSelectionTypeArgs): 'general' | 'specific' | undefined {
if (!dbRegion || !availableRegions) return undefined
if (availableRegions.smartGroup.some((region) => region.name === dbRegion)) return 'general'
if (availableRegions.specific.some((region) => region.name === dbRegion)) return 'specific'
return undefined
}