const { request } = require("undici"); const { DateTime } = require("luxon"); async function fetchGoAccessJson(url) { const res = await request(url, { method: "GET" }); if (res.statusCode < 200 || res.statusCode >= 300) { throw new Error(`HTTP ${res.statusCode}`); } return res.body.json(); } function crawlerRatios(data) { const browsers = data.browsers?.data || []; const crawler = browsers.find((entry) => entry.data === "Crawlers"); if (!crawler) return { hits: 0, visitors: 0 }; const totalHits = (browsers.reduce((sum, entry) => sum + (entry.hits?.count || 0), 0)) || 0; const totalVisitors = (browsers.reduce((sum, entry) => sum + (entry.visitors?.count || 0), 0)) || 0; const hitRatio = totalHits > 0 ? Math.min(1, (crawler.hits?.count || 0) / totalHits) : 0; const visitorRatio = totalVisitors > 0 ? Math.min(1, (crawler.visitors?.count || 0) / totalVisitors) : 0; return { hits: hitRatio, visitors: visitorRatio }; } function groupVisitsByMonth(data, { adjustCrawlers = true } = {}) { const entries = data.visitors?.data || []; const ratios = adjustCrawlers ? crawlerRatios(data) : { hits: 0, visitors: 0 }; const months = new Map(); for (const entry of entries) { const dateStr = entry.data; if (!/^[0-9]{8}$/.test(dateStr)) continue; const year = dateStr.slice(0, 4); const month = dateStr.slice(4, 6); const day = dateStr.slice(6, 8); const key = `${year}-${month}`; const hits = entry.hits?.count || 0; const visitors = entry.visitors?.count || 0; const current = months.get(key) || { hits: 0, visitors: 0, from: null, to: null }; const isoDate = `${year}-${month}-${day}`; current.hits += hits; current.visitors += visitors; if (!current.from || isoDate < current.from) current.from = isoDate; if (!current.to || isoDate > current.to) current.to = isoDate; months.set(key, current); } const adjust = (value, ratio) => { if (!adjustCrawlers) return value; const scaled = value * (1 - ratio); return Math.max(0, Math.round(scaled)); }; const sorted = Array.from(months.entries()) .sort((a, b) => a[0].localeCompare(b[0])) .map(([key, value]) => ({ month: key, from: value.from, to: value.to, hits: adjust(value.hits, ratios.hits), visitors: adjust(value.visitors, ratios.visitors), })); return sorted; } function aggregateLastNDays(data, days = 30, { adjustCrawlers = true } = {}) { const entries = data.visitors?.data || []; if (!entries.length || days <= 0) { return { from: null, to: null, hits: 0, visitors: 0 }; } const valid = entries.filter((entry) => /^[0-9]{8}$/.test(entry.data)); if (valid.length === 0) { return { from: null, to: null, hits: 0, visitors: 0 }; } const sorted = valid.slice().sort((a, b) => a.data.localeCompare(b.data)); const last = sorted[sorted.length - 1]; const end = DateTime.fromFormat(last.data, "yyyyLLdd", { zone: "UTC" }); if (!end.isValid) { return { from: null, to: null, hits: 0, visitors: 0 }; } const start = end.minus({ days: days - 1 }); let from = null; let to = null; let hits = 0; let visitors = 0; for (const entry of sorted) { const current = DateTime.fromFormat(entry.data, "yyyyLLdd", { zone: "UTC" }); if (!current.isValid) continue; if (current < start || current > end) continue; const iso = current.toISODate(); if (!from || iso < from) from = iso; if (!to || iso > to) to = iso; hits += entry.hits?.count || 0; visitors += entry.visitors?.count || 0; } const ratios = adjustCrawlers ? crawlerRatios(data) : { hits: 0, visitors: 0 }; const adjust = (value, ratio) => { if (!adjustCrawlers) return value; const scaled = value * (1 - ratio); return Math.max(0, Math.round(scaled)); }; return { from, to, hits: adjust(hits, ratios.hits), visitors: adjust(visitors, ratios.visitors), }; } module.exports = { fetchGoAccessJson, groupVisitsByMonth, aggregateLastNDays, crawlerRatios, };