Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 44 additions & 16 deletions src/raven.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

var TraceKit = require('../vendor/TraceKit/tracekit');
var stringify = require('../vendor/json-stringify-safe/stringify');
var md5 = require('../vendor/md5/md5');
var RavenConfigError = require('./configError');

var utils = require('./utils');
var isError = utils.isError;
var isObject = utils.isObject;
var isPlainObject = utils.isPlainObject;
var isErrorEvent = utils.isErrorEvent;
var isUndefined = utils.isUndefined;
var isFunction = utils.isFunction;
Expand All @@ -28,6 +30,8 @@ var parseUrl = utils.parseUrl;
var fill = utils.fill;
var supportsFetch = utils.supportsFetch;
var supportsReferrerPolicy = utils.supportsReferrerPolicy;
var serializeKeysForMessage = utils.serializeKeysForMessage;
var serializeException = utils.serializeException;

var wrapConsoleMethod = require('./console').wrapMethod;

Expand Down Expand Up @@ -456,23 +460,34 @@ Raven.prototype = {
*/
captureException: function(ex, options) {
options = objectMerge({trimHeadFrames: 0}, options ? options : {});
// Cases for sending ex as a message, rather than an exception
var isNotError = !isError(ex);
var isNotErrorEvent = !isErrorEvent(ex);
var isErrorEventWithoutError = isErrorEvent(ex) && !ex.error;

if ((isNotError && isNotErrorEvent) || isErrorEventWithoutError) {
return this.captureMessage(
ex,
objectMerge(options, {
stacktrace: true, // if we fall back to captureMessage, default to attempting a new trace
trimHeadFrames: options.trimHeadFrames + 1
})
);
}

// Get actual Error from ErrorEvent
if (isErrorEvent(ex)) ex = ex.error;
if (isPlainObject(ex)) {
// 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
options = this._getCaptureExceptionOptionsFromPlainObject(options, ex);
ex = new Error(options.message);

} else if (isErrorEvent(ex) && ex.error) {
// If it is an ErrorEvent with `error` property, extract it to get actual Error
ex = ex.error;
} else if (isError(ex)){
// we have a real Error object
ex = ex;
} else {
// If none of previous checks were valid, then it means that
// it's not a plain Object
// it's not a valid ErrorEvent (one with an error property)
// it's not an Error
// So bail out and capture it as a simple message:
return this.captureMessage(
ex,
objectMerge(options, {
stacktrace: true, // if we fall back to captureMessage, default to attempting a new trace
trimHeadFrames: options.trimHeadFrames + 1
})
);
}

// Store the raw exception object for potential debugging and introspection
this._lastCapturedException = ex;
Expand All @@ -494,6 +509,19 @@ Raven.prototype = {
return this;
},

_getCaptureExceptionOptionsFromPlainObject: function(currentOptions, ex) {
var exKeys = Object.keys(ex).sort();
var options = objectMerge(currentOptions, {
message:
'Non-Error exception captured with keys: ' + serializeKeysForMessage(exKeys),
fingerprint: [md5(exKeys)],
extra: currentOptions.extra || {}
});
options.extra.__serialized__ = serializeException(ex);

return options;
},

/*
* Manually send a message to Sentry
*
Expand Down
98 changes: 97 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
var stringify = require('../vendor/json-stringify-safe/stringify');

var _window =
typeof window !== 'undefined'
? window
Expand Down Expand Up @@ -441,6 +443,98 @@ function safeJoin(input, delimiter) {
return output.join(delimiter);
}

// Default Node.js REPL depth
var MAX_SERIALIZE_EXCEPTION_DEPTH = 3;
// 50kB, as 100kB is max payload size, so half sounds reasonable
var MAX_SERIALIZE_EXCEPTION_SIZE = 50 * 1024;
var MAX_SERIALIZE_KEYS_LENGTH = 40;

function utf8Length(value) {
return ~-encodeURI(value).split(/%..|./).length;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you only using ~- this for subtracting 1? I trust it's right, just want to understand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use ).length - 1? (it's NBD, just curious)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I trust in every line of code that Substack writes tbh, nothing more - https://github.com/substack/utf8-length/blob/master/index.js :P

}

function jsonSize(value) {
return utf8Length(JSON.stringify(value));
}

function serializeValue(value) {
var maxLength = 40;

if (typeof value === 'string') {
return value.length <= maxLength ? value : value.substr(0, maxLength - 1) + '\u2026';
} else if (
typeof value === 'number' ||
typeof value === 'boolean' ||
typeof value === 'undefined'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any other value types here we could want... dates?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or are those objects?

Copy link
Contributor Author

@kamilogorek kamilogorek Mar 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dates are treated as object with the representation of [object Date]. It's taken care of by JSON.stringify which extracts the date from it

JSON.stringify(new Date('02/03/2017'))
// ""2017-02-02T23:00:00.000Z""

There are definitely some other types that we can add in the future, but right now, they'll be gracefully handled by stringify.

) {
return value;
}

var type = Object.prototype.toString.call(value);

// Node.js REPL notation
if (type === '[object Object]') return '[Object]';
if (type === '[object Array]') return '[Array]';
if (type === '[object Function]')
return value.name ? '[Function: ' + value.name + ']' : '[Function]';

return value;
}

function serializeObject(value, depth) {
if (depth === 0) return serializeValue(value);

if (isPlainObject(value)) {
return Object.keys(value).reduce(function(acc, key) {
acc[key] = serializeObject(value[key], depth - 1);
return acc;
}, {});
} else if (Array.isArray(value)) {
return value.map(function(val) {
return serializeObject(val, depth - 1);
});
}

return serializeValue(value);
}

function serializeException(ex, depth, maxSize) {
if (!isPlainObject(ex)) return ex;

depth = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_DEPTH : depth;
maxSize = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_SIZE : maxSize;

var serialized = serializeObject(ex, depth);

if (jsonSize(stringify(serialized)) > maxSize) {
return serializeException(ex, depth - 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

}

return serialized;
}

function serializeKeysForMessage(keys, maxLength) {
if (typeof keys === 'number' || typeof keys === 'string') return keys.toString();
if (!Array.isArray(keys)) return '';

keys = keys.filter(function(key) {
return typeof key === 'string';
});
if (keys.length === 0) return '[object has no keys]';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i mentioned this in the test but I think this string could be improved a bit to be more declarative/official looking


maxLength = typeof maxLength !== 'number' ? MAX_SERIALIZE_KEYS_LENGTH : maxLength;
if (keys[0].length >= maxLength) return keys[0];

for (var usedKeys = keys.length; usedKeys > 0; usedKeys--) {
var serialized = keys.slice(0, usedKeys).join(', ');
if (serialized.length > maxLength) continue;
if (usedKeys === keys.length) return serialized;
return serialized + '\u2026';
}

return '';
}

module.exports = {
isObject: isObject,
isError: isError,
Expand Down Expand Up @@ -470,5 +564,7 @@ module.exports = {
isSameStacktrace: isSameStacktrace,
parseUrl: parseUrl,
fill: fill,
safeJoin: safeJoin
safeJoin: safeJoin,
serializeException: serializeException,
serializeKeysForMessage: serializeKeysForMessage
};
7 changes: 2 additions & 5 deletions test/integration/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,8 @@ describe('integration', function() {
},
function() {
var ravenData = iframe.contentWindow.ravenData[0];
assert.isAtLeast(ravenData.stacktrace.frames.length, 1);
assert.isAtMost(ravenData.stacktrace.frames.length, 3);

// verify trimHeadFrames hasn't slipped into final payload
assert.isUndefined(ravenData.trimHeadFrames);
assert.isAtLeast(ravenData.exception.values[0].stacktrace.frames.length, 1);
assert.isAtMost(ravenData.exception.values[0].stacktrace.frames.length, 3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it correct to say that the old assertions would also have passed here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just checking my understanding of this change

Copy link
Contributor Author

@kamilogorek kamilogorek Mar 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's behavior change some time ago afaik. We now always call delete data.trimHeadFrames when calling send https://github.com/getsentry/raven-js/blob/master/src/raven.js#L1808

}
);
});
Expand Down
71 changes: 64 additions & 7 deletions test/raven.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3079,13 +3079,6 @@ describe('Raven (public API)', function() {
});
}

it('should send non-Errors as messages', function() {
this.sinon.stub(Raven, 'isSetup').returns(true);
this.sinon.stub(Raven, 'captureMessage');
Raven.captureException({}, {foo: 'bar'});
assert.isTrue(Raven.captureMessage.calledOnce);
});

it('should call handleStackInfo', function() {
var error = new Error('pickleRick');
this.sinon.stub(Raven, 'isSetup').returns(true);
Expand Down Expand Up @@ -3156,6 +3149,70 @@ describe('Raven (public API)', function() {
Raven.captureException(new Error('err'));
});
});

it('should serialize non-error exceptions', function(done) {
this.sinon.stub(Raven, 'isSetup').returns(true);
this.sinon.stub(Raven, '_send').callsFake(function stubbedSend(kwargs) {
kwargs.message.should.equal(
'Non-Error exception captured with keys: aKeyOne, bKeyTwo, cKeyThree, dKeyFour\u2026'
);

var serialized = kwargs.extra.__serialized__;
var fn;

// Yes, I know, it's ugly but...
// unfortunately older browsers are not capable of extracting method names
// therefore we have to use `oneOf` here
fn = serialized.eKeyFive;
delete serialized.eKeyFive;
assert.oneOf(fn, ['[Function: foo]', '[Function]']);

fn = serialized.fKeySix.levelTwo.levelThreeAnonymousFunction;
delete serialized.fKeySix.levelTwo.levelThreeAnonymousFunction;
assert.oneOf(fn, ['[Function: levelThreeAnonymousFunction]', '[Function]']);

fn = serialized.fKeySix.levelTwo.levelThreeNamedFunction;
delete serialized.fKeySix.levelTwo.levelThreeNamedFunction;
assert.oneOf(fn, ['[Function: bar]', '[Function]']);

assert.deepEqual(serialized, {
aKeyOne: 'a',
bKeyTwo: 42,
cKeyThree: {},
dKeyFour: ['d'],
fKeySix: {
levelTwo: {
levelThreeObject: '[Object]',
levelThreeArray: '[Array]',
levelThreeString: 'foo',
levelThreeNumber: 42
}
}
});

done();
});

Raven.captureException({
aKeyOne: 'a',
bKeyTwo: 42,
cKeyThree: {},
dKeyFour: ['d'],
eKeyFive: function foo() {},
fKeySix: {
levelTwo: {
levelThreeObject: {
enough: 42
},
levelThreeArray: [42],
levelThreeAnonymousFunction: function() {},
levelThreeNamedFunction: function bar() {},
levelThreeString: 'foo',
levelThreeNumber: 42
}
}
});
});
});

describe('.captureBreadcrumb', function() {
Expand Down
Loading