UNPKG

minimalytics

Version:

A minimal, on-premise alternative to Google Analytics

327 lines (326 loc) 15.5 kB
"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;