Ana içeriğe geç

Feature Flags ve Gradual Rollout

Feature flag’ler, özellikleri kod deploy’undan bağımsız olarak açıp kapatmayı sağlar; yanlış kullanım bayrak karmaşasına ve test edilemeyen koda yol açar.


1. Hardcoded Feature Toggle Kullanmak

Yanlış Kullanım: Özellikleri if/else ile kontrol etmek.

app.MapGet("/api/products", async (IProductService service) =>
{
    var useNewAlgorithm = true; // Değiştirmek için yeniden deploy gerekir
    if (useNewAlgorithm)
        return await service.GetWithNewAlgorithmAsync();
    else
        return await service.GetAllAsync();
});

İdeal Kullanım: Microsoft.FeatureManagement ile feature flag kullanın.

builder.Services.AddFeatureManagement();

// appsettings.json
// {
//   "FeatureManagement": {
//     "NewSearchAlgorithm": true,
//     "BetaDashboard": false
//   }
// }

app.MapGet("/api/products", async (
    IFeatureManager featureManager,
    IProductService service) =>
{
    if (await featureManager.IsEnabledAsync("NewSearchAlgorithm"))
        return TypedResults.Ok(await service.GetWithNewAlgorithmAsync());

    return TypedResults.Ok(await service.GetAllAsync());
});

2. Feature Gate Kullanmamak

Yanlış Kullanım: Her endpoint’te manuel feature check yapmak.

app.MapGet("/api/beta/dashboard", async (IFeatureManager fm, IDashboardService service) =>
{
    if (!await fm.IsEnabledAsync("BetaDashboard"))
        return Results.NotFound();
    return Results.Ok(await service.GetBetaDashboardAsync());
});

app.MapGet("/api/beta/reports", async (IFeatureManager fm, IReportService service) =>
{
    if (!await fm.IsEnabledAsync("BetaDashboard"))
        return Results.NotFound();
    return Results.Ok(await service.GetBetaReportsAsync());
});

İdeal Kullanım: FeatureGate attribute ile endpoint’leri koruyun.

// Controller ile
[FeatureGate("BetaDashboard")]
public class BetaDashboardController : ControllerBase
{
    [HttpGet("/api/beta/dashboard")]
    public async Task<IActionResult> GetDashboard() => Ok(await _service.GetAsync());

    [HttpGet("/api/beta/reports")]
    public async Task<IActionResult> GetReports() => Ok(await _service.GetReportsAsync());
}

// Minimal API ile
var beta = app.MapGroup("/api/beta")
    .AddEndpointFilter(async (context, next) =>
    {
        var fm = context.HttpContext.RequestServices.GetRequiredService<IFeatureManager>();
        if (!await fm.IsEnabledAsync("BetaDashboard"))
            return TypedResults.NotFound();
        return await next(context);
    });

beta.MapGet("/dashboard", async (IDashboardService service) =>
    TypedResults.Ok(await service.GetAsync()));

3. Percentage Rollout Yapmamak

Yanlış Kullanım: Özelliği herkese aynı anda açmak.

// appsettings.json - Ya herkese açık ya herkese kapalı
// { "FeatureManagement": { "NewCheckout": true } }

İdeal Kullanım: Kademeli rollout ile riski azaltın.

// appsettings.json
// {
//   "FeatureManagement": {
//     "NewCheckout": {
//       "EnabledFor": [
//         {
//           "Name": "Percentage",
//           "Parameters": { "Value": 25 }
//         }
//       ]
//     }
//   }
// }

// Kullanıcı bazlı targeting
// {
//   "FeatureManagement": {
//     "PremiumFeature": {
//       "EnabledFor": [
//         {
//           "Name": "Targeting",
//           "Parameters": {
//             "Audience": {
//               "Users": ["user1@example.com", "user2@example.com"],
//               "Groups": [
//                 { "Name": "Beta", "RolloutPercentage": 50 }
//               ],
//               "DefaultRolloutPercentage": 10
//             }
//           }
//         }
//       ]
//     }
//   }
// }

// Targeting context sağlama
builder.Services.AddFeatureManagement()
    .WithTargeting<HttpContextTargetingContextAccessor>();

public class HttpContextTargetingContextAccessor : ITargetingContextAccessor
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ValueTask<TargetingContext> GetContextAsync()
    {
        var user = _httpContextAccessor.HttpContext?.User;
        return new ValueTask<TargetingContext>(new TargetingContext
        {
            UserId = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value,
            Groups = user?.FindAll("group").Select(c => c.Value) ?? Array.Empty<string>()
        });
    }
}

4. Feature Flag’leri Temizlememek

Yanlış Kullanım: Eski flag’lerin kodda kalması.

if (await featureManager.IsEnabledAsync("NewSearch_v1")) { /* ... */ }
if (await featureManager.IsEnabledAsync("NewSearch_v2")) { /* ... */ }
if (await featureManager.IsEnabledAsync("NewSearch_v3_final")) { /* ... */ }
if (await featureManager.IsEnabledAsync("NewSearch_v3_final_REAL")) { /* ... */ }
// Hangi flag aktif? Kod okunamaz hale gelir

İdeal Kullanım: Feature flag yaşam döngüsünü yönetin.

// Feature flag'leri merkezi enum ile tanımlayın
public static class FeatureFlags
{
    public const string NewCheckout = "NewCheckout";
    public const string BetaDashboard = "BetaDashboard";

    // Deprecated: 2024-Q2'de kaldırılacak
    // public const string OldPayment = "OldPayment";
}

// Kullanım
if (await featureManager.IsEnabledAsync(FeatureFlags.NewCheckout))
{
    // yeni checkout
}

// Flag tamamen açıldıktan sonra:
// 1. Flag kontrolünü kaldırın, sadece yeni kodu bırakın
// 2. Eski kodu silin
// 3. Flag'i konfigürasyondan kaldırın

5. Feature Flag’leri Test Etmemek

Yanlış Kullanım: Feature flag durumlarını test etmemek.

[Fact]
public async Task GetProducts_ReturnsProducts()
{
    var result = await _service.GetAllAsync();
    Assert.NotEmpty(result);
    // Feature flag açık/kapalı senaryoları test edilmiyor
}

İdeal Kullanım: Her flag durumu için test yazın.

public class ProductServiceTests
{
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task GetProducts_RespectsFeatureFlag(bool flagEnabled)
    {
        var featureManager = new Mock<IFeatureManager>();
        featureManager
            .Setup(f => f.IsEnabledAsync("NewSearchAlgorithm"))
            .ReturnsAsync(flagEnabled);

        var service = new ProductService(_context, featureManager.Object);
        var result = await service.GetAllAsync();

        Assert.NotEmpty(result);
        if (flagEnabled)
            featureManager.Verify(f => f.IsEnabledAsync("NewSearchAlgorithm"), Times.Once);
    }
}

// Integration test ile
public class FeatureFlagIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task BetaEndpoint_Returns404_WhenFlagDisabled()
    {
        var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton<IFeatureManager>(new TestFeatureManager(
                    new Dictionary<string, bool> { ["BetaDashboard"] = false }));
            });
        }).CreateClient();

        var response = await client.GetAsync("/api/beta/dashboard");
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
}