From 4e0e238ecdb6db200e4bdda3f0ca6b7452b0d69d Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:52:40 +0000 Subject: [PATCH 1/2] fix(command-palette): Prevent orgId stripping from settings URLs --- .../settingsCommandPaletteActions.spec.tsx | 67 +++++++++++++++++++ .../settingsCommandPaletteActions.tsx | 15 ++++- 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 static/app/views/settings/settingsCommandPaletteActions.spec.tsx diff --git a/static/app/views/settings/settingsCommandPaletteActions.spec.tsx b/static/app/views/settings/settingsCommandPaletteActions.spec.tsx new file mode 100644 index 000000000000..faf24986b546 --- /dev/null +++ b/static/app/views/settings/settingsCommandPaletteActions.spec.tsx @@ -0,0 +1,67 @@ +import type {Config} from 'sentry/types/system'; +import {ConfigStore} from 'sentry/stores/configStore'; +import {getSettingsFieldSections} from 'sentry/views/settings/settingsCommandPaletteActions'; + +describe('getSettingsFieldSections', () => { + let configState: Config; + + beforeEach(() => { + configState = ConfigStore.getState(); + }); + + afterEach(() => { + ConfigStore.loadInitialData(configState); + }); + + it('includes the org slug in org-level route paths when not on a customer domain', () => { + // No customerDomain set — normalizeUrl is a no-op, so the slug should be substituted. + ConfigStore.set('customerDomain', null); + + const sections = getSettingsFieldSections('doogi'); + + // At least one section should target an org-level settings path. + const allPathnames = sections.flatMap(section => + section.fields.map(field => field.to.pathname) + ); + const orgPaths = allPathnames.filter(p => p.startsWith('/settings/')); + + // Every org-level path must include the org slug and must NOT contain the literal `:orgId`. + for (const pathname of orgPaths) { + if (pathname.startsWith('/settings/account/')) { + // Account-level routes never contain orgId. + continue; + } + expect(pathname).toContain('doogi'); + expect(pathname).not.toContain(':orgId'); + } + }); + + it('does not include the org slug in org-level route paths when on a customer domain', () => { + // With customerDomain set, normalizeUrl strips the orgId segment from the + // template before the slug is substituted, so the resulting path should NOT + // contain the real slug (the org is identified by the subdomain instead). + ConfigStore.loadInitialData({ + ...configState, + customerDomain: { + subdomain: 'doogi', + organizationUrl: 'https://doogi.sentry.io', + sentryUrl: 'https://sentry.io', + }, + }); + + const sections = getSettingsFieldSections('doogi'); + + const allPathnames = sections.flatMap(section => + section.fields.map(field => field.to.pathname) + ); + const orgPaths = allPathnames.filter( + p => p.startsWith('/settings/') && !p.startsWith('/settings/account/') + ); + + // Org paths must not include the slug and must not contain `:orgId`. + for (const pathname of orgPaths) { + expect(pathname).not.toContain('doogi'); + expect(pathname).not.toContain(':orgId'); + } + }); +}); diff --git a/static/app/views/settings/settingsCommandPaletteActions.tsx b/static/app/views/settings/settingsCommandPaletteActions.tsx index afe65a11cd1d..8e531973768e 100644 --- a/static/app/views/settings/settingsCommandPaletteActions.tsx +++ b/static/app/views/settings/settingsCommandPaletteActions.tsx @@ -5,6 +5,7 @@ import {CommandPaletteSlot} from 'sentry/components/commandPalette/ui/commandPal import {IconLock, IconMail, IconSettings, IconSubscribed, IconUser} from 'sentry/icons'; import {t} from 'sentry/locale'; import {replaceRouterParams} from 'sentry/utils/replaceRouterParams'; +import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useOrganization} from 'sentry/utils/useOrganization'; import {FORM_FIELD_REGISTRY} from 'sentry/views/settings/fieldRegistry.generated'; import {getUserOrgNavigationConfiguration} from 'sentry/views/settings/organization/userOrgNavigationConfiguration'; @@ -26,7 +27,17 @@ function normalizeRouteForLookup(route: string): string { } function resolveRoutePath(route: string, orgSlug: string): string { - return replaceRouterParams(normalizeRouteForLookup(route), {orgId: orgSlug}); + // Apply normalizeUrl to the route template *before* substituting the real org + // slug. On customer-domain sessions, normalizeUrl strips the `:orgId` segment + // from the template (e.g. `/settings/:orgId/feature-flags/…` → + // `/settings/feature-flags/…`), so replaceRouterParams becomes a no-op and + // the stored URL is already canonical. Without this, the command palette's + // own normalizeUrl call (commandPalette.tsx) would strip a real slug that had + // already been substituted, producing a broken path such as + // `/settings/feature-flags/…` where `feature-flags` would be mis-matched as + // `:orgId` by the router. + const normalizedRoute = normalizeUrl(normalizeRouteForLookup(route)); + return replaceRouterParams(normalizedRoute, {orgId: orgSlug}); } function titleFromRoute(route: string): string { @@ -75,7 +86,7 @@ type SettingsFieldSection = { type FormFieldDefinition = (typeof FORM_FIELD_REGISTRY)[string]; -function getSettingsFieldSections(orgSlug: string): SettingsFieldSection[] { +export function getSettingsFieldSections(orgSlug: string): SettingsFieldSection[] { const routeTitleMap = new Map(); for (const section of getUserOrgNavigationConfiguration()) { for (const item of section.items) { From f9dfd09b30c63fe465d91421efb3122f5539adac Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:12:02 +0000 Subject: [PATCH 2/2] fix(command-palette): Preserve orgId in settings navigation URLs --- .../settings/settingsCommandPaletteActions.spec.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/static/app/views/settings/settingsCommandPaletteActions.spec.tsx b/static/app/views/settings/settingsCommandPaletteActions.spec.tsx index faf24986b546..940e4c3a0277 100644 --- a/static/app/views/settings/settingsCommandPaletteActions.spec.tsx +++ b/static/app/views/settings/settingsCommandPaletteActions.spec.tsx @@ -1,5 +1,5 @@ -import type {Config} from 'sentry/types/system'; import {ConfigStore} from 'sentry/stores/configStore'; +import type {Config} from 'sentry/types/system'; import {getSettingsFieldSections} from 'sentry/views/settings/settingsCommandPaletteActions'; describe('getSettingsFieldSections', () => { @@ -19,18 +19,15 @@ describe('getSettingsFieldSections', () => { const sections = getSettingsFieldSections('doogi'); - // At least one section should target an org-level settings path. const allPathnames = sections.flatMap(section => section.fields.map(field => field.to.pathname) ); - const orgPaths = allPathnames.filter(p => p.startsWith('/settings/')); + const orgPaths = allPathnames.filter( + p => p.startsWith('/settings/') && !p.startsWith('/settings/account/') + ); // Every org-level path must include the org slug and must NOT contain the literal `:orgId`. for (const pathname of orgPaths) { - if (pathname.startsWith('/settings/account/')) { - // Account-level routes never contain orgId. - continue; - } expect(pathname).toContain('doogi'); expect(pathname).not.toContain(':orgId'); }