-
Notifications
You must be signed in to change notification settings - Fork 54
/
stackdriver-errors.js
210 lines (190 loc) · 7.66 KB
/
stackdriver-errors.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var StackTrace = require('stacktrace-js');
/**
* URL endpoint of the Stackdriver Error Reporting report API.
*/
var baseAPIUrl = 'https://clouderrorreporting.googleapis.com/v1beta1/projects/';
/**
* An Error handler that sends errors to the Stackdriver Error Reporting API.
*/
var StackdriverErrorReporter = function() {};
/**
* Initialize the StackdriverErrorReporter object.
* @param {Object} config - the init configuration.
* @param {Object} [config.context={}] - the context in which the error occurred.
* @param {string} [config.context.user] - the user who caused or was affected by the error.
* @param {String} config.key - the API key to use to call the API.
* @param {String} config.projectId - the Google Cloud Platform project ID to report errors to.
* @param {Function} config.customReportingFunction - Custom function to be called with the error payload for reporting, instead of HTTP request. The function should return a Promise.
* @param {String} [config.service=web] - service identifier.
* @param {String} [config.version] - version identifier.
* @param {Boolean} [config.reportUncaughtExceptions=true] - Set to false to stop reporting unhandled exceptions.
* @param {Boolean} [config.disabled=false] - Set to true to not report errors when calling report(), this can be used when developping locally.
*/
StackdriverErrorReporter.prototype.start = function(config) {
if (!config.key && !config.targetUrl && !config.customReportingFunction) {
throw new Error('Cannot initialize: No API key, target url or custom reporting function provided.');
}
if (!config.projectId && !config.targetUrl && !config.customReportingFunction) {
throw new Error('Cannot initialize: No project ID, target url or custom reporting function provided.');
}
this.customReportingFunction = config.customReportingFunction;
this.apiKey = config.key;
this.projectId = config.projectId;
this.targetUrl = config.targetUrl;
this.context = config.context || {};
this.serviceContext = {service: config.service || 'web'};
if (config.version) {
this.serviceContext.version = config.version;
}
this.reportUncaughtExceptions = config.reportUncaughtExceptions !== false;
this.reportUnhandledPromiseRejections = config.reportUnhandledPromiseRejections !== false;
this.disabled = !!config.disabled;
registerHandlers(this);
};
function registerHandlers(reporter) {
// Register as global error handler if requested
var noop = function() {};
if (reporter.reportUncaughtExceptions) {
var oldErrorHandler = window.onerror || noop;
window.onerror = function(message, source, lineno, colno, error) {
if (error) {
reporter.report(error).catch(noop);
}
oldErrorHandler(message, source, lineno, colno, error);
return true;
};
}
if (reporter.reportUnhandledPromiseRejections) {
var oldPromiseRejectionHandler = window.onunhandledrejection || noop;
window.onunhandledrejection = function(promiseRejectionEvent) {
if (promiseRejectionEvent) {
reporter.report(promiseRejectionEvent.reason).catch(noop);
}
oldPromiseRejectionHandler(promiseRejectionEvent);
return true;
};
}
}
/**
* Report an error to the Stackdriver Error Reporting API
* @param {Error|String} err - The Error object or message string to report.
* @param {Object} options - Configuration for this report.
* @param {number} [options.skipLocalFrames=1] - Omit number of frames if creating stack.
* @returns {Promise} A promise that completes when the report has been sent.
*/
StackdriverErrorReporter.prototype.report = function(err, options) {
if (this.disabled) {
return Promise.resolve(null);
}
if (!err) {
return Promise.reject(new Error('no error to report'));
}
options = options || {};
var payload = {};
payload.serviceContext = this.serviceContext;
payload.context = this.context;
payload.context.httpRequest = {
userAgent: window.navigator.userAgent,
url: window.location.href,
};
var firstFrameIndex = 0;
if (typeof err == 'string' || err instanceof String) {
// Transform the message in an error, use try/catch to make sure the stacktrace is populated.
try {
throw new Error(err);
} catch (e) {
err = e;
}
// the first frame when using report() is always this library
firstFrameIndex = options.skipLocalFrames || 1;
}
var reportUrl = this.targetUrl || (
baseAPIUrl + this.projectId + '/events:report?key=' + this.apiKey);
var customFunc = this.customReportingFunction;
return resolveError(err, firstFrameIndex)
.then(function(message) {
payload.message = message;
if (customFunc) {
return customFunc(payload);
}
return sendErrorPayload(reportUrl, payload);
});
};
function resolveError(err, firstFrameIndex) {
// This will use sourcemaps and normalize the stack frames
return StackTrace.fromError(err).then(function(stack) {
var lines = [err.toString()];
// Reconstruct to a JS stackframe as expected by Error Reporting parsers.
for (var s = firstFrameIndex; s < stack.length; s++) {
// Cannot use stack[s].source as it is not populated from source maps.
lines.push([
' at ',
// If a function name is not available '<anonymous>' will be used.
stack[s].getFunctionName() || '<anonymous>', ' (',
stack[s].getFileName(), ':',
stack[s].getLineNumber(), ':',
stack[s].getColumnNumber(), ')',
].join(''));
}
return lines.join('\n');
}, function(reason) {
// Failure to extract stacktrace
return [
'Error extracting stack trace: ', reason, '\n',
err.toString(), '\n',
' (', err.file, ':', err.line, ':', err.column, ')',
].join('');
});
}
function sendErrorPayload(url, payload) {
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
return new Promise(function(resolve, reject) {
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var code = xhr.status;
if (code >= 200 && code < 300) {
resolve({message: payload.message});
} else if (code === 429) {
// HTTP 429 responses are returned by Stackdriver when API quota
// is exceeded. We should not try to reject these as unhandled errors
// or we may cause an infinite loop with 'reportUncaughtExceptions'.
reject(
{
message: 'quota or rate limiting error on stackdriver report',
name: 'Http429FakeError',
});
} else {
var condition = code ? code + ' http response' : 'network error';
reject(new Error(condition + ' on stackdriver report'));
}
}
};
xhr.send(JSON.stringify(payload));
});
}
/**
* Set the user for the current context.
*
* @param {string} user - the unique identifier of the user (can be ID, email or custom token) or `undefined` if not logged in.
*/
StackdriverErrorReporter.prototype.setUser = function(user) {
this.context.user = user;
};
module.exports = StackdriverErrorReporter;