React Native'de Feature Flag: Stale-While-Revalidate Deseni
Senkron feature flag yüklemesi checkout'u neden tıkar ve stale-while-revalidate cache React Native'de flag'leri arka planda tazeleyerek nasıl anında sunar.
Feature flag’ler checkout akışının kritik yolunda durur; bu yüzden onları nasıl yüklediğiniz bir erişilebilirlik kararıdır. Senkron bir API çağrısı her render’a bir ağ gidiş dönüşü ekler. Cold start koşullarında bu gidiş dönüş saniyelere uzar, checkout timeout’a düşer ve kullanıcı sepeti terk eder. Stale-while-revalidate okumayı kritik yoldan çıkarır: cache’deki değer anında döner, taze değer arka planda çekilir. Cache’i varsayılan cevap, ağı ise bir arka plan tazelemesi olarak görün; asla bir kapı olarak değil.
Senkron Flag Yüklemesinin Bedeli#
Temel bir flag implementasyonu her değere ihtiyaç duyulduğunda senkron API çağrısı yapar:
// Senkron flag yüklemesi - timeout riski yaratır
const getFeatureFlag = async (flagName) => {
const response = await fetch(`/api/flags/${flagName}`);
return response.json();
};
// Checkout akışında çağrılır
if (await getFeatureFlag('new-payment-processor')) {
// Yeni sistemle işle
}
Bu kurulumdan dört arıza biçimi doğar:
- Cold start: Lambda cold start’ı ile Parameter Store gidiş dönüşü yanıtı saniyelere taşıyabilir
- Cache yok: her checkout ağ maliyetinin tamamını yeniden öder
- Cascading timeout: yavaş tek bir flag, onu okuyan her ekranı yavaşlatır
- Offline desteği yok: bağlantı koptuğunda uygulamanın elinde hiçbir flag değeri kalmaz
SWR Pattern’inin Avantajları#
“Flag’i yükle, bekle, işe yarar umuduyla” yaklaşımı yerine SWR şunu yapar:
- Cached veriyi anında döndür (eski olsa bile)
- Fresh veriyi arka planda getir (revalidate)
- Cache’i sessizce güncelle yeni veri geldiğinde
Kullanıcı deneyimi değişimi:
- Önce: checkout, render olmadan önce bir ağ çağrısında bloklanır
- Sonra: cache’den anında yanıt, taze veri geldiğinde sessiz güncelleme
- Offline: uygulama son bilinen değerlerle çalışmaya devam eder
Production Mimarisi#
SWR tabanlı sistem dört bileşenden oluşur:
- FeatureFlagCache: AsyncStorage persistence ile in-memory cache
- useFeatureFlag: Arka plan revalidation ile SWR-style hook
- Smart invalidation: App focus/ağ değişikliklerinde otomatik güncellemeler
- AWS backend: Proper caching ile Parameter Store + Lambda
İlk üçü flag okumasını cihazda tutar; böylece hiçbir render ağı beklemez. Dördüncüsü kaynağı ucuz tutar; böylece arka plan revalidation’ı bir Parameter Store faturasına dönüşmez.
Production Implementasyonu#
Dört bileşen tek bir modül olarak birleşir; aşağıda parça parça kuruluyor:
Cache Manager#
// SWR-style cache manager
import { 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;
// 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() {
// iOS arka plan geçişlerinde flag'leri yenile
AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'active' && this.revalidateOnFocus) {
console.log('App focused, revalidating all flags');
this.revalidateAll();
}
});
// Ağ yeniden bağlanmalarında flag'leri yenile
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,
// cache hit sayısını 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
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
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 okuma 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 yazma 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 kaos yaratır
const flagCache = new FeatureFlagCache();
React Hook#
// SWR-style flag hook
export function useFeatureFlag(flagName, options = {}) {
const {
refreshInterval = 0,
revalidateOnMount = true,
fallbackData = null, // Kritik: her zaman fallback sağla
onSuccess,
onError,
// Ek 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);
// cache hit sayısını 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
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]);
// 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
console.log(`Flag ${flagName} unmount edildi. Cache hit'leri: ${cacheHits}`);
};
}, [flagName, fetcher, refreshInterval, revalidateOnMount, staleTime, cacheHits]);
// return objesi
return {
data,
error,
isLoading,
isValidating,
mutate,
// Debug ve optimizasyon için ekstra metadata
cacheHits,
lastUpdated: flagCache.getCache(flagName)?.timestamp,
// Debug helper methodlar
refresh: () => flagCache.revalidate(flagName),
clearCache: () => {
flagCache.cache.delete(flagName);
AsyncStorage.removeItem(flagCache.getCacheKey(flagName));
},
};
}
Batch Hook: Çoklu Flag Yönetimi#
Koddan önce bir kısıt: useFeatureFlag’i döngü içinde çağırmak, yalnızca flagNames her render’da aynı uzunluk ve sırada kaldığı sürece geçerlidir. Modül seviyesinde sabit bir dizi geçirin, satır içinde oluşturulmuş birini asla; aksi halde hook sırası kayar ve React hata fırlatır.
// Birden fazla flag'i verimli handle eder
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,
};
}
AWS Backend Mimarisi#
Parameter Store’u istemciden doğrudan okumak asıl darboğaz. Önüne konan ince bir Lambda ikinci bir önbellek katmanı ekler; tekrar eden okumalar SSM’e hiç ulaşmaz ve istemci cache’i revalidate ederken hızlı bir kaynak bulur:
Production Lambda#
// Önbellek destekli feature flag Lambda
const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm');
const { CloudWatchClient, PutMetricDataCommand } = require('@aws-sdk/client-cloudwatch');
// Connection'ları yeniden kullan - maliyet optimizasyonu
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 önünde in-memory cache
const 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 targeting
async 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 metrikleri
async 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#
# Production 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"
# Timeout toleransı için flag
aws ssm put-parameter \
--name "/feature-flags/checkout-timeout-extended" \
--value '{
"enabled": true,
"timeout_ms": 30000,
"fallback_enabled": true,
"emergency_override": false,
"purpose": "checkout-timeout-tolerance"
}' \
--type "SecureString"
# Kademeli rollout ile feature flag'ler
aws 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"
Production Kullanım Örnekleri#
Checkout Component#
// SWR-style checkout component
import 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', {
// Kritik: fallback her zaman sağla
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 kullanılsın mı?',
[
{ 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#
// Çok sayıda 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
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#
// User targeting ve kill-switch destekli A/B test wrapper
function 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';
}
// Tutarlı hash tabanlı variant ataması (React Native'de node:crypto yok)
const seed = userId + experimentConfig.experiment_id;
let h = 0;
for (let i = 0; i < seed.length; i++) {
h = ((h << 5) - h + seed.charCodeAt(i)) | 0;
}
const userPercentile = Math.abs(h) % 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>
);
}
Performans Optimizasyonları#
Memory Management#
// Cache cleanup: sınırlı boyut + periyodik tahliye bellek baskısını önler
class FeatureFlagCache {
constructor() {
// ... mevcut kod
this.maxCacheSize = 200; // Belleği sabit tutmak için sınırlı
this.maxStorageAge = 7 * 24 * 60 * 60 * 1000; // 7 gün
// Her 10 dakikada cleanup
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 Deduplication#
// Concurrent request deduplication
class 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;
}
}
Test Stratejisi#
Unit Test’ler#
import { renderHook, act } from '@testing-library/react-hooks';
import { useFeatureFlag } from '../useFeatureFlag';
// AsyncStorage'ı mock'la
jest.mock('@react-native-async-storage/async-storage', () => ({
getItem: jest.fn(),
setItem: jest.fn(),
}));
// fetch'i mock'la
global.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://api.yourapp.com/v2/feature-flags/test-flag',
expect.any(Object)
);
});
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#
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ı#
// İyi: açıklayıcı ve hiyerarşik
'checkout.payment-v2.enabled'
'ui.dark-mode.rollout-percentage'
'experiment.recommendation-algorithm.variant'
// Kötü: belirsiz veya tutarsız
'flag1'
'newThing'
'test_feature'
2. Error Boundary’ler#
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ım
function App() {
return (
<FeatureFlagErrorBoundary fallback={<LegacyComponent />}>
<FeatureFlagComponent />
</FeatureFlagErrorBoundary>
);
}
3. Kademeli Rollout’lar#
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ç#
Biraz bayat bir flag değeri, yavaş bir flag değerinden güvenliyse varsayılan seçim stale-while-revalidate olmalı. UI rollout’ları, deney atamaları ve progressive enhancement birkaç dakikalık gecikmeyi kaldırır; karşılığında flag okuması hiçbir render’ı bloklamaz. Bedeli sınırlı bir bayatlıktır. Kullanıcı, bir sonraki revalidation gelene kadar önceki değerle hareket edebilir; yani cache TTL’i, hatalı bir flag’in en kötü durumda ne kadar süre yayında kalacağını belirler.
Bayatlığın kabul edilemez olduğu yerlerde bu varsayılanı değiştirin. Yetkilendirme kontrolleri, ücretli içeriği açan kararlar ve onay anahtarları ya yetkili bir çağrıdan okunmalı ya da cache’ten okunup işlem tamamlanmadan sunucu tarafında yeniden doğrulanmalı. Kill switch’ler de aynı özeni ister: kısa bir TTL veya push kanalıyla eşleştirin, çünkü beş dakikalık cache beş dakikalık geri alma demektir.
Cache oturduktan sonraki faydalı adım bu pencereyi bir push kanalıyla daraltmak; böylece kill switch, bir sonraki focus olayını beklemeden cihazlara ulaşır.
Kaynaklar#
- SWR - React Hooks ile Veri Çekme (yeni sekmede açılır) - Arka planda yeniden doğrulama ile anlık yanıtlar için stale-while-revalidate kalıbını açıklayan resmi SWR dokümantasyonu
- Feature Toggles (aka Feature Flags) - Martin Fowler (yeni sekmede açılır) - Feature toggle kalıpları, kategorileri ve yönetim stratejileri üzerine kapsamlı rehber
- RFC 5861 - Eski İçerik için HTTP Cache-Control Uzantıları (yeni sekmede açılır) - Stale-while-revalidate’i önbellek geçersizleştirme stratejisi olarak tanımlayan IETF spesifikasyonu
- AWS Systems Manager Parameter Store (yeni sekmede açılır) - IAM erişim kontrolüyle yapılandırma değerlerini ve feature flag’leri saklamak için resmi AWS dokümantasyonu
- React Native Performans Genel Bakış (yeni sekmede açılır) - Senkron flag yüklemesinin ek yükü ile ilgili JavaScript thread performansı üzerine resmi dokümantasyon
- SWR - Otomatik Yeniden Doğrulama (yeni sekmede açılır) - Odaklanma, ağ kurtarma ve aralık tabanlı yenileme dahil yeniden doğrulama tetikleyicileri üzerine SWR dokümantasyonu
İlgili yazılar
Sunucu güdümlü arayüz, sunucu tarafı kompozisyonun mobil karşılığıdır. Zor kısmı JSON render etmek değil, eski sürümlerde ayakta kalan sürümlenmiş bileşen sözleşmesidir.
mobile · react-native · architecture
Async backend'le çalışan tasarımcılar için pragmatik rehber: üç etkileşim şekli, hangisi ne zaman, ve karşı durmanız gereken dört anti-pattern.
event-driven · state-management · design-patterns +2
Headless CMS çözümlerinin (Strapi, Contentful, Kontent, Storyblok) pratik karşılaştırması: Cloudinary ile görsel yönetimi ve framework entegrasyonu.
typescript · nextjs · react-native +3
React Native ve WebView'lar ile mobil micro frontend'ler oluşturma. Çok kanallı uygulamalar için production stratejileri ve performans optimizasyonu.
expo · performance · re-pack +4
React Native ve Expo WebView'lar ile mobil micro frontend mimarisi oluşturma. Gerçek dünya örnekleri ve en iyi uygulamalar.
expo · mobile · re-pack +2