// The drilldown behind a row of the stats page's top-alerts list: when one // alert fired, how fast it was picked up and fixed. An occurrence is an // incident that alert opened (alert rows carry no history of their own). import * as api from './api.js'; import { h, emptyState, spinner } from './ui.js'; import { duration } from './format.js'; import { chips, card, columns, rangeQuery, pad2, WEEKDAYS, MONDAY_FIRST } from './charts.js'; const sec = (s) => (s == null ? '—' : duration(s * 1000)); const fmtTime = (unix) => { const d = new Date(unix * 1000); const mon = d.toLocaleString('en', { month: 'short', timeZone: 'UTC' }); return `${mon} ${d.getUTCDate()}, ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}`; }; export const path = (name) => `/stats/alert/${encodeURIComponent(name)}`; export async function load(name) { return api.statsAlert(name, rangeQuery()); } function tiles(d) { const tile = (label, value, note) => h('div', { class: 'stat-tile' }, h('div', { class: 'stat-value', text: String(value) }), h('div', { class: 'stat-label', text: label }), note && h('div', { class: 'stat-note', text: note })); return h('div', { class: 'stat-tiles' }, tile('Occurrences', d.count), tile('Last seen', fmtTime(d.last_seen), 'UTC'), tile('Average gap', d.mean_gap_seconds == null ? '—' : sec(d.mean_gap_seconds), 'between occurrences'), tile('Mean time to acknowledge', sec(d.mean_ack_seconds), d.median_ack_seconds == null ? '' : `median ${sec(d.median_ack_seconds)}`), tile('Mean time to resolve', sec(d.mean_resolve_seconds), d.median_resolve_seconds == null ? '' : `median ${sec(d.median_resolve_seconds)}`), tile('Longest open', sec(d.longest_open_seconds))); } // Plain-language observations, derived from the same occurrences. function patterns(d) { const n = d.count; const out = []; const sunday = d.by_weekday[0]; const top = d.by_weekday.indexOf(Math.max(...d.by_weekday)); if (n > 1 && d.by_weekday[top] > n / 2) out.push(`${d.by_weekday[top]} of ${n} fired on a ${['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][top]}`); else if (sunday > 0 && n > 1) out.push(`${sunday} of ${n} fired on a Sunday`); const peak = d.by_hour.indexOf(Math.max(...d.by_hour)); if (n > 1 && d.by_hour[peak] > 1) out.push(`Peak hour is ${pad2(peak)}:00 UTC (${d.by_hour[peak]} of ${n})`); const times = d.occurrences.map((o) => o.triggered_at).sort((a, b) => a - b); const repeats = times.filter((t, i) => i > 0 && t - times[i - 1] < 86400).length; if (repeats) out.push(`${repeats} of ${n} fired again within 24 hours of the previous one`); return out; } function patternStrip(d) { const items = patterns(d); if (!items.length) return null; return h('div', { class: 'pattern-strip card card-pad' }, h('span', { class: 'chart-title', text: 'Pattern' }), items.map((t) => h('span', { text: t }))); } // One block per occurrence along a time axis from the first to the last, as wide // as the incident stayed open. Overlaps drop to a second lane. function timeline(d) { const occ = [...d.occurrences].sort((a, b) => a.triggered_at - b.triggered_at); const start = occ[0].triggered_at; const end = Math.max(...occ.map((o) => o.triggered_at + (o.resolve_seconds || 0)), start + 3600); const span = end - start; let prev = -100; const marks = occ.map((o) => { const left = ((o.triggered_at - start) / span) * 100; const lane = left - prev < 1.5 ? 1 : 0; prev = left; const m = h('div', { class: 'tl-mark', title: `${fmtTime(o.triggered_at)} UTC · ${o.resolve_seconds == null ? 'still open' : `open for ${sec(o.resolve_seconds)}`}`, }); m.style.left = `${left}%`; m.style.width = `${((o.resolve_seconds || 0) / span) * 100}%`; m.style.top = `${8 + lane * 22}px`; return m; }); return h('div', {}, h('div', { class: 'tl', role: 'img', 'aria-label': `Timeline of ${occ.length} occurrences between ${fmtTime(start)} and ${fmtTime(end)} UTC.` }, marks), h('div', { class: 'tl-axis' }, h('span', { text: fmtTime(start) }), h('span', { text: fmtTime(end) }))); } // Per occurrence: the wait for an acknowledgement, then the time being worked. function fixBars(d) { const rows = d.occurrences.filter((o) => o.resolve_seconds != null).slice(0, 12); if (!rows.length) return h('p', { class: 'chart-note', text: 'Nothing resolved in this range.' }); const max = Math.max(...rows.map((o) => o.resolve_seconds), 1); return h('div', { class: 'fixbars' }, rows.map((o) => { const ack = Math.min(o.ack_seconds ?? 0, o.resolve_seconds); const a = h('span', { class: 'fix-ack', title: `Waiting for acknowledgement: ${sec(ack)}` }); const w = h('span', { class: 'fix-work', title: `Being worked: ${sec(o.resolve_seconds - ack)}` }); a.style.width = `${(ack / max) * 100}%`; w.style.width = `${((o.resolve_seconds - ack) / max) * 100}%`; return h('div', { class: 'fix-row' }, h('span', { class: 'fix-label', text: fmtTime(o.triggered_at) }), h('span', { class: 'fix-track' }, a, w), h('span', { class: 'fix-total', text: sec(o.resolve_seconds) })); })); } function table(d) { return h('div', { class: 'occ-wrap' }, h('table', { class: 'occ' }, h('thead', {}, h('tr', {}, ['Fired (UTC)', 'Time to ack', 'Duration', 'Acknowledged by', 'Status'].map((t) => h('th', { text: t })))), h('tbody', {}, d.occurrences.map((o) => h('tr', {}, h('td', {}, h('a', { href: `/incidents/${o.incident_id}`, text: fmtTime(o.triggered_at) }), ` ${WEEKDAYS[new Date(o.triggered_at * 1000).getUTCDay()]}`), h('td', { text: sec(o.ack_seconds) }), h('td', { text: sec(o.resolve_seconds) }), h('td', { text: o.acknowledged_by || '—' }), h('td', { text: o.status })))))); } export function body(name, d, error, onRange) { const head = h('div', { class: 'stats-head' }, h('a', { class: 'btn btn-ghost', href: '/stats', text: '← Stats' }), h('h2', { class: 'stats-alert-name', text: name }), chips(onRange)); if (error && !d) return h('div', {}, head, h('div', { class: 'load-error', text: error })); if (!d) return h('div', {}, head, spinner()); if (d === 'none') return h('div', {}, head, emptyState('No occurrences in this range', 'This alert did not open an incident in this window.', 'chart')); return h('div', {}, head, h('div', { class: 'stats' }, error && h('div', { class: 'load-error', text: `Showing older data: ${error}` }), tiles(d), patternStrip(d), card('When it fired', timeline(d), 'Each block spans how long the incident stayed open'), card('By hour (UTC)', columns(d.by_hour.map((v, hr) => ({ label: pad2(hr), value: v, tick: hr % 6 === 0 })), 'Occurrences by hour of day')), card('By weekday', columns(MONDAY_FIRST.map((w) => ({ label: WEEKDAYS[w], value: d.by_weekday[w], tick: true })), 'Occurrences by weekday')), card('Time to resolve', fixBars(d), 'Amber waited for an acknowledgement; blue was worked'), card('All occurrences', table(d), `Newest first${d.count > d.occurrences.length ? `, latest ${d.occurrences.length} of ${d.count}` : ''}`))); }