Source Maps — Debugging

Code production di-minify dan di-bundle — stack trace terlihat seperti a.b.c:1:4723. Source maps memetakan kode hasil build kembali ke source asli. Yang…

Source map adalah file .map yang mencatat, untuk posisi-posisi di file JavaScript hasil build, di file mana, baris berapa, dan kolom berapa kode itu aslinya kamu tulis. Bayangin novel yang dicetak ulang jadi satu halaman panjang tanpa paragraf supaya hemat kertas. Pembaca yang menemukan typo cuma bisa bilang "halaman 10, huruf ke-25". Source map itu lembar contekan penerbit yang menerjemahkan posisi itu jadi "bab App, baris 42, huruf ke-10" di naskah aslinya.

🧪 Tebakan cepat: file bundled.js.map ikut ter-upload ke server dan bisa dibuka siapa saja. Apa yang bisa didapat orang lain dari file itu: cuma daftar nama file, atau seluruh kode aslimu? Jawabannya ada di bagian "Source map di production".

Kenapa stack trace production gak bisa dibaca

Sebelum di-deploy, kode kamu diproses bundler (Vite, webpack, esbuild): banyak file digabung jadi satu (bundle), lalu di-minify, yaitu spasi dan komentar dibuang dan nama variabel dipendekkan. Ini dua potongan kode asli:

