UNPKG

web-analyst

Version:

Web Analyst is a simple back-end tracking system to measure your web app performance.

493 lines (435 loc) 14 kB
const {mkdirSync, writeFileSync, existsSync, readFileSync} = require("fs"); const {isJson, joinPath} = require("@thimpat/libutils"); const {INIT_DATA_CHART, CHART_TYPE, CHART_DATA_FILES} = require("../../hybrid/cjs/wa-constants.cjs"); const path = require("path"); const {getRegisteredBrowsers} = require("../builders/indexers/map-browsers.cjs"); const {get24HoursLabels, getWeekLabels, getYearLabels} = require("../../hybrid/cjs/wa-fixed-label-generator.cjs"); const {getPopularityLabels} = require("./dynamic-label-generator.cjs"); const {getServerLogDir} = require("../utils/core.cjs"); const {getRegisteredOses} = require("../builders/indexers/map-oses.cjs"); const {getRegisteredLanguages} = require("../builders/indexers/map-languages.cjs"); const {getRegisteredEndpoints} = require("../builders/indexers/map-endpoints.cjs"); const {getTodayDate, getStringFormattedDate} = require("../utils/common.cjs"); const {getAgeMaxUnique, TIME_UNIT} = require("../utils/options.cjs"); const {getRegisteredReferrers} = require("../builders/indexers/map-referers.cjs"); const getChartPath = (pathname) => { if (!pathname) { console.error({lid: "WA2667"}, `No pathname given`); return null; } const dataDir = getServerLogDir(); return joinPath(dataDir, pathname); }; /** * Update the number of unique visitors without checks * @param currentData * @param ip * @param index * @param cookieData * @returns {boolean} */ const updateReturningVisitors = (currentData, index, {ip, cookieData}) => { try { if (!Array.isArray(currentData.dataReturningVisitors[index])) { currentData.dataReturningVisitors[index] = []; } const token = cookieData?.token; if (token) { if (!currentData.dataReturningVisitors[index].includes(token)) { currentData.dataReturningVisitors[index].push(token); } } else if (!currentData.dataReturningVisitors[index].includes(ip)) { currentData.dataReturningVisitors[index].push(ip); } } catch (e) { console.error({lid: "WA2669"}, e.message); } }; /** * Update the number of unique visitors without checks * @param currentData * @param ip * @param index * @returns {boolean} */ const updateUniqueVisitors = (currentData, ip, index) => { try { ++currentData.dataUniqueVisitors[index]; } catch (e) { console.error({lid: "WA2669"}, e.message); } }; /** * Add information needed to count the number of visitors * @param ipInfo * @param infoType * @returns {boolean} */ const determineReturningVisitor = (ipInfo, {infoType, cookieData}) => { try { if (!ipInfo) { return false; } const now = new Date(); let ipAge ; if (infoType === TIME_UNIT.WEEK) { ipAge = (now.getTime() - (new Date(ipInfo.lastWeekVisited || ipInfo.date)).getTime()) / 1000; } else if (infoType === TIME_UNIT.YEAR) { ipAge = (now.getTime() - (new Date(ipInfo.lastYearVisited || ipInfo.date)).getTime()) / 1000; } else { ipAge = (now.getTime() - (new Date(ipInfo.lastDayVisited || ipInfo.date)).getTime()) / 1000; } const ageMax = getAgeMaxUnique(infoType); // They visited not so long ago if (ipAge < ageMax) { return true; } // ------------------------------------- // It's a returning visitor, but the time it took them to return count them as a new unique visitor // ------------------------------------- if (infoType === TIME_UNIT.WEEK) { ipInfo.lastWeekVisited = now; } else if (infoType === TIME_UNIT.YEAR) { ipInfo.lastYearVisited = now; } else { // Reset as new unique visitor as their last visit was more than a day ago ipInfo.lastDayVisited = now; } ipInfo.date = getStringFormattedDate(now); if (!ipInfo.initialDate) { ipInfo.initialDate = now; } } catch (e) { console.error({lid: "WA26"}, e.message); } return false; }; /** * Update the number of visits and daily, weekly, yearly unique visitors * @param currentDataByPeriod * @param ip * @param index * @param seen Flag handled by GenServe. It creates a cookie that indicates if a user is new * The cookie expires after a day of not being seen again. * That doesn't work well with the new implementation which take into account daily, weekly and yearly visitors. * seen should pass an object rather than a boolean so we can determine the visitor unique type. The cookie should stay longer * */ const updateVisitors = (currentDataByPeriod, ip, index, {info, cookieData, infoType} = {}) => { try { ++currentDataByPeriod.dataVisitors[index]; const returningVisitor = determineReturningVisitor(info, {infoType, cookieData}); if (returningVisitor) { updateReturningVisitors(currentDataByPeriod, index, {ip, cookieData}); return; } updateUniqueVisitors(currentDataByPeriod, ip, index); } catch (e) { console.error({lid: "WA2669"}, e.message); } }; const readChartContent = (jsonPath, {type, dataLength} = {}) => { try { if (!existsSync(jsonPath)) { return null; } let strJson = readFileSync(jsonPath, {encoding: "utf8"}); if (!isJson(strJson)) { console.error({lid: "WA2411"}, `Invalid json file: ${jsonPath}. The content will be updated.`); return null; } const json = JSON.parse(strJson); if (type === CHART_TYPE.BAR) { json.dataVisitors = json.dataVisitors || new Array(dataLength).fill(0); json.dataUniqueVisitors = json.dataUniqueVisitors || new Array(dataLength).fill(0); json.dataReturningVisitors = json.dataReturningVisitors || new Array(dataLength).fill(0); } else if (type === CHART_TYPE.PIE) { json.dataVisits = json.dataVisits || new Array(dataLength).fill(0); } else if (type === CHART_TYPE.TABLE) { json.dataVisits = json.dataVisits || new Array(dataLength).fill(0); } return json; } catch (e) { console.error({lid: "WA2337"}, e.message); } return null; }; /** * Create data for empty chart * @param jsonPath * @returns {{}} */ const createEmptyChart = (jsonPath) => { const dir = path.parse(jsonPath).dir; if (!existsSync(dir)) { mkdirSync(dir); } writeFileSync(jsonPath, INIT_DATA_CHART, {encoding: "utf8"}); return {}; }; /** * Save data into a json file * @param {string} pathname * @param {*} data * @returns */ const saveChartData = (pathname, data) => { try { if (!data) { return false; } const jsonPath = getChartPath(pathname); const dir = path.parse(jsonPath).dir; if (!existsSync(dir)) { mkdirSync(dir, {recursive: true}); } const str = JSON.stringify(data, null, 2); writeFileSync(jsonPath, str, {encoding: "utf8"}); return true; } catch (e) { console.error({lid: "WA2339"}, e.message); } return false; }; const savePopularityChartData = (pathname, data) => { return saveChartData(pathname, data); }; const getLabelsByPathname = function (pathname) { try { // Get labels let labels; if (pathname === CHART_DATA_FILES.TODAY_DATA_FILENAME) { labels = get24HoursLabels(); } else if (pathname === CHART_DATA_FILES.WEEK_DATA_FILENAME) { labels = getWeekLabels(); } else if (pathname === CHART_DATA_FILES.YEAR_DATA_FILENAME) { labels = getYearLabels(); } return labels; } catch (e) { console.error({lid: "WA2341"}, e.message); } return false; }; const resetDataChart = function (pathname) { const json = {}; try { const labels = getLabelsByPathname(pathname); json.dataVisitors = new Array(labels.length).fill(0); json.dataUniqueVisitors = new Array(labels.length).fill(0); json.dataReturningVisitors = new Array(labels.length).fill(0); json.date = getTodayDate(); json.labels = labels; const jsonPath = getChartPath(pathname); writeFileSync(jsonPath, JSON.stringify(json, null, 2), {encoding: "utf8"}); } catch (e) { console.error({lid: "WA2343"}, e.message); } return json; }; /** * Read json file for today chart to determine whether it needs resetting * Create one if it does not exist * Reset it if needed * @returns {null|any} */ const generateVisitorsChartData = (pathname) => { try { const labels = getLabelsByPathname(pathname); if (!labels) { console.error({lid: "WA2353"}, `Invalid pathname`); return null; } const dataLength = labels.length; // Read existing data chart const jsonPath = getChartPath(pathname); let json = readChartContent(jsonPath, {type: CHART_TYPE.BAR, dataLength}); if (!json) { json = createEmptyChart(jsonPath); } // Apply labels (We should check the labels in the file are valid and reject any update) json.labels = json.labels || labels; return json; } catch (e) { console.error({lid: "WA2355"}, e.message); } return null; }; /** * Read one of the indexers to generate popularity data (Pie charts) * @returns {null|any} */ const loadPopularityChartData = (pathname) => { try { // Get labels dynamically const labels = getPopularityLabels(pathname); let items; // Get labels dynamically if (pathname === CHART_DATA_FILES.BROWSERS_DATA_FILENAME) { items = getRegisteredBrowsers(); } else if (pathname === CHART_DATA_FILES.OSES_DATA_FILENAME) { items = getRegisteredOses(); } else if (pathname === CHART_DATA_FILES.LANGUAGES_DATA_FILENAME) { items = getRegisteredLanguages(); } else if (pathname === CHART_DATA_FILES.ENDPOINTS_DATA_FILENAME) { items = getRegisteredEndpoints(); } else { items = null; console.error({lid: "WA2143"}, `No pathname given`); return null; } let total = 0; const visits = []; for (let browserName in items) { const browserObject = items[browserName]; const visited = browserObject.seen || browserObject.visited; visits.push(visited); total += visited; } const percentages = []; for (let i = 0; i < visits.length; ++i) { const percent = (visits[i] / total * 100).toFixed(2); percentages.push(parseFloat(percent)); } const json = {}; // Apply labels json.labels = json.labels || labels; // Apply data json.dataVisits = visits; json.percentages = percentages; return json; } catch (e) { console.error({lid: "WA2353"}, e.message); } return null; }; const loadFrequencyTableData = (pathname) => { try { let items; if (pathname === CHART_DATA_FILES.ENDPOINTS_DATA_FILENAME) { items = getRegisteredEndpoints(); } else if (pathname === CHART_DATA_FILES.REFERRERS_DATA_FILENAME) { items = getRegisteredReferrers(); } else { items = null; console.error({lid: "WA2141"}, `Unsupported data file: ${pathname}`); return null; } let total = 0; Object.values(items).forEach(item => total += item.seen || item.visited || 0 ); const json = []; for (let key in items) { const itemObject = items[key]; const visited = itemObject.seen || itemObject.visited || 0; const percent = (visited / total * 100).toFixed(2); json.push({ pathname: key, hits : visited, percent : percent + "%" }); } return json; } catch (e) { console.error({lid: "WA2353"}, e.message); } return null; }; module.exports.getChartPath = getChartPath; module.exports.saveChartData = saveChartData; module.exports.savePopularityChartData = savePopularityChartData; module.exports.updateVisitors = updateVisitors; module.exports.updateUniqueVisitors = updateUniqueVisitors; module.exports.updateReturningVisitors = updateReturningVisitors; module.exports.generateVisitorsChartData = generateVisitorsChartData; module.exports.loadPopularityChartData = loadPopularityChartData; module.exports.loadFrequencyTableData = loadFrequencyTableData; module.exports.readChartContent = readChartContent; module.exports.createEmptyChart = createEmptyChart; module.exports.resetDataChart = resetDataChart;