Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions static/app/utils/regions/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {ConfigStore} from 'sentry/stores/configStore';
import type {Config} from 'sentry/types/system';
import {getRegionUrlOptions, getRegionNameOptions} from 'sentry/utils/regions';

describe('getRegionUrlOptions', () => {
let configstate: Config;

beforeEach(() => {
configstate = ConfigStore.getState();
});

afterEach(() => {
ConfigStore.loadInitialData(configstate);
});

it('filters out excluded names', () => {
ConfigStore.set('regions', [
{name: 'us', url: 'https://us.sentry.io'},
{name: 'de', url: 'https://de.sentry.io'},
{name: 'ja', url: 'https://ja.sentry.io'},
]);

const res = getRegionUrlOptions([
{name: 'us', url: 'https://us.sentry.io', displayName: 'us'},
]);
expect(res).toHaveLength(2);
expect(res[0]).toEqual({
value: 'https://de.sentry.io',
label: '🇪🇺 European Union (EU)',
});
expect(res[1]).toEqual({value: 'https://ja.sentry.io', label: ' ja'});

// Excluding the only included option = empty set.
const none = getRegionUrlOptions(
[{name: 'us', url: 'https://us.sentry.io', displayName: 'us'}],
['us']
);
expect(none).toHaveLength(0);
});

it('limits to only parameter', () => {
ConfigStore.set('regions', [
{name: 'us', url: 'https://us.sentry.io'},
{name: 'de', url: 'https://de.sentry.io'},
{name: 'ja', url: 'https://ja.sentry.io'},
]);

const res = getRegionUrlOptions([], ['us']);
expect(res).toHaveLength(1);
expect(res[0]).toEqual({
value: 'https://us.sentry.io',
label: '🇺🇸 United States of America (US)',
});
});

it('always excludes US2', () => {
ConfigStore.set('regions', [
{name: 'us', url: 'https://us.sentry.io'},
{name: 'us2', url: 'https://us2.sentry.io'},
{name: 'de', url: 'https://de.sentry.io'},
{name: 'ja', url: 'https://ja.sentry.io'},
]);

const res = getRegionUrlOptions();
expect(res).toHaveLength(3);
res.forEach(item => expect(item.value).not.toContain('us2'));
});
});

describe('getRegionNameOptions', () => {
let configstate: Config;

beforeEach(() => {
configstate = ConfigStore.getState();
});

afterEach(() => {
ConfigStore.loadInitialData(configstate);
});
it('always excludes US2', () => {
ConfigStore.set('regions', [
{name: 'us', url: 'https://us.sentry.io'},
{name: 'us2', url: 'https://us2.sentry.io'},
{name: 'de', url: 'https://de.sentry.io'},
{name: 'ja', url: 'https://ja.sentry.io'},
]);

const res = getRegionNameOptions();
expect(res).toHaveLength(3);

expect(res[0]).toEqual({value: 'us', label: '🇺🇸 United States of America (US)'});

res.forEach(item => expect(item.label).not.toContain('us2'));
});
});
50 changes: 36 additions & 14 deletions static/app/utils/regions/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {t} from 'sentry/locale';
import {ConfigStore} from 'sentry/stores/configStore';
import type {SelectValue} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import type {Region} from 'sentry/types/system';

