airbrake
Version:
Node.js client for airbrakeapp.com, formerly hoptoad.
265 lines (218 loc) • 6.11 kB
JavaScript
var HTTP_STATUS_CODES = require('http').STATUS_CODES;
var fs = require('fs');
var os = require('os');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var request = require('request');
var xmlbuilder = require('xmlbuilder');
var stackTrace = require('stack-trace');
var traverse = require('traverse');
module.exports = Airbrake;
util.inherits(Airbrake, EventEmitter);
function Airbrake() {
this.key = null;
this.env = process.env.NODE_ENV;
this.projectRoot = null;
this.appVersion = null;
this.timeout = 30 * 1000;
// Disabled, see comments in appendServerEnvironmentXml()
// this.hostname = os.hostname();
this.protocol = 'http';
this.host = 'airbrakeapp.com';
}
Airbrake.PACKAGE = (function() {
var json = fs.readFileSync(__dirname + '/../package.json', 'utf8');
return JSON.parse(json);
})();
Airbrake.createClient = function(key, env) {
var instance = new this();
instance.key = key;
instance.env = env || instance.env;
return instance;
};
Airbrake.prototype.handleExceptions = function() {
var self = this;
process.on('uncaughtException', function(err) {
self.notify(err, function(notifyErr, url) {
if (notifyErr) {
self.log('Uncaught Exception: Could not notify Airbreak!');
self.log(notifyErr.stack);
} else {
self.log('Uncaught Exception: Airbreak was notified: ' + url);
}
self.log(err.stack);
process.exit(1);
});
});
};
Airbrake.prototype.log = function(str) {
console.error(str);
};
Airbrake.prototype.notify = function(err, cb) {
var body = this.notifyXml(err);
var options = {
method: 'POST',
url: this.url('/notifier_api/v2/notices'),
body: body,
timeout: this.timeout,
headers: {
'Content-Length': body.length,
'Content-Type': 'text/xml',
},
};
var self = this;
var callback = function(err) {
if (cb) {
cb.apply(self, arguments);
return;
}
if (err) {
self.emit('error', err);
}
};
request(options, function(err, res, body) {
if (err) {
return callback(err);
}
if (res.statusCode >= 300) {
var status = HTTP_STATUS_CODES[res.statusCode];
return callback(new Error(
'Notification failed: ' + res.statusCode + ' ' + status
));
}
// Give me a break, this is legit : )
var m = body.match(/<url>([^<]+)/i);
var url = (m)
? m[1]
: null;
callback(null, url);
});
};
Airbrake.prototype.url = function(path) {
return this.protocol + '://' + this.host + path;
};
Airbrake.prototype.notifyXml = function(err, pretty) {
var notice = xmlbuilder().begin('notice', {
version: '1.0',
encoding: 'UTF-8'
});
this.appendHeaderXml(notice);
this.appendErrorXml(notice, err);
this.appendRequestXml(notice, err);
this.appendServerEnvironmentXml(notice);
return notice.up().toString({pretty: pretty});
};
Airbrake.prototype.appendHeaderXml = function(notice) {
notice
.att('version', '2.1')
.ele('api-key')
.txt(this.key)
.up()
.ele('notifier')
.ele('name')
.txt(Airbrake.PACKAGE.name)
.up()
.ele('version')
.txt(Airbrake.PACKAGE.version)
.up()
.ele('url')
.txt(Airbrake.PACKAGE.homepage)
.up()
.up();
};
Airbrake.prototype.appendErrorXml = function(notice, err) {
var trace = stackTrace.parse(err);
var error = notice
.ele('error')
.ele('class')
.txt(err.type || 'Error')
.up()
.ele('message')
.txt(err.message)
.up()
.ele('backtrace')
trace.forEach(function(callSite) {
error
.ele('line')
.att('method', callSite.getFunctionName())
.att('file', callSite.getFileName())
.att('number', callSite.getLineNumber())
});
};
Airbrake.prototype.appendRequestXml = function(notice, err) {
var request = notice.ele('request');
['url', 'component', 'action'].forEach(function(nodeName) {
var node = request.ele(nodeName);
if (err[nodeName]) {
node.txt(err[nodeName]);
}
});
this.addRequestVars(request, 'cgi-data', this.cgiDataVars(err));
this.addRequestVars(request, 'session', this.sessionVars(err));
this.addRequestVars(request, 'params', this.paramsVars(err));
};
Airbrake.prototype.addRequestVars = function(request, type, vars) {
this.emit('vars', type, vars);
var node;
Object.keys(vars).forEach(function(key) {
// JSON.stringify throws on circular structures, lets remove those
var val = traverse(vars[key]).map(function(node) {
if (this.circular) {
this.update('[Circular]');
}
});
val = JSON.stringify(val);
node = node || request.ele(type);
node
.ele('var')
.att('key', key)
.txt(val);
});
};
Airbrake.prototype.cgiDataVars = function(err) {
var cgiData = {};
Object.keys(process.env).forEach(function(key) {
cgiData[key] = process.env[key];
});
var errorEnv = (err.env instanceof Object)
? err.env
: {};
Object.keys(errorEnv).forEach(function(key) {
cgiData[key] = err.env[key];
});
return cgiData;
};
Airbrake.prototype.sessionVars = function(err) {
return (err.session instanceof Object)
? err.session
: {};
};
Airbrake.prototype.paramsVars = function(err) {
return (err.params instanceof Object)
? err.params
: {};
};
Airbrake.prototype.appendServerEnvironmentXml = function(notice) {
var serverEnvironment = notice.ele('server-environment');
if (this.projectRoot) {
serverEnvironment
.ele('project-root')
.txt(this.projectRoot);
}
serverEnvironment
.ele('environment-name')
.txt(this.env)
if (this.appVersion) {
serverEnvironment
.ele('app-version')
.txt(this.appVersion);
}
// This is not documented, but the Airbrake API told me about it when sending
// some invalid data. Going to find out if they want people to use it first
// before enabling.
//if (this.hostname) {
//serverEnvironment
//.ele('hostname')
//.txt(this.hostname);
//}
};