stackdriver-errors.js (133 lines of code) (raw):
/**
* 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;