Mert Özen Her Satırda Daha İleri
Geliştirici Araçları Debugging ve Problem Çözme — #13

Gerçek Hayattan Bug Avları 2: Frontend ve Entegrasyon — Cache, CORS, Tarih ve Memory Leak

Mert Özen 8 Kas 2026 15 dk 5 görüntülenme
Gerçek Hayattan Bug Avları 2: Frontend ve Entegrasyon — Cache, CORS, Tarih ve Memory Leak

Frontend bug'larının çoğu kodda değil, tarayıcı ile sunucu arasındaki farkta doğar. Beş gerçek vaka: deploy sonrası index.html cache'i ve kaybolan chunk yüzünden beyaz ekran, sondaki slash yüzünden yalnız production'da düşen CORS preflight, saat dilimiyle bir gün kayan doğum tarihi, floating point para toplamı ve unsubscribe edilmeyen observable ile büyüyen bellek; her birinde belirleyici araç ve serinin debugging kontrol listesi.

Sürüm notu: Bu yazıdaki kodlar .NET 10, ASP.NET Core 10, Angular 22.2, RxJS 7.8 ve Vitest 5.1 ile derlenip denenmiştir; tarayıcı çıktıları Chrome 156 ile alınmıştır.

Önce Tahmin Et

Bookstore'un Angular uygulaması yeni sürümle deploy edildi. Kullanıcının tarayıcısında uygulama açık duruyor; deploy bittikten sonra "Books" link'ine tıklıyor. Ne olur: yeni sayfa mı açılır, eski sayfa mı, yoksa hiçbir şey mi? Bir de şunu düşün: sunucu müşterinin doğum tarihi için JSON (JavaScript Object Notation) içinde "1990-05-14" gönderiyor. New York'taki bir müşterinin ekranında hangi gün yazar? İkisinin de cevabını gerçek tarayıcı ve test çıktılarıyla göreceğiz.

Serinin son yazısında her vaka bir önceki yazıdaki sırayla ilerliyor: belirti, ilk yanlış hipotez, daraltma, kök sebep, düzeltme ve onu kanıtlayan test. Sahne bu kez tarayıcı ile sunucu arasındaki sınır. Tarayıcı çıktılarını ayrı bir profille açtığım headless Chrome'dan CDP (Chrome DevTools Protocol) ile topladım; DevTools panel adımlarını dokümana göre anlatıyorum.

Vaka 1: Deploy Sonrası Beyaz Ekran

Belirti: deploy'dan sonra "sayfa bembeyaz" ve "Books'a tıklayınca hiçbir şey olmuyor" şikâyetleri geliyor, ekipte kimse tekrarlayamıyor. İlk yanlış hipotez: "Yeni sürümde bir JavaScript hatası var." Ama yeni sürümü açan herkes için uygulama çalışıyor; sorun kodda değil, kullanıcının elindeki sürüm ile sunucudakinin farkında.

Angular CLI (Command Line Interface) production build'de dosya adlarına içerik hash'i ekler. İki build arasında yalnız bir component'in metnini değiştirdik:

v1: index.html, main-UNNPJ3LA.js, chunk-HACNQHZ2.js, chunk-C2JN2BIZ.js (book-list)
v2: index.html, main-7RZUROQY.js, chunk-HACNQHZ2.js, chunk-DTIQWNSM.js (book-list)

Static dosyaları ASP.NET Core sunuyor ve herkesin bir kez yazdığı "her şeye bir gün cache" kuralı var:

StaticFileOptions staticFiles = new()
{
    FileProvider = new PhysicalFileProvider(root),
    OnPrepareResponse = ctx =>
    {
        string cacheControl = fixedCaching
            ? (ctx.File.Name == "index.html" ? "no-cache" : "public, max-age=31536000, immutable")
            : "public, max-age=86400"; // HATA: index.html dahil her şeye tek kural
        ctx.Context.Response.Headers[HeaderNames.CacheControl] = cacheControl;
    },
};

