Sources Paneli ile JavaScript Debugging: Breakpoint Türleri, Source Map ve Ignore List
Sources panelinde line, conditional ve logpoint breakpoint'leri, DOM, XHR/fetch ve exception breakpoint'leri, async call stack ve Scope/Watch ile bir hatayı tahminle değil kanıtla bulursun. Angular 22'de yerinde değiştirilen array yüzünden tetiklenmeyen signal effect'i adım adım; Angular CLI source map'leri, hidden source map, x_google_ignoreList ile framework frame'lerini gizlemek ve Workspaces.
Sürüm notu: Bu yazıdaki örnekler Chrome 156 (156.0.8078.80), Angular 22.2, Angular CLI (Command Line Interface) 22.2.6 ve .NET 10 ile denenmiştir. Breakpoint, call stack ve scope çıktıları aynı Chrome sürümünde, headless modda Chrome DevTools Protocol (CDP) üzerinden alındı ve source map'lerle orijinal TypeScript satırlarına çevrildi; panel adımları Chrome DevTools dokümanından aktarıldı.
Önce Tahmin Et
Bookstore'da sepet bir root service'te, signal olarak duruyor. Üst menüdeki rozet ürün sayısını ve toplamı gösteriyor, bir effect de sepeti her değişimde localStorage'a yazıyor:
@Injectable({ providedIn: 'root' })
export class CartStore {
readonly items = signal<CartItem[]>([]);
readonly total = computed(() => this.items().reduce((sum, i) => sum + i.price * i.quantity, 0));
constructor() {
effect(() => {
const items = this.items();
localStorage.setItem('bookstore.cart', JSON.stringify(items));
console.log('cart saved', items.length);
});
}
add(book: Book): void {
this.items.update(list => {
list.push({ bookId: book.id, title: book.title, price: book.price, quantity: 1 });
return list;
});
}
}
Kitap listesinde 25 kez "Add to cart"a bastık. Rozetin metni ve konsoldaki log'lar:
initial badge: 0 · $0.00 | logs: ["cart saved 0"]
after 25 clicks badge: 25 · $0.00 | cart saved logs: ["cart saved 0"]
Sayaç 25, toplam hâlâ $0.00, effect yalnız başlangıçta bir kez çalışmış ve localStorage'da boş bir array var. Aynı signal'den beslenen üç şeyden biri güncelleniyor, ikisi güncellenmiyor. Sence neden? Cevabı biliyor olabilirsin; ama bu yazıda onu tahminle değil, Sources panelinde adım adım kanıtlayarak bulacağız. Çünkü bir dahaki hata bu kadar tanıdık olmayacak.
Sources Paneli ve Angular'ın Source Map'leri
Sources panelinin solunda sekmeler var: Page sayfanın yüklediği her şeyi origin'e göre gösterir, Workspace yerel klasörünü bağladığın yer, Overrides yanıtları ezdiğin yer, Snippets de kendi küçük script'lerin. Ortada editör, sağda debugger bölümleri (Breakpoints, Scope, Call Stack, Watch) durur. Dosya açmak için Ctrl+P (Mac'te Cmd+P), tüm dosyalarda aramak için Ctrl+Shift+F (Cmd+Option+F), satıra gitmek için Ctrl+G.
Ctrl+P'ye cart.store yazınca karşına derlenmiş JavaScript değil, TypeScript dosyası çıkar. Bunu source map sağlıyor. ng serve development configuration'ında sourceMap: true ile çalışır ve map'i script'in sonuna data URL (Uniform Resource Locator) olarak gömer. Sayfanın yüklediği script'lerin map'lerini CDP'nin Debugger.scriptParsed event'inden okuduğumuzda uygulama kodunun hepsi tek bir main.js'teydi:
/main.js sources=15 ignoreList=0
.../vite/deps/core-CP38VavK.js sources=5 ignoreList=5
.../vite/deps/esm5-Doa4YBQt.js sources=222 ignoreList=222
.../vite/deps/@angular_router.js sources=3 ignoreList=3
main.js'in 15 kaynağı src/main.ts, src/app/cart.store.ts gibi senin dosyaların. Angular, RxJS ve tslib ise ayrı chunk'larda ve her birinin map'inde tüm kaynaklar ignoreList'te. Bu listeye birazdan döneceğiz. Breakpoint'i TypeScript satırına koyarsın, DevTools onu map üzerinden derlenmiş koddaki konuma çevirir; bizim denemede cart.store.ts:19 main.js:269'a düştü.
Production'da durum farklı. Varsayılan production configuration'da source map yok. Hata ayıklamak için map üretmek ama kullanıcıya göstermemek istiyorsan angular.json'da hidden source map açarsın:
"production": {
"sourceMap": { "scripts": true, "styles": false, "hidden": true, "vendor": false },
"outputHashing": "all"
}
Build çıktısında her script'in yanında bir .map dosyası oluştu, ama main-X24X3BNP.js'in son satırında sourceMappingURL yorumu yok:
main-X24X3BNP.js.map
chunk-XEUCJQTH.js.map
chunk-3Y4P6I6V.js.map
...;ri(Tr,pa).catch(t=>console.error(t));
//# debugId=37424c98-4b75-59f3-9fae-bccd5b16ba28
Tarayıcı map'i kendiliğinden bulamaz; map dosyalarını sunucuya koymaz, error tracking aracına yüklersin. Canlı bir hatayı DevTools'ta incelemen gerektiğinde Sources editöründe derlenmiş dosyaya sağ tıklayıp Add source map ile map'in adresini elle verirsin. Bu build'in main map'inde 34 kaynaktan 12'si, framework chunk'ında 97 kaynaktan 89'u ignoreList'teydi.
Line Breakpoint, Scope ve Watch: Hatayı Kanıtlamak
Hipotezimiz şu: effect ve computed çalışmıyor, demek ki signal "değiştim" demiyor. cart.store.ts'i açıp return list; satırının numarasına tıklıyoruz (ya da imleç satırdayken Ctrl+B). Butona basınca debugger duruyor. Call Stack ve Scope:
line breakpoint cart.store.ts:20: reason=other
(anonymous) (src/app/cart.store.ts:20)
add (src/app/cart.store.ts:18)
BookList_For_4_Template_app_book_card_addToCart_0_listener (src/app/book-list/book-list.ts:16)
BookCard_Template_button_click_8_listener (src/app/book-card/book-card.ts:14)
(8 frames from ignore-listed sources hidden)
Local scope: list=Array(1)
Call stack'i aşağıdan yukarı oku: BookCard template'indeki butonun click listener'ı, BookList template'indeki (addToCart) listener'ı, CartStore.add ve update'e verdiğimiz arrow function. Template listener'larının adları Angular derleyicisinin ürettiği fonksiyonlar, ama source map onları doğru TypeScript satırına bağlıyor. Aradaki Angular frame'leri ignore list sayesinde gizli.
Şimdi kanıt. Scope'ta list'e sağ tıklayıp Store as global variable diyoruz, temp1 oluyor. Call Stack'te add frame'ine tıklıyoruz; konsol artık o frame'in context'inde çalışıyor. Gerçek değerler:
frame (anonymous): list.length = 1
frames between (anonymous) and add: signalUpdateFn, update
frame add: this.items().length = 1
temp1 (list) === this.items() in frame add = true
update daha dönmeden signal'in kendi değeri bir element'li olmuş ve list ile aynı referans. Yani biz signal'in elindeki array'i yerinde değiştirdik, sonra aynı array'i "yeni değer" diye geri verdik. Signal'ler varsayılan olarak Object.is ile karşılaştırır; eski ve yeni değer aynı referans olduğu için signal değişiklik bildirmez. Bildirim gelmeyince total computed'ı cache'teki 0'ı döndürmeye, effect de hiç çalışmamaya devam eder.
Sayaç neden doğru peki? Butona tıklamak, Angular'ın template listener'ı üzerinden o view'ı ve atalarını kirli işaretler. Bir sonraki change detection'da App template'i cart.items().length'i yeniden okur ve yerinde değişmiş array'in uzunluğunu görür. Yani sayaç signal sayesinde değil, tıklamanın yan etkisiyle güncelleniyor. Bu tam olarak "bazen çalışıyor" türünden bir bug: sepete bir HTTP (HyperText Transfer Protocol) yanıtı sonrasında ürün eklenseydi sayaç da donardı.
Watch bölümüne list.length ya da book.title gibi ifadeler eklersen her duraklamada yeniden hesaplanır. Watch'a, Scope'a ya da editörde bir değişkenin üzerine gelince açılan popover'a bakmak, kodu durdurmanın asıl getirisi: console.log eklemeden, rebuild beklemeden o anki state'i görürsün.
Conditional Breakpoint ve Logpoint
Her tıklamada durmak istemiyorsan satır numarasına sağ tıklayıp Add conditional breakpoint dersin (ya da Ctrl/Cmd ile satır numarasına tıklarsın). list.push satırına book.price > 40 koşulunu yazdık, 24 kitaba tıkladık, debugger yalnız beş kez durdu:
conditional breakpoint (book.price > 40) paused for: "21:41", "22:42", "23:43", "24:44", "25:45"
Logpoint ise hiç durmayan bir breakpoint: aynı menüden Add logpoint seçip 'add', book.id, 'length', list.length gibi bir ifade yazarsın, satır her çalıştığında konsola düşer. Gerçek çıktının ilk üç satırı ve toplamı:
[logpoint] add 2 length 2
[logpoint] add 3 length 3
[logpoint] add 4 length 4
total logpoint lines: 24
Logpoint kodu değiştirmez, commit'e console.log kaçırmaz, rebuild beklemez ve DevTools kapanınca kaybolur. Sırf "bu satır çalışıyor mu, kaç kez, hangi değerle" sorusu için kod değiştirmek yerine bunu kullan. Breakpoints bölümünde her breakpoint'in yanında bir onay kutusu var; hepsini silmeden geçici olarak kapatabilirsin. Araç çubuğundaki Deactivate breakpoints (Ctrl+F8) ise tümünü birden askıya alır.
Stepping ve Async Call Stack
Durduğun yerden ilerlemek için beş komut var: Resume (F8 ya da Ctrl+\), Step over (F10), Step into (F11), Step out (Shift+F11) ve Step (F9). Step into bir async fonksiyon çağrısında fonksiyonun içine girer; await'ten sonra devam eden koda da step ile ulaşırsın. Satır numarasına sağ tıklayıp Continue to here dersen geçici bir breakpoint gibi orada durursun.
Call Stack'in en güçlü tarafı async frame'ler. Zoneless bir Angular uygulamasında change detection'ı Angular'ın scheduler'ı planlar. Rozetin DOM'u (Document Object Model) değiştiğinde (aşağıdaki DOM breakpoint'iyle) duran call stack şöyleydi:
DOM breakpoint .cart-badge subtree-modified: reason=DOM
App_Template (src/app/app.ts:12)
-- async: requestAnimationFrame
(30 frames from ignore-listed sources hidden)
Senkron kısımda senin kodundan yalnız App_Template var, gerisi Angular'ın render döngüsü. Altındaki -- async: requestAnimationFrame çizgisi işin asıl kısmı: bu render bir requestAnimationFrame callback'inde çalışmış. DevTools async call stack'leri varsayılan olarak toplar; CDP'de bunu Debugger.setAsyncCallStackDepth ayarlıyor. Event loop'un bu sırası JavaScript İleri serisinin "JavaScript Event Loop Derinlemesine: Task, Microtask ve Rendering Sırası" yazısında.
Satırın Değil Olayın Üzerine Breakpoint: DOM, XHR/fetch, Event Listener ve Exception
Bazen hangi satıra breakpoint koyacağını bilmezsin. "Bu element'i kim değiştiriyor" sorusu için Elements panelinde node'a sağ tıklayıp Break on altından subtree modifications, attribute modifications ya da node removal seçersin. Rozet için subtree modifications kurduk ve yukarıdaki call stack'i aldık: değişikliği app.ts'in 12. satırındaki template yapıyor.
"Bu isteği kim atıyor" sorusu için Sources'ta XHR (XMLHttpRequest)/fetch Breakpoints bölümüne bir URL parçası eklersin. /api/books/ ekleyip bir kitabın detayına tıkladık:
XHR/fetch breakpoint "/api/books/": reason=XHR
ngOnInit (src/app/book-detail/book-detail.ts:27)
-- async: requestAnimationFrame
(72 frames from ignore-listed sources hidden)
Ignore list açıkken 72 frame'lik HttpClient, RxJS ve router zinciri tek satıra iniyor: isteği BookDetail.ngOnInit başlatmış. Ignore list olmadan bu stack'te kendi satırını bulmak için ekranı kaydırman gerekirdi.
Event Listener Breakpoints bölümünde kategori kategori event'ler var (Mouse > click, Keyboard > keydown gibi); işaretlediğin event'in herhangi bir listener'ı çalışmadan hemen önce durur. Breakpoints bölümünün üstündeki iki kutu da exception'lar için: Pause on uncaught exceptions yakalanmayan hatada, Pause on caught exceptions ise try/catch içinde yutulan hatada bile durur. İkincisi, bir service'in hatayı yakalayıp sessizce boş liste döndürdüğü durumların ilacı.
Ignore List: Framework Kodunu Atlamak
Yukarıdaki çıktılarda "hidden" sayıları rastgele değil. Angular CLI, ürettiği source map'lere x_google_ignoreList alanını koyuyor: node_modules'tan gelen kaynakların indeksleri bu listede. Chrome'un Settings > Ignore list sekmesindeki "Automatically add known third-party scripts to ignore list" ayarı bu alanı okur ve dokümanın da belirttiği gibi Angular bunu sağlayan framework'lerden biri. Aynı sekmede custom exclusion rule olarak kendi regex'ini ekleyebilirsin; debugger varsayılan olarak /node_modules/ ve /bower_components/'ı zaten atlar.
Ignore list'teki script'ler Call Stack'te gizlenir ("Show ignore-listed frames" ile açarsın) ve step into onların içine girmez; update'e step into yaptığında Angular'ın signalUpdateFn'i yerine doğrudan senin arrow function'ına inersin. Bir framework hatasından şüpheleniyorsan frame'e sağ tıklayıp script'i listeden çıkarabilir, işin bitince geri ekleyebilirsin.
Düzeltme ve Kalıcı Kayıt: Workspaces, Snippets ve Local Overrides
Düzeltme tek satır: array'i yerinde değiştirmek yerine yeni bir array döndür.
add(book: Book): void {
this.items.update(list => [...list, { bookId: book.id, title: book.title, price: book.price, quantity: 1 }]);
}
FIXED after 3 clicks badge: 3 · $66.00 | logs: [..., "cart saved 1", "cart saved 2", "cart saved 3"]
Toplam artık doğru (21 + 22 + 23 = 66) ve effect her eklemede çalışıyor. Signal'lerle immutable update'in neden şart olduğu Angular serisinin "Angular 16 Yenilikleri: Signals, Required Input ve takeUntilDestroyed" yazısında.
Bu düzeltmeyi DevTools içinden yapıp doğrudan dosyaya kaydetmek istersen Workspaces var. Angular dev server bunu kolaylaştırıyor: ng serve /.well-known/appspecific/com.chrome.devtools.json adresinden projenin kök yolunu ve bir UUID'yi (Universally Unique Identifier) sunuyor:
{
"workspace": {
"root": ".../devtoolswork_01-10/bookstore-web",
"uuid": "09b65a18-b614-462d-af5d-5b989bd00a2a"
Sources > Workspace'te Connect'e tıklayıp dosya düzenleme iznini verdiğinde Sources'ta yaptığın değişiklikler diskteki dosyaya yazılır, dev server da yeniden derler. Dokümana göre Elements panelindeki DOM değişiklikleri kaydedilmez; kalıcı değişikliği Sources'ta yap.
Snippets, Sources > Snippets'te sakladığın ve Ctrl+Enter ile sayfada çalıştırdığın küçük script'lerdir; Command Menu'de ! yazıp adını vererek de çalıştırırsın. "Sepeti 20 ürünle doldur" ya da "tüm kartların fiyatlarını tabloya dök" gibi tekrar eden hata ayıklama adımları için iyi bir yer. Local Overrides ise kaynak kodunu değil, sunucudan gelen yanıtı ezer; bir sonraki yazıda API (Application Programming Interface) mock'lamak için kullanacağız.
Üretimde Dikkat
Hidden source map'leri production sunucusuna koyma. Dosya erişilebilir durumdaysa sourceMappingURL olmasa bile main-X24X3BNP.js.map adını tahmin eden herkes kaynak kodunu okur. Map'leri build pipeline'ında error tracking aracına yükleyip artefakttan çıkar.
Production build minify edilmiş ve ignore list'siz bir dünya: map yüklemediysen call stack ri, Tr gibi isimlerle dolar. Bu yüzden canlı bir hatayı önce development build'de reproduce etmeyi dene; olmuyorsa map'i elle ekleyerek Sources'ta incele.
Breakpoint'te duran bir sayfa her şeyi dondurur: timer'lar, animasyonlar ve bekleyen istekler. Timing'e duyarlı bir bug'da (debounce, timeout, WebSocket heartbeat) duraklamak davranışı değiştirebilir; orada logpoint daha dürüst bir gözlem aracıdır.
Sık Düşülen Tuzaklar
- Breakpoint'i derlenmiş
main.js'e koymak: TypeScript dosyası açılmıyorsa source map'in yüklenip yüklenmediğine bak. - Arrow function frame'inde
this'e bakmak: bizim denemedethisoradaundefinedgeldi; class'ın state'i için Call Stack'te method'un frame'ini seç. - Pause on caught exceptions'ı açık unutmak ve her framework içi exception'da durmak.
- Sayaç doğru diye signal'in doğru çalıştığını sanmak: template'in yeniden okuması signal bildiriminin yerini tutmaz.
- Ignore list'i kapatıp framework'ün içinde kaybolmak; ya da tersine, framework'ten şüphelenirken listeyi açık bırakıp ilgili frame'i hiç görmemek.
- Hidden source map'leri
distklasörüyle birlikte deploy etmek.
Küçük Bir Deneme
★☆☆ Hatalı CartStore'u geri getir, return list; satırına bir breakpoint koy ve durduğunda Call Stack'te "Show ignore-listed frames"i aç. Gizlenen frame'lerin adlarını oku, sonra tekrar kapat.
★★☆ list.push satırına logpoint koy, ardından aynı satıra book.stock === 0 koşullu bir breakpoint ekle. Stokta olmayan bir kitap sepete eklendiğinde durduğunu doğrula ve Watch'a book.title ile list.length ekle.
★★★ BookService.getBooks'a bir catchError ekleyip hatada boş array döndür, API'yi durdur ve listeyi aç. Sayfa boş geliyor ama konsolda hata yok. Pause on caught exceptions ile hatanın nerede yutulduğunu bul, sonra XHR/fetch breakpoint'iyle isteği hangi component'in başlattığını doğrula.
XHR/fetch breakpoint'i isteği kimin attığını gösterdi; ama isteğin kendisi, header'ları, ne kadar beklediği ve neden yavaş olduğu Network panelinin işi. Bir sonraki yazıda waterfall'u, timing fazlarını, throttling'i, HAR (HTTP Archive) dosyalarını ve backend hazır değilken API'yi Local Overrides ile mock'lamayı ele alıyoruz.