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ı.

React Native uygulaması lansmanımızın altıncı ayında, körü körüne uçuyorduk. Kullanıcılar reprodüce edemediğimiz crash'lerden şikâyet ediyordu. Performans sorunları rastgele ortaya çıkıyordu. En büyük enterprise müşterimiz uygulamanın "yavaş hissettiği" için ayrılmakla tehdit etti - ama aksini kanıtlayacak hiçbir verimiz yoktu.

Tanıdık geliyor mu? 200.000+ kullanıcıya hizmet veren bir React Native uygulaması için 18 ay boyunca kapsamlı gözlemlenebilirlik kurduktan sonra, gerçekten işe yarayan production monitoring hakkında öğrendiklerimi paylaşıyorum.

Uyandıran Telefon: Bir Hafta Sonunda Kaybedilen 50 Bin Dolar#

Mart 2023. iOS kullanıcıları için ödeme akışımız sessizce başarısız olmaya başladı. Bunu ancak en büyük müşterimiz Pazartesi sabahı aradığında öğrendik - hafta sonu boyunca 50.000 dolarlık işlem kaybetmişlerdi.

Loglar hiçbir şey göstermiyordu. Crashlytics hiçbir şey göstermiyordu. Flipper development'ta sorunsuz çalışıyordu. Yalnızca iOS 14.8 kullanıcılarını spesifik network koşullarında etkileyen ödeme işlemlerindeki race condition'ı debug etmek için 14 saat harcadık.

Bu olay bana üç şey öğretti:

  1. Göremediğinizi debug edemezsiniz
  2. Mobile debugging web debugging'den farklıdır
  3. İyi gözlemlenebilirlik anında kendisini amorti eder

Ertesi gün, gerçek bir monitoring sistemi inşa etmeye başladım.

Neden OpenTelemetry'yi Seçtim (Her Şeyi Denedikten Sonra)#

OpenTelemetry'den önce, her React Native monitoring çözümünü denedim:

Firebase Performance Monitoring (2 ay)#

Artıları: Kolay kurulum, ücretsiz tier, iyi temel metrikler Eksileri: Sınırlı özelleştirme, dağıtık tracing yok, vendor lock-in

Datadog RUM (3 ay)#

Artıları: Zengin dashboard'lar, mükemmel alerting, gerçek kullanıcı monitoring Eksileri: Pahalı (kullanıcı başına $50/ay), React Native desteği buggy'di

New Relic Mobile (1 ay)#

Eksileri: Yüksek trafik sırasında uygulamamızı crash'ledi, zayıf React Native dokümanları

Sentry Performance (2 hafta)#

Eksileri: İhtiyacımız olan 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:

Loading diagram...

Production'da Gerçekten İşe Yarayan Kurulum#

18 aylık iterasyondan sonra, production-ready implementasyon:

Temel OpenTelemetry Kurulumu#

TypeScript
// 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:

TypeScript
// 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:

TypeScript
// 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 &lt;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 &lt;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:

TypeScript
// 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:

TypeScript
// 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#

TypeScript
// 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#

TypeScript
// 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#

TypeScript
// 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{*} &lt;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#

TypeScript
// 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() &lt;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() &lt;0.8; // Sampling'i artır
    }

    // Varsayılan sampling
    return Math.random() &lt;0.1; // 10% base oran
  }
}

Sonuçlar: Gerçekten Yardımcı Olan Gözlemlenebilirlik#

Kullanıcılar Fark Etmeden Yakalanan Sorunlar#

  1. 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ı
  2. Image Caching'de Memory Leak: Kullanıcı şikâyetlerinden önce 20% RAM kullanımı artışını tespit etti
  3. Payment Race Condition: Journey tracking kullanarak hızlı network'lerde 0.3% ödeme başarısızlığı buldu
  4. 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:

  1. Kritik business akışları (ödemeler, login, temel özellikler)
  2. Context'li error tracking
  3. Ana screen'ler için performans monitoring
  4. 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.

The Wake-Up Call: $50K Lost in One Weekend#

Çeviri eklenecek.

Why I Chose OpenTelemetry (After Trying Everything Else)#

Çeviri eklenecek.

The Architecture That Handles 2M+ Events Daily#

Çeviri eklenecek.

The Setup That Actually Works in Production#

Çeviri eklenecek.

Error Tracking That Actually Catches Issues#

Çeviri eklenecek.

The Firebase Integration That Doesn't Break#

Çeviri eklenecek.

Real Usage Patterns That Actually Help#

Çeviri eklenecek.

The Monitoring Setup That Prevented Outages#

Çeviri eklenecek.

Performance Impact and Optimization#

Çeviri eklenecek.

The Results: Observability That Actually Helped#

Çeviri eklenecek.

Hard-Learned Lessons#

Çeviri eklenecek.

Getting Started: The 7-Day Implementation Plan#

Çeviri eklenecek.

Final Thoughts: Observability as a Product Feature#

Çeviri eklenecek.

Loading...

Yorumlar (0)

Sohbete katıl

Düşüncelerini paylaşmak ve toplulukla etkileşim kurmak için giriş yap

Henüz yorum yok

Bu yazı hakkında ilk düşüncelerini paylaşan sen ol!

Related Posts