← Semua pembelajaran / Astro Nol → Portal Berita
Fase 9 · Migrasi dari CodeIgniter 3

Uji paritas — membuktikan Astro sama benarnya

"Sudah saya cek, kelihatannya sama" tidak cukup untuk 200 ribu halaman. Ini cara membandingkannya dengan mesin.

Sumber asli docs.astro.build Resmi Rangkuman ~8 menit baca

Intisari

  • Bandingkan data yang penting, bukan HTML mentah — markup memang sengaja berbeda.
  • Ekstrak: judul, kanonik, JSON-LD, jumlah paragraf, jumlah gambar, status HTTP.
  • Jalankan terhadap sampel yang mewakili: artikel populer, artikel arsip, artikel premium, kasus aneh.
  • Uji bayangan mengirim salinan trafik nyata ke Astro tanpa pembaca melihat hasilnya.
  • Selisih yang disengaja harus didaftarkan, supaya laporannya tetap berguna.

Bandingkan apa

BandingkanJangan bandingkan
Status HTTPHTML mentah
<title>Kelas CSS
URL kanonikUrutan atribut
JSON-LD (field yang penting)Spasi & indentasi
Teks judul <h1>Struktur DOM
Jumlah paragraf badan artikelMarkup pembungkusnya
Jumlah & URL gambarUkuran yang dihasilkan
Ada tidaknya blok paywallTeks persis ajakannya

Skrip pembanding

// skrip/paritas.ts
import { parseHTML } from "linkedom";

interface Sidik {
  status: number;
  judul: string | null;
  kanonik: string | null;
  h1: string | null;
  jumlahParagraf: number;
  jumlahGambar: number;
  adaPaywall: boolean;
  tipeJsonLd: string | null;
  gratis: boolean | null;
}

async function sidik(url: string): Promise<Sidik> {
  const res = await fetch(url, {
    redirect: "manual",
    headers: { "User-Agent": "uji-paritas/1.0" },
    signal: AbortSignal.timeout(15_000),
  });

  if (res.status >= 300) {
    return { status: res.status, judul: null, kanonik: res.headers.get("location"),
             h1: null, jumlahParagraf: 0, jumlahGambar: 0, adaPaywall: false,
             tipeJsonLd: null, gratis: null };
  }

  const { document } = parseHTML(await res.text());
  const ld = document.querySelector('script[type="application/ld+json"]');
  let json: any = null;
  try { json = ld ? JSON.parse(ld.textContent ?? "") : null; } catch {}

  return {
    status: res.status,
    judul: document.querySelector("title")?.textContent?.trim() ?? null,
    kanonik: document.querySelector('link[rel="canonical"]')?.getAttribute("href") ?? null,
    h1: document.querySelector("h1")?.textContent?.trim() ?? null,
    jumlahParagraf: document.querySelectorAll(".badan-artikel p").length,
    jumlahGambar: document.querySelectorAll(".badan-artikel img").length,
    adaPaywall: document.querySelector(".paywall, .badan-berbayar") !== null,
    tipeJsonLd: json?.["@type"] ?? null,
    gratis: json?.isAccessibleForFree ?? null,
  };
}
const ABAI = new Set(["jumlahGambar"]);   // selisih yang disengaja

for (const jalur of daftarUrl) {
  const [lama, baru] = await Promise.all([
    sidik(`https://ci3.internal${jalur}`),
    sidik(`https://astro.internal${jalur}`),
  ]);

  for (const k of Object.keys(lama) as (keyof Sidik)[]) {
    if (ABAI.has(k)) continue;
    if (lama[k] !== baru[k]) {
      console.log(JSON.stringify({ jalur, field: k, lama: lama[k], baru: baru[k] }));
    }
  }
}

Daftar ABAI harus eksplisit dan ditinjau. Beberapa selisih memang disengaja — jumlah gambar berbeda karena Astro memakai srcset, atau judul berbeda karena kamu memperbaiki formatnya. Mendaftarkannya menjaga laporan tetap pendek dan berarti. Laporan yang berisi ribuan selisih yang diketahui akan berhenti dibaca.

Sampel yang mewakili

-- Bukan 100 artikel acak. Ambil yang mewakili tiap kelas.
(SELECT CONCAT('/berita/', slug) FROM artikel
 WHERE status='terbit' ORDER BY hits DESC LIMIT 100)          -- terpopuler