app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = staticFiles.FileProvider });
app.UseStaticFiles(staticFiles);
app.UseRouting();
app.MapFallbackToFile("index.html", staticFiles);

UseRouting()'i açıkça static file'lardan sonra çağırmamın bir sebebi var. Çağırmazsan WebApplication routing'i pipeline'ın başına koyar; / isteği önce fallback endpoint'ine eşleşir ve static file middleware endpoint seçilmiş isteği atlar. İlk denememde index.html tam da bu yüzden OnPrepareResponse'tan geçmedi ve hiç Cache-Control header'ı almadı.

Daraltma için senaryoyu headless Chrome'da adım adım oynattım: v1'i deploy et, sayfayı aç, v2'yi deploy et, link'e tıkla, ertesi ziyareti taklit et. Hatalı cache kuralıyla gerçek çıktı:

1) open http://localhost:5303/ (v1 deployed) -> h1="Bookstore v1"
  net 200 / text/html
  net 200 /styles-5INURTSO.css text/css
  net 200 /main-UNNPJ3LA.js text/javascript
  net 200 /chunk-HACNQHZ2.js text/javascript
  net 200 /favicon.ico image/x-icon
2) v2 deployed while tab is open, click Books -> list="(none)" url=/
  net 404 /chunk-C2JN2BIZ.js text/plain
  log: Failed to load resource: the server responded with a status of 404 (Not Found)
  console.error: ERROR TypeError: Failed to fetch dynamically imported module: http://localhost:5303/chunk-C2JN2BIZ.js
3) next visit (navigate, not reload) -> h1="Bookstore v1"
  net 200 / text/html (disk cache)
  net 200 /styles-5INURTSO.css text/css
  net 200 /chunk-HACNQHZ2.js text/javascript
  net 200 /main-UNNPJ3LA.js text/javascript

İkinci adım "Books'a tıklayınca hiçbir şey olmuyor" şikâyetinin ta kendisi: açık sekmedeki v1, lazy loading ile v1'in chunk-C2JN2BIZ.js dosyasını istiyor, sunucuda artık yok. Üçüncü adım daha sinsi: index.html disk cache'ten geliyor, kullanıcı bir gün boyunca v1'de kalıyor ve her Books tıklamasında aynı 404'ü yiyor. Beyaz ekran ise eski index.html'in, eski main dosyası cache'te olmayan bir tarayıcıya ya da araya giren bir CDN (Content Delivery Network) üzerinden gelmesiyle oluşuyor. Sunucuda v2 dosyaları dururken eski index.html'i sunan durumu taklit eden ziyaret:

  net 200 / text/html
  net 200 /styles-5INURTSO.css text/css
  net 200 /chunk-HACNQHZ2.js text/javascript
  net 404 /main-UNNPJ3LA.js text/plain
  log: Failed to load resource: the server responded with a status of 404 (Not Found)
  net 200 /favicon.ico image/x-icon
  app-root: <app-root></app-root>

Boş bir app-root: beyaz ekran. Kök sebep kodda değil, cache politikasında: index.html bir sürümün "giriş kapısı" ve asla uzun süre cache'lenmemeli; adında hash olan dosyalar ise sonsuza kadar cache'lenebilir, çünkü içerik değişince adı da değişir. Düzeltilmiş sunucunun header'ları:

GET /
Cache-Control: no-cache
GET /main-7RZUROQY.js
Cache-Control: public, max-age=31536000, immutable
GET /main-UNNPJ3LA.js
HTTP/1.1 404 Not Found

no-cache "cache'leme" demek değil, "her kullanımda sunucuya sor" demek; ETag sayesinde değişmediyse 304 ile ucuza döner. Eski chunk'ın 404 dönmesi de önemli: ASP.NET Core'un MapFallbackToFile'ı varsayılan olarak yalnız uzantısız yollara ({*path:nonfile}) index.html döner. Her yolu index.html'e düşüren bir rewrite kuralı ise eksik bir .js için de 200 ile index.html döndürür; o zaman Console'da 404 yerine yanlış MIME (Multipurpose Internet Mail Extensions) türü hatası görürsün ve iz kaybolur.

