spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { Bar, LevelLegend, Section } from '@/components/ui/primitives';4import { apiTry } from '@/lib/api';5import { fmt } from '@/lib/format';6import { COMPONENT_LABEL, LEVELS } from '@/lib/pressure';7import type { ComponentId, Methodology } from '@/lib/types';89export const revalidate = 300;10export const metadata: Metadata = { title: 'Methodology', description: 'How the Global Internet Pressure Index is computed: components, weights, baselines, robust z, saturation, levels, confidence, Pressure Fronts and self-exclusion.' };1112export default async function MethodologyPage() {13 const m = await apiTry<Methodology>('/api/v1/methodology');14 const weights = m?.weights ?? {};15 const levels = m?.levels ?? LEVELS.map((l) => ({ max: l.max, id: l.id, label: l.label }));16 const eng = m?.engine ?? {};17 return (18 <div className="pb-8">19 <header className="pt-6 pb-4">20 <p className="label">Methodology {m ? <span className="num normal-case tracking-normal text-ink-3">· config v{m.version} · {m.updated_at ? `updated ${m.updated_at.slice(0, 10)}` : `source: ${m.source ?? "file"}`}</span> : <span className="text-warn">· live config unavailable, showing documented defaults</span>}</p>21 <h1 className="mt-1 text-[26px] font-medium tracking-tight sm:text-[32px]">How the pressure is computed</h1>22 <p className="mt-2 max-w-[780px] text-[14px] leading-relaxed text-ink-2">The Global Internet Pressure Index is a composite observability index between 0 and 100 describing how stressed, unstable, congested, degraded or abnormal the public Internet currently is. It is not scientific truth and it is not an uptime percentage: it is a baseline-relative synthesis of independent telemetry, and every number on this site decomposes into the signals that produced it.</p>23 </header>2425 <Section label="1 · Data hierarchy" title="Our own measurements first">26 <ol className="max-w-[780px] list-decimal space-y-1 pl-5 text-[13.5px] leading-relaxed text-ink-2">27 <li>Direct measurements from the InternetPressure Observability Network — our probes (HTTP, TCP, TLS, ICMP, DNS, traceroute) toward a registry of representative targets.</li>28 <li>Open public Internet telemetry — BGP messages from RIPE RIS Live and RouteViews collectors.</li>29 <li>Public raw feeds and polled public infrastructure information.</li>30 <li>External APIs and vendor status pages only as optional corroboration (weight 0.05) — the instrument must remain useful if every commercial API disappears.</li>31 </ol>32 </Section>3334 <Section label="2 · Components & weights" title="Configuration-driven, never hard-coded">35 <div className="grid grid-cols-[minmax(0,1fr)] gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]">36 <table className="tbl self-start">37 <thead>38 <tr>39 <th>Component</th>40 <th className="r">Weight</th>41 <th className="w-[40%]"></th>42 </tr>43 </thead>44 <tbody>45 {Object.entries(weights).map(([id, w]) => (46 <tr key={id}>47 <td className="text-ink">{COMPONENT_LABEL[id as ComponentId] ?? id}</td>48 <td className="num r">{fmt(w, 2)}</td>49 <td>50 <Bar value={w * 100} max={30} color="var(--accent)" />51 </td>52 </tr>53 ))}54 <tr>55 <td className="text-ink-2">Σ</td>56 <td className="num r text-ink-2">{fmt(Object.values(weights).reduce((a, b) => a + b, 0), 2)}</td>57 <td></td>58 </tr>59 </tbody>60 </table>61 <div className="space-y-3 text-[13.5px] leading-relaxed text-ink-2">62 <p>63 <span className="text-ink">Routing</span> — BGP announcements and withdrawals per second, churn, origin-ASN changes, visibility loss, path instability and collector disagreement. <span className="text-ink">Latency</span> — ICMP RTT, TCP connect, TLS handshake, HTTP time-to-first-byte, inter-region latency, packet loss and jitter, always relative to baseline (20 → 100 ms matters; 100 ms alone does not). <span className="text-ink">DNS</span> — lookup latency, SERVFAIL and timeouts, NXDOMAIN anomalies, resolver disagreement across local, Google, Cloudflare and Quad9 resolvers. <span className="text-ink">Availability</span> — representative endpoints failing from at least two probe regions (single-probe failures are the probe's problem, not the Internet's). <span className="text-ink">HTTP/TLS</span> — 5xx, certificate and handshake failures, resets and timeouts. <span className="text-ink">Path</span> — traceroute fingerprints, hop-count deviation and latency shift on changed paths. <span className="text-ink">Corroboration</span> — incidents publicly declared by major providers.64 </p>65 {m?.components && (66 <details className="rounded-[4px] border border-line p-3">67 <summary className="cursor-pointer text-[12px] text-ink">Signal recipe per component (from the live configuration)</summary>68 <div className="mt-3 grid gap-3 sm:grid-cols-2">69 {Object.entries(m.components).map(([cid, c]) => (70 <div key={cid}>71 <p className="label">{COMPONENT_LABEL[cid as ComponentId] ?? cid}</p>72 <ul className="mt-1 space-y-0.5 text-[12px]">73 {c.signals.map((s) => (74 <li key={s.id} className="flex justify-between gap-3">75 <span>76 {s.label} <span className="num text-ink-3">{s.id}</span>77 </span>78 <span className="num text-ink">{fmt(s.weight, 2)}</span>79 </li>80 ))}81 </ul>82 </div>83 ))}84 </div>85 </details>86 )}87 </div>88 </div>89 </Section>9091 <Section label="3 · Baselines & anomaly" title="Robust z against a rolling, seasonal baseline">92 <div className="max-w-[780px] space-y-3 text-[13.5px] leading-relaxed text-ink-2">93 <p>94 For every signal and scope we keep a trailing baseline of <span className="num text-ink">{String(eng.baseline_days ?? 7)} days</span>, restricted to the same hour of day ±1 h once at least <span className="num text-ink">{String(eng.seasonality_min_days ?? 3)}</span> days of history exist (normal daily patterns are not pressure). The most recent <span className="num text-ink">{String(eng.baseline_exclude_seconds ?? 600)} s</span> are excluded so an incident cannot baseline itself. Anomaly strength is the robust z-score95 </p>96 <p className="num rounded-[4px] border border-line bg-panel px-4 py-3 text-center text-[14px] text-ink">z = (x − median) / MAD, clipped to [{String(eng.z_clip_low ?? -3)}, {String(eng.z_clip_high ?? 8)}]</p>97 <p>98 A (probe, target) pair is abnormal above z = <span className="num text-ink">{String(eng.z_anomaly ?? 3)}</span>. Signals with fewer than <span className="num text-ink">{String(eng.baseline_min_samples ?? 24)}</span> samples are “weak coverage” and their weight is damped: no strong conclusions from thin data. Each component sums its weighted signal stresses and saturates into a 0–100 score with <span className="num text-ink">score = 100 × (1 − e^(−{String(eng.saturation_k ?? 0.35)} × stress))</span>, then the index is the weighted sum of component scores. The engine runs every <span className="num text-ink">{String(eng.cycle_seconds ?? 10)} s</span> over a <span className="num text-ink">{String(eng.window_seconds ?? 120)} s</span> window ({String(eng.bgp_window_seconds ?? 60)} s for BGP rates).99 </p>100 </div>101 </Section>102103 <Section label="4 · Levels" title="Seven levels, one colour each">104 <table className="tbl max-w-[560px]">105 <thead>106 <tr>107 <th>Level</th>108 <th className="r">Up to</th>109 <th>Meaning</th>110 </tr>111 </thead>112 <tbody>113 {levels.map((l) => {114 const c = LEVELS.find((x) => x.id === l.id);115 return (116 <tr key={l.id}>117 <td>118 <span className="inline-flex items-center gap-2 text-ink">119 <span className="size-2 rounded-[1px]" style={{ background: c?.color }} aria-hidden="true" />120 {c?.short ?? l.id}121 </span>122 </td>123 <td className="num r">{l.max}</td>124 <td className="text-ink-2">{l.label}</td>125 </tr>126 );127 })}128 </tbody>129 </table>130 <div className="mt-3">131 <LevelLegend />132 </div>133 </Section>134135 <Section label="5 · Importance, confidence & velocity" title="Not all networks weigh the same">136 <div className="max-w-[780px] space-y-3 text-[13.5px] leading-relaxed text-ink-2">137 <p>Each target and ASN carries a Network Importance Score (1–5) reflecting centrality, prefixes, downstream dependency and known services; aggregates weight importance 5 about five times more than importance 1. Event pressure is anomaly strength × affected scope × network importance × confidence. Confidence combines probe count, geographic diversity, signal agreement, BGP corroboration, magnitude, duration and external corroboration. Alongside the index we publish velocity (points per hour), acceleration (per hour²) and volatility so that “high but recovering” is distinguishable from “moderate but worsening”.</p>138 </div>139 </Section>140141 <Section id="fronts" label="6 · Pressure Fronts & incidents" title="Storm systems for the Internet">142 <div className="max-w-[780px] space-y-3 text-[13.5px] leading-relaxed text-ink-2">143 <p>144 A Pressure Front is declared when at least <span className="num text-ink">{String(m?.fronts?.min_pairs ?? 3)}</span> source-region → destination-region pairs rise together above z = <span className="num text-ink">{String(m?.fronts?.z_threshold ?? 2.5)}</span> with intensity ≥ <span className="num text-ink">{String(m?.fronts?.min_intensity ?? 35)}</span>; it is drawn on the map as a great-circle arc with a direction. Incidents open when a component or regional score exceeds <span className="num text-ink">{String(m?.events?.detect_threshold ?? 45)}</span> for <span className="num text-ink">{String(m?.events?.confirm_cycles ?? 2)}</span> consecutive cycles (detected → developing), become active after <span className="num text-ink">{String(m?.events?.active_cycles ?? 6)}</span>, recover below <span className="num text-ink">{String(m?.events?.recover_threshold ?? 30)}</span> and resolve after <span className="num text-ink">{String(m?.events?.resolve_after_seconds ?? 600)} s</span> continuously below. Correlation is rule-based first — BGP spike + latency + path change + failures raise confidence together — and hypotheses are always phrased as “possible”, “probable” or “high-confidence”.145 </p>146 </div>147 </Section>148149 <Section label="7 · Self-exclusion & no fake real-time" title="Our own failure is never an Internet event">150 <div className="max-w-[780px] space-y-3 text-[13.5px] leading-relaxed text-ink-2">151 <p>152 If fewer than <span className="num text-ink">{String(eng.min_probes_for_scoring ?? 2)}</span> probes are fresh (a probe is fresh if it reported within <span className="num text-ink">{String(eng.probe_fresh_seconds ?? 180)} s</span>), if the BGP feed is older than <span className="num text-ink">{String(eng.bgp_fresh_seconds ?? 120)} s</span>, or if our stores are unhealthy, the engine freezes the index at its last value and flags <span className="num text-ink">internal_status = degraded</span>. The interface then shows a visible “Instrument degraded” state with the freeze time instead of interpreting a frozen number. A probe whose targets fail at ≥ {String(eng.probe_local_failure_ratio ?? 0.8)} at once is excluded — its own uplink is down, not the Internet.153 </p>154 <p>Nothing on this site is animated without a measurement behind it: if one update arrives every 10 s, the numbers move every 10 s. We never randomise values or synthesise events, and pages for countries, ASNs, services and incidents exist only where we hold real data.</p>155 </div>156 </Section>157158 <Section label="8 · Ethics" title="Ordinary lightweight client traffic">159 <p className="max-w-[780px] text-[13.5px] leading-relaxed text-ink-2">160 No scanning, no authentication bypass, no private infrastructure, no exploitation, no excessive traffic, no rate-limit evasion and no personal data. Probes fetch a handful of public endpoints on an adaptive schedule that resembles a normal user, and reveal only an approximate position, provider and ASN. Questions: see the{' '}161 <Link href="/api" className="text-accent hover:underline">162 API page163 </Link>{' '}164 or write to contact@spboucher.ai.165 </p>166 </Section>167 </div>168 );169}170