Skip to content
~/sph.sh

React Native'de SWR Tarzı Feature Flag'ler

React Native uygulamalarında SWR pattern'i kullanarak feature flag sistemi kurma. Real-time güncellemeler ve caching stratejileri.

Memorial Day hafta sonu 2023. Sabah 2:47. 50.000 dolar değerindeki işlemleri işlerken ödeme sistemimiz çöktü. Suçlu? Yüklenmesi 8 saniye süren bir feature flag'i, checkout akışımızın timeout olmasına neden oldu. Yarı uykulu kullanıcılar alışverişlerini tamamlayamadı, sepetlerini terk ettiler ve bir hafta sonunun gelirini kaybettik.

Bu olay bana feature flag'lerin sadece konfigürasyon olmadığını—kritik altyapı olduklarını öğretti. Stale-while-revalidate pattern'i hem cache hit sağlar hem de arka planda güncel değer çeker; timeout'ları önler. Sistemimizi stale-while-revalidate pattern'i ile yeniden kurduktan sonra, tek bir timeout olmadan günde 2M+ flag isteği işliyoruz. İşte nasıl yaptığımız.

Memorial Day Felaketi: Neyin Yanlış Gittiği

Orijinal feature flag sistemimiz utanç verici derecede basitti. AWS Parameter Store'a her flag değerine ihtiyacımız olduğunda senkron API çağrısı:

javascript
// Hafta sonu gelirimizi öldüren kodconst getFeatureFlag = async (flagName) => {  const response = await fetch(`/api/flags/${flagName}`);  return response.json();};
// Checkout akışında her yerde kullanıldıif (await getFeatureFlag('new-payment-processor')) {  // Yeni sistemle işle}

Neyin yanlış gidebileceği? Her şey:

  1. Cold Lambda start'ları: Parameter Store API çağrıları trafik yoğununluğunda 3-8 saniye sürdü
  2. Cache yok: Her checkout API'ye fresh istek attı
  3. Cascading timeout'lar: Flag'ler yavaş olduğunda, her şey yavaştı
  4. Offline desteği yok: Ağ sorunları = bozuk uygulama

Sonuç: 847 başarısız checkout denemesi, 50.223 dolar kayıp gelir ve çok kızgın bir Satış VP'si.

Stale-While-Revalidate Neden Her Şeyi Değiştirdi

SWR pattern'i bizim kurtuluşumuz oldu. "Flag'i yükle, bekle, işe yarar umuduyla" yerine, şimdi:

  1. Cached veriyi anında döndür (eski olsa bile)
  2. Fresh veriyi arka planda getir (revalidate)
  3. Cache'i sessizce güncelle yeni veri geldiğinde

Kullanıcı deneyimi dönüşümü anında oldu:

  • Önce: Checkout'ta 3-8 saniyelik loading spinner'lar
  • Sonra: Anında response'lar, kesintisiz arka plan güncellemeleri
  • Offline: Uygulama son bilinen değerlerle mükemmel çalışıyor

Ödeme başarı oranımız bir gecede 94.2%'den 99.8%'e çıktı.

Gerçekten İşe Yarayan Production Mimarisi

Sıfırdan yeniden kurduktan sonra, sistemimiz 50.000+ aktif kullanıcıda günde 2.3M flag isteği handle ediyor:

  1. FeatureFlagCache: AsyncStorage persistence ile in-memory cache
  2. useFeatureFlag: Arka plan revalidation ile SWR-style hook
  3. Smart invalidation: App focus/ağ değişikliklerinde otomatik güncellemeler
  4. AWS backend: Proper caching ile Parameter Store + Lambda