Açık sekme sorunu cache header'ıyla çözülmez; o sekme zaten v1'i çalıştırıyor. Router'a, kaybolan chunk'ta sayfayı bir kez yeniden yükleyen bir handler ekledim:

provideRouter(
  routes,
  withNavigationErrorHandler(error => {
    // eski sürümün chunk'ı deploy'dan sonra yok: yeni sürümü bir kez yükle
    const chunkMissing = String(error.error).includes('Failed to fetch dynamically imported module');
    if (chunkMissing && sessionStorage.getItem('chunk-reload') !== error.url) {
      sessionStorage.setItem('chunk-reload', error.url);
      location.assign(error.url);
    }
  }),
),

Aynı senaryonun düzeltilmiş hâlinde ikinci adım artık şöyle bitiyor:

2) v2 deployed while tab is open, click Books -> list="BooksDune, Emma, Ulysses, Middlemarch" url=/books
  net 404 /chunk-C2JN2BIZ.js text/plain
  log: Failed to load resource: the server responded with a status of 404 (Not Found)
  console.error: ERROR TypeError: Failed to fetch dynamically imported module: http://localhost:5306/chunk-C2JN2BIZ.js
  net 200 /books text/html
  net 200 /styles-5INURTSO.css text/css
  net 200 /chunk-HACNQHZ2.js text/javascript
  net 200 /main-BBB3MOOU.js text/javascript
  net 200 /chunk-DTIQWNSM.js text/javascript

404 hâlâ oluyor ama kullanıcı v2'nin Books sayfasına iniyor. sessionStorage kontrolü sonsuz yeniden yükleme döngüsünü engelliyor. Mesaj metni tarayıcıya göre değişir (bu metin Chromium'un); Firefox ve Safari için kendi metinlerini test et.

Service Worker Varsa

Uygulama @angular/service-worker kullanıyorsa eski sürüm HTTP (Hypertext Transfer Protocol) cache'inde değil Service Worker'ın kendi cache'inde yaşar; sunucu header'larını düzeltmek tek başına yetmez. Angular dokümanına göre SwUpdate.versionUpdates yeni sürüm hazır olduğunda VERSION_READY event'i yayar; uygulama kullanıcıya sorup document.location.reload() çağırmalıdır. activateUpdate() sayfayı yenilemeden sürüm değiştirir ve doküman, lazy chunk adları değiştiği için bunun uygulamayı bozabileceği uyarısını açıkça yapar; SwUpdate.unrecoverable da yalnız tam yeniden yüklemenin kurtarabileceği durumu bildirir. Bu bölüm dokümana dayanıyor, çalıştırmadım. Teşhiste Application paneli belirleyicidir: aktif worker'ı ve Cache Storage içeriğini gösterir, "Clear site data" deneyi temiz sayfayla tekrarlatır.

Vaka 2: Yalnız Production'da Patlayan CORS Preflight

Belirti: sipariş butonu production'da çalışmıyor; development'ta, staging'de, Postman'de ve curl'de çalışıyor. İlk yanlış hipotez: "Token production'da geçersiz." Ama istek API'ye (Application Programming Interface) hiç ulaşmıyor; API log'unda o kullanıcıya ait bir POST yok.

Development'ta bu bug'ı görmemen tesadüf değil. Angular dev server'ı proxy configuration'ıyla çalışıyor:

{
  "/api": {
    "target": "http://localhost:5311",
    "secure": false
  }
}

