Saya membuat sistem pelaporan error di sisi klien karena ingin melihat masalah yang benar-benar dialami pengguna dan sulit saya reproduksi secara konsisten di lokal. Sistem itu melakukan persis seperti yang saya minta: menangkap kegagalan lalu mengirimkannya kepada saya.
Masalahnya, hampir semua kejadian terlihat sama penting.
Skrip analitik pihak luar gagal dimuat? Peringatan merah. Skrip iklan diblokir? Peringatan merah. Crawler tidak bisa memuat Google Analytics? Peringatan merah. Pratinjau video memanggil play() lalu dihentikan sebelum Promise selesai? Peringatan merah. Muncul Script error. tanpa sumber atau stack yang berguna? Peringatan merah lagi.
Di aliran yang sama ada kegagalan yang memang layak ditangani: URL milik aplikasi saya sendiri berubah menjadi https://example.comhttps://example.com/..., dan browser tidak bisa memuat file Next.js milik aplikasi di bawah /_next/static/chunks/....
Lapisan pengumpulan bekerja. Lapisan pemantauan belum.
Perbedaan itu mengubah cara saya melihat observabilitas frontend. Sebuah kejadian error hanya membuktikan bahwa sesuatu terjadi. Itu belum merupakan diagnosis, tingkat keparahan, atau insiden.
Kesalahan pertama adalah menganggap kata “error” berarti “mendesak”
Model awal saya kira-kira seperti ini:
browser melaporkan error
↓
kirim CLIENT ERROR
↓
developer harus bertindak
Model itu mencampur banyak pertanyaan berbeda. Apakah kegagalan ada di kode saya? Apakah rute aktif benar-benar rusak? Apakah ini pembatalan yang memang diharapkan? Apakah browser punya cukup informasi untuk mengenali sumbernya? Apakah aplikasi sudah pulih? Sepuluh pesan berarti sepuluh insiden, atau sepuluh gejala dari satu insiden?
Sebelum pertanyaan-pertanyaan itu terjawab, sebuah kejadian tidak seharusnya otomatis berubah menjadi peringatan.
Dalam salah satu peninjauan awal, saya melihat sekitar delapan belas pesan. Sebagian besar hanyalah gangguan dari pihak luar atau perilaku normal siklus hidup. Dua kasus jelas berbeda: URL aplikasi saya yang rusak adalah bug yang pasti, sedangkan chunk JavaScript milik aplikasi yang gagal dimuat bisa membuat halaman kehilangan kode yang dibutuhkan. Namun sistem pelaporan menampilkan hampir semuanya dengan tingkat urgensi yang sama seperti iklan yang diblokir.
Sejak itu saya berhenti menganggap “mengumpulkan semua error browser” dan “membangun pemantauan produksi” sebagai masalah yang sama. Lapisan pengumpulan menyimpan bukti; lapisan pemantauan mengubah bukti itu menjadi keputusan.
Browser tidak memiliki satu kanal error universal dengan makna yang sama untuk semua kegagalan
Berbagai kegagalan di sisi klien tidak datang dengan semantik yang setara.
Kejadian error pada window digunakan untuk error skrip sinkron dan juga terlibat dalam kegagalan pemuatan sumber daya. Promise yang ditolak tanpa ditangani melalui jalur lain: browser memicu unhandledrejection. Elemen yang memuat skrip, gambar, atau media juga dapat memicu error sendiri. React dan Next.js menambahkan sinyal error boundary di tingkat framework di atasnya.
window.error
→ error script sinkron mungkin lolos keluar
unhandledrejection
→ Promise rejected tidak ditangani saat itu
element error
→ resource gagal dimuat atau digunakan
framework boundary
→ rendering atau execution mencapai batas error framework
Hook global melihat gejala di batas sistem; ia tidak selalu mengetahui seluruh rantai penyebab.
Setelah menerima perbedaan itu, saya berhenti memaksa setiap kejadian langsung menjadi satu Error generik dengan tingkat keparahan yang sama.
Kepemilikan sumber daya adalah filter pertama yang berguna
Pemisahan pertama yang benar-benar berguna adalah menentukan siapa pemilik kode atau sumber daya yang gagal.
Kegagalan /_next/static/chunks/app/... tidak sama dengan SDK iklan pada origin lain yang gagal. URL rusak yang dibuat oleh pembangun URL saya tidak sama dengan permintaan analitik yang diblokir. Error dari ekstensi browser adalah kategori lain lagi.
- Aplikasi sendiri: JavaScript, CSS, API, media, dan URL yang dibuat oleh kode saya;
- Framework dan runtime: Next.js atau React yang berada dalam jalur eksekusi aplikasi;
- Integrasi eksternal: analitik, iklan, widget, dan SDK eksternal;
- Lingkungan: ekstensi browser, crawler, kondisi jaringan, alat privasi, dan perilaku khusus browser.
Ini bukan berarti pihak luar tidak penting. Penyedia pembayaran atau autentikasi bisa sangat kritis, dan kegagalan iklan bisa berdampak pada pendapatan. Namun kesehatan integrasi yang buruk tidak otomatis berarti aplikasi mengalami crash.
Jika semua hal masuk ke kanal peringatan mendesak yang sama, kanal itu kehilangan maknanya.
Dua kegagalan milik aplikasi mengajari saya arti sinyal yang bisa ditindaklanjuti
URL rusak adalah kasus yang jelas:
https://example.comhttps://example.com/resource
Untuk kasus ini saya tidak perlu menebak-nebak soal AdBlock, VPN, atau kebijakan browser. URL-nya sendiri tidak valid. Di suatu tempat, kode menambahkan origin ke nilai yang sebenarnya sudah berupa URL absolut.
Kejadian ini bisa langsung ditindaklanjuti karena buktinya spesifik, sumber dayanya milik saya, dan kegagalan menunjuk ke jalur kode yang saya kendalikan.
Kegagalan chunk Next.js berbeda:
Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js
Itu juga milik aplikasi dan bisa merusak halaman, tetapi kejadiannya tidak membuktikan penyebab. Klien lama bisa meminta berkas dari deployment sebelumnya; permintaan bisa timeout; reverse proxy atau CDN bisa gagal; koneksi bisa terputus; atau berkas memang benar-benar tidak ada.
Respons yang tepat bukan “saya sudah tahu penyebabnya”, tetapi “kelas ini berprioritas tinggi dan membutuhkan lebih banyak konteks”.
Tingkat keparahan bisa tinggi meskipun kepastian tentang penyebab rendah.
Script error. adalah petunjuk, bukan stack trace
Error: Script error.
filename: unknown
line: 0
column: 0
Terlihat serius, tetapi hampir tidak membawa informasi.
Browser sengaja membatasi detail error skrip lintas origin. MDN menjelaskan bahwa tanpa konfigurasi CORS yang benar, window.onerror hanya menerima informasi terbatas; perilaku crossorigin pada <script> langsung memengaruhi apakah detail error lengkap dapat tersedia.
Karena itu saya tidak otomatis menafsirkan Script error. yang samar sebagai “aplikasi saya crash”. Sumbernya bisa kode saya, kode pihak ketiga, kode yang disisipkan, atau error yang detailnya tidak boleh dibuka browser karena batasan lintas origin.
Saya menyimpan kejadiannya dan menghubungkannya dengan halaman, build, browser, dan kejadian di sekitarnya; tetapi satu 0:0 saja tidak memicu peringatan mendesak. Jika pola yang sama mulai berkelompok di sekitar satu rilis atau rute, barulah prioritasnya berubah.
Tidak diketahui bukan berarti aman. Juga bukan berarti kritis.
AbortError bisa sepenuhnya nyata dan tetap merupakan bagian normal dari siklus hidup
Pratinjau video memberi contoh paling jelas:
AbortError:
The play() request was interrupted by a call to pause()
HTMLMediaElement.play() mengembalikan Promise, dan Promise itu bisa ditolak. Operasi siklus hidup media juga dapat sengaja membatalkan pemutaran yang masih tertunda; MDN secara eksplisit mendokumentasikan bahwa load() membatalkan Promise play() yang masih tertunda dengan AbortError.
Dalam grid pratinjau, ini mudah terjadi tanpa ada kerusakan yang terlihat oleh pengguna. Item masuk ke viewport, kode memanggil play(); pengguna menggulir, item keluar; sebelum pemutaran benar-benar dimulai, kode menghentikan atau mengganti medianya.
Penolakan Promise itu nyata. Insiden bagi pengguna bisa jadi sama sekali tidak ada.
Perbaikan yang tepat biasanya berada dekat pemanggilannya: tangani Promise di sana dan bedakan pembatalan yang diharapkan dari kegagalan pemutaran yang sebenarnya. unhandledrejection sebaiknya menjadi jaring pengaman, bukan tempat pertama untuk memahami siklus hidup normal komponen.
Kegagalan pihak luar membutuhkan model kesehatan sendiri
Log awal saya dipenuhi kegagalan dari domain analitik dan iklan. Sebagian datang dari browser yang berfokus pada privasi, sebagian dari crawler. Salah satu peringatan mendesak yang paling tidak berguna adalah crawler yang gagal memuat Google Analytics.
Itu hanya membuktikan satu permintaan jaringan gagal. Hampir tidak memberi tahu apakah pengguna manusia masih bisa memakai aplikasi.
Menyimpan kejadian itu bukan masalah. Masalahnya adalah memasukkannya ke aliran insiden yang sama dengan chunk JavaScript milik aplikasi yang gagal dimuat.
- Apakah aplikasi rusak untuk pengguna?
- Apakah integrasi eksternal sehat?
Skrip iklan yang diblokir bisa masuk ke metrik pengiriman iklan. Kegagalan analitik bisa masuk ke metrik cakupan analitik. Keduanya tidak seharusnya memicu peringatan “frontend crash” kecuali ada bukti bahwa fungsi utama benar-benar bergantung pada layanan tersebut.
Pemisahan ini malah membuat masalah pihak luar lebih mudah dilihat: saya dapat mengelompokkannya berdasarkan penyedia, browser, dan wilayah alih-alih menerimanya sebagai gangguan merah acak.
navigator.onLine memberi konteks, bukan bukti konektivitas
Saya juga mulai mencatat apakah browser menganggap dirinya online. Informasi ini berguna, tetapi hanya sebagai petunjuk.
Ada kejadian yang diklasifikasikan logika tambahan sebagai kegagalan jaringan, sementara log yang sama tetap mengatakan:
Online: true
Itu tidak bertentangan. MDN secara eksplisit memperingatkan bahwa navigator.onLine bergantung pada heuristik browser dan sistem operasi. Mesin dapat terhubung ke LAN tetapi tetap tidak bisa mencapai origin saya. VPN, firewall, DNS, dan gangguan jaringan parsial membuat gambarnya makin rumit.
online === false
→ petunjuk kuat adanya masalah environment
online === true
→ TIDAK membuktikan origin atau resource dapat dijangkau
Perbedaan kecil ini mencegah sistem pemantauan mengubah petunjuk menjadi diagnosis yang yakin tetapi salah.
Satu kegagalan akar dapat menghasilkan beberapa kejadian browser
Setelah pengumpulan membaik, jenis gangguan lain menjadi jelas: satu insiden dapat menghasilkan beberapa pesan.
Sebuah chunk JavaScript bisa lebih dulu memicu resource.error, lalu loader modul melempar ChunkLoadError, React atau Next.js melewati error boundary, dan logika pemulihan menjadwalkan reload. Jika setiap lapisan mengirim peringatan sendiri, satu tindakan pengguna terlihat seperti beberapa kegagalan produksi yang terpisah.
Lima pesan terasa seperti lima pengguna terdampak, padahal semuanya bisa berasal dari satu sesi dan satu sumber daya.
Menghapus duplikat hanya berdasarkan teks pesan tidak cukup. Yang dibutuhkan adalah korelasi di tingkat insiden:
session
+ time window pendek
+ error class yang dinormalisasi
+ first-party resource
+ client build
+ route
Saya tetap menyimpan kejadian mentah, tetapi manusia seharusnya menerima representasi insiden dengan sinyal paling kuat. Jika error boundary sudah memiliki stack kode saya dan URL chunk yang tepat, error sumber daya generik sebelumnya tidak perlu membuat peringatan mendesak kedua.
Beri peringatan pada insiden. Simpan kejadian.
Konteks di sekitar error kemudian menjadi lebih berharga daripada string error itu sendiri
Telemetri saya berikutnya jauh lebih terstruktur:
clientBuildId
resource URL
resourceResponseStatus
resourceTransferSize
resourceDurationMs
serviceWorkerVersion
serviceWorkerController
serviceWorkerState
chunkRecoveryScheduled
online
stack / component stack
Untuk media, saya juga mencatat kode error media, status HTTP jika dapat diamati secara terpisah, Content-Type yang benar-benar dikembalikan, serta apakah kegagalan terlihat sebagai masalah HTTP atau pengiriman jaringan.
Field-field ini memungkinkan pertanyaan yang tidak bisa dijawab satu string exception: apakah kegagalan dimulai pada satu build? Apakah browser menerima respons HTTP? Apakah Service Worker mengendalikan halaman? Apakah logika pemulihan sudah berjalan? Apakah beberapa kejadian menunjuk ke sumber daya yang sama? Apakah rute saat ini benar-benar terpengaruh?
Resource Timing API dapat memberi durasi sumber daya, informasi transfer, dan status respons jika didukung serta diizinkan. Ada batasannya: timing lintas origin dibatasi, sumber daya dari cache dapat memiliki transferSize: 0, dan responseStatus tidak tersedia di semua browser. Karena itu null dan 0 harus tetap menjadi keadaan yang bermakna, bukan diubah menjadi kepastian palsu.
ChunkLoadError adalah gejala, bukan detektor 404
Satu kejadian kemudian mengubah cara saya membaca kegagalan chunk.
Browser melaporkan ChunkLoadError untuk chunk layout Next.js, sementara telemetri yang diperkaya juga berisi:
resourceResponseStatus: 200
resourceDurationMs: 170523
serviceWorkerState: activated
chunkRecoveryScheduled: true
Durasi yang tercatat sekitar 170 detik. Apa pun akar penyebab persis insiden itu, data tersebut sudah cukup untuk menolak aturan yang terlalu sederhana:
ChunkLoadError === server mengembalikan 404
Kegagalan chunk lain sama sekali tidak memiliki status respons yang dapat diamati. Sebagian muncul sebagai timeout, sebagian punya informasi transfer. Kelas error sama; bukti di sekitarnya berbeda.
Ini sangat penting pada Next.js karena file di bawah /_next/static/ biasanya memiliki hash konten dan ditujukan untuk cache immutable. Dokumentasi self-hosting Next.js saat ini menjelaskan cache header jangka panjang untuk aset hashed immutable tersebut. Jadi ChunkLoadError dapat melibatkan ketidaksinkronan deployment, klien lama, pengiriman jaringan, reverse proxy, CDN, cache, Service Worker, atau artefak build yang benar-benar hilang.
Saya tidak ingin lapisan peringatan mengarang penyebab. Saya ingin ia menyimpan bukti yang dibutuhkan untuk penyelidikan.
Error media mengajarkan pelajaran yang sama dari lapisan lain
Dalam beberapa kejadian, elemen media melaporkan:
MEDIA_ELEMENT_ERROR: Format error
Jika hanya melihat pesan itu, masalahnya terdengar seperti ketidakcocokan codec.
Namun verifikasi pengiriman tambahan untuk beberapa kejadian tersebut menunjukkan:
HTTP status: 410
Content-Type: text/html; charset=utf-8
failure kind: http
Browser meminta video tetapi menerima respons error HTTP yang berisi HTML. Elemen media tidak dapat mendekode HTML sebagai video, sehingga gejala luarnya menjadi “Format error”. Diagnosis yang berguna ada di lapisan pengiriman.
Lapisan yang pertama kali melihat kegagalan belum tentu lapisan yang menyebabkannya.
“Kegagalan codec”, “gangguan jaringan”, “bug cache”, dan “chunk hilang” adalah kesimpulan. Telemetri seharusnya lebih dulu mencatat apa yang benar-benar diamati.
Saya menilai kegagalan browser pada lima dimensi
1. Kepemilikan
Apakah sumbernya aplikasi sendiri, framework/runtime, integrasi eksternal, atau lingkungan?
2. Dampak pengguna
Apakah rute aktif, rendering, autentikasi, chat, checkout, atau alur inti lain benar-benar rusak? Atau hanya iklan opsional, analitik, preload, atau pratinjau yang gagal sementara halaman tetap dapat dipakai?
3. Kualitas bukti
Apakah saya punya stack dari kode sendiri, URL sumber daya, status HTTP, ID build, dan component stack? Atau hanya Script error. di 0:0?
4. Pengulangan dan sebaran
Apakah ini satu kejadian dari satu sesi, atau pola yang sama muncul pada pengguna, rute, dan browser berbeda setelah rilis yang sama?
5. Pemulihan
Apakah aplikasi pulih sendiri? Apakah reload chunk sudah dijadwalkan? Apakah fallback bekerja? Apakah pengguna masih terblokir?
jelas first-party
+ dampak tinggi
+ evidence kuat
+ banyak session
+ tanpa recovery
= incident urgent
third-party
+ fitur opsional
+ evidence lemah
+ terisolasi
+ user tidak terdampak
= metric atau prioritas rendah
Penyaringan harus dilakukan secara konservatif
Saat gangguan mulai menyakitkan, godaannya adalah menulis puluhan regular expression dan membuang semua yang mengganggu. Itu berbahaya.
Jika semua AbortError saya bungkam, permintaan API yang benar-benar dibatalkan bisa tersembunyi. Jika semua Script error. saya buang, saya bisa kehilangan klaster khusus browser yang baru bermakna setelah agregasi. Jika semua kegagalan pihak luar saya abaikan, saya bisa melewatkan penyedia pembayaran, autentikasi, atau persetujuan yang rusak.
ALERT
→ incident kuat yang butuh tindakan
RETAIN / AGGREGATE
→ simpan dan hitung; alert ketika membentuk cluster
METRIC / SAMPLE
→ noise yang diharapkan atau berdampak rendah; simpan trend dan contoh
Sistem bisa lebih tenang tanpa menjadi buta.
Pengklasifikasi yang baik pada dasarnya adalah kebijakan yang ditulis dalam kode
Kode berikut bukan salinan dari proyek produksi saya. Ini hanya contoh ringkas dari kebijakan yang seharusnya saya pakai sejak awal:
function classifyClientEvent(event) {
const owner = classifyOwner(event);
if (isExpectedMediaCancellation(event)) {
return { severity: "metric", reason: "expected-cancellation" };
}
if (owner === "first-party" && breaksActiveRoute(event)) {
return { severity: "alert", reason: "first-party-user-impact" };
}
if (isActiveFirstPartyChunkFailure(event)) {
return { severity: "alert", reason: "application-chunk" };
}
if (owner === "third-party") {
return { severity: "aggregate", reason: "integration-health" };
}
if (isOpaqueScriptError(event)) {
return { severity: "aggregate", reason: "insufficient-evidence" };
}
return { severity: "aggregate", reason: "needs-correlation" };
}
Bagian yang sulit tersembunyi di fungsi seperti breaksActiveRoute(). Nama exception saja tidak cukup; kita butuh konteks rute, kepemilikan sumber daya, data error boundary, dan kadang pengetahuan khusus produk.
Sidik insiden seharusnya mengikuti insiden, bukan teks pesan
Kesamaan teks penuh adalah strategi buruk untuk deduplikasi. Offset stack yang sudah diminify berubah antar-build, hash chunk berubah, URL berisi identifier dinamis, dan pilihan kata browser berbeda.
{
errorClass,
normalizedFirstPartyResource,
routeFamily,
clientBuild,
sessionId,
shortTimeBucket
}
Untuk crash aplikasi secara global, frame teratas dari stack milik kode saya bisa lebih berguna. Untuk kegagalan chunk, sumber daya chunk yang dinormalisasi lebih berguna. Untuk insiden pengiriman media, kelas kegagalan HTTP dan rute media bisa lebih penting daripada pesan error terluar dari browser.
resource.error
→ ChunkLoadError
→ framework boundary
→ recovery scheduled
Dengan begitu, rangkaian kejadian terkait bisa menjadi satu insiden dengan empat observasi terlampir, bukan empat kegagalan mendesak yang terpisah.
Kanal peringatan mendesak seharusnya memiliki tugas yang jauh lebih sempit
Saya akan menyisakan peringatan langsung untuk kasus seperti: runtime error milik aplikasi dengan stack berguna yang merusak rute aktif; error boundary React/Next.js yang benar-benar mengganggu interaksi; JavaScript atau CSS milik aplikasi yang dibutuhkan rute aktif tetapi gagal dimuat; ChunkLoadError yang berulang di banyak sesi atau build; kegagalan API/data inti yang membuat pengguna tidak bisa lanjut; atau pelanggaran aturan internal yang jelas seperti menghasilkan URL rusak.
Satu Script error. yang samar, kegagalan sumber daya aplikasi yang berhasil dipulihkan, error media yang lapisan akarnya belum diketahui, atau anomali khusus browser yang masih perlu dikelompokkan akan saya simpan, tetapi tidak langsung memicu peringatan.
Dan biasanya saya kirim ke metrik atau diagnostik sampel: kegagalan sumber daya iklan/analitik yang sudah dikenal; pembatalan media AbortError yang memang diharapkan; kegagalan pihak luar yang hanya muncul pada crawler; kegagalan dengan petunjuk putus koneksi yang kuat; dan sumber daya spekulatif opsional yang tidak memengaruhi rute aktif.
Peringatan mendesak seharusnya mewakili dampak pengguna yang bisa ditindaklanjuti, bukan jumlah mentah keluhan browser.
Saya lebih memilih mengukur insiden daripada satu “jumlah error” global
- Insiden milik aplikasi per 1.000 sesi;
- Sesi terdampak berdasarkan ID build;
- Insiden error boundary berdasarkan rute;
- Kegagalan chunk berdasarkan sumber daya dan deployment;
- Tingkat kegagalan integrasi eksternal berdasarkan penyedia;
- Volume pembatalan yang diharapkan, agar lonjakan mendadak tetap terlihat;
- Tingkat keberhasilan pemulihan;
- Jumlah insiden unik dipisahkan dari jumlah kejadian mentah.
“Satu kejadian terjadi” hampir tidak pernah menjadi ambang peringatan produksi yang baik. “Insiden milik aplikasi yang sama kini memengaruhi beberapa sesi independen pada build baru dan pemulihannya gagal” jauh lebih dekat ke sinyal yang layak ditindaklanjuti.
Pemantauan di sisi klien tetap tidak bisa membuktikan akar penyebab sendirian
Telemetri browser memiliki batas yang nyata.
Tidak adanya status HTTP bisa berarti API tidak menyediakan data itu, browser tidak mendukung field tersebut, ada pembatasan lintas origin, permintaan dibatalkan, atau masih ada celah observabilitas lain. Service Worker yang aktif tidak membuktikan bahwa dialah yang mengembalikan sumber daya lama. ChunkLoadError setelah deployment tidak membuktikan ketidaksinkronan versi. online: true juga tidak membuktikan origin dapat dijangkau.
Pemantauan klien mempersempit hipotesis. Untuk membuktikan penyebab, log server, log reverse proxy, manifest deployment, kondisi cache, dan reproduksi nyata mungkin tetap diperlukan.
Saya juga tidak ingin observabilitas berubah menjadi pengumpulan data pengguna tanpa batas. Setiap field harus ada karena benar-benar membantu membedakan jenis kegagalan.
Telemetri yang lebih baik bukan berarti lebih banyak telemetri; melainkan telemetri yang lebih mampu membedakan kegagalan.
Aturan saya sekarang: kumpulkan kejadian, selidiki insiden, beri peringatan pada dampak
Awalnya saya ingin sistem pelaporan menjawab pertanyaan sederhana: “Apakah ada yang rusak?” Di produksi, pertanyaan itu terlalu luas untuk berguna. Selalu ada crawler yang tidak mencapai analitik, alat privasi yang memblokir iklan, Promise media yang sengaja dibatalkan, pengguna yang kehilangan jaringan, atau SDK pihak luar yang bertingkah buruk.
Apakah ini milik kita?
Apakah user kehilangan fungsi?
Seberapa kuat evidence?
Apakah berulang?
Apakah app recover?
Apakah ini beberapa event atau satu incident?
Setelah pemantauan dibangun di sekitar pertanyaan yang lebih sempit itu, banjir pesan merah berubah dari gangguan menjadi alat rekayasa.
Error browser adalah observasi. Insiden adalah penjelasan terkorelasi tentang dampak pada pengguna. Peringatan adalah keputusan bahwa sekarang manusia perlu bertindak.
Saya tidak ingin menganggap ketiganya sebagai hal yang sama lagi.