// src/App.tsx, baris 40 sampai 43
export function sapaan(user: User) {
  return "Hai kak " +
    user.name +
    ", apa kabarnya";

// src/utils.ts, baris 6 sampai 9
export function tiapItem<T>(list: T[], fn: (item: T) => void) {
  for (const item of list) {
    fn(item);
  }

Di bundled.js, keduanya jadi satu baris, baris ke-10:

const r=t=>"Hai kak "+t.name+", apa kabarnya";function o(t,n){for(const e of t)n(e)}

Waktu tiapItem dipanggil dengan daftar yang isinya undefined, Chrome mencetak stack trace ini (alamat lengkap file-nya disingkat jadi bundled.js):

TypeError: Cannot read properties of undefined (reading 'name')
    at r (bundled.js:10:25)
    at o (bundled.js:10:80)
    at bundled.js:99:1

Formatnya file:baris:kolom, dan stack trace menghitung baris dan kolom mulai dari 1. Kolom 25 di baris 10 adalah name di t.name, dan kolom 80 adalah n(e). Tapi r, o, dan bundled.js gak bilang apa-apa soal file aslimu.

Isi file .map

Bundler bisa ikut membuat bundled.js.map, sebuah file JSON:

{
  "version": 3,
  "file": "bundled.js",
  "sources": ["src/App.tsx", "src/utils.ts"],
  "sourcesContent": ["...isi lengkap App.tsx...", "...isi lengkap utils.ts..."],
  "names": [],
  "mappings": ";;;;;;;;;wBAyCS,uDClCL"
}

Tiap segmen menyimpan empat angka dalam kode Base64 VLQ (angka yang ditulis pakai huruf supaya ringkas): kolom di bundle, index file asli, baris asli, dan kolom asli. Kalau di-decode dan dihitung mulai dari 1, isinya dua mapping ini:

Posisi di bundle File asli Baris Kolom
baris 10, kolom 25 src/App.tsx 42 10
baris 10, kolom 80 src/utils.ts 8 5

Mencocokkan posisi (yang dikerjakan latihan)

Di latihan, mappings sudah di-decode jadi array seperti tabel tadi. Tugasmu mencari posisi asli dari posisi bundle:

function createSourceMapResolver(mappings) {
  let unmapped = 0;

  function resolve(line, column) {
    const cocok = mappings.find((m) => m.bundled.line === line && m.bundled.column === column);
    if (!cocok) {
      unmapped++; // dihitung di sini saja, sekali per pencarian yang gagal
      return null;
    }
    return cocok.original; // { file, line, column }
  }

  return {
    resolve,
    resolveFrame(frame) {
      const [file, line, column] = frame.split(':'); // "bundled.js:10:80"
      const asli = resolve(Number(line), Number(column));
      if (!asli) return frame; // gak ada mapping: kembalikan apa adanya
      return asli.file + ':' + asli.line + ':' + asli.column;
    },
    unmappedCount: () => unmapped,
  };
}

const r = createSourceMapResolver([
  { bundled: { line: 10, column: 25 }, original: { file: 'src/App.tsx', line: 42, column: 10 } },
  { bundled: { line: 10, column: 80 }, original: { file: 'src/utils.ts', line: 8, column: 5 } },
]);

const r1 = r.resolve(10, 25);
console.log(r1.file);                            // src/App.tsx
console.log(r1.line);                            // 42
console.log(r.resolveFrame('bundled.js:10:80')); // src/utils.ts:8:5
console.log(r.resolveFrame('bundled.js:99:1'));  // bundled.js:99:1
console.log(r.resolve(5, 5));                    // null
console.log(r.unmappedCount());                  // 2

Hasilnya, stack trace tadi jadi at src/App.tsx:42:10, at src/utils.ts:8:5, dan at bundled.js:99:1. Baris 42 di App.tsx adalah user.name: user-nya undefined.

Di latihan pencocokannya persis. DevTools dan Sentry sedikit lebih longgar: mereka memakai segmen terdekat di sebelah kiri pada baris yang sama, jadi kolom 30 pun tetap jatuh ke src/App.tsx:42:10.

Source map di production

Source map berisi sourcesContent, yaitu seluruh kode aslimu, lengkap dengan nama variabel dan komentar. Kalau file .map bisa diunduh publik, siapa saja bisa membaca kodemu. Itu jawaban tebakannya. Di vite.config.js, atur build.sourcemap:

Di Chrome, source map dipakai otomatis kalau Settings → Preferences → Sources → "Enable JavaScript source maps" menyala (bawaannya menyala) dan file .map-nya bisa diambil. Panel Sources lalu menampilkan src/App.tsx asli, dan breakpoint bisa dipasang di situ.

💡 Membaca satu segmen VLQ

Segmen wBAyCS di-decode per huruf. Tiap huruf Base64 bernilai 0 sampai 63 (A = 0, a = 26, w = 48). Bit bernilai 32 artinya "angkanya masih berlanjut ke huruf berikutnya", 5 bit sisanya adalah isinya, dan bit paling kecil dari angka jadinya adalah tanda minus.

Huruf Nilai Lanjut? Angka jadi
w B 48, 1 ya, tidak 16 + 1 × 32 = 48, bit kecil 0, jadi 48 / 2 = 24
A 0 tidak 0
y C 50, 2 ya, tidak 18 + 2 × 32 = 82, jadi 41
S 18 tidak 18, jadi 9

Hasilnya [24, 0, 41, 9]: kolom 24, file index 0, baris 41, kolom 9, semuanya dihitung dari 0. Ditambah 1, jadi kolom 25 di bundle sama dengan src/App.tsx:42:10. Segmen berikutnya, uDClCL, berisi [55, 1, -34, -5]: angka-angka itu selisih dari segmen sebelumnya, jadi kolomnya 24 + 55 = 79, filenya index 1, barisnya 41 - 34 = 7, kolomnya 9 - 5 = 4.

⚠️ Jebakan yang sering ketemu

TL;DR: Source map (.map) memetakan posisi di bundle yang sudah di-minify ke file, baris, dan kolom aslinya. Stack trace bundled.js:10:25 lewat mapping jadi src/App.tsx:42:10. Isi mappings adalah segmen Base64 VLQ, dipisah ; per baris bundle dan , per segmen, dihitung dari 0. Di latihan: resolve mencari yang cocok persis atau null, resolveFrame memecah file:baris:kolom dan mengembalikan frame aslinya kalau gak ketemu, dan unmappedCount menghitung tiap pencarian yang gagal sekali. Di production pakai sourcemap: 'hidden' dan upload ke error tracker, karena .map publik berarti seluruh kode aslimu ikut terbaca. Di Chrome, nyalakan "Enable JavaScript source maps".

Yang akan kamu pelajari