Tarayıcının gözünde istek sayfanın kendi origin'ine gidiyor, yani CORS (Cross-Origin Resource Sharing) hiç devreye girmiyor. Dev server üzerinden atılan POST, API'nin izin verdiği origin listesi boşken bile 201 dönüyor. Production'da ise sayfa https://shop.bookstore.test'ten, API başka bir origin'den sunuluyor. Content-Type: application/json ve Authorization header'ı taşıyan bir POST "basit istek" sayılmaz; tarayıcı önce bir OPTIONS preflight'ı gönderir. API'nin CORS ayarı:

string[] allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [];
builder.Services.AddCors(options => options.AddPolicy("Shop", policy => policy
    .WithOrigins(allowedOrigins)
    .WithHeaders("authorization", "content-type")
    .WithMethods("GET", "POST")));

WebApplication app = builder.Build();

app.UseRouting();
app.UseCors("Shop");

Ve appsettings.Production.json:

{
  "Cors": {
    "AllowedOrigins": [ "https://shop.bookstore.test/" ]
  }
}

Daraltmanın en hızlı yolu, tarayıcının preflight'ını curl ile birebir taklit etmek:

curl -v -X OPTIONS http://localhost:5311/api/orders \
  -H "Origin: https://shop.bookstore.test" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization,content-type"

< HTTP/1.1 204 No Content
< Date: Sun, 08 Nov 2026 09:02:15 GMT
< Server: Kestrel

204 dönüyor ama hiçbir Access-Control-Allow-* header'ı yok. Tarayıcı bunu ret olarak okur ve asıl POST'u hiç göndermez; Console'da "No 'Access-Control-Allow-Origin' header is present" hatasını görürsün. Burada belirleyici olan Network paneli değil, API'nin kendi log'u. Microsoft.AspNetCore.Cors kategorisini Information seviyesine açınca:

info: Microsoft.AspNetCore.Cors.Infrastructure.CorsService[5]
      CORS policy execution failed.
info: Microsoft.AspNetCore.Cors.Infrastructure.CorsService[6]
      Request origin https://shop.bookstore.test does not have permission to access the resource.

Kök sebep configuration'daki son karakter: Microsoft Learn'ün CORS sayfası açıkça söylüyor, WithOrigins'e verilen URL (Uniform Resource Locator) sonda / içermemeli, içerirse karşılaştırma tutmaz. Tarayıcının gönderdiği Origin header'ında asla sondaki slash yoktur. Slash'ı silince aynı preflight:

< HTTP/1.1 204 No Content
< Date: Sun, 08 Nov 2026 09:02:24 GMT
< Server: Kestrel
< Access-Control-Allow-Headers: authorization,content-type
< Access-Control-Allow-Methods: GET,POST
< Access-Control-Allow-Origin: https://shop.bookstore.test

Bu vakanın dersi: curl ve Postman'de çalışması CORS hakkında hiçbir şey kanıtlamaz, çünkü CORS'u uygulayan tarayıcıdır. Bu curl komutunu deploy sonrası smoke test'ine koymak, bir sonraki sürprizi dakikalar içinde yakalar. Yaygın diğer sebepler aynı yöntemle bulunur: UseCors'un yanlış yerde olması (doküman UseRouting'den sonra, UseAuthorization'dan önce diyor), izin listesinde olmayan bir custom header ya da preflight'ı başka bir adrese yönlendiren bir redirect.

Vaka 3: Bir Gün Kayan Doğum Tarihi

Belirti: ABD'deki birkaç müşteri profillerinde doğum tarihlerinin bir gün eksik göründüğünü söylüyor. Türkiye'deki ekip tekrarlayamıyor. İlk yanlış hipotez: "Veritabanında yanlış kaydedilmiş." Veritabanında 1990-05-14 duruyor.

Daraltma iki katmanda: sunucu ne gönderiyor, tarayıcı onu nasıl yorumluyor? Sunucu tarafında aynı tarihin üç olası JSON hâli:

DateTime fromDb = new(1990, 5, 14);
DateTime asUtc = TimeZoneInfo.ConvertTimeToUtc(fromDb, istanbul);

