diff --git a/src/sentry/static/sentry/app/bootstrap.jsx b/src/sentry/static/sentry/app/bootstrap.jsx new file mode 100644 index 000000000000..3412d8d3a33f --- /dev/null +++ b/src/sentry/static/sentry/app/bootstrap.jsx @@ -0,0 +1,151 @@ +import 'bootstrap/js/alert'; +import 'bootstrap/js/tab'; +import 'bootstrap/js/dropdown'; + +import 'app/utils/statics-setup'; +import 'app/utils/emotion-setup'; + +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import Reflux from 'reflux'; +import * as Router from 'react-router'; +import * as Sentry from '@sentry/browser'; +import {ExtraErrorData, Tracing} from '@sentry/integrations'; +import createReactClass from 'create-react-class'; +import jQuery from 'jquery'; +import moment from 'moment'; + +import {metric} from 'app/utils/analytics'; +import ConfigStore from 'app/stores/configStore'; +import Main from 'app/main'; +import ajaxCsrfSetup from 'app/utils/ajaxCsrfSetup'; +import plugins from 'app/plugins'; + +// SDK INIT -------------------------------------------------------- +Sentry.init({ + ...window.__SENTRY__OPTIONS, + integrations: [ + new ExtraErrorData({ + // 6 is arbitrary, seems like a nice number + depth: 6, + }), + new Tracing({ + tracingOrigins: ['localhost', 'sentry.io', /^\//], + autoStartOnDomReady: false, + }), + ], +}); + +Sentry.configureScope(scope => { + if (window.__SENTRY__USER) { + scope.setUser(window.__SENTRY__USER); + } + if (window.__SENTRY__VERSION) { + scope.setTag('sentry_version', window.__SENTRY__VERSION); + } +}); + +// ----------------------------------------------------------------- + +// Used for operational metrics to determine that the application js +// bundle was loaded by browser. +metric.mark('sentry-app-init'); + +// setup jquery for CSRF tokens +jQuery.ajaxSetup({ + //jQuery won't allow using the ajaxCsrfSetup function directly + beforeSend: ajaxCsrfSetup, +}); + +// App setup +if (window.__initialData) { + ConfigStore.loadInitialData(window.__initialData); +} + +// these get exported to a global variable, which is important as its the only +// way we can call into scoped objects + +const render = Component => { + const rootEl = document.getElementById('blk_router'); + + try { + ReactDOM.render(, rootEl); + } catch (err) { + // 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)' + ) + ); + if (err.message === 'URI malformed') { + window.location.assign(window.location.pathname); + } + } +}; + +// 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) { + const module = await import(/* webpackChunkName: "passwordStrength" */ 'app/components/passwordStrength'); + callback(module); +} + +const globals = { + // This is the primary entrypoint for rendering the sentry app. + SentryRenderApp: () => render(Main), + + // 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 + // makes use of it. + $: jQuery, + jQuery, + + // django templates make use of these globals + createReactClass, +}; + +// 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, + Alerts: require('app/components/alerts').default, + Indicators: require('app/components/indicators').default, + ProjectSelector: require('app/components/projectHeader/projectSelector').default, + Sidebar: require('app/components/sidebar').default, + SetupWizard: require('app/components/setupWizard').default, + OrganizationsLoader: require('app/components/organizations/organizationsLoader') + .default, +}; + +// Make globals available on the window object +Object.keys(globals).forEach(name => (window[name] = globals[name])); + +export default globals; diff --git a/src/sentry/static/sentry/app/index.js b/src/sentry/static/sentry/app/index.js index ce538c18ee45..4780e2b7cbe4 100644 --- a/src/sentry/static/sentry/app/index.js +++ b/src/sentry/static/sentry/app/index.js @@ -1,153 +1,15 @@ import '@babel/polyfill'; -import 'bootstrap/js/alert'; -import 'bootstrap/js/tab'; -import 'bootstrap/js/dropdown'; -import 'app/utils/statics-setup'; -import 'app/utils/emotion-setup'; - -import PropTypes from 'prop-types'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import Reflux from 'reflux'; -import * as Router from 'react-router'; -import * as Sentry from '@sentry/browser'; -import {ExtraErrorData, Tracing} from '@sentry/integrations'; -import createReactClass from 'create-react-class'; -import jQuery from 'jquery'; -import moment from 'moment'; - -import {metric} from 'app/utils/analytics'; -import ConfigStore from 'app/stores/configStore'; -import Main from 'app/main'; -import ajaxCsrfSetup from 'app/utils/ajaxCsrfSetup'; -import plugins from 'app/plugins'; - -// SDK INIT -------------------------------------------------------- -// window.__SENTRY__OPTIONS will be emmited by sdk-config.html before loading this script -Sentry.init({ - ...window.__SENTRY__OPTIONS, - integrations: [ - new ExtraErrorData({ - // 6 is arbitrary, seems like a nice number - depth: 6, - }), - new Tracing({ - tracingOrigins: ['localhost', 'sentry.io', /^\//], - autoStartOnDomReady: false, - }), - ], -}); - -Sentry.configureScope(scope => { - if (window.__SENTRY__USER) { - scope.setUser(window.__SENTRY__USER); - } - if (window.__SENTRY__VERSION) { - scope.setTag('sentry_version', window.__SENTRY__VERSION); - } -}); - -// ----------------------------------------------------------------- - -// Used for operational metrics to determine that the application js -// bundle was loaded by browser. -metric.mark('sentry-app-init'); - -// setup jquery for CSRF tokens -jQuery.ajaxSetup({ - //jQuery won't allow using the ajaxCsrfSetup function directly - beforeSend: ajaxCsrfSetup, -}); - -// App setup +// Initial data hydration. The __initialData will be set by the django backend +// serving sentry. if (window.__initialData) { - ConfigStore.loadInitialData(window.__initialData, window.__languageCode); -} - -// these get exported to a global variable, which is important as its the only -// way we can call into scoped objects + const {distPrefix, csrfCookieName, sentryConfig, userIdentity} = window.__initialData; -const render = Component => { - const rootEl = document.getElementById('blk_router'); - - try { - ReactDOM.render(, rootEl); - } catch (err) { - // 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)' - ) - ); - if (err.message === 'URI malformed') { - window.location.assign(window.location.pathname); - } - } -}; - -// 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) { - const module = await import(/* webpackChunkName: "passwordStrength" */ 'app/components/passwordStrength'); - callback(module); + window.csrfCookieName = csrfCookieName; + window.__sentryGlobalStaticPrefix = distPrefix; + window.__SENTRY__OPTIONS = sentryConfig; + window.__SENTRY__USER = userIdentity; } -const globals = { - // This is the primary entrypoint for rendering the sentry app. - SentryRenderApp: () => render(Main), - - // 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 - // makes use of it. - $: jQuery, - jQuery, - - // django templates make use of these globals - createReactClass, -}; - -// 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, - Alerts: require('app/components/alerts').default, - Indicators: require('app/components/indicators').default, - ProjectSelector: require('app/components/projectHeader/projectSelector').default, - Sidebar: require('app/components/sidebar').default, - SetupWizard: require('app/components/setupWizard').default, - OrganizationsLoader: require('app/components/organizations/organizationsLoader') - .default, -}; - -// Make globals available on the window object -Object.keys(globals).forEach(name => (window[name] = globals[name])); - -export default globals; +// Once data hydration is done we can initialize the app +require('app/bootstrap'); diff --git a/src/sentry/static/sentry/app/stores/configStore.jsx b/src/sentry/static/sentry/app/stores/configStore.jsx index 3f615bd0e75c..fad536777f86 100644 --- a/src/sentry/static/sentry/app/stores/configStore.jsx +++ b/src/sentry/static/sentry/app/stores/configStore.jsx @@ -23,10 +23,13 @@ const ConfigStore = Reflux.createStore({ return this.config; }, - loadInitialData(config, languageOverride = null) { + loadInitialData(config) { config.features = new Set(config.features || []); this.config = config; + // Language code is passed from django + let languageCode = config.languageCode; + // TODO(dcramer): abstract this out of ConfigStore if (config.user) { config.user.permissions = new Set(config.user.permissions); @@ -43,15 +46,13 @@ const ConfigStore = Reflux.createStore({ // e.g. unencoded "%" } - // Priority: - // "?lang=en" --> user configuration options --> django request.LANGUAGE_CODE --> "en" - setLocale( - queryString.lang || config.user.options.language || languageOverride || 'en' - ); - } else if (languageOverride) { - setLocale(languageOverride); + languageCode = queryString.lang || config.user.options.language || languageCode; } + // Priority: + // "?lang=en" --> user configuration options --> django request.LANGUAGE_CODE --> "en" + setLocale(languageCode || 'en'); + this.trigger(config); }, }); diff --git a/src/sentry/static/sentry/app/views/admin/adminUserEdit.jsx b/src/sentry/static/sentry/app/views/admin/adminUserEdit.jsx new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/sentry/templates/sentry/layout.html b/src/sentry/templates/sentry/layout.html index 8ebe3d98667b..9a1ce4c47881 100644 --- a/src/sentry/templates/sentry/layout.html +++ b/src/sentry/templates/sentry/layout.html @@ -34,14 +34,10 @@ {% block title %}Sentry{% endblock %} {% block scripts %} - {% include "sentry/includes/sdk-config.html" %} {% locale_js_include %} diff --git a/src/sentry/templatetags/sentry_react.py b/src/sentry/templatetags/sentry_react.py index 2cc7c3351d2f..72f737e42ab7 100644 --- a/src/sentry/templatetags/sentry_react.py +++ b/src/sentry/templatetags/sentry_react.py @@ -78,10 +78,25 @@ def get_react_config(context): messages = get_messages(request) session = getattr(request, 'session', None) is_superuser = is_active_superuser(request) + language_code = getattr(request, 'LANGUAGE_CODE', 'en') else: user = None messages = [] is_superuser = False + language_code = 'en' + + # User identity is used by the sentry SDK + if request and user: + user_identity = {'ip_address': request.META['REMOTE_ADDR']} + if user and user.is_authenticated(): + user_identity.update({ + 'email': user.email, + 'id': user.id, + }) + if user.name: + user_identity['name'] = user.name + else: + user_identity = {} enabled_features = [] if features.has('organizations:create', actor=user): @@ -103,6 +118,7 @@ def get_react_config(context): 'version': version_info, 'features': enabled_features, 'mediaUrl': get_asset_url('sentry', ''), + 'distPrefix': get_asset_url('sentry', 'dist/'), 'needsUpgrade': needs_upgrade, 'dsn': get_public_dsn(), 'statuspage': _get_statuspage(), @@ -119,6 +135,14 @@ def get_react_config(context): # It should only be used on a fresh browser nav to a path where an # organization is not in context 'lastOrganization': session['activeorg'] if session and 'activeorg' in session else None, + 'languageCode': language_code, + 'userIdentity': user_identity, + 'csrfCookieName': settings.CSRF_COOKIE_NAME, + 'sentryConfig': { + 'dsn': get_public_dsn(), + 'release': version_info['build'], + 'whitelistUrls': list(settings.ALLOWED_HOSTS), + }, } if user and user.is_authenticated(): context.update({ diff --git a/src/sentry/web/helpers.py b/src/sentry/web/helpers.py index 8bb807f418d0..f77bbbd3844b 100644 --- a/src/sentry/web/helpers.py +++ b/src/sentry/web/helpers.py @@ -27,7 +27,6 @@ def get_default_context(request, existing_context=None, team=None): from sentry.plugins import plugins context = { - 'CSRF_COOKIE_NAME': settings.CSRF_COOKIE_NAME, 'URL_PREFIX': options.get('system.url-prefix'), 'SINGLE_ORGANIZATION': settings.SENTRY_SINGLE_ORGANIZATION, 'PLUGINS': plugins,