diff options
-rw-r--r-- | app/assets/javascripts/raven/index.js | 16 | ||||
-rw-r--r-- | app/assets/javascripts/raven/raven_config.js | 47 | ||||
-rw-r--r-- | app/helpers/sentry_helper.rb | 10 | ||||
-rw-r--r-- | app/views/layouts/_head.html.haml | 3 | ||||
-rw-r--r-- | app/views/layouts/application.html.haml | 2 | ||||
-rw-r--r-- | app/views/layouts/devise.html.haml | 1 | ||||
-rw-r--r-- | app/views/layouts/devise_empty.html.haml | 1 | ||||
-rw-r--r-- | config/webpack.config.js | 5 | ||||
-rw-r--r-- | lib/gitlab/gon_helper.rb | 5 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | spec/features/raven_js_spec.rb | 24 | ||||
-rw-r--r-- | spec/helpers/sentry_helper_spec.rb | 22 | ||||
-rw-r--r-- | spec/javascripts/.eslintrc | 15 | ||||
-rw-r--r-- | spec/javascripts/raven/index_spec.js | 41 | ||||
-rw-r--r-- | spec/javascripts/raven/raven_config_spec.js | 192 | ||||
-rw-r--r-- | yarn.lock | 12 |
16 files changed, 390 insertions, 8 deletions
diff --git a/app/assets/javascripts/raven/index.js b/app/assets/javascripts/raven/index.js new file mode 100644 index 00000000000..3c5656040b9 --- /dev/null +++ b/app/assets/javascripts/raven/index.js @@ -0,0 +1,16 @@ +import RavenConfig from './raven_config'; + +const index = function index() { + RavenConfig.init({ + sentryDsn: gon.sentry_dsn, + currentUserId: gon.current_user_id, + whitelistUrls: [gon.gitlab_url], + isProduction: gon.is_production, + }); + + return RavenConfig; +}; + +index(); + +export default index; diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js new file mode 100644 index 00000000000..bb9be7cb196 --- /dev/null +++ b/app/assets/javascripts/raven/raven_config.js @@ -0,0 +1,47 @@ +import Raven from 'raven-js'; +import $ from 'jquery'; + +const RavenConfig = { + init(options = {}) { + this.options = options; + + this.configure(); + this.bindRavenErrors(); + if (this.options.currentUserId) this.setUser(); + }, + + configure() { + Raven.config(this.options.sentryDsn, { + whitelistUrls: this.options.whitelistUrls, + environment: this.options.isProduction ? 'production' : 'development', + }).install(); + }, + + setUser() { + Raven.setUserContext({ + id: this.options.currentUserId, + }); + }, + + bindRavenErrors() { + $(document).on('ajaxError.raven', this.handleRavenErrors); + }, + + handleRavenErrors(event, req, config, err) { + const error = err || req.statusText; + + Raven.captureMessage(error, { + extra: { + type: config.type, + url: config.url, + data: config.data, + status: req.status, + response: req.responseText.substring(0, 100), + error, + event, + }, + }); + }, +}; + +export default RavenConfig; diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb index 3d255df66a0..4b07f71bcea 100644 --- a/app/helpers/sentry_helper.rb +++ b/app/helpers/sentry_helper.rb @@ -6,4 +6,14 @@ module SentryHelper def sentry_context Gitlab::Sentry.context(current_user) end + + def sentry_dsn_public + sentry_dsn = ApplicationSetting.current.sentry_dsn + + return unless sentry_dsn + + uri = URI.parse(sentry_dsn) + uri.password = nil + uri.to_s + end end diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 19473b6ab27..8aef5cbdc04 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,9 +28,12 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" + = Gon::Base.render_data + = webpack_bundle_tag "runtime" = webpack_bundle_tag "common" = webpack_bundle_tag "main" + = webpack_bundle_tag "raven" if sentry_enabled? - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 36543edc040..4c7f0b57d16 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,8 +2,6 @@ %html{ lang: "en", class: "#{page_class}" } = render "layouts/head" %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } - = Gon::Base.render_data - = render "layouts/header/default", title: header_title = render 'layouts/page', sidebar: sidebar, nav: nav diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 3368a9beb29..52fb46eb8c9 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -3,7 +3,6 @@ = render "layouts/head" %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page } } .page-wrap - = Gon::Base.render_data = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index 7466423a934..ed6731bde95 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -2,7 +2,6 @@ %html{ lang: "en" } = render "layouts/head" %body.ui_charcoal.login-page.application.navless - = Gon::Base.render_data = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/config/webpack.config.js b/config/webpack.config.js index ffb16190093..f034a8ae27d 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -50,8 +50,9 @@ var config = { u2f: ['vendor/u2f'], users: './users/users_bundle.js', vue_pipelines: './vue_pipelines_index/index.js', + raven: './raven/index.js', issue_show: './issue_show/index.js', - group: './group.js', + group: './group.js' }, output: { @@ -67,7 +68,7 @@ var config = { { test: /\.js$/, exclude: /(node_modules|vendor\/assets)/, - loader: 'babel-loader', + loader: 'babel-loader?plugins=rewire', }, { test: /\.vue$/, diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 5ab84266b7d..4de504e9bf9 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -1,3 +1,5 @@ +include SentryHelper + module Gitlab module GonHelper def add_gon_variables @@ -10,6 +12,9 @@ module Gitlab gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css') gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js') + gon.sentry_dsn = sentry_dsn_public if sentry_enabled? + gon.gitlab_url = Gitlab.config.gitlab.url + gon.is_production = Rails.env.production? if current_user gon.current_user_id = current_user.id diff --git a/package.json b/package.json index a17399ddb8f..cf66d7ec58e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "mousetrap": "^1.4.6", "pikaday": "^1.5.1", "raphael": "^2.2.7", + "raven-js": "^3.14.0", "raw-loader": "^0.5.1", "react-dev-utils": "^0.5.2", "select2": "3.5.2-browserify", @@ -54,6 +55,7 @@ }, "devDependencies": { "babel-plugin-istanbul": "^4.0.0", + "babel-plugin-rewire": "^1.1.0", "eslint": "^3.10.1", "eslint-config-airbnb-base": "^10.0.1", "eslint-import-resolver-webpack": "^0.8.1", diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb new file mode 100644 index 00000000000..74df52d80a7 --- /dev/null +++ b/spec/features/raven_js_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +feature 'RavenJS', :feature, :js do + let(:raven_path) { '/raven.bundle.js' } + + it 'should not load raven if sentry is disabled' do + visit new_user_session_path + + expect(has_requested_raven).to eq(false) + end + + it 'should load raven if sentry is enabled' do + allow_any_instance_of(SentryHelper).to receive_messages(sentry_dsn_public: 'https://key@domain.com/id', + sentry_enabled?: true) + + visit new_user_session_path + + expect(has_requested_raven).to eq(true) + end + + def has_requested_raven + page.driver.network_traffic.one? {|request| request.url.end_with?(raven_path)} + end +end diff --git a/spec/helpers/sentry_helper_spec.rb b/spec/helpers/sentry_helper_spec.rb new file mode 100644 index 00000000000..ff218235cd1 --- /dev/null +++ b/spec/helpers/sentry_helper_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe SentryHelper do + describe '#sentry_dsn_public' do + it 'returns nil if no sentry_dsn is set' do + mock_sentry_dsn(nil) + + expect(helper.sentry_dsn_public).to eq nil + end + + it 'returns the uri string with no password if sentry_dsn is set' do + mock_sentry_dsn('https://test:dsn@host/path') + + expect(helper.sentry_dsn_public).to eq 'https://test@host/path' + end + end + + def mock_sentry_dsn(value) + allow_message_expectations_on_nil + allow(ApplicationSetting.current).to receive(:sentry_dsn).and_return(value) + end +end diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc index 3d922021978..a3a8bb050c9 100644 --- a/spec/javascripts/.eslintrc +++ b/spec/javascripts/.eslintrc @@ -27,6 +27,19 @@ "jasmine/no-suite-dupes": [1, "branch"], "jasmine/no-spec-dupes": [1, "branch"], "no-console": 0, - "prefer-arrow-callback": 0 + "prefer-arrow-callback": 0, + "no-underscore-dangle": [ + 2, + { + "allow": [ + "__GetDependency__", + "__Rewire__", + "__ResetDependency__", + "__get__", + "__set__", + "__RewireAPI__" + ] + } + ] } } diff --git a/spec/javascripts/raven/index_spec.js b/spec/javascripts/raven/index_spec.js new file mode 100644 index 00000000000..85ec1de4e4e --- /dev/null +++ b/spec/javascripts/raven/index_spec.js @@ -0,0 +1,41 @@ +import RavenConfig from '~/raven/raven_config'; +import index from '~/raven/index'; + +describe('RavenConfig options', () => { + let sentryDsn; + let currentUserId; + let gitlabUrl; + let isProduction; + let indexReturnValue; + + beforeEach(() => { + sentryDsn = 'sentryDsn'; + currentUserId = 'currentUserId'; + gitlabUrl = 'gitlabUrl'; + isProduction = 'isProduction'; + + window.gon = { + sentry_dsn: sentryDsn, + current_user_id: currentUserId, + gitlab_url: gitlabUrl, + is_production: isProduction, + }; + + spyOn(RavenConfig, 'init'); + + indexReturnValue = index(); + }); + + it('should init with .sentryDsn, .currentUserId, .whitelistUrls and .isProduction', () => { + expect(RavenConfig.init).toHaveBeenCalledWith({ + sentryDsn, + currentUserId, + whitelistUrls: [gitlabUrl], + isProduction, + }); + }); + + it('should return RavenConfig', () => { + expect(indexReturnValue).toBe(RavenConfig); + }); +}); diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js new file mode 100644 index 00000000000..b8bb558d22e --- /dev/null +++ b/spec/javascripts/raven/raven_config_spec.js @@ -0,0 +1,192 @@ +import Raven from 'raven-js'; +import RavenConfig, { __RewireAPI__ as RavenConfigRewire } from '~/raven/raven_config'; + +describe('RavenConfig', () => { + describe('init', () => { + let options; + + beforeEach(() => { + options = { + sentryDsn: '//sentryDsn', + ravenAssetUrl: '//ravenAssetUrl', + currentUserId: 1, + whitelistUrls: ['//gitlabUrl'], + isProduction: true, + }; + + spyOn(RavenConfig, 'configure'); + spyOn(RavenConfig, 'bindRavenErrors'); + spyOn(RavenConfig, 'setUser'); + + RavenConfig.init(options); + }); + + it('should set the options property', () => { + expect(RavenConfig.options).toEqual(options); + }); + + it('should call the configure method', () => { + expect(RavenConfig.configure).toHaveBeenCalled(); + }); + + it('should call the error bindings method', () => { + expect(RavenConfig.bindRavenErrors).toHaveBeenCalled(); + }); + + it('should call setUser', () => { + expect(RavenConfig.setUser).toHaveBeenCalled(); + }); + + it('should not call setUser if there is no current user ID', () => { + RavenConfig.setUser.calls.reset(); + + RavenConfig.init({ + sentryDsn: '//sentryDsn', + ravenAssetUrl: '//ravenAssetUrl', + currentUserId: undefined, + whitelistUrls: ['//gitlabUrl'], + isProduction: true, + }); + + expect(RavenConfig.setUser).not.toHaveBeenCalled(); + }); + }); + + describe('configure', () => { + let options; + let raven; + + beforeEach(() => { + options = { + sentryDsn: '//sentryDsn', + whitelistUrls: ['//gitlabUrl'], + isProduction: true, + }; + + raven = jasmine.createSpyObj('raven', ['install']); + + spyOn(Raven, 'config').and.returnValue(raven); + + RavenConfig.configure.call({ + options, + }); + }); + + it('should call Raven.config', () => { + expect(Raven.config).toHaveBeenCalledWith(options.sentryDsn, { + whitelistUrls: options.whitelistUrls, + environment: 'production', + }); + }); + + it('should call Raven.install', () => { + expect(raven.install).toHaveBeenCalled(); + }); + + it('should set .environment to development if isProduction is false', () => { + options.isProduction = false; + + RavenConfig.configure.call({ + options, + }); + + expect(Raven.config).toHaveBeenCalledWith(options.sentryDsn, { + whitelistUrls: options.whitelistUrls, + environment: 'development', + }); + }); + }); + + describe('setUser', () => { + let ravenConfig; + + beforeEach(() => { + ravenConfig = { options: { currentUserId: 1 } }; + spyOn(Raven, 'setUserContext'); + + RavenConfig.setUser.call(ravenConfig); + }); + + it('should call .setUserContext', function () { + expect(Raven.setUserContext).toHaveBeenCalledWith({ + id: ravenConfig.options.currentUserId, + }); + }); + }); + + describe('bindRavenErrors', () => { + let $document; + let $; + + beforeEach(() => { + $document = jasmine.createSpyObj('$document', ['on']); + $ = jasmine.createSpy('$').and.returnValue($document); + + RavenConfigRewire.__set__('$', $); + + RavenConfig.bindRavenErrors(); + }); + + it('should query for document using jquery', () => { + expect($).toHaveBeenCalledWith(document); + }); + + it('should call .on', function () { + expect($document.on).toHaveBeenCalledWith('ajaxError.raven', RavenConfig.handleRavenErrors); + }); + }); + + describe('handleRavenErrors', () => { + let event; + let req; + let config; + let err; + + beforeEach(() => { + event = {}; + req = { status: 'status', responseText: 'responseText', statusText: 'statusText' }; + config = { type: 'type', url: 'url', data: 'data' }; + err = {}; + + spyOn(Raven, 'captureMessage'); + + RavenConfig.handleRavenErrors(event, req, config, err); + }); + + it('should call Raven.captureMessage', () => { + expect(Raven.captureMessage).toHaveBeenCalledWith(err, { + extra: { + type: config.type, + url: config.url, + data: config.data, + status: req.status, + response: req.responseText.substring(0, 100), + error: err, + event, + }, + }); + }); + + describe('if no err is provided', () => { + beforeEach(() => { + Raven.captureMessage.calls.reset(); + + RavenConfig.handleRavenErrors(event, req, config); + }); + + it('should use req.statusText as the error value', () => { + expect(Raven.captureMessage).toHaveBeenCalledWith(req.statusText, { + extra: { + type: config.type, + url: config.url, + data: config.data, + status: req.status, + response: req.responseText.substring(0, 100), + error: req.statusText, + event, + }, + }); + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index e16cd9c3673..e41f737c88c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -425,6 +425,10 @@ babel-plugin-istanbul@^4.0.0: istanbul-lib-instrument "^1.4.2" test-exclude "^4.0.0" +babel-plugin-rewire@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-rewire/-/babel-plugin-rewire-1.1.0.tgz#a6b966d9d8c06c03d95dcda2eec4e2521519549b" + babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" @@ -3157,7 +3161,7 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" -json-stringify-safe@~5.0.1: +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -4530,6 +4534,12 @@ raphael@^2.2.7: dependencies: eve-raphael "0.5.0" +raven-js@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.14.0.tgz#94dda81d975fdc4a42f193db437cf70021d654e0" + dependencies: + json-stringify-safe "^5.0.1" + raw-body@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" |