Deno Deploy ile A/B Testing ve Feature Flag Yönetimi

Edge computing dünyasında A/B testing ve feature flag yönetimi, modern web uygulamalarının vazgeçilmez bileşenlerine dönüştü. Deno Deploy’un global edge ağı üzerinde bu mekanizmaları kurmak, hem düşük gecikme süresi hem de merkezi bir kontrol paneli olmadan ölçeklenebilir deney altyapısı oluşturma açısından ciddi avantajlar sunuyor. Bu yazıda, Deno Deploy üzerinde production-ready bir A/B testing sistemi ve feature flag yönetimi nasıl kurulur, adım adım inceleyeceğiz.

Neden Deno Deploy Üzerinde A/B Testing?

Geleneksel A/B testing yaklaşımlarında karşılaştığımız en büyük sorun, testlerin sunucu tarafında değil genellikle istemci tarafında çalışmasıdır. Bu durum “layout shift” denen o kötü görüntüyü, sayfa yanıp sönmelerini ve SEO sorunlarını beraberinde getirir. Deno Deploy’un edge fonksiyonları, kullanıcı isteği henüz origin sunucuya ulaşmadan, kullanıcıya en yakın edge node’da çalışır. Yani A/B testi mantığını milisaniyeler içinde, istemci hiçbir şey fark etmeden uygulayabilirsiniz.

Bir başka avantaj ise maliyet. Deno Deploy’un ücretsiz katmanı günlük 100.000 istek destekler ve edge’de çalışan hafif TypeScript fonksiyonları için bu oldukça cömert bir limit.

Temel Mimari

Sistemimiz üç ana bileşenden oluşacak:

  • Edge Worker: Gelen her isteği karşılayan ve karar veren Deno Deploy fonksiyonu
  • KV Store: Deno’nun dahili KV veritabanı üzerinde feature flag ve varyant konfigürasyonu
  • Kullanıcı Atama Motoru: Deterministik hash fonksiyonu ile kullanıcıları varyantlara atayan mantık

Önce basit bir proje yapısı oluşturalım:

mkdir deno-ab-testing && cd deno-ab-testing
touch main.ts
touch flags.ts
touch assignment.ts
touch middleware.ts
deno init

Feature Flag Veri Modeli

Feature flag sisteminin kalbi, flag tanımlamalarını içeren veri modelidir. Deno KV’yi kullanarak bu tanımları saklayacağız.

# flags.ts - Feature flag tip tanımları ve KV işlemleri

export interface FeatureFlag {
  id: string;
  name: string;
  enabled: boolean;
  rolloutPercentage: number;
  variants?: Variant[];
  targeting?: TargetingRule[];
  createdAt: number;
  updatedAt: number;
}

export interface Variant {
  id: string;
  name: string;
  weight: number; // 0-100 arası yüzde
  config: Record<string, unknown>;
}

export interface TargetingRule {
  attribute: string;
  operator: "equals" | "contains" | "startsWith" | "regex";
  value: string;
}

const kv = await Deno.openKv();

export async function getFlag(flagId: string): Promise<FeatureFlag | null> {
  const result = await kv.get<FeatureFlag>(["flags", flagId]);
  return result.value;
}

export async function setFlag(flag: FeatureFlag): Promise<void> {
  await kv.set(["flags", flag.id], {
    ...flag,
    updatedAt: Date.now(),
  });
}

export async function listFlags(): Promise<FeatureFlag[]> {
  const flags: FeatureFlag[] = [];
  const iter = kv.list<FeatureFlag>({ prefix: ["flags"] });
  for await (const entry of iter) {
    flags.push(entry.value);
  }
  return flags;
}

export async function deleteFlag(flagId: string): Promise<void> {
  await kv.delete(["flags", flagId]);
}

Deterministik Kullanıcı Atama Motoru

A/B testingde en kritik konu tutarlılıktır. Aynı kullanıcı her seferinde aynı varyantı görmelidir. Bunun için kullanıcı ID’si ve flag ID’sini birleştirerek deterministik bir hash üretiyoruz.

# assignment.ts - Kullanıcı-varyant atama motoru

