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}