Console.WriteLine(JsonSerializer.Serialize(new CustomerV1(fromDb), web));
Console.WriteLine(JsonSerializer.Serialize(new CustomerV1(asUtc), web));
Console.WriteLine(JsonSerializer.Serialize(new CustomerV2(DateOnly.FromDateTime(fromDb)), web));
{"birthDate":"1990-05-14T00:00:00"}
{"birthDate":"1990-05-13T21:00:00Z"}
{"birthDate":"1990-05-14"}

Tarayıcı tarafını Node.js ile, saat dilimini TZ ortam değişkeniyle değiştirerek denedim. Her girdi new Date(s) ile parse ediliyor:

Europe/Istanbul  1990-05-14             -> getDate()=14  toLocaleDateString('en-US')=5/14/1990
Europe/Istanbul  1990-05-14T00:00:00    -> getDate()=14  toLocaleDateString('en-US')=5/14/1990
Europe/Istanbul  1990-05-13T21:00:00Z   -> getDate()=14  toLocaleDateString('en-US')=5/14/1990
America/New_York 1990-05-14             -> getDate()=13  toLocaleDateString('en-US')=5/13/1990
America/New_York 1990-05-14T00:00:00    -> getDate()=14  toLocaleDateString('en-US')=5/14/1990
America/New_York 1990-05-13T21:00:00Z   -> getDate()=13  toLocaleDateString('en-US')=5/13/1990

Bu tablo iki kök sebebi birden gösteriyor. Birincisi sunucuda: doğum tarihi bir "an" değil, bir takvim günü; onu Istanbul saatiyle UTC'ye (Coordinated Universal Time) çevirmek 13 Mayıs 21:00 gibi bir ana dönüştürüyor ve her saat diliminde başka bir güne düşüyor. İkincisi tarayıcıda ve daha az bilinen: ECMAScript, yalnız tarih içeren ISO (International Organization for Standardization) 8601 metnini UTC olarak, saat içeren ama offset'siz metni ise yerel saat olarak yorumlar. Yani sunucuyu DateOnly'e çevirip "1990-05-14" göndermek tek başına yetmiyor; frontend bunu new Date'e verirse New York'ta yine 13 Mayıs çıkıyor.

Düzeltme iki taraflı: sunucuda DateOnly, frontend'de değeri string olarak tutup Angular'ın tarih biçimlendirmesine bırakmak. TZ=America/New_York ile koşan Vitest testinin çıktısı:

America/New_York formatDate('1990-05-14') = May 14, 1990
America/New_York formatDate('1990-05-13T21:00:00Z') = May 13, 1990

Angular'ın formatDate'i yalnız tarih içeren metni yerel takvim günü olarak ele alıyor. Bu test, CI (Continuous Integration) ortamında saat dilimini sabitleyerek koşan regression testi olarak kalıyor. Belirleyici araç debugger değil, saat dilimini değiştirip aynı girdiyi denemek oldu. Backend'deki saat dilimi tuzaklarını bir önceki yazıda DateTime.Now vakasıyla konuştuk.

Vaka 4: Para Hesabında Floating Point

Belirti: sepet sayfasında toplam 94,05 TL görünüyor ama "sepet toplamı ödeme toplamıyla eşleşmiyor" uyarısı çıkıyor. İlk yanlış hipotez: "Backend toplamı yanlış hesaplıyor." Backend decimal kullanıyor ve doğru.

const items = [
  { title: 'Dune', price: 29.9, quantity: 3 },
  { title: 'Emma', price: 4.35, quantity: 1 },
];

const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
console.log('total            :', total);
console.log('total === 94.05  :', total === 94.05);
console.log('Math.trunc(4.35 * 100):', Math.trunc(4.35 * 100));
console.log('(1.005).toFixed(2):', (1.005).toFixed(2));

