208 lines
5.2 KiB
JavaScript
208 lines
5.2 KiB
JavaScript
import { randomId, firstOfMonth, lastOfMonth } from './util.js';
|
|
import pool from './db.js';
|
|
import express from 'express';
|
|
import basicAuth from 'express-basic-auth';
|
|
import { basicAuthConfig } from './config.js'
|
|
|
|
export default class Event {
|
|
static types = [
|
|
'view',
|
|
'click',
|
|
'error',
|
|
'crash',
|
|
];
|
|
|
|
id = randomId(32);
|
|
appId = '';
|
|
date = new Date();
|
|
|
|
// For web only
|
|
userAgent = '';
|
|
|
|
platform = '';
|
|
// For native only
|
|
manufacturer = '';
|
|
// This doubles as the browser for web
|
|
model = '';
|
|
version = '';
|
|
|
|
locale = '';
|
|
sessionId = '';
|
|
|
|
/**
|
|
* This can have different meanings depending on what the event's type is:
|
|
*
|
|
* view -> page path
|
|
* click -> element identifier
|
|
* error & crash -> stacktrace
|
|
*/
|
|
data;
|
|
|
|
/**
|
|
* view,click, error, or crash
|
|
*/
|
|
type;
|
|
|
|
constructor(
|
|
appId,
|
|
date,
|
|
userAgent,
|
|
platform,
|
|
manufacturer,
|
|
model,
|
|
version,
|
|
locale,
|
|
sessionId,
|
|
data,
|
|
type,
|
|
) {
|
|
this.appId = appId;
|
|
this.date = date;
|
|
this.userAgent = userAgent;
|
|
this.platform = platform;
|
|
this.manufacturer = manufacturer;
|
|
this.model = model;
|
|
this.version = version;
|
|
this.locale = locale;
|
|
this.sessionId = sessionId;
|
|
this.data = data;
|
|
this.type = type;
|
|
}
|
|
}
|
|
|
|
pool.query(`CREATE TABLE IF NOT EXISTS events (
|
|
id VARCHAR(32) PRIMARY KEY,
|
|
appId VARCHAR(32) NOT NULL,
|
|
date DATETIME NOT NULL,
|
|
userAgent VARCHAR(256),
|
|
platform VARCHAR(32),
|
|
manufacturer VARCHAR(256),
|
|
model VARCHAR(256),
|
|
version VARCHAR(32),
|
|
locale VARCHAR(8),
|
|
sessionId VARCHAR(32),
|
|
data TEXT DEFAULT NULL,
|
|
type VARCHAR(256) DEFAULT NULL,
|
|
FOREIGN KEY (appId)
|
|
REFERENCES apps(id)
|
|
ON DELETE CASCADE
|
|
)`);
|
|
|
|
export class EventRepository {
|
|
static getEvents(
|
|
appId,
|
|
from,
|
|
to,
|
|
count,
|
|
page,
|
|
) {
|
|
return new Promise((resolve, reject) => {
|
|
let query = 'SELECT * FROM events WHERE appId = ?';
|
|
let queryParams = [appId]
|
|
if (from) {
|
|
query += ' AND date >= ?'
|
|
queryParams.push(from)
|
|
}
|
|
if (to) {
|
|
query += ' AND date <= ?'
|
|
queryParams.push(to)
|
|
}
|
|
if (count) {
|
|
let limit = count;
|
|
let offset = 0;
|
|
if (page) {
|
|
offset = count * (page - 1);
|
|
limit = count * page;
|
|
}
|
|
query += ' LIMIT ?,?';
|
|
queryParams.push(offset, limit);
|
|
}
|
|
pool.query(query, queryParams, (err, res) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
resolve(res);
|
|
});
|
|
});
|
|
}
|
|
|
|
static createEvent(event) {
|
|
return new Promise((resolve, reject) => {
|
|
pool.query('INSERT INTO events SET ?', event, (err, res, fields) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
resolve(event);
|
|
});
|
|
})
|
|
}
|
|
}
|
|
|
|
export const eventRouter = express.Router()
|
|
eventRouter.get('/', basicAuth(basicAuthConfig), (req, res) => {
|
|
const appId = req.query.appId;
|
|
if (!appId) {
|
|
res.status(400).send({ message: 'Invalid appId' });
|
|
}
|
|
const from = req.query.from || firstOfMonth()
|
|
const to = req.query.to || lastOfMonth()
|
|
const count = req.query.count || 1000;
|
|
const page = req.query.count || 1;
|
|
EventRepository.getEvents(appId, from, to, count, page)
|
|
.then((events) => {
|
|
res.json(events);
|
|
}).catch((err) => {
|
|
res.status(500).send();
|
|
})
|
|
});
|
|
|
|
// This is one of the few routes that don't require authentication. Since
|
|
// events will be coming from all over the place, I don't think it makes
|
|
// sense to try to put auth in front of this. Even some kind of client
|
|
// "secret" would be trivial to deduce by examining the requests.
|
|
eventRouter.post('/', (req, res) => {
|
|
if (typeof req.body.appId === "undefined") {
|
|
res.status(400).json({ message: 'Invalid appId' });
|
|
return;
|
|
}
|
|
|
|
if (typeof req.body.sessionId === "undefined") {
|
|
res.status(400).json({ message: 'Invalid sessionId' });
|
|
return;
|
|
}
|
|
|
|
if (Event.types.indexOf(req.body.type) === -1) {
|
|
res.status(400).json({ message: 'Invalid event type' });
|
|
return;
|
|
}
|
|
|
|
if (typeof req.body.data === "undefined") {
|
|
// TODO: Handle data validation better than this
|
|
res.status(400).json({ message: 'Invalid data' });
|
|
return;
|
|
}
|
|
|
|
EventRepository.createEvent(new Event(
|
|
req.body.appId,
|
|
req.body.date,
|
|
req.body.userAgent,
|
|
req.body.platform,
|
|
req.body.manufacturer,
|
|
req.body.model,
|
|
req.body.version,
|
|
req.body.locale,
|
|
req.body.sessionId,
|
|
req.body.data,
|
|
req.body.type,
|
|
))
|
|
.then((event) => {
|
|
res.json(event);
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
res.sendStatus(500);
|
|
});
|
|
});
|