diff --git a/.travis.yml b/.travis.yml index 6830f42aa67b..a678cd782dcb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,12 @@ sudo: false language: node_js node_js: - - "4" + - "8" +dist: trusty +addons: + chrome: stable +cache: + directories: + - node_modules script: - - npm test + - npm test diff --git a/Gruntfile.js b/Gruntfile.js index 027e44d923d1..9945fe2ff6ab 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,7 +1,6 @@ 'use strict'; module.exports = function(grunt) { - var _ = require('lodash'); var path = require('path'); var os = require('os'); var through = require('through2'); @@ -14,7 +13,7 @@ module.exports = function(grunt) { var plugins = grunt.option('plugins'); // Create plugin paths and verify they exist - plugins = _.map(plugins ? plugins.split(',') : [], function(plugin) { + plugins = (plugins ? plugins.split(',') : []).map(function(plugin) { var p = 'plugins/' + plugin + '.js'; if (!grunt.file.exists(p)) @@ -51,12 +50,12 @@ module.exports = function(grunt) { } }; - var excluded = _.map(excludedPlugins, function(plugin) { + var excluded = excludedPlugins.map(function(plugin) { return 'plugins/' + plugin + '.js'; }); // Remove the plugins that we don't want to build - a = _.filter(a, function(n) { + a = a.filter(function(n) { return excluded.indexOf(n) === -1; }); @@ -88,21 +87,17 @@ module.exports = function(grunt) { }); var pluginCombinations = combine(plugins); - var pluginConcatFiles = _.reduce( - pluginCombinations, - function(dict, comb) { - var key = _.map(comb, function(plugin) { - return path.basename(plugin, '.js'); - }); - key.sort(); + var pluginConcatFiles = pluginCombinations.reduce(function(dict, comb) { + var key = comb.map(function(plugin) { + return path.basename(plugin, '.js'); + }); + key.sort(); - var dest = path.join('build/', key.join(','), '/raven.js'); - dict[dest] = ['src/singleton.js'].concat(comb); + var dest = path.join('build/', key.join(','), '/raven.js'); + dict[dest] = ['src/singleton.js'].concat(comb); - return dict; - }, - {} - ); + return dict; + }, {}); var browserifyConfig = { options: { @@ -202,30 +197,6 @@ module.exports = function(grunt) { } }, - eslint: { - target: ['.'] - }, - - mocha: { - options: { - mocha: { - ignoreLeaks: true, - grep: grunt.option('grep') - }, - log: true, - reporter: 'Dot', - run: true - }, - unit: { - src: ['test/index.html'], - nonull: true - }, - integration: { - src: ['test/integration/index.html'], - nonull: true - } - }, - release: { options: { npm: false, @@ -338,12 +309,10 @@ module.exports = function(grunt) { // 3rd party Grunt tasks grunt.loadNpmTasks('grunt-browserify'); - grunt.loadNpmTasks('grunt-mocha'); grunt.loadNpmTasks('grunt-release'); grunt.loadNpmTasks('grunt-s3'); grunt.loadNpmTasks('grunt-gitinfo'); grunt.loadNpmTasks('grunt-sri'); - grunt.loadNpmTasks('grunt-eslint'); // Build tasks grunt.registerTask('_prep', ['clean', 'gitinfo', 'version']); @@ -355,7 +324,7 @@ module.exports = function(grunt) { '_prep', 'browserify:plugins-combined' ]); - grunt.registerTask('build.test', ['_prep', 'browserify:test']); + grunt.registerTask('build.test', ['_prep', 'browserify.core', 'browserify:test']); grunt.registerTask('build.core', ['browserify.core', 'uglify', 'sri:dist']); grunt.registerTask('build.plugins-combined', [ 'browserify.plugins-combined', @@ -366,13 +335,9 @@ module.exports = function(grunt) { grunt.registerTask('build', ['build.plugins-combined']); grunt.registerTask('dist', ['build.core', 'copy:dist']); - // Test task - grunt.registerTask('test', ['eslint', 'browserify.core', 'browserify:test', 'mocha']); - // Webserver tasks grunt.registerTask('run:test', ['connect:test']); grunt.registerTask('run:docs', ['connect:docs']); - grunt.registerTask('publish', ['test', 'build.plugins-combined', 's3']); - grunt.registerTask('default', ['test']); + grunt.registerTask('publish', ['build.plugins-combined', 's3']); }; diff --git a/package.json b/package.json index 73093fd3ab9c..8918ca22de03 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,11 @@ "scripts": { "lint": "eslint .", "precommit": "lint-staged", - "pretest": "npm install", - "test": "grunt test && npm run-script test-typescript", - "test-typescript": "node_modules/typescript/bin/tsc --noEmit --noImplicitAny typescript/raven-tests.ts" + "publish": "npm run test && grunt publish", + "test": "npm run lint && grunt build.test && npm run test:unit && npm run test:integration && npm run test:typescript", + "test:unit": "mocha-chrome test/index.html", + "test:integration": "mocha-chrome test/integration/index.html --chrome-flags '[\"--disable-web-security\"]' --ignore-resource-errors --ignore-exceptions", + "test:typescript": "tsc --noEmit --noImplicitAny typescript/raven-tests.ts" }, "devDependencies": { "bluebird": "^3.4.1", @@ -28,27 +30,23 @@ "chai": "2.3.0", "derequire": "2.0.3", "es6-promise": "^4.0.5", + "eslint": "^4.6.1", "eslint-config-prettier": "^2.3.0", "grunt": "^0.4.5", "grunt-browserify": "^4.0.1", "grunt-cli": "^0.1.13", "grunt-contrib-clean": "^0.7.0", - "grunt-contrib-concat": "^0.5.1", "grunt-contrib-connect": "^0.11.2", "grunt-contrib-copy": "^0.8.2", - "grunt-contrib-jshint": "^0.11.3", "grunt-contrib-uglify": "^0.11.0", - "grunt-eslint": "^20.0.0", "grunt-gitinfo": "^0.1.7", - "grunt-mocha": "1.0.4", "grunt-release": "^0.13.0", "grunt-s3": "0.2.0-alpha.3", "grunt-sri": "mattrobenolt/grunt-sri#pretty", "husky": "^0.14.3", - "jquery": "^2.1.4", "lint-staged": "^4.0.4", - "lodash": "^3.10.1", "mocha": "2.5.3", + "mocha-chrome": "^0.2.1", "prettier": "^1.6.1", "proxyquireify": "^3.0.2", "sinon": "1.7.3", diff --git a/src/raven.js b/src/raven.js index 78991f21e5cc..4d35c28ca4d2 100644 --- a/src/raven.js +++ b/src/raven.js @@ -1369,7 +1369,7 @@ Raven.prototype = { var frames = []; if (stackInfo.stack && stackInfo.stack.length) { each(stackInfo.stack, function(i, stack) { - var frame = self._normalizeFrame(stack); + var frame = self._normalizeFrame(stack, stackInfo.url); if (frame) { frames.push(frame); } @@ -1386,9 +1386,7 @@ Raven.prototype = { return frames; }, - _normalizeFrame: function(frame) { - if (!frame.url) return; - + _normalizeFrame: function(frame, stackInfoUrl) { // normalize the frames data var normalized = { filename: frame.url, @@ -1397,6 +1395,15 @@ Raven.prototype = { function: frame.func || '?' }; + // Case when we don't have any information about the error + // E.g. throwing a string or raw object, instead of an `Error` in Firefox + // Generating synthetic error doesn't add any value here + // + // We should probably somehow let a user know that they should fix their code + if (!frame.url) { + normalized.filename = stackInfoUrl; // fallback to whole stacks url from onerror handler + } + normalized.in_app = !// determine if an exception came from outside of our app // first we check the global includePaths list. ( diff --git a/test/index.html b/test/index.html index c273d684532b..7f8b0464afe6 100644 --- a/test/index.html +++ b/test/index.html @@ -33,29 +33,27 @@ diff --git a/test/integration/frame.html b/test/integration/frame.html index e3d8197f894c..672bd9b61e8e 100644 --- a/test/integration/frame.html +++ b/test/integration/frame.html @@ -37,6 +37,70 @@ clearTimeout(id); }; }()); + + /** + * Custom event factories for cross-browser compatibility + * + * Gecko browsers are using non-standard `initKeyEvent`, where others implemented `initKeyboardEvent`. + * To make it more consistent, we try to use standardized `MouseEvent`/`KeyboardEvent` now + * and fallback to older implementations for legacy browsers only. + * + * See deprecation notes: + * https://developer.mozilla.org/en-US/docs/Web/API/Document/createEvent + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyEvent + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyboardEvent + * + * References: + * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent#Specifications + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Specifications + */ + function createMouseEvent (options) { + var options = { + bubbles: true, + cancelable: true, + view: window + } + + if ('MouseEvent' in window) { + return new MouseEvent('click', options); + } else { + var event = document.createEvent('MouseEvent'); + event.initMouseEvent( + 'click', + options.bubbles, + options.cancelable, + options.view + ); + + return event; + } + } + + function createKeyboardEvent (key) { + var options = { + bubbles: true, + cancelable: true, + view: window, + key: key || 'a' + } + options.charCode = options.key.charCodeAt(); + + if ('KeyboardEvent' in window) { + return new KeyboardEvent('keypress', options); + } else { + var event = document.createEvent('KeyboardEvent'); + event.initKeyboardEvent( + 'keypress', + options.bubbles, + options.cancelable, + options.view, + options.key, + options.charCode + ); + + return event; + } + } diff --git a/test/integration/index.html b/test/integration/index.html index 4e54424d5c08..679ed89f907f 100644 --- a/test/integration/index.html +++ b/test/integration/index.html @@ -39,28 +39,26 @@ diff --git a/test/integration/test.js b/test/integration/test.js index bff0cc818a4b..877ef222d214 100644 --- a/test/integration/test.js +++ b/test/integration/test.js @@ -97,7 +97,7 @@ describe('integration', function() { }, function() { var ravenData = iframe.contentWindow.ravenData[0]; - assert.isAbove(ravenData.stacktrace.frames.length, 1); + assert.isAbove(ravenData.stacktrace.frames.length, 0); // verify trimHeadFrames hasn't slipped into final payload assert.isUndefined(ravenData.trimHeadFrames); @@ -189,8 +189,6 @@ describe('integration', function() { // same error message, but different stacks means that these are considered // different errors - // NOTE: PhantomJS can't derive function/lineno/colno from evaled frames, must - // use frames declared in frame.html (foo(), bar()) // stack: // bar @@ -458,14 +456,8 @@ describe('integration', function() { false ); - var evt; - if (document.createEvent) { - evt = document.createEvent('MouseEvents'); - evt.initEvent('click', true, false); - div.dispatchEvent(evt); - } else if (document.createEventObject) { - div.fireEvent('onclick'); - } + var click = createMouseEvent(); + div.dispatchEvent(click); }, function() { var ravenData = iframe.contentWindow.ravenData[0]; @@ -828,24 +820,7 @@ describe('integration', function() { input.addEventListener('click', clickHandler); // click - var evt = document.createEvent('MouseEvent'); - evt.initMouseEvent( - 'click', - true /* bubble */, - true /* cancelable */, - window, - null, - 0, - 0, - 0, - 0 /* coordinates */, - false, - false, - false, - false /* modifier keys */, - 0 /*left*/, - null - ); + var evt = createMouseEvent(); input.dispatchEvent(evt); }, function() { @@ -878,24 +853,7 @@ describe('integration', function() { Raven._breadcrumbs = []; // click - var evt = document.createEvent('MouseEvent'); - evt.initMouseEvent( - 'click', - true /* bubble */, - true /* cancelable */, - window, - null, - 0, - 0, - 0, - 0 /* coordinates */, - false, - false, - false, - false /* modifier keys */, - 0 /*left*/, - null - ); + var evt = createMouseEvent(); var input = document.getElementsByTagName('input')[0]; input.dispatchEvent(evt); @@ -941,24 +899,7 @@ describe('integration', function() { document.querySelector('.c').addEventListener('click', clickHandler); // click - var evt = document.createEvent('MouseEvent'); - evt.initMouseEvent( - 'click', - true /* bubble */, - true /* cancelable */, - window, - null, - 0, - 0, - 0, - 0 /* coordinates */, - false, - false, - false, - false /* modifier keys */, - 0 /*left*/, - null - ); + var evt = createMouseEvent(); var input = document.querySelector('.a'); // leaf node input.dispatchEvent(evt); @@ -975,64 +916,43 @@ describe('integration', function() { ); }); - // doesn't work in PhantomJS - if (!/PhantomJS/.test(window.navigator.userAgent)) { - it('should bail out if accessing the `type` and `target` properties of an event throw an exception', function( - done - ) { - // see: https://github.com/getsentry/raven-js/issues/768 - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(done); + it('should bail out if accessing the `type` and `target` properties of an event throw an exception', function( + done + ) { + // see: https://github.com/getsentry/raven-js/issues/768 + var iframe = this.iframe; - // some browsers trigger onpopstate for load / reset breadcrumb state - Raven._breadcrumbs = []; - - // click - var evt = document.createEvent('MouseEvent'); - evt.initMouseEvent( - evt, - 'click', - true /* bubble */, - true /* cancelable */, - window, - null, - 0, - 0, - 0, - 0 /* coordinates */, - false, - false, - false, - false /* modifier keys */, - 0 /*left*/, - null - ); - - function kaboom() { - throw new Error('lol'); - } - Object.defineProperty(evt, 'type', {get: kaboom}); - Object.defineProperty(evt, 'target', {get: kaboom}); + iframeExecute( + iframe, + done, + function() { + setTimeout(done); - var input = document.querySelector('.a'); // leaf node - input.dispatchEvent(evt); - }, - function() { - var Raven = iframe.contentWindow.Raven, - breadcrumbs = Raven._breadcrumbs; + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].category, 'ui.click'); - assert.equal(breadcrumbs[0].message, ''); + // click + var evt = createMouseEvent(); + + function kaboom() { + throw new Error('lol'); } - ); - }); - } // if PhantomJS + Object.defineProperty(evt, 'type', {get: kaboom}); + Object.defineProperty(evt, 'target', {get: kaboom}); + + var input = document.querySelector('.a'); // leaf node + input.dispatchEvent(evt); + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs; + + assert.equal(breadcrumbs.length, 1); + assert.equal(breadcrumbs[0].category, 'ui.click'); + assert.equal(breadcrumbs[0].message, ''); + } + ); + }); it('should record consecutive keypress events into a single "input" breadcrumb', function( done @@ -1049,31 +969,8 @@ describe('integration', function() { Raven._breadcrumbs = []; // keypress twice - var keypress1 = document.createEvent('KeyboardEvent'); - keypress1.initKeyboardEvent( - 'keypress', - true, - true, - window, - 'b', - 66, - 0, - '', - false - ); - - var keypress2 = document.createEvent('KeyboardEvent'); - keypress2.initKeyboardEvent( - 'keypress', - true, - true, - window, - 'a', - 65, - 0, - '', - false - ); + var keypress1 = createKeyboardEvent('a'); + var keypress2 = createKeyboardEvent('b'); var input = document.getElementsByTagName('input')[0]; input.dispatchEvent(keypress1); @@ -1107,18 +1004,7 @@ describe('integration', function() { Raven._breadcrumbs = []; // keypress - var keypress = document.createEvent('KeyboardEvent'); - keypress.initKeyboardEvent( - 'keypress', - true, - true, - window, - 'b', - 66, - 0, - '', - false - ); + var keypress = createKeyboardEvent(); var input = document.getElementsByTagName('input')[0]; input.dispatchEvent(keypress); @@ -1156,52 +1042,11 @@ describe('integration', function() { Raven._breadcrumbs = []; // 1st keypress - var keypress1 = document.createEvent('KeyboardEvent'); - keypress1.initKeyboardEvent( - 'keypress', - true, - true, - window, - 'b', - 66, - 0, - '', - false - ); - + var keypress1 = createKeyboardEvent('a'); // click - var click = document.createEvent('MouseEvent'); - click.initMouseEvent( - 'click', - true /* bubble */, - true /* cancelable */, - window, - null, - 0, - 0, - 0, - 0 /* coordinates */, - false, - false, - false, - false /* modifier keys */, - 0 /*left*/, - null - ); - + var click = createMouseEvent(); // 2nd keypress - var keypress2 = document.createEvent('KeyboardEvent'); - keypress2.initKeyboardEvent( - 'keypress', - true, - true, - window, - 'a', - 65, - 0, - '', - false - ); + var keypress2 = createKeyboardEvent('b'); var input = document.getElementsByTagName('input')[0]; input.dispatchEvent(keypress1); @@ -1251,31 +1096,8 @@ describe('integration', function() { Raven._breadcrumbs = []; // keypress twice - var keypress1 = document.createEvent('KeyboardEvent'); - keypress1.initKeyboardEvent( - 'keypress', - true, - true, - window, - 'b', - 66, - 0, - '', - false - ); - - var keypress2 = document.createEvent('KeyboardEvent'); - keypress2.initKeyboardEvent( - 'keypress', - true, - true, - window, - 'a', - 65, - 0, - '', - false - ); + var keypress1 = createKeyboardEvent('a'); + var keypress2 = createKeyboardEvent('b'); var div = document.querySelector('[contenteditable]'); div.dispatchEvent(keypress1); @@ -1332,7 +1154,6 @@ describe('integration', function() { assert.equal(breadcrumbs[2].category, 'navigation'); // bar?a=1#fragment => [object%20Object] assert.equal(breadcrumbs[3].category, 'navigation'); // [object%20Object] => bar?a=1#fragment (back button) - // assert end of string because PhantomJS uses full system path assert.ok( /\/test\/integration\/frame\.html$/.test(Raven._breadcrumbs[0].data.from), "'from' url is incorrect"