// düzeltme: tam sayı kuruş; dönüşüm bir kez, Math.round ile
const totalMinor = items.reduce((sum, i) => sum + Math.round(i.price * 100) * i.quantity, 0);
console.log('totalMinor       :', totalMinor);
console.log('display          :', new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(totalMinor / 100));
total            : 94.04999999999998
total === 94.05  : false
Math.trunc(4.35 * 100): 434
(1.005).toFixed(2): 1.00
totalMinor       : 9405
display          : ₺94,05

Ekran yuvarlanmış toplamı gösteriyordu, karşılaştırma ise ham değerle yapılıyordu. JavaScript'in number'ı IEEE (Institute of Electrical and Electronics Engineers) 754 double'dır ve 29,9 ile 4,35 ikilik tabanda tam ifade edilemez. Aynı hesap C#'ta double ile 94.04999999999998, decimal ile tam 94.05 veriyor. Math.trunc(4.35 * 100) satırı da ayrı bir bug: kuruşa çevirirken kesmek 1 kuruş kaybettiriyor.

Kalıcı düzeltme para hesabını frontend'den almak: toplamı ve indirimi backend decimal ile hesaplar, frontend yalnız gösterir. Frontend'de mutlaka hesap gerekiyorsa tutarı tam sayı kuruş olarak tut, ondalıktan dönüşümü tek yerde Math.round ile yap, gösterimi Intl.NumberFormat'a bırak. Serinin ana vakası olan "sipariş toplamı bazen yanlış" ticket'ını backend tarafında 01–03 ve 11. yazılarda izledik; bu vaka aynı ailenin frontend üyesi.

Vaka 5: Unsubscribe Edilmeyen Observable ile Büyüyen Bellek

Belirti: kullanıcılar sekmeyi bütün gün açık tutunca uygulamanın yavaşladığını söylüyor; sayfa yenilenince düzeliyor. İlk yanlış hipotez: "Sipariş listesi çok büyük, pagination lazım." Liste 20 satır.

Daraltma için önce hangi component'in sızdırdığını bulmak gerekiyor. DevTools'un Memory panelinde yöntem şu: bir heap snapshot al, şüpheli sayfaya gir çık (on kez), bir snapshot daha al, ikincisini Comparison görünümünde ilkine karşı aç ve "# Delta" sütununa göre sırala. Sayısı ziyaret sayısı kadar artan bir component class'ı arıyorsun. Bizim vakada aday rozet component'iydi:

export class CartBadgeLeaky implements OnInit {
  private readonly events = inject(CartEvents);
  readonly count = signal(0);

  ngOnInit(): void {
    // HATA: hiç unsubscribe edilmiyor
    this.events.changes$.subscribe(c => this.count.set(c));
  }
}

export class CartBadge implements OnInit {
  private readonly events = inject(CartEvents);
  private readonly destroyRef = inject(DestroyRef);
  readonly count = signal(0);

  ngOnInit(): void {
    this.events.changes$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(c => this.count.set(c));
  }
}

CartEvents root'ta yaşayan bir service ve aktif subscriber sayısını tap'in subscribe/finalize callback'leriyle sayıyor. Her component'i 1000 kez oluşturup yok eden testin çıktısı:

leaky : active subscribers after 1000 visits = 1000
fixed : active subscribers after 1000 visits = 0

Sızıntının bedeli subscription'ın kendisi değil, tuttuğu şey. Callback this'i yakalıyor; root service'teki Subject callback'i, callback de yok edilmiş component'i ve onun bütün state'ini hayatta tutuyor. Bunu ölçmek için Node.js'te, her biri yaklaşık 200 KB state taşıyan bir view'u 500 kez açıp kapatan küçük bir script koştum (node --expose-gc ile, ölçümden önce garbage collector çalıştırılarak):

leaky: heap 5.6 MB -> 101.4 MB after 500 visits, still observed: true
fixed: heap 5.8 MB -> 5.9 MB after 500 visits, still observed: false

