Skip to content

Commit 4ea2de2

Browse files
committed
error-reporting: Unhandled rejections are reported
Now unhandled rejections are reported, by default, to the error reporting console. The `reportUnhandledRejections` configuration option can be used to disable the reporting of unhandled rejections.
1 parent d8d24a1 commit 4ea2de2

5 files changed

Lines changed: 180 additions & 39 deletions

File tree

packages/error-reporting/README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,17 @@ errors.report(new Error('Something broke!'));
6464

6565
Open Stackdriver Error Reporting at https://console.cloud.google.com/errors to view the reported errors.
6666

67+
## Unhandled Rejections
68+
69+
Unhandled Rejections are reported by default. The reporting of unhandled rejections can be disabled using the `reportUnhandledRejections` configuration option. See the [Configuration](#configuration) section for more details.
70+
71+
If unhandled rejections are set to be reported, then, when an unhandled rejection occurs, a message is printed to standard out indicated that an unhandled rejection had occurred and is being reported, and the value causing the rejection is reported to the error-reporting console.
72+
6773
## Catching and Reporting Application-wide Uncaught Errors
6874

69-
Uncaught exceptions and unhandled rejections are not reported by default. *It is recommended to process `uncaughtException`s and `unhandledRejection`s for production-deployed applications.*
75+
Uncaught exceptions are not reported by default. *It is recommended to process `uncaughtException`s for production-deployed applications.*
76+
77+
Note that uncaught exceptions are not reported by default because to do so would require adding a listener to the `uncaughtException` event. However, whether or not, and if so how, the addition of such a listener influences the execution of an application is specific to that particular application. As such, it is necessary for `uncaughtException`s to be reported manually.
7078

7179
```js
7280
var errors = require('@google-cloud/error-reporting')();
@@ -76,13 +84,9 @@ process.on('uncaughtException', (e) => {
7684
// Report that same error the Stackdriver Error Service
7785
errors.report(e);
7886
});
79-
80-
process.on('unhandledRejection', (reason, p) => {
81-
errors.report('Unhandled rejection of promise: ' + p + ', reason: ' + reason);
82-
});
8387
```
8488

85-
More information on uncaught exception handling in Node.js can be found [here](https://nodejs.org/api/process.html#process_event_uncaughtexception), and more information on unhandled promise handling can be found [here](https://nodejs.org/api/process.html#process_event_unhandledrejection).
89+
More information about uncaught exception handling in Node.js and what it means for your application can be found [here](https://nodejs.org/api/process.html#process_event_uncaughtexception).
8690

8791
## Running on Google Cloud Platform
8892

@@ -149,6 +153,9 @@ var errors = require('@google-cloud/error-reporting')({
149153
// should be reported
150154
// defaults to 2 (warnings)
151155
logLevel: 2,
156+
// determines whether or not unhandled rejections are reported to the
157+
// error-reporting console. The default value of this property is true.
158+
reportUnhandledRejections: false,
152159
serviceContext: {
153160
service: 'my-service',
154161
version: 'my-service-version'

packages/error-reporting/src/configuration.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ var Configuration = function(givenConfig, logger) {
135135
* @type {Object}
136136
*/
137137
this._serviceContext = {service: 'nodejs', version: ''};
138+
/**
139+
* The _reportUnhandledRejections property is meant to specify whether or
140+
* not unhandled rejections should be reported to the error-reporting console.
141+
* @memberof Configuration
142+
* @private
143+
* @type {Boolean}
144+
*/
145+
this._reportUnhandledRejections = true;
138146
/**
139147
* The _version of the Error reporting library that is currently being run.
140148
* This information will be logged in errors communicated to the Stackdriver
@@ -267,6 +275,12 @@ Configuration.prototype._gatherLocalConfiguration = function() {
267275
} else if (has(this._givenConfiguration, 'credentials')) {
268276
throw new Error('config.credentials must be a valid credentials object');
269277
}
278+
if (isBoolean(this._givenConfiguration.reportUnhandledRejections)) {
279+
this._reportUnhandledRejections =
280+
this._givenConfiguration.reportUnhandledRejections;
281+
} else if (has(this._givenConfiguration, 'reportUnhandledRejections')) {
282+
throw new Error('config.reportUnhandledRejections must be a boolean');
283+
}
270284
};
271285
/**
272286
* The _checkLocalProjectId function is responsible for determing whether the
@@ -374,4 +388,14 @@ Configuration.prototype.getServiceContext = function() {
374388
Configuration.prototype.getVersion = function() {
375389
return this._version;
376390
};
391+
/**
392+
* Returns the _reportUnhandledRejections property on the instance.
393+
* @memberof Configuration
394+
* @public
395+
* @function getReportUnhandledRejections
396+
* @returns {Boolean} - returns the _reportUnhandledRejections property
397+
*/
398+
Configuration.prototype.getReportUnhandledRejections = function() {
399+
return this._reportUnhandledRejections;
400+
};
377401
module.exports = Configuration;

packages/error-reporting/src/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ function Errors(initConfiguration) {
154154
* app.use(errors.koa);
155155
*/
156156
this.koa = koa(this._client, this._config);
157+
158+
if (this._config.getReportUnhandledRejections()) {
159+
var that = this;
160+
process.on('unhandledRejection', function(reason) {
161+
console.log('UnhandledPromiseRejectionWarning: ' +
162+
'Unhandled promise rejection: ' + reason +
163+
'. This rejection has been reported to the error-reporting console.');
164+
that.report(reason);
165+
});
166+
}
157167
}
158168

159169
module.exports = Errors;

packages/error-reporting/system-test/testAuthClient.js

Lines changed: 110 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ var forEach = require('lodash.foreach');
3131
var assign = require('lodash.assign');
3232
var pick = require('lodash.pick');
3333
var omitBy = require('lodash.omitby');
34+
var util = require('util');
3435

3536
const ERR_TOKEN = '_@google_STACKDRIVER_INTEGRATION_TEST_ERROR__';
36-
const TIMEOUT = 20000;
37+
const TIMEOUT = 30000;
3738

3839
const envKeys = ['GOOGLE_APPLICATION_CREDENTIALS', 'GCLOUD_PROJECT',
3940
'NODE_ENV'];
@@ -371,54 +372,91 @@ describe('error-reporting', function() {
371372

372373
var errors;
373374
var transport;
375+
var oldLogger;
376+
var logOutput = '';
374377
before(function() {
375-
errors = require('../src/index.js')({
378+
// This test assumes that only the error-reporting library will be
379+
// adding listeners to the 'unhandledRejection' event. Thus we need to
380+
// make sure that no listeners for that event exist. If this check
381+
// fails, then the reinitialize() method below will need to updated to
382+
// more carefully reinitialize the error-reporting library without
383+
// interfering with existing listeners of the 'unhandledRejection' event.
384+
assert.strictEqual(process.listenerCount('unhandledRejection'), 0);
385+
oldLogger = console.log;
386+
console.log = function() {
387+
var text = util.format.apply(null, arguments);
388+
oldLogger(text);
389+
logOutput += text;
390+
};
391+
reinitialize();
392+
});
393+
394+
function reinitialize(extraConfig) {
395+
process.removeAllListeners('unhandledRejection');
396+
var config = Object.assign({
376397
ignoreEnvironmentCheck: true,
377398
serviceContext: {
378399
service: SERVICE_NAME,
379400
version: SERVICE_VERSION
380401
}
381-
});
402+
}, extraConfig || {});
403+
errors = require('../src/index.js')(config);
382404
transport = new ErrorsApiTransport(errors._config, errors._logger);
383-
});
405+
}
384406

385407
after(function(done) {
386-
transport.deleteAllEvents(function(err) {
387-
assert.ifError(err);
388-
done();
389-
});
408+
console.log = oldLogger;
409+
if (transport) {
410+
transport.deleteAllEvents(function(err) {
411+
assert.ifError(err);
412+
done();
413+
});
414+
}
415+
});
416+
417+
afterEach(function() {
418+
logOutput = '';
390419
});
391420

421+
function verifyAllGroups(messageTest, timeout, cb) {
422+
setTimeout(function() {
423+
transport.getAllGroups(function(err, groups) {
424+
assert.ifError(err);
425+
assert.ok(groups);
426+
427+
var matchedErrors = groups.filter(function(errItem) {
428+
return errItem && errItem.representative &&
429+
messageTest(errItem.representative.message);
430+
});
431+
432+
cb(matchedErrors);
433+
});
434+
}, timeout);
435+
}
436+
437+
function verifyServerResponse(messageTest, timeout, cb) {
438+
verifyAllGroups(messageTest, timeout, function(matchedErrors) {
439+
// The error should have been reported exactly once
440+
assert.strictEqual(matchedErrors.length, 1);
441+
var errItem = matchedErrors[0];
442+
assert.ok(errItem);
443+
assert.equal(errItem.count, 1);
444+
var rep = errItem.representative;
445+
assert.ok(rep);
446+
var context = rep.serviceContext;
447+
assert.ok(context);
448+
assert.strictEqual(context.service, SERVICE_NAME);
449+
assert.strictEqual(context.version, SERVICE_VERSION);
450+
cb();
451+
});
452+
}
453+
392454
function verifyReporting(errOb, messageTest, timeout, cb) {
393455
errors.report(errOb, function(err, response, body) {
394456
assert.ifError(err);
395457
assert(isObject(response));
396458
assert.deepEqual(body, {});
397-
398-
setTimeout(function() {
399-
transport.getAllGroups(function(err, groups) {
400-
assert.ifError(err);
401-
assert.ok(groups);
402-
403-
var matchedErrors = groups.filter(function(errItem) {
404-
return errItem && errItem.representative &&
405-
messageTest(errItem.representative.message);
406-
});
407-
408-
// The error should have been reported exactly once
409-
assert.strictEqual(matchedErrors.length, 1);
410-
var errItem = matchedErrors[0];
411-
assert.ok(errItem);
412-
assert.equal(errItem.count, 1);
413-
var rep = errItem.representative;
414-
assert.ok(rep);
415-
var context = rep.serviceContext;
416-
assert.ok(context);
417-
assert.strictEqual(context.service, SERVICE_NAME);
418-
assert.strictEqual(context.version, SERVICE_VERSION);
419-
cb();
420-
});
421-
}, timeout);
459+
verifyServerResponse(messageTest, timeout, cb);
422460
});
423461
}
424462

@@ -465,4 +503,43 @@ describe('error-reporting', function() {
465503
}, TIMEOUT, done);
466504
})();
467505
});
506+
507+
it('Should report unhandledRejections if enabled', function(done) {
508+
this.timeout(TIMEOUT * 2);
509+
reinitialize({ reportUnhandledRejections: true });
510+
var rejectValue = buildName('promise-rejection');
511+
Promise.reject(rejectValue);
512+
setImmediate(function() {
513+
var expected = 'UnhandledPromiseRejectionWarning: Unhandled ' +
514+
'promise rejection: ' + rejectValue +
515+
'. This rejection has been reported to the error-reporting console.';
516+
assert.notStrictEqual(logOutput.indexOf(expected), -1);
517+
verifyServerResponse(function(message) {
518+
return message.startsWith(rejectValue);
519+
}, TIMEOUT, done);
520+
});
521+
});
522+
523+
it('Should not report unhandledRejections if disabled', function(done) {
524+
this.timeout(TIMEOUT * 2);
525+
reinitialize({ reportUnhandledRejections: false });
526+
var rejectValue = buildName('promise-rejection');
527+
Promise.reject(rejectValue);
528+
setImmediate(function() {
529+
var notExpected = 'UnhandledPromiseRejectionWarning: Unhandled ' +
530+
'promise rejection: ' + rejectValue +
531+
'. This rejection has been reported to the error-reporting console.';
532+
assert.strictEqual(logOutput.indexOf(notExpected), -1);
533+
// Get all groups that that start with the rejection value and hence all
534+
// of the groups corresponding to the above rejection (Since the
535+
// buildName() creates a string unique enough to single out only the
536+
// above rejection.) and verify that there are no such groups reported.
537+
verifyAllGroups(function(message) {
538+
return message.startsWith(rejectValue);
539+
}, TIMEOUT, function(matchedErrors) {
540+
assert.strictEqual(matchedErrors.length, 0);
541+
done();
542+
});
543+
});
544+
});
468545
});

packages/error-reporting/test/unit/configuration.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ describe('Configuration class', function() {
9696
it('Should have a version corresponding to package.json', function() {
9797
assert.strictEqual(c.getVersion(), version);
9898
});
99+
it('Should specify to report unhandledRejections', function() {
100+
assert.strictEqual(c.getReportUnhandledRejections(), true);
101+
});
99102
});
100103
describe('with ignoreEnvironmentCheck', function() {
101104
var conf = merge({}, stubConfig, {ignoreEnvironmentCheck: true});
@@ -138,6 +141,13 @@ describe('Configuration class', function() {
138141
new Configuration({serviceContext: {version: true}}, logger);
139142
});
140143
});
144+
it('Should throw if invalid for reportUnhandledRejections',
145+
function() {
146+
assert.throws(function() {
147+
new Configuration({ reportUnhandledRejections: 'INVALID' },
148+
logger);
149+
});
150+
});
141151
it('Should not throw given an empty object for serviceContext',
142152
function() {
143153
assert.doesNotThrow(function() {
@@ -282,6 +292,19 @@ describe('Configuration class', function() {
282292
assert.strictEqual(c.getKey(), key);
283293
});
284294
});
295+
describe('reportUnhandledRejections', function() {
296+
var c;
297+
var reportRejections = false;
298+
before(function() {
299+
c = new Configuration({
300+
reportUnhandledRejections: reportRejections
301+
});
302+
});
303+
it('Should assign', function() {
304+
assert.strictEqual(c.getReportUnhandledRejections(),
305+
reportRejections);
306+
});
307+
});
285308
});
286309
});
287310
});

0 commit comments

Comments
 (0)