Skip to content
This repository was archived by the owner on Oct 23, 2023. It is now read-only.
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
34 changes: 27 additions & 7 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var transports = require('./transports');
var nodeUtil = require('util'); // nodeUtil to avoid confusion with "utils"
var events = require('events');
var domain = require('domain');
var md5 = require('md5');

var instrumentor = require('./instrumentation/instrumentor');

Expand Down Expand Up @@ -355,20 +356,39 @@ extend(Raven.prototype, {
},

captureException: function captureException(err, kwargs, cb) {
if (!(err instanceof Error)) {
// This handles when someone does:
// throw "something awesome";
// We synthesize an Error here so we can extract a (rough) stack trace.
err = new Error(err);
}

if (!cb && typeof kwargs === 'function') {
cb = kwargs;
kwargs = {};
} else {
kwargs = kwargs || {};
}

if (!(err instanceof Error)) {
if (utils.isPlainObject(err)) {
// 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Damn, that's a great idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

image

Copy link
Contributor

Choose a reason for hiding this comment

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

lol

Copy link
Contributor

Choose a reason for hiding this comment

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

ah, that's awesome kamil. do they need to be hashed? I guess it's a length thing, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without hashing, very large objects will create way too large fingerprints. This way it's always fixed length as you mentioned.

var keys = Object.keys(err).sort();
var hash = md5(keys);
var message =
'Non-Error exception captured with keys: ' +
utils.serializeKeysForMessage(keys);
var serializedException = utils.serializeException(err);

kwargs.message = message;
kwargs.fingerprint = [hash];
kwargs.extra = {
__serialized__: serializedException
};

err = new Error(message);
} else {
// This handles when someone does:
// throw "something awesome";
// We synthesize an Error here so we can extract a (rough) stack trace.
err = new Error(err);
}
}

var self = this;
var eventId = this.generateEventId();
parsers.parseError(err, kwargs, function(kw) {
Expand Down
103 changes: 103 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var transports = require('./transports');
var path = require('path');
var lsmod = require('lsmod');
var stacktrace = require('stack-trace');
var stringify = require('../vendor/json-stringify-safe');

var ravenVersion = require('../package.json').version;

Expand All @@ -16,6 +17,108 @@ var protocolMap = {

var consoleAlerts = {};

// 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;
}

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

function isPlainObject(what) {
return Object.prototype.toString.call(what) === '[object Object]';
}

module.exports.isPlainObject = isPlainObject;

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'
) {
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);
}

return serialized;
}

module.exports.serializeException = serializeException;

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]';

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.serializeKeysForMessage = serializeKeysForMessage;

module.exports.disableConsoleAlerts = function disableConsoleAlerts() {
consoleAlerts = false;
};
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"dependencies": {
"cookie": "0.3.1",
"lsmod": "1.0.0",
"md5": "^2.2.1",
"stack-trace": "0.0.9",
"timed-out": "4.0.1",
"uuid": "3.0.0"
Expand Down
99 changes: 69 additions & 30 deletions test/raven.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
/* global Promise */
'use strict';

var versionRegexp = /^v(\d+)\.(\d+)\.(\d+)$/i;
var majorVersion = parseInt(versionRegexp.exec(process.version)[1], 10);

var raven = require('../'),
nock = require('nock'),
url = require('url'),
Expand Down Expand Up @@ -246,6 +249,62 @@ describe('raven.Client', function() {
client.captureException('wtf?');
});

it('should serialize non-error exceptions', function(done) {
var old = client.send;
client.send = function mockSend(kwargs) {
client.send = old;

kwargs.message.should.equal(
'Non-Error exception captured with keys: aKeyOne, bKeyTwo, cKeyThree, dKeyFour\u2026'
);

// Remove superfluous node version data to simplify the test itself
delete kwargs.extra.node;
kwargs.extra.should.have.property('__serialized__', {
aKeyOne: 'a',
bKeyTwo: 42,
cKeyThree: {},
dKeyFour: ['d'],
eKeyFive: '[Function: foo]',
fKeySix: {
levelTwo: {
levelThreeObject: '[Object]',
levelThreeArray: '[Array]',
// Node < 6 is not capable of pulling function name from unnamed object methods
levelThreeAnonymousFunction:
majorVersion < 6
? '[Function]'
: '[Function: levelThreeAnonymousFunction]',
levelThreeNamedFunction: '[Function: bar]',
levelThreeString: 'foo',
levelThreeNumber: 42
}
}
});

done();
};
client.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
}
}
});
});

