summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/raven/index.js16
-rw-r--r--app/assets/javascripts/raven/raven_config.js100
-rw-r--r--app/helpers/sentry_helper.rb10
-rw-r--r--app/views/layouts/_head.html.haml3
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml1
-rw-r--r--app/views/layouts/devise_empty.html.haml1
-rw-r--r--config/webpack.config.js1
-rw-r--r--lib/gitlab/gon_helper.rb5
-rw-r--r--package.json1
-rw-r--r--spec/features/raven_js_spec.rb24
-rw-r--r--spec/helpers/sentry_helper_spec.rb22
-rw-r--r--spec/javascripts/raven/index_spec.js41
-rw-r--r--spec/javascripts/raven/raven_config_spec.js278
-rw-r--r--yarn.lock8
15 files changed, 508 insertions, 5 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..6c20cd9aabd
--- /dev/null
+++ b/app/assets/javascripts/raven/raven_config.js
@@ -0,0 +1,100 @@
+import Raven from 'raven-js';
+
+const IGNORE_ERRORS = [
+ // Random plugins/extensions
+ 'top.GLOBALS',
+ // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html
+ 'originalCreateNotification',
+ 'canvas.contentDocument',
+ 'MyApp_RemoveAllHighlights',
+ 'http://tt.epicplay.com',
+ 'Can\'t find variable: ZiteReader',
+ 'jigsaw is not defined',
+ 'ComboSearch is not defined',
+ 'http://loading.retry.widdit.com/',
+ 'atomicFindClose',
+ // Facebook borked
+ 'fb_xd_fragment',
+ // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
+ // reduce this. (thanks @acdha)
+ // See http://stackoverflow.com/questions/4113268
+ 'bmi_SafeAddOnload',
+ 'EBCallBackMessageReceived',
+ // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
+ 'conduitPage',
+];
+
+const IGNORE_URLS = [
+ // Facebook flakiness
+ /graph\.facebook\.com/i,
+ // Facebook blocked
+ /connect\.facebook\.net\/en_US\/all\.js/i,
+ // Woopra flakiness
+ /eatdifferent\.com\.woopra-ns\.com/i,
+ /static\.woopra\.com\/js\/woopra\.js/i,
+ // Chrome extensions
+ /extensions\//i,
+ /^chrome:\/\//i,
+ // Other plugins
+ /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
+ /webappstoolbarba\.texthelp\.com\//i,
+ /metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
+];
+
+const SAMPLE_RATE = 95;
+
+const RavenConfig = {
+ IGNORE_ERRORS,
+ IGNORE_URLS,
+ SAMPLE_RATE,
+ 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',
+ ignoreErrors: this.IGNORE_ERRORS,
+ ignoreUrls: this.IGNORE_URLS,
+ shouldSendCallback: this.shouldSendSample,
+ }).install();
+ },
+
+ setUser() {
+ Raven.setUserContext({
+ id: this.options.currentUserId,
+ });
+ },
+
+ bindRavenErrors() {
+ window.$(document).on('ajaxError.raven', this.handleRavenErrors);
+ },
+
+ handleRavenErrors(event, req, config, err) {
+ const error = err || req.statusText;
+ const responseText = req.responseText || 'Unknown response text';
+
+ Raven.captureMessage(error, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: responseText.substring(0, 100),
+ error,
+ event,
+ },
+ });
+ },
+
+ shouldSendSample() {
+ return Math.random() * 100 <= this.SAMPLE_RATE;
+ },
+};
+
+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 cb0a57a3a41..fc5754b6db9 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -52,6 +52,7 @@ var config = {
terminal: './terminal/terminal_bundle.js',
u2f: ['vendor/u2f'],
users: './users/users_bundle.js',
+ raven: './raven/index.js',
},
output: {
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 f8c151ebd81..9d2312163dd 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"pikaday": "^1.5.1",
"prismjs": "^1.6.0",
"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",
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/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..78251b4f61b
--- /dev/null
+++ b/spec/javascripts/raven/raven_config_spec.js
@@ -0,0 +1,278 @@
+import Raven from 'raven-js';
+import RavenConfig from '~/raven/raven_config';
+
+describe('RavenConfig', () => {
+ describe('IGNORE_ERRORS', () => {
+ it('should be an array of strings', () => {
+ const areStrings = RavenConfig.IGNORE_ERRORS.every(error => typeof error === 'string');
+
+ expect(areStrings).toBe(true);
+ });
+ });
+
+ describe('IGNORE_URLS', () => {
+ it('should be an array of regexps', () => {
+ const areRegExps = RavenConfig.IGNORE_URLS.every(url => url instanceof RegExp);
+
+ expect(areRegExps).toBe(true);
+ });
+ });
+
+ describe('SAMPLE_RATE', () => {
+ it('should be a finite number', () => {
+ expect(typeof RavenConfig.SAMPLE_RATE).toEqual('number');
+ });
+ });
+
+ 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',
+ ignoreErrors: Raven.IGNORE_ERRORS,
+ ignoreUrls: Raven.IGNORE_URLS,
+ shouldSendCallback: Raven.shouldSendSample,
+ });
+ });
+
+ 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',
+ ignoreErrors: Raven.IGNORE_ERRORS,
+ ignoreUrls: Raven.IGNORE_URLS,
+ shouldSendCallback: Raven.shouldSendSample,
+ });
+ });
+ });
+
+ 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);
+
+ window.$ = $;
+
+ RavenConfig.bindRavenErrors();
+ });
+
+ it('should query for document using jquery', () => {
+ expect(window.$).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,
+ },
+ });
+ });
+ });
+
+ describe('if no req.responseText is provided', () => {
+ beforeEach(() => {
+ req.responseText = undefined;
+
+ Raven.captureMessage.calls.reset();
+
+ RavenConfig.handleRavenErrors(event, req, config, err);
+ });
+
+ it('should use `Unknown response text` as the response', () => {
+ expect(Raven.captureMessage).toHaveBeenCalledWith(err, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: 'Unknown response text',
+ error: err,
+ event,
+ },
+ });
+ });
+ });
+ });
+
+ describe('shouldSendSample', () => {
+ let randomNumber;
+
+ beforeEach(() => {
+ RavenConfig.SAMPLE_RATE = 50;
+
+ spyOn(Math, 'random').and.callFake(() => randomNumber);
+ });
+
+ it('should call Math.random', () => {
+ RavenConfig.shouldSendSample();
+
+ expect(Math.random).toHaveBeenCalled();
+ });
+
+ it('should return true if the sample rate is greater than the random number * 100', () => {
+ randomNumber = 0.1;
+
+ expect(RavenConfig.shouldSendSample()).toBe(true);
+ });
+
+ it('should return false if the sample rate is less than the random number * 100', () => {
+ randomNumber = 0.9;
+
+ expect(RavenConfig.shouldSendSample()).toBe(false);
+ });
+
+ it('should return true if the sample rate is equal to the random number * 100', () => {
+ randomNumber = 0.5;
+
+ expect(RavenConfig.shouldSendSample()).toBe(true);
+ });
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index 8f38fb4a9a4..ecb54b509a2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3179,7 +3179,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"
@@ -4562,6 +4562,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"