Expand Down Expand Up @@ -36,7 +37,7 @@ export function getRegionDataFromOrganization(
): RegionData | undefined {
const {regionUrl} = organization.links;

const regions = ConfigStore.get('regions') ?? [];
const regions = getRegions();

const region = regions.find(value => {
return value.url === regionUrl;
Expand All @@ -58,32 +59,53 @@ export function getRegions(): Region[] {
return ConfigStore.get('regions') ?? [];
}

export function getRegionChoices(exclude: RegionData[] = []): Array<[string, string]> {
/**
* Get a list of choice tuples with (url, display name)
*/
export function getRegionUrlOptions(
exclude: RegionData[] = [],
only: string[] = []
): Array<SelectValue<string>> {
const regions = getRegions();
const excludedRegionNames = exclude.map(region => region.name);

return regions
.filter(region => {
return !excludedRegionNames.includes(region.name);
if (
excludedRegionNames.includes(region.name) ||
(only.length > 0 && !only.includes(region.name)) ||
CUSTOMER_HIDDEN_REGIONS.has(region.name)
) {
return false;
}
return true;
})
.map(region => {
const {url} = region;
return [
url,
`${getRegionFlagIndicator(region) || ''} ${getRegionDisplayName(region)}`,
];
return {
value: url,
label: `${getRegionFlagIndicator(region) || ''} ${getRegionDisplayName(region)}`,
};
});
}

export function getRegionNameChoices(): Array<[string, string]> {
// TODO(cells) Rework/remove this once Region -> Locality config changes are completed.
const CUSTOMER_HIDDEN_REGIONS = new Set(['us2']);

/**
* Create a list of Choice tuples with (name, display name)
*/
export function getRegionNameOptions(): Array<SelectValue<string>> {
const regions = getRegions();

return regions.map(region => {
return [
region.name,
`${getRegionFlagIndicator(region) || ''} ${getRegionDisplayName(region)}`,
];
});
return regions
.filter(region => !CUSTOMER_HIDDEN_REGIONS.has(region.name))
.map(region => {
return {
value: region.name,
label: `${getRegionFlagIndicator(region) || ''} ${getRegionDisplayName(region)}`,
};
});
}

export function shouldDisplayRegions(): boolean {
Expand Down
6 changes: 3 additions & 3 deletions static/app/views/organizationCreate/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {t, tct} from 'sentry/locale';
import {getOverride} from 'sentry/overrideRegistry';
import {ConfigStore} from 'sentry/stores/configStore';
import type {OrganizationSummary} from 'sentry/types/organization';
import {getRegionNameChoices, shouldDisplayRegions} from 'sentry/utils/regions';
import {getRegionNameOptions, shouldDisplayRegions} from 'sentry/utils/regions';
import {testableWindowLocation} from 'sentry/utils/testableWindowLocation';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
import {useApi} from 'sentry/utils/useApi';
Expand All @@ -34,7 +34,7 @@ function OrganizationCreate() {
const privacyUrl = ConfigStore.get('privacyUrl');
const isSelfHosted = ConfigStore.get('isSelfHosted');
const relocationUrl = normalizeUrl('/relocation/');
const regionChoices = getRegionNameChoices();
const regionOptions = getRegionNameOptions();
const client = useApi();

const hasDataConsent =
Expand Down Expand Up @@ -119,7 +119,7 @@ function OrganizationCreate() {
"Choose where to store your organization's data. Please note, you won't be able to change locations once your organization has been created. [learnMore:Learn More]",
{learnMore: <a href={DATA_STORAGE_DOCS_LINK} />}
)}
choices={regionChoices}
options={regionOptions}
inline={false}
stacked
required
Expand Down
18 changes: 3 additions & 15 deletions static/app/views/relocation/getStarted.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {Select} from '@sentry/scraps/select';
import {addErrorMessage} from 'sentry/actionCreators/indicator';
import {t} from 'sentry/locale';
import {ConfigStore} from 'sentry/stores/configStore';
import {getRegionUrlOptions} from 'sentry/utils/regions';
import {useApi} from 'sentry/utils/useApi';
import {ContinueButton} from 'sentry/views/relocation/components/continueButton';
import {StepHeading} from 'sentry/views/relocation/components/stepHeading';
Expand All @@ -18,17 +19,6 @@ const PROMO_CODE_ERROR_MSG = t(
'That promotional code has already been claimed, does not have enough remaining uses, is no longer valid, or never existed.'
);

// Best-effort region name prettification.
function prettyRegionName(name: string): string {
if (name === 'de') {
return '🇪🇺 European Union (EU)';
}
if (name === 'us') {
return '🇺🇸 United States of America (US)';
}
return name;
}

export function GetStarted({
relocationState,
onUpdateRelocationState,
Expand All @@ -38,9 +28,7 @@ export function GetStarted({
const {orgSlugs, regionUrl, promoCode} = relocationState;
const [showPromoCode, setShowPromoCode] = useState(!!promoCode);
const selectableRegions = ConfigStore.get('relocationConfig')?.selectableRegions || [];
const regions = ConfigStore.get('regions').filter(region =>
selectableRegions.includes(region.name)
);
const regionOptions = getRegionUrlOptions([], selectableRegions);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty selectableRegions now shows all regions instead of none

Medium Severity

When selectableRegions is an empty array (e.g., when relocationConfig is undefined), the old code filtered to an empty list since no region matched the whitelist. The new call getRegionUrlOptions([], selectableRegions) passes an empty only array, which triggers the only.length > 0 short-circuit in the filter, causing all regions to be shown. This changes the semantics from "nothing is selectable" to "everything is selectable" when relocation config is missing.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a57167b. Configure here.


const handleContinue = async (event: any) => {
event.preventDefault();
Expand Down Expand Up @@ -92,7 +80,7 @@ export function GetStarted({
name="region"
aria-label={t('region')}
placeholder="Select Location"
options={regions.map(r => ({label: prettyRegionName(r.name), value: r.url}))}
options={regionOptions}
onChange={(opt: any) => {
onUpdateRelocationState({regionUrl: opt.value});
}}
Expand Down
21 changes: 12 additions & 9 deletions static/app/views/relocation/relocation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {Redirect} from 'sentry/components/redirect';
import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle';
import {IconArrow} from 'sentry/icons';
import {t} from 'sentry/locale';
import {ConfigStore} from 'sentry/stores/configStore';
import {getRegionUrlOptions} from 'sentry/utils/regions';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
import {useApi} from 'sentry/utils/useApi';
import {useNavigate} from 'sentry/utils/useNavigate';
Expand Down Expand Up @@ -76,7 +76,7 @@ export function RelocationOnboarding() {
const stepObj = onboardingSteps.find(({id}) => stepId === id);
const stepIndex = onboardingSteps.findIndex(({id}) => stepId === id);
const api = useApi();
const regions = ConfigStore.get('regions');
const regionOptions = getRegionUrlOptions();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

US2 excluded from relocation API data-fetching calls

High Severity

getRegionUrlOptions() filters out US2 via CUSTOMER_HIDDEN_REGIONS, but in relocation.tsx this result is used for API data-fetching — querying /relocations/ and /publickeys/relocations/ across all regions. The old code used ConfigStore.get('regions') (unfiltered). Now, existing relocations on US2 won't be detected (so users won't be redirected to the in-progress page and could attempt a second relocation), and public keys for US2 won't be fetched. The UI-display filter is being incorrectly applied to backend data-fetching logic. This call site needs unfiltered regions (e.g. via getRegions()), not the customer-facing dropdown options.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a57167b. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The new getRegionUrlOptions() function incorrectly filters out hidden regions like us2, breaking admin tools and failing to detect existing relocations in those regions.
Severity: HIGH

Suggested Fix

The getRegionUrlOptions function should accept an optional parameter to include hidden regions, e.g., includeHidden: true. This parameter should be used in internal tools and system checks like forkCustomer.tsx and fetchExistingRelocation that require a complete list of all regions. The default behavior for customer-facing UIs can remain as is.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: static/app/views/relocation/relocation.tsx#L79

Potential issue: The new `getRegionUrlOptions()` utility function filters out regions
defined in `CUSTOMER_HIDDEN_REGIONS`, which includes `us2`. This function is now used in
two places where a complete list of all regions is required. First, in the admin tool
`forkCustomer.tsx`, it prevents admins from forking customer data into the `us2` region.
Second, in `relocation.tsx`, it prevents the `fetchExistingRelocation` check from
querying the `us2` region. This could lead to a user with an active relocation in `us2`
being able to start a new, conflicting relocation, as the system would fail to detect
the existing one.

Also affects:

  • static/gsAdmin/components/forkCustomer.tsx:68

Did we get this right? 👍 / 👎 to inform future reviews.

const [existingRelocationState, setExistingRelocationState] = useState(
LoadingState.FETCHING
);
Expand All @@ -95,10 +95,10 @@ export function RelocationOnboarding() {
const fetchExistingRelocation = useCallback(() => {
setExistingRelocationState(LoadingState.FETCHING);
return Promise.all(
regions.map(region =>
regionOptions.map(option =>
api.requestPromise('/relocations/', {
method: 'GET',
host: region.url,
host: option.value,
})
)
)
Expand Down Expand Up @@ -142,7 +142,7 @@ export function RelocationOnboarding() {
setExistingRelocation('');
setExistingRelocationState(LoadingState.ERROR);
});
}, [api, navigate, regions, relocationState, stepId]);
}, [api, navigate, regionOptions, relocationState, stepId]);
useEffect(() => {
fetchExistingRelocation();
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -151,17 +151,20 @@ export function RelocationOnboarding() {
const fetchPublicKeys = useCallback(() => {
setPublicKeysState(LoadingState.FETCHING);
return Promise.all(
regions.map(region =>
regionOptions.map(option =>
api.requestPromise('/publickeys/relocations/', {
method: 'GET',
host: region.url,
host: option.value,
})
)
)
.then(responses => {
setPublicKeys(
new Map<string, string>(
regions.map((region, index) => [region.url, responses[index].public_key])
regionOptions.map((option, index) => [
option.value,
responses[index].public_key,
])
)
);
setPublicKeysState(LoadingState.FETCHED);
Expand All @@ -170,7 +173,7 @@ export function RelocationOnboarding() {
setPublicKeys(new Map<string, string>());
setPublicKeysState(LoadingState.ERROR);
});
}, [api, regions]);
}, [api, regionOptions]);
useEffect(() => {
fetchPublicKeys();
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
8 changes: 5 additions & 3 deletions static/gsAdmin/components/forkCustomer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {Client} from 'sentry/api';
import {SelectField} from 'sentry/components/forms/fields/selectField';
import type {Organization} from 'sentry/types/organization';
import {
getRegionChoices,
getRegionUrlOptions,
getRegionDataFromOrganization,
getRegions,
} from 'sentry/utils/regions';
Expand Down Expand Up @@ -65,14 +65,16 @@ class ForkCustomerActionImpl extends Component<Props> {
render() {
const {organization} = this.props;
const currentRegionData = getRegionDataFromOrganization(organization);
const regionChoices = getRegionChoices(currentRegionData ? [currentRegionData] : []);
const regionOptions = getRegionUrlOptions(
currentRegionData ? [currentRegionData] : []
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Admin fork dropdown incorrectly hides US2 region

Medium Severity

The admin "Duplicate into Region" dropdown in forkCustomer.tsx now uses getRegionUrlOptions(), which filters out US2 via CUSTOMER_HIDDEN_REGIONS. This is an internal admin tool (gsAdmin/), not a customer-facing view. The PR title specifies hiding US2 in "customer facing dropdowns," but this change also prevents admins from forking organizations into US2.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a57167b. Configure here.

return (
<Fragment>
<SelectField
name="regionUrl"
label="Duplicate into Region"
help="Choose which region to duplicate this organization's low volume metadata into. This will kick off a SAAS->SAAS relocation job, but the source organization will not be affected."
choices={regionChoices}
options={regionOptions}
inline={false}
stacked
required
Expand Down
Loading