Application Paneli: Storage, Cookie Flag'leri, Cache Storage ve Angular Service Worker
Application paneli tarayıcının sakladığı her şeyi gösterir: localStorage, IndexedDB, kota, HttpOnly/Secure/SameSite/Partitioned cookie'ler ve reddedilen bir cookie'nin nedeni (SameSiteNoneInsecure). Angular Service Worker deploy sonrası ilk yenilemede eski bundle'ı sunar; /ngsw/state, Cache Storage, SwUpdate VERSION_READY, Update on reload ve Bypass for network ile teşhis, manifest ve bfcache testi.
Sürüm notu: Bu yazıdaki örnekler Chrome 156 (156.0.8078.80), Angular 22.2 (@angular/service-worker 22.2) ve .NET 10 (ASP.NET Core 10) ile denenmiştir. Storage, cookie ve Service Worker çıktıları aynı Chrome sürümünde, headless modda Chrome DevTools Protocol (CDP) üzerinden, Application panelinin kullandığı domain'lerden alındı; panel adımları Chrome DevTools dokümanından aktarıldı.
Önce Tahmin Et
Bookstore'un production build'i ASP.NET Core'un static file'larından, Angular Service Worker'ıyla sunuluyor. Üst menüdeki "Books" linkinin adını "Catalog" yaptın, build alıp dosyaları sunucuya kopyaladın. Siteyi önceden açmış bir kullanıcı sayfayı yeniliyor. Ne görür: "Books" mu, "Catalog" mu? Peki bir kez daha yenilerse?
İkinci soru: API (Application Programming Interface) şu dört cookie'yi yazıyor. Hangileri tarayıcıda saklanır, hangileri document.cookie'de görünür?
Set-Cookie: cart_id=c-1042; path=/; secure; samesite=lax; httponly
Set-Cookie: theme=dark; path=/; samesite=lax
Set-Cookie: tracking=t-77; path=/; samesite=none
Set-Cookie: widget_pref=compact; path=/; secure; samesite=none; Partitioned
Site http://localhost:5391, yani HTTPS (HTTP Secure) değil. Cevabın bu ayrıntıya bağlı olduğunu düşünüyorsan doğru yoldasın.
localStorage, sessionStorage ve IndexedDB
Application panelinin solunda Storage başlığı altında Local storage, Session storage, IndexedDB ve Cookies durur; her biri origin'e göre listelenir. Bookstore sepeti localStorage'a, son bakılan kitapları IndexedDB'ye yazıyor:
remember(bookId: number): void {
const open = indexedDB.open('bookstore-db', 1);
open.onupgradeneeded = () => open.result.createObjectStore('recentlyViewed', { keyPath: 'bookId' });
open.onsuccess = () => {
const tx = open.result.transaction('recentlyViewed', 'readwrite');
tx.objectStore('recentlyViewed').put({ bookId, viewedAt: new Date().toISOString() });
tx.oncomplete = () => open.result.close();
};
}
İki kitabı sepete ekleyip birinin detayına girdikten sonra panelin gösterdiği veriler:
--- localStorage
bookstore.cart = [{"bookId":1,"title":"Compilers Volume 1","price":21,"quantity":1},{"bookId":2,"title":"Databases Volume 2","price":22,"
--- IndexedDB databases: ["bookstore-db"]
version 1 stores recentlyViewed keyPath=bookId
entry 1 {"bookId":1,"viewedAt":"2026-11-08T12:03:17.254Z"}
Local storage tablosunda bir satıra çift tıklayıp key ya da value'yu düzenleyebilir, satırı seçip Delete ile silebilir, üstteki filtreyle key arayabilirsin. Sepeti elle bozmak ("price" alanını string yap, JSON'u (JavaScript Object Notation) yarıda kes) uygulamanın bozuk veriye nasıl tepki verdiğini görmenin en hızlı yolu; JSON.parse etrafında try/catch yoksa sayfa beyaz kalır. Session storage aynı tablo, ama yalnız o sekme yaşadıkça durur.
IndexedDB görünümü canlı değildir: kod yeni bir kayıt yazdığında tablo kendiliğinden yenilenmez, object store'un üstündeki yenile düğmesine basman gerekir. Chrome 155–156 ile IndexedDB tablosunda iç içe object'ler satır yüksekliği büyüyerek açılabiliyor ve kaydırma konumu korunuyor. Veritabanına sağ tıklayıp Delete database dersen o origin'in veritabanı tamamen silinir; version yükseltme (onupgradeneeded) kodunu sıfırdan test etmek için bunu kullanırsın. Bu API'lerin kendisi JavaScript Orta serisinin "JavaScript Web Storage ve Cookie: localStorage, sessionStorage ve document.cookie" yazısında.
Storage Kotası ve Clear site data
Application > Storage sayfası origin'in ne kadar yer kullandığını tipine göre gösterir. Bookstore'un değerleri:
--- quota: usage 423559 bytes, quota 555.8 GiB, breakdown [{"storageType":"indexeddb","usage":2276},{"storageType":"cache_storage","usage":336640},{"storageType":"service_workers","usage":84643}]
Kullanılan 423 KB'nin büyük kısmı Service Worker'ın Cache Storage'ı; IndexedDB yalnız 2 KB. Kota disk boyutuna göre hesaplanır, buradaki 555.8 GiB bu makineye özgü; kullanıcının telefonunda çok daha küçük olur. Aynı sayfada "Simulate custom storage quota" ile düşük bir kota tanımlayıp "kota dolunca ne oluyor" senaryosunu deneyebilirsin.
Clear site data düğmesi, seçtiğin tipleri (cookie'ler, storage, cache, Service Worker kaydı) tek tıkla siler. Bizim denemede düğmenin CDP karşılığı olan Storage.clearDataForOrigin sonrasında:
--- after Clear site data: localStorage entries 0 | cookies 0
"Bende bozuk, yeni profilde düzgün" türündeki hataların çoğu eski bir storage kaydı ya da eski bir Service Worker'dır. Hatayı temiz bir state'te tekrar etmenin en kısa yolu bu düğme; ama önce neyin bozuk olduğunu tablolardan okumayı unutma, silince kanıt da gider.
Cookie'ler: HttpOnly, Secure, SameSite ve Partitioned
Önce Tahmin Et'teki ikinci sorunun cevabı. /api/session'dan sonra Cookies tablosunda kalanlar ve sayfanın gördüğü:
stored cookies:
theme=dark httpOnly=false secure=false sameSite=Lax partitionKey=- session=true
cart_id=c-1042 httpOnly=true secure=true sameSite=Lax partitionKey=- session=true
widget_pref=compact httpOnly=false secure=true sameSite=None partitionKey={"topLevelSite":"http://localhost","hasCrossSiteAncestor":false} session=true
document.cookie -> "theme=dark; widget_pref=compact"
Dört cookie'den üçü saklandı. cart_id HttpOnly olduğu için document.cookie'de yok: JavaScript onu okuyamaz, bir XSS (Cross-Site Scripting) açığı olsa bile çalınamaz. Tablodaki HttpOnly kolonunda tik gördüğün cookie'ler bunlar. Secure işaretli cart_id'nin düz HTTP üzerinden saklanması şaşırtıcı görünebilir; Chrome localhost'u güvenli bir context sayar, bu yüzden yerelde çalışır. Aynı davranışı gerçek bir HTTP domain'inde bekleme.
widget_pref Partitioned attribute'u taşıdığı için CHIPS (Cookies Having Independent Partitioned State) kuralıyla saklandı: Cookies tablosundaki Partition Key Site kolonunda üst seviye site (http://localhost) görünür. Partitioned bir cookie, bir iframe içinde üçüncü taraf olarak yazıldığında yalnız o üst seviye site altında okunur; aynı widget başka bir sitede gömülüyken ayrı bir cookie kavanozu kullanır. ASP.NET Core'un CookieOptions'ında bunun için ayrı bir property yok, attribute'u Extensions ile ekledik:
CookieOptions partitioned = new() { Secure = true, SameSite = SameSiteMode.None, Path = "/" };
partitioned.Extensions.Add("Partitioned");
http.Response.Cookies.Append("widget_pref", "compact", partitioned);
Cookies tablosunda bir cookie'ye çift tıklayıp değerini değiştirebilir, Chrome 155–156'dan beri seçili satırları klavyeyle ya da sağ tık menüsüyle silebilirsin. Bu flag'lerin güvenlik açısından ne anlama geldiği, OWASP (Open Worldwide Application Security Project) Top 10'u işleyen ve JWT (JSON Web Token) ile CSRF (Cross-Site Request Forgery) konularına da giren Web Güvenliği serisinin "OWASP A07: Authentication Failures (2) — Cookie Flag'leri, JWT Doğrulama ve CSRF" yazısında; burada aracı nasıl okuyacağımıza odaklanıyoruz.
Bloklanan Bir Cookie'nin Nedenini Okumak
Dördüncü cookie tracking tabloda yok. Chrome onu sessizce reddetti ve nedeni yalnız DevTools'ta yazıyor. Network panelinde /api/session isteğinin Cookies sekmesinde, reddedilen Set-Cookie satırı sarı arka planla ve bir uyarı ikonuyla görünür; üzerine gelince sebep çıkar. CDP'nin Network.responseReceivedExtraInfo event'inden okuduğumuz:
blocked Set-Cookie:
tracking=t-77; path=/; samesite=none -> SameSiteNoneInsecure
SameSite=None diyen bir cookie Secure da olmak zorunda; değilse reddedilir. Aynı sekmedeki "show filtered out request cookies" seçeneği de istekle gönderilmeyen cookie'leri ve nedenlerini (SameSite, path, domain uyuşmazlığı) gösterir. "Login oluyorum ama bir sonraki istekte oturum yok" şikâyetinin ilk bakılacak yeri burası.
Denemede bir tuzağa da düştük. Production build'de istek Service Worker'ın üzerinden geçtiğinde sayfanın Network kaydında bloklanan cookie bilgisi gelmedi; Service Worker'ı bypass edince (Application > Service workers > Bypass for network'ün CDP karşılığı) yukarıdaki satır göründü. Service Worker'ın kendi yaptığı fetch'ler Network panelinde ayrı satırlar olarak, dişli ikonuyla görünür; cookie'yi orada ara ya da bypass'ı aç.
Service Worker ve Cache Storage: Angular'ın ngsw'si
ng add @angular/pwa projeye ngsw-config.json'ı, manifest'i ve şu provider'ı ekler:
provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(),
registrationStrategy: 'registerWhenStable:30000',
}),
enabled: !isDevMode() yüzünden ng serve'de Service Worker yok; bu yazının bütün Service Worker çıktıları production build'den. Application > Service workers bölümü kayıtlı worker'ı, durumunu ve script'ini gösterir; Cache Storage altında da Angular'ın açtığı cache'ler listelenir:
--- SW versions: id=0 status=activated running=running script=/ngsw-worker.js
--- Cache Storage:
ngsw:/:db:control
ngsw:/:cdfdb3838596c99e0523bcd9e2c09cc01eab4adf:assets:app:cache
ngsw:/:db:cdfdb3838596c99e0523bcd9e2c09cc01eab4adf:assets:app:meta
ngsw:/:cdfdb3838596c99e0523bcd9e2c09cc01eab4adf:assets:assets:cache
ngsw:/:db:cdfdb3838596c99e0523bcd9e2c09cc01eab4adf:assets:assets:meta
entries in ngsw:/:cdfdb...:assets:app:cache : /chunk-A4SHIOZ7.js, /chunk-E6ZZI456.js, /favicon.ico, /index.html, /main-66HIL7BB.js, /manifest.webmanifest, /styles-5INURTSO.css
Cache adlarındaki uzun hash, ngsw.json manifest'inin hash'i, yani uygulamanın sürümü. app grubu ngsw-config.json'daki "installMode": "prefetch" yüzünden index.html'den main-*.js'e kadar her şeyi önceden indirmiş. Angular Service Worker'ın kendi debug sayfası da var; aynı origin'de /ngsw/state'i açman yeter:
NGSW Debug Info:
Driver version: 22.2.5
Driver state: NORMAL ((nominal))
Latest manifest hash: cdfdb3838596c99e0523bcd9e2c09cc01eab4adf
Last update check: 2s397u
=== Version cdfdb3838596c99e0523bcd9e2c09cc01eab4adf ===
Bu sayfa kullanıcının tarayıcısında da çalışır. Destek ekibine "adres çubuğuna siteadi/ngsw/state yazıp ekran görüntüsü gönderin" demek, "hangi sürümdesiniz" sorusunun kesin cevabını verir. Driver state'in NORMAL dışında bir değer olması (örneğin cache bozulduğunda EXISTING_CLIENTS_ONLY) da kendi başına bir teşhistir.
Deploy Sonrası Eski Bundle: Adım Adım Teşhis
Önce Tahmin Et'teki ilk soruyu gerçekten denedik: "Books"u "Catalog" yapan yeni build'i sunucuya kopyaladık ve sayfayı aralıklarla yeniledik (/ngsw/state satırlarını ve hash'leri kısalttık):
--- reload before deploy: nav text="Books" main=main-66HIL7BB.js fromServiceWorker=true
--- 1st reload after deploy: nav text="Books" main=main-66HIL7BB.js fromServiceWorker=true
--- /ngsw/state after 2000 ms more: Latest manifest hash: cdfdb383... | Last update run: never
--- /ngsw/state after 8000 ms more: Latest manifest hash: 967d18e8... | === Version cdfdb383... === | === Version 967d18e8... ===
--- app logs so far: ["new version ready cdfdb3838596c99e0523bcd9e2c09cc01eab4adf -> 967d18e899c22aee9e1447696e0da770fa68f19c"]
--- 2nd reload after deploy: nav text="Catalog" main=main-HRXDAST5.js fromServiceWorker=true
Deploy sonrası ilk yenilemede kullanıcı hâlâ eski sürümü, eski main-66HIL7BB.js'i gördü; yanıt sunucudan değil Service Worker'dan geldi. Angular Service Worker güncelleme kontrolünü navigasyonda planlıyor ama sayfa boşta kalınca çalıştırıyor: iki saniye sonra Last update run: never, on saniye içinde ise yeni manifest indirilmiş ve iki sürüm yan yana duruyor. Uygulama bunu SwUpdate ile öğrendi:
if (this.swUpdate.isEnabled) {
this.swUpdate.versionUpdates
.pipe(filter((e): e is VersionReadyEvent => e.type === 'VERSION_READY'))
.subscribe(e => console.log('new version ready', e.currentVersion.hash, '->', e.latestVersion.hash));
}
Ve ancak ikinci yenilemede "Catalog" geldi. Gerçek hayatta bu "deploy ettim ama kullanıcı eski ekranı görüyor" şikâyeti olarak gelir; yeni bir API alanını bekleyen eski bundle da hata fırlatır. Çözüm Angular dokümanının önerdiği yol: VERSION_READY geldiğinde kullanıcıya "yeni sürüm var, yenile" diyen bir uyarı göster ya da uygun bir anda document.location.reload() çağır.
Geliştirirken bu davranış seni yavaşlatır. Application > Service workers'ta üç kutu var. Update on reload her yenilemede worker'ı güncellemeye zorlar; bir sonraki build'i sunucuya koyup yalnız bu ayar açıkken yenilediğimizde ilk yenilemede yeni sürüm ("All Books") geldi. Bypass for network istekleri Service Worker'ı atlayıp doğrudan ağa gönderir; o da ilk yenilemede yeni bundle'ı, bu kez fromServiceWorker=false ile getirdi. Offline ise Service Worker'ın çevrimdışı davranışını test etmek içindir. Unregister kaydı siler. Kullanıcının makinesinde bu kutular yok; orada teşhis /ngsw/state ve Cache Storage'daki hash ile yapılır, kalıcı çözüm de SwUpdate akışıdır. Çok kötü bir sürümü acilen geri çekmen gerekirse Angular build çıktısındaki safety-worker.js var: ngsw-worker.js'in yerine konduğunda eski worker'ı unregister eder ve ngsw: cache'lerini siler.
Manifest, Web Install ve bfcache
Application > Manifest, manifest.webmanifest'i ayrıştırıp alanlarını, ikonlarını ve hatalarını gösterir. Bookstore'unki temiz:
--- manifest url /manifest.webmanifest errors [] name bookstore-web display standalone
Chrome 155–156 ile Issues paneli, navigator.install ve <install> ile kullanılan Web Install akışının manifest hatalarını raporluyor: manifest indirilemiyorsa, syntax hatası varsa, start_url geçersizse, name ya da id eksikse ilgili manifest dosyasına link veren bir issue düşüyor. Uygulamayı yükletilebilir yapmaya çalışırken önce Issues'a bak.
Back/forward cache (bfcache), kullanıcı geri tuşuna bastığında sayfayı yeniden yüklemek yerine bellekteki hâlini geri getirir. Application > Back/forward cache'te "Test back/forward cache" düğmesi sayfadan uzaklaşıp geri döner ve sayfa cache'ten gelmediyse nedenlerini listeler. Bookstore'da aynı testi elle yaptık: sayfaya bir işaret koyup başka bir adrese gittik ve geri döndük.
after back: marker 42 persisted true navType navigate notUsed 0
pageshow event'inin persisted: true gelmesi ve JavaScript state'inin (marker 42) yerinde durması sayfanın bfcache'ten geldiğini gösteriyor; CDP'nin "kullanılmadı" event'i hiç düşmedi. unload listener'ı, açık bir WebSocket ya da Cache-Control: no-store bu sonucu bozabilir; düğmenin listelediği neden genelde tek satırlık bir düzeltmedir.
Üretimde Dikkat
Service Worker ile deploy, "dosyaları kopyaladım, bitti" değildir. Kullanıcıların bir kısmı en az bir yenileme boyunca eski bundle'la yeni API'ye konuşur. API değişikliklerini geriye uyumlu yap ve SwUpdate ile güncellemeyi kullanıcıya duyur. ngsw.json'ı Angular Service Worker zaten ngsw-cache-bust query'siyle cache'i atlayarak ister; ama index.html ve ngsw-worker.js'i CDN'de (Content Delivery Network) uzun süre cache'lersen güncelleme hiç ulaşmayabilir.
localStorage'daki sepet ya da IndexedDB'deki kayıtlar da sürüm değiştirir: yeni sürüm eski formattaki veriyi okuyabilmeli. Clear site data geliştiricinin aracıdır, kullanıcıdan bunu istemek bir çözüm değil.
Cookie'lerde localhost istisnasına güvenme: Secure cookie'ler gerçek domain'de yalnız HTTPS'te saklanır ve SameSite=None her zaman Secure ister. Staging ortamını HTTPS'le kur ki farkı production'da öğrenmeyesin.
Sık Düşülen Tuzaklar
- Service Worker'ı
ng serve'de aramak:enabled: !isDevMode()yüzünden yalnız production build'de kayıtlı. - Deploy sonrası "cache'i temizleyin" demek: Service Worker'ın Cache Storage'ı tarayıcının HTTP cache'inden ayrıdır.
- Update on reload ya da Bypass for network'ü açık unutup "bende güncelleme hemen geliyor, kullanıcıda neden gelmiyor" diye şaşırmak.
- IndexedDB tablosunun canlı olduğunu sanmak ve yenile düğmesine basmadan "kayıt yazılmamış" demek.
- Reddedilen bir cookie'yi Application > Cookies'te aramak: orada yok, sebep Network'teki isteğin Cookies sekmesinde.
- Clear site data'ya basıp sorunun kanıtını silmek.
Küçük Bir Deneme
★☆☆ Production build'i aç, sepete iki kitap ekle ve Local storage'daki bookstore.cart değerini elle bozarak (son karakteri sil) sayfayı yenile. Uygulamanın nasıl tepki verdiğini gözle, sonra Clear site data ile temizle.
★★☆ /api/session'ı çağır, Network'teki isteğin Cookies sekmesinde reddedilen tracking cookie'sinin sebebini oku. API'de cookie'ye Secure = true ekle ve tablonun nasıl değiştiğini karşılaştır.
★★★ Uygulamaya VERSION_READY geldiğinde "Yeni sürüm hazır" bandı gösteren bir component ekle. Bir build alıp deploy et, sayfayı yenilemeden /ngsw/state'te iki sürümün belirmesini bekle ve bandın çıktığını doğrula. Ardından Update on reload'u açıp aynı akışı tekrarla ve farkı yaz.
Buraya kadar uygulamanın neyi, nerede ve hangi sürümle sakladığını gördük. Sıradaki soru "ne kadar hızlı": bir sonraki yazıda Performance paneline geçiyoruz. Live metrics, trace kaydı, Insights ve Angular'ın kendi custom track'iyle, filtreye yazarken takılan kitap listesinin INP (Interaction to Next Paint) değerini ölçüp düzelteceğiz.