export async function hashString(input: string): Promise<number> {
  const encoder = new TextEncoder();
  const data = encoder.encode(input);
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  const hashArray = new Uint8Array(hashBuffer);
  
  // İlk 4 byte'ı kullanarak 0-99 arası sayı üret
  const num = (hashArray[0] << 24) | (hashArray[1] << 16) | 
              (hashArray[2] << 8) | hashArray[3];
  return Math.abs(num) % 100;
}

export async function assignVariant(
  userId: string,
  flagId: string,
  variants: { id: string; weight: number }[]
): Promise<string | null> {
  const hashInput = `${userId}:${flagId}`;
  const hashValue = await hashString(hashInput);
  
  let cumulativeWeight = 0;
  for (const variant of variants) {
    cumulativeWeight += variant.weight;
    if (hashValue < cumulativeWeight) {
      return variant.id;
    }
  }
  
  return null;
}

export async function isUserInRollout(
  userId: string,
  flagId: string,
  rolloutPercentage: number
): Promise<boolean> {
  const hashInput = `rollout:${userId}:${flagId}`;
  const hashValue = await hashString(hashInput);
  return hashValue < rolloutPercentage;
}

export function getUserId(request: Request): string {
  // Cookie'den kullanıcı ID'si al veya yeni oluştur
  const cookies = request.headers.get("cookie") || "";
  const match = cookies.match(/ab_user_id=([^;]+)/);
  
  if (match) {
    return match[1];
  }
  
  // Anonim kullanıcılar için IP tabanlı ID
  const ip = request.headers.get("x-forwarded-for") || 
             request.headers.get("x-real-ip") || 
             "unknown";
  return `anon_${ip}`;
}

Ana Edge Worker

Şimdi gelen istekleri karşılayan ve A/B testing mantığını uygulayan ana worker’ı yazalım:

# main.ts - Ana Deno Deploy edge worker

import { getFlag, listFlags } from "./flags.ts";
import { assignVariant, isUserInRollout, getUserId } from "./assignment.ts";

const kv = await Deno.openKv();

async function evaluateFlag(flagId: string, request: Request) {
  const flag = await getFlag(flagId);
  
  if (!flag || !flag.enabled) {
    return { enabled: false, variant: null };
  }
  
  const userId = getUserId(request);
  
  // Rollout yüzdesini kontrol et
  const inRollout = await isUserInRollout(userId, flagId, flag.rolloutPercentage);
  if (!inRollout) {
    return { enabled: false, variant: null };
  }
  
  // Targeting kurallarını kontrol et
  if (flag.targeting && flag.targeting.length > 0) {
    const userAgent = request.headers.get("user-agent") || "";
    const country = request.headers.get("cf-ipcountry") || 
                    request.headers.get("x-country") || "";
    
    for (const rule of flag.targeting) {
      let attributeValue = "";
      
      if (rule.attribute === "country") attributeValue = country;
      if (rule.attribute === "userAgent") attributeValue = userAgent;
      
      const matches = checkTargetingRule(attributeValue, rule.operator, rule.value);
      if (!matches) return { enabled: false, variant: null };
    }
  }
  
  // Varyant ata
  if (flag.variants && flag.variants.length > 0) {
    const variantId = await assignVariant(userId, flagId, flag.variants);
    const variant = flag.variants.find(v => v.id === variantId);
    
    // Atamayı KV'ye kaydet (analitik için)
    await kv.set(
      ["assignments", userId, flagId],
      { variantId, timestamp: Date.now() },
      { expireIn: 30 * 24 * 60 * 60 * 1000 } // 30 gün
    );
    
    return { enabled: true, variant: variant || null };
  }
  
  return { enabled: true, variant: null };
}

function checkTargetingRule(
  value: string,
  operator: string,
  target: string
): boolean {
  switch (operator) {
    case "equals": return value === target;
    case "contains": return value.includes(target);
    case "startsWith": return value.startsWith(target);
    case "regex": return new RegExp(target).test(value);
    default: return false;
  }
}

