minimalytics
Version:
A minimal, on-premise alternative to Google Analytics
327 lines (326 loc) • 15.5 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
};
Object.defineProperty(exports, "__esModule", { value: true });
var CountryEmoji = require("country-emoji");
var fs = require("fs");
var GeoIp = require("geoip-lite");
var IpAnonymize = require("ip-anonymize");
var Mustache = require("mustache");
var RequestIp = require("request-ip");
var LogSchema_1 = require("./db/LogSchema");
var iso3666_1 = require("iso-3166-1");
var Minimalytics = /** @class */ (function () {
function Minimalytics(opt) {
var _this = this;
this.opt = opt;
if (!this.opt.deltaMs) {
this.opt.deltaMs = 60 * 1000;
}
this.logModel = LogSchema_1.createLogModel(opt.mongoose, opt.collection);
this.handleBasicAuthRequests();
this.handleAnalyticsDashboardRequests();
// Put the `use` hook below the GET /analytics routes to avoid tracking them
this.opt.express.use(function (req, res, next) {
_this.use(req, res, next);
});
}
Minimalytics.init = function (opt) {
if (Minimalytics.instance) {
throw Error("You must initialize Minimalytics only once");
}
Minimalytics.instance = new Minimalytics(opt);
return Minimalytics.instance;
};
Minimalytics.prototype.use = function (req, res, next) {
var _a, _b;
return __awaiter(this, void 0, void 0, function () {
var isValidPath, _i, _c, validPath, ip, isLocalHost, anonymizedIp, avoidAddingNewLog, country;
return __generator(this, function (_d) {
switch (_d.label) {
case 0:
// Do not block the request while logging
next();
if (req.path.includes("favicon.ico")) {
return [2 /*return*/, this.log("Not logging favicon.ico requests")];
}
if (this.opt.validPaths) {
isValidPath = false;
for (_i = 0, _c = this.opt.validPaths; _i < _c.length; _i++) {
validPath = _c[_i];
if (validPath instanceof RegExp) {
if (validPath.test(req.path)) {
isValidPath = true;
break;
}
}
else {
if (validPath === req.path) {
isValidPath = true;
break;
}
}
}
if (!isValidPath) {
return [2 /*return*/, this.log("Not logging request at path \"" + req.path + "\"")];
}
}
ip = RequestIp.getClientIp(req);
if (!ip) {
return [2 /*return*/, this.log("Could not get the IP of the client")];
}
isLocalHost = ip === "127.0.0.1";
if (isLocalHost && !this.opt.debug) {
return [2 /*return*/, this.log("Excluding requests from localhost")];
}
anonymizedIp = IpAnonymize(ip);
if (!anonymizedIp) {
return [2 /*return*/, this.log("Could not anonymize IP \"" + ip + "\"")];
}
return [4 /*yield*/, this.logModel.exists({
$and: [
{ ip: anonymizedIp },
{
timestamp: {
$gte: new Date(Date.now() - this.opt.deltaMs)
}
}
]
})];
case 1:
avoidAddingNewLog = _d.sent();
if (avoidAddingNewLog) {
this.log("Client already made a request less than " + this.opt.deltaMs + "ms ago");
return [2 /*return*/];
}
country = (_a = GeoIp.lookup(ip)) === null || _a === void 0 ? void 0 : _a.country;
if (!country) {
if (isLocalHost) {
if (!this.opt.debug) {
return [2 /*return*/, this.log("Could not get country for IP \"" + ip + "\"")];
}
country = "United States of America";
}
else {
return [2 /*return*/, this.log("Unable to get client IP")];
}
}
country = (_b = iso3666_1.whereAlpha2(country)) === null || _b === void 0 ? void 0 : _b.country;
if (!country) {
return [2 /*return*/, this.log("Couldn't convert ISO-3666-1 to country name")];
}
this.logModel.create({
ip: anonymizedIp,
timestamp: new Date().getTime(),
country: country
});
this.log("Adding request log...");
return [2 /*return*/];
}
});
});
};
Minimalytics.prototype.handleBasicAuthRequests = function () {
var _this = this;
this.opt.express.use(function (req, res, next) {
if (!req.path.startsWith("/analytics")) {
_this.log("Received a non-analytics request", req.path);
return next();
}
var denyAccess = function () {
_this.log("Denying access to analytics dashboard");
res.set("WWW-Authenticate", 'Basic realm="401"');
res.status(401).send("Authentication required.");
};
try {
var b64auth = (req.headers.authorization || "").split(" ")[1] || "";
var _a = Buffer.from(b64auth, "base64").toString().split(":"), username = _a[0], password = _a[1];
var areCredentialsValid = username && password && username === _this.opt.username && password === _this.opt.password;
if (areCredentialsValid) {
next();
}
else {
denyAccess();
}
}
catch (_b) {
denyAccess();
}
});
};
Minimalytics.prototype.handleAnalyticsDashboardRequests = function () {
return __awaiter(this, void 0, void 0, function () {
var mustacheTemplateHtml;
var _this = this;
return __generator(this, function (_a) {
mustacheTemplateHtml = fs.readFileSync(__dirname + "/mustache/analytics.mustache").toString();
this.opt.express.get("/analytics", function (_, res) { return __awaiter(_this, void 0, void 0, function () {
var todayViews, thisMonthViews, totalViews, viewsGroupedByCountry;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.getTodayViews()];
case 1:
todayViews = _a.sent();
return [4 /*yield*/, this.getThisMonthViews()];
case 2:
thisMonthViews = _a.sent();
return [4 /*yield*/, this.getTotalViews()];
case 3:
totalViews = _a.sent();
return [4 /*yield*/, this.getViewsGroupedByCountry()];
case 4:
viewsGroupedByCountry = _a.sent();
res.status(200).send(Mustache.render(mustacheTemplateHtml, {
todayViews: todayViews,
thisMonthViews: thisMonthViews,
totalViews: totalViews,
viewsGroupedByCountry: viewsGroupedByCountry
}));
return [2 /*return*/];
}
});
}); });
return [2 /*return*/];
});
});
};
Minimalytics.prototype.getTodayViews = function () {
return __awaiter(this, void 0, void 0, function () {
var today;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
today = new Date();
today.setHours(0);
return [4 /*yield*/, this.logModel.countDocuments({
timestamp: {
$gte: today
}
})];
case 1: return [2 /*return*/, _a.sent()];
}
});
});
};
Minimalytics.prototype.getThisMonthViews = function () {
return __awaiter(this, void 0, void 0, function () {
var thisMonth;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
thisMonth = new Date();
thisMonth.setDate(1);
thisMonth.setHours(0);
return [4 /*yield*/, this.logModel.countDocuments({
timestamp: {
$gte: thisMonth
}
})];
case 1: return [2 /*return*/, _a.sent()];
}
});
});
};
Minimalytics.prototype.getTotalViews = function () {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.logModel.countDocuments()];
case 1: return [2 /*return*/, _a.sent()];
}
});
});
};
Minimalytics.prototype.getViewsGroupedByCountry = function () {
return __awaiter(this, void 0, void 0, function () {
var models;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.logModel.aggregate([
{
$group: {
_id: "$country",
totalViews: { $sum: 1 }
}
},
{
$sort: {
_id: 1
}
}
])];
case 1:
models = _a.sent();
return [2 /*return*/, models.map(function (item) { return (__assign(__assign({}, item), { emoji: CountryEmoji.flag(item._id) })); })];
}
});
});
};
Minimalytics.prototype.log = function () {
var what = [];
for (var _i = 0; _i < arguments.length; _i++) {
what[_i] = arguments[_i];
}
if (this.opt.debug) {
if (what) {
console.log.apply(console, __spreadArray([Minimalytics.TAG], what));
}
else {
console.log(Minimalytics.TAG, "undefined");
}
}
};
Minimalytics.TAG = "[Minimalytics]";
return Minimalytics;
}());
exports.default = Minimalytics;