Error Tracking (Sentry) — DevOps

Mengapa Perlu Error Tracking? Log file saja tidak cukup — kamu butuh tool yang bisa mengelompokkan error, menunjukkan stacktrace, dan memberi notifikasi saat…

Error tracking adalah layanan yang menangkap setiap error di production, dari server maupun dari browser user, mengelompokkan error yang sama jadi satu issue, lalu memberi tahu tim sekali saja. Bayangin puskesmas saat musim penyakit. Tiap pasien dicatat, tapi 30 pasien dengan gejala khas yang sama dicatat sebagai satu penyakit dengan 30 kasus, bukan 30 berkas terpisah. Kepala puskesmas cuma ditelepon kalau muncul penyakit yang belum pernah ada. Di error tracking, pasien itu event, penyakitnya issue, dan gejala khasnya fingerprint. Layanan yang paling sering dipakai Sentry; alternatifnya Bugsnag, Rollbar, dan GlitchTip.

Di pelajaran Application Logging, error catatan-api tercatat sebagai baris log. Itu belum cukup: error yang sama 500 kali jadi 500 baris, dan error di browser user (frontend catatanku) gak pernah sampai ke log server sama sekali.

🧪 Tebakan cepat: setelah deploy pukul 10:00 UTC, empat error EACCES masuk, masing-masing untuk file lampiran yang berbeda. Kalau dikelompokkan per pesan, jadi berapa issue? Kalau per fingerprint? Jawabannya ada di bagian "Fingerprint: kapan dua error dianggap sama".

Event dan issue

Setiap kali error terjadi, SDK Sentry di app mengirim satu event berisi:

Itu informasi yang paling penting saat error di production: stack trace, data request, info user, dan environment. Sentry lalu menggabungkan event yang sama jadi satu issue. Halaman Issues di Sentry adalah daftar issue: tiap baris berisi judul (type dan message), culprit (fungsi dan file tempat error terjadi), jumlah event, jumlah user, serta kapan pertama dan terakhir terlihat. Klik satu issue, dan kamu dapat stack trace, breadcrumbs, dan tag-nya. Dibanding membaca log, error tracking memberi tiga hal sekaligus: pengelompokan otomatis, alert, dan konteks (browser, user, stack trace).

Fingerprint: kapan dua error dianggap sama

Lab memuat 14 event dalam 10 menit setelah catatan-api e4b2d90 dan catatanku b7c1f02 di-deploy. Pesannya:

Event Pesan Frame pertama di kodemu
5 Cannot read properties of undefined (reading 'judul') formatCatatan di routes/catatan.js
4 EACCES: permission denied, open '/app/storage/lampiran/7f3a9c1e.jpg' (nama file beda tiap event) saveLampiran di lampiran.js
3 Cannot read properties of undefined (reading 'id') renderCatatanCard di CatatanCard.jsx
2 Cannot read properties of undefined (reading 'id') ShareButton di ShareButton.jsx

Per pesan, keempat EACCES jadi 4 issue, karena nama file membuat tiap pesan berbeda, padahal bug-nya satu. Dua bug "reading 'id'" malah tercampur jadi satu issue karena pesannya sama. Totalnya 6 issue yang salah.

Per fingerprint (type + fungsi dan file dari frame pertama yang milik kodemu), keempat EACCES jadi 1 issue: type-nya Error dan frame-nya saveLampiran semua. Itu jawaban tebakannya: 4 per pesan, 1 per fingerprint. Dua bug "reading 'id'" terpisah karena frame-nya beda. Hasilnya 4 issue, satu per bug.

Frame node:internal/fs/promises dilewati karena bukan kodemu (not in-app). Sentry bekerja dengan ide yang sama: kalau ada stack trace, issue dikelompokkan dari frame-frame milik kodemu; kalau gak ada, dari type dan message.

Mengelompokkan sendiri: groupErrors

Versi paling sederhana mengelompokkan per pesan dan mencatat jumlah serta waktu pertama dan terakhir:

function groupErrors(errors) {
  const groups = {};
  for (const err of errors) {
    if (!groups[err.message]) {
      // pertama kali pesan ini muncul: siapkan dulu
      groups[err.message] = { count: 0, firstSeen: err.timestamp, lastSeen: err.timestamp };
    }
    const g = groups[err.message];
    g.count++;
    g.firstSeen = Math.min(g.firstSeen, err.timestamp);
    g.lastSeen = Math.max(g.lastSeen, err.timestamp);
  }
  return groups;
}