Deno.serve(async (request: Request) => {
  const url = new URL(request.url);
  
  // Admin API endpoint'leri
  if (url.pathname.startsWith("/admin/flags")) {
    return handleAdminAPI(request, url);
  }
  
  // Flag değerlendirme endpoint'i
  if (url.pathname === "/evaluate") {
    return handleEvaluate(request, url);
  }
  
  // Tüm flag'leri döndür (SDK için)
  if (url.pathname === "/flags/all") {
    return handleAllFlags(request);
  }
  
  return new Response("Deno AB Testing Edge Worker", { status: 200 });
});

Admin API ve Flag Yönetimi

Production ortamında flag’leri programatik olarak yönetmek için basit bir REST API ekleyelim:

# admin_api.ts - Flag yönetim API'si

async function handleAdminAPI(request: Request, url: URL): Promise<Response> {
  // Basit API key authentication
  const apiKey = request.headers.get("x-api-key");
  const expectedKey = Deno.env.get("ADMIN_API_KEY");
  
  if (!apiKey || apiKey !== expectedKey) {
    return new Response(JSON.stringify({ error: "Unauthorized" }), {
      status: 401,
      headers: { "content-type": "application/json" },
    });
  }
  
  const flagId = url.pathname.split("/").pop();
  
  if (request.method === "GET" && url.pathname === "/admin/flags") {
    const flags = await listFlags();
    return jsonResponse(flags);
  }
  
  if (request.method === "POST" && url.pathname === "/admin/flags") {
    const body = await request.json() as FeatureFlag;
    
    if (!body.id || !body.name) {
      return jsonResponse({ error: "id ve name zorunludur" }, 400);
    }
    
    const flag: FeatureFlag = {
      ...body,
      enabled: body.enabled ?? false,
      rolloutPercentage: body.rolloutPercentage ?? 100,
      createdAt: Date.now(),
      updatedAt: Date.now(),
    };
    
    await setFlag(flag);
    return jsonResponse(flag, 201);
  }
  
  if (request.method === "PATCH" && flagId) {
    const existing = await getFlag(flagId);
    if (!existing) {
      return jsonResponse({ error: "Flag bulunamadi" }, 404);
    }
    
    const updates = await request.json() as Partial<FeatureFlag>;
    const updated = { ...existing, ...updates, updatedAt: Date.now() };
    await setFlag(updated);
    return jsonResponse(updated);
  }
  
  if (request.method === "DELETE" && flagId) {
    await deleteFlag(flagId);
    return jsonResponse({ success: true });
  }
  
  return jsonResponse({ error: "Method not allowed" }, 405);
}

function jsonResponse(data: unknown, status = 200): Response {
  return new Response(JSON.stringify(data, null, 2), {
    status,
    headers: {
      "content-type": "application/json",
      "access-control-allow-origin": "*",
    },
  });
}

// Kullanım örneği - yeni flag oluşturma
async function createCheckoutButtonTest() {
  const flag: FeatureFlag = {
    id: "checkout_button_color",
    name: "Checkout Buton Rengi A/B Testi",
    enabled: true,
    rolloutPercentage: 80,
    variants: [
      { id: "control", name: "Mavi Buton", weight: 50, config: { color: "#0066cc" } },
      { id: "treatment", name: "Yeşil Buton", weight: 50, config: { color: "#28a745" } },
    ],
    targeting: [
      { attribute: "country", operator: "equals", value: "TR" }
    ],
    createdAt: Date.now(),
    updatedAt: Date.now(),
  };
  
  await setFlag(flag);
  console.log("Flag oluşturuldu:", flag.id);
}

Gerçek Dünya Senaryosu: E-ticaret Checkout Akışı

Diyelim ki bir e-ticaret platformu için checkout sayfasında iki farklı akış test etmek istiyorsunuz. Biri tek sayfalı checkout, diğeri adım adım ilerleyen bir wizard. Bu senaryo için edge’de nasıl yönlendirme yapacağımızı görelim:

# checkout_experiment.ts - E-ticaret checkout A/B testi

interface CheckoutConfig {
  layout: "single-page" | "wizard";
  showProgressBar: boolean;
  trustBadges: boolean;
  ctaText: string;
}