Aynı garbage collection'dan sonra bile 95 MB geri dönmüyor. Kök sebep: uzun ömürlü bir kaynağa kısa ömürlü bir nesneden subscribe olup aboneliği bitirmemek. Düzeltme takeUntilDestroyed, ya da daha iyisi, Observable'ı template'te AsyncPipe veya toSignal ile bağlamak; ikisi de temizliği Angular'a bırakır. HTTP çağrıları tek değer verip tamamlandığı için bu sızıntıyı üretmez; tehlikeli olanlar Subject, interval, WebSocket ve store gibi bitmeyen kaynaklar. Bu test sızıntının regression testi olarak kalıyor.

Hangi Vakada Hangi Araç Belirleyici Oldu?

VakaYanıltan ilk ipucuBelirleyici araçKalıcı kanıt
Deploy sonrası beyaz ekran"JavaScript hatası var"Network paneli: 404 ve (disk cache), response header'larıDeploy senaryosunu oynatan tarayıcı testi, Cache-Control kontrolü
Yalnız production'da CORS"Token geçersiz"API log'u ve curl ile taklit edilen preflightDeploy sonrası smoke test'te OPTIONS isteği
Bir gün kayan doğum tarihi"Veri yanlış kaydedilmiş"Aynı girdiyi farklı TZ ile denemekTZ=America/New_York ile koşan unit test
Para hesabında floating point"Backend yanlış topluyor"Ham değeri yuvarlamadan yazdırmakHam toplamı kontrol eden test (total === 94.05)
Büyüyen bellek"Liste çok büyük"Memory paneli: heap snapshot ComparisonAktif subscriber sayan unit test

Son sütun serinin özeti: her vaka, bir daha olmasını engelleyen bir testle bitiyor.

Üretimde Dikkat

Frontend vakalarında hata kullanıcının tarayıcısında olur, senin elinde yalnız onun anlattıkları kalır. Bu yüzden önceden kur: global ErrorHandler'dan sunucuya giden hata raporu (sürüm numarası, route, tarayıcı, saat dilimi ve request'in trace ID'siyle); deploy edilen sürümü gösteren bir /version endpoint'i ya da index.html'de bir meta etiketi, böylece "kullanıcı hangi sürümde?" sorusu tek bakışta cevaplanır.

Kullanıcıdan HAR (HTTP Archive) dosyası istemek Network sorunlarında çok değerlidir ama HAR cookie, token ve form verisi içerir. Paylaşmadan önce sanitize edilmesini iste ve onu bir ticket'a ek olarak bırakma; serinin 02. yazısındaki kural burada da geçerli. Aynı şey log için de geçerli: doğum tarihi vakasında log'a müşterinin adını değil, ID'sini ve gönderilen ham değeri yaz.

CDN arkasında CDN'in kendi cache'i de index.html'i tutabilir. Deploy'da hash'li dosyaları önce, index.html'i en son yükle ve eski chunk'ları birkaç sürüm silme; açık sekmeler için en ucuz çözüm bu.

Sık Düşülen Tuzaklar

"Bende çalışıyor" demek ve kendi tarayıcının cache'ini, saat dilimini, origin'ini kullanıcınınkiyle aynı sanmak. Bu yazıdaki beş vakanın dördü tam olarak bu farktan doğdu. Tekrarlarken kullanıcının ortamını taklit et: temiz profil, aynı saat dilimi, aynı origin.

CORS hatasını tarayıcıda "kapatmak" (eklenti, AllowAnyOrigin) ve sebebi bulmadan geçmek. Kimlik bilgisi taşıyan isteklerde AllowAnyOrigin zaten AllowCredentials ile birlikte kullanılamaz; asıl sorun origin'in birebir eşleşmesi.

