Browse Source

ref(ui): Move PipelineView to new webpack entry (#25174)

Refactor our bootstrap script to more discrete modules. Add a separate webpack entry for rendering pipeline view. Move initial rendering of SPA/PipelineView out of server templates and into the JS bundles. This is necessary for splitting up frontend deploys as there are more guarantees of modules being loaded inside of the JS entry, instead of in the server templates.

Removed `__SENTRY__OPTIONS/USER` globals as we can pass and use the `config/initialData` objects directly.

Note, I've removed the router instrumentation for Pipeline view as it significantly increases bundle size due to importing `app/routes`.

The Pipeline entrypoint also does not include `bootstrap/js/*` imports.

Works locally:
![image](https://user-images.githubusercontent.com/79684/114439107-7620a400-9b7d-11eb-8076-a1f006b9c442.png)
Billy Vong 3 years ago
parent
commit
262eef0b3d

+ 0 - 209
src/sentry/static/sentry/app/bootstrap.tsx

@@ -1,209 +0,0 @@
-import 'bootstrap/js/alert';
-import 'bootstrap/js/tab';
-import 'bootstrap/js/dropdown';
-import 'focus-visible';
-
-import React from 'react';
-import ReactDOM from 'react-dom';
-import * as Router from 'react-router';
-import {ExtraErrorData} from '@sentry/integrations';
-import * as Sentry from '@sentry/react';
-import SentryRRWeb from '@sentry/rrweb';
-import {Integrations} from '@sentry/tracing';
-import createReactClass from 'create-react-class';
-import jQuery from 'jquery';
-import moment from 'moment';
-import PropTypes from 'prop-types';
-import Reflux from 'reflux';
-
-import {DISABLE_RR_WEB, NODE_ENV, SPA_DSN} from 'app/constants';
-import Main from 'app/main';
-import plugins from 'app/plugins';
-import routes from 'app/routes';
-import ConfigStore from 'app/stores/configStore';
-import {metric} from 'app/utils/analytics';
-import {init as initApiSentryClient} from 'app/utils/apiSentryClient';
-import {setupColorScheme} from 'app/utils/matchMedia';
-import PipelineView from 'app/views/integrationPipeline/pipelineView';
-
-if (NODE_ENV === 'development') {
-  import(
-    /* webpackChunkName: "SilenceReactUnsafeWarnings" */ /* webpackMode: "eager" */ 'app/utils/silence-react-unsafe-warnings'
-  );
-}
-
-// App setup
-if (window.__initialData) {
-  ConfigStore.loadInitialData(window.__initialData);
-
-  if (window.__initialData.dsn_requests) {
-    initApiSentryClient(window.__initialData.dsn_requests);
-  }
-}
-
-// SDK INIT  --------------------------------------------------------
-const config = ConfigStore.getConfig();
-
-const tracesSampleRate = config ? config.apmSampling : 0;
-
-function getSentryIntegrations(hasReplays: boolean = false) {
-  const integrations = [
-    new ExtraErrorData({
-      // 6 is arbitrary, seems like a nice number
-      depth: 6,
-    }),
-    new Integrations.BrowserTracing({
-      routingInstrumentation: Sentry.reactRouterV3Instrumentation(
-        Router.browserHistory as any,
-        Router.createRoutes(routes()),
-        Router.match
-      ),
-      idleTimeout: 5000,
-    }),
-  ];
-  if (hasReplays) {
-    // eslint-disable-next-line no-console
-    console.log('[sentry] Instrumenting session with rrweb');
-
-    // TODO(ts): The type returned by SentryRRWeb seems to be somewhat
-    // incompatible. It's a newer plugin, so this can be expected, but we
-    // should fix.
-    integrations.push(
-      new SentryRRWeb({
-        checkoutEveryNms: 60 * 1000, // 60 seconds
-      }) as any
-    );
-  }
-  return integrations;
-}
-
-const hasReplays =
-  window.__SENTRY__USER && window.__SENTRY__USER.isStaff && !DISABLE_RR_WEB;
-
-Sentry.init({
-  ...window.__SENTRY__OPTIONS,
-  /**
-   * For SPA mode, we need a way to overwrite the default DSN from backend
-   * as well as `whitelistUrls`
-   */
-  dsn: SPA_DSN || window.__SENTRY__OPTIONS.dsn,
-  whitelistUrls: SPA_DSN
-    ? ['localhost', 'dev.getsentry.net', 'sentry.dev', 'webpack-internal://']
-    : window.__SENTRY__OPTIONS.whitelistUrls,
-  integrations: getSentryIntegrations(hasReplays),
-  tracesSampleRate,
-});
-
-if (window.__SENTRY__USER) {
-  Sentry.setUser(window.__SENTRY__USER);
-}
-if (window.__SENTRY__VERSION) {
-  Sentry.setTag('sentry_version', window.__SENTRY__VERSION);
-}
-Sentry.setTag('rrweb.active', hasReplays ? 'yes' : 'no');
-
-// Used for operational metrics to determine that the application js
-// bundle was loaded by browser.
-metric.mark({name: 'sentry-app-init'});
-
-const ROOT_ELEMENT = 'blk_router';
-
-const render = (Component: React.ComponentType) => {
-  const rootEl = document.getElementById(ROOT_ELEMENT);
-
-  try {
-    ReactDOM.render(<Component />, rootEl);
-  } catch (err) {
-    if (err.message === 'URI malformed') {
-      // eslint-disable-next-line no-console
-      console.error(
-        new Error(
-          'An unencoded "%" has appeared, it is super effective! (See https://github.com/ReactTraining/history/issues/505)'
-        )
-      );
-      window.location.assign(window.location.pathname);
-    }
-  }
-};
-
-const RenderPipelineView = (pipelineName: string, props: Object) => {
-  const rootEl = document.getElementById(ROOT_ELEMENT);
-  ReactDOM.render(<PipelineView pipelineName={pipelineName} {...props} />, rootEl);
-};
-
-// setup darkmode + favicon
-setupColorScheme();
-
-// The password strength component is very heavyweight as it includes the
-// zxcvbn, a relatively byte-heavy password strength estimation library. Load
-// it on demand.
-async function loadPasswordStrength(callback: Function) {
-  try {
-    const module = await import(
-      /* webpackChunkName: "passwordStrength" */ 'app/components/passwordStrength'
-    );
-    callback(module);
-  } catch (err) {
-    // Ignore if client can't load this, it enhances UX a bit, but is optional
-  }
-}
-
-const globals = {
-  // This is the primary entrypoint for rendering the sentry app.
-  SentryRenderApp: () => render(Main),
-
-  // This is used to render pipeline views (such as the integration popup)
-  RenderPipelineView,
-
-  // The following globals are used in sentry-plugins webpack externals
-  // configuration.
-  PropTypes,
-  React,
-  Reflux,
-  Router,
-  Sentry,
-  moment,
-  ReactDOM: {
-    findDOMNode: ReactDOM.findDOMNode,
-    render: ReactDOM.render,
-  },
-
-  // jQuery is still exported to the window as some bootsrap functionality
-  // and legacy plugins like youtrack make use of it.
-  $: jQuery,
-  jQuery,
-
-  // django templates make use of these globals
-  createReactClass,
-  SentryApp: {},
-};
-
-// The SentryApp global contains exported app modules for use in javascript
-// modules that are not compiled with the sentry bundle.
-globals.SentryApp = {
-  // The following components are used in sentry-plugins.
-  Form: require('app/components/forms/form').default,
-  FormState: require('app/components/forms/index').FormState,
-  LoadingIndicator: require('app/components/loadingIndicator').default,
-  plugins: {
-    add: plugins.add,
-    addContext: plugins.addContext,
-    BasePlugin: plugins.BasePlugin,
-    DefaultIssuePlugin: plugins.DefaultIssuePlugin,
-  },
-
-  // The following components are used in legacy django HTML views
-  passwordStrength: {load: loadPasswordStrength},
-  U2fSign: require('app/components/u2f/u2fsign').default,
-  ConfigStore: require('app/stores/configStore').default,
-  SystemAlerts: require('app/views/app/systemAlerts').default,
-  Indicators: require('app/components/indicators').default,
-  SetupWizard: require('app/components/setupWizard').default,
-  HookStore: require('app/stores/hookStore').default,
-  Modal: require('app/actionCreators/modal'),
-};
-
-// Make globals available on the window object
-Object.keys(globals).forEach(name => (window[name] = globals[name]));
-
-export default globals;

+ 19 - 0
src/sentry/static/sentry/app/bootstrap/commonInitialization.tsx

@@ -0,0 +1,19 @@
+import 'focus-visible';
+
+import {NODE_ENV} from 'app/constants';
+import ConfigStore from 'app/stores/configStore';
+import {Config} from 'app/types';
+import {setupColorScheme} from 'app/utils/matchMedia';
+
+export function commonInitialization(config: Config) {
+  if (NODE_ENV === 'development') {
+    import(
+      /* webpackChunkName: "SilenceReactUnsafeWarnings" */ /* webpackMode: "eager" */ 'app/utils/silence-react-unsafe-warnings'
+    );
+  }
+
+  ConfigStore.loadInitialData(config);
+
+  // setup darkmode + favicon
+  setupColorScheme();
+}

+ 77 - 0
src/sentry/static/sentry/app/bootstrap/exportGlobals.tsx

@@ -0,0 +1,77 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import * as Router from 'react-router';
+import * as Sentry from '@sentry/react';
+import createReactClass from 'create-react-class';
+import jQuery from 'jquery';
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import Reflux from 'reflux';
+
+import plugins from 'app/plugins';
+
+// The password strength component is very heavyweight as it includes the
+// zxcvbn, a relatively byte-heavy password strength estimation library. Load
+// it on demand.
+async function loadPasswordStrength(callback: Function) {
+  try {
+    const module = await import(
+      /* webpackChunkName: "passwordStrength" */ 'app/components/passwordStrength'
+    );
+    callback(module);
+  } catch (err) {
+    // Ignore if client can't load this, it enhances UX a bit, but is optional
+  }
+}
+
+const globals = {
+  // The following globals are used in sentry-plugins webpack externals
+  // configuration.
+  PropTypes,
+  React,
+  Reflux,
+  Router,
+  Sentry,
+  moment,
+  ReactDOM: {
+    findDOMNode: ReactDOM.findDOMNode,
+    render: ReactDOM.render,
+  },
+
+  // jQuery is still exported to the window as some bootsrap functionality
+  // and legacy plugins like youtrack make use of it.
+  $: jQuery,
+  jQuery,
+
+  // django templates make use of these globals
+  createReactClass,
+  SentryApp: {},
+};
+
+// The SentryApp global contains exported app modules for use in javascript
+// modules that are not compiled with the sentry bundle.
+globals.SentryApp = {
+  // The following components are used in sentry-plugins.
+  Form: require('app/components/forms/form').default,
+  FormState: require('app/components/forms/index').FormState,
+  LoadingIndicator: require('app/components/loadingIndicator').default,
+  plugins: {
+    add: plugins.add,
+    addContext: plugins.addContext,
+    BasePlugin: plugins.BasePlugin,
+    DefaultIssuePlugin: plugins.DefaultIssuePlugin,
+  },
+
+  // The following components are used in legacy django HTML views
+  passwordStrength: {load: loadPasswordStrength},
+  U2fSign: require('app/components/u2f/u2fsign').default,
+  ConfigStore: require('app/stores/configStore').default,
+  SystemAlerts: require('app/views/app/systemAlerts').default,
+  Indicators: require('app/components/indicators').default,
+  SetupWizard: require('app/components/setupWizard').default,
+  HookStore: require('app/stores/hookStore').default,
+  Modal: require('app/actionCreators/modal'),
+};
+
+// Make globals available on the window object
+Object.keys(globals).forEach(name => (window[name] = globals[name]));

+ 23 - 0
src/sentry/static/sentry/app/bootstrap/initializeMain.tsx

@@ -0,0 +1,23 @@
+import 'bootstrap/js/alert';
+import 'bootstrap/js/tab';
+import 'bootstrap/js/dropdown';
+import './exportGlobals';
+
+import routes from 'app/routes';
+import {Config} from 'app/types';
+import {metric} from 'app/utils/analytics';
+
+import {commonInitialization} from './commonInitialization';
+import {initializeSdk} from './initializeSdk';
+import {renderMain} from './renderMain';
+import {renderOnDomReady} from './renderOnDomReady';
+
+export function initializeMain(config: Config) {
+  commonInitialization(config);
+  initializeSdk(config, {routes});
+
+  // Used for operational metrics to determine that the application js
+  // bundle was loaded by browser.
+  metric.mark({name: 'sentry-app-init'});
+  renderOnDomReady(renderMain);
+}

+ 24 - 0
src/sentry/static/sentry/app/bootstrap/initializePipelineView.tsx

@@ -0,0 +1,24 @@
+import {Config} from 'app/types';
+import {metric} from 'app/utils/analytics';
+
+import {commonInitialization} from './commonInitialization';
+import {initializeSdk} from './initializeSdk';
+import {renderOnDomReady} from './renderOnDomReady';
+import {renderPipelineView} from './renderPipelineView';
+
+export function initializePipelineView(config: Config) {
+  commonInitialization(config);
+  /**
+   * XXX: Note we do not include routingInstrumentation because importing
+   * `app/routes` significantly increases bundle size.
+   *
+   * A potential solution would be to use dynamic imports here to import
+   * `app/routes` to pass to `initializeSdk()`
+   */
+  initializeSdk(config);
+
+  // Used for operational metrics to determine that the application js
+  // bundle was loaded by browser.
+  metric.mark({name: 'sentry-pipeline-init'});
+  renderOnDomReady(renderPipelineView);
+}

+ 89 - 0
src/sentry/static/sentry/app/bootstrap/initializeSdk.tsx

@@ -0,0 +1,89 @@
+import * as Router from 'react-router';
+import {ExtraErrorData} from '@sentry/integrations';
+import * as Sentry from '@sentry/react';
+import SentryRRWeb from '@sentry/rrweb';
+import {Integrations} from '@sentry/tracing';
+
+import {DISABLE_RR_WEB, SPA_DSN} from 'app/constants';
+import {Config} from 'app/types';
+import {init as initApiSentryClient} from 'app/utils/apiSentryClient';
+
+/**
+ * We accept a routes argument here because importing `app/routes`
+ * is expensive in regards to bundle size. Some entrypoints may opt to forgo
+ * having routing instrumentation in order to have a smaller bundle size.
+ * (e.g.  `app/views/integrationPipeline`)
+ */
+function getSentryIntegrations(hasReplays: boolean = false, routes?: Function) {
+  const integrations = [
+    new ExtraErrorData({
+      // 6 is arbitrary, seems like a nice number
+      depth: 6,
+    }),
+    new Integrations.BrowserTracing({
+      ...(typeof routes === 'function'
+        ? {
+            routingInstrumentation: Sentry.reactRouterV3Instrumentation(
+              Router.browserHistory as any,
+              Router.createRoutes(routes()),
+              Router.match
+            ),
+          }
+        : {}),
+      idleTimeout: 5000,
+    }),
+  ];
+  if (hasReplays) {
+    // eslint-disable-next-line no-console
+    console.log('[sentry] Instrumenting session with rrweb');
+
+    // TODO(ts): The type returned by SentryRRWeb seems to be somewhat
+    // incompatible. It's a newer plugin, so this can be expected, but we
+    // should fix.
+    integrations.push(
+      new SentryRRWeb({
+        checkoutEveryNms: 60 * 1000, // 60 seconds
+      }) as any
+    );
+  }
+  return integrations;
+}
+
+/**
+ * Initialize the Sentry SDK
+ *
+ * If `routes` is passed, we will instrument react-router. Not all
+ * entrypoints require this.
+ */
+export function initializeSdk(config: Config, {routes}: {routes?: Function} = {}) {
+  if (config.dsn_requests) {
+    initApiSentryClient(config.dsn_requests);
+  }
+
+  const {apmSampling, sentryConfig, userIdentity} = config;
+  const tracesSampleRate = apmSampling ?? 0;
+
+  const hasReplays = userIdentity?.isStaff && !DISABLE_RR_WEB;
+
+  Sentry.init({
+    ...sentryConfig,
+    /**
+     * For SPA mode, we need a way to overwrite the default DSN from backend
+     * as well as `whitelistUrls`
+     */
+    dsn: SPA_DSN || sentryConfig?.dsn,
+    whitelistUrls: SPA_DSN
+      ? ['localhost', 'dev.getsentry.net', 'sentry.dev', 'webpack-internal://']
+      : sentryConfig?.whitelistUrls,
+    integrations: getSentryIntegrations(hasReplays, routes),
+    tracesSampleRate,
+  });
+
+  if (userIdentity) {
+    Sentry.setUser(userIdentity);
+  }
+  if (window.__SENTRY__VERSION) {
+    Sentry.setTag('sentry_version', window.__SENTRY__VERSION);
+  }
+  Sentry.setTag('rrweb.active', hasReplays ? 'yes' : 'no');
+}

+ 23 - 0
src/sentry/static/sentry/app/bootstrap/renderMain.tsx

@@ -0,0 +1,23 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import {ROOT_ELEMENT} from 'app/constants';
+import Main from 'app/main';
+
+export function renderMain() {
+  const rootEl = document.getElementById(ROOT_ELEMENT);
+
+  try {
+    ReactDOM.render(<Main />, rootEl);
+  } catch (err) {
+    if (err.message === 'URI malformed') {
+      // eslint-disable-next-line no-console
+      console.error(
+        new Error(
+          'An unencoded "%" has appeared, it is super effective! (See https://github.com/ReactTraining/history/issues/505)'
+        )
+      );
+      window.location.assign(window.location.pathname);
+    }
+  }
+}

+ 7 - 0
src/sentry/static/sentry/app/bootstrap/renderOnDomReady.tsx

@@ -0,0 +1,7 @@
+export function renderOnDomReady(renderFn: () => void) {
+  if (document.readyState === 'complete') {
+    renderFn();
+  } else {
+    document.addEventListener('DOMContentLoaded', renderFn);
+  }
+}

+ 16 - 0
src/sentry/static/sentry/app/bootstrap/renderPipelineView.tsx

@@ -0,0 +1,16 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import {ROOT_ELEMENT} from 'app/constants';
+import {PipelineInitialData} from 'app/types';
+import PipelineView from 'app/views/integrationPipeline/pipelineView';
+
+function render(pipelineName: string, props: PipelineInitialData['props']) {
+  const rootEl = document.getElementById(ROOT_ELEMENT);
+  ReactDOM.render(<PipelineView pipelineName={pipelineName} {...props} />, rootEl);
+}
+
+export function renderPipelineView() {
+  const {name, props} = window.__pipelineInitialData;
+  render(name, props);
+}

+ 3 - 0
src/sentry/static/sentry/app/constants/index.tsx

@@ -6,6 +6,9 @@
 import {t} from 'app/locale';
 import {Scope} from 'app/types';
 
+// This is the element id where we render our React application to
+export const ROOT_ELEMENT = 'blk_router';
+
 // This is considered the "default" route/view that users should be taken
 // to when the application does not have any further context
 //

Some files were not shown because too many files changed in this diff