Просмотр исходного кода

fix(ui) Fix navigation between customer-domain accounts (#44625)

Improve navigation and subdomain support for `yarn dev-ui`. With these
changes the following works:

- https://acme.localhost:7999/issues/
- https://acme.dev.getsentry.net:7999/issues/
- https://acme.abc1243.sentry.dev/issues/

Switching between organizations will also work, as domains will be
rewritten based on the initial page configuration.

Users of dev-ui will still need to clone their session cookies and set
host file entries if they want to use `acme.localhost` domains.

Refs HC-429
Mark Story 2 лет назад
Родитель
Сommit
0fb31f9c7d

+ 3 - 2
fixtures/js-stubs/organization.js

@@ -1,12 +1,13 @@
 import {OrgRoleList, TeamRoleList} from './roleList';
 
 export function Organization(params = {}) {
+  const slug = params.slug ?? 'org-slug';
   return {
     id: '3',
-    slug: 'org-slug',
+    slug,
     name: 'Organization Name',
     links: {
-      organizationUrl: 'https://org-slug.sentry.io',
+      organizationUrl: `https://${slug}.sentry.io`,
       regionUrl: 'https://us.sentry.io',
     },
     access: [

+ 14 - 0
static/app/bootstrap/index.tsx

@@ -1,4 +1,5 @@
 import {Config} from 'sentry/types';
+import {extractSlug} from 'sentry/utils/extractSlug';
 
 const BOOTSTRAP_URL = '/api/client-config/';
 
@@ -18,6 +19,19 @@ async function bootWithHydration() {
   const response = await fetch(BOOTSTRAP_URL);
   const data: Config = await response.json();
 
+  // Shim up the initialData payload to quack like it came from
+  // a customer-domains initial request. Because our initial call to BOOTSTRAP_URL
+  // will not be on a customer domain, the response will not include this context.
+  if (data.customerDomain === null && window.__SENTRY_DEV_UI) {
+    const domain = extractSlug(window.location.host);
+    if (domain) {
+      data.customerDomain = {
+        organizationUrl: `https://${domain.slug}.sentry.io`,
+        sentryUrl: 'https://sentry.io',
+        subdomain: domain.slug,
+      };
+    }
+  }
   window.__initialData = data;
 
   return bootApplication(data);

+ 5 - 0
static/app/types/system.tsx

@@ -84,6 +84,11 @@ declare global {
         current?: FocusTrap;
       };
     };
+    /**
+     * Is the UI running as dev-ui proxy.
+     * Used by webpack-devserver + html-webpack
+     */
+    __SENTRY_DEV_UI?: boolean;
     /**
      * Sentrys version string
      */

+ 21 - 0
static/app/utils/extractSlug.tsx

@@ -0,0 +1,21 @@
+type ExtractedSlug = {
+  domain: string;
+  slug: string;
+};
+
+// XXX: If you change this also change its sibiling in static/index.ejs
+const KNOWN_DOMAINS = /(?:localhost|dev\.getsentry.net|sentry.dev)(?:\:\d*)?$/;
+
+/**
+ * Extract a slug from a known local development host.
+ * If the host is not a known development host null is returned.
+ */
+export function extractSlug(hostname: string): ExtractedSlug | null {
+  const [slug, ...domainParts] = hostname.split('.');
+  const domain = domainParts.join('.');
+  if (!domain.match(KNOWN_DOMAINS)) {
+    return null;
+  }
+
+  return {slug, domain};
+}

+ 111 - 0
static/app/utils/useResolveRoute.spec.tsx

@@ -0,0 +1,111 @@
+import {reactHooks} from 'sentry-test/reactTestingLibrary';
+
+import {OrganizationContext} from 'sentry/views/organizationContext';
+
+import useResolveRoute from './useResolveRoute';
+
+describe('useResolveRoute', () => {
+  let devUi, host;
+
+  const organization = TestStubs.Organization();
+  const otherOrg = TestStubs.Organization({
+    features: ['customer-domains'],
+    slug: 'other-org',
+  });
+
+  beforeEach(() => {
+    devUi = window.__SENTRY_DEV_UI;
+    host = window.location.host;
+  });
+  afterEach(() => {
+    window.__SENTRY_DEV_UI = devUi;
+    window.location.host = host;
+  });
+
+  it('should use sentryUrl when no org is provided', () => {
+    window.__SENTRY_DEV_UI = true;
+    window.location.host = 'localhost:7999';
+
+    const wrapper = ({children}) => (
+      <OrganizationContext.Provider value={organization}>
+        {children}
+      </OrganizationContext.Provider>
+    );
+    const {result} = reactHooks.renderHook(() => useResolveRoute('/organizations/new/'), {
+      wrapper,
+    });
+    expect(result.current).toBe('/organizations/new/');
+  });
+
+  it('should replace domains with dev-ui mode on localhost', () => {
+    window.__SENTRY_DEV_UI = true;
+    window.location.host = 'acme.localhost:7999';
+
+    const wrapper = ({children}) => (
+      <OrganizationContext.Provider value={organization}>
+        {children}
+      </OrganizationContext.Provider>
+    );
+    const {result} = reactHooks.renderHook(() => useResolveRoute('/issues/', otherOrg), {
+      wrapper,
+    });
+    expect(result.current).toBe('https://other-org.localhost:7999/issues/');
+  });
+
+  it('should replace domains with dev-ui mode on dev.getsentry.net', () => {
+    window.__SENTRY_DEV_UI = true;
+    window.location.host = 'acme.dev.getsentry.net:7999';
+
+    const wrapper = ({children}) => (
+      <OrganizationContext.Provider value={organization}>
+        {children}
+      </OrganizationContext.Provider>
+    );
+    const {result} = reactHooks.renderHook(() => useResolveRoute('/issues/', otherOrg), {
+      wrapper,
+    });
+    expect(result.current).toBe('https://other-org.dev.getsentry.net:7999/issues/');
+  });
+
+  it('should replace domains with dev-ui mode on sentry.dev', () => {
+    window.__SENTRY_DEV_UI = true;
+    window.location.host = 'acme.sentry-abc123.sentry.dev';
+
+    const wrapper = ({children}) => (
+      <OrganizationContext.Provider value={organization}>
+        {children}
+      </OrganizationContext.Provider>
+    );
+    const {result} = reactHooks.renderHook(() => useResolveRoute('/issues/', otherOrg), {
+      wrapper,
+    });
+    expect(result.current).toBe('https://other-org.sentry-abc123.sentry.dev/issues/');
+  });
+
+  it('will not replace domains with dev-ui mode and an unsafe host', () => {
+    window.__SENTRY_DEV_UI = true;
+    window.location.host = 'bad-domain.com';
+
+    const wrapper = ({children}) => (
+      <OrganizationContext.Provider value={organization}>
+        {children}
+      </OrganizationContext.Provider>
+    );
+    const {result} = reactHooks.renderHook(() => useResolveRoute('/issues/', otherOrg), {
+      wrapper,
+    });
+    expect(result.current).toBe('https://other-org.sentry.io/issues/');
+  });
+
+  it('should not replace domains normally', () => {
+    const wrapper = ({children}) => (
+      <OrganizationContext.Provider value={organization}>
+        {children}
+      </OrganizationContext.Provider>
+    );
+    const {result} = reactHooks.renderHook(() => useResolveRoute('/issues/', otherOrg), {
+      wrapper,
+    });
+    expect(result.current).toBe('https://other-org.sentry.io/issues/');
+  });
+});

+ 20 - 2
static/app/utils/useResolveRoute.tsx

@@ -2,11 +2,29 @@ import {useContext} from 'react';
 
 import ConfigStore from 'sentry/stores/configStore';
 import {OrganizationSummary} from 'sentry/types';
+import {extractSlug} from 'sentry/utils/extractSlug';
 import {OrganizationContext} from 'sentry/views/organizationContext';
 
 import shouldUseLegacyRoute from './shouldUseLegacyRoute';
 import {normalizeUrl} from './withDomainRequired';
 
+/**
+ * In yarn dev-ui mode we proxy API calls to sentry.io.
+ * However, all of the browser URLs are either acme.localhost,
+ * or acme.dev.getsentry.net so we need to hack up the server provided
+ * domain values.
+ */
+function localizeDomain(domain?: string) {
+  if (!window.__SENTRY_DEV_UI || !domain) {
+    return domain;
+  }
+  const slugDomain = extractSlug(window.location.host);
+  if (!slugDomain) {
+    return domain;
+  }
+  return domain.replace('sentry.io', slugDomain.domain);
+}
+
 /**
  * If organization is passed, then a URL with the route will be returned with the customer domain prefix attached if the
  * organization has customer domain feature enabled.
@@ -14,9 +32,9 @@ import {normalizeUrl} from './withDomainRequired';
  * use the sentry URL as the prefix.
  */
 function useResolveRoute(route: string, organization?: OrganizationSummary) {
-  const {sentryUrl} = ConfigStore.get('links');
   const currentOrganization = useContext(OrganizationContext);
   const hasCustomerDomain = currentOrganization?.features.includes('customer-domains');
+  const sentryUrl = localizeDomain(ConfigStore.get('links').sentryUrl);
 
   if (!organization) {
     if (hasCustomerDomain) {
@@ -25,7 +43,7 @@ function useResolveRoute(route: string, organization?: OrganizationSummary) {
     return route;
   }
 
-  const {organizationUrl} = organization.links;
+  const organizationUrl = localizeDomain(organization.links.organizationUrl);
 
   const useLegacyRoute = shouldUseLegacyRoute(organization);
   if (useLegacyRoute) {

+ 25 - 4
static/index.ejs

@@ -14,9 +14,28 @@
     </script>
     <script type="text/javascript">
     try {
-      var reg = new RegExp(/\/organizations\/(.+?(?=(\/|$)))(\/|$)/, 'i');
-      var organization = window.location.pathname;
-      var slug = organization.match(reg)[1];
+      function extractSlug() {
+        // XXX: If you change this also change its sibiling in static/app/utils/extractSlug.tsx
+        var knownDomains = /(?:localhost|dev\.getsentry.net|sentry.dev)(?:\:\d*)?$/;
+
+        var domainParts = window.location.host.split('.');
+        var slug = domainParts.shift();
+        var domain = domainParts.join('.');
+        if (domain.match(knownDomains)) {
+          return slug;
+        }
+
+        var pathReg = /\/organizations\/(.+?(?=(\/|$)))(\/|$)/i;
+        var matches = pathReg.exec(window.location.pathname);
+        if (matches) {
+          return matches[1];
+        }
+        console.error(`Could not extract an organization slug from ${window.location}. Assuming 'sentry'`);
+
+        return 'sentry';
+      }
+
+      var slug = extractSlug();
       var preloadPromises = { orgSlug: slug };
 
       function promiseRequest(url) {
@@ -50,7 +69,9 @@
         preloadPromises.projects =  promiseRequest(makeUrl('/projects/?all_projects=1&collapse=latestDeploys'));
         preloadPromises.teams = promiseRequest(makeUrl('/teams/'));
       }
-    } catch(_) {}
+    } catch(err) {
+      console.error(err)
+    }
     </script>
   </head>
 

+ 6 - 2
webpack.config.ts

@@ -526,8 +526,8 @@ if (
       'Access-Control-Allow-Origin': '*',
       'Access-Control-Allow-Credentials': 'true',
     },
-    // Required for getsentry
-    allowedHosts: 'all',
+    // Cover the various environments we use (vercel, getsentry-dev, localhost)
+    allowedHosts: ['.sentry.dev', '.dev.getsentry.net', '.localhost', '127.0.0.1'],
     static: {
       directory: './src/sentry/static/sentry',
       watch: true,
@@ -605,6 +605,7 @@ if (IS_UI_DEV_ONLY) {
         headers: {
           Referer: 'https://sentry.io/',
         },
+        cookieDomainRewrite: {'.sentry.io': 'localhost'},
       },
     ],
     historyApiFallback: {
@@ -636,6 +637,9 @@ if (IS_UI_DEV_ONLY || SENTRY_EXPERIMENTAL_SPA) {
       mobile: true,
       excludeChunks: ['pipeline'],
       title: 'Sentry',
+      window: {
+        __SENTRY_DEV_UI: true,
+      },
     })
   );
 }