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