web-analyst
Version:
Web Analyst is a simple back-end tracking system to measure your web app performance.
327 lines (280 loc) • 9.24 kB
JavaScript
/**
* Created by thimpat on 04/11/2015.
*/
const url = require("url");
const {startLogEngine, registerHit} = require("./lib/hits-manager.cjs");
const minimist = require("minimist");
const {joinPath, convertToUrl, sleep} = require("@thimpat/libutils");
const {setOptions} = require("./lib/utils/options.cjs");
const {setSession, getSessionProperty} = require("./lib/utils/session.cjs");
const {isPagePattern, isIgnorePattern} = require("./lib/utils/patterns.cjs");
const {setJwtSecretToken} = require("./auth/helpers/token-helpers.cjs");
const {getLoggable, setGenserveDir} = require("./auth/helpers/genserve-helpers.cjs");
const {DETECTION_METHOD, PAGES} = require("./constants.cjs");
const {ltr} = require("semver");
// ----------------------------------------------------------------
// Run from Genserve thread
// ----------------------------------------------------------------
/**
* Returns whether the client has already contacted the server,
* otherwise add a cookie to the client to be detected next time
* @param req
* @param res
* @param loggable
* @returns {{seen: boolean}}
*/
const processSeenStatus = function (req, res, {loggable = null} = {})
{
try
{
const cookieString = req.headers.cookie;
if (!cookieString)
{
const expiration = Date.now() + (60 * 60) * 1000 * 24;
res.setHeader("Set-Cookie", [`web-analyst-token=485; HttpOnly`, `expires=${new Date(expiration)}`]);
return {seen: false};
}
return {seen: true};
}
catch (e)
{
loggable.error({lid: "WA5427"}, e.message);
}
return {seen: false};
};
/**
* Add extra information to the plugin options.
* Populate staticDirs, dynDirs and validators for GenServe to be able to find the stats web folder.
* These directories will be extracted and used later by GenServe (@see updateSessionForPlugin)
* @note Run in the same thread as the server.
* @note The server will wait for onInit to complete
* @note Called before onGenserveMessage() receives its first message
* @param pluginOptions
* @param session
* @param loggable
* @returns {boolean}
*/
const onInit = async function ({options: pluginOptions, session, loggable})
{
try
{
let dir = pluginOptions.staticDirs || pluginOptions.dir || ["./stats/"];
if (!Array.isArray(dir))
{
dir = [dir];
}
const authDir = joinPath(__dirname, "auth/");
const dynDir = authDir;
if (!process.env.JWT_SECRET_TOKEN)
{
if (pluginOptions.token)
{
setJwtSecretToken(pluginOptions.token);
}
else
{
setJwtSecretToken(Math.random() * 99999999 + "");
}
}
// Update staticDirs to add web/ folder
pluginOptions.staticDirs = dir;
// Update dynamicDirs to add auth/ folder
pluginOptions.dynamicDirs = [authDir];
// Add validator to allow the server restricting the added static directory
pluginOptions.validators = pluginOptions.validators || [joinPath(dynDir, "validate.cjs")];
// File (json or js) containing allowed user map
pluginOptions.credentials = pluginOptions.credentials || joinPath(dynDir, "creds.cjs");
// Errors
pluginOptions.errors = pluginOptions.errors || {};
const serverUrl = convertToUrl(session);
pluginOptions.url = serverUrl + PAGES.LOGIN_PAGE_NAME;
loggable.log({lid: "WA2002", color: "#4158b7"}, `Statistics plugin URL: ${pluginOptions.url}`);
return true;
}
catch (e)
{
loggable.error({lid: "GS7547"}, e.message);
}
return false;
};
/**
* Each time a request is done, this function is called.
* This function is run from inside GenServe thread and is called before Genserve reaches out
* the plugin within its own thread.
* The value returned by this function will be sent to the plugin process via onGenserveMessage
* @see onGenserveMessage
* @param req
* @param res
* @param {*} pluginProps
* @param loggable
* @returns {{[seen]: boolean}}
*/
const onInformingPlugins = function (req, res, pluginProps = {detectionMethodUnique: DETECTION_METHOD.IP}, {loggable = null} = {})
{
try
{
const {options} = pluginProps || {};
if (options.detectionMethodUnique !== DETECTION_METHOD.COOKIE)
{
return {};
}
return processSeenStatus(req, res, {loggable});
}
catch (e)
{
loggable.error({lid: "WA6553"}, e.message);
}
return {};
};
// ----------------------------------------------------------------
// Run in forked process
// ----------------------------------------------------------------
/**
* Harvest data
* @returns {Function}
*/
function trackData(req, res, {headers = {}, ip, seen = false} = {}, {loggable = null} = {})
{
try
{
const infoReq = url.parse(req.url, true);
for (let k in headers)
{
headers[k] = headers[k] || "";
}
for (let k in infoReq)
{
infoReq[k] = infoReq[k] || "";
}
if (isIgnorePattern(infoReq.pathname))
{
return;
}
if (!isPagePattern(infoReq.pathname))
{
return;
}
registerHit({
ip : ip,
acceptLanguage: headers["accept-language"],
userAgent : headers["user-agent"],
pathname : infoReq.pathname,
search : infoReq.search,
seen
});
return true;
}
catch (e)
{
loggable.error({lid: "WA5441"}, e.message);
}
return false;
}
/**
* - Generate HTML web pages for statistic pages
* - Build data directory if non-existent
* - Save session data (in the plugin memory space process)
* - Review and update stats plugin options
*/
const setupEngine = function ({session, options}, {loggable = null} = {})
{
try
{
// Save session information in plugin progress
setSession(session);
const server = getSessionProperty("serverName");
const namespace = getSessionProperty("namespace");
// By default, we ignore the stats page
const statDir = "/" + server + "." + namespace + "/";
setOptions(options, {ignore: statDir});
startLogEngine(server, namespace);
return true;
}
catch (e)
{
loggable.error({lid: "WA2189"}, e.message);
}
return false;
};
/**
* GenServe message handler
* Entrypoint for requests
* @param pagePattern
* @param action
* @param req
* @param res
* @param headers
* @param data
* @param extraData
* @param ip
* @returns {boolean}
*/
function onGenserveMessage({
action,
headers,
req,
res,
data,
extraData,
ip,
informingPluginsResult = {},
session,
genserveDir,
genserveVersion,
genserveName,
options,
loggable
} = {})
{
try
{
data = data || {};
if (action === "initialisation")
{
setGenserveDir(genserveDir);
// Only the forked process processes this line
setupEngine({session, options}, {loggable});
process.send && process.send("initialised");
}
else if (action === "request")
{
const {seen} = informingPluginsResult;
trackData(req, res, {headers, ip, data, extraData, seen}, {loggable});
}
}
catch (e)
{
loggable.error({lid: "WA2125"}, e.message);
}
}
// Do not use console.log
(async function ()
{
try
{
const argv = minimist(process.argv.slice(2));
if (argv.genserveDir)
{
// Set a listener on Genserve events
if (argv.genserveVersion && ltr(argv.genserveVersion, "5.6.0"))
{
process.send && process.send("incompatible");
await sleep(200);
return;
}
if (argv.genserveDir)
{
process.send && process.send("loaded");
process.on("message", onGenserveMessage);
await sleep(200);
}
}
}
catch (e)
{
console.error(e);
}
}());
module.exports.onGenserveMessage = onGenserveMessage;
module.exports.onInformingPlugins = onInformingPlugins;
module.exports.onInit = onInit;