Anatomi Startup Crash Loop Pasca Deployment

Migrasi skema database lokal atau perubahan struktur serialisasi state sering kali menjadi penyebab utama startup crash loop pada aplikasi React Native. Ketika rilis baru diluncurkan, fungsi inisialisasi runtime langsung mengeksekusi proses de-serialisasi data lokal (seperti MMKV, WatermelonDB, atau SQLite). Jika skema baru berasumsi bahwa suatu kolom atau property bertipe non-nullable sementara data lama bernilai undefined atau memiliki struktur berbeda, JavaScript engine melempar unhandled exception secara sinkron di fase entry point.

Dampaknya, aplikasi mengalami terminasi paksa (crash on launch) dalam hitungan milidetik setelah proses OS dimulai. Pengguna terjebak dalam siklus di mana aplikasi langsung menutup setiap kali dibuka kembali.

Limitasi Remote Config dan Over-The-Air (OTA) Update

Banyak tim mengandalkan Remote Config (Firebase) atau Over-The-Air (OTA) updates (CodePush atau Expo Updates) sebagai garis pertahanan darurat. Namun, strategi ini gagal total ketika insiden startup crash terjadi. Alasannya:

  • Ketiadaan Network Window: Eksekusi JS thread terhenti sebelum modul networking (seperti fetch atau XMLHttpRequest) sempat menginisiasi koneksi HTTP ke server OTA.
  • Bridge/JSI Breakdown: Jika crash terjadi di level inisialisasi native module atau sinkronisasi awal JSI (JavaScript Interface), runtime React Native dihentikan oleh OS sebelum bundle update sempat diverifikasi dan dipasang ke disk.
  • Dead-on-Arrival: Mekanisme kill-switch berbasis API membutuhkan minimal satu siklus event loop yang bersih untuk membaca payload server. Jika runtime mati sinkron saat mengevaluasi index.js, logika tersebut tidak akan pernah dieksekusi.
Ketika aplikasi mati sebelum runtime siap, pemulihan harus dilakukan sepenuhnya di sisi klien (client-side mitigation) menggunakan status lokal yang persisten.

Pola Crash Counter: Deteksi Kegagalan Beruntun

Solusi deterministik untuk menangani crash loop adalah menerapkan pola Startup Crash Counter. Pola ini memantau siklus hidup aplikasi dengan aturan:

  1. Baca counter insiden sebelum proses inisialisasi state utama dimulai.
  2. Jika counter melampaui batas toleransi (misalnya: threshold >= 3), alihkan aplikasi ke Emergency Safe Mode.
  3. Jika di bawah ambang batas, inkremen nilai counter dan simpan secara sinkron.
  4. Jalankan timer stabilisasi (misalnya: 5-10 detik). Jika aplikasi tetap berjalan tanpa crash fatal hingga timer selesai, setel counter kembali ke 0 (clean exit/stable runtime).

Karena pembacaan harus instan dan sinkron sebelum engine me-render komponen berat, instance MMKV khusus atau native key-value storage (SharedPreferences / NSUserDefaults) adalah pilihan ideal.

Implementasi Entry Point (`index.js`)

import { AppRegistry } from 'react-native';
import { MMKV } from 'react-native-mmkv';
import App from './src/App';
import EmergencyRecoveryApp from './src/EmergencyRecoveryApp';
import { name as appName } from './app.json';

const bootStorage = new MMKV({ id: 'boot-monitor' });
const CRASH_KEY = 'consecutive_crashes';
const THRESHOLD = 3;
const STABILIZATION_DELAY_MS = 6000;

function evaluateBootHealth() {
  const crashes = bootStorage.getNumber(CRASH_KEY) ?? 0;

  if (crashes >= THRESHOLD) {
    return { shouldEnterRecovery: true, crashCount: crashes };
  }

  // ponytail: increment synch; native-bridge crash before JS evaluates won't trigger this.
  bootStorage.set(CRASH_KEY, crashes + 1);

  const timer = setTimeout(() => {
    bootStorage.set(CRASH_KEY, 0);
  }, STABILIZATION_DELAY_MS);

  return { shouldEnterRecovery: false, crashCount: crashes };
}

const bootStatus = evaluateBootHealth();

const RootComponent = bootStatus.shouldEnterRecovery
  ? EmergencyRecoveryApp
  : App;

AppRegistry.registerComponent(appName, () => RootComponent);

Implementasi Emergency Recovery Mode

Saat flag darurat aktif, aplikasi tidak boleh me-render tree utama yang memanggil store (Redux, Zustand, WatermelonDB). Sebagai gantinya, EmergencyRecoveryApp memuat UI minimal berbasis native components tanpa dependensi state kompleks. UI ini mengeksekusi reset state lokal.

