Skip to content

Commit e4b3aff

Browse files
bz2steren
authored andcommitted
Return a Promise from report method (#50)
* Return a Promise from report method Remove the callback argument from StackdriverErrorReporter report() and sendErrorPayload() and instead return a Promise that resolves once both the stacktrace is generated and the XHR request completes. Update tests to return promises to the framework rather than use the done() callback where possible. Suppress promise rejections in for top level error handlers to avoid potential loops. * Add new tests for XHR errors These reflect current behaviour and should be updated when the interface is more clearly defined.
1 parent 9112c19 commit e4b3aff

2 files changed

Lines changed: 65 additions & 42 deletions

File tree

stackdriver-errors.js

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,24 +64,25 @@
6464
this.disabled = config.disabled || false;
6565

6666
// Register as global error handler if requested
67+
var noop = function() {};
6768
var that = this;
6869
if(this.reportUncaughtExceptions) {
69-
var oldErrorHandler = window.onerror || function(){};
70+
var oldErrorHandler = window.onerror || noop;
7071

7172
window.onerror = function(message, source, lineno, colno, error) {
7273
if(error){
73-
that.report(error);
74+
that.report(error).catch(noop);
7475
}
7576
oldErrorHandler(message, source, lineno, colno, error);
7677
return true;
7778
};
7879
}
7980
if(this.reportUnhandledPromiseRejections) {
80-
var oldPromiseRejectionHandler = window.onunhandledrejection || function(){};
81+
var oldPromiseRejectionHandler = window.onunhandledrejection || noop;
8182

8283
window.onunhandledrejection = function(promiseRejectionEvent) {
8384
if(promiseRejectionEvent){
84-
that.report(promiseRejectionEvent.reason);
85+
that.report(promiseRejectionEvent.reason).catch(noop);
8586
}
8687
oldPromiseRejectionHandler(promiseRejectionEvent.reason);
8788
return true;
@@ -92,14 +93,14 @@
9293
/**
9394
* Report an error to the Stackdriver Error Reporting API
9495
* @param {Error|String} err - The Error object or message string to report.
95-
* @param callback - Calback function to be called once error has been reported.
96+
* @returns {Promise} A promise that completes when the report has been sent.
9697
*/
97-
StackdriverErrorReporter.prototype.report = function(err, callback) {
98+
StackdriverErrorReporter.prototype.report = function(err) {
9899
if(this.disabled) {
99-
return typeof callback === 'function' && callback();
100+
return Promise.resolve(null);
100101
}
101102
if(!err) {
102-
return typeof callback === 'function' && callback('no error to report');
103+
return Promise.reject(new Error('no error to report'));
103104
}
104105

105106
var payload = {};
@@ -123,7 +124,7 @@
123124
}
124125
var that = this;
125126
// This will use sourcemaps and normalize the stack frames
126-
StackTrace.fromError(err).then(function(stack){
127+
return StackTrace.fromError(err).then(function(stack){
127128
payload.message = err.toString();
128129
for(var s = firstFrameIndex; s < stack.length; s++) {
129130
payload.message += '\n';
@@ -133,32 +134,31 @@
133134
// If functionName or methodName isn't available <anonymous> will be used as the name.
134135
payload.message += [' at ', stack[s].getFunctionName() || '<anonymous>', ' (', stack[s].getFileName(), ':', stack[s].getLineNumber() ,':', stack[s].getColumnNumber() , ')'].join('');
135136
}
136-
that.sendErrorPayload(payload, callback);
137+
return that.sendErrorPayload(payload);
137138
}, function(reason) {
138139
// Failure to extract stacktrace
139140
payload.message = [
140141
'Error extracting stack trace: ', reason, '\n',
141142
err.toString(), '\n',
142143
' (', err.file, ':', err.line, ':', err.column, ')',
143144
].join('');
144-
that.sendErrorPayload(payload, callback);
145+
return that.sendErrorPayload(payload);
145146
});
146147
};
147148

148-
StackdriverErrorReporter.prototype.sendErrorPayload = function(payload, callback) {
149+
StackdriverErrorReporter.prototype.sendErrorPayload = function(payload) {
149150
var defaultUrl = baseAPIUrl + this.projectId + "/events:report?key=" + this.apiKey;
150151
var url = this.targetUrl || defaultUrl;
151152

152153
var xhr = new XMLHttpRequest();
153154
xhr.open('POST', url, true);
154155
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
155-
xhr.onloadend = function() {
156-
return typeof callback === 'function' && callback();
157-
};
158-
xhr.onerror = function(e) {
159-
return typeof callback === 'function' && callback(e);
160-
};
161-
xhr.send(JSON.stringify(payload));
156+
157+
return new Promise(function(resolve, reject) {
158+
xhr.onloadend = resolve;
159+
xhr.onerror = reject;
160+
xhr.send(JSON.stringify(payload));
161+
});
162162
};
163163

164164
/**

test/test.js

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
var expect = chai.expect;
1717

1818
var errorHandler;
19-
var xhr, requests;
19+
var xhr, requests, requestHandler;
2020
var WAIT_FOR_STACKTRACE_FROMERROR = 15;
2121

2222
/**
@@ -49,13 +49,16 @@ beforeEach(function() {
4949
});
5050

5151
requests = [];
52+
requestHandler = function (req) {
53+
req.respond(200, {"Content-Type": "application/json"}, '{}');
54+
}
5255
xhr.onCreate = function (req) {
5356
// Allow `onCreate` to complete so `xhr` can finish instantiating.
5457
setTimeout(function(){
5558
if(req.url.match('clouderrorreporting')) {
5659
requests.push(req);
5760
}
58-
req.respond(200, {"Content-Type": "application/json"}, '{}');
61+
requestHandler(req);
5962
}, 1);
6063
};
6164
});
@@ -109,12 +112,11 @@ describe('Initialization', function () {
109112

110113
describe('Disabling', function () {
111114

112-
it('should not report errors if disabled', function (done) {
115+
it('should not report errors if disabled', function () {
113116
errorHandler.start({key:'key', projectId:'projectId', disabled: true});
114-
errorHandler.report('do not report', function() {
115-
expect(requests.length).to.equal(0);
116-
done();
117-
});
117+
return errorHandler.report('do not report').then(function() {
118+
expect(requests.length).to.equal(0);
119+
});
118120
});
119121

120122
});
@@ -125,67 +127,88 @@ describe('Reporting errors', function () {
125127
errorHandler.start({key:'key', projectId:'projectId'});
126128
});
127129

128-
it('should report error messages with location', function (done) {
130+
it('should report error messages with location', function () {
129131
var message = 'Something broke!';
130-
errorHandler.report(message, function() {
132+
return errorHandler.report(message).then(function() {
131133
expectRequestWithMessage(message);
132-
done();
133134
});
134135
});
135136

136-
it('should extract and send stack traces from Errors', function (done) {
137+
it('should extract and send stack traces from Errors', function () {
137138
var message = 'custom message';
138139
// PhantomJS only attaches a stack to thrown errors
139140
try {
140141
throw new TypeError(message);
141142
} catch(e) {
142-
errorHandler.report(e, function() {
143+
return errorHandler.report(e).then(function() {
143144
expectRequestWithMessage(message);
144-
done();
145145
});
146146
}
147147
});
148148

149-
it('should extract and send functionName in stack traces', function (done) {
149+
it('should extract and send functionName in stack traces', function () {
150150
var message = 'custom message';
151151
// PhantomJS only attaches a stack to thrown errors
152152
try {
153153
throwError(message)
154154
} catch(e) {
155-
errorHandler.report(e, function() {
155+
return errorHandler.report(e).then(function() {
156156
expectRequestWithMessage('throwError');
157-
done();
158157
});
159158
}
160159
});
161160

162-
it('should set in stack traces when frame is anonymous', function (done) {
161+
it('should set in stack traces when frame is anonymous', function () {
163162
var message = 'custom message';
164163
// PhantomJS only attaches a stack to thrown errors
165164
try {
166165
(function () {
167166
throw new TypeError(message);
168167
})()
169168
} catch(e) {
170-
errorHandler.report(e, function() {
169+
return errorHandler.report(e).then(function() {
171170
expectRequestWithMessage('<anonymous>');
172-
done();
173171
});
174172
}
175173
});
174+
175+
describe('XHR error handling', function() {
176+
it('should handle network error', function () {
177+
requestHandler = function (req) {
178+
req.error();
179+
};
180+
var message = 'News that will fail to send';
181+
return errorHandler.report(message).then(function() {
182+
throw new Error('unexpected fulfilled report');
183+
}, function(e) {
184+
expectRequestWithMessage(message);
185+
// TODO: Expose a tidied up error object
186+
expect(e.target.status).to.equal(0);
187+
});
188+
});
189+
190+
it('should handle http error', function () {
191+
requestHandler = function (req) {
192+
req.respond(503, {"Content-Type": "text/plain"}, '');
193+
};
194+
errorHandler.start({key:'key', projectId:'projectId'});
195+
var message = 'News that was rejected on send';
196+
return errorHandler.report(message).then(function() {
197+
expectRequestWithMessage(message);
198+
});
199+
});
200+
});
176201
});
177202

178203
describe('Custom target url configuration', function() {
179-
it('should report error messages with custom url config', function (done) {
204+
it('should report error messages with custom url config', function () {
180205
var targetUrl = 'config-uri-clouderrorreporting';
181206
errorHandler.start({targetUrl:targetUrl});
182207

183208
var message = 'Something broke!';
184-
errorHandler.report(message, function() {
209+
return errorHandler.report(message).then(function() {
185210
expectRequestWithMessage(message);
186211
expect(requests[0].url).to.equal(targetUrl);
187-
188-
done();
189212
});
190213
});
191214
});

0 commit comments

Comments
 (0)