it('should send an Error to Sentry server on another port', function(done) {
var scope = nock('https://app.getsentry.com:8443')
.filteringRequestBody(/.*/, '*')
Expand Down Expand Up @@ -380,19 +439,15 @@ describe('raven.Client', function() {

describe('exit conditions', function() {
var exitStr = 'exit test assertions complete\n';
it('should catch an uncaughtException and capture it before exiting', function(
done
) {
it('should catch an uncaughtException and capture it before exiting', function(done) {
child_process.exec('node test/exit/capture.js', function(err, stdout, stderr) {
stdout.should.equal(exitStr);
stderr.should.startWith('Error: derp');
done();
});
});

it('should catch an uncaughtException and capture it before calling a provided callback', function(
done
) {
it('should catch an uncaughtException and capture it before calling a provided callback', function(done) {
child_process.exec('node test/exit/capture_callback.js', function(
err,
stdout,
Expand All @@ -405,9 +460,7 @@ describe('raven.Client', function() {
});
});

it('should catch an uncaughtException and capture it without a second followup exception causing premature shutdown', function(
done
) {
it('should catch an uncaughtException and capture it without a second followup exception causing premature shutdown', function(done) {
child_process.exec('node test/exit/capture_with_second_error.js', function(
err,
stdout,
Expand All @@ -419,9 +472,7 @@ describe('raven.Client', function() {
});
});

it('should treat an error thrown by captureException from uncaughtException handler as a sending error passed to onFatalError', function(
done
) {
it('should treat an error thrown by captureException from uncaughtException handler as a sending error passed to onFatalError', function(done) {
this.timeout(4000);
child_process.exec('node test/exit/throw_on_send.js', function(
err,
Expand All @@ -447,9 +498,7 @@ describe('raven.Client', function() {
});
});

it('should catch a domain exception and capture it before calling a provided callback', function(
done
) {
it('should catch a domain exception and capture it before calling a provided callback', function(done) {
child_process.exec('node test/exit/domain_capture_callback.js', function(
err,
stdout,
Expand All @@ -462,9 +511,7 @@ describe('raven.Client', function() {
});
});

it('should catch a domain exception and capture it without a second followup exception causing premature shutdown', function(
done
) {
it('should catch a domain exception and capture it without a second followup exception causing premature shutdown', function(done) {
child_process.exec('node test/exit/domain_capture_with_second_error.js', function(
err,
stdout,
Expand All @@ -476,9 +523,7 @@ describe('raven.Client', function() {
});
});

it('should treat an error thrown by captureException from domain exception handler as a sending error passed to onFatalError', function(
done
) {
it('should treat an error thrown by captureException from domain exception handler as a sending error passed to onFatalError', function(done) {
this.timeout(4000);
child_process.exec('node test/exit/domain_throw_on_send.js', function(
err,
Expand All @@ -492,9 +537,7 @@ describe('raven.Client', function() {
});
});

it('should catch an uncaughtException and capture it without a second followup domain exception causing premature shutdown', function(
done
) {
it('should catch an uncaughtException and capture it without a second followup domain exception causing premature shutdown', function(done) {
child_process.exec('node test/exit/capture_with_second_domain_error.js', function(
err,
stdout,
Expand All @@ -506,9 +549,7 @@ describe('raven.Client', function() {
});
});

it('should catch an uncaughtException and capture it and failsafe shutdown if onFatalError throws', function(
done
) {
it('should catch an uncaughtException and capture it and failsafe shutdown if onFatalError throws', function(done) {
child_process.exec('node test/exit/throw_on_fatal.js', function(
err,
stdout,
Expand Down Expand Up @@ -579,9 +620,7 @@ describe('raven.Client', function() {
);
});

it('should pass original shouldSendCallback to newer shouldSendCallback', function(
done
) {
it('should pass original shouldSendCallback to newer shouldSendCallback', function(done) {
var cb1 = function(data) {
return false;
};
Expand Down
Loading