Skip to content
This repository was archived by the owner on Oct 23, 2023. It is now read-only.

Commit af36fcc

Browse files
authored
feat: Sensible non-Error exception serializer (#416)
* feat: Sensible non-Error exception serializer * feat: Use serialized keys for non-error messages * test: Integration test for non-error ex serializer * test: Fix non-errors exception tests for node < 6
1 parent a0e7da9 commit af36fcc

File tree

5 files changed

+428
-44
lines changed

5 files changed

+428
-44
lines changed

lib/client.js

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ var transports = require('./transports');
99
var nodeUtil = require('util'); // nodeUtil to avoid confusion with "utils"
1010
var events = require('events');
1111
var domain = require('domain');
12+
var md5 = require('md5');
1213

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

@@ -355,20 +356,39 @@ extend(Raven.prototype, {
355356
},
356357

357358
captureException: function captureException(err, kwargs, cb) {
358-
if (!(err instanceof Error)) {
359-
// This handles when someone does:
360-
// throw "something awesome";
361-
// We synthesize an Error here so we can extract a (rough) stack trace.
362-
err = new Error(err);
363-
}
364-
365359
if (!cb && typeof kwargs === 'function') {
366360
cb = kwargs;
367361
kwargs = {};
368362
} else {
369363
kwargs = kwargs || {};
370364
}
371365

366+
if (!(err instanceof Error)) {
367+
if (utils.isPlainObject(err)) {
368+
// This will allow us to group events based on top-level keys
369+
// which is much better than creating new group when any key/value change
370+
var keys = Object.keys(err).sort();
371+
var hash = md5(keys);
372+
var message =
373+
'Non-Error exception captured with keys: ' +
374+
utils.serializeKeysForMessage(keys);
375+
var serializedException = utils.serializeException(err);
376+
377+
kwargs.message = message;
378+
kwargs.fingerprint = [hash];
379+
kwargs.extra = {
380+
__serialized__: serializedException
381+
};
382+
383+
err = new Error(message);
384+
} else {
385+
// This handles when someone does:
386+
// throw "something awesome";
387+
// We synthesize an Error here so we can extract a (rough) stack trace.
388+
err = new Error(err);
389+
}
390+
}
391+
372392
var self = this;
373393
var eventId = this.generateEventId();
374394
parsers.parseError(err, kwargs, function(kw) {

lib/utils.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var transports = require('./transports');
66
var path = require('path');
77
var lsmod = require('lsmod');
88
var stacktrace = require('stack-trace');
9+
var stringify = require('../vendor/json-stringify-safe');
910

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

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

1718
var consoleAlerts = {};
1819

20+
// Default Node.js REPL depth
21+
var MAX_SERIALIZE_EXCEPTION_DEPTH = 3;
22+
// 50kB, as 100kB is max payload size, so half sounds reasonable
23+
var MAX_SERIALIZE_EXCEPTION_SIZE = 50 * 1024;
24+
var MAX_SERIALIZE_KEYS_LENGTH = 40;
25+
26+
function utf8Length(value) {
27+
return ~-encodeURI(value).split(/%..|./).length;
28+
}
29+
30+
function jsonSize(value) {
31+
return utf8Length(JSON.stringify(value));
32+
}
33+
34+
function isPlainObject(what) {
35+
return Object.prototype.toString.call(what) === '[object Object]';
36+
}
37+
38+
module.exports.isPlainObject = isPlainObject;
39+
40+
function serializeValue(value) {
41+
var maxLength = 40;
42+
43+
if (typeof value === 'string') {
44+
return value.length <= maxLength ? value : value.substr(0, maxLength - 1) + '\u2026';
45+
} else if (
46+
typeof value === 'number' ||
47+
typeof value === 'boolean' ||
48+
typeof value === 'undefined'
49+
) {
50+
return value;
51+
}
52+
53+
var type = Object.prototype.toString.call(value);
54+
55+
// Node.js REPL notation
56+
if (type === '[object Object]') return '[Object]';
57+
if (type === '[object Array]') return '[Array]';
58+
if (type === '[object Function]')
59+
return value.name ? '[Function: ' + value.name + ']' : '[Function]';
60+
61+
return value;
62+
}
63+
64+
function serializeObject(value, depth) {
65+
if (depth === 0) return serializeValue(value);
66+
67+
if (isPlainObject(value)) {
68+
return Object.keys(value).reduce(function(acc, key) {
69+
acc[key] = serializeObject(value[key], depth - 1);
70+
return acc;
71+
}, {});
72+
} else if (Array.isArray(value)) {
73+
return value.map(function(val) {
74+
return serializeObject(val, depth - 1);
75+
});
76+
}
77+
78+
return serializeValue(value);
79+
}
80+
81+
function serializeException(ex, depth, maxSize) {
82+
if (!isPlainObject(ex)) return ex;
83+
84+
depth = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_DEPTH : depth;
85+
maxSize = typeof depth !== 'number' ? MAX_SERIALIZE_EXCEPTION_SIZE : maxSize;
86+
87+
var serialized = serializeObject(ex, depth);
88+
89+
if (jsonSize(stringify(serialized)) > maxSize) {
90+
return serializeException(ex, depth - 1);
91+
}
92+
93+
return serialized;
94+
}
95+
96+
module.exports.serializeException = serializeException;
97+
98+
function serializeKeysForMessage(keys, maxLength) {
99+
if (typeof keys === 'number' || typeof keys === 'string') return keys.toString();
100+
if (!Array.isArray(keys)) return '';
101+
102+
keys = keys.filter(function(key) {
103+
return typeof key === 'string';
104+
});
105+
if (keys.length === 0) return '[object has no keys]';
106+
107+
maxLength = typeof maxLength !== 'number' ? MAX_SERIALIZE_KEYS_LENGTH : maxLength;
108+
if (keys[0].length >= maxLength) return keys[0];
109+
110+
for (var usedKeys = keys.length; usedKeys > 0; usedKeys--) {
111+
var serialized = keys.slice(0, usedKeys).join(', ');
112+
if (serialized.length > maxLength) continue;
113+
if (usedKeys === keys.length) return serialized;
114+
return serialized + '\u2026';
115+
}
116+
117+
return '';
118+
}
119+
120+
module.exports.serializeKeysForMessage = serializeKeysForMessage;
121+
19122
module.exports.disableConsoleAlerts = function disableConsoleAlerts() {
20123
consoleAlerts = false;
21124
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"dependencies": {
3333
"cookie": "0.3.1",
3434
"lsmod": "1.0.0",
35+
"md5": "^2.2.1",
3536
"stack-trace": "0.0.9",
3637
"timed-out": "4.0.1",
3738
"uuid": "3.0.0"

test/raven.client.js

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
/* global Promise */
33
'use strict';
44

5+
var versionRegexp = /^v(\d+)\.(\d+)\.(\d+)$/i;
6+
var majorVersion = parseInt(versionRegexp.exec(process.version)[1], 10);
7+
58
var raven = require('../'),
69
nock = require('nock'),
710
url = require('url'),
@@ -246,6 +249,62 @@ describe('raven.Client', function() {
246249
client.captureException('wtf?');
247250
});
248251

252+
it('should serialize non-error exceptions', function(done) {
253+
var old = client.send;
254+
client.send = function mockSend(kwargs) {
255+
client.send = old;
256+
257+
kwargs.message.should.equal(
258+
'Non-Error exception captured with keys: aKeyOne, bKeyTwo, cKeyThree, dKeyFour\u2026'
259+
);
260+
261+
// Remove superfluous node version data to simplify the test itself
262+
delete kwargs.extra.node;
263+
kwargs.extra.should.have.property('__serialized__', {
264+
aKeyOne: 'a',
265+
bKeyTwo: 42,
266+
cKeyThree: {},
267+
dKeyFour: ['d'],
268+
eKeyFive: '[Function: foo]',
269+
fKeySix: {
270+
levelTwo: {
271+
levelThreeObject: '[Object]',
272+
levelThreeArray: '[Array]',
273+
// Node < 6 is not capable of pulling function name from unnamed object methods
274+
levelThreeAnonymousFunction:
275+
majorVersion < 6
276+
? '[Function]'
277+
: '[Function: levelThreeAnonymousFunction]',
278+
levelThreeNamedFunction: '[Function: bar]',
279+
levelThreeString: 'foo',
280+
levelThreeNumber: 42
281+
}
282+
}
283+
});
284+
285+
done();
286+
};
287+
client.captureException({
288+
aKeyOne: 'a',
289+
bKeyTwo: 42,
290+
cKeyThree: {},
291+
dKeyFour: ['d'],
292+
eKeyFive: function foo() {},
293+
fKeySix: {
294+
levelTwo: {
295+
levelThreeObject: {
296+
enough: 42
297+
},
298+
levelThreeArray: [42],
299+
levelThreeAnonymousFunction: function() {},
300+
levelThreeNamedFunction: function bar() {},
301+
levelThreeString: 'foo',
302+
levelThreeNumber: 42
303+
}
304+
}
305+
});
306+
});
307+
249308
it('should send an Error to Sentry server on another port', function(done) {
250309
var scope = nock('https://app.getsentry.com:8443')
251310
.filteringRequestBody(/.*/, '*')
@@ -380,19 +439,15 @@ describe('raven.Client', function() {
380439

381440
describe('exit conditions', function() {
382441
var exitStr = 'exit test assertions complete\n';
383-
it('should catch an uncaughtException and capture it before exiting', function(
384-
done
385-
) {
442+
it('should catch an uncaughtException and capture it before exiting', function(done) {
386443
child_process.exec('node test/exit/capture.js', function(err, stdout, stderr) {
387444
stdout.should.equal(exitStr);
388445
stderr.should.startWith('Error: derp');
389446
done();
390447
});
391448
});
392449

393-
it('should catch an uncaughtException and capture it before calling a provided callback', function(
394-
done
395-
) {
450+
it('should catch an uncaughtException and capture it before calling a provided callback', function(done) {
396451
child_process.exec('node test/exit/capture_callback.js', function(
397452
err,
398453
stdout,
@@ -405,9 +460,7 @@ describe('raven.Client', function() {
405460
});
406461
});
407462

408-
it('should catch an uncaughtException and capture it without a second followup exception causing premature shutdown', function(
409-
done
410-
) {
463+
it('should catch an uncaughtException and capture it without a second followup exception causing premature shutdown', function(done) {
411464
child_process.exec('node test/exit/capture_with_second_error.js', function(
412465
err,
413466
stdout,
@@ -419,9 +472,7 @@ describe('raven.Client', function() {
419472
});
420473
});
421474

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

450-
it('should catch a domain exception and capture it before calling a provided callback', function(
451-
done
452-
) {
501+
it('should catch a domain exception and capture it before calling a provided callback', function(done) {
453502
child_process.exec('node test/exit/domain_capture_callback.js', function(
454503
err,
455504
stdout,
@@ -462,9 +511,7 @@ describe('raven.Client', function() {
462511
});
463512
});
464513

465-
it('should catch a domain exception and capture it without a second followup exception causing premature shutdown', function(
466-
done
467-
) {
514+
it('should catch a domain exception and capture it without a second followup exception causing premature shutdown', function(done) {
468515
child_process.exec('node test/exit/domain_capture_with_second_error.js', function(
469516
err,
470517
stdout,
@@ -476,9 +523,7 @@ describe('raven.Client', function() {
476523
});
477524
});
478525

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

495-
it('should catch an uncaughtException and capture it without a second followup domain exception causing premature shutdown', function(
496-
done
497-
) {
540+
it('should catch an uncaughtException and capture it without a second followup domain exception causing premature shutdown', function(done) {
498541
child_process.exec('node test/exit/capture_with_second_domain_error.js', function(
499542
err,
500543
stdout,
@@ -506,9 +549,7 @@ describe('raven.Client', function() {
506549
});
507550
});
508551

509-
it('should catch an uncaughtException and capture it and failsafe shutdown if onFatalError throws', function(
510-
done
511-
) {
552+
it('should catch an uncaughtException and capture it and failsafe shutdown if onFatalError throws', function(done) {
512553
child_process.exec('node test/exit/throw_on_fatal.js', function(
513554
err,
514555
stdout,
@@ -579,9 +620,7 @@ describe('raven.Client', function() {
579620
);
580621
});
581622

582-
it('should pass original shouldSendCallback to newer shouldSendCallback', function(
583-
done
584-
) {
623+
it('should pass original shouldSendCallback to newer shouldSendCallback', function(done) {
585624
var cb1 = function(data) {
586625
return false;
587626
};

0 commit comments

Comments
 (0)