analytics: add crawler tracking, pretty UA/IP display and copy-to-clipboard

This commit is contained in:
2026-08-20 20:31:04 +00:00
parent 1a1a21712e
commit 7556bb7f4f
6 changed files with 275 additions and 17 deletions
+67 -2
View File
@@ -3,6 +3,38 @@
* visit trail.
*/
/**
* IPv4 unchanged, IPv6 returns the /64 network prefix in compact form.
* Falls back to the original value when parsing fails.
*/
export const hostIP = (ip) => {
try {
if (!ip || !ip.includes(':')) return ip
const strip = (s) => s.replace(/^\[|\]$/g, '')
const norm = strip(new URL(`http://[${ip}]/`).hostname)
const [l, r] = norm.split('::').map((s) => (s ? s.split(':') : []))
const full = r
? [...l, ...Array(8 - l.length - r.length).fill('0'), ...r]
: l
return strip(
new URL(`http://[${full.slice(0, 4).join(':')}::]/`).hostname,
).replace(/::$/, '')
} catch (e) {
console.error('hostIP processing failed for:', ip, e)
return ip
}
}
/** Copy the full IP to the clipboard, ignoring failures. */
export async function copyIp(ip) {
if (!ip) return
try {
await navigator.clipboard.writeText(ip)
} catch {
/* ignore */
}
}
/** Total page views across every page and every bucket. */
export function calcTotalViews(views) {
let n = 0
@@ -87,6 +119,37 @@ export function formatCounts(entries) {
return entries.map(([value, count]) => `${value} (${count})`).join(', ')
}
/**
* Count distinct User-Agent strings among crawler hits, most common first.
* Returns an array of [ua, count] pairs.
*/
export function countCrawlerUas(crawlers) {
const counts = {}
for (const c of crawlers || []) {
const value = c.ua_pretty || c.ua || '(no UA)'
counts[value] = (counts[value] || 0) + 1
}
return Object.entries(counts).sort((a, b) => b[1] - a[1])
}
/**
* Format raw crawler hit records as rows for a technical table. Missing
* values become "—".
*/
export function formatCrawlerRows(crawlers) {
const dash = (s) => (s || '—')
return [...(crawlers || [])].reverse().map((c) => ({
when: new Date(c.start).toLocaleString(),
entry: dash(c.entry),
ip: c.ip || '',
ipDisplay: c.host || hostIP(c.ip) || c.ip || '—',
ua: c.ua_pretty || c.ua || '—',
uaRaw: c.ua || '',
referer: dash(c.referer),
query: dash(c.query),
}))
}
/**
* Format raw visit records as rows for a technical table. Returns objects
* with display strings; missing values become "—". ``trail`` joins page
@@ -110,11 +173,13 @@ export function formatVisitRows(visits, pageTree) {
when: new Date(v.start).toLocaleString(),
trail,
referer: dash(v.referer),
ip: dash(v.ip),
ip: v.ip || '',
ipDisplay: v.host || hostIP(v.ip) || v.ip || '—',
host: dash(v.host),
lang: dash(v.lang),
country: dash(v.country),
ua: dash(v.ua),
ua: v.ua_pretty || v.ua || '—',
uaRaw: v.ua || '',
utm: utm || '—',
}
})