groupErrors([
  { message: "TypeError", stack: "...", timestamp: 100 },
  { message: "TypeError", stack: "...", timestamp: 200 },
  { message: "RangeError", stack: "...", timestamp: 150 },
]);
Event groups setelahnya
TypeError, 100 { TypeError: { count: 1, firstSeen: 100, lastSeen: 100 } }
TypeError, 200 { TypeError: { count: 2, firstSeen: 100, lastSeen: 200 } }
RangeError, 150 TypeError tetap, ditambah RangeError: { count: 1, firstSeen: 150, lastSeen: 150 }

Untuk mengelompokkan per fingerprint, ganti err.message dengan key dari type dan frame. Lab melakukan persis itu.

Release dan environment

Di catatan-api, Sentry dinyalakan di file pertama yang dijalankan:

// instrument.js, di-require di baris pertama app.js
const Sentry = require("@sentry/node");

Sentry.init({
  dsn: process.env.SENTRY_DSN,          // alamat project Sentry, dari env
  release: process.env.RELEASE,         // "e4b2d90", SHA commit yang di-deploy
  environment: process.env.NODE_ENV,    // "production" atau "staging"
});

Di app.js, setelah semua route, tambahkan Sentry.setupExpressErrorHandler(app) supaya error di route Express ikut dikirim. Nilai RELEASE datang dari pipeline: tag image yang sama dengan yang dipakai IMAGE_TAG=e4b2d90 docker compose up -d (pelajaran CI/CD).

Dengan release, tiap issue tahu di versi mana dia pertama muncul. TypeError judul pertama terlihat di e4b2d90, jadi deploy pagi ini yang membawanya, dan kamu bisa rollback ke 7d03b5f. EACCES sudah ada sejak 7d03b5f, jadi bukan gara-gara deploy ini. Dengan environment, error dari staging gak bercampur dengan production.

Source map untuk error frontend

Kode React catatanku di-build jadi satu file app.3f9a1c.js yang sudah di-minify: nama fungsi dipendekkan dan semuanya ditaruh di satu baris. Error dari HP user jadinya begini:

TypeError: Cannot read properties of undefined (reading 'id')
    at r (https://catatan.example.com/assets/app.3f9a1c.js:1:48213)

Source map (app.3f9a1c.js.map) adalah file yang memetakan tiap posisi di bundle ke kode aslinya. Kalau source map untuk release itu sudah di-upload, Sentry menampilkan:

    at renderCatatanCard (src/components/CatatanCard.jsx:14:22)

Buat source map saat build (di Vite: build: { sourcemap: "hidden" }, file .map dibuat tanpa ditautkan dari bundle), upload ke Sentry di CI untuk release yang sama (misalnya sentry-cli sourcemaps upload --release=b7c1f02 ./dist), lalu jangan ikut sajikan file .map ke publik: isinya kode sumber lengkap.

Alert: issue baru, bukan setiap event

Alert yang bunyi untuk setiap event mengirim 14 pesan Slack dalam 10 menit di Lab. Setelah pesan kelima, gak ada yang membacanya lagi. Di Sentry, buat issue alert dengan kondisi:

Batasi juga ke environment production. Cara mendesain alert yang gak bikin capek dibahas di pelajaran Alerting & On-Call.

⚠️ Jebakan yang sering ketemu

TL;DR: Error tracking (Sentry, Bugsnag, Rollbar, GlitchTip) menangkap error dari server dan browser, mengirim tiap kejadian sebagai event dengan stack trace, request, user, environment, release, dan breadcrumbs, lalu menggabungkan event yang sama jadi satu issue. Pengelompokan per pesan gagal saat pesannya berisi variabel (4 EACCES jadi 4 issue) atau saat dua bug punya pesan sama; fingerprint dari type dan frame pertama di kodemu memberi satu issue per bug. groupErrors menyiapkan key baru dengan count: 0, lalu menjaga firstSeen dengan Math.min dan lastSeen dengan Math.max. Tag release menunjukkan deploy mana yang membawa bug, environment memisahkan staging dari production. Upload source map per release supaya app.3f9a1c.js:1:48213 jadi CatatanCard.jsx:14, tanpa menyajikannya ke publik. Pasang alert untuk issue baru dan regresi, bukan setiap event.

Yang akan kamu pelajari