Console Paneli Derinlemesine: console API, Live Expression, Utility'ler ve Angular'ın ng Global'leri
Console paneli bir log penceresi değil, sayfanın içine açılan bir REPL'dir: seviyeler ve filtreler, console.time/count/assert/trace, top-level await, Live Expression ve $$, getEventListeners, monitorEvents gibi utility'ler. Angular 22'de ng.getComponent, getContext, getListeners ve getDirectiveMetadata; OnPush ve zoneless dünyada düz field için neden ng.applyChanges gerektiği ve log'lanan object'in canlı referans tuzağı, Chrome 156'dan gerçek çıktılarla.
Sürüm notu: Bu yazıdaki örnekler Chrome 156 (156.0.8078.80), Angular 22.2 ve .NET 10 ile denenmiştir. Konsol çıktıları aynı Chrome sürümünde, headless modda Chrome DevTools Protocol (CDP) üzerinden, Console panelinin kullandığı Command Line API (Application Programming Interface) açıkken alındı; panel ayarları Chrome DevTools dokümanından aktarıldı.
Önce Tahmin Et
İki küçük soru. Birincisi: konsolda şunu çalıştırıyorsun:
window.order = { id: 1042, status: 'Created', items: [{ title: 'Compilers Volume 1' }] };
console.log('order', order);
order.status = 'Paid';
order.items.push({ title: 'Patterns Volume 4' });
Log satırındaki object'i birkaç saniye sonra açtığında status ne yazar, items kaç element'li görünür? İkincisi: Bookstore'un sepet sayfasındaki Cart component'inde note adında düz bir field var. Konsoldan ng.getComponent($('app-cart')).note = 'Changed from console' yazınca ekrandaki metin değişir mi? Angular 22'de component'ler varsayılan olarak OnPush ve uygulama zoneless; bu iki bilgi cevabı değiştiriyor mu?
Önceki yazıda Elements panelinde seçtiğin node'u konsolda $0 olarak kullandık. Bu yazıda Console panelini log okuma penceresi olmaktan çıkarıp sayfanın içine açılan bir REPL (Read-Eval-Print Loop) gibi kullanıyoruz.
Log Seviyeleri, Filtreler ve Preserve log
Console'da dört seviye var: Verbose, Info, Warning ve Error. console.log ve console.info Info, console.warn Warning, console.error Error, console.debug ise Verbose seviyesine düşer. Varsayılan seviye seçiminde Verbose kapalıdır; "debug ile yazdım, görünmüyor" şikâyetinin cevabı genelde Log Levels açılır menüsündedir. Sol taraftaki Console Sidebar (araç çubuğundaki ikon) mesajları kaynağa ve seviyeye göre gruplar; hangi dosyanın konsolu doldurduğunu tek bakışta görürsün.
Filter kutusu üç tür girdi kabul eder. Düz metin o metni içeren mesajları gösterir. /cart\s(saved|cleared)/ gibi bir regex de yazabilirsin; boşluk yerine \s kullanman gerekir. url: önekiyle yalnız belli bir kaynaktan gelen mesajları, -url: ile o kaynak dışındakileri görürsün. Angular dev server'ın [vite] connecting... satırlarından sıkıldıysan -url:@vite/client iş görür.
Console Settings (dişli ikonu) altında üç ayar günlük işte önemli. Preserve log, sayfa yenilendiğinde ya da başka bir sayfaya gidildiğinde mesajların silinmemesini sağlar; login sonrası redirect'te kaybolan hatayı yakalamanın tek yolu budur. Selected context only, iframe ya da worker gibi birden fazla JavaScript context'i olduğunda yalnız seçili context'in mesajlarını gösterir. Log XMLHttpRequests her fetch ve XHR (XMLHttpRequest) isteğini konsola yazar. Konsolu temizlemek için Ctrl+L (Mac'te Cmd+K) ya da clear().
console API'nin Az Kullanılan Method'ları
Çoğu ekip konsolu yalnız console.log ile kullanır. Oysa hata ayıklamayı hızlandıran method'lar daha fazlası. Bookstore listesinde şunu çalıştırdık:
console.time('filter');
const hits = $$('app-book-card').filter(c => c.textContent.includes('Compilers'));
console.timeEnd('filter');
console.count('add-to-cart'); console.count('add-to-cart');
console.assert(hits.length === 0, 'expected no Compilers books, got', hits.length);
console.log('%cBookstore%c debug', 'color: #38bdf8; font-weight: 700', 'color: inherit');
console.group('cart'); console.info('items: 0'); console.groupEnd();
function addToCart() { console.trace('who called addToCart?'); }
function onClick() { addToCart(); }
onClick();
console.debug('verbose message');
CDP'nin Runtime.consoleAPICalled event'inden gelen mesajlar, tipleriyle birlikte:
[timeEnd] filter: 1.671142578125 ms
[count] add-to-cart: 1
[count] add-to-cart: 2
[assert] expected no Compilers books, got 286
[log] %cBookstore%c debug color: #38bdf8; font-weight: 700 color: inherit
[startGroup] cart
[info] items: 0
[endGroup] console.groupEnd
[trace] who called addToCart? | stack: addToCart:9 <- onClick:10 <- (anonymous):11
[debug] verbose message
Satır satır okuyalım. time/timeEnd aynı etiketle eşleşir ve aradaki süreyi milisaniye olarak yazar; 2000 kart içinde metin aramak 1.67 ms sürdü. Bu bir mikro ölçümdür, gerçek performans sorusu için Performance paneline gideceğiz. count aynı etiketin kaçıncı kez çağrıldığını sayar; bir event handler'ın kaç kez tetiklendiğini görmek için birebir. assert yalnız koşul yanlışsa yazar, burada 286 "Compilers" kitabı bulunduğu için mesaj geldi; panelde bu satır Error seviyesinde "Assertion failed:" önekiyle görünür. %c sonraki argümandaki CSS'i (Cascading Style Sheets) uygular, CDP ham argümanları verir, renklendirmeyi panel yapar. group/groupEnd mesajları katlanabilir bir başlık altında toplar, groupCollapsed kapalı başlatır. trace ise o noktadaki call stack'i yazar: addToCart'ı onClick çağırmış.
console.table bu listede yok çünkü çıktısı bir tablo görünümü; ama en çok vakit kazandıranlardan biri. console.table(books.slice(0, 5), ['title', 'price']) yazınca yalnız seçtiğin kolonlar gelir ve kolon başlığına tıklayarak sıralayabilirsin. Array of object'leri log'larken düz console.log yerine bunu dene.
Konsolda Kod Çalıştırmak: Eager Evaluation, await ve Live Expression
Konsola yazarken Eager evaluation ayarı açıksa sonuç sen Enter'a basmadan önce gri bir önizleme olarak çıkar. DevTools bu önizlemeyi yan etkisi olmayacağından emin olduğu ifadeler için yapar; order.items.push(...) gibi state değiştiren bir ifadenin önizlemesi gelmez.
Konsol top-level await destekler, async fonksiyon sarmalına gerek yok:
(await (await fetch('http://localhost:5391/api/books/3')).json()).title
// "Networks Volume 3"
Son değerlendirilen ifadenin sonucu $_ içinde durur: 6 * 7 yazıp ardından $_ dediğimizde 42 döndü. Log'lanmış bir object'e sağ tıklayıp Store as global variable dersen temp1, temp2 gibi global değişkenlere atanır ve konsolda üzerinde çalışabilirsin.
Live Expression ise sürekli izlemek istediğin bir ifade için. Console araç çubuğundaki göz ikonuna (Create live expression) tıklayıp ifadeyi yazarsın, sonuç konsolun üstünde sabit durur ve 250 milisaniyede bir yeniden hesaplanır. Bookstore'da iki iyi aday: document.activeElement klavye focus'unun nerede olduğunu canlı gösterir, $$('app-book-card').length filtreye yazarken DOM'daki (Document Object Model) kart sayısını izler. İfade her 250 ms'de çalıştığı için ağır bir hesaplama koyma, sayfayı sen yavaşlatırsın.
Console Utilities API: $, $$, monitorEvents, getEventListeners, queryObjects
Bunlar JavaScript'in parçası değil; yalnız DevTools konsolunda var olan yardımcılar. Sayfanın kendi kodundan çağıramazsın. Bookstore'da çalıştırdıklarımız ve gerçek sonuçları:
$('app-book-card .price').textContent
// "$21.00"
$$('app-book-card').length
// 2000
Object.keys(getEventListeners($0)) // $0: ilk kartın butonu
// ["click"]
getEventListeners($0).click[0] // yalnız seçtiğimiz alanlar
// {"type":"click","useCapture":false,"passive":false,"once":false}
$() document.querySelector'ın, $$() ise querySelectorAll'ın kısaltması; fark şu ki $$ NodeList değil gerçek bir array döndürür, filter ve map doğrudan çalışır. Sayfada jQuery yüklüyse $ jQuery'nin olur, DevTools'unki ezilir. $0 Elements'te son seçtiğin node, $1'den $4'e kadar ondan önce seçtiklerin.
getEventListeners element'e addEventListener ile bağlanmış listener'ları event tipine göre gruplanmış bir object olarak döndürür. Elements panelindeki Event Listeners sekmesi aynı bilgiyi gösterir; orada "Framework listeners" seçeneği de vardır. monitorEvents ise bir element'in event'lerini konsola yazar. Filtre input'u için monitorEvents($('.filter'), 'input') çalıştırıp iki harf yazınca konsola iki satır düştü:
input InputEvent
input InputEvent
İşin bitince unmonitorEvents($('.filter')). İkinci argümana 'key' ya da 'mouse' gibi bir grup adı da verebilirsin. copy(value) değeri panoya kopyalar; copy(JSON.stringify(temp1, null, 2)) bir API yanıtını test fixture'ına taşımanın en kısa yolu. queryObjects(Constructor) heap'te o constructor'dan yaratılmış tüm instance'ları listeler; Memory yazısında leak avında kullanacağız.
Angular'ın ng Global'leri
Development build'de Angular window.ng'ye hata ayıklama fonksiyonları ekler. Angular 22.2'de Object.keys(ng)'nin çıktısı:
applyChanges, enableProfiling, getComponent, getContext, getDirectiveMetadata, getDirectives,
getHostElement, getInjector, getListeners, getOwningComponent, getRootComponents, isSignal,
toggleWatchSignal, ɵgetComponentInstanceDeepLinkId, ɵgetControlFlowBlocks,
ɵgetDependenciesFromInjectable, ɵgetInjectorMetadata, ɵgetInjectorProviders,
ɵgetInjectorResolutionPath, ɵgetLoadedRoutes, ɵgetRouterInstance, ɵgetSignalGraph,
ɵgetTransferState, ɵnavigateByUrl, ɵsetProfiler
Angular dokümanının "window.ng globals" bölümünde yalnız ilk on bir fonksiyon yer alıyor. ɵ ile başlayanlar private API: Angular DevTools extension'ı bunları kullanıyor, sen kodunda kullanma, sürümden sürüme değişebilir. Kartın butonu $0 iken gelen sonuçlar:
ng.getListeners($0)
// [{"name":"click","type":"dom","useCapture":false}]
ng.getListeners($0.closest('app-book-card'))
// [{"name":"addToCart","type":"dom"},{"name":"addToCart","type":"output"}]
ng.getContext($0.closest('app-book-card')) // anahtarlar, $index ve kitabın adı
// {"keys":["lContainer","$implicit","$index"],"index":0,"title":"Compilers Volume 1"}
ng.getDirectiveMetadata(ng.getComponent($0.closest('app-book-card')))
// {"inputs":{"book":"book"},"outputs":{"addToCart":"addToCart"},"encapsulation":0,"changeDetection":0}
getListeners, getEventListeners'tan farklı olarak Angular'ın template'te bağladığı listener'ları gösterir ve tipini söyler: dom bir DOM event'i, output bir component output'u. Host element'te addToCart'ın iki kez çıkması da bundan: BookList template'indeki (addToCart) binding'i için Angular hem host element'e aynı adlı bir DOM listener'ı hem de output'a bir abonelik kaydetmiş; emit çağrısı output tarafından gelir. getContext bir @for içindeki element için loop context'ini verir: $implicit o satırın kitabı, $index sırası. "Bu satır hangi veriyle çizildi" sorusunun en kısa cevabı.
getDirectiveMetadata'daki iki sayıya dikkat. encapsulation: 0 ViewEncapsulation.Emulated, changeDetection: 0 ise ChangeDetectionStrategy.OnPush. BookCard'da changeDetection yazmadık; Angular 22'de varsayılan OnPush olduğu için 0 geldi. Angular 22'nin kaynağında eski Default değeri yerini Eager'a bırakmış, ikisi de 1 ve Default deprecated.
getInjector element'in injector'ını verir; ng.getInjector(card).get(CartStoreClass) ile component'in gördüğü service instance'ına ulaşırsın. Bizim denemede karttan alınan CartStore, BookList'teki instance'la aynı çıktı (sameStore: true): root'ta provide edilmiş tek instance. getRootComponents ise beklenmedik bir şey gösterdi: app-root ve nav linki için ["_App"] dönerken router'ın yüklediği BookList'in içindeki bir element için ["_BookList"] döndü. Router'ın oluşturduğu component'in view'ı kendi kök view'ı gibi davranıyor; "root component" deyince bootstrap edileni bekliyorsan bu sonucu bil.
applyChanges: OnPush ve Zoneless Dünyasında Konsoldan Değişiklik
Önce Tahmin Et'teki ikinci soruya dönelim. Cart component'i:
@Component({
selector: 'app-cart',
imports: [CurrencyPipe],
template: `
<h2>Cart</h2>
<p class="summary">{{ cart.items().length }} items, total {{ cart.total() | currency: 'USD' }}</p>
<p class="note">{{ note }}</p>
`,
})
export class Cart {
protected readonly cart = inject(CartStore);
protected note = 'Free shipping over $50';
}
Konsoldan field'ı değiştirip 300 ms bekledik, sonra ng.applyChanges çağırdık:
$('.note').textContent
// "Free shipping over $50"
ng.getComponent($('app-cart')).note = 'Changed from console'
$('.note').textContent // 300 ms sonra
// "Free shipping over $50"
ng.applyChanges(ng.getComponent($('app-cart')))
$('.note').textContent
// "Changed from console"
Ekran değişmedi. Zone.js'li ve Default stratejili eski dünyada bu atama, bir sonraki change detection turunda (örneğin sayfadaki ilk tıklamada) ekrana yansırdı; zoneless bir uygulamada ise Angular'a "bir şey değişti" diyen bir sinyal yok ve component OnPush olduğu için zaten kirli işaretlenmeden kontrol edilmez. applyChanges dokümanda yazdığı gibi component'i check için işaretler ve uygulamada change detection'ı senkron çalıştırır. Signal'de durum farklı: ng.getComponent($('app-book-list')).query.set('Compilers') yazdığımızda, applyChanges çağırmadan sayaç "286 books" oldu, çünkü signal'e yazmak kendi başına bir render planlıyor. Zone.js ve OnPush'un mekaniği Angular serisinin "Angular'da Change Detection ve Zone.js: OnPush, ChangeDetectorRef ve NgZone" ve "Angular 22: Varsayılan OnPush, Stable Signal Forms ve Angular 12 ile Proje Farkı" yazılarında.
Listenin son önemli üyesi ng.enableProfiling(): Performance panelinde Angular'a özel bir track açar. Onu 06. yazıda trace kaydederken kullanacağız.
Log'lanan Object Canlı Referanstır
Şimdi ilk soruya. Aynı object'i iki kez log'ladık, biri doğrudan, biri structuredClone ile kopyalanarak, sonra object'i değiştirdik. Log anında alınan önizleme ile object'i sonradan açınca (Runtime.getProperties) gelen değerler:
[order] preview at log time: {id: 1042, status: Created, items: Array(1)} | expanded now: status=Paid, items=Array(2)
[order snapshot] preview at log time: {id: 1042, status: Created, items: Array(1)} | expanded now: status=Created, items=Array(1)
Konsoldaki kapalı satır log anının önizlemesini gösterir: status: Created. Ama ok'a tıklayıp açtığın an DevTools object'in o anki property'lerini ister ve Paid ile iki element'li array görürsün. Panel bunu açılan object'in yanına küçük bir "i" ikonuyla da belirtir: değer şimdi hesaplandı. Sorun genelde şöyle yaşanır: bir effect'in içinde console.log(items) yazarsın, satırı açınca son hâli görürsün ve "log zaten doğru değeri gösteriyor, demek ki hata başka yerde" diye yanlış yöne gidersin.
İkinci satır structuredClone ile alınmış derin kopya; açtığında da log anındaki hâlini koruyor: status=Created, tek element'li array. Log anındaki değeri görmek istiyorsan ya kopya log'la ya da console.log('status', order.status) gibi primitive bir değer yaz. JSON.stringify(order) da işe yarar ama Map, Set, Date gibi tipleri bozar.
Üretimde Dikkat
console.log çağrıları production build'den kendiliğinden silinmez. Bookstore'un ng build çıktısında main-*.js içinde console.log("cart saved",n.length) aynen duruyordu. Kullanıcının tarayıcısında açık duran konsola sipariş, token ya da kişisel veri yazdığını unutma; production'da log'u bir logging service'i üzerinden seviyeye göre kapatılabilir yap.
DevTools açıkken konsola log'ladığın object'ler panel tarafından referans olarak tutulur, garbage collector onları toplayamaz. Bellek ölçerken konsolu temizle ve gereksiz log'ları kaldır, yoksa sızıntıyı kendi log'un üretir.
ng global'leri production build'de yok; canlı ortamda bir component'in state'ine bakman gerekiyorsa source map'li Sources panelini ve breakpoint'leri kullan. Console Utilities de yalnız konsolda çalışır; $$ ya da copy'yi bir Snippet'te kullanabilirsin ama uygulama koduna koyarsan ReferenceError alırsın.
Sık Düşülen Tuzaklar
- Log'u açınca gördüğün değeri log anındaki değer sanmak; yukarıdaki iki satırı hatırla.
console.debugyazıp Verbose seviyesi kapalı olduğu için mesajı hiç görmemek.- Redirect ya da reload sonrası kaybolan hatayı aramak; Preserve log'u aç.
- Konsoldan düz bir field'ı değiştirip ekranın güncellenmesini beklemek; Angular 22'de
ng.applyChangesgerekir ya da state'i signal'de tut. ɵöneklingfonksiyonlarına dayanan script'ler yazmak; her Angular sürümünde kırılabilir.- Live Expression'a ağır bir ifade koyup sayfanın neden yavaşladığını aramak.
Küçük Bir Deneme
★☆☆ Bookstore listesinde console.table($$('app-book-card').slice(0, 5).map(c => ng.getComponent(c).book()), ['title', 'price']) çalıştır, kolonları sırala. Sonra $$('app-book-card').length için bir Live Expression oluştur ve filtreye yazarken değerin değiştiğini izle.
★★☆ Sepete ekleme butonuna tıkladıkça kaç kez çağrıldığını görmek için BookList'teki handler'a geçici bir console.count('add-to-cart') koy. Konsolda ng.getListeners ile butonun listener'ını, ng.getContext ile o satırın kitabını bul ve sayacı Preserve log açık/kapalı iken sayfayı yenileyerek karşılaştır.
★★★ Bir effect içinde sepet array'ini log'la, sonra array'i yerinde değiştiren bir kod yolu ekle. Konsoldaki önizleme ile açılmış hâli karşılaştır, structuredClone ile düzelt. Ardından aynı veriyi signal'e yeni bir array referansı vererek güncelle ve ng.applyChanges olmadan ekranın güncellendiğini doğrula.
Konsolda bir şeyin "ne" olduğunu görmek kolay; "neden ve ne zaman" olduğunu görmek için kodu durdurman gerekir. Son denemede sepetin bazen güncellenmediğini fark edeceksin; bir sonraki yazıda Sources paneliyle breakpoint türlerini, source map'leri ve ignore list'i kullanarak tam olarak bu effect'in neden tetiklenmediğini adım adım bulacağız.