| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| (function () { |
| "use strict"; |
|
|
| const NAB = "https://raw.githubusercontent.com/numenta/NAB/master"; |
| const REAL_CORPORA = new Set([ |
| "realAWSCloudwatch", "realAdExchange", "realKnownCause", "realTraffic", "realTweets", |
| ]); |
| const N_GRID = 120; |
|
|
| const cache = { labels: null, series: new Map(), scores: new Map(), sweeps: new Map() }; |
|
|
| const SAMPLES = [ |
| { |
| series: "realKnownCause/nyc_taxi.csv", |
| detector: "random", |
| note: "Uniform noise on NYC taxi demand, at its own point-adjusted optimum.", |
| }, |
| { |
| series: "realKnownCause/machine_temperature_system_failure.csv", |
| detector: "window_pca", |
| note: "A real detector on a real machine failure — the honest case.", |
| }, |
| { |
| series: "realAWSCloudwatch/ec2_cpu_utilization_5f5533.csv", |
| detector: "random", |
| note: "Noise again, on EC2 CPU utilisation.", |
| }, |
| { |
| series: "realTraffic/speed_7578.csv", |
| detector: "seasonal", |
| note: "A seasonal-profile detector on highway speeds.", |
| }, |
| ]; |
|
|
| async function labels() { |
| if (!cache.labels) { |
| const response = await fetch(`${NAB}/labels/combined_windows.json`); |
| if (!response.ok) throw new Error(`could not reach NAB (${response.status})`); |
| cache.labels = await response.json(); |
| } |
| return cache.labels; |
| } |
|
|
| async function series(key) { |
| if (cache.series.has(key)) return cache.series.get(key); |
| const [all, response] = await Promise.all([ |
| labels(), fetch(`${NAB}/data/${key}`), |
| ]); |
| if (!response.ok) throw new Error(`could not load ${key} (${response.status})`); |
| const parsed = outliar.parseCsv(await response.text()); |
| const windows = outliar.windowsToIndices(parsed.timestamps, all[key] || []); |
| const value = { |
| key, |
| name: key.split("/")[1].replace(/\.csv$/, ""), |
| corpus: key.split("/")[0], |
| timestamps: parsed.timestamps, |
| values: parsed.values, |
| windows, |
| n: parsed.values.length, |
| samplingMinutes: outliar.samplingMinutes(parsed.timestamps), |
| }; |
| cache.series.set(key, value); |
| return value; |
| } |
|
|
| function scores(s, detector) { |
| const id = `${s.key}::${detector}`; |
| if (!cache.scores.has(id)) { |
| cache.scores.set(id, outliar.DETECTORS[detector](s, 7)); |
| } |
| return cache.scores.get(id); |
| } |
|
|
| async function evaluate(key, detector, threshold) { |
| const s = await series(key); |
| const raw = scores(s, detector); |
| const probation = outliar.probationLength(s.n); |
|
|
| const scored = Array.from(raw.slice(probation)); |
| const yTrue = outliar.pointLabels(s.n, s.windows).slice(probation); |
|
|
| const sweepId = `${key}::${detector}`; |
| if (!cache.sweeps.has(sweepId)) { |
| cache.sweeps.set(sweepId, outliar.sweep(yTrue, scored, N_GRID)); |
| } |
| const best = cache.sweeps.get(sweepId); |
| const paOptimal = best.pa[0]; |
| if (threshold === null || threshold === undefined) threshold = paOptimal; |
|
|
| const yPred = scored.map((v) => v >= threshold); |
| const protocols = outliar.allProtocols(yTrue, yPred); |
| const [caught, missed] = outliar.eventRecall(yTrue, yPred); |
|
|
| let alarms = 0, inside = 0; |
| for (let i = 0; i < yPred.length; i++) { |
| if (yPred[i]) { alarms++; if (yTrue[i]) inside++; } |
| } |
| const credited = outliar.adjustPredictions(yTrue, yPred).reduce((a, b) => a + (b ? 1 : 0), 0); |
|
|
| const step = Math.max(1, Math.floor(s.n / 2400)); |
| const timestamps = [], values = [], drawn = []; |
| for (let i = 0; i < s.n; i += step) { |
| timestamps.push(s.timestamps[i]); |
| values.push(s.values[i]); |
| drawn.push(raw[i]); |
| } |
|
|
| const finite = scored.filter(Number.isFinite); |
| return { |
| series: key, |
| name: s.name, |
| detector, |
| is_control: outliar.CONTROLS.has(detector), |
| description: outliar.DESCRIPTIONS[detector], |
| threshold, |
| pa_optimal_threshold: paOptimal, |
| composite_optimal_threshold: best.composite[0], |
| score_range: [Math.min(...finite), outliar.quantile(finite, 0.999)], |
| probation, |
| n: s.n, |
| timestamps, |
| values, |
| scores: drawn, |
| windows: s.windows, |
| metrics: protocols, |
| alarms, |
| alarms_inside: inside, |
| pa_credited: credited, |
| windows_caught: caught, |
| windows_total: caught + missed, |
| false_alarms_per_day: outliar.falseAlarmsPerDay(yTrue, yPred, s.samplingMinutes), |
| }; |
| } |
|
|
| async function catalog() { |
| const all = await labels(); |
| const keys = Object.keys(all).filter((k) => REAL_CORPORA.has(k.split("/")[0])).sort(); |
| return { |
| series: keys.map((k) => ({ |
| key: k, |
| name: k.split("/")[1].replace(/\.csv$/, ""), |
| corpus: k.split("/")[0], |
| windows: all[k].length, |
| })), |
| detectors: Object.keys(outliar.DETECTORS).map((name) => ({ |
| name, |
| description: outliar.DESCRIPTIONS[name], |
| is_control: outliar.CONTROLS.has(name), |
| })), |
| n_samples: SAMPLES.length, |
| }; |
| } |
|
|
| function json(payload) { |
| return new Response(JSON.stringify(payload), { |
| status: 200, headers: { "content-type": "application/json" }, |
| }); |
| } |
|
|
| const original = window.fetch.bind(window); |
|
|
| window.fetch = async function (input, init) { |
| const url = typeof input === "string" ? input : input.url; |
| |
| if (!/^\/(api\/|sample|healthz)/.test(url)) return original(input, init); |
|
|
| const parsed = new URL(url, location.origin); |
| const path = parsed.pathname; |
| const query = parsed.searchParams; |
|
|
| try { |
| if (path === "/api/catalog") return json(await catalog()); |
| if (path === "/healthz") return json({ status: "ok", static: true }); |
| if (path === "/api/findings") { |
| const response = await original("findings.json"); |
| if (!response.ok) throw new Error("findings.json missing"); |
| return json(await response.json()); |
| } |
| if (path === "/api/evaluate") { |
| const threshold = query.has("threshold") ? parseFloat(query.get("threshold")) : null; |
| return json(await evaluate(query.get("series"), query.get("detector"), threshold)); |
| } |
| if (path === "/sample") { |
| const choice = SAMPLES[(parseInt(query.get("index") || "0", 10) || 0) % SAMPLES.length]; |
| const payload = await evaluate(choice.series, choice.detector, null); |
| payload.note = choice.note; |
| return json(payload); |
| } |
| } catch (err) { |
| return new Response(String(err && err.message ? err.message : err), { status: 502 }); |
| } |
| return new Response("not found", { status: 404 }); |
| }; |
| })(); |
|
|