simple-breakpad-server
Version:
Simple breakpad crash reports collecting server
381 lines (354 loc) • 12.3 kB
JavaScript
(function() {
var Crashreport, Sequelize, Symfile, addr, bodyParser, busboy, config, crashreportToApiJson, crashreportToViewJson, db, exphbs, express, hbsPaginate, methodOverride, moment, paginate, path, run, streamToArray, symfileToViewJson, titleCase,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
config = require('./config');
moment = require('moment');
bodyParser = require('body-parser');
methodOverride = require('method-override');
path = require('path');
express = require('express');
exphbs = require('express-handlebars');
hbsPaginate = require('handlebars-paginate');
paginate = require('express-paginate');
Crashreport = require('./model/crashreport');
Symfile = require('./model/symfile');
db = require('./model/db');
titleCase = require('title-case');
busboy = require('connect-busboy');
streamToArray = require('stream-to-array');
Sequelize = require('sequelize');
addr = require('addr');
crashreportToApiJson = function(crashreport) {
var json, k, v;
json = crashreport.toJSON();
for (k in json) {
v = json[k];
if (Buffer.isBuffer(json[k])) {
json[k] = "/crashreports/" + json.id + "/files/" + k;
}
}
return json;
};
crashreportToViewJson = function(report) {
var fields, hidden, json, k, name, ref, v, value;
hidden = ['id', 'updated_at'];
fields = {
id: report.id,
props: {}
};
ref = Crashreport.attributes;
for (name in ref) {
value = ref[name];
if (value.type instanceof Sequelize.BLOB) {
fields.props[name] = {
path: "/crashreports/" + report.id + "/files/" + name
};
}
}
json = report.toJSON();
for (k in json) {
v = json[k];
if (indexOf.call(hidden, k) >= 0) {
} else if (Buffer.isBuffer(json[k])) {
} else if (k === 'created_at') {
fields.props['created'] = moment(v).fromNow();
} else if (v instanceof Date) {
fields.props[k] = moment(v).fromNow();
} else {
fields.props[k] = v != null ? v : 'not present';
}
}
return fields;
};
symfileToViewJson = function(symfile) {
var fields, hidden, json, k, v;
hidden = ['id', 'updated_at', 'contents'];
fields = {
id: symfile.id,
contents: symfile.contents,
props: {}
};
json = symfile.toJSON();
for (k in json) {
v = json[k];
if (indexOf.call(hidden, k) >= 0) {
} else if (k === 'created_at') {
fields.props['created'] = moment(v).fromNow();
} else if (v instanceof Date) {
fields.props[k] = moment(v).fromNow();
} else {
fields.props[k] = v != null ? v : 'not present';
}
}
return fields;
};
db.sync().then(function() {
return Symfile.findAll().then(function(symfiles) {
return Promise.all(symfiles.map(function(s) {
return Symfile.saveToDisk(s);
})).then(run);
});
})["catch"](function(err) {
console.error(err.stack);
return process.exit(1);
});
run = function() {
var app, baseUrl, breakpad, bsStatic, hbs, port;
app = express();
breakpad = express();
hbs = exphbs.create({
defaultLayout: 'main',
partialsDir: path.resolve(__dirname, '..', 'views'),
layoutsDir: path.resolve(__dirname, '..', 'views', 'layouts'),
helpers: {
paginate: hbsPaginate,
reportUrl: function(id) {
return "/crashreports/" + id;
},
symfileUrl: function(id) {
return "/symfiles/" + id;
},
titleCase: titleCase
}
});
breakpad.set('json spaces', 2);
breakpad.set('views', path.resolve(__dirname, '..', 'views'));
breakpad.engine('handlebars', hbs.engine);
breakpad.set('view engine', 'handlebars');
breakpad.use(bodyParser.json());
breakpad.use(bodyParser.urlencoded({
extended: true
}));
breakpad.use(methodOverride());
baseUrl = config.get('baseUrl');
port = config.get('port');
app.use(baseUrl, breakpad);
bsStatic = path.resolve(__dirname, '..', 'node_modules/bootstrap/dist');
breakpad.use('/assets', express["static"](bsStatic));
app.use(function(err, req, res, next) {
if (err.message == null) {
console.log('warning: error thrown without a message');
}
console.trace(err);
return res.status(500).send("Bad things happened:<br/> " + (err.message || err));
});
breakpad.use(busboy());
breakpad.post('/crashreports', function(req, res, next) {
var props, streamOps;
props = {};
streamOps = [];
props.ip = addr(req, ['127.0.0.1', '::ffff:127.0.0.1']);
req.busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
return streamOps.push(streamToArray(file).then(function(parts) {
var buffers, i, j, part, ref;
buffers = [];
for (i = j = 0, ref = parts.length - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) {
part = parts[i];
buffers.push(part instanceof Buffer ? part : new Buffer(part));
}
return Buffer.concat(buffers);
}).then(function(buffer) {
if (fieldname in Crashreport.attributes) {
return props[fieldname] = buffer;
}
}));
});
req.busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
if (fieldname === 'prod') {
return props['product'] = val;
} else if (fieldname === 'ver') {
return props['version'] = val;
} else if (fieldname in Crashreport.attributes) {
return props[fieldname] = val.toString();
}
});
req.busboy.on('finish', function() {
return Promise.all(streamOps).then(function() {
return Crashreport.create(props).then(function(report) {
return res.json(crashreportToApiJson(report));
});
})["catch"](function(err) {
return next(err);
});
});
return req.pipe(req.busboy);
});
breakpad.get('/', function(req, res, next) {
return res.redirect('/crashreports');
});
breakpad.use(paginate.middleware(10, 50));
breakpad.get('/crashreports', function(req, res, next) {
var attributes, findAllQuery, limit, name, offset, page, ref, value;
limit = req.query.limit;
offset = req.offset;
page = req.query.page;
attributes = [];
ref = Crashreport.attributes;
for (name in ref) {
value = ref[name];
if (!(value.type instanceof Sequelize.BLOB)) {
attributes.push(name);
}
}
findAllQuery = {
order: 'created_at DESC',
limit: limit,
offset: offset,
attributes: attributes
};
return Crashreport.findAndCountAll(findAllQuery).then(function(q) {
var count, fields, pageCount, records, viewReports;
records = q.rows;
count = q.count;
pageCount = Math.ceil(count / limit);
viewReports = records.map(crashreportToViewJson);
fields = viewReports.length ? Object.keys(viewReports[0].props) : [];
return res.render('crashreport-index', {
title: 'Crash Reports',
crashreportsActive: true,
records: viewReports,
fields: fields,
pagination: {
hide: pageCount <= 1,
page: page,
pageCount: pageCount
}
});
});
});
breakpad.use(paginate.middleware(10, 50));
breakpad.get('/symfiles', function(req, res, next) {
var findAllQuery, limit, offset, page;
limit = req.query.limit;
offset = req.offset;
page = req.query.page;
findAllQuery = {
order: 'created_at DESC',
limit: limit,
offset: offset
};
return Symfile.findAndCountAll(findAllQuery).then(function(q) {
var count, fields, pageCount, records, viewSymfiles;
records = q.rows;
count = q.count;
pageCount = Math.ceil(count / limit);
viewSymfiles = records.map(symfileToViewJson);
fields = viewSymfiles.length ? Object.keys(viewSymfiles[0].props) : [];
return res.render('symfile-index', {
title: 'Symfiles',
symfilesActive: true,
records: viewSymfiles,
fields: fields,
pagination: {
hide: pageCount <= 1,
page: page,
pageCount: pageCount
}
});
});
});
breakpad.get('/symfiles/:id', function(req, res, next) {
return Symfile.findById(req.params.id).then(function(symfile) {
if (symfile == null) {
resturn(res.send(404, 'Symfile not found'));
}
if ('raw' in req.query) {
res.set('content-type', 'text/plain');
res.send(symfile.contents.toString());
return res.end();
} else {
return res.render('symfile-view', {
title: 'Symfile',
symfile: symfileToViewJson(symfile)
});
}
});
});
breakpad.get('/crashreports/:id', function(req, res, next) {
return Crashreport.findById(req.params.id).then(function(report) {
if (report == null) {
return res.send(404, 'Crash report not found');
}
return Crashreport.getStackTrace(report, function(err, stackwalk) {
var fields;
if (err != null) {
return next(err);
}
fields = crashreportToViewJson(report).props;
return res.render('crashreport-view', {
title: 'Crash Report',
stackwalk: stackwalk,
product: fields.product,
version: fields.version,
fields: fields
});
});
});
});
breakpad.get('/crashreports/:id/stackwalk', function(req, res, next) {
return Crashreport.findById(req.params.id).then(function(report) {
if (report == null) {
return res.send(404, 'Crash report not found');
}
return Crashreport.getStackTrace(report, function(err, stackwalk) {
if (err != null) {
return next(err);
}
res.set('Content-Type', 'text/plain');
return res.send(stackwalk.toString('utf8'));
});
});
});
breakpad.get('/crashreports/:id/files/:filefield', function(req, res, next) {
return Crashreport.findById(req.params.id).then(function(crashreport) {
var contents, field, filename;
if (crashreport == null) {
return res.status(404).send('Crash report not found');
}
field = req.params.filefield;
contents = crashreport.get(field);
if (!Buffer.isBuffer(contents)) {
return res.status(404).send('Crash report field is not a file');
}
filename = config.get("customFields:filesById:" + field + ":downloadAs") || field;
filename = filename.replace('{{id}}', req.params.id);
res.setHeader('content-disposition', "attachment; filename=\"" + filename + "\"");
return res.send(contents);
});
});
breakpad.get('/api/crashreports', function(req, res, next) {
var name, ref, value, where;
where = {};
ref = Crashreport.attributes;
for (name in ref) {
value = ref[name];
if (!(value.type instanceof Sequelize.BLOB)) {
if (req.query[name]) {
where[name] = req.query[name];
}
}
}
return Crashreport.count({
where: where
}).then(function(result) {
return res.json({
count: result
});
}).error(next);
});
breakpad.use(busboy());
breakpad.post('/symfiles', function(req, res, next) {
return Symfile.createFromRequest(req, function(err, symfile) {
var symfileJson;
if (err != null) {
return next(err);
}
symfileJson = symfile.toJSON();
delete symfileJson.contents;
return res.json(symfileJson);
});
});
app.listen(port);
return console.log("Listening on port " + port);
};
}).call(this);