Added raven and raven-vue plugin, updated gon_helper with data needed for raven and created raven_config, required by application.js

Added is_production to define sentry environment

Removed as much jQuery as possible

Added public_sentry_dsn application_settings helper method

Use URI module instead of regex for public dsn

Removed raven-vue and load raven on if sentry is enabled

Add load_script spec

added raven_config spec

added class_spec_helper and tests

added sentry_helper spec

added feature spec
This commit is contained in:
Luke Bennett 2016-10-09 23:40:58 +01:00 committed by Luke "Jared" Bennett
parent 8a6e415268
commit c252c03401
No known key found for this signature in database
GPG Key ID: 402ED51FB5D306C2
14 changed files with 2977 additions and 0 deletions

View File

@ -62,6 +62,7 @@
/*= require_directory . */
/*= require fuzzaldrin-plus */
/*= require es6-promise.auto */
/*= require raven_config */
(function () {
document.addEventListener('page:fetch', function () {

View File

@ -0,0 +1,26 @@
(() => {
const global = window.gl || (window.gl = {});
class LoadScript {
static load(source, id = '') {
if (!source) return Promise.reject('source url must be defined');
if (id && document.querySelector(`#${id}`)) return Promise.reject('script id already exists');
return new Promise((resolve, reject) => this.appendScript(source, id, resolve, reject));
}
static appendScript(source, id, resolve, reject) {
const scriptElement = document.createElement('script');
scriptElement.type = 'text/javascript';
if (id) scriptElement.id = id;
scriptElement.onload = resolve;
scriptElement.onerror = reject;
scriptElement.src = source;
document.body.appendChild(scriptElement);
}
}
global.LoadScript = LoadScript;
return global.LoadScript;
})();

View File

@ -0,0 +1,66 @@
/* global Raven */
/*= require lib/utils/load_script */
(() => {
const global = window.gl || (window.gl = {});
class RavenConfig {
static init(options = {}) {
this.options = options;
if (!this.options.sentryDsn || !this.options.ravenAssetUrl) return Promise.reject('sentry dsn and raven asset url is required');
return global.LoadScript.load(this.options.ravenAssetUrl, 'raven-js')
.then(() => {
this.configure();
this.bindRavenErrors();
if (this.options.currentUserId) this.setUser();
});
}
static configure() {
Raven.config(this.options.sentryDsn, {
whitelistUrls: this.options.whitelistUrls,
environment: this.options.isProduction ? 'production' : 'development',
}).install();
}
static setUser() {
Raven.setUserContext({
id: this.options.currentUserId,
});
}
static bindRavenErrors() {
$(document).on('ajaxError.raven', this.handleRavenErrors);
}
static 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,
},
});
}
}
global.RavenConfig = RavenConfig;
document.addEventListener('DOMContentLoaded', () => {
if (!window.gon) return;
global.RavenConfig.init({
sentryDsn: gon.sentry_dsn,
ravenAssetUrl: gon.raven_asset_url,
currentUserId: gon.current_user_id,
whitelistUrls: [gon.gitlab_url],
isProduction: gon.is_production,
}).catch($.noop);
});
})();

View File

@ -6,4 +6,12 @@ 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

View File

@ -92,6 +92,7 @@ module Gitlab
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "raven.js"
config.assets.precompile << "graphs/graphs_bundle.js"
config.assets.precompile << "users/users_bundle.js"
config.assets.precompile << "network/network_bundle.js"

View File

@ -1,3 +1,5 @@
include SentryHelper
module Gitlab
module GonHelper
def add_gon_variables
@ -10,6 +12,10 @@ module Gitlab
gon.award_menu_url = emojis_path
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.raven_asset_url = ActionController::Base.helpers.asset_path('raven.js') 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

View File