async function handleCheckoutRequest(request: Request): Promise<Response> {
  const userId = getUserId(request);
  const result = await evaluateFlag("checkout_layout_v2", request);
  
  let config: CheckoutConfig;
  
  if (result.enabled && result.variant) {
    config = result.variant.config as CheckoutConfig;
  } else {
    // Kontrol grubu - varsayılan config
    config = {
      layout: "single-page",
      showProgressBar: false,
      trustBadges: true,
      ctaText: "Siparişi Tamamla",
    };
  }
  
  // Kullanıcıya cookie set et
  const response = await fetchCheckoutPage(config);
  const headers = new Headers(response.headers);
  
  headers.append(
    "set-cookie",
    `ab_user_id=${userId}; Max-Age=2592000; Path=/; SameSite=Lax`
  );
  
  // Analitik için header ekle
  headers.set("x-ab-variant", result.variant?.id || "control");
  headers.set("x-ab-flag", "checkout_layout_v2");
  
  // Impression'ı kaydet
  await recordImpression(userId, "checkout_layout_v2", result.variant?.id || "control");
  
  return new Response(response.body, {
    status: response.status,
    headers,
  });
}

async function recordImpression(
  userId: string,
  flagId: string,
  variantId: string
): Promise<void> {
  const kv = await Deno.openKv();
  const key = ["impressions", flagId, variantId, Date.now().toString()];
  
  await kv.set(key, {
    userId,
    flagId,
    variantId,
    timestamp: Date.now(),
  }, { expireIn: 7 * 24 * 60 * 60 * 1000 }); // 7 gün sakla
}

async function getImpressionStats(flagId: string): Promise<Record<string, number>> {
  const kv = await Deno.openKv();
  const stats: Record<string, number> = {};
  const iter = kv.list({ prefix: ["impressions", flagId] });
  
  for await (const entry of iter) {
    const data = entry.value as { variantId: string };
    stats[data.variantId] = (stats[data.variantId] || 0) + 1;
  }
  
  return stats;
}

Deploy ve Konfigürasyon

Uygulamayı Deno Deploy’a almak için gerekli adımlar:

# Deno Deploy CLI ile deploy
deployctl deploy --project=ab-testing-edge main.ts

# Ortam değişkenlerini ayarla
deployctl env set ADMIN_API_KEY="gizli-admin-anahtariniz" 
  --project=ab-testing-edge

# Deploy durumunu kontrol et
deployctl status --project=ab-testing-edge

# Bir flag oluştur
curl -X POST https://ab-testing-edge.deno.dev/admin/flags 
  -H "Content-Type: application/json" 
  -H "x-api-key: gizli-admin-anahtariniz" 
  -d '{
    "id": "new_homepage_hero",
    "name": "Ana Sayfa Hero Testi",
    "enabled": true,
    "rolloutPercentage": 50,
    "variants": [
      {"id": "v1", "name": "Video Hero", "weight": 50, "config": {"type": "video"}},
      {"id": "v2", "name": "Statik Hero", "weight": 50, "config": {"type": "image"}}
    ]
  }'

# Belirli bir kullanıcı için flag değerlendir
curl "https://ab-testing-edge.deno.dev/evaluate?flagId=new_homepage_hero" 
  -H "Cookie: ab_user_id=user123"

# İstatistikleri çek
curl "https://ab-testing-edge.deno.dev/admin/stats/new_homepage_hero" 
  -H "x-api-key: gizli-admin-anahtariniz"

İstemci Tarafı Entegrasyon

Frontend tarafında bu flag sistemini kullanmak için basit bir JavaScript SDK yazalım:

# client_sdk.js - Tarayıcı tarafı SDK örneği

class ABTestingClient {
  constructor(edgeUrl) {
    this.edgeUrl = edgeUrl;
    this.cache = new Map();
    this.userId = this.getOrCreateUserId();
  }
  
  getOrCreateUserId() {
    let userId = localStorage.getItem("ab_user_id");
    if (!userId) {
      userId = `user_${Math.random().toString(36).substr(2, 9)}`;
      localStorage.setItem("ab_user_id", userId);
    }
    return userId;
  }
  
