From aa636a8a6da2a7a0dbf692dbb8731732fd297142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 3 Aug 2018 14:46:38 +0200 Subject: [PATCH 01/14] browser: Ported and updated integration tests setup (no tests fixed yet) --- .../browser/karma/karma.integration.config.js | 51 + .../karma.unit.config.js} | 10 +- packages/browser/package.json | 8 +- packages/browser/rollup.config.js | 26 + packages/browser/test/integration/123 | 1 + .../browser/test/integration/example.json | 3 + packages/browser/test/integration/frame.html | 148 ++ .../polyfills/es6-promise-4.2.4.js | 1181 +++++++++++++ .../polyfills/whatwg-fetch-2.0.4.js | 466 ++++++ packages/browser/test/integration/test.js | 1463 +++++++++++++++++ .../browser/test/integration/throw-error.js | 5 + .../browser/test/integration/throw-object.js | 7 + .../browser/test/integration/throw-string.js | 5 + 13 files changed, 3365 insertions(+), 9 deletions(-) create mode 100644 packages/browser/karma/karma.integration.config.js rename packages/browser/{karma.config.js => karma/karma.unit.config.js} (95%) create mode 100644 packages/browser/test/integration/123 create mode 100644 packages/browser/test/integration/example.json create mode 100644 packages/browser/test/integration/frame.html create mode 100644 packages/browser/test/integration/polyfills/es6-promise-4.2.4.js create mode 100644 packages/browser/test/integration/polyfills/whatwg-fetch-2.0.4.js create mode 100644 packages/browser/test/integration/test.js create mode 100644 packages/browser/test/integration/throw-error.js create mode 100644 packages/browser/test/integration/throw-object.js create mode 100644 packages/browser/test/integration/throw-string.js diff --git a/packages/browser/karma/karma.integration.config.js b/packages/browser/karma/karma.integration.config.js new file mode 100644 index 000000000000..a15d2249911a --- /dev/null +++ b/packages/browser/karma/karma.integration.config.js @@ -0,0 +1,51 @@ +module.exports = config => { + config.set({ + colors: true, + singleRun: true, + autoWatch: false, + basePath: process.cwd(), + files: [ + { pattern: 'test/integration/polyfills/es6-promise-4.2.4.js', included: false }, + { pattern: 'test/integration/polyfills/whatwg-fetch-2.0.4.js', included: false }, + { pattern: 'test/integration/123', included: false }, + { pattern: 'test/integration/throw-string.js', included: false }, + { pattern: 'test/integration/throw-error.js', included: false }, + { pattern: 'test/integration/throw-object.js', included: false }, + { pattern: 'test/integration/example.json', included: false }, + { pattern: 'test/integration/frame.html', included: false }, + { pattern: 'build/bundle.js', included: false }, + 'test/integration/test.js', + ], + frameworks: ['mocha', 'chai', 'sinon'], + plugins: [ + 'karma-mocha', + 'karma-mocha-reporter', + 'karma-chai', + 'karma-sinon', + 'karma-chrome-launcher', + 'karma-firefox-launcher', + 'karma-failed-reporter', + ], + reporters: ['mocha'], + browsers: ['ChromeHeadlessNoSandbox', 'FirefoxHeadless'], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox', '--disable-setuid-sandbox'], + }, + FirefoxHeadless: { + base: 'Firefox', + flags: ['-headless'], + }, + }, + // https://docs.travis-ci.com/user/gui-and-headless-browsers/#Karma-and-Firefox-inactivity-timeouts + browserNoActivityTimeout: 30000, + concurrency: 2, + client: { + mocha: { + reporter: 'html', + ui: 'bdd', + }, + }, + }); +}; diff --git a/packages/browser/karma.config.js b/packages/browser/karma/karma.unit.config.js similarity index 95% rename from packages/browser/karma.config.js rename to packages/browser/karma/karma.unit.config.js index edfedeb300f8..19544a20546e 100644 --- a/packages/browser/karma.config.js +++ b/packages/browser/karma/karma.unit.config.js @@ -1,19 +1,16 @@ -module.exports = function(config) { +module.exports = config => { config.set({ colors: true, singleRun: true, autoWatch: false, - + basePath: process.cwd(), + files: ['test/**/*.ts', 'src/**/*.+(js|ts)'], frameworks: ['mocha', 'chai', 'sinon', 'karma-typescript'], browsers: ['ChromeHeadless'], reporters: ['mocha', 'karma-typescript'], - - basePath: process.cwd(), - files: ['test/**/*.ts', 'src/**/*.+(js|ts)'], preprocessors: { '**/*.+(js|ts)': ['karma-typescript'], }, - karmaTypescriptConfig: { tsconfig: 'tsconfig.json', compilerOptions: { @@ -30,7 +27,6 @@ module.exports = function(config) { 'text-summary': '', }, }, - // Uncomment if you want to silence console logs in the output // client: { // captureConsole: false, diff --git a/packages/browser/package.json b/packages/browser/package.json index d556b36c4104..2e67799467ef 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -28,6 +28,8 @@ "karma": "^2.0.2", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^2.2.0", + "karma-failed-reporter": "0.0.3", + "karma-firefox-launcher": "^1.1.0", "karma-mocha": "^1.3.0", "karma-mocha-reporter": "^2.2.5", "karma-rollup-preprocessor": "^6.0.0", @@ -58,8 +60,10 @@ "fix": "run-s fix:tslint fix:prettier", "fix:prettier": "prettier --write '{src,test}/**/*.ts'", "fix:tslint": "tslint --fix -t stylish -p .", - "test": "karma start karma.config.js", - "test:watch": "karma start karma.config.js --auto-watch --no-single-run", + "test": "karma start karma/karma.unit.config.js", + "test:watch": "karma start karma/karma.unit.config.js --auto-watch --no-single-run", + "test:integration": "karma start karma/karma.integration.config.js", + "test:integration-sauce": "karma start karma/karma.integration-sauce.config.js", "size:check": "cat build/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print $1,\"kB\";}'", "version": "node ../../scripts/versionbump.js src/version.ts" }, diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js index f24f5ee30bca..b51b637ea48a 100644 --- a/packages/browser/rollup.config.js +++ b/packages/browser/rollup.config.js @@ -30,6 +30,32 @@ export default [ commonjs(), ], }, + { + input: 'src/index.ts', + output: { + file: 'build/bundle.js', + format: 'iife', + name: 'Sentry', + sourcemap: true, + }, + context: 'window', + plugins: [ + typescript({ + tsconfig: 'tsconfig.build.json', + tsconfigOverride: { compilerOptions: { declaration: false } }, + }), + resolve({ + jsnext: true, + main: true, + browser: true, + }), + commonjs(), + license({ + sourcemap: true, + banner: `/*! @sentry/browser <%= pkg.version %> (${commitHash}) | https://github.com/getsentry/raven-js */`, + }), + ], + }, { input: 'src/index.ts', output: { diff --git a/packages/browser/test/integration/123 b/packages/browser/test/integration/123 new file mode 100644 index 000000000000..190a18037c64 --- /dev/null +++ b/packages/browser/test/integration/123 @@ -0,0 +1 @@ +123 diff --git a/packages/browser/test/integration/example.json b/packages/browser/test/integration/example.json new file mode 100644 index 000000000000..c8c4105eb57c --- /dev/null +++ b/packages/browser/test/integration/example.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} diff --git a/packages/browser/test/integration/frame.html b/packages/browser/test/integration/frame.html new file mode 100644 index 000000000000..a06d0a7cab89 --- /dev/null +++ b/packages/browser/test/integration/frame.html @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+
+
+
+ + + diff --git a/packages/browser/test/integration/polyfills/es6-promise-4.2.4.js b/packages/browser/test/integration/polyfills/es6-promise-4.2.4.js new file mode 100644 index 000000000000..30d01304f0a4 --- /dev/null +++ b/packages/browser/test/integration/polyfills/es6-promise-4.2.4.js @@ -0,0 +1,1181 @@ +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE + * @version v4.2.4+314e4831 + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.ES6Promise = factory()); +}(this, (function () { 'use strict'; + +function objectOrFunction(x) { + var type = typeof x; + return x !== null && (type === 'object' || type === 'function'); +} + +function isFunction(x) { + return typeof x === 'function'; +} + + + +var _isArray = void 0; +if (Array.isArray) { + _isArray = Array.isArray; +} else { + _isArray = function (x) { + return Object.prototype.toString.call(x) === '[object Array]'; + }; +} + +var isArray = _isArray; + +var len = 0; +var vertxNext = void 0; +var customSchedulerFn = void 0; + +var asap = function asap(callback, arg) { + queue[len] = callback; + queue[len + 1] = arg; + len += 2; + if (len === 2) { + // If len is 2, that means that we need to schedule an async flush. + // If additional callbacks are queued before the queue is flushed, they + // will be processed by this flush that we are scheduling. + if (customSchedulerFn) { + customSchedulerFn(flush); + } else { + scheduleFlush(); + } + } +}; + +function setScheduler(scheduleFn) { + customSchedulerFn = scheduleFn; +} + +function setAsap(asapFn) { + asap = asapFn; +} + +var browserWindow = typeof window !== 'undefined' ? window : undefined; +var browserGlobal = browserWindow || {}; +var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; +var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && {}.toString.call(process) === '[object process]'; + +// test for web worker but not in IE10 +var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined'; + +// node +function useNextTick() { + // node version 0.10.x displays a deprecation warning when nextTick is used recursively + // see https://github.com/cujojs/when/issues/410 for details + return function () { + return process.nextTick(flush); + }; +} + +// vertx +function useVertxTimer() { + if (typeof vertxNext !== 'undefined') { + return function () { + vertxNext(flush); + }; + } + + return useSetTimeout(); +} + +function useMutationObserver() { + var iterations = 0; + var observer = new BrowserMutationObserver(flush); + var node = document.createTextNode(''); + observer.observe(node, { characterData: true }); + + return function () { + node.data = iterations = ++iterations % 2; + }; +} + +// web worker +function useMessageChannel() { + var channel = new MessageChannel(); + channel.port1.onmessage = flush; + return function () { + return channel.port2.postMessage(0); + }; +} + +function useSetTimeout() { + // Store setTimeout reference so es6-promise will be unaffected by + // other code modifying setTimeout (like sinon.useFakeTimers()) + var globalSetTimeout = setTimeout; + return function () { + return globalSetTimeout(flush, 1); + }; +} + +var queue = new Array(1000); +function flush() { + for (var i = 0; i < len; i += 2) { + var callback = queue[i]; + var arg = queue[i + 1]; + + callback(arg); + + queue[i] = undefined; + queue[i + 1] = undefined; + } + + len = 0; +} + +function attemptVertx() { + try { + var vertx = Function('return this')().require('vertx'); + vertxNext = vertx.runOnLoop || vertx.runOnContext; + return useVertxTimer(); + } catch (e) { + return useSetTimeout(); + } +} + +var scheduleFlush = void 0; +// Decide what async method to use to triggering processing of queued callbacks: +if (isNode) { + scheduleFlush = useNextTick(); +} else if (BrowserMutationObserver) { + scheduleFlush = useMutationObserver(); +} else if (isWorker) { + scheduleFlush = useMessageChannel(); +} else if (browserWindow === undefined && typeof require === 'function') { + scheduleFlush = attemptVertx(); +} else { + scheduleFlush = useSetTimeout(); +} + +function then(onFulfillment, onRejection) { + var parent = this; + + var child = new this.constructor(noop); + + if (child[PROMISE_ID] === undefined) { + makePromise(child); + } + + var _state = parent._state; + + + if (_state) { + var callback = arguments[_state - 1]; + asap(function () { + return invokeCallback(_state, child, callback, parent._result); + }); + } else { + subscribe(parent, child, onFulfillment, onRejection); + } + + return child; +} + +/** + `Promise.resolve` returns a promise that will become resolved with the + passed `value`. It is shorthand for the following: + + ```javascript + let promise = new Promise(function(resolve, reject){ + resolve(1); + }); + + promise.then(function(value){ + // value === 1 + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + let promise = Promise.resolve(1); + + promise.then(function(value){ + // value === 1 + }); + ``` + + @method resolve + @static + @param {Any} value value that the returned promise will be resolved with + Useful for tooling. + @return {Promise} a promise that will become fulfilled with the given + `value` +*/ +function resolve$1(object) { + /*jshint validthis:true */ + var Constructor = this; + + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } + + var promise = new Constructor(noop); + resolve(promise, object); + return promise; +} + +var PROMISE_ID = Math.random().toString(36).substring(2); + +function noop() {} + +var PENDING = void 0; +var FULFILLED = 1; +var REJECTED = 2; + +var TRY_CATCH_ERROR = { error: null }; + +function selfFulfillment() { + return new TypeError("You cannot resolve a promise with itself"); +} + +function cannotReturnOwn() { + return new TypeError('A promises callback cannot return that same promise.'); +} + +function getThen(promise) { + try { + return promise.then; + } catch (error) { + TRY_CATCH_ERROR.error = error; + return TRY_CATCH_ERROR; + } +} + +function tryThen(then$$1, value, fulfillmentHandler, rejectionHandler) { + try { + then$$1.call(value, fulfillmentHandler, rejectionHandler); + } catch (e) { + return e; + } +} + +function handleForeignThenable(promise, thenable, then$$1) { + asap(function (promise) { + var sealed = false; + var error = tryThen(then$$1, thenable, function (value) { + if (sealed) { + return; + } + sealed = true; + if (thenable !== value) { + resolve(promise, value); + } else { + fulfill(promise, value); + } + }, function (reason) { + if (sealed) { + return; + } + sealed = true; + + reject(promise, reason); + }, 'Settle: ' + (promise._label || ' unknown promise')); + + if (!sealed && error) { + sealed = true; + reject(promise, error); + } + }, promise); +} + +function handleOwnThenable(promise, thenable) { + if (thenable._state === FULFILLED) { + fulfill(promise, thenable._result); + } else if (thenable._state === REJECTED) { + reject(promise, thenable._result); + } else { + subscribe(thenable, undefined, function (value) { + return resolve(promise, value); + }, function (reason) { + return reject(promise, reason); + }); + } +} + +function handleMaybeThenable(promise, maybeThenable, then$$1) { + if (maybeThenable.constructor === promise.constructor && then$$1 === then && maybeThenable.constructor.resolve === resolve$1) { + handleOwnThenable(promise, maybeThenable); + } else { + if (then$$1 === TRY_CATCH_ERROR) { + reject(promise, TRY_CATCH_ERROR.error); + TRY_CATCH_ERROR.error = null; + } else if (then$$1 === undefined) { + fulfill(promise, maybeThenable); + } else if (isFunction(then$$1)) { + handleForeignThenable(promise, maybeThenable, then$$1); + } else { + fulfill(promise, maybeThenable); + } + } +} + +function resolve(promise, value) { + if (promise === value) { + reject(promise, selfFulfillment()); + } else if (objectOrFunction(value)) { + handleMaybeThenable(promise, value, getThen(value)); + } else { + fulfill(promise, value); + } +} + +function publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._result); + } + + publish(promise); +} + +function fulfill(promise, value) { + if (promise._state !== PENDING) { + return; + } + + promise._result = value; + promise._state = FULFILLED; + + if (promise._subscribers.length !== 0) { + asap(publish, promise); + } +} + +function reject(promise, reason) { + if (promise._state !== PENDING) { + return; + } + promise._state = REJECTED; + promise._result = reason; + + asap(publishRejection, promise); +} + +function subscribe(parent, child, onFulfillment, onRejection) { + var _subscribers = parent._subscribers; + var length = _subscribers.length; + + + parent._onerror = null; + + _subscribers[length] = child; + _subscribers[length + FULFILLED] = onFulfillment; + _subscribers[length + REJECTED] = onRejection; + + if (length === 0 && parent._state) { + asap(publish, parent); + } +} + +function publish(promise) { + var subscribers = promise._subscribers; + var settled = promise._state; + + if (subscribers.length === 0) { + return; + } + + var child = void 0, + callback = void 0, + detail = promise._result; + + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; + + if (child) { + invokeCallback(settled, child, callback, detail); + } else { + callback(detail); + } + } + + promise._subscribers.length = 0; +} + +function tryCatch(callback, detail) { + try { + return callback(detail); + } catch (e) { + TRY_CATCH_ERROR.error = e; + return TRY_CATCH_ERROR; + } +} + +function invokeCallback(settled, promise, callback, detail) { + var hasCallback = isFunction(callback), + value = void 0, + error = void 0, + succeeded = void 0, + failed = void 0; + + if (hasCallback) { + value = tryCatch(callback, detail); + + if (value === TRY_CATCH_ERROR) { + failed = true; + error = value.error; + value.error = null; + } else { + succeeded = true; + } + + if (promise === value) { + reject(promise, cannotReturnOwn()); + return; + } + } else { + value = detail; + succeeded = true; + } + + if (promise._state !== PENDING) { + // noop + } else if (hasCallback && succeeded) { + resolve(promise, value); + } else if (failed) { + reject(promise, error); + } else if (settled === FULFILLED) { + fulfill(promise, value); + } else if (settled === REJECTED) { + reject(promise, value); + } +} + +function initializePromise(promise, resolver) { + try { + resolver(function resolvePromise(value) { + resolve(promise, value); + }, function rejectPromise(reason) { + reject(promise, reason); + }); + } catch (e) { + reject(promise, e); + } +} + +var id = 0; +function nextId() { + return id++; +} + +function makePromise(promise) { + promise[PROMISE_ID] = id++; + promise._state = undefined; + promise._result = undefined; + promise._subscribers = []; +} + +function validationError() { + return new Error('Array Methods must be provided an Array'); +} + +var Enumerator = function () { + function Enumerator(Constructor, input) { + this._instanceConstructor = Constructor; + this.promise = new Constructor(noop); + + if (!this.promise[PROMISE_ID]) { + makePromise(this.promise); + } + + if (isArray(input)) { + this.length = input.length; + this._remaining = input.length; + + this._result = new Array(this.length); + + if (this.length === 0) { + fulfill(this.promise, this._result); + } else { + this.length = this.length || 0; + this._enumerate(input); + if (this._remaining === 0) { + fulfill(this.promise, this._result); + } + } + } else { + reject(this.promise, validationError()); + } + } + + Enumerator.prototype._enumerate = function _enumerate(input) { + for (var i = 0; this._state === PENDING && i < input.length; i++) { + this._eachEntry(input[i], i); + } + }; + + Enumerator.prototype._eachEntry = function _eachEntry(entry, i) { + var c = this._instanceConstructor; + var resolve$$1 = c.resolve; + + + if (resolve$$1 === resolve$1) { + var _then = getThen(entry); + + if (_then === then && entry._state !== PENDING) { + this._settledAt(entry._state, i, entry._result); + } else if (typeof _then !== 'function') { + this._remaining--; + this._result[i] = entry; + } else if (c === Promise$2) { + var promise = new c(noop); + handleMaybeThenable(promise, entry, _then); + this._willSettleAt(promise, i); + } else { + this._willSettleAt(new c(function (resolve$$1) { + return resolve$$1(entry); + }), i); + } + } else { + this._willSettleAt(resolve$$1(entry), i); + } + }; + + Enumerator.prototype._settledAt = function _settledAt(state, i, value) { + var promise = this.promise; + + + if (promise._state === PENDING) { + this._remaining--; + + if (state === REJECTED) { + reject(promise, value); + } else { + this._result[i] = value; + } + } + + if (this._remaining === 0) { + fulfill(promise, this._result); + } + }; + + Enumerator.prototype._willSettleAt = function _willSettleAt(promise, i) { + var enumerator = this; + + subscribe(promise, undefined, function (value) { + return enumerator._settledAt(FULFILLED, i, value); + }, function (reason) { + return enumerator._settledAt(REJECTED, i, reason); + }); + }; + + return Enumerator; +}(); + +/** + `Promise.all` accepts an array of promises, and returns a new promise which + is fulfilled with an array of fulfillment values for the passed promises, or + rejected with the reason of the first passed promise to be rejected. It casts all + elements of the passed iterable to promises as it runs this algorithm. + + Example: + + ```javascript + let promise1 = resolve(1); + let promise2 = resolve(2); + let promise3 = resolve(3); + let promises = [ promise1, promise2, promise3 ]; + + Promise.all(promises).then(function(array){ + // The array here would be [ 1, 2, 3 ]; + }); + ``` + + If any of the `promises` given to `all` are rejected, the first promise + that is rejected will be given as an argument to the returned promises's + rejection handler. For example: + + Example: + + ```javascript + let promise1 = resolve(1); + let promise2 = reject(new Error("2")); + let promise3 = reject(new Error("3")); + let promises = [ promise1, promise2, promise3 ]; + + Promise.all(promises).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(error) { + // error.message === "2" + }); + ``` + + @method all + @static + @param {Array} entries array of promises + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all `promises` have been + fulfilled, or rejected if any of them become rejected. + @static +*/ +function all(entries) { + return new Enumerator(this, entries).promise; +} + +/** + `Promise.race` returns a new promise which is settled in the same way as the + first passed promise to settle. + + Example: + + ```javascript + let promise1 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 1'); + }, 200); + }); + + let promise2 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 2'); + }, 100); + }); + + Promise.race([promise1, promise2]).then(function(result){ + // result === 'promise 2' because it was resolved before promise1 + // was resolved. + }); + ``` + + `Promise.race` is deterministic in that only the state of the first + settled promise matters. For example, even if other promises given to the + `promises` array argument are resolved, but the first settled promise has + become rejected before the other promises became fulfilled, the returned + promise will become rejected: + + ```javascript + let promise1 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 1'); + }, 200); + }); + + let promise2 = new Promise(function(resolve, reject){ + setTimeout(function(){ + reject(new Error('promise 2')); + }, 100); + }); + + Promise.race([promise1, promise2]).then(function(result){ + // Code here never runs + }, function(reason){ + // reason.message === 'promise 2' because promise 2 became rejected before + // promise 1 became fulfilled + }); + ``` + + An example real-world use case is implementing timeouts: + + ```javascript + Promise.race([ajax('foo.json'), timeout(5000)]) + ``` + + @method race + @static + @param {Array} promises array of promises to observe + Useful for tooling. + @return {Promise} a promise which settles in the same way as the first passed + promise to settle. +*/ +function race(entries) { + /*jshint validthis:true */ + var Constructor = this; + + if (!isArray(entries)) { + return new Constructor(function (_, reject) { + return reject(new TypeError('You must pass an array to race.')); + }); + } else { + return new Constructor(function (resolve, reject) { + var length = entries.length; + for (var i = 0; i < length; i++) { + Constructor.resolve(entries[i]).then(resolve, reject); + } + }); + } +} + +/** + `Promise.reject` returns a promise rejected with the passed `reason`. + It is shorthand for the following: + + ```javascript + let promise = new Promise(function(resolve, reject){ + reject(new Error('WHOOPS')); + }); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + let promise = Promise.reject(new Error('WHOOPS')); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + @method reject + @static + @param {Any} reason value that the returned promise will be rejected with. + Useful for tooling. + @return {Promise} a promise rejected with the given `reason`. +*/ +function reject$1(reason) { + /*jshint validthis:true */ + var Constructor = this; + var promise = new Constructor(noop); + reject(promise, reason); + return promise; +} + +function needsResolver() { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); +} + +function needsNew() { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); +} + +/** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise's eventual value or the reason + why the promise cannot be fulfilled. + + Terminology + ----------- + + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. + + A promise can be in one of three states: pending, fulfilled, or rejected. + + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. + + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. + + + Basic Usage: + ------------ + + ```js + let promise = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); + }); + + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Advanced Usage: + --------------- + + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. + + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + let xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); + + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']')); + } + } + }; + }); + } + + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Unlike callbacks, promises are great composable primitives. + + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class Promise + @param {Function} resolver + Useful for tooling. + @constructor +*/ + +var Promise$2 = function () { + function Promise(resolver) { + this[PROMISE_ID] = nextId(); + this._result = this._state = undefined; + this._subscribers = []; + + if (noop !== resolver) { + typeof resolver !== 'function' && needsResolver(); + this instanceof Promise ? initializePromise(this, resolver) : needsNew(); + } + } + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + Chaining + -------- + The return value of `then` is itself a promise. This second, 'downstream' + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return 'default name'; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `'default name'` + }); + findUser().then(function (user) { + throw new Error('Found user, but still unhappy'); + }, function (reason) { + throw new Error('`findUser` rejected and we're unhappy'); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'. + // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'. + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + ```js + findUser().then(function (user) { + throw new PedagogicalException('Upstream error'); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + Assimilation + ------------ + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + If the assimliated promise rejects, then the downstream promise will also reject. + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + Simple Example + -------------- + Synchronous Example + ```javascript + let result; + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + Errback Example + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + Promise Example; + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + Advanced Example + -------------- + Synchronous Example + ```javascript + let author, books; + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + Errback Example + ```js + function foundBooks(books) { + } + function failure(reason) { + } + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + Promise Example; + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + @method then + @param {Function} onFulfilled + @param {Function} onRejected + Useful for tooling. + @return {Promise} + */ + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + ```js + function findAuthor(){ + throw new Error('couldn't find that author'); + } + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + @method catch + @param {Function} onRejection + Useful for tooling. + @return {Promise} + */ + + + Promise.prototype.catch = function _catch(onRejection) { + return this.then(null, onRejection); + }; + + /** + `finally` will be invoked regardless of the promise's fate just as native + try/catch/finally behaves + + Synchronous example: + + ```js + findAuthor() { + if (Math.random() > 0.5) { + throw new Error(); + } + return new Author(); + } + + try { + return findAuthor(); // succeed or fail + } catch(error) { + return findOtherAuther(); + } finally { + // always runs + // doesn't affect the return value + } + ``` + + Asynchronous example: + + ```js + findAuthor().catch(function(reason){ + return findOtherAuther(); + }).finally(function(){ + // author was either found, or not + }); + ``` + + @method finally + @param {Function} callback + @return {Promise} + */ + + + Promise.prototype.finally = function _finally(callback) { + var promise = this; + var constructor = promise.constructor; + + return promise.then(function (value) { + return constructor.resolve(callback()).then(function () { + return value; + }); + }, function (reason) { + return constructor.resolve(callback()).then(function () { + throw reason; + }); + }); + }; + + return Promise; +}(); + +Promise$2.prototype.then = then; +Promise$2.all = all; +Promise$2.race = race; +Promise$2.resolve = resolve$1; +Promise$2.reject = reject$1; +Promise$2._setScheduler = setScheduler; +Promise$2._setAsap = setAsap; +Promise$2._asap = asap; + +/*global self*/ +function polyfill() { + var local = void 0; + + if (typeof global !== 'undefined') { + local = global; + } else if (typeof self !== 'undefined') { + local = self; + } else { + try { + local = Function('return this')(); + } catch (e) { + throw new Error('polyfill failed because global object is unavailable in this environment'); + } + } + + var P = local.Promise; + + if (P) { + var promiseToString = null; + try { + promiseToString = Object.prototype.toString.call(P.resolve()); + } catch (e) { + // silently ignored + } + + if (promiseToString === '[object Promise]' && !P.cast) { + return; + } + } + + local.Promise = Promise$2; +} + +// Strange compat.. +Promise$2.polyfill = polyfill; +Promise$2.Promise = Promise$2; + +Promise$2.polyfill(); + +return Promise$2; + +}))); + + + +//# sourceMappingURL=es6-promise.auto.map diff --git a/packages/browser/test/integration/polyfills/whatwg-fetch-2.0.4.js b/packages/browser/test/integration/polyfills/whatwg-fetch-2.0.4.js new file mode 100644 index 000000000000..f2f466d7b38e --- /dev/null +++ b/packages/browser/test/integration/polyfills/whatwg-fetch-2.0.4.js @@ -0,0 +1,466 @@ +(function(self) { + 'use strict'; + + if (self.fetch) { + return + } + + var support = { + searchParams: 'URLSearchParams' in self, + iterable: 'Symbol' in self && 'iterator' in Symbol, + blob: 'FileReader' in self && 'Blob' in self && (function() { + try { + new Blob() + return true + } catch(e) { + return false + } + })(), + formData: 'FormData' in self, + arrayBuffer: 'ArrayBuffer' in self + } + + if (support.arrayBuffer) { + var viewClasses = [ + '[object Int8Array]', + '[object Uint8Array]', + '[object Uint8ClampedArray]', + '[object Int16Array]', + '[object Uint16Array]', + '[object Int32Array]', + '[object Uint32Array]', + '[object Float32Array]', + '[object Float64Array]' + ] + + var isDataView = function(obj) { + return obj && DataView.prototype.isPrototypeOf(obj) + } + + var isArrayBufferView = ArrayBuffer.isView || function(obj) { + return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 + } + } + + function normalizeName(name) { + if (typeof name !== 'string') { + name = String(name) + } + if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) { + throw new TypeError('Invalid character in header field name') + } + return name.toLowerCase() + } + + function normalizeValue(value) { + if (typeof value !== 'string') { + value = String(value) + } + return value + } + + // Build a destructive iterator for the value list + function iteratorFor(items) { + var iterator = { + next: function() { + var value = items.shift() + return {done: value === undefined, value: value} + } + } + + if (support.iterable) { + iterator[Symbol.iterator] = function() { + return iterator + } + } + + return iterator + } + + function Headers(headers) { + this.map = {} + + if (headers instanceof Headers) { + headers.forEach(function(value, name) { + this.append(name, value) + }, this) + } else if (Array.isArray(headers)) { + headers.forEach(function(header) { + this.append(header[0], header[1]) + }, this) + } else if (headers) { + Object.getOwnPropertyNames(headers).forEach(function(name) { + this.append(name, headers[name]) + }, this) + } + } + + Headers.prototype.append = function(name, value) { + name = normalizeName(name) + value = normalizeValue(value) + var oldValue = this.map[name] + this.map[name] = oldValue ? oldValue+','+value : value + } + + Headers.prototype['delete'] = function(name) { + delete this.map[normalizeName(name)] + } + + Headers.prototype.get = function(name) { + name = normalizeName(name) + return this.has(name) ? this.map[name] : null + } + + Headers.prototype.has = function(name) { + return this.map.hasOwnProperty(normalizeName(name)) + } + + Headers.prototype.set = function(name, value) { + this.map[normalizeName(name)] = normalizeValue(value) + } + + Headers.prototype.forEach = function(callback, thisArg) { + for (var name in this.map) { + if (this.map.hasOwnProperty(name)) { + callback.call(thisArg, this.map[name], name, this) + } + } + } + + Headers.prototype.keys = function() { + var items = [] + this.forEach(function(value, name) { items.push(name) }) + return iteratorFor(items) + } + + Headers.prototype.values = function() { + var items = [] + this.forEach(function(value) { items.push(value) }) + return iteratorFor(items) + } + + Headers.prototype.entries = function() { + var items = [] + this.forEach(function(value, name) { items.push([name, value]) }) + return iteratorFor(items) + } + + if (support.iterable) { + Headers.prototype[Symbol.iterator] = Headers.prototype.entries + } + + function consumed(body) { + if (body.bodyUsed) { + return Promise.reject(new TypeError('Already read')) + } + body.bodyUsed = true + } + + function fileReaderReady(reader) { + return new Promise(function(resolve, reject) { + reader.onload = function() { + resolve(reader.result) + } + reader.onerror = function() { + reject(reader.error) + } + }) + } + + function readBlobAsArrayBuffer(blob) { + var reader = new FileReader() + var promise = fileReaderReady(reader) + reader.readAsArrayBuffer(blob) + return promise + } + + function readBlobAsText(blob) { + var reader = new FileReader() + var promise = fileReaderReady(reader) + reader.readAsText(blob) + return promise + } + + function readArrayBufferAsText(buf) { + var view = new Uint8Array(buf) + var chars = new Array(view.length) + + for (var i = 0; i < view.length; i++) { + chars[i] = String.fromCharCode(view[i]) + } + return chars.join('') + } + + function bufferClone(buf) { + if (buf.slice) { + return buf.slice(0) + } else { + var view = new Uint8Array(buf.byteLength) + view.set(new Uint8Array(buf)) + return view.buffer + } + } + + function Body() { + this.bodyUsed = false + + this._initBody = function(body) { + this._bodyInit = body + if (!body) { + this._bodyText = '' + } else if (typeof body === 'string') { + this._bodyText = body + } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body + } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body + } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this._bodyText = body.toString() + } else if (support.arrayBuffer && support.blob && isDataView(body)) { + this._bodyArrayBuffer = bufferClone(body.buffer) + // IE 10-11 can't handle a DataView body. + this._bodyInit = new Blob([this._bodyArrayBuffer]) + } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { + this._bodyArrayBuffer = bufferClone(body) + } else { + throw new Error('unsupported BodyInit type') + } + + if (!this.headers.get('content-type')) { + if (typeof body === 'string') { + this.headers.set('content-type', 'text/plain;charset=UTF-8') + } else if (this._bodyBlob && this._bodyBlob.type) { + this.headers.set('content-type', this._bodyBlob.type) + } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8') + } + } + } + + if (support.blob) { + this.blob = function() { + var rejected = consumed(this) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob) + } else if (this._bodyArrayBuffer) { + return Promise.resolve(new Blob([this._bodyArrayBuffer])) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob') + } else { + return Promise.resolve(new Blob([this._bodyText])) + } + } + + this.arrayBuffer = function() { + if (this._bodyArrayBuffer) { + return consumed(this) || Promise.resolve(this._bodyArrayBuffer) + } else { + return this.blob().then(readBlobAsArrayBuffer) + } + } + } + + this.text = function() { + var rejected = consumed(this) + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return readBlobAsText(this._bodyBlob) + } else if (this._bodyArrayBuffer) { + return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as text') + } else { + return Promise.resolve(this._bodyText) + } + } + + if (support.formData) { + this.formData = function() { + return this.text().then(decode) + } + } + + this.json = function() { + return this.text().then(JSON.parse) + } + + return this + } + + // HTTP methods whose capitalization should be normalized + var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] + + function normalizeMethod(method) { + var upcased = method.toUpperCase() + return (methods.indexOf(upcased) > -1) ? upcased : method + } + + function Request(input, options) { + options = options || {} + var body = options.body + + if (input instanceof Request) { + if (input.bodyUsed) { + throw new TypeError('Already read') + } + this.url = input.url + this.credentials = input.credentials + if (!options.headers) { + this.headers = new Headers(input.headers) + } + this.method = input.method + this.mode = input.mode + if (!body && input._bodyInit != null) { + body = input._bodyInit + input.bodyUsed = true + } + } else { + this.url = String(input) + } + + this.credentials = options.credentials || this.credentials || 'omit' + if (options.headers || !this.headers) { + this.headers = new Headers(options.headers) + } + this.method = normalizeMethod(options.method || this.method || 'GET') + this.mode = options.mode || this.mode || null + this.referrer = null + + if ((this.method === 'GET' || this.method === 'HEAD') && body) { + throw new TypeError('Body not allowed for GET or HEAD requests') + } + this._initBody(body) + } + + Request.prototype.clone = function() { + return new Request(this, { body: this._bodyInit }) + } + + function decode(body) { + var form = new FormData() + body.trim().split('&').forEach(function(bytes) { + if (bytes) { + var split = bytes.split('=') + var name = split.shift().replace(/\+/g, ' ') + var value = split.join('=').replace(/\+/g, ' ') + form.append(decodeURIComponent(name), decodeURIComponent(value)) + } + }) + return form + } + + function parseHeaders(rawHeaders) { + var headers = new Headers() + // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space + // https://tools.ietf.org/html/rfc7230#section-3.2 + var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ') + preProcessedHeaders.split(/\r?\n/).forEach(function(line) { + var parts = line.split(':') + var key = parts.shift().trim() + if (key) { + var value = parts.join(':').trim() + headers.append(key, value) + } + }) + return headers + } + + Body.call(Request.prototype) + + function Response(bodyInit, options) { + if (!options) { + options = {} + } + + this.type = 'default' + this.status = options.status === undefined ? 200 : options.status + this.ok = this.status >= 200 && this.status < 300 + this.statusText = 'statusText' in options ? options.statusText : 'OK' + this.headers = new Headers(options.headers) + this.url = options.url || '' + this._initBody(bodyInit) + } + + Body.call(Response.prototype) + + Response.prototype.clone = function() { + return new Response(this._bodyInit, { + status: this.status, + statusText: this.statusText, + headers: new Headers(this.headers), + url: this.url + }) + } + + Response.error = function() { + var response = new Response(null, {status: 0, statusText: ''}) + response.type = 'error' + return response + } + + var redirectStatuses = [301, 302, 303, 307, 308] + + Response.redirect = function(url, status) { + if (redirectStatuses.indexOf(status) === -1) { + throw new RangeError('Invalid status code') + } + + return new Response(null, {status: status, headers: {location: url}}) + } + + self.Headers = Headers + self.Request = Request + self.Response = Response + + self.fetch = function(input, init) { + return new Promise(function(resolve, reject) { + var request = new Request(input, init) + var xhr = new XMLHttpRequest() + + xhr.onload = function() { + var options = { + status: xhr.status, + statusText: xhr.statusText, + headers: parseHeaders(xhr.getAllResponseHeaders() || '') + } + options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL') + var body = 'response' in xhr ? xhr.response : xhr.responseText + resolve(new Response(body, options)) + } + + xhr.onerror = function() { + reject(new TypeError('Network request failed')) + } + + xhr.ontimeout = function() { + reject(new TypeError('Network request failed')) + } + + xhr.open(request.method, request.url, true) + + if (request.credentials === 'include') { + xhr.withCredentials = true + } else if (request.credentials === 'omit') { + xhr.withCredentials = false + } + + if ('responseType' in xhr && support.blob) { + xhr.responseType = 'blob' + } + + request.headers.forEach(function(value, name) { + xhr.setRequestHeader(name, value) + }) + + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit) + }) + } + self.fetch.polyfill = true +})(typeof self !== 'undefined' ? self : this); diff --git a/packages/browser/test/integration/test.js b/packages/browser/test/integration/test.js new file mode 100644 index 000000000000..fd5c39551c33 --- /dev/null +++ b/packages/browser/test/integration/test.js @@ -0,0 +1,1463 @@ +/*global assert*/ +function iframeExecute(iframe, done, execute, assertCallback) { + iframe.contentWindow.done = function() { + try { + assertCallback(iframe); + done(); + } catch (e) { + done(e); + } + }; + // use setTimeout so stack trace doesn't go all the way back to mocha test runner + iframe.contentWindow.eval( + 'window.originalBuiltIns.setTimeout.call(window, ' + execute.toString() + ');' + ); +} + +function createIframe(done) { + var iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.src = './base/test/integration/frame.html'; + iframe.onload = function() { + done(); + }; + document.body.appendChild(iframe); + return iframe; +} + +var anchor = document.createElement('a'); +function parseUrl(url) { + var out = {pathname: '', origin: '', protocol: ''}; + if (!url) anchor.href = url; + for (var key in out) { + out[key] = anchor[key]; + } + return out; +} + +function isBelowIE11() { + return /*@cc_on!@*/ false == !false; +} + +function isEdge14() { + return window.navigator.userAgent.indexOf('Edge/14') !== -1; +} + +// Thanks for nothing IE! +// (╯°□°)╯︵ ┻━┻ +function canReadFunctionName() { + function foo() {} + if (foo.name === 'foo') return true; + return false; +} + +describe('integration', function() { + this.timeout(30000); + + beforeEach(function(done) { + this.iframe = createIframe(done); + }); + + afterEach(function() { + document.body.removeChild(this.iframe); + }); + + describe('API', function() { + it('should capture Raven.captureMessage', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + Raven.captureMessage('Hello'); + done(); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.equal(ravenData.message, 'Hello'); + } + ); + }); + + it('should capture Raven.captureException', function(done) { + var iframe = this.iframe; + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + try { + foo(); + } catch (e) { + Raven.captureException(e); + } + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.isAtLeast(ravenData.exception.values[0].stacktrace.frames.length, 2); + assert.isAtMost(ravenData.exception.values[0].stacktrace.frames.length, 4); + } + ); + }); + + it('should generate a synthetic trace for captureException w/ non-errors', function(done) { + var iframe = this.iframe; + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + Raven.captureException({foo: 'bar'}); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.isAtLeast(ravenData.exception.values[0].stacktrace.frames.length, 1); + assert.isAtMost(ravenData.exception.values[0].stacktrace.frames.length, 3); + } + ); + }); + + it('should capture an Error object passed to Raven.captureException w/ maxMessageLength set (#647)', function(done) { + var iframe = this.iframe; + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + Raven._globalOptions.maxMessageLength = 100; + Raven.captureException(new Error('lol'), { + level: 'warning', + extra: { + foo: 'bar' + } + }); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.equal(ravenData.exception.values[0].type, 'Error'); + assert.equal(ravenData.exception.values[0].value, 'lol'); + assert.equal(ravenData.exception.values[0].stacktrace.frames.length, 1); + } + ); + }); + + it('should reject duplicate, back-to-back errors from captureError', function(done) { + var iframe = this.iframe; + iframeExecute( + iframe, + done, + function() { + Raven._breadcrumbs = []; + + var count = 5; + setTimeout(function invoke() { + // use setTimeout to capture new error objects that have + // identical stack traces (can't call sequentially or callsite + // line number will change) + // + // order: + // Error: foo + // Error: foo (suppressed) + // Error: foo (suppressed) + // Error: bar + // Error: foo + if (count === 2) { + Raven.captureException(new Error('bar')); + } else { + Raven.captureException(new Error('foo')); + } + + if (count-- === 0) return void done(); + else setTimeout(invoke); + }); + }, + function() { + var breadcrumbs = iframe.contentWindow.Raven._breadcrumbs; + // use breadcrumbs to evaluate which errors were sent + // NOTE: can't use ravenData because duplicate error suppression occurs + // AFTER dataCallback/shouldSendCallback (dataCallback will record + // duplicates but they ultimately won't be sent) + assert.equal(breadcrumbs.length, 3); + assert.equal(breadcrumbs[0].message, 'Error: foo'); + assert.equal(breadcrumbs[1].message, 'Error: bar'); + assert.equal(breadcrumbs[2].message, 'Error: foo'); + } + ); + }); + + it('should not reject back-to-back errors with different stack traces', function(done) { + var iframe = this.iframe; + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + Raven._breadcrumbs = []; + + // same error message, but different stacks means that these are considered + // different errors + + // stack: + // bar + try { + bar(); // declared in frame.html + } catch (e) { + Raven.captureException(e); + } + + // stack (different # frames): + // bar + // foo + try { + foo(); // declared in frame.html + } catch (e) { + Raven.captureException(e); + } + + // stack (same # frames, different frames): + // bar + // foo2 + try { + foo2(); // declared in frame.html + } catch (e) { + Raven.captureException(e); + } + }, + function() { + var breadcrumbs = iframe.contentWindow.Raven._breadcrumbs; + assert.equal(breadcrumbs.length, 3); + // NOTE: regex because exact error message differs per-browser + assert.match(breadcrumbs[0].message, /^ReferenceError.*baz/); + assert.match(breadcrumbs[1].message, /^ReferenceError.*baz/); + assert.match(breadcrumbs[2].message, /^ReferenceError.*baz/); + } + ); + }); + + it('should reject duplicate, back-to-back messages from captureMessage', function(done) { + var iframe = this.iframe; + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + Raven._breadcrumbs = []; + + Raven.captureMessage('this is fine'); + Raven.captureMessage('this is fine'); // suppressed + Raven.captureMessage('this is fine', {stacktrace: true}); + Raven.captureMessage("i'm okay with the events that are unfolding currently"); + Raven.captureMessage("that's okay, things are going to be okay"); + }, + function() { + var breadcrumbs = iframe.contentWindow.Raven._breadcrumbs; + + assert.equal(breadcrumbs.length, 4); + assert.equal(breadcrumbs[0].message, 'this is fine'); + assert.equal(breadcrumbs[1].message, 'this is fine'); // with stacktrace + assert.equal( + breadcrumbs[2].message, + "i'm okay with the events that are unfolding currently" + ); + assert.equal( + breadcrumbs[3].message, + "that's okay, things are going to be okay" + ); + } + ); + }); + }); + + describe('window.onerror', function() { + it('should catch syntax errors', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + eval('foo{};'); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + // ¯\_(ツ)_/¯ + if (isBelowIE11() || isEdge14()) { + assert.equal(ravenData.exception.values[0].type, undefined); + } else { + assert.match(ravenData.exception.values[0].type, /SyntaxError/); + } + assert.equal(ravenData.exception.values[0].stacktrace.frames.length, 1); // just one frame + } + ); + }); + + it('should catch thrown strings', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + // intentionally loading this error via a script file to make + // sure it is 1) not caught by instrumentation 2) doesn't trigger + // "Script error" + var script = document.createElement('script'); + script.src = 'throw-string.js'; + script.onload = function() { + done(); + }; + document.head.appendChild(script); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.match(ravenData.exception.values[0].value, /stringError$/); + assert.equal(ravenData.exception.values[0].stacktrace.frames.length, 1); // always 1 because thrown strings can't provide > 1 frame + + // some browsers extract proper url, line, and column for thrown strings + // but not all - falls back to frame url + assert.match( + ravenData.exception.values[0].stacktrace.frames[0].filename, + /\/test\/integration\// + ); + assert.match( + ravenData.exception.values[0].stacktrace.frames[0]['function'], + /\?|global code/i + ); + } + ); + }); + + it('should catch thrown objects', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + // intentionally loading this error via a script file to make + // sure it is 1) not caught by instrumentation 2) doesn't trigger + // "Script error" + var script = document.createElement('script'); + script.src = 'throw-object.js'; + script.onload = function() { + done(); + }; + document.head.appendChild(script); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.equal(ravenData.exception.values[0].type, undefined); + + // # is covering default Android 4.4 and 5.1 browser + assert.match( + ravenData.exception.values[0].value, + /^(\[object Object\]|#)$/ + ); + assert.equal(ravenData.exception.values[0].stacktrace.frames.length, 1); // always 1 because thrown objects can't provide > 1 frame + + // some browsers extract proper url, line, and column for thrown objects + // but not all - falls back to frame url + assert.match( + ravenData.exception.values[0].stacktrace.frames[0].filename, + /\/test\/integration\// + ); + assert.match( + ravenData.exception.values[0].stacktrace.frames[0]['function'], + /\?|global code/i + ); + } + ); + }); + + it('should catch thrown errors', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + // intentionally loading this error via a script file to make + // sure it is 1) not caught by instrumentation 2) doesn't trigger + // "Script error" + var script = document.createElement('script'); + script.src = 'throw-error.js'; + script.onload = function() { + done(); + }; + document.head.appendChild(script); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + // ¯\_(ツ)_/¯ + if (isBelowIE11() || isEdge14()) { + assert.equal(ravenData.exception.values[0].type, undefined); + } else { + assert.match(ravenData.exception.values[0].type, /^Error/); + } + assert.match(ravenData.exception.values[0].value, /realError$/); + // 1 or 2 depending on platform + assert.isAtLeast(ravenData.exception.values[0].stacktrace.frames.length, 1); + assert.isAtMost(ravenData.exception.values[0].stacktrace.frames.length, 2); + assert.match( + ravenData.exception.values[0].stacktrace.frames[0].filename, + /\/test\/integration\/throw-error\.js/ + ); + assert.match( + ravenData.exception.values[0].stacktrace.frames[0]['function'], + /\?|global code|throwRealError/i + ); + } + ); + }); + + it('should NOT catch an exception already caught via Raven.wrap', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + Raven.wrap(function() { + foo(); + })(); + }, + function() { + var ravenData = iframe.contentWindow.ravenData; + assert.equal(ravenData.length, 1); // one caught error + } + ); + }); + + it('should catch an exception already caught [but rethrown] via Raven.captureException', function(done) { + // unlike Raven.wrap which ALWAYS re-throws, we don't know if the user will + // re-throw an exception passed to Raven.captureException, and so we cannot + // automatically suppress the next error caught through window.onerror + var iframe = this.iframe; + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + try { + foo(); + } catch (e) { + Raven.captureException(e); + throw e; // intentionally re-throw + } + }, + function() { + var ravenData = iframe.contentWindow.ravenData; + assert.equal(ravenData.length, 2); + } + ); + }); + }); + + describe('wrapped built-ins', function() { + it('should capture exceptions from event listeners', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + var div = document.createElement('div'); + document.body.appendChild(div); + div.addEventListener( + 'click', + function() { + foo(); + }, + false + ); + + var click = new MouseEvent('click'); + div.dispatchEvent(click); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.isAtLeast(ravenData.exception.values[0].stacktrace.frames.length, 3); + assert.isAtMost(ravenData.exception.values[0].stacktrace.frames.length, 5); + } + ); + }); + + it('should transparently remove event listeners from wrapped functions', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + var div = document.createElement('div'); + document.body.appendChild(div); + var fooFn = function() { + foo(); + }; + div.addEventListener('click', fooFn, false); + div.removeEventListener('click', fooFn); + + var click = new MouseEvent('click'); + div.dispatchEvent(click); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.equal(ravenData, null); // should never trigger error + } + ); + }); + + it('should capture exceptions inside setTimeout', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(function() { + setTimeout(done); + foo(); + }, 10); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.isAtLeast(ravenData.exception.values[0].stacktrace.frames.length, 3); + assert.isAtMost(ravenData.exception.values[0].stacktrace.frames.length, 4); + } + ); + }); + + it('should capture exceptions inside setInterval', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + var exceptionInterval = setInterval(function() { + setTimeout(done); + clearInterval(exceptionInterval); + foo(); + }, 10); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.isAtLeast(ravenData.exception.values[0].stacktrace.frames.length, 3); + assert.isAtMost(ravenData.exception.values[0].stacktrace.frames.length, 4); + } + ); + }); + + it('should capture exceptions inside requestAnimationFrame', function(done) { + var iframe = this.iframe; + // needs to be visible or requestAnimationFrame won't ever fire + iframe.style.display = 'block'; + + iframeExecute( + iframe, + done, + function() { + requestAnimationFrame(function() { + setTimeout(done); + foo(); + }); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + assert.isAtLeast(ravenData.exception.values[0].stacktrace.frames.length, 3); + assert.isAtMost(ravenData.exception.values[0].stacktrace.frames.length, 4); + } + ); + }); + + it('should capture exceptions from XMLHttpRequest event handlers (e.g. onreadystatechange)', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + var xhr = new XMLHttpRequest(); + + // intentionally assign event handlers *after* XMLHttpRequest.prototype.open, + // since this is what jQuery does + // https://github.com/jquery/jquery/blob/master/src/ajax/xhr.js#L37 + + xhr.open('GET', 'example.json'); + xhr.onreadystatechange = function() { + setTimeout(done); + // replace onreadystatechange with no-op so exception doesn't + // fire more than once as XHR changes loading state + xhr.onreadystatechange = function() {}; + foo(); + }; + xhr.send(); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + // # of frames alter significantly between chrome/firefox & safari + assert.isAtLeast(ravenData.exception.values[0].stacktrace.frames.length, 3); + assert.isAtMost(ravenData.exception.values[0].stacktrace.frames.length, 4); + } + ); + }); + + it("should capture built-in's mechanism type as instrument", function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(function() { + setTimeout(done); + foo(); + }, 10); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + + var fn = ravenData.exception.mechanism.data.function; + delete ravenData.exception.mechanism.data; + + if (canReadFunctionName()) { + assert.equal(fn, 'setTimeout'); + } else { + assert.equal(fn, ''); + } + + assert.deepEqual(ravenData.exception.mechanism, { + type: 'instrument', + handled: true + }); + } + ); + }); + + it("should capture built-in's handlers fn name in mechanism data", function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + var div = document.createElement('div'); + document.body.appendChild(div); + div.addEventListener( + 'click', + function namedFunction() { + foo(); + }, + false + ); + + var click = new MouseEvent('click'); + div.dispatchEvent(click); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + + var handler = ravenData.exception.mechanism.data.handler; + delete ravenData.exception.mechanism.data.handler; + var target = ravenData.exception.mechanism.data.target; + delete ravenData.exception.mechanism.data.target; + + if (canReadFunctionName()) { + assert.equal(handler, 'namedFunction'); + } else { + assert.equal(handler, ''); + } + + // IE vs. Rest of the world + assert.oneOf(target, ['Node', 'EventTarget']); + assert.deepEqual(ravenData.exception.mechanism, { + type: 'instrument', + handled: true, + data: { + function: 'addEventListener' + } + }); + } + ); + }); + + it('should fallback to fn name in mechanism data if one is unavailable', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + var div = document.createElement('div'); + document.body.appendChild(div); + div.addEventListener( + 'click', + function() { + foo(); + }, + false + ); + + var click = new MouseEvent('click'); + div.dispatchEvent(click); + }, + function() { + var ravenData = iframe.contentWindow.ravenData[0]; + + var target = ravenData.exception.mechanism.data.target; + delete ravenData.exception.mechanism.data.target; + + // IE vs. Rest of the world + assert.oneOf(target, ['Node', 'EventTarget']); + assert.deepEqual(ravenData.exception.mechanism, { + type: 'instrument', + handled: true, + data: { + function: 'addEventListener', + handler: '' + } + }); + } + ); + }); + }); + + describe('breadcrumbs', function() { + it('should record an XMLHttpRequest', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + var xhr = new XMLHttpRequest(); + + xhr.open('GET', 'example.json'); + xhr.setRequestHeader('Content-type', 'application/json'); + xhr.onreadystatechange = function() { + // don't fire `done` handler until at least *one* onreadystatechange + // has occurred (doesn't actually need to finish) + if (xhr.readyState === 4) { + setTimeout(done); + } + }; + xhr.send(); + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs; + + assert.equal(breadcrumbs.length, 1); + assert.equal(breadcrumbs[0].type, 'http'); + assert.equal(breadcrumbs[0].data.method, 'GET'); + } + ); + }); + + it('should record an XMLHttpRequest without any handlers set', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + // I hate to do a time-based "done" trigger, but unfortunately we can't + // set an onload/onreadystatechange handler on XHR to verify that it finished + // - that's the whole point of this test! :( + setTimeout(done, 1000); + + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + var xhr = new XMLHttpRequest(); + + xhr.open('GET', 'example.json'); + xhr.setRequestHeader('Content-type', 'application/json'); + xhr.send(); + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs; + + assert.equal(breadcrumbs.length, 1); + + assert.equal(breadcrumbs[0].type, 'http'); + assert.equal(breadcrumbs[0].category, 'xhr'); + assert.equal(breadcrumbs[0].data.method, 'GET'); + } + ); + }); + + it('should NOT denote XMLHttpRequests to the Sentry store endpoint as requiring breadcrumb capture', function(done) { + var iframe = this.iframe; + iframeExecute( + iframe, + done, + function() { + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'http://example.com/api/1/store/?sentry_key=public'); + + // can't actually transmit an XHR (breadcrumb isnt recorded until + // onreadystatechange fires), so enough to just verify that + // __raven_xhr wasn't set on xhr object + + window.ravenData = xhr.hasOwnProperty('__raven_xhr'); + setTimeout(done); + }, + function() { + assert.isFalse(iframe.contentWindow.ravenData); + } + ); + }); + + it('should record a fetch request', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + fetch('example.json').then( + function() { + setTimeout(done); + }, + function() { + setTimeout(done); + } + ); + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs, + breadcrumbUrl = 'example.json'; + + if ('fetch' in window) { + assert.equal(breadcrumbs.length, 1); + + assert.equal(breadcrumbs[0].type, 'http'); + assert.equal(breadcrumbs[0].category, 'fetch'); + assert.equal(breadcrumbs[0].data.method, 'GET'); + assert.equal(breadcrumbs[0].data.url, breadcrumbUrl); + } else { + // otherwise we use a fetch polyfill based on xhr + assert.equal(breadcrumbs.length, 2); + + assert.equal(breadcrumbs[0].type, 'http'); + assert.equal(breadcrumbs[0].category, 'xhr'); + assert.equal(breadcrumbs[0].data.method, 'GET'); + assert.equal(breadcrumbs[0].data.url, breadcrumbUrl); + + assert.equal(breadcrumbs[1].type, 'http'); + assert.equal(breadcrumbs[1].category, 'fetch'); + assert.equal(breadcrumbs[1].data.method, 'GET'); + assert.equal(breadcrumbs[1].data.url, breadcrumbUrl); + } + } + ); + }); + + it('should record a fetch request with Request obj instead of URL string', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + fetch(new Request('example.json')).then( + function() { + setTimeout(done); + }, + function() { + setTimeout(done); + } + ); + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs, + breadcrumbUrl = 'example.json'; + + if ('fetch' in window) { + assert.equal(breadcrumbs.length, 1); + + assert.equal(breadcrumbs[0].type, 'http'); + assert.equal(breadcrumbs[0].category, 'fetch'); + assert.equal(breadcrumbs[0].data.method, 'GET'); + // Request constructor normalizes the url + assert.ok(breadcrumbs[0].data.url.indexOf(breadcrumbUrl) !== -1); + } else { + // otherwise we use a fetch polyfill based on xhr + assert.equal(breadcrumbs.length, 2); + + assert.equal(breadcrumbs[0].type, 'http'); + assert.equal(breadcrumbs[0].category, 'xhr'); + assert.equal(breadcrumbs[0].data.method, 'GET'); + assert.ok(breadcrumbs[0].data.url.indexOf(breadcrumbUrl) !== -1); + + assert.equal(breadcrumbs[1].type, 'http'); + assert.equal(breadcrumbs[1].category, 'fetch'); + assert.equal(breadcrumbs[1].data.method, 'GET'); + assert.ok(breadcrumbs[1].data.url.indexOf(breadcrumbUrl) !== -1); + } + } + ); + }); + + it('should record a fetch request with an arbitrary type argument', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + fetch(123).then( + function() { + setTimeout(done); + }, + function() { + setTimeout(done); + } + ); + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs, + breadcrumbUrl = '123'; + + if ('fetch' in window) { + assert.equal(breadcrumbs.length, 1); + + assert.equal(breadcrumbs[0].type, 'http'); + assert.equal(breadcrumbs[0].category, 'fetch'); + assert.equal(breadcrumbs[0].data.method, 'GET'); + // Request constructor normalizes the url + assert.ok(breadcrumbs[0].data.url.indexOf(breadcrumbUrl) !== -1); + } else { + // otherwise we use a fetch polyfill based on xhr + assert.equal(breadcrumbs.length, 2); + + assert.equal(breadcrumbs[0].type, 'http'); + assert.equal(breadcrumbs[0].category, 'xhr'); + assert.equal(breadcrumbs[0].data.method, 'GET'); + assert.ok(breadcrumbs[0].data.url.indexOf(breadcrumbUrl) !== -1); + + assert.equal(breadcrumbs[1].type, 'http'); + assert.equal(breadcrumbs[1].category, 'fetch'); + assert.equal(breadcrumbs[1].data.method, 'GET'); + assert.ok(breadcrumbs[1].data.url.indexOf(breadcrumbUrl) !== -1); + } + } + ); + }); + + it('should record a mouse click on element WITH click handler present', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + // add an event listener to the input. we want to make sure that + // our breadcrumbs still work even if the page has an event listener + // on an element that cancels event bubbling + var input = document.getElementsByTagName('input')[0]; + var clickHandler = function(evt) { + evt.stopPropagation(); // don't bubble + }; + input.addEventListener('click', clickHandler); + + // click + var click = new MouseEvent('click'); + input.dispatchEvent(click); + }, + 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, + 'body > form#foo-form > input[name="foo"]' + ); + } + ); + }); + + it('should record a mouse click on element WITHOUT click handler present', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + // click + var click = new MouseEvent('click'); + var input = document.getElementsByTagName('input')[0]; + input.dispatchEvent(click); + }, + 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, + 'body > form#foo-form > input[name="foo"]' + ); + } + ); + }); + + it('should only record a SINGLE mouse click for a tree of elements with event listeners', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + var clickHandler = function(evt) { + //evt.stopPropagation(); + }; + + // mousemove event shouldnt clobber subsequent "breadcrumbed" events (see #724) + document.querySelector('.a').addEventListener('mousemove', clickHandler); + + document.querySelector('.a').addEventListener('click', clickHandler); + document.querySelector('.b').addEventListener('click', clickHandler); + document.querySelector('.c').addEventListener('click', clickHandler); + + // click + var click = new MouseEvent('click'); + var input = document.querySelector('.a'); // leaf node + input.dispatchEvent(click); + }, + 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, 'body > div.c > div.b > div.a'); + } + ); + }); + + 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); + + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + // click + var click = new MouseEvent('click'); + function kaboom() { + throw new Error('lol'); + } + Object.defineProperty(click, 'type', {get: kaboom}); + Object.defineProperty(click, 'target', {get: kaboom}); + + var input = document.querySelector('.a'); // leaf node + input.dispatchEvent(click); + }, + 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) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + // keypress twice + var keypress1 = new KeyboardEvent('keypress'); + var keypress2 = new KeyboardEvent('keypress'); + + var input = document.getElementsByTagName('input')[0]; + input.dispatchEvent(keypress1); + input.dispatchEvent(keypress2); + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs; + + assert.equal(breadcrumbs.length, 1); + + assert.equal(breadcrumbs[0].category, 'ui.input'); + assert.equal( + breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + } + ); + }); + + it('should flush keypress breadcrumbs when an error is thrown', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + // keypress + var keypress = new KeyboardEvent('keypress'); + + var input = document.getElementsByTagName('input')[0]; + input.dispatchEvent(keypress); + + foo(); // throw exception + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs; + + // 2 breadcrumbs: `ui_event`, then `error` + assert.equal(breadcrumbs.length, 2); + + assert.equal(breadcrumbs[0].category, 'ui.input'); + assert.equal( + breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + } + ); + }); + + it('should flush keypress breadcrumb when input event occurs immediately after', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + // 1st keypress + var keypress1 = new KeyboardEvent('keypress'); + // click + var click = new MouseEvent('click'); + // 2nd keypress + var keypress2 = new KeyboardEvent('keypress'); + + var input = document.getElementsByTagName('input')[0]; + input.dispatchEvent(keypress1); + input.dispatchEvent(click); + input.dispatchEvent(keypress2); + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs; + + // 2x `ui_event` + assert.equal(breadcrumbs.length, 3); + + assert.equal(breadcrumbs[0].category, 'ui.input'); + assert.equal( + breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + + assert.equal(breadcrumbs[1].category, 'ui.click'); + assert.equal( + breadcrumbs[1].message, + 'body > form#foo-form > input[name="foo"]' + ); + + assert.equal(breadcrumbs[2].category, 'ui.input'); + assert.equal( + breadcrumbs[2].message, + 'body > form#foo-form > input[name="foo"]' + ); + } + ); + }); + + it('should record consecutive keypress events in a contenteditable into a single "input" breadcrumb', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + // keypress twice + var keypress1 = new KeyboardEvent('keypress'); + var keypress2 = new KeyboardEvent('keypress'); + + var div = document.querySelector('[contenteditable]'); + div.dispatchEvent(keypress1); + div.dispatchEvent(keypress2); + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs; + + assert.equal(breadcrumbs.length, 1); + + assert.equal(breadcrumbs[0].category, 'ui.input'); + assert.equal( + breadcrumbs[0].message, + 'body > form#foo-form > div.contenteditable' + ); + } + ); + }); + + it('should record history.[pushState|replaceState] changes as navigation breadcrumbs', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + // some browsers trigger onpopstate for load / reset breadcrumb state + Raven._breadcrumbs = []; + + history.pushState({}, '', '/foo'); + history.pushState({}, '', '/bar?a=1#fragment'); + history.pushState({}, '', {}); // pushState calls toString on non-string args + history.pushState({}, '', null); // does nothing / no-op + + // can't call history.back() because it will change url of parent document + // (e.g. document running mocha) ... instead just "emulate" a back button + // press by calling replaceState + history.replaceState({}, '', '/bar?a=1#fragment'); + + done(); + }, + function() { + var Raven = iframe.contentWindow.Raven, + breadcrumbs = Raven._breadcrumbs, + from, + to; + + assert.equal(breadcrumbs.length, 4); + assert.equal(breadcrumbs[0].category, 'navigation'); // (start) => foo + assert.equal(breadcrumbs[1].category, 'navigation'); // foo => bar?a=1#fragment + 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.ok( + /\/test\/integration\/frame\.html$/.test(Raven._breadcrumbs[0].data.from), + "'from' url is incorrect" + ); + assert.ok(/\/foo$/.test(breadcrumbs[0].data.to), "'to' url is incorrect"); + + assert.ok(/\/foo$/.test(breadcrumbs[1].data.from), "'from' url is incorrect"); + assert.ok( + /\/bar\?a=1#fragment$/.test(breadcrumbs[1].data.to), + "'to' url is incorrect" + ); + + assert.ok( + /\/bar\?a=1#fragment$/.test(breadcrumbs[2].data.from), + "'from' url is incorrect" + ); + assert.ok( + /\[object Object\]$/.test(breadcrumbs[2].data.to), + "'to' url is incorrect" + ); + + assert.ok( + /\[object Object\]$/.test(breadcrumbs[3].data.from), + "'from' url is incorrect" + ); + assert.ok( + /\/bar\?a=1#fragment/.test(breadcrumbs[3].data.to), + "'to' url is incorrect" + ); + } + ); + }); + + it('should preserve native code detection compatibility', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + done(); + }, + function() { + assert.include( + Function.prototype.toString.call(window.setTimeout), + '[native code]' + ); + assert.include( + Function.prototype.toString.call(window.setInterval), + '[native code]' + ); + assert.include( + Function.prototype.toString.call(window.addEventListener), + '[native code]' + ); + assert.include( + Function.prototype.toString.call(window.removeEventListener), + '[native code]' + ); + assert.include( + Function.prototype.toString.call(window.requestAnimationFrame), + '[native code]' + ); + if ('fetch' in window) { + assert.include( + Function.prototype.toString.call(window.fetch), + '[native code]' + ); + } + } + ); + }); + }); + + describe('uninstall', function() { + it('should restore original built-ins', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + Raven.uninstall(); + + window.isRestored = { + setTimeout: originalBuiltIns.setTimeout === setTimeout, + setInterval: originalBuiltIns.setInterval === setInterval, + requestAnimationFrame: + originalBuiltIns.requestAnimationFrame === requestAnimationFrame, + xhrProtoOpen: originalBuiltIns.xhrProtoOpen === XMLHttpRequest.prototype.open, + headAddEventListener: + originalBuiltIns.headAddEventListener === document.body.addEventListener, + headRemoveEventListener: + originalBuiltIns.headRemoveEventListener === + document.body.removeEventListener, + consoleDebug: originalBuiltIns.consoleDebug === console.debug, + consoleInfo: originalBuiltIns.consoleInfo === console.info, + consoleWarn: originalBuiltIns.consoleWarn === console.warn, + consoleError: originalBuiltIns.consoleError === console.error, + consoleLog: originalBuiltIns.consoleLog === console.log + }; + }, + function() { + var isRestored = iframe.contentWindow.isRestored; + assert.isTrue(isRestored.setTimeout); + assert.isTrue(isRestored.setInterval); + assert.isTrue(isRestored.requestAnimationFrame); + assert.isTrue(isRestored.xhrProtoOpen); + assert.isTrue(isRestored.headAddEventListener); + assert.isTrue(isRestored.headRemoveEventListener); + assert.isTrue(isRestored.consoleDebug); + assert.isTrue(isRestored.consoleInfo); + assert.isTrue(isRestored.consoleWarn); + assert.isTrue(isRestored.consoleError); + assert.isTrue(isRestored.consoleLog); + } + ); + }); + + it('should not restore XMLHttpRequest instance methods', function(done) { + var iframe = this.iframe; + + iframeExecute( + iframe, + done, + function() { + setTimeout(done); + + var xhr = new XMLHttpRequest(); + var origOnReadyStateChange = (xhr.onreadystatechange = function() {}); + xhr.open('GET', '/foo/'); + xhr.abort(); + + Raven.uninstall(); + + window.isOnReadyStateChangeRestored = xhr.onready === origOnReadyStateChange; + }, + function() { + assert.isFalse(iframe.contentWindow.isOnReadyStateChangeRestored); + } + ); + }); + }); +}); diff --git a/packages/browser/test/integration/throw-error.js b/packages/browser/test/integration/throw-error.js new file mode 100644 index 000000000000..453a10c41f36 --- /dev/null +++ b/packages/browser/test/integration/throw-error.js @@ -0,0 +1,5 @@ +function throwRealError() { + throw new Error('realError'); +} + +throwRealError(); diff --git a/packages/browser/test/integration/throw-object.js b/packages/browser/test/integration/throw-object.js new file mode 100644 index 000000000000..7fe17ebe766f --- /dev/null +++ b/packages/browser/test/integration/throw-object.js @@ -0,0 +1,7 @@ +function throwStringError() { + // never do this; just making sure Raven.js handles this case + // gracefully + throw {error: 'stuff is broken'}; +} + +throwStringError(); diff --git a/packages/browser/test/integration/throw-string.js b/packages/browser/test/integration/throw-string.js new file mode 100644 index 000000000000..7429303cab1a --- /dev/null +++ b/packages/browser/test/integration/throw-string.js @@ -0,0 +1,5 @@ +function throwStringError() { + throw 'stringError'; +} + +throwStringError(); From 1ed9cc2160317470fc7f5e46547e140649413731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 3 Aug 2018 18:41:03 +0200 Subject: [PATCH 02/14] test: Update karma config for integration tests --- .../browser/karma/karma.integration.config.js | 1 + packages/browser/package.json | 5 +++-- .../integration/polyfills/es6-promise-4.2.4.js | 15 ++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/browser/karma/karma.integration.config.js b/packages/browser/karma/karma.integration.config.js index a15d2249911a..9e84ec46481f 100644 --- a/packages/browser/karma/karma.integration.config.js +++ b/packages/browser/karma/karma.integration.config.js @@ -14,6 +14,7 @@ module.exports = config => { { pattern: 'test/integration/example.json', included: false }, { pattern: 'test/integration/frame.html', included: false }, { pattern: 'build/bundle.js', included: false }, + { pattern: 'build/bundle.js.map', included: false }, 'test/integration/test.js', ], frameworks: ['mocha', 'chai', 'sinon'], diff --git a/packages/browser/package.json b/packages/browser/package.json index 2e67799467ef..bde0bdd99363 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -60,10 +60,11 @@ "fix": "run-s fix:tslint fix:prettier", "fix:prettier": "prettier --write '{src,test}/**/*.ts'", "fix:tslint": "tslint --fix -t stylish -p .", - "test": "karma start karma/karma.unit.config.js", + "test": "run-s test:unit test:integration", + "test:unit": "karma start karma/karma.unit.config.js", "test:watch": "karma start karma/karma.unit.config.js --auto-watch --no-single-run", "test:integration": "karma start karma/karma.integration.config.js", - "test:integration-sauce": "karma start karma/karma.integration-sauce.config.js", + "test:integration:watch": "karma start karma/karma.integration.config.js --auto-watch --no-single-run", "size:check": "cat build/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print $1,\"kB\";}'", "version": "node ../../scripts/versionbump.js src/version.ts" }, diff --git a/packages/browser/test/integration/polyfills/es6-promise-4.2.4.js b/packages/browser/test/integration/polyfills/es6-promise-4.2.4.js index 30d01304f0a4..c3bbb9fb1676 100644 --- a/packages/browser/test/integration/polyfills/es6-promise-4.2.4.js +++ b/packages/browser/test/integration/polyfills/es6-promise-4.2.4.js @@ -1068,9 +1068,9 @@ var Promise$2 = function () { /** `finally` will be invoked regardless of the promise's fate just as native try/catch/finally behaves - + Synchronous example: - + ```js findAuthor() { if (Math.random() > 0.5) { @@ -1078,7 +1078,7 @@ var Promise$2 = function () { } return new Author(); } - + try { return findAuthor(); // succeed or fail } catch(error) { @@ -1088,9 +1088,9 @@ var Promise$2 = function () { // doesn't affect the return value } ``` - + Asynchronous example: - + ```js findAuthor().catch(function(reason){ return findOtherAuther(); @@ -1098,7 +1098,7 @@ var Promise$2 = function () { // author was either found, or not }); ``` - + @method finally @param {Function} callback @return {Promise} @@ -1176,6 +1176,3 @@ return Promise$2; }))); - - -//# sourceMappingURL=es6-promise.auto.map From e6e0d2b3073f89c486919085de2cf41b56d36520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 3 Aug 2018 18:41:31 +0200 Subject: [PATCH 03/14] utils: Fixed parseUrl and serializeObject methods --- packages/utils/src/misc.ts | 4 ++++ packages/utils/src/object.ts | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts index 8cd9839543a3..4858a34c502f 100644 --- a/packages/utils/src/misc.ts +++ b/packages/utils/src/misc.ts @@ -159,6 +159,10 @@ export function parseUrl( protocol?: string; relative?: string; } { + if (!url) { + return {}; + } + const match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/); if (!match) { diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index 0bea011738bc..8ed583b2787b 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -202,8 +202,6 @@ function serializeValue(value: T): T | string { /** JSDoc */ function serializeObject(value: T, depth: number): T | string | {} { - return value; - if (depth === 0) { return serializeValue(value); } From 3b51e30f254e8078676bbb56488a992731a2efe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 3 Aug 2018 18:42:14 +0200 Subject: [PATCH 04/14] browser: Fixed event creation based on the type --- packages/browser/src/backend.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/browser/src/backend.ts b/packages/browser/src/backend.ts index 59bf1d2a43eb..d08df5bfff46 100644 --- a/packages/browser/src/backend.ts +++ b/packages/browser/src/backend.ts @@ -2,7 +2,7 @@ import { Backend, logger, Options, SentryError } from '@sentry/core'; import { SentryEvent, SentryResponse, Status } from '@sentry/types'; import { isDOMError, isDOMException, isError, isErrorEvent, isPlainObject } from '@sentry/utils/is'; import { supportsFetch } from '@sentry/utils/supports'; -import { eventFromStacktrace, getEventOptionsFromPlainObject, prepareFramesForEvent } from './parsers'; +import { eventFromPlainObject, eventFromStacktrace, prepareFramesForEvent } from './parsers'; import { computeStackTrace } from './tracekit'; import { FetchTransport, XHRTransport } from './transports'; @@ -58,10 +58,13 @@ export class BrowserBackend implements Backend { * @inheritDoc */ public async eventFromException(exception: any, syntheticException: Error | null): Promise { + let event; + if (isErrorEvent(exception as ErrorEvent) && (exception as ErrorEvent).error) { // If it is an ErrorEvent with `error` property, extract it to get actual Error const ex = exception as ErrorEvent; exception = ex.error; // tslint:disable-line:no-parameter-reassignment + event = eventFromStacktrace(computeStackTrace(exception as Error)); } else if (isDOMError(exception as DOMError) || isDOMException(exception as DOMException)) { // If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers) // then we just extract the name and message, as they don't provide anything else @@ -71,16 +74,16 @@ export class BrowserBackend implements Backend { const name = ex.name || (isDOMError(ex) ? 'DOMError' : 'DOMException'); const message = ex.message ? `${name}: ${ex.message}` : name; - return this.eventFromMessage(message, syntheticException); + event = await this.eventFromMessage(message, syntheticException); } else if (isError(exception as Error)) { // we have a real Error object, do nothing + event = eventFromStacktrace(computeStackTrace(exception as Error)); } else if (isPlainObject(exception as {})) { // If it is plain Object, serialize it manually and extract options // This will allow us to group events based on top-level keys // which is much better than creating new group when any key/value change const ex = exception as {}; - const options = getEventOptionsFromPlainObject(ex); - exception = new Error(options.message); // tslint:disable-line:no-parameter-reassignment + event = eventFromPlainObject(ex, syntheticException); } else { // If none of previous checks were valid, then it means that // it's not a DOMError/DOMException @@ -89,11 +92,9 @@ export class BrowserBackend implements Backend { // it's not an Error // So bail out and capture it as a simple message: const ex = exception as string; - return this.eventFromMessage(ex, syntheticException); + event = await this.eventFromMessage(ex, syntheticException); } - let event: SentryEvent = eventFromStacktrace(computeStackTrace(exception as Error)); - event = { ...event, exception: { From b60060d59ebc2711719a1aaf330d5123967a3855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 3 Aug 2018 18:42:47 +0200 Subject: [PATCH 05/14] browser: Filter out our own requests from breadcrumb integrations --- .../browser/src/integrations/breadcrumbs.ts | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 6dd976ac175c..a8eda53f686f 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,3 +1,4 @@ +import { DSN } from '@sentry/core'; import { getCurrentHub } from '@sentry/hub'; import { Integration, Severity } from '@sentry/types'; import { isFunction, isString } from '@sentry/utils/is'; @@ -5,6 +6,7 @@ import { getGlobalObject, parseUrl } from '@sentry/utils/misc'; import { fill } from '@sentry/utils/object'; import { safeJoin } from '@sentry/utils/string'; import { supportsFetch, supportsHistory } from '@sentry/utils/supports'; +import { BrowserOptions } from '../backend'; import { breadcrumbEventHandler, keypressEventHandler, wrap } from './helpers'; const global = getGlobalObject() as Window; @@ -36,7 +38,7 @@ export class Breadcrumbs implements Integration { * @inheritDoc */ public constructor( - private readonly options: { + private readonly config: { console?: boolean; dom?: boolean; fetch?: boolean; @@ -109,7 +111,7 @@ export class Breadcrumbs implements Integration { } /** JSDoc */ - private instrumentFetch(): void { + private instrumentFetch(options: { filterUrl?: string }): void { if (!supportsFetch()) { return; } @@ -131,6 +133,11 @@ export class Breadcrumbs implements Integration { url = String(fetchInput); } + // if Sentry key appears in URL, don't capture, as it's our own request + if (options.filterUrl && url.includes(options.filterUrl)) { + return originalFetch.apply(global, args); + } + if (args[1] && args[1].method) { method = args[1].method; } @@ -178,7 +185,12 @@ export class Breadcrumbs implements Integration { const captureUrlChange = (from: string | undefined, to: string | undefined): void => { const parsedLoc = parseUrl(global.location.href); const parsedTo = parseUrl(to as string); - const parsedFrom = parseUrl(from as string); + let parsedFrom = parseUrl(from as string); + + // Initial pushState doesn't provide `from` information + if (!parsedFrom.path) { + parsedFrom = parsedLoc; + } // because onpopstate only tells you the "new" (to) value of location.href, and // not the previous (from) value, we need to track the value of the current URL @@ -211,7 +223,7 @@ export class Breadcrumbs implements Integration { const currentHref = global.location.href; captureUrlChange(lastHref, currentHref); if (oldOnPopState) { - return oldOnPopState.apply(global, args); + return oldOnPopState.apply(this, args); } }; @@ -219,14 +231,14 @@ export class Breadcrumbs implements Integration { function historyReplacementFunction(originalHistoryFunction: () => void): () => void { // note history.pushState.length is 0; intentionally not declaring // params to preserve 0 arity - return function(...args: any[]): void { + return function(this: History, ...args: any[]): void { const url = args.length > 2 ? args[2] : undefined; // url argument is optional if (url) { // coerce to string (this is what pushState does) captureUrlChange(lastHref, String(url)); } - return originalHistoryFunction.apply(global, ...args); + return originalHistoryFunction.apply(this, args); }; } @@ -234,7 +246,7 @@ export class Breadcrumbs implements Integration { fill(global.history, 'replaceState', historyReplacementFunction); } /** JSDoc */ - private instrumentXHR(): void { + private instrumentXHR(options: { filterUrl?: string }): void { if (!('XMLHttpRequest' in global)) { return; } @@ -250,6 +262,7 @@ export class Breadcrumbs implements Integration { function: prop, handler: (original && original.name) || '', }, + handled: true, type: 'instrument', }, }), @@ -263,7 +276,9 @@ export class Breadcrumbs implements Integration { 'open', originalOpen => function(this: SentryWrappedXMLHttpRequest, ...args: any[]): void { - if (isString(args[1])) { + const url = args[1]; + // if Sentry key appears in URL, don't capture, as it's our own request + if (isString(url) && (options.filterUrl && !url.includes(options.filterUrl))) { this.__sentry_xhr__ = { method: args[0], url: args[1], @@ -312,6 +327,7 @@ export class Breadcrumbs implements Integration { function: 'onreadystatechange', handler: (original && original.name) || '', }, + handled: true, type: 'instrument', }, }, @@ -323,7 +339,7 @@ export class Breadcrumbs implements Integration { // are free to set our own and capture the breadcrumb xhr.onreadystatechange = onreadystatechangeHandler; } - return originalSend.apply(XMLHttpRequest, args); + return originalSend.apply(this, args); }, ); } @@ -337,20 +353,23 @@ export class Breadcrumbs implements Integration { * * Can be disabled or individually configured via the `autoBreadcrumbs` config option */ - public install(): void { - if (this.options.console) { + public install(options: BrowserOptions = {}): void { + // TODO: Use API provider instead of raw `new DSN` + const filterUrl = options.dsn && new DSN(options.dsn).user; + + if (this.config.console) { this.instrumentConsole(); } - if (this.options.dom) { + if (this.config.dom) { this.instrumentDOM(); } - if (this.options.xhr) { - this.instrumentXHR(); + if (this.config.xhr) { + this.instrumentXHR({ filterUrl }); } - if (this.options.fetch) { - this.instrumentFetch(); + if (this.config.fetch) { + this.instrumentFetch({ filterUrl }); } - if (this.options.history) { + if (this.config.history) { this.instrumentHistory(); } } From a4bb104d056a2e82411c22915047f0e92a37e3bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 3 Aug 2018 18:43:25 +0200 Subject: [PATCH 06/14] browser: Fixed ignoreNextOnError --- packages/browser/src/integrations/globalhandlers.ts | 4 ++++ packages/browser/src/integrations/helpers.ts | 11 ++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index dd159849628e..5048fa424347 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -8,6 +8,7 @@ import { StackTrace as TraceKitStackTrace, subscribe, } from '../tracekit'; +import { shouldIgnoreOnError } from './helpers'; /** Global handlers */ export class GlobalHandlers implements Integration { @@ -43,6 +44,9 @@ export class GlobalHandlers implements Integration { // 9: " foo();" // 10: " }" // ] + if (shouldIgnoreOnError()) { + return; + } captureEvent(this.eventFromGlobalHandler(stack)); }); diff --git a/packages/browser/src/integrations/helpers.ts b/packages/browser/src/integrations/helpers.ts index 68343fbecdd3..63505f9adebb 100644 --- a/packages/browser/src/integrations/helpers.ts +++ b/packages/browser/src/integrations/helpers.ts @@ -1,16 +1,17 @@ import { getCurrentHub } from '@sentry/hub'; -import { SentryEvent, SentryWrappedFunction } from '@sentry/types'; +import { Mechanism, SentryEvent, SentryWrappedFunction } from '@sentry/types'; import { isFunction } from '@sentry/utils/is'; import { htmlTreeAsString } from '@sentry/utils/misc'; const debounceDuration: number = 1000; let keypressTimeout: number | undefined; let lastCapturedEvent: Event | undefined; -let ignoreOnError: number = -1; - -// TODO: Fix `ignoreNextOnError`. Just temporary build fix for unused variable -ignoreOnError = ignoreOnError + 1; +let ignoreOnError: number = 0; +/** JSDoc */ +export function shouldIgnoreOnError(): boolean { + return ignoreOnError > 0; +} /** JSDoc */ export function ignoreNextOnError(): void { // onerror should trigger before setTimeout From c80cf0218a7b7073732fd15cdf6165fa15cd3896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 3 Aug 2018 18:44:11 +0200 Subject: [PATCH 07/14] browser: Fixed mechanism for wrapped functions and eventFromObject thingy --- packages/browser/src/integrations/helpers.ts | 20 +++++++++++------ packages/browser/src/integrations/trycatch.ts | 4 ++++ packages/browser/src/parsers.ts | 22 +++++++++++-------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/browser/src/integrations/helpers.ts b/packages/browser/src/integrations/helpers.ts index 63505f9adebb..810c2bf9c013 100644 --- a/packages/browser/src/integrations/helpers.ts +++ b/packages/browser/src/integrations/helpers.ts @@ -30,9 +30,9 @@ export function ignoreNextOnError(): void { */ export function wrap( fn: SentryWrappedFunction, - options?: { - mechanism?: object; - }, + options: { + mechanism?: Mechanism; + } = {}, before?: SentryWrappedFunction, ): any { try { @@ -66,10 +66,16 @@ export function wrap( ignoreNextOnError(); getCurrentHub().withScope(async () => { - getCurrentHub().addEventProcessor(async (event: SentryEvent) => ({ - ...event, - ...(options && options.mechanism), - })); + getCurrentHub().addEventProcessor(async (event: SentryEvent) => { + const processedEvent = { ...event }; + + if (options.mechanism) { + processedEvent.exception = processedEvent.exception || {}; + processedEvent.exception.mechanism = options.mechanism; + } + + return processedEvent; + }); getCurrentHub().captureException(ex); }); diff --git a/packages/browser/src/integrations/trycatch.ts b/packages/browser/src/integrations/trycatch.ts index 016221a319b9..c47ff67b7b77 100644 --- a/packages/browser/src/integrations/trycatch.ts +++ b/packages/browser/src/integrations/trycatch.ts @@ -20,6 +20,7 @@ export class TryCatch implements Integration { args[0] = wrap(originalCallback, { mechanism: { data: { function: original.name || '' }, + handled: true, type: 'instrument', }, }); @@ -37,6 +38,7 @@ export class TryCatch implements Integration { function: 'requestAnimationFrame', handler: (original && original.name) || '', }, + handled: true, type: 'instrument', }, }), @@ -70,6 +72,7 @@ export class TryCatch implements Integration { handler: ((fn as any) as SentryWrappedFunction).name || '', target, }, + handled: true, type: 'instrument', }, }); @@ -124,6 +127,7 @@ export class TryCatch implements Integration { handler: ((fn as any) as SentryWrappedFunction).name || '', target, }, + handled: true, type: 'instrument', }, }, diff --git a/packages/browser/src/parsers.ts b/packages/browser/src/parsers.ts index 355048ba1edf..338055c2dd81 100644 --- a/packages/browser/src/parsers.ts +++ b/packages/browser/src/parsers.ts @@ -1,7 +1,7 @@ import { SentryEvent, StackFrame } from '@sentry/types'; import { limitObjectDepthToSize, serializeKeysToEventMessage } from '@sentry/utils/object'; import * as md5proxy from 'md5'; -import { StackFrame as TraceKitStackFrame, StackTrace as TraceKitStackTrace } from './tracekit'; +import { computeStackTrace, StackFrame as TraceKitStackFrame, StackTrace as TraceKitStackTrace } from './tracekit'; // Workaround for Rollup issue with overloading namespaces // https://github.com/rollup/rollup/issues/1267#issuecomment-296395734 @@ -10,21 +10,25 @@ const md5 = ((md5proxy as any).default || md5proxy) as (input: string) => string const STACKTRACE_LIMIT = 50; /** JSDoc */ -export function getEventOptionsFromPlainObject(exception: {}): { - extra: { - __serialized__: object; - }; - fingerprint: [string]; - message: string; -} { +export function eventFromPlainObject(exception: {}, syntheticException: Error | null): SentryEvent { const exceptionKeys = Object.keys(exception).sort(); - return { + const event: SentryEvent = { extra: { __serialized__: limitObjectDepthToSize(exception), }, fingerprint: [md5(exceptionKeys.join(''))], message: `Non-Error exception captured with keys: ${serializeKeysToEventMessage(exceptionKeys)}`, }; + + if (syntheticException) { + const stacktrace = computeStackTrace(syntheticException); + const frames = prepareFramesForEvent(stacktrace.stack); + event.stacktrace = { + frames, + }; + } + + return event; } /** JSDoc */ From 8daff23eb5603e3c7f357090e725303da1e40252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 3 Aug 2018 18:44:34 +0200 Subject: [PATCH 08/14] tracekit: onerror url patch for Firefox --- packages/browser/src/tracekit/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/tracekit/index.js b/packages/browser/src/tracekit/index.js index 7c382f9483be..4fd7ee714369 100644 --- a/packages/browser/src/tracekit/index.js +++ b/packages/browser/src/tracekit/index.js @@ -230,6 +230,7 @@ TraceKit.report = (function reportModuleWrapper() { * @memberof TraceKit.report */ function traceKitWindowOnError(message, url, lineNo, columnNo, errorObj) { + debugger; var stack = null; // If 'errorObj' is ErrorEvent, get real Error from inside errorObj = isErrorEvent(errorObj) ? errorObj.error : errorObj; @@ -265,7 +266,15 @@ TraceKit.report = (function reportModuleWrapper() { name: name, message: msg, mode: 'onerror', - stack: [location], + stack: [ + { + ...location, + // Firefox sometimes doesn't return url correctly and this is an old behavior + // that I prefer to port here as well. + // It can be altered only here, as previously it's using `location.url` for other things — Kamil + url: location.url || getLocationHref(), + }, + ], }; notifyHandlers(stack, true, null); From 6db523cb703b67c2f10ff4378241c91991c8a076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 3 Aug 2018 18:45:25 +0200 Subject: [PATCH 09/14] browser: Ported all integration tests from raven-js --- packages/browser/rollup.config.js | 92 ++- packages/browser/test/integration/frame.html | 35 +- packages/browser/test/integration/test.js | 633 +++++++------------ 3 files changed, 294 insertions(+), 466 deletions(-) diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js index b51b637ea48a..2559c9087219 100644 --- a/packages/browser/rollup.config.js +++ b/packages/browser/rollup.config.js @@ -8,6 +8,32 @@ const commitHash = require('child_process') .execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }) .trim(); +const bundleConfig = { + input: 'src/index.ts', + output: { + format: 'iife', + name: 'Sentry', + sourcemap: true, + }, + context: 'window', + plugins: [ + typescript({ + tsconfig: 'tsconfig.build.json', + tsconfigOverride: { compilerOptions: { declaration: false } }, + }), + resolve({ + jsnext: true, + main: true, + browser: true, + }), + commonjs(), + license({ + sourcemap: true, + banner: `/*! @sentry/browser <%= pkg.version %> (${commitHash}) | https://github.com/getsentry/raven-js */`, + }), + ], +}; + export default [ { input: 'src/index.ts', @@ -30,57 +56,19 @@ export default [ commonjs(), ], }, - { - input: 'src/index.ts', - output: { - file: 'build/bundle.js', - format: 'iife', - name: 'Sentry', - sourcemap: true, - }, - context: 'window', - plugins: [ - typescript({ - tsconfig: 'tsconfig.build.json', - tsconfigOverride: { compilerOptions: { declaration: false } }, - }), - resolve({ - jsnext: true, - main: true, - browser: true, - }), - commonjs(), - license({ - sourcemap: true, - banner: `/*! @sentry/browser <%= pkg.version %> (${commitHash}) | https://github.com/getsentry/raven-js */`, - }), - ], - }, - { - input: 'src/index.ts', - output: { + Object.assign({}, bundleConfig, { + output: Object.assign({}, bundleConfig.output, { file: 'build/bundle.min.js', - format: 'iife', - name: 'Sentry', - sourcemap: true, - }, - context: 'window', - plugins: [ - typescript({ - tsconfig: 'tsconfig.build.json', - tsconfigOverride: { compilerOptions: { declaration: false } }, - }), - resolve({ - jsnext: true, - main: true, - browser: true, - }), - commonjs(), - uglify(), - license({ - sourcemap: true, - banner: `/*! @sentry/browser <%= pkg.version %> (${commitHash}) | https://github.com/getsentry/raven-js */`, - }), - ], - }, + }), + }), + Object.assign({}, bundleConfig, { + output: Object.assign({}, bundleConfig.output, { + file: 'build/bundle.js', + }), + // Uglify has to be at the end of compilation, BUT before the license banner + plugins: bundleConfig.plugins + .slice(0, -1) + .concat(uglify()) + .concat(bundleConfig.plugins.slice(-1)), + }), ]; diff --git a/packages/browser/test/integration/frame.html b/packages/browser/test/integration/frame.html index a06d0a7cab89..7be23064d733 100644 --- a/packages/browser/test/integration/frame.html +++ b/packages/browser/test/integration/frame.html @@ -87,9 +87,6 @@