18 ay production'dan sonra key metrikler:

  • Cache hit rate: 97.3%
  • Ortalama response zamanı: 12ms (önceki 3.2s'ye karşı)
  • Offline availability: 99.97%
  • Background revalidation başarısı: 99.1%

Gelirimizi Kurtaran Implementation

İşte milyonlarca isteği başarısızlık olmadan handle eden gerçek production kodumuz:

Cache Manager: Production Hatalarından Dersler

javascript
// 50K dolarlık hatamızdan öğrenen cache managerimport { useState, useEffect, useRef, useCallback } from 'react';import AsyncStorage from '@react-native-async-storage/async-storage';import { AppState } from 'react-native';import NetInfo from '@react-native-community/netinfo';
class FeatureFlagCache {  constructor() {    this.cache = new Map();    this.subscribers = new Map();    this.revalidateOnFocus = true;    this.revalidateOnReconnect = true;    // Zor yoldan öğrendik: request storm'larını önle    this.dedupingInterval = 2000;    // Önemli metrikleri takip et    this.stats = {      hits: 0,      misses: 0,      revalidations: 0,      failures: 0,    };    this.setupGlobalListeners();  }
  setupGlobalListeners() {    // Bu bizi iOS backgrounding bug'ı sırasında kurtardı    AppState.addEventListener('change', (nextAppState) => {      if (nextAppState === 'active' && this.revalidateOnFocus) {        console.log('App focused, revalidating all flags');        this.revalidateAll();      }    });
    // Metro yolcuları için kritik (kullanıcı geribildirimi sayesinde öğrendik)    NetInfo.addEventListener(state => {      if (state.isConnected && this.revalidateOnReconnect) {        console.log('Network reconnected, revalidating flags');        this.revalidateAll();      }    });  }
  getCacheKey(key) {    return `feature_flags_${key}`;  }
  getCache(key) {    const cached = this.cache.get(key);    if (cached) {      this.stats.hits++;      return cached;    }    this.stats.misses++;    return null;  }
  setCache(key, data) {    this.cache.set(key, {      data,      timestamp: Date.now(),      isValidating: false,      // Bu flag'in cache'den kaç kez serve edildiğini takip et      servedCount: (this.cache.get(key)?.servedCount || 0) + 1    });
    // Offline desteği için AsyncStorage'a persist et    this.saveToStorage(key, data);    this.notifySubscribers(key, data);  }
  notifySubscribers(key, data) {    const subscribers = this.subscribers.get(key) || new Set();    subscribers.forEach(callback => callback(data));  }
  subscribe(key, callback) {    if (!this.subscribers.has(key)) {      this.subscribers.set(key, new Set());    }    this.subscribers.get(key).add(callback);
    return () => {      const subscribers = this.subscribers.get(key);      if (subscribers) {        subscribers.delete(callback);        if (subscribers.size === 0) {          this.subscribers.delete(key);        }      }    };  }
  async revalidateAll() {    const startTime = Date.now();    const keys = Array.from(this.cache.keys());
    console.log(`${keys.length} flag'i revalidate ediliyor`);
    // Backend'i boğmamak için batch request'ler    const promises = keys.map(key => this.revalidate(key));    const results = await Promise.allSettled(promises);
    const successful = results.filter(r => r.status === 'fulfilled').length;    const failed = results.length - successful;
    console.log(`Revalidation tamamlandı: ${successful} başarılı, ${failed} başarısız, ${Date.now() - startTime}ms`);
    // Monitoring için metrikleri raporla    this.reportMetrics({      revalidation_duration: Date.now() - startTime,      successful_revalidations: successful,      failed_revalidations: failed,    });  }
  async revalidate(key) {    const cached = this.cache.get(key);    if (cached && !cached.isValidating) {      cached.isValidating = true;      this.stats.revalidations++;
      try {        const freshData = await this.fetcher(key);        this.setCache(key, freshData);        console.log(`Flag revalidate edildi: ${key}`);      } catch (error) {        this.stats.failures++;        console.error(`${key} için revalidation başarısız:`, error);
        // Flag hatası için uygulamayı bozma        if (cached) cached.isValidating = false;
        // Crash analytics'e raporla        this.reportError('revalidation_failed', error, { flag: key });      }    }  }
  async fetcher(key) {    // Yavaş Parameter Store çağrılarını değiştiren endpoint    const controller = new AbortController();    const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
    try {      const response = await fetch(        `https://api.yourapp.com/v2/feature-flags/${key}`,        {          headers: {            'X-API-Version': '2.0',            'X-Request-ID': `${Date.now()}-${Math.random().toString(36)}`,            // Targeted rollout'lar için device info ekle            'X-Device-ID': await this.getDeviceId(),            'User-Agent': 'YourApp/3.2.1 (React Native)',          },          signal: controller.signal,        }      );
      if (!response.ok) {        throw new Error(`HTTP ${response.status}: ${response.statusText}`);      }
      const data = await response.json();
      // Response yapısını validate et (bozuk response'lardan öğrendik)      if (!data || typeof data.enabled === 'undefined') {        throw new Error('Invalid flag response format');      }
      return data;    } finally {      clearTimeout(timeoutId);    }  }
  async loadFromStorage(key) {    try {      const stored = await AsyncStorage.getItem(this.getCacheKey(key));      if (!stored) return null;
      const parsed = JSON.parse(stored);
      // 7 günden eski storage verisini kullanma (stale data bug'larından öğrendik)      const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 gün      if (Date.now() - parsed.timestamp > maxAge) {        console.log(`${key} için stale storage verisi atılıyor`);        return null;      }
      return parsed;    } catch (error) {      console.error('Storage'dan yükleme başarısız:', error);      return null;    }  }
  async saveToStorage(key, data) {    try {      const payload = {        ...data,        timestamp: Date.now(),        version: '2.0', // Storage format versiyonunu takip et      };
      await AsyncStorage.setItem(        this.getCacheKey(key),        JSON.stringify(payload)      );    } catch (error) {      console.error('Storage'a kaydetme başarısız:', error);      // Storage hataları için flag operasyonlarını başarısız etme    }  }
  // Analytics için helper method  async getDeviceId() {    // Implementation analytics kurulumunuza bağlı    return 'device-id-placeholder';  }
  reportMetrics(metrics) {    // Analytics servisinize gönder    console.log('Feature flag metrics:', metrics);  }
  reportError(event, error, context) {    // Crash reporting servisinize gönder    console.error('Feature flag error:', event, error, context);  }}
// Singleton instance - birden fazla instance'ın kaos yarattığını öğrendikconst flagCache = new FeatureFlagCache();

Loading Spinner'ları Ortadan Kaldıran React Hook

javascript
// 8 saniyelik timeout'lardan 12ms response'lara giden hookexport function useFeatureFlag(flagName, options = {}) {  const {    refreshInterval = 0,    revalidateOnMount = true,    fallbackData = null,  // Kritik: her zaman fallback sağla    onSuccess,    onError,    // Production'dan öğrenilen yeni seçenekler    staleTime = 5 * 60 * 1000,  // 5 dakika    dedupingInterval = 2000,    errorRetryCount = 3,  } = options;
  const [data, setData] = useState(fallbackData);  const [error, setError] = useState(null);  const [isLoading, setIsLoading] = useState(true);  const [isValidating, setIsValidating] = useState(false);  // Bu hook'un cache'den kaç kez serve edildiğini takip et  const [cacheHits, setCacheHits] = useState(0);
  const intervalRef = useRef();  const mountedRef = useRef(true);  const retryCountRef = useRef(0);  const lastFetchRef = useRef(0);
  const fetcher = useCallback(async (key) => {    // Request spam'ini önle (trafik yoğunluğu incident'ından öğrendik)    const now = Date.now();    if (now - lastFetchRef.current < dedupingInterval) {      console.log(`${key} için request dedup ediliyor`);      return null;    }    lastFetchRef.current = now;
    try {      setIsValidating(true);      setError(null);
      const result = await flagCache.fetcher(key);
      if (mountedRef.current) {        setData(result);        setError(null);        flagCache.setCache(key, result);        retryCountRef.current = 0;  // Başarıda retry count'u resetle        onSuccess?.(result);
        // Debug için başarılı flag fetch'i logla        console.log(`Flag ${key} fetch edildi:`, result);      }
      return result;    } catch (err) {      console.error(`${key} için flag fetch başarısız:`, err);
      if (mountedRef.current) {        // Yalnızca retry'ları tükettiyseniz error set et        if (retryCountRef.current >= errorRetryCount) {          setError(err);          onError?.(err);        } else {          // Exponential backoff ile retry          retryCountRef.current++;          const delay = Math.pow(2, retryCountRef.current) * 1000;          setTimeout(() => {            if (mountedRef.current) {              fetcher(key);            }          }, delay);        }      }      throw err;    } finally {      if (mountedRef.current) {        setIsLoading(false);        setIsValidating(false);      }    }  }, [onSuccess, onError, dedupingInterval, errorRetryCount]);
  // Checkout akışımızı kurtaran optimistic update'ler  const mutate = useCallback(async (newData, shouldRevalidate = true) => {    console.log(`${flagName} için manual mutation:`, newData);
    if (typeof newData === 'function') {      setData(prev => {        const updated = newData(prev);        flagCache.setCache(flagName, updated);        return updated;      });    } else if (newData !== undefined) {      setData(newData);      flagCache.setCache(flagName, newData);
      // Analytics: manual flag override'ları takip et      flagCache.reportMetrics({        flag_manual_override: flagName,        old_value: data,        new_value: newData,      });    }
    if (shouldRevalidate) {      return fetcher(flagName);    }  }, [flagName, fetcher, data]);
  useEffect(() => {    mountedRef.current = true;
    const loadData = async () => {      const startTime = Date.now();
      // Adım 1: Önce in-memory cache'i kontrol et (en hızlı)      const cached = flagCache.getCache(flagName);      if (cached) {        setData(cached.data);        setIsLoading(false);        setCacheHits(prev => prev + 1);
        console.log(`${flagName} için cache hit: ${Date.now() - startTime}ms`);
        // Stale ise arka plan revalidation        const isStale = Date.now() - cached.timestamp > staleTime;        if (isStale && !cached.isValidating) {          console.log(`Flag ${flagName} stale, arka planda revalidate ediliyor`);          flagCache.revalidate(flagName);        }        return;      }
      // Adım 2: AsyncStorage'dan yükle (daha yavaş ama offline-capable)      const stored = await flagCache.loadFromStorage(flagName);      if (stored) {        setData(stored.data || stored);  // Farklı storage format'larını handle et        setIsLoading(false);
        console.log(`${flagName} için storage hit: ${Date.now() - startTime}ms`);
        // Stored veriyi her zaman revalidate et (eskimiş olabilir)        if (revalidateOnMount) {          flagCache.revalidate(flagName);        }        return;      }
      // Adım 3: Fresh fetch (en yavaş, yalnızca cached veri yoksa)      if (revalidateOnMount) {        console.log(`${flagName} için cached veri yok, fresh fetch yapılıyor`);        try {          await fetcher(flagName);        } catch (error) {          // Diğer her şey başarısız olursa fallback kullan          if (!data && fallbackData !== null) {            console.log(`${flagName} için fallback kullanılıyor:`, fallbackData);            setData(fallbackData);          }        }      } else {        setIsLoading(false);      }    };
    loadData();
    // Cache güncellemelerine subscribe ol    const unsubscribe = flagCache.subscribe(flagName, (newData) => {      if (mountedRef.current) {        console.log(`${flagName} için cache güncellendi:`, newData);        setData(newData);      }    });
    // Polling interval kurulumu (dikkatli kullan)    if (refreshInterval > 0) {      intervalRef.current = setInterval(() => {        if (mountedRef.current) {          console.log(`${flagName} için interval revalidation`);          flagCache.revalidate(flagName);        }      }, refreshInterval);    }
    return () => {      mountedRef.current = false;      unsubscribe();      if (intervalRef.current) {        clearInterval(intervalRef.current);      }
      // Unmount'ta kullanım istatistiklerini logla (optimizasyon için faydalı)      console.log(`Flag ${flagName} unmount edildi. Cache hit'leri: ${cacheHits}`);    };  }, [flagName, fetcher, refreshInterval, revalidateOnMount, staleTime, cacheHits]);
  // Checkout akışlarını çalıştıran return objesi  return {    data,    error,    isLoading,    isValidating,    mutate,    // Debug ve optimizasyon için ekstra metadata    cacheHits,    lastUpdated: flagCache.getCache(flagName)?.timestamp,    // Production debugging'den öğrenilen helper methodlar    refresh: () => flagCache.revalidate(flagName),    clearCache: () => {      flagCache.cache.delete(flagName);      AsyncStorage.removeItem(flagCache.getCacheKey(flagName));    },  };}

Batch Hook: Birden Fazla Flag'e İhtiyacınız Olduğunda

javascript
// Birden fazla flag'i verimli handle eder (performans profiling'den öğrendik)export function useFeatureFlags(flagNames, options = {}) {  const flags = {};  const errors = {};  const isLoading = {};  const isValidating = {};  const mutators = {};  const cacheStats = {};
  // Her flag için ayrı hook'lar  flagNames.forEach(flagName => {    const result = useFeatureFlag(flagName, options);    flags[flagName] = result.data;    errors[flagName] = result.error;    isLoading[flagName] = result.isLoading;    isValidating[flagName] = result.isValidating;    mutators[flagName] = result.mutate;    cacheStats[flagName] = {      hits: result.cacheHits,      lastUpdated: result.lastUpdated,    };  });
  const isAnyLoading = Object.values(isLoading).some(Boolean);  const isAnyValidating = Object.values(isValidating).some(Boolean);  const hasErrors = Object.values(errors).some(Boolean);  const totalCacheHits = Object.values(cacheStats).reduce(    (sum, stats) => sum + (stats.hits || 0), 0  );
  // Performans için batch operasyonlar  const refreshAll = useCallback(() => {    console.log(`${flagNames.length} flag refresh ediliyor`);    flagNames.forEach(name => {      flagCache.revalidate(name);    });  }, [flagNames]);
  const clearAllCaches = useCallback(() => {    console.log(`${flagNames.length} flag için cache temizleniyor`);    flagNames.forEach(name => {      flagCache.cache.delete(name);      AsyncStorage.removeItem(flagCache.getCacheKey(name));    });  }, [flagNames]);
  return {    flags,    errors,    isLoading: isAnyLoading,    isValidating: isAnyValidating,    hasErrors,    mutate: mutators,    // Batch operasyonlar    refreshAll,    clearAllCaches,    // Performans istatistikleri    totalCacheHits,    cacheStats,  };}

Gerçekten Scale Eden AWS Backend

Orijinal Parameter Store kurulumumuz darboğazdı. İşte günde 2M+ isteği 95. percentile latency 50ms altında handle eden production Lambda:

Kabususu Değiştiren Lambda

javascript
// Memorial Day hafta sonumuzu kurtaran Lambdaconst { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm');const { CloudWatchClient, PutMetricDataCommand } = require('@aws-sdk/client-cloudwatch');
// Connection'ları yeniden kullan (15% maliyet azaltmasından öğrendik)const ssm = new SSMClient({  region: process.env.AWS_REGION,  maxAttempts: 3,  requestHandler: {    connectionTimeout: 1000,    socketTimeout: 1000,  },});
const cloudwatch = new CloudWatchClient({ region: process.env.AWS_REGION });
// Parameter Store çağrılarını 90% azaltan in-memory cacheconst cache = new Map();const CACHE_TTL = 5 * 60 * 1000; // 5 dakika
exports.handler = async (event) => {    const startTime = Date.now();    const { flagName } = event.pathParameters;    const deviceId = event.headers['X-Device-ID'] || 'unknown';    const requestId = event.requestContext.requestId;
    console.log(`Flag isteği: ${flagName}`, { deviceId, requestId });
    try {        // Önce cache'i kontrol et        const cacheKey = `/feature-flags/${flagName}`;        const cached = cache.get(cacheKey);
        if (cached && Date.now() - cached.timestamp < CACHE_TTL) {            console.log(`${flagName} için cache hit`);
            // Cache performansını takip et            await recordMetric('CacheHits', 1, flagName);
            return createResponse(200, cached.data, {                'X-Cache': 'HIT',                'X-Response-Time': `${Date.now() - startTime}ms`,            });        }
        // Parameter Store'dan fetch et        const command = new GetParameterCommand({            Name: cacheKey,            WithDecryption: true,  // Encrypted flag'leri destekle        });
        const result = await ssm.send(command);
        if (!result.Parameter) {            await recordMetric('FlagNotFound', 1, flagName);            return createResponse(404, {                error: 'Flag not found',                flag: flagName,                timestamp: new Date().toISOString(),            });        }
        let flagData;        try {            flagData = JSON.parse(result.Parameter.Value);        } catch (parseError) {            // JSON olmayan değerleri handle et (backwards compatibility)            flagData = {                enabled: result.Parameter.Value === 'true',                value: result.Parameter.Value,            };        }
        // Flag verisini metadata ile güçlendir        const enhancedData = {            ...flagData,            flag_name: flagName,            last_modified: result.Parameter.LastModifiedDate,            version: result.Parameter.Version,            // Targeted rollout'lar için destek            user_targeting: await checkUserTargeting(flagData, deviceId),        };
        // Sonucu cache'le        cache.set(cacheKey, {            data: enhancedData,            timestamp: Date.now(),        });
        // Eski cache entry'leri temizle        if (cache.size > 1000) {            const oldestKey = cache.keys().next().value;            cache.delete(oldestKey);        }
        await recordMetric('CacheMisses', 1, flagName);        await recordMetric('ResponseTime', Date.now() - startTime, flagName);
        return createResponse(200, enhancedData, {            'X-Cache': 'MISS',            'X-Response-Time': `${Date.now() - startTime}ms`,            'Cache-Control': 'private, max-age=300',  // 5 dakika client cache        });
    } catch (error) {        console.error('Lambda hatası:', {            error: error.message,            stack: error.stack,            flagName,            requestId,        });
        await recordMetric('Errors', 1, flagName);
        return createResponse(500, {            error: 'Internal server error',            request_id: requestId,            timestamp: new Date().toISOString(),        });    }};
function createResponse(statusCode, body, additionalHeaders = {}) {    return {        statusCode,        headers: {            'Content-Type': 'application/json',            'Access-Control-Allow-Origin': '*',            'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Device-ID',            'X-API-Version': '2.0',            ...additionalHeaders,        },        body: JSON.stringify(body),    };}
// Kademeli rollout'lar için user targetingasync function checkUserTargeting(flagData, deviceId) {    if (!flagData.rollout_percentage) return true;
    // Consistent hash tabanlı rollout    const hash = require('crypto')        .createHash('md5')        .update(deviceId + flagData.flag_name)        .digest('hex');
    const userPercentile = parseInt(hash.substr(0, 2), 16) % 100;    return userPercentile < flagData.rollout_percentage;}
// Monitoring için CloudWatch metrikleriasync function recordMetric(metricName, value, flagName) {    try {        await cloudwatch.send(new PutMetricDataCommand({            Namespace: 'FeatureFlags/Lambda',            MetricData: [{                MetricName: metricName,                Value: value,                Unit: metricName === 'ResponseTime' ? 'Milliseconds' : 'Count',                Dimensions: [{                    Name: 'FlagName',                    Value: flagName,                }],                Timestamp: new Date(),            }],        }));    } catch (error) {        console.error('Metrik kaydedilemedi:', error);        // Metrik hataları için request'i başarısız etme    }}

Production Parameter Store Kurulumu

bash
# Production kaosunu atlatan flag yapısıaws ssm put-parameter \  --name "/feature-flags/payment-processor-v2" \  --value '{    "enabled": true,    "rollout_percentage": 85,    "created_at": "2023-05-30T10:00:00Z",    "created_by": "payment-team",    "description": "3DS2 desteği ile yeni Stripe payment processor",    "kill_switch": false,    "environments": ["production"],    "monitoring": {      "error_threshold": 0.05,      "latency_threshold_ms": 2000    }  }' \  --type "SecureString" \  --description "Kritik ödeme flag'i - SİLMEYİN"
# Memorial Day hafta sonumuzu kurtaran flagaws ssm put-parameter \  --name "/feature-flags/checkout-timeout-extended" \  --value '{    "enabled": true,    "timeout_ms": 30000,    "fallback_enabled": true,    "emergency_override": false,    "last_incident": "2023-05-29-payment-timeout"  }' \  --type "SecureString"
# Kademeli rollout ile feature flag'leraws ssm put-parameter \  --name "/feature-flags/new-checkout-ui" \  --value '{    "enabled": true,    "rollout_percentage": 10,    "target_segments": ["beta-users", "premium"],    "a_b_test": {      "experiment_id": "checkout-ui-v2",      "variant": "treatment"    },    "metrics_to_watch": [      "checkout_conversion_rate",      "checkout_abandonment_rate",      "payment_success_rate"    ]  }' \  --type "SecureString"

Gerçek Production Kullanımı (Gerçekten İşe Yarayan)

Gelirimizi Kurtaran Checkout Component'ı

javascript
// Timeout'tan anında response'lara giden componentimport React from 'react';import { View, Text, Button, Alert } from 'react-native';import { useFeatureFlag } from './hooks/useFeatureFlag';
function CheckoutFlow() {  const {    data: paymentConfig,    error,    isLoading,    isValidating,    mutate,    cacheHits,  } = useFeatureFlag('payment-processor-v2', {    // Memorial Day incident'ından öğrendik    fallbackData: {      enabled: false,  // Güvenli default      processor: 'legacy',      timeout_ms: 15000,    },    staleTime: 2 * 60 * 1000,  // 2 dakika (sık ödemeler fresh data gerektirir)    onSuccess: (data) => {      console.log('Payment config güncellendi:', data);      // Analytics için başarılı flag load'ları takip et      analytics.track('feature_flag_loaded', {        flag: 'payment-processor-v2',        value: data,        cache_hits: cacheHits,      });    },    onError: (error) => {      console.error('Payment flag hatası:', error);      // Payment flag hatalarında alert (gelir için kritik)      crashlytics().recordError(error);    }  });
  // Ödeme sorunları için emergency kill switch  const handleEmergencyFallback = () => {    Alert.alert(      'Emergency Fallback',      'Legacy payment processor'a geçilsin mi?',      [        { text: 'İptal', style: 'cancel' },        {          text: 'Evet',          onPress: () => {            mutate({              ...paymentConfig,              enabled: false,              emergency_override: true            }, false);            analytics.track('emergency_payment_fallback');          }        },      ]    );  };
  // Kritik checkout akışı için asla loading gösterme  const config = paymentConfig || {    enabled: false,    processor: 'legacy',    timeout_ms: 15000,  };
  return (    <View>      <Text>Payment Processor: {config.enabled ? 'v2 (Stripe)' : 'Legacy'}</Text>
      {/* UI'ı bloklamadan sistem durumunu göster */}      {isValidating && (        <Text style={{ color: 'gray', fontSize: 12 }}>          Payment config arka planda yenileniyor...        </Text>      )}
      {error && (        <View style={{ backgroundColor: '#fff3cd', padding: 8 }}>          <Text style={{ color: '#856404' }}>            Cached payment ayarları kullanılıyor (flag servisi kullanılamıyor)          </Text>        </View>      )}
      <Button        title="Emergency Fallback"        onPress={handleEmergencyFallback}        color="red"      />
      {/* Gerçek payment component'ı */}      {config.enabled ? (        <StripePaymentForm          timeout={config.timeout_ms}          onSuccess={() => analytics.track('payment_success', { processor: 'v2' })}          onError={(err) => {            // Payment hatalarında auto-fallback            if (err.code === 'TIMEOUT') {              mutate({ ...config, enabled: false }, false);            }          }}        />      ) : (        <LegacyPaymentForm          onSuccess={() => analytics.track('payment_success', { processor: 'legacy' })}        />      )}
      {/* Development için debug info */}      {__DEV__ && (        <Text style={{ fontSize: 10, color: 'gray' }}>          Cache hit'leri: {cacheHits} | Config: {JSON.stringify(config, null, 2)}        </Text>      )}    </View>  );}

Birden Fazla Flag ile Dashboard (Gerçek Performans Verisi)

javascript
// 50+ feature flag handle eden dashboard component'ıfunction Dashboard() {  const {    flags,    isLoading,    hasErrors,    mutate,    refreshAll,    totalCacheHits,    cacheStats,  } = useFeatureFlags([    'new-checkout-ui',    'payment-processor-v2',    'dark-mode',    'a-b-test-homepage',    'premium-features',    'mobile-push-notifications',    'analytics-enhanced',    'referral-program',    'social-login',    'advanced-search',  ], {    // Refresh interval yok - SWR pattern'ine güven    staleTime: 10 * 60 * 1000,  // 10 dakika    fallbackData: null,  // Her flag'in kendi fallback'ini handle etmesine izin ver  });
  // Flag'ler için dashboard rendering'i asla blokla  // Bu bizim performans analizimizden çıkan key insight'tı
  return (    <View style={{ flex: 1 }}>      {/* Progressive enhancement - flag'ler yüklendiğinde özellikler görünür */}
      {flags['new-checkout-ui'] && (        <NewCheckoutBanner          onDismiss={() => {            // Bu kullanıcı için geçici olarak devre dışı bırak            mutate['new-checkout-ui']({              ...flags['new-checkout-ui'],              user_dismissed: true            }, false);          }}        />      )}
      <ScrollView>        {/* Core özellikler her zaman render olur */}        <ProductList />
        {/* Enhanced özellikler yalnızca flag'ler hazır olduğunda */}        {flags['premium-features'] && (          <PremiumSection            config={flags['premium-features']}            onUpgrade={() => {              analytics.track('premium_upgrade_clicked', {                feature_flag_config: flags['premium-features']              });            }}          />        )}
        {flags['referral-program']?.enabled && (          <ReferralWidget            incentive={flags['referral-program'].incentive_amount}            onShare={() => analytics.track('referral_shared')}          />        )}
        {/* A/B test component */}        {flags['a-b-test-homepage'] && (          <ABTestComponent            variant={flags['a-b-test-homepage'].variant}            experimentId={flags['a-b-test-homepage'].experiment_id}            onConversion={(event) => {              analytics.track('ab_test_conversion', {                experiment_id: flags['a-b-test-homepage'].experiment_id,                variant: flags['a-b-test-homepage'].variant,                event_type: event,              });            }}          />        )}      </ScrollView>
      {/* Development için debug paneli */}      {__DEV__ && (        <View style={{ position: 'absolute', top: 50, right: 10, backgroundColor: 'rgba(0,0,0,0.8)', padding: 10 }}>          <Text style={{ color: 'white', fontSize: 10 }}>Flag Cache İstatistikleri:</Text>          <Text style={{ color: 'white', fontSize: 8 }}>Toplam hit'ler: {totalCacheHits}</Text>          <Text style={{ color: 'white', fontSize: 8 }}>Hatalar: {hasErrors ? 'EVET' : 'HAYIR'}</Text>          <Button            title="Tümünü Yenile"            onPress={refreshAll}            color="orange"          />          {Object.entries(cacheStats).map(([flag, stats]) => (            <Text key={flag} style={{ color: 'gray', fontSize: 8 }}>              {flag}: {stats.hits} hit            </Text>          ))}        </View>      )}    </View>  );}

User Targeting ile Karmaşık A/B Testing

javascript
// Gelir deneyimlerimizi güçlendiren gelişmiş configfunction ABTestWrapper({ children, userId, userSegment }) {  const { data: experimentConfig, mutate, refresh } = useFeatureFlag(    'homepage-conversion-experiment',    {      fallbackData: {        enabled: false,        experiment_id: 'homepage-v1',        variants: {          control: 50,          treatment_a: 25,  // Yeni CTA button          treatment_b: 25,  // Sadeleştirilmiş form        },        targeting: {          min_account_age_days: 0,          allowed_segments: ['free', 'trial', 'premium'],          excluded_user_ids: [],        },        kill_switch: false,      },      staleTime: 30 * 60 * 1000, // Deneyimler için 30 dakika      onSuccess: (data) => {        console.log('A/B test config yüklendi:', data.experiment_id);
        // Deney exposure'ını takip et        analytics.track('experiment_config_loaded', {          experiment_id: data.experiment_id,          user_id: userId,        });      },    }  );
  // Consistent hashing ile kullanıcının variant'ını belirle  const userVariant = useMemo(() => {    if (!experimentConfig?.enabled || experimentConfig.kill_switch) {      return 'control';    }
    // Targeting kriterlerini kontrol et    const targeting = experimentConfig.targeting;    if (!targeting.allowed_segments.includes(userSegment)) {      return 'control';    }
    if (targeting.excluded_user_ids.includes(userId)) {      return 'control';    }
    // Consistent hash tabanlı variant assignment    const hash = require('crypto')      .createHash('md5')      .update(userId + experimentConfig.experiment_id)      .digest('hex');
    const userPercentile = parseInt(hash.substr(0, 4), 16) % 100;
    const variants = experimentConfig.variants;    let cumulativePercentage = 0;
    for (const [variant, percentage] of Object.entries(variants)) {      cumulativePercentage += percentage;      if (userPercentile < cumulativePercentage) {        return variant;      }    }
    return 'control';  // Fallback  }, [experimentConfig, userId, userSegment]);
  // Experiment exposure'ını session başına bir kez takip et  useEffect(() => {    if (experimentConfig?.enabled && userVariant !== 'control') {      analytics.track('experiment_exposed', {        experiment_id: experimentConfig.experiment_id,        variant: userVariant,        user_id: userId,        user_segment: userSegment,      });    }  }, [experimentConfig?.experiment_id, userVariant, userId, userSegment]);
  // Test için manual refresh  const handleRefreshExperiment = () => {    console.log('A/B test config yenileniyor');    refresh();  };
  // Emergency kill switch  const handleKillSwitch = () => {    Alert.alert(      'Kill Switch',      'Bu deneyi tüm kullanıcılar için devre dışı bırak?',      [        { text: 'İptal', style: 'cancel' },        {          text: 'Kill',          style: 'destructive',          onPress: () => {            mutate({              ...experimentConfig,              kill_switch: true,              killed_at: new Date().toISOString(),              killed_by: userId,            }, false);
            analytics.track('experiment_killed', {              experiment_id: experimentConfig.experiment_id,              killed_by: userId,            });          }        },      ]    );  };
  return (    <View>      {/* Variant'a özel içerik render et */}      {React.cloneElement(children, {        variant: userVariant,        experimentId: experimentConfig?.experiment_id,        onConversion: (eventType) => {          analytics.track('conversion', {            experiment_id: experimentConfig?.experiment_id,            variant: userVariant,            event_type: eventType,            user_id: userId,          });        },      })}
      {/* Test için admin kontrolleri */}      {__DEV__ && (        <View style={{ position: 'absolute', bottom: 100, right: 10 }}>          <Button title="Deneyi Yenile" onPress={handleRefreshExperiment} />          <Button title="Kill Switch" onPress={handleKillSwitch} color="red" />          <Text style={{ fontSize: 10 }}>Variant: {userVariant}</Text>        </View>      )}    </View>  );}

Production'da 18 Aydan Performans Dersleri

Gerçekten Önemli Memory Management

javascript
// Memory leak'lerimizi önleyen cache cleanupclass FeatureFlagCache {  constructor() {    // ... mevcut kod    this.maxCacheSize = 200;  // Profiling sonrası artırıldı    this.maxStorageAge = 7 * 24 * 60 * 60 * 1000;  // 7 gün
    // Her 10 dakikada cleanup (memory pressure crash'lerinden öğrendik)    this.cleanupInterval = setInterval(() => {      this.cleanup();    }, 10 * 60 * 1000);
    // Monitoring için memory kullanımını takip et    this.memoryStats = {      cleanupRuns: 0,      entriesDeleted: 0,      lastCleanupTime: Date.now(),    };  }
  cleanup() {    const startTime = Date.now();    const initialSize = this.cache.size;
    // Adım 1: Süresi dolan entry'leri kaldır    const now = Date.now();    for (const [key, value] of this.cache.entries()) {      if (now - value.timestamp > this.maxStorageAge) {        this.cache.delete(key);        console.log(`Süresi dolan cache entry silindi: ${key}`);      }    }
    // Adım 2: Hâlâ limit üzerindeyse LRU cleanup    if (this.cache.size > this.maxCacheSize) {      const entries = Array.from(this.cache.entries());      // Son erişim zamanına göre sırala (LRU)      const sorted = entries.sort((a, b) =>        (a[1].lastAccessed || a[1].timestamp) - (b[1].lastAccessed || b[1].timestamp)      );
      const toDelete = sorted.slice(0, entries.length - this.maxCacheSize);
      toDelete.forEach(([key]) => {        this.cache.delete(key);        console.log(`LRU cache entry silindi: ${key}`);      });    }
    // İstatistikleri güncelle    this.memoryStats.cleanupRuns++;    this.memoryStats.entriesDeleted += initialSize - this.cache.size;    this.memoryStats.lastCleanupTime = Date.now();
    console.log(`Cache cleanup: ${initialSize} -> ${this.cache.size} entry, ${Date.now() - startTime}ms`);
    // Analytics'e memory kullanımını raporla    this.reportMetrics({      cache_size: this.cache.size,      cleanup_duration: Date.now() - startTime,      memory_freed_mb: (initialSize - this.cache.size) * 0.001,  // Rough estimate    });  }
  // LRU için erişimi takip et  getCache(key) {    const cached = this.cache.get(key);    if (cached) {      cached.lastAccessed = Date.now();  // LRU timestamp'ını güncelle      this.stats.hits++;      return cached;    }    this.stats.misses++;    return null;  }}

Request Storm'umuzu Önleyen Request Deduplication

javascript
// Black Friday incident'ı sırasında bizi kurtaran deduplicationclass FeatureFlagCache {  constructor() {    // ... mevcut kod    this.pendingRequests = new Map();    this.requestStats = {      dedupedRequests: 0,      concurrentRequestsPrevented: 0,    };  }
  async fetcher(key) {    // Request zaten in-flight mı kontrol et    if (this.pendingRequests.has(key)) {      console.log(`${key} için concurrent request dedup ediliyor`);      this.requestStats.dedupedRequests++;
      // Mevcut promise'i döndür      return this.pendingRequests.get(key);    }
    // Timeout ve retry logic ile yeni request oluştur    const promise = this.makeRequestWithRetry(key, 3);    this.pendingRequests.set(key, promise);
    try {      const result = await promise;      return result;    } finally {      // Pending request'i her zaman temizle      this.pendingRequests.delete(key);    }  }
  async makeRequestWithRetry(key, maxRetries) {    let lastError;
    for (let attempt = 1; attempt <= maxRetries; attempt++) {      try {        console.log(`${key} fetch ediliyor, deneme ${attempt}/${maxRetries}`);
        const controller = new AbortController();        const timeoutId = setTimeout(() => {          controller.abort();          console.log(`${key} için request timeout`);        }, 8000);  // 8 saniye timeout
        const response = await fetch(          `https://api.yourapp.com/v2/feature-flags/${key}`,          {            signal: controller.signal,            headers: {              'X-Retry-Attempt': attempt.toString(),              'X-Request-ID': `${Date.now()}-${Math.random().toString(36)}`,            },          }        );
        clearTimeout(timeoutId);
        if (!response.ok) {          throw new Error(`HTTP ${response.status}: ${response.statusText}`);        }
        const data = await response.json();
        // Başarı - retry istatistiklerini resetle        if (attempt > 1) {          console.log(`${key} için request ${attempt}. denemede başarılı`);          this.reportMetrics({            successful_retry: key,            attempts_needed: attempt,          });        }
        return data;
      } catch (error) {        lastError = error;        console.error(`${key} için request deneme ${attempt} başarısız:`, error.message);
        // Bazı hatalarda retry yapma        if (error.name === 'AbortError' ||            (error.message && error.message.includes('404'))) {          break;        }
        // Retry'ler arasında exponential backoff        if (attempt < maxRetries) {          const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);          console.log(`${key} ${delay}ms sonra retry yapılacak`);          await new Promise(resolve => setTimeout(resolve, delay));        }      }    }
    // Tüm retry'ler başarısız    this.reportMetrics({      request_failed_after_retries: key,      max_retries: maxRetries,      final_error: lastError.message,    });
    throw lastError;  }}

Gerçek Bug'ları Yakalayan Test Stratejisi

Gerçekten Önemli Unit Test'ler

javascript
import { renderHook, act } from '@testing-library/react-hooks';import { useFeatureFlag } from '../useFeatureFlag';
// AsyncStorage'ı mock'lajest.mock('@react-native-async-storage/async-storage', () => ({  getItem: jest.fn(),  setItem: jest.fn(),}));
// fetch'i mock'laglobal.fetch = jest.fn();
describe('useFeatureFlag', () => {  beforeEach(() => {    fetch.mockClear();    AsyncStorage.getItem.mockClear();    AsyncStorage.setItem.mockClear();  });
  it('should return cached data immediately', async () => {    // Cache kurulumu    flagCache.setCache('test-flag', true);
    const { result } = renderHook(() =>      useFeatureFlag('test-flag')    );
    expect(result.current.data).toBe(true);    expect(result.current.isLoading).toBe(false);  });
  it('should revalidate stale data', async () => {    fetch.mockResolvedValueOnce({      ok: true,      json: () => Promise.resolve(false)    });
    // Stale cache kurulumu (1 dakikadan eski)    const staleTimestamp = Date.now() - 120000;    flagCache.cache.set('test-flag', {      data: true,      timestamp: staleTimestamp,      isValidating: false    });
    const { result, waitForNextUpdate } = renderHook(() =>      useFeatureFlag('test-flag')    );
    // Stale veriyi anında döndürmeli    expect(result.current.data).toBe(true);
    // Revalidation'ı bekle    await waitForNextUpdate();
    expect(result.current.data).toBe(false);    expect(fetch).toHaveBeenCalledWith(      'https://your-api-gateway-url/feature-flags/test-flag'    );  });
  it('should handle optimistic updates', async () => {    const { result } = renderHook(() =>      useFeatureFlag('test-flag', { fallbackData: false })    );
    act(() => {      result.current.mutate(true, false); // Optimistic update    });
    expect(result.current.data).toBe(true);  });});

Integration Test'leri

javascript
import { render, waitFor } from '@testing-library/react-native';import { FeatureFlagProvider } from '../FeatureFlagProvider';import TestComponent from './TestComponent';
describe('Feature Flag Integration', () => {  it('should handle app state changes', async () => {    const { getByText } = render(      <FeatureFlagProvider>        <TestComponent />      </FeatureFlagProvider>    );
    // Uygulamanın background'a ve foreground'a geçişini simüle et    AppState.currentState = 'background';    AppState.currentState = 'active';
    // App state change event'ini emit et    AppState.addEventListener.mock.calls[0][1]('active');
    await waitFor(() => {      expect(fetch).toHaveBeenCalled();    });  });});

Best Practice'ler

1. Flag İsimlendirme Kuralları

javascript
// Good: İyi: Açıklayıcı ve hiyerarşik'checkout.payment-v2.enabled''ui.dark-mode.rollout-percentage''experiment.recommendation-algorithm.variant'
// Bad: Kötü: Belirsiz veya tutarsız'flag1''newThing''test_feature'

2. Error Boundary'ler

javascript
import React from 'react';
class FeatureFlagErrorBoundary extends React.Component {  constructor(props) {    super(props);    this.state = { hasError: false };  }
  static getDerivedStateFromError(error) {    return { hasError: true };  }
  componentDidCatch(error, errorInfo) {    console.error('Feature flag hatası:', error, errorInfo);    // Crash reporting servisine logla    crashlytics().recordError(error);  }
  render() {    if (this.state.hasError) {      return this.props.fallback || this.props.children;    }
    return this.props.children;  }}
// Kullanımfunction App() {  return (    <FeatureFlagErrorBoundary fallback={<LegacyComponent />}>      <FeatureFlagComponent />    </FeatureFlagErrorBoundary>  );}

3. Kademeli Rollout'lar

javascript
function useGradualRollout(flagName, userId, percentage = 0) {  const { data: flag } = useFeatureFlag(flagName);
  const isEnabled = useMemo(() => {    if (!flag?.enabled) return false;
    // Consistent hash tabanlı rollout    const hash = hashString(userId + flagName);    const userPercentile = hash % 100;
    return userPercentile < (flag.rolloutPercentage || percentage);  }, [flag, userId, percentage, flagName]);
  return isEnabled;}
function hashString(str) {  let hash = 0;  for (let i = 0; i < str.length; i++) {    const char = str.charCodeAt(i);    hash = ((hash << 5) - hash) + char;    hash = hash & hash; // 32-bit integer'a çevir  }  return Math.abs(hash);}

Sonuç

SWR-style feature flag sistemimiz birkaç key avantaj sağlıyor:

  • Anında UI güncellemeleri cached veri ile
  • Arka plan senkronizasyonu fresh veri için
  • Offline dayanıklılık persistent storage ile
  • Akıllı revalidation app lifecycle'ına göre
  • Memory verimli otomatik cleanup ile
  • Type-safe tam TypeScript desteğiyle

Bu implementation performans, kullanıcı deneyimi ve developer productivity arasında mükemmel denge kuruyor. Stale-while-revalidate pattern'i React Native uygulamanızın hızlı ve responsive hissetmesini sağlarken feature flag'leri güncel tutuyor.

Sonraki Adımlar

Bu sistemi şunlarla genişletmeyi değerlendirin:

  • Real-time güncellemeler WebSocket ile
  • A/B testing yetenekleri
  • Analytics entegrasyonu flag kullanım takibi için
  • Admin dashboard flag yönetimi için
  • Otomatik rollback error rate'lere göre

Kurduğumuz foundation bu gelişmiş özellikleri core SWR faydalarını koruyarak barındıracak kadar esnek.

İlgili Yazılar