@ -0,0 +1,24 @@
require 'spec_helper'
feature 'RavenJS', feature: true, js: true do
let(:raven_path) { '/raven.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(ApplicationController).to receive_messages(sentry_dsn_public: 'https://mock:sentry@dsn/path',
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

View File

@ -0,0 +1,15 @@
require 'spec_helper'
describe SentryHelper do
describe '#sentry_dsn_public' do
it 'returns nil if no sentry_dsn is set' do
allow(ApplicationSetting.current).to receive(:sentry_dsn).and_return(nil)
expect(helper.sentry_dsn_public).to eq(nil)
end
it 'returns the uri string with no password if sentry_dsn is set' do
allow(ApplicationSetting.current).to receive(:sentry_dsn).and_return('https://test:dsn@host/path')
expect(helper.sentry_dsn_public).to eq('https://test@host/path')
end
end
end

View File

@ -0,0 +1,10 @@
/* eslint-disable no-unused-vars */
class ClassSpecHelper {
static itShouldBeAStaticMethod(base, method) {
return it('should be a static method', () => {
expect(base[method]).toBeDefined();
expect(base.prototype[method]).toBeUndefined();
});
}
}

View File

@ -0,0 +1,35 @@
/* global ClassSpecHelper */
//= require ./class_spec_helper
describe('ClassSpecHelper', () => {
describe('.itShouldBeAStaticMethod', function () {
beforeEach(() => {
class TestClass {
instanceMethod() { this.prop = 'val'; }
static staticMethod() {}
}
this.TestClass = TestClass;
});
ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod');
it('should have a defined spec', () => {
expect(ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod').description).toBe('should be a static method');
});
it('should pass for a static method', () => {
const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod');
expect(spec.status()).toBe('passed');
});
it('should fail for an instance method', (done) => {
const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'instanceMethod');
spec.resultCallback = (result) => {
expect(result.status).toBe('failed');
done();
};
spec.execute();
});
});
});

View File

@ -0,0 +1,95 @@
/* global ClassSpecHelper */
/*= require lib/utils/load_script */
/*= require class_spec_helper */
describe('LoadScript', () => {
const global = window.gl || (window.gl = {});
const LoadScript = global.LoadScript;
it('should be defined in the global scope', () => {
expect(LoadScript).toBeDefined();
});
describe('.load', () => {
ClassSpecHelper.itShouldBeAStaticMethod(LoadScript, 'load');
it('should reject if no source argument is provided', () => {
spyOn(Promise, 'reject');
LoadScript.load();
expect(Promise.reject).toHaveBeenCalledWith('source url must be defined');
});
it('should reject if the script id already exists', () => {
spyOn(Promise, 'reject');
spyOn(document, 'querySelector').and.returnValue({});
LoadScript.load('src.js', 'src-id');
expect(Promise.reject).toHaveBeenCalledWith('script id already exists');
});
it('should return a promise on completion', () => {
expect(LoadScript.load('src.js')).toEqual(jasmine.any(Promise));
});
it('should call appendScript when the promise is constructed', () => {
spyOn(LoadScript, 'appendScript');
LoadScript.load('src.js', 'src-id');
expect(LoadScript.appendScript).toHaveBeenCalledWith('src.js', 'src-id', jasmine.any(Promise.resolve.constructor), jasmine.any(Promise.reject.constructor));
});
});
describe('.appendScript', () => {
beforeEach(() => {
spyOn(document.body, 'appendChild');
});
ClassSpecHelper.itShouldBeAStaticMethod(LoadScript, 'appendScript');
describe('when called', () => {
let mockScriptTag;
beforeEach(() => {
mockScriptTag = {};
spyOn(document, 'createElement').and.returnValue(mockScriptTag);
LoadScript.appendScript('src.js', 'src-id', () => {}, () => {});
});
it('should create a script tag', () => {
expect(document.createElement).toHaveBeenCalledWith('script');
});
it('should set the MIME type', () => {
expect(mockScriptTag.type).toBe('text/javascript');
});
it('should set the script id', () => {
expect(mockScriptTag.id).toBe('src-id');
});
it('should set an onload handler', () => {
expect(mockScriptTag.onload).toEqual(jasmine.any(Function));
});
it('should set an onerror handler', () => {
expect(mockScriptTag.onerror).toEqual(jasmine.any(Function));
});
it('should set the src attribute', () => {
expect(mockScriptTag.src).toBe('src.js');
});
it('should append the script tag to the body element', () => {
expect(document.body.appendChild).toHaveBeenCalledWith(mockScriptTag);
});
});
it('should not set the script id if no id is provided', () => {
const mockScriptTag = {};
spyOn(document, 'createElement').and.returnValue(mockScriptTag);
LoadScript.appendScript('src.js', undefined);
expect(mockScriptTag.id).toBeUndefined();
});
});
});

View File

@ -0,0 +1,142 @@
/* global ClassSpecHelper */
/*= require raven */
/*= require lib/utils/load_script */
/*= require raven_config */
/*= require class_spec_helper */
describe('RavenConfig', () => {
const global = window.gl || (window.gl = {});
const RavenConfig = global.RavenConfig;
it('should be defined in the global scope', () => {
expect(RavenConfig).toBeDefined();
});
describe('.init', () => {
beforeEach(() => {
spyOn(global.LoadScript, 'load').and.callThrough();
spyOn(document, 'querySelector').and.returnValue(undefined);
spyOn(RavenConfig, 'configure');
spyOn(RavenConfig, 'bindRavenErrors');
spyOn(RavenConfig, 'setUser');
spyOn(Promise, 'reject');
});
ClassSpecHelper.itShouldBeAStaticMethod(RavenConfig, 'init');
describe('when called', () => {
let options;
let initPromise;
beforeEach(() => {
options = {
sentryDsn: '//sentryDsn',
ravenAssetUrl: '//ravenAssetUrl',
currentUserId: 1,
whitelistUrls: ['//gitlabUrl'],
isProduction: true,
};
initPromise = RavenConfig.init(options);
});
it('should set the options property', () => {
expect(RavenConfig.options).toEqual(options);
});
it('should load a #raven-js script with the raven asset URL', () => {
expect(global.LoadScript.load).toHaveBeenCalledWith(options.ravenAssetUrl, 'raven-js');
});
it('should return a promise', () => {
expect(initPromise).toEqual(jasmine.any(Promise));
});
it('should call the configure method', () => {
initPromise.then(() => {
expect(RavenConfig.configure).toHaveBeenCalled();
});
});
it('should call the error bindings method', () => {
initPromise.then(() => {
expect(RavenConfig.bindRavenErrors).toHaveBeenCalled();
});
});
it('should call setUser', () => {
initPromise.then(() => {
expect(RavenConfig.setUser).toHaveBeenCalled();
});
});
});
it('should not call setUser if there is no current user ID', () => {
RavenConfig.init({
sentryDsn: '//sentryDsn',
ravenAssetUrl: '//ravenAssetUrl',
currentUserId: undefined,
whitelistUrls: ['//gitlabUrl'],
isProduction: true,
});
expect(RavenConfig.setUser).not.toHaveBeenCalled();
});
it('should reject if there is no Sentry DSN', () => {
RavenConfig.init({
sentryDsn: undefined,
ravenAssetUrl: '//ravenAssetUrl',
currentUserId: 1,
whitelistUrls: ['//gitlabUrl'],
isProduction: true,
});
expect(Promise.reject).toHaveBeenCalledWith('sentry dsn and raven asset url is required');
});
it('should reject if there is no Raven asset URL', () => {
RavenConfig.init({
sentryDsn: '//sentryDsn',
ravenAssetUrl: undefined,
currentUserId: 1,
whitelistUrls: ['//gitlabUrl'],
isProduction: true,
});
expect(Promise.reject).toHaveBeenCalledWith('sentry dsn and raven asset url is required');
});
});
describe('.configure', () => {
ClassSpecHelper.itShouldBeAStaticMethod(RavenConfig, 'configure');
describe('when called', () => {
beforeEach(() => {});
});
});
describe('.setUser', () => {
ClassSpecHelper.itShouldBeAStaticMethod(RavenConfig, 'setUser');
describe('when called', () => {
beforeEach(() => {});
});
});
describe('.bindRavenErrors', () => {
ClassSpecHelper.itShouldBeAStaticMethod(RavenConfig, 'bindRavenErrors');
describe('when called', () => {
beforeEach(() => {});
});
});
describe('.handleRavenErrors', () => {
ClassSpecHelper.itShouldBeAStaticMethod(RavenConfig, 'handleRavenErrors');
describe('when called', () => {
beforeEach(() => {});
});
});
});

View File

@ -11,6 +11,7 @@
/*= require jquery.turbolinks */
/*= require bootstrap */
/*= require underscore */
/*= require es6-promise.auto */
// Teaspoon includes some support files, but you can use anything from your own
// support path too.

2547
vendor/assets/javascripts/raven.js vendored Normal file

File diff suppressed because it is too large Load Diff