2025-09-04
React Native Uygulamalarda OpenTelemetry ve Firebase ile Gözlemlenebilirlik
React Native uygulamalarında OpenTelemetry ve Firebase kullanarak kapsamlı gözlemlenebilirlik kurma. Tracing, metrics ve logging en iyi uygulamaları.
Birçok React Native ekibi production görünürlük sorunları yaşar. Yerel ortamda çoğaltılamayan çökmeler, rastgele performans sorunları ve destekleyici veri olmadan kullanıcı şikayetleri yaygın zorluklardır. Bu rehber, production sorun giderme ve optimizasyon için gereken içgörüleri sağlayan kapsamlı gözlemlenebilirlik implementasyonunu kapsar.
Zorluk: Mobile Gözlemlenebilirlik Gereksinimleri
Production mobile uygulamaları, web uygulamalarından farklı benzersiz monitoring zorluklarına sahiptir. Sessiz hatalar, cihaza özel sorunlar ve network değişkenliği, geleneksel loglama yaklaşımlarının ele alamayacağı kör noktalar yaratır.
Mobile gözlemlenebilirlik için temel gereksinimler:
- Kullanıcı etkileşimleri ve sistem davranışlarına kapsamlı görünürlük
- Debugging için cihaz ve OS-spesifik bağlam
- Mobile kısıtlamaları hesaba katan performans izleme
- Offline-capable telemetri toplama
Bu rehber, bu zorlukları ele alan bir monitoring sistemi kurma sürecini gösterir. OpenTelemetry ve Firebase kombinasyonu bu gereksinimlerin hepsini karşılayan esnek bir çözüm sunar – vendor bağımsızlığı sayesinde gerektiğinde backend değiştirebilirsiniz.
React Native için Neden OpenTelemetry
OpenTelemetry, React Native gözlemlenebilirliği için çeşitli avantajlar sağlar:
Alternatif Çözümler Karşılaştırması
Firebase Performance Monitoring (2 ay): Kolay kurulum, ücretsiz tier; sınırlı özelleştirme, dağıtık tracing yok.
Datadog RUM (3 ay): Zengin dashboard’lar, mükemmel alerting; pahalı, React Native desteği buggy.
New Relic Mobile (1 ay): Yüksek trafik sırasında crash; zayıf React Native dokümanları.
Sentry Performance (2 hafta): Kritik mobile-spesifik özellikler eksikti.
OpenTelemetry tüm bu sorunları çözdü:
- Vendor bağımsızlığı: Kod değişikliği olmadan monitoring provider değiştirme
- Standardize veri: Trace’ler, metrikler, loglar için aynı format
- Zengin ekosistem: Her şeyle çalışıyor
- Gelecek garantisi: CNCF tarafından desteklenen endüstri standardı
En önemlisi: Production’da gerçekten çalıştı.
Günde 2M+ Event’i Handle Eden Mimari
Günlük 2 milyondan fazla telemetry event işleyen production kurulumumuz:
Production’da Gerçekten İşe Yarayan Kurulum
Sürekli iterasyondan sonra, production-ready implementasyon:
Temel OpenTelemetry Kurulumu
// telemetry/provider.ts - Günde 2M event handle eden temel
import { NodeSDK } from '@opentelemetry/sdk-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
interface TelemetryConfig {
environment: 'development' | 'staging' | 'production';
enabledExporters: string[];
samplingRate: number;
maxBatchSize: number;
exportInterval: number;
}
class ProductionTelemetryProvider {
private sdk: NodeSDK | null = null;
private isInitialized = false;
async initialize(config: TelemetryConfig) {
if (this.isInitialized) {
console.warn('Telemetry already initialized');
return;
}
try {
const deviceInfo = await this.getDeviceInfo();
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'my-react-native-app',
[SemanticResourceAttributes.SERVICE_VERSION]: deviceInfo.appVersion,
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: config.environment,
// Mobile-spesifik attribute'lar debugging zamanı kazandırdı
'mobile.platform': Platform.OS,
'mobile.platform.version': deviceInfo.systemVersion,
'device.model': deviceInfo.deviceId,
'device.manufacturer': deviceInfo.brand,
'app.build': deviceInfo.buildNumber,
'app.bundle_id': deviceInfo.bundleId,
// Network bilgisi connectivity sorunlarını debug etmede yardımcı
'network.carrier': deviceInfo.carrier,
'device.memory': deviceInfo.totalMemory,
});
// Redundancy için birden fazla exporter - production outage'larından öğrendik
const exporters = this.createExporters(config);
this.sdk = new NodeSDK({
resource,
spanProcessors: exporters.spanProcessors,
metricReader: new PeriodicExportingMetricReader({
exporter: exporters.metricExporter,
exportIntervalMillis: config.exportInterval,
}),
// Black Friday trafiğinde hayatta kalan sampling stratejisi
sampler: this.createAdaptiveSampler(config.samplingRate),
instrumentations: this.getInstrumentations(),
});
await this.sdk.start();
this.isInitialized = true;
console.log('Production telemetry initialized', {
environment: config.environment,
exporters: config.enabledExporters,
samplingRate: config.samplingRate,
});
} catch (error) {
console.error('Failed to initialize telemetry:', error);
// Telemetry başarısız olursa uygulamayı crash etme
}
}
private async getDeviceInfo() {
// Daha hızlı startup için tüm device bilgilerini paralel topla
const [
appVersion,
buildNumber,
bundleId,
deviceId,
brand,
systemVersion,
carrier,
totalMemory,
] = await Promise.all([
DeviceInfo.getVersion(),
DeviceInfo.getBuildNumber(),
DeviceInfo.getBundleId(),
DeviceInfo.getUniqueId(),
DeviceInfo.getBrand(),
DeviceInfo.getSystemVersion(),
DeviceInfo.getCarrier().catch(() => 'unknown'),
DeviceInfo.getTotalMemory().catch(() => 0),
]);
return {
appVersion,
buildNumber,
bundleId,
deviceId,
brand,
systemVersion,
carrier,
totalMemory,
};
}
private createExporters(config: TelemetryConfig) {
const spanProcessors: any[] = [];
let metricExporter: any = null;
// Birincil exporter - zengin analytics için Datadog
if (config.enabledExporters.includes('datadog')) {
const datadogExporter = new DatadogExporter({
apiKey: process.env.DATADOG_API_KEY!,
service: 'mobile-app',
env: config.environment,
});
spanProcessors.push(new BatchSpanProcessor(datadogExporter, {
maxExportBatchSize: config.maxBatchSize,
scheduledDelayMillis: config.exportInterval,
// Memory buildup'ı önlemek için agresif timeout
exportTimeoutMillis: 10000,
}));
metricExporter = datadogExporter;
}
// İkincil exporter - temel monitoring için Firebase
if (config.enabledExporters.includes('firebase')) {
spanProcessors.push(new BatchSpanProcessor(new FirebaseExporter(), {
maxExportBatchSize: 50, // Firebase için daha küçük batch'ler
scheduledDelayMillis: 30000, // Free tier için daha az sıklık
}));
}
return { spanProcessors, metricExporter };
}
private createAdaptiveSampler(baseRate: number) {
// Stress altında sampling'i azaltan özel sampler
return {
shouldSample: (context: any, traceId: string, spanName: string) => {
// Error'ları her zaman sample'la
if (spanName.includes('error') || spanName.includes('crash')) {
return { decision: 1 }; // RECORD_AND_SAMPLE
}
// Kritik kullanıcı akışlarını daha yüksek oranda sample'la
if (spanName.includes('payment') || spanName.includes('login')) {
return { decision: Math.random() < (baseRate * 2) ? 1 : 0 };
}
// Yüksek frekanslı event'ler için azaltılmış sampling
if (spanName.includes('scroll') || spanName.includes('animation')) {
return { decision: Math.random() < (baseRate * 0.1) ? 1 : 0 };
}
return { decision: Math.random() < baseRate ? 1 : 0 };
},
};
}
async shutdown() {
if (this.sdk && this.isInitialized) {
await this.sdk.shutdown();
this.isInitialized = false;
}
}
}
export const telemetryProvider = new ProductionTelemetryProvider();
React Native Performans Monitoring
Bu, ödeme akışı bug’ımızı yakalayan sınıf:
// telemetry/performance-monitor.ts - 50K dolar kurtaran sınıf
import { trace, metrics, context } from '@opentelemetry/api';
import perf from '@react-native-firebase/perf';
import { AppState, AppStateStatus } from 'react-native';
class ProductionPerformanceMonitor {
private tracer = trace.getTracer('app-performance', '1.0.0');
private meter = metrics.getMeter('app-metrics', '1.0.0');
// Production'da gerçekten önemli olan metrikler
private screenLoadTime = this.meter.createHistogram('screen_load_duration', {
description: 'Time to load screens',
unit: 'ms',
});
private apiCallDuration = this.meter.createHistogram('api_call_duration', {
description: 'API response times by endpoint',
unit: 'ms',
});
private userJourneyCompletion = this.meter.createCounter('user_journey_completion', {
description: 'Completed user journeys',
});
private criticalErrors = this.meter.createCounter('critical_errors', {
description: 'Errors that affect core functionality',
});
constructor() {
this.setupAppStateTracking();
}
// Gerçek business etkisi olan screen load'ları track et
async measureScreenLoad<T>(
screenName: string,
loadFunction: () => Promise<T>,
isBusinessCritical = false
): Promise<T> {
const span = this.tracer.startSpan(`screen_load_${screenName}`);
const startTime = Date.now();
// Ücretsiz monitoring için Firebase trace
let firebaseTrace: any = null;
try {
firebaseTrace = perf().newTrace(`screen_${screenName}`);
firebaseTrace.start();
} catch (error) {
// Firebase başarısız olabilir, uygulamayı crash etme
console.warn('Firebase trace failed:', error);
}
span.setAttributes({
'screen.name': screenName,
'screen.business_critical': isBusinessCritical,
'screen.timestamp': startTime,
});
try {
const result = await loadFunction();
const duration = Date.now() - startTime;
// Metrikleri kaydet
this.screenLoadTime.record(duration, {
screen: screenName,
success: 'true',
critical: isBusinessCritical.toString(),
});
// Yavaş kritik screen'lerde alert
if (isBusinessCritical && duration > 3000) {
this.criticalErrors.add(1, {
type: 'slow_critical_screen',
screen: screenName,
duration: duration.toString(),
});
}
span.setAttributes({
'screen.load_duration': duration,
'screen.success': true,
});
span.setStatus({ code: 1 }); // OK
return result;
} catch (error) {
const duration = Date.now() - startTime;
this.screenLoadTime.record(duration, {
screen: screenName,
success: 'false',
error: error.name,
});
// Screen load başarısızlıklarında her zaman alert
this.criticalErrors.add(1, {
type: 'screen_load_failure',
screen: screenName,
error: error.message,
});
span.recordException(error);
span.setStatus({ code: 2, message: error.message });
firebaseTrace?.putAttribute('error', 'true');
throw error;
} finally {
span.end();
firebaseTrace?.stop();
}
}
// Ödeme bug'ımızı yakalayan API monitoring
async instrumentApiCall<T>(
endpoint: string,
method: string,
apiCall: () => Promise<T>,
businessContext?: {
userId?: string;
feature?: string;
monetaryValue?: number;
}
): Promise<T> {
const span = this.tracer.startSpan(`api_${method.toLowerCase()}_${this.sanitizeEndpoint(endpoint)}`);
const startTime = Date.now();
span.setAttributes({
'http.method': method,
'http.url': endpoint,
'api.business_context': JSON.stringify(businessContext || {}),
'api.timestamp': startTime,
});
try {
const result = await apiCall();
const duration = Date.now() - startTime;
this.apiCallDuration.record(duration, {
endpoint: this.sanitizeEndpoint(endpoint),
method,
status: 'success',
business_critical: businessContext?.monetaryValue ? 'true' : 'false',
});
// Yavaş ödeme API'larında alert
if (businessContext?.monetaryValue && duration > 5000) {
this.criticalErrors.add(1, {
type: 'slow_payment_api',
endpoint: this.sanitizeEndpoint(endpoint),
duration: duration.toString(),
value: businessContext.monetaryValue.toString(),
});
}
span.setAttributes({
'http.status_code': 200,
'http.response_time': duration,
'api.success': true,
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
this.apiCallDuration.record(duration, {
endpoint: this.sanitizeEndpoint(endpoint),
method,
status: 'error',
error_type: error.name,
});
// Ödeme API başarısızlıklarında her zaman alert
if (businessContext?.monetaryValue) {
this.criticalErrors.add(1, {
type: 'payment_api_failure',
endpoint: this.sanitizeEndpoint(endpoint),
error: error.message,
user_id: businessContext.userId || 'unknown',
value: businessContext.monetaryValue.toString(),
});
}
span.recordException(error);
span.setAttributes({
'http.status_code': error.status || 500,
'error.name': error.name,
'error.message': error.message,
'api.success': false,
});
throw error;
} finally {
span.end();
}
}
// Bireysel aksiyonları değil, tam kullanıcı yolculuklarını track et
startUserJourney(journeyName: string, userId?: string): string {
const journeyId = `${journeyName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const span = this.tracer.startSpan(`user_journey_${journeyName}`, {
attributes: {
'journey.name': journeyName,
'journey.id': journeyId,
'user.id': userId || 'anonymous',
'journey.start_time': Date.now(),
},
});
// Sonraki adımlar için context'te sakla
context.with(trace.setSpan(context.active(), span), () => {
// Context artık subsequent operasyonlar için mevcut
});
return journeyId;
}
completeUserJourney(journeyId: string, success: boolean, metadata?: Record<string, any>) {
const activeSpan = trace.getActiveSpan();
if (activeSpan) {
activeSpan.setAttributes({
'journey.completed': success,
'journey.end_time': Date.now(),
...metadata,
});
if (success) {
this.userJourneyCompletion.add(1, {
journey: activeSpan.attributes['journey.name'] as string || 'unknown',
success: 'true',
});
} else {
this.criticalErrors.add(1, {
type: 'journey_failure',
journey: activeSpan.attributes['journey.name'] as string || 'unknown',
step: metadata?.failedStep || 'unknown',
});
}
activeSpan.setStatus({
code: success ? 1 : 2,
message: success ? 'Journey completed' : 'Journey failed',
});
activeSpan.end();
}
}
private sanitizeEndpoint(endpoint: string): string {
// Metrikler için endpoint'lerden hassas verileri kaldır
return endpoint
.replace(/\/\d+/g, '/:id')
.replace(/[?&]token=[^&]*/g, '?token=***')
.replace(/[?&]api_key=[^&]*/g, '?api_key=***');
}
private setupAppStateTracking() {
let backgroundTime = 0;
AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
if (nextAppState === 'background') {
backgroundTime = Date.now();
// Background'a girmeden önce telemetry'yi force flush
this.flushTelemetry();
} else if (nextAppState === 'active' && backgroundTime > 0) {
const backgroundDuration = Date.now() - backgroundTime;
// App resume track et
const resumeSpan = this.tracer.startSpan('app_resume');
resumeSpan.setAttributes({
'app.background_duration': backgroundDuration,
'app.resume_time': Date.now(),
});
resumeSpan.end();
backgroundTime = 0;
}
});
}
private async flushTelemetry() {
try {
// Bekleyen telemetry verilerini force export et
await telemetryProvider.sdk?.getTracerProvider()?.forceFlush(5000);
} catch (error) {
console.warn('Failed to flush telemetry:', error);
}
}
}
export const performanceMonitor = new ProductionPerformanceMonitor();
Gerçekten Yardımcı Olan Navigation Tracking
Standart navigation tracking işe yaramaz. Bu, gerçekten önemli olanı track eder:
// telemetry/navigation-instrumentation.ts - Önemli olan navigation tracking
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native';
import { trace, metrics } from '@opentelemetry/api';
import React, { useRef, useCallback } from 'react';
const tracer = trace.getTracer('navigation', '1.0.0');
const meter = metrics.getMeter('navigation-metrics', '1.0.0');
// Kullanıcı deneyimini optimize etmeye yardımcı olan metrikler
const screenTransitionTime = meter.createHistogram('screen_transition_duration', {
description: 'Time between screen transitions',
unit: 'ms',
});
const navigationDropoff = meter.createCounter('navigation_dropoff', {
description: 'Users who drop off at specific screens',
});
const deepLinkUsage = meter.createCounter('deep_link_usage', {
description: 'Deep link navigation usage',
});
interface NavigationEvent {
from: string;
to: string;
params?: any;
timestamp: number;
userId?: string;
}
class NavigationTelemetry {
private navigationHistory: NavigationEvent[] = [];
private maxHistorySize = 50;
trackNavigation(event: NavigationEvent) {
// History'e ekle
this.navigationHistory.push(event);
if (this.navigationHistory.length > this.maxHistorySize) {
this.navigationHistory.shift();
}
// Navigation için span oluştur
const span = tracer.startSpan('screen_navigation');
span.setAttributes({
'navigation.from': event.from,
'navigation.to': event.to,
'navigation.params': JSON.stringify(event.params || {}),
'navigation.timestamp': event.timestamp,
'user.id': event.userId || 'anonymous',
});
// Metrikleri kaydet
if (this.navigationHistory.length > 1) {
const previousEvent = this.navigationHistory[this.navigationHistory.length - 2];
const transitionTime = event.timestamp - previousEvent.timestamp;
screenTransitionTime.record(transitionTime, {
from: event.from,
to: event.to,
});
// Hızlı çıkışları track et (kullanıcı kafası karışması göstergesi)
if (transitionTime < 2000) {
navigationDropoff.add(1, {
screen: event.from,
quick_exit: 'true',
time_spent: transitionTime.toString(),
});
}
}
// Deep link kullanımını track et
if (event.params && Object.keys(event.params).length > 0) {
deepLinkUsage.add(1, {
screen: event.to,
has_params: 'true',
});
}
span.end();
}
getNavigationPath(): string[] {
return this.navigationHistory.map(event => event.to);
}
analyzeFunnelDropoff(): Record<string, number> {
const dropoffRates: Record<string, number> = {};
for (let i = 0; i < this.navigationHistory.length - 1; i++) {
const current = this.navigationHistory[i];
const next = this.navigationHistory[i + 1];
const timeSpent = next.timestamp - current.timestamp;
if (timeSpent < 5000) { // 5 saniyeden az = potansiyel kafası karışması
dropoffRates[current.to] = (dropoffRates[current.to] || 0) + 1;
}
}
return dropoffRates;
}
}
const navigationTelemetry = new NavigationTelemetry();
export function createTelemetryNavigationContainer() {
return React.forwardRef<NavigationContainerRef<any>, any>((props, ref) => {
const navigationRef = useRef<NavigationContainerRef<any>>(null);
const routeNameRef = useRef<string>();
const navigationStartTime = useRef<number>();
const onReady = useCallback(() => {
const initialRoute = navigationRef.current?.getCurrentRoute();
routeNameRef.current = initialRoute?.name;
if (initialRoute?.name) {
navigationTelemetry.trackNavigation({
from: 'app_start',
to: initialRoute.name,
params: initialRoute.params,
timestamp: Date.now(),
});
}
}, []);
const onStateChange = useCallback(() => {
const previousRouteName = routeNameRef.current;
const currentRoute = navigationRef.current?.getCurrentRoute();
const currentRouteName = currentRoute?.name;
if (previousRouteName !== currentRouteName && currentRouteName) {
const now = Date.now();
navigationTelemetry.trackNavigation({
from: previousRouteName || 'unknown',
to: currentRouteName,
params: currentRoute.params,
timestamp: now,
});
routeNameRef.current = currentRouteName;
}
}, []);
return (
<NavigationContainer
ref={ref || navigationRef}
onReady={onReady}
onStateChange={onStateChange}
{...props}
/>
);
});
}
export { navigationTelemetry };
Gerçekten Sorunları Yakalayan Error Tracking
Standart error tracking ihtiyacınız olan context’i kaçırır. Bu, bug’ları düzeltmek için gerekenleri yakalar:
// telemetry/error-tracking.ts - Debugging'e yardımcı error tracking
import { trace, context } from '@opentelemetry/api';
import crashlytics from '@react-native-firebase/crashlytics';
interface ErrorContext {
userId?: string;
screenName?: string;
userJourney?: string[];
networkState?: string;
memoryUsage?: number;
batteryLevel?: number;
businessContext?: {
feature?: string;
monetaryValue?: number;
customerTier?: string;
};
}
class ProductionErrorTracker {
private tracer = trace.getTracer('error-tracking', '1.0.0');
private errorCount = 0;
private recentErrors: Array<{ error: Error; context?: ErrorContext; timestamp: number }> = [];
captureError(error: Error, errorContext?: ErrorContext) {
const timestamp = Date.now();
this.errorCount++;
// Pattern analizi için son error'ları sakla
this.recentErrors.push({ error, context: errorContext, timestamp });
if (this.recentErrors.length > 100) {
this.recentErrors.shift();
}
// Kapsamlı error span oluştur
const span = this.tracer.startSpan('error_occurred');
span.setAttributes({
'error.type': error.name,
'error.message': error.message,
'error.stack': this.sanitizeStack(error.stack || ''),
'error.timestamp': timestamp,
'error.sequence_number': this.errorCount,
// Device context
'device.memory_usage': errorContext?.memoryUsage || 0,
'device.battery_level': errorContext?.batteryLevel || 1,
'device.network_state': errorContext?.networkState || 'unknown',
// User context
'user.id': errorContext?.userId || 'anonymous',
'user.screen': errorContext?.screenName || 'unknown',
'user.journey': JSON.stringify(errorContext?.userJourney || []),
// Business context
'business.feature': errorContext?.businessContext?.feature || 'unknown',
'business.monetary_value': errorContext?.businessContext?.monetaryValue || 0,
'business.customer_tier': errorContext?.businessContext?.customerTier || 'unknown',
});
// Gelişmiş Firebase Crashlytics logging
try {
if (errorContext?.userId) {
crashlytics().setUserId(errorContext.userId);
}
// Daha iyi filtering için custom attribute'lar ayarla
crashlytics().setAttributes({
screen_name: errorContext?.screenName || 'unknown',
network_state: errorContext?.networkState || 'unknown',
business_feature: errorContext?.businessContext?.feature || 'unknown',
customer_tier: errorContext?.businessContext?.customerTier || 'unknown',
error_sequence: this.errorCount.toString(),
});
// User journey'den breadcrumb'lar ekle
if (errorContext?.userJourney) {
errorContext.userJourney.forEach((step, index) => {
crashlytics().log(`Journey step ${index + 1}: ${step}`);
});
}
crashlytics().recordError(error);
} catch (crashlyticsError) {
console.warn('Crashlytics logging failed:', crashlyticsError);
}
// Pattern detection
this.detectErrorPatterns();
// Mevcut span context varsa ekle
const activeSpan = trace.getActiveSpan();
if (activeSpan) {
activeSpan.recordException(error);
activeSpan.setStatus({
code: 2, // ERROR
message: error.message,
});
}
span.end();
// Anında debugging için log
console.error('Production error captured:', {
error: error.message,
context: errorContext,
sequence: this.errorCount,
});
}
// Sistemik sorunları gösteren error pattern'leri detect et
private detectErrorPatterns() {
const recentWindow = Date.now() - 5 * 60 * 1000; // Son 5 dakika
const recentErrors = this.recentErrors.filter(e => e.timestamp > recentWindow);
if (recentErrors.length >= 5) {
// Error storm kontrolü
const errorTypes = new Map<string, number>();
recentErrors.forEach(({ error }) => {
errorTypes.set(error.name, (errorTypes.get(error.name) || 0) + 1);
});
errorTypes.forEach((count, errorType) => {
if (count >= 3) {
this.reportErrorPattern('error_storm', {
error_type: errorType,
count: count.toString(),
time_window: '5_minutes',
});
}
});
}
// Kullanıcı-spesifik sorunları kontrol et
const userErrors = new Map<string, number>();
recentErrors.forEach(({ context }) => {
if (context?.userId) {
userErrors.set(context.userId, (userErrors.get(context.userId) || 0) + 1);
}
});
userErrors.forEach((count, userId) => {
if (count >= 3) {
this.reportErrorPattern('user_error_cluster', {
user_id: userId,
count: count.toString(),
});
}
});
}
private reportErrorPattern(patternType: string, attributes: Record<string, string>) {
const span = this.tracer.startSpan(`error_pattern_${patternType}`);
span.setAttributes({
'pattern.type': patternType,
'pattern.timestamp': Date.now(),
...attributes,
});
span.end();
console.warn(`Error pattern detected: ${patternType}`, attributes);
}
private sanitizeStack(stack: string): string {
// Stack trace'lerden hassas bilgileri kaldır
return stack
.replace(/token=[^&\s]*/g, 'token=***')
.replace(/apikey=[^&\s]*/g, 'apikey=***')
.replace(/password=[^&\s]*/g, 'password=***');
}
// Production'ı kurtaran global error handler'lar
setupGlobalErrorHandling() {
// React Native JS error'ları
const originalHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler((error, isFatal) => {
this.captureError(error, {
businessContext: { feature: 'global_js_error' },
});
// Orijinal handler'ın çalışmasını engelleme
originalHandler(error, isFatal);
});
// Promise rejection'ları
const originalRejectionHandler = require('react-native/Libraries/Core/ExceptionsManager').installConsoleErrorReporter;
// Handle edilmemiş promise rejection'ları
global.addEventListener?.('unhandledrejection', (event: any) => {
this.captureError(
new Error(`Unhandled Promise Rejection: ${event.reason}`),
{
businessContext: { feature: 'unhandled_promise' },
}
);
});
console.log('Global error handlers installed');
}
// Business-spesifik error tracking
trackBusinessError(
errorType: 'payment_failure' | 'login_failure' | 'api_timeout' | 'feature_unavailable',
error: Error,
businessContext: {
userId?: string;
monetaryValue?: number;
customerTier?: string;
feature: string;
}
) {
this.captureError(error, {
businessContext,
screenName: 'business_operation',
});
// Yüksek değerli error'lar için anında alert'ler
if (businessContext.monetaryValue && businessContext.monetaryValue > 100) {
console.error('HIGH VALUE ERROR:', {
type: errorType,
value: businessContext.monetaryValue,
customer: businessContext.customerTier,
user: businessContext.userId,
});
}
}
}
export const errorTracker = new ProductionErrorTracker();
// Gerçekten yardımcı olan error boundary
export class TelemetryErrorBoundary extends React.Component<
{
children: React.ReactNode;
fallback?: React.ComponentType<{ error: Error; retry: () => void }>;
context?: Partial<ErrorContext>;
},
{ hasError: boolean; error?: Error }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
errorTracker.captureError(error, {
...this.props.context,
businessContext: {
feature: 'react_error_boundary',
},
});
}
render() {
if (this.state.hasError && this.state.error) {
if (this.props.fallback) {
return React.createElement(this.props.fallback, {
error: this.state.error,
retry: () => this.setState({ hasError: false, error: undefined })
});
}
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Bir şeyler yanlış gitti. Lütfen uygulamayı yeniden başlatın.</Text>
</View>
);
}
return this.props.children;
}
}
Çalışan Firebase Entegrasyonu
Firebase Performance Monitoring başlamak için harika, ama dikkatli entegrasyon gerektirir:
// telemetry/firebase-integration.ts - Çalışan Firebase entegrasyonu
import perf from '@react-native-firebase/perf';
import { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base';
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
export class ProductionFirebaseExporter implements SpanExporter {
private activeTraces = new Map<string, any>();
private maxConcurrentTraces = 50; // Firebase'in limitleri var
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
try {
// Firebase'i boğmamak için span'ları chunk'larda işle
const chunks = this.chunkArray(spans, 10);
chunks.forEach((chunk, index) => {
setTimeout(() => {
chunk.forEach(span => this.processSpan(span));
}, index * 100); // İşlemeyi basamakla
});
resultCallback({ code: ExportResultCode.SUCCESS });
} catch (error) {
console.error('Firebase export error:', error);
resultCallback({ code: ExportResultCode.FAILED });
}
}
private async processSpan(span: ReadableSpan) {
const { name, duration, attributes, status } = span;
// Firebase'in iyi handle etmediği span'ları atla
if (this.shouldSkipSpan(name, attributes)) {
return;
}
// Firebase için trace adını temizle
const traceName = this.cleanTraceName(name);
// Firebase limitlerini aşmamak için concurrent trace'leri yönet
if (this.activeTraces.size >= this.maxConcurrentTraces) {
console.warn('Too many active Firebase traces, skipping:', traceName);
return;
}
try {
const trace = perf().newTrace(traceName);
this.activeTraces.set(traceName, trace);
// Attribute'lar ekle (Firebase'in bunlarda da limitleri var)
this.addSafeAttributes(trace, attributes);
// Business metrikleri ekle
this.addBusinessMetrics(trace, attributes);
// Trace timing'ini simüle et
trace.start();
setTimeout(() => {
try {
if (status?.code === 2) { // ERROR
trace.putAttribute('error', 'true');
trace.putMetric('error_count', 1);
}
trace.stop();
this.activeTraces.delete(traceName);
} catch (stopError) {
console.warn('Firebase trace stop failed:', stopError);
}
}, Math.min(duration / 1000000, 60000)); // Max 60s trace
} catch (error) {
console.warn('Firebase trace creation failed:', error);
this.activeTraces.delete(traceName);
}
}
private shouldSkipSpan(name: string, attributes: any): boolean {
// Yüksek frekanslı, düşük değerli span'ları atla
if (name.includes('scroll') || name.includes('animation')) {
return true;
}
// Internal telemetry span'larını atla
if (name.includes('telemetry') || name.includes('metric')) {
return true;
}
// Duration'ı olmayan span'ları atla
if (!attributes['duration'] && !attributes['http.response_time']) {
return true;
}
return false;
}
private cleanTraceName(name: string): string {
// Firebase'in sıkı naming gereklilikleri var
return name
.replace(/[^a-zA-Z0-9_]/g, '_')
.substring(0, 100) // Firebase limiti
.toLowerCase();
}
private addSafeAttributes(trace: any, attributes: any) {
const safeAttributes: Record<string, string> = {};
let attributeCount = 0;
const maxAttributes = 5; // Firebase free tier limiti
// Business-ilişkili attribute'lara öncelik ver
const priorities = [
'user.id',
'screen.name',
'http.status_code',
'business.feature',
'error.type',
];
priorities.forEach(key => {
if (attributes[key] && attributeCount < maxAttributes) {
safeAttributes[key.replace('.', '_')] = String(attributes[key]).substring(0, 100);
attributeCount++;
}
});
// Limite kadar kalan attribute'ları ekle
Object.entries(attributes).forEach(([key, value]) => {
if (!priorities.includes(key) && attributeCount < maxAttributes) {
const safeKey = key.replace(/[^a-zA-Z0-9_]/g, '_');
safeAttributes[safeKey] = String(value).substring(0, 100);
attributeCount++;
}
});
// Trace'e attribute'ları set et
Object.entries(safeAttributes).forEach(([key, value]) => {
try {
trace.putAttribute(key, value);
} catch (error) {
console.warn(`Failed to set Firebase attribute ${key}:`, error);
}
});
}
private addBusinessMetrics(trace: any, attributes: any) {
// Business monitoring için önemli olan metrikleri ekle
try {
if (attributes['http.status_code']) {
trace.putMetric('http_status', Number(attributes['http.status_code']));
}
if (attributes['api.response_time']) {
trace.putMetric('response_time_ms', Number(attributes['api.response_time']));
}
if (attributes['business.monetary_value']) {
trace.putMetric('monetary_value', Number(attributes['business.monetary_value']));
}
if (attributes['screen.load_duration']) {
trace.putMetric('load_time_ms', Number(attributes['screen.load_duration']));
}
} catch (error) {
console.warn('Failed to add Firebase metrics:', error);
}
}
private chunkArray<T>(array: T[], chunkSize: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
async shutdown(): Promise<void> {
// Kalan trace'leri temizle
this.activeTraces.forEach(trace => {
try {
trace.stop();
} catch (error) {
console.warn('Error stopping Firebase trace during shutdown:', error);
}
});
this.activeTraces.clear();
}
}
Gerçekten Yardımcı Olan Kullanım Pattern’leri
Telemetry sistemini gerçek uygulama kodunda nasıl kullandığım:
Screen Component Tracking
// Gerçek screen component'ta
import React, { useEffect, useState } from 'react';
import { performanceMonitor } from '../telemetry/performance-monitor';
import { errorTracker } from '../telemetry/error-tracking';
export function PaymentScreen({ route }: any) {
const [loading, setLoading] = useState(true);
const [paymentData, setPaymentData] = useState(null);
useEffect(() => {
loadPaymentScreen();
}, []);
const loadPaymentScreen = async () => {
try {
// User journey tracking başlat
const journeyId = performanceMonitor.startUserJourney('payment_flow', route.params?.userId);
// Business context ile screen load ölç
const data = await performanceMonitor.measureScreenLoad(
'payment_screen',
async () => {
// Ödeme methodlarını yükle
const methods = await performanceMonitor.instrumentApiCall(
'/api/payment-methods',
'GET',
() => api.getPaymentMethods(),
{
userId: route.params?.userId,
feature: 'payment_methods',
monetaryValue: route.params?.totalAmount,
}
);
// Kullanıcı tercihlerini yükle
const preferences = await api.getUserPreferences();
return { methods, preferences };
},
true // Bu business critical
);
setPaymentData(data);
setLoading(false);
} catch (error) {
errorTracker.trackBusinessError('payment_failure', error as Error, {
userId: route.params?.userId,
monetaryValue: route.params?.totalAmount,
customerTier: route.params?.customerTier,
feature: 'payment_screen_load',
});
setLoading(false);
}
};
const handlePaymentSubmit = async (paymentDetails: any) => {
try {
const result = await performanceMonitor.instrumentApiCall(
'/api/process-payment',
'POST',
() => api.processPayment(paymentDetails),
{
userId: route.params?.userId,
feature: 'payment_processing',
monetaryValue: route.params?.totalAmount,
}
);
// Journey'yi başarıyla tamamla
performanceMonitor.completeUserJourney(journeyId, true, {
paymentMethod: paymentDetails.method,
amount: route.params?.totalAmount,
});
// Success'e git
navigation.navigate('PaymentSuccess', { transactionId: result.id });
} catch (error) {
// Journey'yi başarısızlıkla tamamla
performanceMonitor.completeUserJourney(journeyId, false, {
failedStep: 'payment_processing',
error: error.message,
});
errorTracker.trackBusinessError('payment_failure', error as Error, {
userId: route.params?.userId,
monetaryValue: route.params?.totalAmount,
customerTier: route.params?.customerTier,
feature: 'payment_processing',
});
}
};
if (loading) {
return <LoadingSpinner />;
}
return (
<PaymentForm
data={paymentData}
onSubmit={handlePaymentSubmit}
/>
);
}
Outage’ları Önleyen Monitoring Kurulumu
Bu sistemi implement ettikten sonra, production’da izlediğimiz şeyler:
Datadog Dashboard Konfigürasyonu
// Bizi birden fazla incident'tan kurtaran dashboard
export const productionDashboards = {
"mobile_app_health": {
"title": "Mobile App Health - Production",
"widgets": [
{
"title": "Critical Business Errors",
"type": "timeseries",
"queries": [
{
"query": "sum:custom.critical_errors{*} by {error_type}",
"display_type": "bars"
}
],
"alert_threshold": 5 // 5 dakikada 5'ten fazla kritik error'da alert
},
{
"title": "Payment API Response Times",
"type": "timeseries",
"queries": [
{
"query": "avg:custom.api_call_duration{endpoint:payment*} by {endpoint}",
"display_type": "line"
}
],
"alert_threshold": 5000 // Payment API'lar 5s'yi geçerse alert
},
{
"title": "Screen Load Performance",
"type": "heatmap",
"queries": [
{
"query": "custom.screen_load_duration{business_critical:true}"
}
]
},
{
"title": "User Journey Completion Rate",
"type": "query_value",
"queries": [
{
"query": "sum:custom.user_journey_completion{success:true} / sum:custom.user_journey_completion{*} * 100"
}
]
},
{
"title": "App Crashes by Device",
"type": "toplist",
"queries": [
{
"query": "sum:custom.critical_errors{type:crash} by {device_model}"
}
]
}
]
}
};
Gerçekten İşe Yarayan Alert’ler
// Gerçek sorunlar için beni uyandıran, gürültü olmayan alert'ler
export const productionAlerts = {
"payment_failure_spike": {
"name": "Payment API Failure Spike",
"query": "sum(last_5m):sum:custom.critical_errors{type:payment_api_failure} > 3",
"message": "@slack-payments @pagerduty-critical",
"priority": "P1",
"escalation": "immediate"
},
"user_journey_drop": {
"name": "User Journey Completion Drop",
"query": "avg(last_15m):sum:custom.user_journey_completion{success:true} / sum:custom.user_journey_completion{*} < 0.8",
"message": "@slack-product @email-team",
"priority": "P2",
"escalation": "15_minutes"
},
"critical_screen_slow": {
"name": "Critical Screen Load Time",
"query": "avg(last_10m):avg:custom.screen_load_duration{business_critical:true} > 5000",
"message": "@slack-engineering",
"priority": "P2",
"escalation": "30_minutes"
}
};
Performans Etkisi ve Optimizasyon
18 aylık production kullanımından sonra, gerçek performans rakamları:
Kaynak Kullanımı
- CPU overhead: 2-3% ortalama (Xcode Instruments ile ölçüldü)
- Memory overhead: 15-20MB (çoğunlukla trace buffering)
- Batarya etkisi: İhmal edilebilir (günlük 1%‘den az tüketim)
- Network kullanımı: Kullanıcı başına günde 50-100KB
Maliyet Analizi (Aylık)
- Datadog: $400/ay (100M span, 50GB log)
- Firebase: $0 (ücretsiz tier limitleri içinde)
- AWS infrastructure: $50/ay (OTEL collector)
- Kazanılan development zamanı: 40+ saat/ay
- ROI: 10x (debugging verimliliği + önlenen outage’lar)
İşe Yarayan Optimizasyon Stratejileri
// Maliyetleri 60% azaltan akıllı sampling
class AdaptiveSampler {
private errorRate = new Map<string, number>();
private criticalSessions = new Set<string>();
shouldSample(spanName: string, attributes: any): boolean {
// Error'ları ve kritik business akışlarını her zaman sample'la
if (spanName.includes('error') || attributes['business.monetary_value']) {
return true;
}
// Kritik kullanıcı session'larını daha yüksek oranda sample'la
if (attributes['user.tier'] === 'premium') {
return Math.random() < 0.5; // 50% sampling
}
// Error oranlarına göre adaptif sampling
const errorRate = this.errorRate.get(spanName) || 0;
if (errorRate > 0.05) { // 5%'ten fazla error
return Math.random() < 0.8; // Sampling'i artır
}
// Varsayılan sampling
return Math.random() < 0.1; // 10% base oran
}
}
Sonuçlar: Gerçekten Yardımcı Olan Gözlemlenebilirlik
Kullanıcılar Fark Etmeden Yakalanan Sorunlar
- iOS 15.4 Network Bug’ı: Major rollout’tan 2 gün önce iOS 15.4 WiFi kullanıcılarına spesifik API timeout’larını yakaladı
- Image Caching’de Memory Leak: Kullanıcı şikâyetlerinden önce 20% RAM kullanımı artışını tespit etti
- Payment Race Condition: Journey tracking kullanarak hızlı network’lerde 0.3% ödeme başarısızlığı buldu
- Android Battery Drain: Samsung cihazlarda 15% batarya tüketen background process’i tanımladı
Business Etkisi
- Daha Hızlı Sorun Çözme: Ortalama debugging süresi 6 saatten 45 dakikaya düştü
- Proaktif Düzeltmeler: Sorunların 60%‘ı kullanıcı şikâyetlerinden önce düzeltildi
- Müşteri Memnuniyeti: App store değerlendirmesi 3.2’den 4.6’ya yükseldi
- Gelir Koruması: Tahmini $1100K+ kayıp işlem önlendi
Developer Mutluluğu
- Körü Körüne Debug Yok: Kullanıcı yolculuğu ile zengin context’li error raporları
- Deployment’larda Güven: Kapsamlı monitoring regression’ları hızla yakalar
- Veri Temelli Kararlar: Gerçek metriklerle desteklenen performans budgetleri
Zor Öğrenilen Dersler
1. Basit Başla, Kademeli Evrimleş
İlk günden her şeyi monitor etmeye çalışma. Şunlarla başla:
- Kritik business akışları (ödemeler, login, temel özellikler)
- Context’li error tracking
- Ana screen’ler için performans monitoring
- Temel user journey tracking
2. Context Her Şeydir
Ham metrikler işe yaramaz. Her zaman şunları dahil et:
- User context (ID, session, journey)
- Business context (özellik, para değeri, müşteri seviyesi)
- Teknik context (cihaz, network, app versiyonu)
- Error context (kullanıcı ne yapıyordu)
3. Sampling Stratejisi Önemli
- Kritik akışlar: 100% sampling
- Business özellikleri: 50% sampling
- UI etkileşimleri: 10% sampling
- Background task’lar: 1% sampling
4. Alert’ler Sizi Uyandırmalı
Yalnızca anında aksiyon gerektiren şeylerde alert:
- Ödeme işleme başarısızlıkları
- Crash oranı artışları
- Kritik business akış tamamlanma düşüşleri
- Güvenlik-ilişkili event’ler
5. Birden Fazla Exporter = Güvenilirlik
Tek monitoring provider’a güvenme:
- Birincil: Datadog (zengin analytics)
- İkincil: Elastic APM (maliyet kontrolü)
- Backup: Firebase (her zaman çalışır)
Başlangıç: 7 Günlük Implementation Planı
1-2. Gün: Temel
- OpenTelemetry provider kurulumu
- Temel error tracking ekle
- Global error handler’ları implement et
3-4. Gün: Performans Monitoring
- Screen load tracking ekle
- API call instrumentation implement et
- Navigation tracking kur
5-6. Gün: Business Metrikleri
- User journey’leri track et
- Custom business event’leri ekle
- Kritik akış monitoring kur
7. Gün: Production Deployment
- Sampling oranlarını configure et
- Alert’leri kur
- Monitoring dashboard’ları oluştur
Son Düşünceler: Ürün Özelliği Olarak Gözlemlenebilirlik
18 ay boyunca production gözlemlenebilirliği kurduktan sonra, monitoring’in sadece “olması güzel” bir şey değil - rekabet avantajı olduğunu öğrendim.
Sorunları hızla debug etme, outage’ları önleme ve gerçek verilere dayalı kullanıcı deneyimini optimize etme yeteneği, ekibimizin özellik geliştirme şeklini transform etti. Reaktif debugging’den proaktif optimizasyona geçtik.
İlk yatırım (2 hafta development + ayda $500 araç) kendisini çözmeye yardım ettiği ilk büyük sorunla amorti ediyor.
Kullanıcılarınız iyi gözlemlenebilirlik için teşekkür etmeyecek, ama yoksa kesinlikle şikâyet edecekler. Bugün sizinkini inşa etmeye başlayın.
İlgili yazılar
React Native Expo uygulamasına Sentry hata izleme entegrasyonu için adım adım rehber. SDK başlatma, Expo Router enstrümantasyonu, session replay, EAS Build ve EAS Update için source map yükleme ve sık karşılaşılan sorunları kapsar.
Pratik implementasyon örnekleri, yaygın hatalar ve detaylı terminoloji sözlüğü ile OpenTelemetry'nin trace, metric ve log sistemlerini kapsayan kapsamlı başlangıç rehberi.
Yeşil ışıklarla dolu dashboard'lardan, dağıtık izleme ve AI destekli analiz ile sistem davranışı, kullanıcı yolculukları ve iş etkisi hakkında etkileyici hikayeler anlatan observability sistemlerine geçiş
Single Table Design uygulamalarında DynamoDB throttling'i önleme ve yönetme stratejileri. Partition key tasarımı, write sharding, kapasite modları, DAX caching, retry pattern'leri ve yüksek throughput sistemler için CloudWatch monitoring konularını kapsar.
LangChain uygulamalarını production'a taşırken öğrendiklerim. Başarısızlığa yol açan anti-patternler, başarıyı sağlayan patternler, çalışan kod örnekleri ve maliyet optimizasyon stratejileri.