Kode Safe-Fallback UI

import React, { useState } from 'react';
import { StyleSheet, Text, View, Pressable, ActivityIndicator } from 'react-native';
import { MMKV } from 'react-native-mmkv';

const bootStorage = new MMKV({ id: 'boot-monitor' });

export default function EmergencyRecoveryApp() {
  const [resetting, setResetting] = useState(false);
  const [completed, setCompleted] = useState(false);

  const handleStatePurge = async () => {
    setResetting(true);
    try {
      // 1. Purge general application storage
      const appStorage = new MMKV({ id: 'app-storage' });
      appStorage.clearAll();

      // 2. Clear crash monitor state
      bootStorage.delete('consecutive_crashes');

      // ponytail: WatermelonDB/SQLite deletion requires native file unlink if driver locks up.
      setCompleted(true);
    } catch (error) {
      console.error('State purge failed:', error);
    } finally {
      setResetting(false);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Mode Pemulihan Sistem</Text>
      <Text style={styles.body}>
        Aplikasi mendeteksi kegagalan berulang saat proses startup. Data cache lokal
        mungkin tidak kompatibel dengan pembaruan saat ini.
      </Text>

      {completed ? (
        <Text style={styles.success}>State berhasil direset. Silakan restart aplikasi.</Text>
      ) : (
        <Pressable
          style={styles.button}
          onPress={handleStatePurge}
          disabled={resetting}
        >
          {resetting ? (
            <ActivityIndicator color="#fff" />
          ) : (
            <Text style={styles.buttonText}>Reset Data Lokal & Pulihkan</Text>
          )}
        </Pressable>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24, backgroundColor: '#0f172a' },
  title: { fontSize: 20, fontWeight: '700', color: '#f8fafc', marginBottom: 12 },
  body: { fontSize: 14, color: '#94a3b8', textAlign: 'center', marginBottom: 24, lineHeight: 20 },
  button: { backgroundColor: '#ef4444', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8 },
  buttonText: { color: '#ffffff', fontWeight: '600' },
  success: { color: '#22c55e', fontWeight: '600', textAlign: 'center' },
});

Pelacakan Telemetri ke Sentry dan Datadog

Ketika aplikasi masuk ke EmergencyRecoveryApp, jaringan telemetri harus diinisialisasi secara terkontrol untuk mengirim diagnostik tanpa memicu crash sekunder.

  • Isolated Payload: Jangan kirim state global yang korup ke Sentry/Datadog. Kirim metadata ringkas: versi bundle, jumlah crash beruntun, serta last known state migration version.
  • Custom Tagging: Pasang tag eksplisit seperti emergency_recovery: true dan crash_count: 3 agar insiden dapat dikelompokkan secara terpisah di dashboard error tracking.
  • Manual Flush: Karena aplikasi berada dalam safe-mode, panggil flush SDK secara eksplisit sebelum komponen dimatikan atau direstart oleh pengguna:
import * as Sentry from '@sentry/react-native';

export function reportRecoveryTriggered(consecutiveCrashes) {
  Sentry.withScope((scope) => {
    scope.setLevel('fatal');
    scope.setTag('recovery_mode', 'active');
    scope.setExtra('consecutive_crashes', consecutiveCrashes);
    Sentry.captureMessage('Startup crash loop threshold exceeded. Safe mode triggered.');
  });
}

Pencegahan Regresi Skema State di Pipeline CI/CD

Menghindari crash loop di level produksi memerlukan validasi otomatis sebelum deployment disetujui. Langkah pencegahan yang wajib diterapkan di pipeline integrasi:

  • Validasi Skema Parsial dengan Zod: Hindari blind casting data dari local storage. Terapkan schema parser dengan safeParse dan berikan nilai fallback default jika payload tidak valid secara struktural.
  • Automated Migration Unit Tests: Jalankan unit test spesifik yang membaca mock data versi skema lama, kemudian jalankan fungsi migrasi skema WatermelonDB/SQLite, lalu validasi integritas struktur akhir.
  • Storage Migration Matrix Test: Uji migrasi dari versi rilis n-1, n-2, dan n-3 langsung ke versi terbaru di CI menggunakan runner Jest/Node.

Crash loop saat inisialisasi adalah skenario terburuk pada aplikasi mobile karena menutup akses interaksi pengguna secara total. Memasang isolasi safe mode berbasis native counter memastikan aplikasi tetap memiliki jalur pemulihan mandiri tanpa harus menunggu intervensi update manual dari App Store atau Play Store.