Mert Özen Her Satırda Daha İleri
Proje Yönetimi Jira ve Proje Araçları — #03

JQL, Filter ve Dashboard: Doğru Soruyu Sormak ve Metrikleri Yanlış Kullanmamak

Mert Özen 15 Kas 2026 13 dk 5 görüntülenme
JQL, Filter ve Dashboard: Doğru Soruyu Sormak ve Metrikleri Yanlış Kullanmamak

JQL alan, operatör ve değer ya da fonksiyondan oluşan clause'larla çalışır; currentUser(), openSprints(), startOfWeek() ve membersOf() her biri yalnız belirli operatörleri destekler. Yazı Bookstore ekibinin sorgularını, filter'ları repoda tutup bir .NET aracıyla kontrol etmeyi, filter ve dashboard paylaşımını, Jira raporlarının hangi soruya cevap verdiğini ve dashboard'un kişi performansı panosuna dönüştüğü tuzağı anlatıyor.

Sürüm notu: Bu yazıdaki C# kodu .NET 10 (SDK 10.0.402) ile derlenip çalıştırılmıştır. Sorgu örnekleri ve Jira davranışı Jira Cloud'un 2026-11 itibarıyla güncel belgelerine dayanır (Atlassian Support, 15 Kasım 2026'da kontrol edildi).

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) ekibi ikinci sprint'in ortasında. Üç kişi aynı gün üç şey kaydediyor. Emre, sprint'te biten işleri görmek için project = BOOK AND sprint = openSprints() AND status = Done yazıyor. Ayşe, review'da bekleyen PR'ları (pull request; değişikliğin ana branch'e alınması için açılan istek) bulmak için sorgusunun sonuna updated < -2d ekliyor. Product Owner Selin de yönetim için "Ekip Performansı" adında bir dashboard açıyor: her geliştiricinin bu sprint'te çözdüğü item sayısı yan yana.

Tahmin et: Bu üçünden hangisi Jira'nın hata vermesine yol açar, hangisi belgelerin önerdiği yazımdan sapar ve hangisi teknik olarak kusursuz olduğu hâlde ekibe zarar verir? Cevaplar yazının içinde; üçüncüsü sorgu diliyle değil, sorulan soruyla ilgili.

JQL'in Yapısı: Alan, Operatör, Değer

JQL (Jira Query Language; Jira'nın arama sorgu dili) SQL'e benzeyen ama veritabanı sorgu dili olmayan bir arama dili; Atlassian'ın belgesi bunu açıkça vurguluyor. En küçük birim clause: bir alan, bir operatör ve bir ya da birkaç değer veya fonksiyon. Clause'lar AND, OR ve NOT ile birleşiyor, sonuç ORDER BY ile sıralanıyor. Parantez kullanılmazsa ifade soldan sağa değerlendiriliyor; öncelik istiyorsan parantez koyman gerekiyor.

project = BOOK AND type = Bug AND statusCategory != Done ORDER BY priority DESC, created ASC

(status = "In Review" AND project = BOOK) OR assignee = currentUser()

assignee != currentUser() OR assignee is EMPTY

Üçüncü satır sık atlanan bir kuralı gösteriyor: JQL boş alanları != ve NOT IN sonuçlarına katmıyor. "Bana atanmamış işler" derken atanmamış işleri de görmek istiyorsan is EMPTY koşulunu açıkça eklemelisin. İkinci kural tarihlerle ilgili: created, updated, resolved gibi tarih alanlarına "-5d" gibi göreli bir değer verilebiliyor (w hafta, d gün, h saat, m dakika). Belge bu değerin tırnak içinde yazılmasını istiyor, çünkü tırnaksız bir sayı 1970'ten beri geçen milisaniye olarak yorumlanıyor. Ayşe'nin updated < -2d yazımı bu yüzden belgelerin önerdiği biçim değil.

Alan adlarında geçiş dönemindeyiz. JQL'de space alanı var ve project eskisi gibi çalışıyor; work type için type ve takma adı workType kullanılabiliyor. JQL fields sayfası, yeni terimle yazılan bazı ifadelerin henüz çalışmayabileceğini, öyle bir durumda eski terimin denenmesini söylüyor. Bu yazıdaki örneklerde iki tarafta da çalışan project kullanıyoruz.

Bu bölümde resmî belgelerden yararlanılmıştır.

Fonksiyonlar: currentUser, openSprints, startOfWeek, membersOf

JQL fonksiyonları sorguyu kalıcı hâle getiriyor: bir sprint numarası ya da tarih yerine "şu an açık sprint" veya "bu haftanın başı" diyebiliyorsun. Bookstore ekibinin en çok kullandığı dört fonksiyon ve belgede yazan kuralları:

  • currentUser(): sorguyu çalıştıran kullanıcı. Yalnız = ve != ile ve assignee, reporter, watcher gibi kullanıcı alanlarıyla çalışıyor; giriş yapmamış kullanıcılar için kullanılamıyor.
  • openSprints() ve closedSprints(): başlamış ama tamamlanmamış ya da tamamlanmış sprint'ler. Yalnız IN ve NOT IN ile çalışıyor; Emre'nin sprint = openSprints() yazımı belgede desteklenmeyen operatörler arasında.
  • startOfWeek(), startOfDay() ve benzerleri: dönemin başlangıcı, "-1" ya da "+1d" gibi isteğe bağlı bir artışla. Haftanın ilk günü, Look and feel ayarlarında tarih seçici için ISO8601 seçeneği (uluslararası tarih biçimi standardı) açılmamışsa pazar kabul ediliyor; pazartesi için startOfWeek("+1d") yazılıyor.
  • membersOf(): bir grubun ya da team'in üyeleri. Yalnız IN, NOT IN, WAS IN ve WAS NOT IN ile çalışıyor; 10.000'den fazla kullanıcı dönerse sorgu çalışmıyor ve team'ler ada göre değil id: önekiyle team ID'siyle aranıyor.

Fonksiyonlar geçmişe dönük operatörlerle de birleşebiliyor. CHANGED operatörü status, assignee, priority, resolution gibi alanların değişimini FROM, TO, BY, BEFORE, AFTER ile arıyor ve belgedeki örnekte BEFORE endOfWeek() AFTER startOfWeek() gibi fonksiyonlarla birlikte kullanılıyor.

Bu bölümde resmî belgelerden yararlanılmıştır.

Bookstore Ekibinin Sorguları

Ekibin board'u ve dashboard'u aşağıdaki sorgulardan besleniyor. Her birinin altında tek bir soru var; soruyu yazamadığın bir filter muhtemelen gereksizdir.

assignee = currentUser() AND statusCategory = "In Progress" AND updated <= "-5d" ORDER BY updated ASC

project = BOOK AND status = "In Progress" AND status CHANGED TO "In Progress" BEFORE startOfDay("-5d") ORDER BY updated ASC

project = BOOK AND status = "In Review" AND assignee in membersOf("bookstore-developers") ORDER BY updated ASC

project = BOOK AND type = Bug AND resolved >= startOfWeek("+1d") ORDER BY resolved DESC

project = BOOK AND sprint in openSprints() AND statusCategory = Done ORDER BY key ASC

İlk sorgu "bana atanmış, üzerinde çalışılıyor görünen ama beş gündür hiç güncellenmemiş işler" sorusu; Daily Scrum'dan önce her geliştiricinin kendine bakması için. İkincisi biraz farklı: status'ü beş günden önce In Progress'e geçmiş ve hâlâ orada olan item'lar. Bu Kanban serisinin 4. yazısındaki work item age'e en yakın JQL karşılığı, ama tam karşılığı değil: item In Progress'e iki kez girdiyse (örneğin Request Changes ile geri döndüyse) eski geçiş de koşulu sağlar. Üçüncüsü review kuyruğu, dördüncüsü bu hafta çözülen bug'lar (pazartesiden itibaren), beşincisi sprint'te bitmiş işler.

İki soru planda vardı ama JQL'de doğrudan karşılığı yok. "Sprint'e başladıktan sonra eklenen item'lar" için JQL'de bir clause yok; Sprint Report bu item'ları yıldız işaretiyle gösteriyor ve doğru yer orası. "Epic'e bağlanmamış bug'lar" sorusu da zorlaşmış durumda: eski "Epic Link" is EMPTY filter'ları çalışmaya devam ediyor, ama Epic Link Şubat 2024'te emekliye ayrıldı ve yeni filter'larda Parent kullanılması gerekiyor. Belgede Parent için listelenen operatörler =, !=, IN ve NOT IN; IS EMPTY bu listede yok ve yukarıdaki kural gereği parent != BOOK-120 parent'ı boş olan bug'ları döndürmez. Bu sorguyu kurmadan önce kendi sitende JQL editörünün syntax yardımına ve otomatik tamamlamasına bak; ekip olarak "her bug triage'da bir parent'a bağlanır" gibi bir kural koymak çoğu zaman sorgudan daha sağlam.

Bu bölümde resmî belgelerden yararlanılmıştır.

Filter'ları Repoda Tutup Kontrol Etmek

Ekip, board ve dashboard'ları besleyen filter'ların JQL'ini Bookstore reposunda düz bir metin dosyasında tutmaya karar verdi. Amaç Jira'yı repodan yönetmek değil; bir filter değiştiğinde değişikliğin PR'da görünmesi ve basit hataların review'dan önce yakalanması. Dosya biçimi basit: her satırda bir ad ve JQL.

# Bookstore team filters (space key BOOK)
my-stale-work: assignee = currentUser() AND statusCategory = "In Progress" AND updated <= "-5d" ORDER BY updated ASC
bugs-without-epic: project = BOOK AND type = Bug AND "Epic Link" is EMPTY
done-this-sprint: project = BOOK AND sprint = openSprints() AND status = Done
review-queue: project = BOOK AND assignee in membersOf("bookstore-developers") AND status = "In Review" AND updated < -2d ORDER BY updated ASC

Kontrolü yapan küçük bir .NET 10 console uygulaması, Bookstore.JiraTools. Bir JQL parser'ı değil; yukarıda belgelerden çıkardığımız kuralları regular expression ile arayan bir lint aracı. Kuralların her biri bir belge satırına dayanıyor:

using System.Text.RegularExpressions;

namespace Bookstore.JiraTools;

public enum Severity { Info, Warn }

public sealed record LintRule(string Id, Severity Severity, Regex Pattern, string Message, bool WhenMissing = false);

public sealed record Finding(string RuleId, Severity Severity, string Message);

public static class JqlLinter
{
    private const RegexOptions Options = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;

    public static readonly IReadOnlyList<LintRule> Rules =
    [
        new("JQL001", Severity.Warn, new(@"""epic link""", Options),
            "\"Epic Link\" was retired in February 2024; use parent = KEY"),
        new("JQL002", Severity.Warn, new(@"!?=\s*(openSprints|closedSprints|futureSprints)\(\)", Options),
            "sprint functions only support IN / NOT IN"),
        new("JQL003", Severity.Warn, new(@"!?=\s*membersOf\(", Options),
            "membersOf() only supports IN / NOT IN"),
        new("JQL004", Severity.Warn, new(@"[<>=]\s*(?<value>-\d+[wdhm])\b", Options),
            "relative date {0} without quotes; write \"{0}\""),
        new("JQL005", Severity.Info, new(@"\bstatus\s*(=|in)\s*\(?\s*""?done""?", Options),
            "status name Done is workflow-specific; statusCategory = Done covers every finished status"),
        new("JQL006", Severity.Info, new(@"currentUser\(\)", Options),
            "currentUser() needs a logged-in user; do not share this filter anonymously"),
        new("JQL007", Severity.Info, new(@"\border\s+by\b", Options),
            "no ORDER BY; say which order the readers of this filter expect", WhenMissing: true),
    ];

    public static IReadOnlyList<Finding> Check(string jql)
    {
        List<Finding> findings = [];
        foreach (LintRule rule in Rules)
        {
            Match match = rule.Pattern.Match(jql);
            // WhenMissing kuralları eşleşme YOKSA bulgu üretir (ORDER BY gibi)
            if (match.Success == rule.WhenMissing)
                continue;
            string value = match.Groups["value"].Value;
            findings.Add(new Finding(rule.Id, rule.Severity, string.Format(rule.Message, value)));
        }
        return findings;
    }
}

Dosyayı okuyan kısım ve komut satırı da kısa. Uyarı varsa çıkış kodu 1 dönüyor; böylece aynı komut bir CI adımında da kullanılabiliyor.

namespace Bookstore.JiraTools;

public static class FilterFile
{
    // Biçim: her satırda bir filter, "ad: JQL". # ile başlayan satırlar yorumdur.
    public static IEnumerable<(string Name, string Jql)> Read(string path) =>
        File.ReadLines(path)
            .Where(line => line.Trim() is { Length: > 0 } trimmed && !trimmed.StartsWith('#'))
            .Select(line => line.Split(':', 2))
            .Where(parts => parts.Length == 2)
            .Select(parts => (parts[0].Trim(), parts[1].Trim()));
}
using Bookstore.JiraTools;

return args switch
{
    ["lint-jql", var path] => LintFilters(path),
    _ => Usage(),
};

static int LintFilters(string path)
{
    int filters = 0, warnings = 0, notes = 0;
    foreach (var (name, jql) in FilterFile.Read(path))
    {
        filters++;
        IReadOnlyList<Finding> findings = JqlLinter.Check(jql);
        Console.WriteLine(findings.Count == 0 ? $"{name}: ok" : name);
        foreach (Finding f in findings)
        {
            Console.WriteLine($"  {f.Severity.ToString().ToLowerInvariant(),-4}  {f.RuleId}  {f.Message}");
            if (f.Severity == Severity.Warn) warnings++; else notes++;
        }
    }
    Console.WriteLine($"{filters} filters, {warnings} warnings, {notes} notes");
    return warnings > 0 ? 1 : 0;
}

static int Usage()
{
    Console.Error.WriteLine("usage: jiratools lint-jql <filters.txt>");
    return 2;
}

Reponun kökünden çalıştırınca gerçek çıktı şu (çıkış kodu 1):

dotnet run --project tools/Bookstore.JiraTools -- lint-jql jira/filters.txt
my-stale-work
  info  JQL006  currentUser() needs a logged-in user; do not share this filter anonymously
bugs-without-epic
  warn  JQL001  "Epic Link" was retired in February 2024; use parent = KEY
  info  JQL007  no ORDER BY; say which order the readers of this filter expect
done-this-sprint
  warn  JQL002  sprint functions only support IN / NOT IN
  info  JQL005  status name Done is workflow-specific; statusCategory = Done covers every finished status
  info  JQL007  no ORDER BY; say which order the readers of this filter expect
review-queue
  warn  JQL004  relative date -2d without quotes; write "-2d"
4 filters, 3 warnings, 4 notes

Baştaki ilk iki sorunun cevabı burada. Emre'nin filter'ı (JQL002) belgede desteklenmeyen bir operatör kullanıyor; düzeltmesi sprint in openSprints(). Aynı satırdaki status = Done bir hata değil, ama yalnız adı tam olarak Done olan status'ü buluyor; Won't Do gibi başka bir bitiş status'ü eklendiğinde filter onu kaçırır. statusCategory = Done bütün bitiş status'lerini kapsıyor. Ayşe'nin tarihi (JQL004) tırnak içine alınmalı. bugs-without-epic için ise yukarıda gördüğümüz gibi belgede tarif edilmiş tek satırlık bir yeni karşılık yok; uyarı orada kalıyor ve ekibin konuşması gereken bir karar olarak duruyor.

Bu aracın sınırını bilmek gerek: regular expression ile yapılan bir kontrol JQL'i gerçekten parse etmiyor. Tırnak içindeki bir metinde "epic link" geçerse yanlış alarm verir, kurallarda olmayan bir hatayı da görmez. Asıl doğrulama yine Jira'nın JQL editörü; bu araç yalnız ekibin bildiği hataların tekrar etmesini engelliyor.

Kayıtlı Filter, Paylaşım ve Dashboard

JQL sorgusu kaydedilince filter oluyor. Filter'ı paylaşabilir, favorilere ekleyebilir, sonuçlarını belirli aralıklarla e-postayla alabilir, bir rapora ya da dashboard gadget'ına kaynak yapabilirsin. Company-managed board'ların kendisi de bir filter'a dayanıyor; board filter'ını değiştirmek board'da görünen her şeyi değiştiriyor.

Paylaşımda iki ayrı yetki var: viewer'lar filter'ı kullanabiliyor, editor'ler değiştirebiliyor. Seçenekler Private (yalnız sahibi ve Jira admin'leri), kullanıcı, grup, space ya da space rolü ve My organization (sitedeki bütün giriş yapmış kullanıcılar). Belge, paylaşılan her filter'ın Administer Jira yetkisi olanlarca görülebildiğini ve sahipliği değiştirmenin Jira admin'i gerektirdiğini de not ediyor. E-posta aboneliklerinde yalnız ilk 200 sonuç gönderiliyor. Bir filter'ı başka bir filter içinde kullanacaksan adıyla değil ID'siyle çağır; belgenin gerekçesi, adın değişebilmesi ve bağımlı filter'ın sessizce kırılması.

Dashboard'ları herhangi bir Jira kullanıcısı oluşturabiliyor; paylaşım yine viewer ve editor olarak ayarlanıyor. Gadget'lar bir space'i ya da kayıtlı bir filter'ı kaynak alıyor ve doğru alanlara ihtiyaç duyuyor: belgedeki örneğe göre Workload pie chart story point'i değil, original estimate ve time spent gibi zaman alanlarını okuyor. currentUser() içeren bir filter'a dayanan gadget, dashboard'u açan herkese kendi işlerini gösteriyor; ortak bir "benim işlerim" panosu kurmanın en basit yolu bu. Varsayılan dashboard'daki bir değişiklik ise onu kullanan herkesin ekranını değiştiriyor.

Bu bölümde resmî belgelerden yararlanılmıştır.

Jira Raporları Hangi Soruya Cevap Verir?

Jira'nın Scrum ve Kanban raporları board'a bağlı: yalnız board filter'ına uyan item'ları sayıyor ve bir item'ın To Do ya da Done olduğuna kolon eşlemesine bakarak karar veriyor. Bu yüzden 02. yazıdaki kolon ve resolution kararları doğrudan rapora yansıyor.

RaporCevapladığı soruDayandığı veri ve dikkat
Velocity chartEkip bir sprint'te ortalama ne kadar iş bitiriyor?Gri çubuk sprint başındaki tahmin toplamı (sonradan eklenenler dahil değil), yeşil çubuk sprint sonunda biten iş; en sağdaki kolon Done sayılır
Burndown chartSprint goal'a ulaşma ihtimali nasıl gidiyor?Kalan tahmin; subtask'lardaki story point'ler dahil edilmiyor; yalnız Scrum board'larda
Cumulative flow diagramİş hangi kolonda birikiyor?Her renkli alan bir kolon; dikeyde genişleyen alan genellikle darboğaz
Control chartBir item seçilen status'lerde ne kadar kalıyor?Cycle time ya da lead time, ortalama, rolling average ve standart sapma; yalnız company-managed space'lerde
Created vs ResolvedGelen iş mi fazla, çözülen mi?Tamamen resolution alanına dayanıyor

Bu raporların ne gösterip ne göstermediğini Scrum serisinin 8. yazısında (velocity ve burndown) ve Kanban serisinin 4. yazısında (cycle time, throughput, work item age) ayrıntılı konuştuk. Burada eklenecek tek şey Jira'ya özgü: control chart'ta cycle time'ın hangi status'ler arasında ölçüleceğini sen seçiyorsun ve belge yeniden açılıp tekrar üzerinde çalışılan bir item'ın ek süresinin de cycle time'a eklendiğini söylüyor. Velocity'de ise gri çubuk sprint başladıktan sonra eklenen işleri içermiyor; sprint ortasında sık iş eklenen bir ekipte gri ve yeşil çubuğu karşılaştırmak yanıltıcı olabilir.

Bu bölümde resmî belgelerden yararlanılmıştır.

Üretimde Dikkat

Board filter'ı her şeyin temeli; birden fazla space'i kapsayan bir board filter'ı yazarken 5.000 item sınırını ve performansı düşün, filter'ı space ve gerekiyorsa tarih ile daralt. membersOf() büyük şemsiye gruplarla 10.000 kullanıcı sınırına takılabiliyor; ekip grupları küçük tutulmalı. Ve bir filter birçok board, gadget ve abonelikte kullanılıyorsa onu değiştirmek hepsini aynı anda değiştiriyor. Bookstore ekibinin filter'ları repoda tutmasının asıl getirisi burada: kimin, neyi, neden değiştirdiği PR geçmişinde duruyor.

Sık Düşülen Tuzaklar

Selin'in "Ekip Performansı" dashboard'u baştaki üçüncü sorunun cevabı: JQL'de hiçbir hata yok, ama kişi başına çözülen item sayısını yan yana koymak ekibe bir şey öğretmiyor, yalnız davranışı değiştiriyor. İnsanlar büyük işleri bölmeye, review'u ertelemeye, başkasına yardım etmek yerine kendi item'ını kapatmaya başlıyor. Scrum ve Kanban serilerindeki bütün metrikler ekip seviyesinde anlamlı; dashboard da ekibin akışı hakkında soru sormalı, kişiler hakkında değil.

İkinci tuzak, yanlış resolution ya da yanlış kolon eşlemesiyle doğru görünen ama yanlış grafikler: Won't Do'nun Done kolonunda durması velocity'yi şişiriyor, resolution'ın boş kalması Created vs Resolved'u düz bir çizgiye çeviriyor. Üçüncüsü, status = Done ile yazılmış filter'ların yeni bir bitiş status'ü eklendiğinde sessizce eksik sonuç vermesi. Dördüncüsü, hassas bilgiler içeren bir filter'ı düşünmeden My organization ile paylaşmak; filter'ın adı ve koşulları bile, örneğin bir kurumsal müşterinin adı, herkesin görmemesi gereken bir şey anlatabiliyor.

Küçük Bir Deneme

★☆☆ Ekibinin en çok kullandığı üç filter'ı al ve her biri için tek cümlelik soruyu yaz. Sorusu yazılamayan ya da başka bir filter'la aynı soruyu soran filter var mı?

★★☆ Bookstore.JiraTools'a JQL008 kuralını ekle: belgeye göre currentUser() yalnız = ve != ile çalışıyor, kural assignee in (currentUser()) gibi bir kullanımı yakalasın. filters.txt'e bu hatayı içeren bir satır ekleyip çıktıyı ve çıkış kodunu gör.

★★★ Selin'in dashboard'unu yeniden tasarla: kişi adı geçmeyen, yalnız ekibin akışını gösteren üç gadget seç ve her birinin hangi filter'dan beslendiğini, hangi soruya cevap verdiğini ve hangi kolon ya da resolution kararına dayandığını yaz.

Artık doğru soruyu sorabiliyoruz; sıradaki adım tekrar eden işleri Jira'ya bırakmak. Bir sonraki ve serinin son yazısında automation rule'ların anatomisini, smart value'ları, Atlassian'ın 3 Aralık 2026'da başlayan kullanım bazlı faturalandırmasını, GitHub entegrasyonunu ve REST (HTTP üzerinden kaynak odaklı API tarzı) API ile release notu taslağı üreten küçük bir C# aracını ele alıyoruz.