UNION
(SELECT CONCAT('/berita/', slug) FROM artikel
 WHERE status='terbit' ORDER BY terbit_pada DESC LIMIT 100)   -- terbaru
UNION
(SELECT CONCAT('/berita/', slug) FROM artikel
 WHERE status='terbit' ORDER BY terbit_pada ASC LIMIT 100)    -- tertua
UNION
(SELECT CONCAT('/berita/', slug) FROM artikel
 WHERE premium=1 AND status='terbit' LIMIT 50)                -- berbayar
UNION
(SELECT CONCAT('/berita/', slug) FROM artikel
 WHERE kategori_id IS NULL LIMIT 20)                          -- tanpa kategori
UNION
(SELECT CONCAT('/berita/', slug) FROM artikel
 WHERE gambar IS NULL LIMIT 20)                               -- tanpa gambar
UNION
(SELECT CONCAT('/berita/', slug) FROM artikel
 WHERE CHAR_LENGTH(isi) > 50000 LIMIT 20)                     -- sangat panjang
UNION
(SELECT CONCAT('/berita/', slug) FROM artikel
 WHERE judul REGEXP '[^a-zA-Z0-9 -]' LIMIT 20);               -- karakter khusus

Empat baris terakhir yang menemukan bug. Artikel tanpa kategori, tanpa gambar, dengan isi sangat panjang, atau berjudul mengandung karakter khusus adalah kasus yang tidak pernah muncul saat pengembangan — tapi ada ribuan di arsipmu. Ini persis tempat null yang tidak ditangani bersembunyi.

Uji bayangan dengan trafik nyata

# Ambil URL yang benar-benar diminta satu jam terakhir, kirim salinannya ke Astro
tail -n 20000 /var/log/nginx/access.log \
  | awk '$9 == 200 && $6 == "\"GET" {print $7}' \
  | grep '^/berita/' | sed 's/?.*//' | sort -u \
  | while read -r p; do
      s=$(curl -s -o /dev/null -w '%{http_code}' -m 10 "https://astro.internal$p")
      [ "$s" != "200" ] && echo "$s $p"
    done

Ini menangkap kelas kesalahan yang tidak akan muncul dari sampel manapun: URL dengan karakter yang di-encode aneh, slug yang mengandung karakter yang tidak kamu duga, dan artikel yang datanya tidak konsisten dengan cara yang hanya terjadi beberapa kali di seluruh arsip.

Paritas performa

for h in ci3.internal astro.internal; do
  printf '%-16s ' "$h"
  for i in $(seq 1 20); do
    curl -s -o /dev/null -w '%{time_total} ' "https://$h/berita/artikel-uji"
  done | tr ' ' '\n' | grep -v '^$' | sort -n | awk '{a[NR]=$1} END {printf "p50=%.3f p95=%.3f\n", a[int(NR*0.5)], a[int(NR*0.95)]}'
done

Bandingkan dengan cache dimatikan di kedua sisi. Membandingkan Astro yang dilayani dari edge dengan CI3 yang selalu MISS akan memberimu angka yang mengesankan tapi tidak berarti. Yang ingin kamu ketahui adalah: pada beban yang sama, mana yang lebih cepat merender dari nol. Keuntungan cache-nya sudah kamu hitung terpisah di Fase 8.

Gerbang sebelum memindahkan trafik

KriteriaAmbang
Selisih status HTTPNol
Selisih kanonikNol
Selisih JSON-LD @type / isAccessibleForFreeNol
Selisih judul< 0,1%, semuanya ditinjau
Halaman dengan paragraf nolNol
Paywall muncul di artikel gratisNol
Isi premium bocor tanpa sesiNol
p95 render≤ CI3

Latihan: tulis skrip sidik() dan jalankan terhadap 50 URL dari kueri sampel di atas, membandingkan CI3 dengan Astro lokal. Perbaiki setiap selisih atau daftarkan sebagai disengaja. Lalu jalankan uji bayangan terhadap seribu URL dari log akses satu jam terakhir dan catat berapa yang tidak mengembalikan 200 — angka itu biasanya lebih dari nol, dan tiap satu adalah bug yang baru saja kamu hindari di produksi.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.