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üğü olmadan yayına çıkar. Yerel ortamda bir türlü tekrarlanamayan çökmeler, arkasında zamanlama verisi bulunmayan performans şikayetleri, belirtiyi anlatan ama uygulamanın o anki durumunu anlatmayan destek kayıtları.
İşe yarayan varsayılan şu: uygulamanın içinde OpenTelemetry JavaScript SDK, OTLP üzerinden kendi işlettiğiniz bir collector’a export, ikinci sinyal olarak da Firebase Performance ve Crashlytics. Vendor SDK’ları daha hızlı kurulur; collector ise backend değiştirmenizi yeni bir sürüm yayınlamadan mümkün kılar ve APM kimlik bilgilerinin uygulama paketine girmesini engeller.
Mobil Gözlemlenebilirlik Gereksinimleri#
Production mobil uygulamaları, web uygulamalarından farklı 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. Mobil bir telemetri kurulumu; kullanıcı etkileşimleri ve sistem davranışını birlikte kapsamalı, debugging için cihaz ve OS bağlamı taşımalı, mobile özgü performans kısıtlarını hesaba katmalı ve cihaz offline’ken de toplamaya devam etmelidir.
React Native İçin Neden OpenTelemetry#
Firebase Performance Monitoring
- Artıları: Kolay kurulum, ücretsiz tier, temel metrikler
- Eksileri: Sınırlı özelleştirme, dağıtık tracing yok, vendor bağımlılığı
Bu iki eksi, dağıtık tracing olmaması ve vendor bağımlılığı, OpenTelemetry SDK’nın üzerine eklediği şey: Firebase’in, kendi işlettiğiniz bir collector’ın ya da bir APM backend’inin hepsinin tüketebileceği vendor-bağımsız span’lar.
Telemetri Mimarisi#
Hat üç katmandan oluşur: uygulama içinde sinyal üretimi, SDK içinde batching ve sampling, ardından bir veya birden fazla backend’e export.
Enstrümantasyon Kurulumu#
Provider üç şeyi yönetir: derlemeyi tanımlayan resource attribute’ları, exporter’lar ve sampler.
Temel OpenTelemetry Kurulumu#
// telemetry/provider.ts - React Native için OpenTelemetry provider
import { metrics } from '@opentelemetry/api';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { BatchSpanProcessor, SpanProcessor } from '@opentelemetry/sdk-trace-base';
import { MeterProvider, PeriodicExportingMetricReader, PushMetricExporter } from '@opentelemetry/sdk-metrics';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { ProductionFirebaseExporter } from './firebase-integration';
interface TelemetryConfig {
environment: 'development' | 'staging' | 'production';
collectorUrl: string;
enabledExporters: string[];
samplingRate: number;
maxBatchSize: number;
exportInterval: number;
}
class ProductionTelemetryProvider {
private tracerProvider: WebTracerProvider | null = null;
private meterProvider: MeterProvider | 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,
// Mobil tarafa özgü attribute'lar debugging'i kolaylaştırır
'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 etmeye yardım eder
'network.carrier': deviceInfo.carrier,
'device.memory': deviceInfo.totalMemory,
});
// Redundancy ve güvenilirlik için birden fazla exporter
const exporters = this.createExporters(config);
// Span processor'lar constructor'da veriliyor; 2.x SDK hattında
// provider'dan addSpanProcessor kaldırıldı
this.tracerProvider = new WebTracerProvider({
resource,
sampler: this.createAdaptiveSampler(config.samplingRate),
spanProcessors: exporters.spanProcessors,
});
// Global kayıt: diğer dosyalardaki trace.getTracer() bu provider'a düşer
this.tracerProvider.register();
if (exporters.metricExporter) {
this.meterProvider = new MeterProvider({
resource,
readers: [new PeriodicExportingMetricReader({
exporter: exporters.metricExporter,
exportIntervalMillis: config.exportInterval,
})],
});
metrics.setGlobalMeterProvider(this.meterProvider);
}
// Instrumentation'ları kaydet
registerInstrumentations({
instrumentations: [
new XMLHttpRequestInstrumentation(),
new FetchInstrumentation(),
],
});
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: SpanProcessor[] = [];
let metricExporter: PushMetricExporter | null = null;
// Birincil yol - kendi işlettiğiniz collector'a OTLP. Vendor kimlik
// bilgileri collector'da durur, APM API key'i uygulama paketine girmez.
if (config.enabledExporters.includes('otlp')) {
spanProcessors.push(new BatchSpanProcessor(
new OTLPTraceExporter({ url: `${config.collectorUrl}/v1/traces` }),
{
maxExportBatchSize: config.maxBatchSize,
scheduledDelayMillis: config.exportInterval,
// Memory buildup'ı önlemek için agresif timeout
exportTimeoutMillis: 10000,
},
));
metricExporter = new OTLPMetricExporter({
url: `${config.collectorUrl}/v1/metrics`,
});
}
// İkincil exporter - temel monitoring için Firebase
if (config.enabledExporters.includes('firebase')) {
spanProcessors.push(new BatchSpanProcessor(new ProductionFirebaseExporter(), {
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 forceFlush() {
await Promise.all([
this.tracerProvider?.forceFlush(),
this.meterProvider?.forceFlush(),
]);
}
async shutdown() {
if (!this.isInitialized) return;
await this.tracerProvider?.shutdown();
await this.meterProvider?.shutdown();
this.isInitialized = false;
}
}
export const telemetryProvider = new ProductionTelemetryProvider();
React Native Performans Monitoring#
Screen load’lar, API çağrıları ve çok ekranlı yolculuklar farklı span şekilleri ister:
// telemetry/performance-monitor.ts - Screen, API ve journey enstrümantasyonu
import { trace, metrics, context } from '@opentelemetry/api';
import perf from '@react-native-firebase/perf';
import { AppState, AppStateStatus } from 'react-native';
import { telemetryProvider } from './provider';
class ProductionPerformanceMonitor {
private tracer = trace.getTracer('app-performance', '1.0.0');
private meter = metrics.getMeter('app-metrics', '1.0.0');
// Alert'lerin üzerine kurulduğu dört enstrüman
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();
}
// Screen load'ları track et, gelirle ilişkili olanları işaretle
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();
}
}
// 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.forceFlush();
} catch (error) {
console.warn('Failed to flush telemetry:', error);
}
}
}
export const performanceMonitor = new ProductionPerformanceMonitor();
Navigation Takibi#
Ekran görüntülenme sayacı kullanıcının nereye gittiğini söyler. Geçiş süresi ve hızlı çıkışlar ise nerede takıldığını söyler; enstrümantasyon maliyetine değen kısım budur:
// telemetry/navigation-instrumentation.tsx
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');
// Hacmi değil, sürtünmeyi gösteren 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 };
Context’li Error Tracking#
Tek başına stack trace bir mobil çökmeyi nadiren açıklar. Cihaz durumu, o anki ekran ve oraya götüren adımlar hatayı tekrarlanabilir kılar:
// telemetry/error-tracking.tsx - Cihaz ve journey context'iyle hata yakalama
import React from 'react';
import { View, Text } from 'react-native';
import { trace } 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=***');
}
// Global handler'lar hiçbir try/catch'in ulaşamadığını yakalar
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);
});
// 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();
// Fallback'i render etmeden önce raporlayan 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;
}
}
Firebase Entegrasyon Pattern’leri#
Firebase Performance Monitoring başlamak için iyi bir seçenek, ama dikkatli entegrasyon ister. Doğru kurulması gereken yer export callback’i: başarıyı ancak her chunk işlendikten sonra bildirin, yoksa uygulama arka plana geçerken çalışan flush, span’lar hâlâ sırada beklerken geri döner.
// telemetry/firebase-integration.ts
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
private pendingExports = new Set<Promise<void>>();
private isShutdown = false;
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
if (this.isShutdown) {
resultCallback({ code: ExportResultCode.FAILED });
return;
}
// Sonucu ancak sıradaki chunk'lar tamamlandıktan sonra bildir; forceFlush()
// ve shutdown() hâlâ bekleyen span'ların üzerine kapanmasın
const pending = this.exportChunks(spans);
this.pendingExports.add(pending);
pending
.then(() => resultCallback({ code: ExportResultCode.SUCCESS }))
.catch(error => {
console.error('Firebase export error:', error);
resultCallback({ code: ExportResultCode.FAILED });
})
.finally(() => this.pendingExports.delete(pending));
}
private async exportChunks(spans: ReadableSpan[]): Promise<void> {
// Firebase'i boğmamak için span'ları chunk'larda işle
const chunks = this.chunkArray(spans, 10);
for (let i = 0; i < chunks.length; i++) {
if (i > 0) {
await this.delay(100); // İşlemeyi basamakla
}
await Promise.all(chunks[i].map(span => this.processSpan(span)));
}
}
private delay(ms: number): Promise<void> {
return new Promise<void>(resolve => setTimeout(resolve, ms));
}
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> {
this.isShutdown = true;
// Trace'ler kapatılmadan önce sıradaki chunk'lar bitsin
await Promise.allSettled([...this.pendingExports]);
// 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();
}
}
Kullanım Pattern’leri#
Üç yardımcının bir screen component’ta birlikte kullanımı:
Screen Component Tracking#
// screens/PaymentScreen.tsx
import React, { useEffect, useRef, 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);
// Journey, load'dan uzun yaşar; loadPaymentScreen scope'unda duramaz
const journeyId = useRef<string>('');
useEffect(() => {
loadPaymentScreen();
}, []);
const loadPaymentScreen = async () => {
try {
// User journey tracking başlat
journeyId.current = 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.current, 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.current, 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}
/>
);
}
Monitoring Kurulumu#
Bu sistemi implement ettikten sonra production’da izlenecek metrikler:
Datadog Dashboard Konfigürasyonu#
// Production monitoring 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}"
}
]
}
]
}
};
Alert Konfigürasyonu#
// Eskalasyon süresi içinde aksiyon alınabilecek koşullara kurulmuş 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 Maliyet Kalemleri#
Ek yük SDK’nın kendisiyle değil, ne kadar sample aldığınızla ölçeklenir. Enstrümantasyon öncesi ve sonrası kendi derlemenizde ölçün: iOS’ta Xcode Instruments, Android’de Android Studio Profiler. İlk etken bellek: batch span processor, span’ları bir sonraki export’a kadar tutar; tavanı maxExportBatchSize ve scheduledDelayMillis belirler. Ardından batarya ve radyo gelir: her export radyoyu uyandırır; batching’in span başına export’a tercih edilme sebebi budur. Network kullanımı sample’lanan span sayısı ve attribute boyutuyla orantılıdır, uygulamanın açık kalma süresinden bağımsızdır.
Maliyet Kalemleri#
Backend faturalandırması alınan veri hacmine ve saklama süresine bağlıdır, yani maliyet kontrolü sampler’dadır. Bir tier’a bağlanmadan önce güncel oranları Datadog fiyatlandırma sayfasında (yeni sekmede açılır) ve Firebase fiyatlandırma sayfasında (yeni sekmede açılır) kontrol edin. Collector ise doğrudan sizin kontrolünüzdeki küçük ve sürekli açık bir compute maliyeti ekler.
Sampling Stratejisi#
// Her error'ı sakla, rutin trafiği seyrelt
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
}
}
Edinilen Dersler#
Basit Başla, Kademeli Evrimleş#
Gözlemlenebilirliği kademeli kurun. Şunlarla başlayın:
- Kritik business akışları (ödemeler, login, temel özellikler)
- Context’li error tracking
- Ana screen’ler için performans monitoring
- Temel user journey tracking
Metriği Kullanılabilir Kılan Şey Context#
Boyutsuz bir süre histogramı bir şeyin yavaş olduğunu söyler, kimin için yavaş olduğunu söylemez. Her zaman şunları dahil edin:
- 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)
Sampling Stratejisi Önemli#
| Akış tipi | Sampling oranı |
|---|---|
| Kritik akışlar | %100 |
| Business özellikleri | %50 |
| UI etkileşimleri | %10 |
| Background task’lar | %1 |
Alert Stratejisi#
Alert’leri aksiyon alınabilir sorunlara odaklayın: ödeme işleme başarısızlıkları, crash oranı artışları, kritik business akış tamamlanma düşüşleri ve güvenlik-ilişkili event’ler.
İki Bağımsız Yol#
Collector yolu ile Firebase farklı sebeplerle bozulur. Birincil yol OTLP collector’dır; buradan APM backend’ine dağıtım yapılır. Yedek yol ise Firebase Performance ve Crashlytics’tir; collector’a erişilemediğinde de raporlamayı sürdürür.
Enstrümantasyon Anti-Pattern’leri#
- Tek bir alert bile yokken her ekranı enstrümante etmek. Hacim artar, sonucu kimse okumaz.
- Kullanıcı kimliklerini veya token’ları span adına koymak; bunlar sonra yüksek kardinaliteli metrik etiketine dönüşür.
- Batching yerine span başına export etmek; bu, ekranlar arasında radyoyu açık tutar.
- Error’ları rutin trafikle aynı oranda sample’lamak; tam da ihtiyaç duyulan trace’ler atılır.
Bu Varsayılan Ne Zaman Geçerli#
SDK artı collector kurulumu, elinizde birden fazla makul backend adayı olduğunda ya da vendor kimlik bilgilerini binary içinde göndermek politika gereği mümkün olmadığında masrafını çıkarır. Uygulama, zaten trace ettiğiniz servislere gidiyorsa da geçerlidir: tek bir trace ID mobil ile backend’i birlikte kapsar.
İki durumda tersini seçin. Tek ekipli, vendor değiştirme planı olmayan küçük bir uygulama için tek başına vendor SDK daha uygundur: daha az hareketli parça, işletilecek collector yok. Alert triyajını üstlenen kimse yoksa da yalnızca error tracking’i enstrümante edin.
Makul bir sonraki adım: iş açısından kritik tek bir akışı uçtan uca enstrümante etmek, bir sürüm döngüsü boyunca izlemek, sonra kapsamı genişletmek.
Kaynaklar#
- OpenTelemetry JavaScript enstrümantasyon rehberi (yeni sekmede açılır) - OTel SDK ile JavaScript ve React Native uygulamalarını enstrümante etme resmi rehberi
- OpenTelemetry React Native uygulama demosu (yeni sekmede açılır) - React Native uygulamasında OTel entegrasyonunu gösteren referans implementasyon
- Firebase Performans İzleme (yeni sekmede açılır) - Mobil uygulamalarda iz ve metrik yakalamak için Firebase Performans İzleme’ye genel bakış
- Firebase Crashlytics (yeni sekmede açılır) - Uygulama çökmelerini izlemek, önceliklendirmek ve düzeltmek için hafif çökme raporlayıcı
- OpenTelemetry JavaScript - Enstrümantasyon kütüphaneleri kullanımı (yeni sekmede açılır) - Bağımlılıklardan telemetri yakalamak için otomatik enstrümantasyon kütüphanelerinin kaydedilmesi
- OpenTelemetry - Dil API’leri ve SDK’ları (yeni sekmede açılır) - Desteklenen tüm dil SDK’larına ve olgunluk düzeylerine genel bakış
- Firebase Performans İzleme - Özel kod izleri (yeni sekmede açılır) - Uygulamanızdaki belirli işlemleri enstrümante etmek için özel performans izleri ekleme
İlgili yazılar
React Native Expo uygulamasına Sentry ekleme rehberi: SDK kurulumu, Expo Router enstrümantasyonu, session replay ve EAS için source map yükleme.
react-native · expo · monitoring +2
Pratik örnekler, yaygın hatalar ve terminoloji sözlüğü ile OpenTelemetry'nin trace, metric ve log sistemlerini kapsayan başlangıç rehberi.
opentelemetry · observability · monitoring +4
Yeşil ışıklı dashboard'lardan, dağıtık izleme ile sistem davranışını ve iş etkisini anlatan observability sistemlerine geçişin yolu.
observability · monitoring · opentelemetry +3
Event-driven sistemler için production-ready DLQ kalıpları: monitoring, circuit breaker, exponential backoff, recovery ve kaçınılması gereken anti-pattern'lar.
cloud-providers · reliability · messaging +2
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