stats.js
3.4 kB · javascript · 123 lines
1const PATH = '/stats/stats.json';2const EVERY = 60 * 1000;34const cloud = document.querySelector('[data-stats="cloud"]');5const board = document.querySelector('[data-stats="errors"]');67/* FORMAT */89function ago(seconds) {10 if (!seconds) return 'never';11 const gap = Math.max(0, Math.floor(Date.now() / 1000 - seconds));12 if (gap < 90) return `${gap} s ago`;13 if (gap < 5400) return `${Math.round(gap / 60)} min ago`;14 if (gap < 172800) return `${(gap / 3600).toFixed(1)} h ago`;15 return `${Math.round(gap / 86400)} d ago`;16}1718const size = (n) => (n == null ? '' : n < 1e6 ? `${(n / 1e3).toFixed(0)} kB` : n < 1e9 ? `${(n / 1e6).toFixed(1)} MB` : `${(n / 1e9).toFixed(2)} GB`);1920const head = (label) => label.charAt(0).toUpperCase() + label.slice(1);2122/* DRAW */2324function table(rows) {25 const wrap = document.createElement('div');26 wrap.className = 'table';27 const out = document.createElement('table');28 const body = document.createElement('tbody');29 for (const [name, value] of rows) {30 const line = document.createElement('tr');31 const key = document.createElement('th');32 key.textContent = name;33 const cell = document.createElement('td');34 cell.textContent = value;35 line.append(key, cell);36 body.append(line);37 }38 out.append(body);39 wrap.append(out);40 return wrap;41}4243function note(where, text) {44 const line = document.createElement('p');45 line.className = 'fine';46 line.textContent = text;47 where.replaceChildren(line);48}4950function counts(bucket) {51 const rows = [];52 for (const [key, value] of Object.entries(bucket)) {53 if (key.endsWith('_bytes')) continue;54 if (key.endsWith('_objects')) {55 const label = key.slice(0, -8);56 rows.push([head(label), `${value} files (${size(bucket[`${label}_bytes`])})`]);57 continue;58 }59 rows.push([head(key), String(value)]);60 }61 return rows;62}6364function rows(data) {65 const cdn = data.cdn ?? {};66 return [67 ['Stats', `${ago(data.at)}, last ${data.hours} h`],68 ['CDN', cdn.requests == null ? 'no distribution' : `${cdn.requests} requests, ${size(cdn.bytes)}, ${cdn.error_4xx}% 4xx, ${cdn.error_5xx}% 5xx`],69 ...counts(data.bucket ?? {}),70 ];71}7273const runs = (data) =>74 Object.entries(data.lambdas ?? {}).map(([name, one]) => [name, `${one.invocations} runs, ${one.errors} errors, ${one.throttles} throttles, ${Math.round(one.duration_ms)} ms avg`]);7576const lines = (data) =>77 Object.entries(data.errors ?? {}).flatMap(([name, list]) => list.map((one) => `${one.at ?? 'sometime'} ${one.level} ${name} ${one.message}`));7879function draw(data) {80 if (!data) {81 note(cloud, 'No data yet.');82 note(board, 'No data yet.');83 return;84 }85 cloud.replaceChildren(table(rows(data)), table(runs(data)));86 const found = lines(data);87 if (!found.length) {88 note(board, 'None in the window.');89 return;90 }91 const block = document.createElement('pre');92 const text = document.createElement('code');93 text.textContent = found.join('\n');94 block.append(text);95 board.replaceChildren(block);96}9798/* POLL */99100async function grab() {101 try {102 const reply = await fetch(PATH, { cache: 'no-store' });103 return reply.ok ? await reply.json() : null;104 } catch {105 return null;106 }107}108109let timer = 0;110111const refresh = async () => draw(await grab());112113function run() {114 clearInterval(timer);115 if (document.hidden) return;116 void refresh();117 timer = setInterval(refresh, EVERY);118}119120if (cloud && board) {121 document.addEventListener('visibilitychange', run);122 run();123}