Skip to content

Commit 4029a11

Browse files
DominicKramerstephenplusplus
authored andcommitted
errors-reporting: Unhandled rejections are reported (#2360)
1 parent b44753d commit 4029a11

File tree

5 files changed

+181
-34
lines changed

5 files changed

+181
-34
lines changed

README.md

Lines changed: 12 additions & 1 deletion
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 not reported by default. The reporting of unhandled rejections can be enabled 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-
*It is recommended to catch `uncaughtExceptions` 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. Adding such a listener without knowledge of other `uncaughtException` listeners can cause interference between the event handlers or prevent the process from terminating cleanly. As such, it is necessary for `uncaughtException`s to be reported manually.
7078

7179
```js
7280
var errors = require('@google-cloud/error-reporting')();
@@ -156,6 +164,9 @@ var errors = require('@google-cloud/error-reporting')({
156164
// should be reported
157165
// defaults to 2 (warnings)
158166
logLevel: 2,
167+
// determines whether or not unhandled rejections are reported to the
168+
// error-reporting console
169+
reportUnhandledRejections: true,
159170
serviceContext: {
160171
service: 'my-service',
161172
version: 'my-service-version'

src/configuration.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@ var Configuration = function(givenConfig, logger) {
134134
* @type {Object}
135135
*/
136136
this._serviceContext = {service: 'nodejs', version: ''};
137+
/**
138+
* The _reportUnhandledRejections property is meant to specify whether or
139+
* not unhandled rejections should be reported to the error-reporting console.
140+
* @memberof Configuration
141+
* @private
142+
* @type {Boolean}
143+
*/
144+
this._reportUnhandledRejections = false;
137145
/**
138146
* The _givenConfiguration property holds a ConfigurationOptions object
139147
* which, if valid, will be merged against by the values taken from the meta-
@@ -257,6 +265,12 @@ Configuration.prototype._gatherLocalConfiguration = function() {
257265
} else if (has(this._givenConfiguration, 'credentials')) {
258266
throw new Error('config.credentials must be a valid credentials object');
259267
}
268+
if (isBoolean(this._givenConfiguration.reportUnhandledRejections)) {
269+
this._reportUnhandledRejections =
270+
this._givenConfiguration.reportUnhandledRejections;
271+
} else if (has(this._givenConfiguration, 'reportUnhandledRejections')) {
272+
throw new Error('config.reportUnhandledRejections must be a boolean');
273+
}
260274
};
261275
/**
262276
* The _checkLocalProjectId function is responsible for determing whether the
@@ -354,4 +368,14 @@ Configuration.prototype.getCredentials = function() {
354368
Configuration.prototype.getServiceContext = function() {
355369
return this._serviceContext;
356370
};
371+
/**
372+
* Returns the _reportUnhandledRejections property on the instance.
373+
* @memberof Configuration
374+
* @public
375+
* @function getReportUnhandledRejections
376+
* @returns {Boolean} - returns the _reportUnhandledRejections property
377+
*/
378+
Configuration.prototype.getReportUnhandledRejections = function() {
379+
return this._reportUnhandledRejections;
380+
};
357381
module.exports = Configuration;

src/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ 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 ' +
164+
'Google Cloud Platform error-reporting console.');
165+
that.report(reason);
166+
});
167+
}
157168
}
158169

159170
module.exports = Errors;

system-test/testAuthClient.js

Lines changed: 111 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,44 @@ 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 ' +
516+
'Google Cloud Platform error-reporting console.';
517+
assert.notStrictEqual(logOutput.indexOf(expected), -1);
518+
verifyServerResponse(function(message) {
519+
return message.startsWith(rejectValue);
520+
}, TIMEOUT, done);
521+
});
522+
});
523+
524+
it('Should not report unhandledRejections if disabled', function(done) {
525+
this.timeout(TIMEOUT * 2);
526+
reinitialize({ reportUnhandledRejections: false });
527+
var rejectValue = buildName('promise-rejection');
528+
Promise.reject(rejectValue);
529+
setImmediate(function() {
530+
var notExpected = 'UnhandledPromiseRejectionWarning: Unhandled ' +
531+
'promise rejection: ' + rejectValue +
532+
'. This rejection has been reported to the error-reporting console.';
533+
assert.strictEqual(logOutput.indexOf(notExpected), -1);
534+
// Get all groups that that start with the rejection value and hence all
535+
// of the groups corresponding to the above rejection (Since the
536+
// buildName() creates a string unique enough to single out only the
537+
// above rejection.) and verify that there are no such groups reported.
538+
verifyAllGroups(function(message) {
539+
return message.startsWith(rejectValue);
540+
}, TIMEOUT, function(matchedErrors) {
541+
assert.strictEqual(matchedErrors.length, 0);
542+
done();
543+
});
544+
});
545+
});
468546
});

test/unit/configuration.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ describe('Configuration class', function() {
9292
assert.deepEqual(c.getServiceContext(),
9393
{service: 'node', version: undefined});
9494
});
95+
it('Should specify to not report unhandledRejections', function() {
96+
assert.strictEqual(c.getReportUnhandledRejections(), false);
97+
});
9598
});
9699
describe('with ignoreEnvironmentCheck', function() {
97100
var conf = merge({}, stubConfig, {ignoreEnvironmentCheck: true});
@@ -134,6 +137,13 @@ describe('Configuration class', function() {
134137
new Configuration({serviceContext: {version: true}}, logger);
135138
});
136139
});
140+
it('Should throw if invalid for reportUnhandledRejections',
141+
function() {
142+
assert.throws(function() {
143+
new Configuration({ reportUnhandledRejections: 'INVALID' },
144+
logger);
145+
});
146+
});
137147
it('Should not throw given an empty object for serviceContext',
138148
function() {
139149
assert.doesNotThrow(function() {
@@ -278,6 +288,19 @@ describe('Configuration class', function() {
278288
assert.strictEqual(c.getKey(), key);
279289
});
280290
});
291+
describe('reportUnhandledRejections', function() {
292+
var c;
293+
var reportRejections = false;
294+
before(function() {
295+
c = new Configuration({
296+
reportUnhandledRejections: reportRejections
297+
});
298+
});
299+
it('Should assign', function() {
300+
assert.strictEqual(c.getReportUnhandledRejections(),
301+
reportRejections);
302+
});
303+
});
281304
});
282305
});
283306
});

0 commit comments

Comments
 (0)