  async evaluate(flagId) {
    if (this.cache.has(flagId)) {
      return this.cache.get(flagId);
    }
    
    try {
      const response = await fetch(
        `${this.edgeUrl}/evaluate?flagId=${flagId}`,
        {
          headers: { "Cookie": `ab_user_id=${this.userId}` },
          credentials: "include",
        }
      );
      
      const result = await response.json();
      this.cache.set(flagId, result);
      
      // Varyant bilgisini data layer'a gönder (GA4, GTM için)
      if (window.dataLayer && result.enabled) {
        window.dataLayer.push({
          event: "ab_test_impression",
          flagId: flagId,
          variantId: result.variant?.id,
        });
      }
      
      return result;
    } catch (err) {
      console.error("Flag değerlendirme hatası:", err);
      return { enabled: false, variant: null };
    }
  }
  
  async getVariantConfig(flagId) {
    const result = await this.evaluate(flagId);
    return result.variant?.config || null;
  }
}

// Kullanım örneği
const abClient = new ABTestingClient("https://ab-testing-edge.deno.dev");

const heroConfig = await abClient.getVariantConfig("new_homepage_hero");
if (heroConfig?.type === "video") {
  document.getElementById("hero").classList.add("video-hero");
} else {
  document.getElementById("hero").classList.add("image-hero");
}

Performans ve Dikkat Edilmesi Gerekenler

Deno Deploy üzerinde A/B testing sistemi kurarken bazı konulara dikkat etmek gerekiyor:

KV okuma maliyeti: Her istek için birden fazla KV okuma yapmak gecikme süresini artırabilir. Flag konfigürasyonlarını in-memory cache’e alarak belirli aralıklarla yenilemek daha sağlıklı bir yaklaşım.

Varyant tutarlılığı: Aynı kullanıcının farklı cihazlardan gelmesi durumunda cookie tabanlı ID çalışmaz. User ID’yi backend authentication sisteminden almak daha güvenilir sonuçlar verir.

İstatistiksel anlamlılık: Flag açma ve kapatma kararlarını anekdot veriye değil, istatistiksel anlamlılık testlerine dayandırın. Minimum %95 güven aralığı ve yeterli örneklem büyüklüğü sağlanmadan sonuç çıkarmayın.

Rollout stratejisi: Yeni bir feature’ı önce %5 kullanıcıya açın, hata izleme sisteminde anomali yoksa %25, %50, %100 şeklinde kademeli artırın. Deno KV üzerindeki tek bir PATCH isteği ile bunu anlık olarak değiştirebilirsiniz.

Cache invalidation: Edge’deki flag cache’i stale hale gelebilir. Deno Deploy’un built-in KV watch özelliği veya kısa TTL ile cache yenileme stratejisi belirleyin.

GDPR uyumu: Kullanıcı ID’lerini ve atama verilerini saklarken veri minimizasyonu ilkesine uyun. Kişisel veri içermeyen anonim ID’ler tercih edin ve saklama sürelerini mümkün olduğunca kısa tutun.

Sonuç

Deno Deploy üzerinde edge-native bir A/B testing ve feature flag sistemi kurmak, hem teknik hem de iş açısından güçlü avantajlar sağlıyor. Sıfır cold start süresi, global dağıtım ve Deno KV’nin düşük gecikmeli veri erişimi bir araya gelince kullanıcılar hiçbir performans kaybı yaşamadan farklı deneyimler görebiliyor. Yazdığımız sistem aşağıdakileri destekliyor:

  • Yüzde tabanlı kademeli rollout
  • Ülke ve tarayıcı gibi özelliklere dayalı hedefleme
  • Çok varyantlı deneyler
  • Deterministik kullanıcı ataması
  • Analitik için impression kaydı
  • REST API üzerinden programatik yönetim

Bu altyapıyı kendi ihtiyaçlarınıza göre genişletebilirsiniz. Örneğin Slack webhook entegrasyonu ile flag değişikliklerinde ekibe otomatik bildirim, flag sonuçlarını Grafana’ya aktarmak için Prometheus metrics endpoint’i veya mevcut kullanıcı yönetim sisteminizle entegrasyon gibi eklemeler yapmak oldukça kolay. Edge computing ve serverless teknolojilerin olgunlaştığı bu dönemde A/B testing altyapınızı da edge’e taşımak, ölçekleme sorunlarından kurtulmanın ve gerçek anlamda global bir kullanıcı deneyimi sunmanın en pratik yolu haline geliyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir