diff --git a/docs/config.rst b/docs/config.rst index 95e1955e29be..0035eaba32ae 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -135,6 +135,22 @@ Those configuration options are documented below: includePaths: [/https?:\/\/getsentry\.com/, /https?:\/\/cdn\.getsentry\.com/] } +.. describe:: normalizePath + + A callback that is given a file path and can return an alternate + representation of it. This is called for all frames in the stack. Helpful + for cleaning up your stack trace. + + Note that if you provide a custom dataCallback, this _will not_ be called. + + .. code-block:: javascript + + { + normalizePath: function(path) { + return path.replace(/^.*\/[^\.]+\.app/, ''); + } + } + .. describe:: dataCallback A function that allows mutation of the data payload right before being diff --git a/plugins/react-native.js b/plugins/react-native.js index 9712b2473168..afb23221bede 100644 --- a/plugins/react-native.js +++ b/plugins/react-native.js @@ -9,8 +9,10 @@ * * Options: * - * pathStrip: A RegExp that matches the portions of a file URI that should be - * removed from stacks prior to submission. + * pathStrip: DEPRECATED: Please use Raven's normalizePath option instead. + * + * A RegExp that matches the portions of a file URI that should be removed + * from stacks prior to submission. * * onInitialize: A callback that fires once the plugin has fully initialized * and checked for any previously thrown fatals. If there was a fatal, its @@ -59,10 +61,13 @@ function reactNativePlugin(Raven, options) { // transport - use XMLHttpRequest instead Raven.setTransport(reactNativePlugin._transport); - // Use data callback to strip device-specific paths from stack traces - Raven.setDataCallback(function(data) { - reactNativePlugin._normalizeData(data, options.pathStrip) - }); + if (!Raven._globalOptions.normalizePath) { + // Strip device-specific paths from stack traces. + var pathStripRe = options.pathStrip || PATH_STRIP_RE; + Raven.setNormalizePath(function(filename) { + return normalizeUrl(filename, pathStripRe); + }); + } // Check for a previously persisted payload, and report it. reactNativePlugin._restorePayload() @@ -200,25 +205,4 @@ reactNativePlugin._transport = function (options) { request.send(JSON.stringify(options.data)); }; -/** - * Strip device-specific IDs found in culprit and frame filenames - * when running React Native applications on a physical device. - */ -reactNativePlugin._normalizeData = function (data, pathStripRe) { - if (!pathStripRe) { - pathStripRe = PATH_STRIP_RE; - } - - if (data.culprit) { - data.culprit = normalizeUrl(data.culprit, pathStripRe); - } - - if (data.exception) { - // if data.exception exists, all of the other keys are guaranteed to exist - data.exception.values[0].stacktrace.frames.forEach(function (frame) { - frame.filename = normalizeUrl(frame.filename, pathStripRe); - }); - } -}; - module.exports = reactNativePlugin; diff --git a/src/raven.js b/src/raven.js index d9ca09bc62bf..58c6b1335c77 100644 --- a/src/raven.js +++ b/src/raven.js @@ -493,6 +493,23 @@ Raven.prototype = { return this; }, + /* + * Set the normalizePath option + * + * Note that setting dataCallback overrides this option. + * + * @param {function} callback A callback that is given a file path and can + * return an alternate representation of it. This + * is called for all frames in the stack. + * + * @return {Raven} + */ + setNormalizePath: function(callback) { + this._globalOptions.normalizePath = callback; + + return this; + }, + /* * Set the shouldSendCallback option * @@ -1242,6 +1259,8 @@ Raven.prototype = { if (isFunction(globalOptions.dataCallback)) { data = globalOptions.dataCallback(data) || data; + } else { + data = this._defaultDataCallback(data); } // Why?????????? @@ -1377,6 +1396,27 @@ Raven.prototype = { } else { this._globalContext[key] = objectMerge(this._globalContext[key] || {}, context); } + }, + + /* + * Processes all file names via the `normalizePath` Raven option. + */ + _defaultDataCallback: function(data) { + var normalizePath = this._globalOptions.normalizePath; + if (!normalizePath) return data; + + if (data.culprit) { + data.culprit = normalizePath(data.culprit); + } + + if (data.exception) { + // if data.exception exists, all of the other keys are guaranteed to exist + data.exception.values[0].stacktrace.frames.forEach(function (frame) { + frame.filename = normalizePath(frame.filename); + }); + } + + return data; } }; diff --git a/test/plugins/react-native.test.js b/test/plugins/react-native.test.js index 5eea410dc665..4c183dd04b1a 100644 --- a/test/plugins/react-native.test.js +++ b/test/plugins/react-native.test.js @@ -16,8 +16,11 @@ describe('React Native plugin', function () { reactNativePlugin._clearPayload = self.sinon.stub().returns(Promise.resolve()); }); - describe('_normalizeData()', function () { + describe('path normalization', function () { it('should normalize culprit and frame filenames/URLs from app', function () { + ErrorUtils.setGlobalHandler = function() {}; + reactNativePlugin(Raven); + var data = { project: '2', logger: 'javascript', @@ -45,7 +48,7 @@ describe('React Native plugin', function () { }], } }; - reactNativePlugin._normalizeData(data); + data = Raven._defaultDataCallback(data); assert.equal(data.culprit, '/app.js'); var frames = data.exception.values[0].stacktrace.frames; @@ -54,6 +57,9 @@ describe('React Native plugin', function () { }); it('should normalize culprit and frame filenames/URLs from CodePush', function () { + ErrorUtils.setGlobalHandler = function() {}; + reactNativePlugin(Raven); + var data = { project: '2', logger: 'javascript', @@ -81,7 +87,7 @@ describe('React Native plugin', function () { }], } }; - reactNativePlugin._normalizeData(data); + data = Raven._defaultDataCallback(data); assert.equal(data.culprit, '/app.js'); var frames = data.exception.values[0].stacktrace.frames; diff --git a/test/raven.test.js b/test/raven.test.js index acf4455b76f6..92fc7be20918 100644 --- a/test/raven.test.js +++ b/test/raven.test.js @@ -784,6 +784,87 @@ describe('globals', function() { }); }); + it('should honor normalizePath for stack frames', function() { + this.sinon.stub(Raven, 'isSetup').returns(true); + this.sinon.stub(Raven, '_makeRequest'); + + Raven._globalOptions = { + projectId: 2, + logger: 'javascript', + maxMessageLength: 100, + normalizePath: function(path) { + return '/foo' + path; + } + }; + + Raven._send({ + exception: { + values: [{ + type: 'BadaBoom', + value: 'boom', + stacktrace: { + frames: [ + { + filename: '/bar.js', + lineno: 123, + colno: 456, + in_app: true + }, + { + filename: '/baz.js', + lineno: 1, + in_app: false + }, + ], + } + }] + } + }); + + assert.deepEqual(Raven._makeRequest.lastCall.args[0].data.exception, { + values: [{ + type: 'BadaBoom', + value: 'boom', + stacktrace: { + frames: [ + { + filename: '/foo/bar.js', + lineno: 123, + colno: 456, + in_app: true + }, + { + filename: '/foo/baz.js', + lineno: 1, + in_app: false + }, + ], + } + }] + }); + }); + + it('should honor normalizePath for culprits', function() { + this.sinon.stub(Raven, 'isSetup').returns(true); + this.sinon.stub(Raven, '_makeRequest'); + + Raven._globalOptions = { + projectId: 2, + logger: 'javascript', + maxMessageLength: 100, + normalizePath: function(path) { + return '/foo' + path; + } + }; + + Raven._send({ + message: 'boom', + culprit: '/bar.js', + }); + + assert.equal(Raven._makeRequest.lastCall.args[0].data.culprit, '/foo/bar.js'); + }); + it('should let dataCallback override everything', function() { this.sinon.stub(Raven, 'isSetup').returns(true); this.sinon.stub(Raven, '_makeRequest');