Takvim günlerini new Date("yyyy-MM-dd") ile parse etmek, parayı number ile toplayıp karşılaştırmak, Subject'e component'ten subscribe olup temizliği unutmak. Üçü de test ortamında görünmez, çünkü testin saat dilimi, verisi ve süresi kullanıcınınkine benzemez.

Seri Kapanışı: Tek Sayfalık Debugging Kontrol Listesi

On üç yazının yöntemini tek listeye indirdim. Kopyalayıp ekibinin wiki'sine ya da bir ticket şablonuna koyabilirsin:

  • Belirtiyi yaz: ne bekleniyordu, ne oldu, kimde, ne zaman, hangi sürümde (01, 02).
  • Tekrarla: aynı veri, culture, saat dilimi, configuration, tarayıcı ve cache durumu; tekrarlayamıyorsan düzeltme deneme (02).
  • Önce kırmızı test: bug'ı yakalayan en küçük testi yaz, düzeltme onu yeşile çevirsin (02).
  • Ortadan böl: UI (User Interface) → state → HTTP → API → veritabanı zincirinde ve commit geçmişinde (git bisect run) yarıya inerek ilerle (03, 07).
  • Hata mesajını ve stack trace'i baştan sona oku; log'daki ilk exception'ı ve traceId'yi bul (04, 08).
  • Tek seferde tek değişiklik yap ve günlüğe yaz: hipotez, deney, sonuç (01).
  • Debugger'ı akıllı kullan: conditional breakpoint, tracepoint, exception settings; timing bug'ında log ve dump'a geç (05, 06).
  • Production'da log'la zaman çizelgesi kur: deploy, config değişikliği, trafik ve ilk hata aynı eksende (08).
  • Crash ve hang için kanıt topla: dump, dotnet-stack, kısa dotnet-trace; dump'ların gizli veri içerdiğini unutma (09, 10).
  • Kök sebebe in: belirti, proximate cause ve root cause'u ayır; 5 Whys ve fishbone ile birden fazla etkeni ara (11).
  • Düzeltmeyi üç katmanda bitir: hotfix, regression test, bu sınıf bug'ı imkânsız kılan değişiklik (11, 12, 13).
  • Düzeldiğini doğrula: aynı repro artık geçiyor mu, production metriği normale döndü mü? Düzeltmediysen düzelmemiştir (01).
  • Blameless postmortem yaz ve öğrendiğini paylaş (11).

Küçük Bir Deneme

★☆☆ Kendi Angular uygulamanı production modda build et ve iki build arasında yalnız bir lazy component'i değiştir. Hangi dosya adları değişti, hangileri aynı kaldı? Sunucunun index.html ve hash'li dosyalar için döndürdüğü Cache-Control header'larını curl ile kontrol et.

★★☆ API'ndeki CORS policy'sine kasıtlı olarak yanlış bir origin yaz, preflight'ı curl ile taklit et ve Microsoft.AspNetCore.Cors log'unda başarısızlık satırını gör. Sonra aynı kontrolü deploy pipeline'ına smoke test olarak ekle.

★★★ Uygulamandaki bütün subscribe çağrılarını listele ve her birini "tek değer verip tamamlanıyor" ya da "bitmeyen kaynak" diye sınıfla. Bitmeyen kaynaklara bağlı olanlar için component'i 1000 kez oluşturup yok eden ve aktif subscriber sayısını ölçen bir test yaz.

Bu seriyle bug'a bir yöntemle yaklaşmayı, onu daraltmayı, araçlarla kanıt toplamayı ve kök sebebe inip bir daha olmasını engellemeyi konuştuk. Burada bitiyor ama yolun devamı var: CPU (Central Processing Unit) ve allocation analizini ilerideki .NET'te Profiling ve Diagnostics serisinde, log, trace ve metric altyapısını DevOps tarafındaki Observability serisinde, panellerin ayrıntısını da Browser DevTools serisinde ele alacağız. Bir sonraki bug'ında ilk işin, yukarıdaki listenin ilk satırını yazmak olsun.