Dilema Rollback Biner dan Revert OTA

Menemukan bug penyebab crash (unhandled exception) pada production release React Native menuntut penurunan Mean Time to Recovery (MTTR) sesingkat mungkin. Dua metode konvensional memiliki limitasi operasional tinggi:

  • Rollback Biner (App Store & Google Play Store): Membutuhkan build biner baru, bump versi, review manual Apple (memakan waktu 4 hingga 24 jam), serta propagasi bertahap di Play Store. Metode ini lambat dan tidak mengatasi perangkat pengguna yang belum memperbarui aplikasi.
  • Revert Total OTA Bundle (CodePush / Expo Updates): Mengembalikan bundle JS ke rilis sebelumnya memang instan, namun membatalkan seluruh perubahan yang dikirim dalam batch tersebut. Jika deployment memuat 5 perbaikan bug krusial dan 1 fitur bermasalah, revert OTA mengorbankan 5 perbaikan lainnya.

Solusi deterministik: Remote Kill Switch (client-side circuit breaker). Pendekatan ini menonaktifkan cabang kode bermasalah secara granular melalui konfigurasi remote dan mengalihkan UI ke mekanisme graceful degradation tanpa intervensi build store atau revert bundle massal.

Arsitektur Remote Kill Switch (Client-Side Circuit Breaker)

Arsitektur ini memisahkan ketersediaan fungsionalitas aplikasi dari siklus hidup deployment. Konfigurasi remote di-fetch saat cold start atau interval background tertentu dengan strategi stale-while-revalidate lokal.

Remote switch harus bersifat fail-safe: jika request remote config gagal (timeout/offline), aplikasi harus menggunakan cache lokal terakhir atau nilai default yang aman (biasanya state non-aktif untuk fitur baru atau fallback legacy).

Status Circuit Breaker Komponen

  • Closed (Normal): Fitur aktif. Remote switch bernilai true. Komponen baru dirender.
  • Open (Tripped/Killed): Remote switch bernilai false atau error boundary lokal mendeteksi crash berulang. Render langsung dialihkan ke fallback component.
  • Half-Open (Canary/Recovery): Flag diaktifkan bertahap berdasarkan user ID hash (persentase rollout) untuk memverifikasi kestabilan patch.

Implementasi Hook dan Boundary Wrapper

Implementasi berikut mengombinasikan integrasi remote flag dan React Error Boundary lokal untuk mengisolasi failure domain per modul.

1. Hook Akses Remote Config: useFeatureGate

import { useState, useEffect } from 'react';

// Abstraksi client config (Firebase Remote Config / LaunchDarkly / Custom API)
import { remoteConfigClient } from '../services/remoteConfig';

export const useFeatureGate = (gateKey: string, defaultValue = false): boolean => {
  const [isEnabled, setIsEnabled] = useState<boolean>(() => {
    // ponytail: fallback ke cache lokal sinkron sebelum network fetch selesai
    return remoteConfigClient.getBooleanFast(gateKey) ?? defaultValue;
  });

  useEffect(() => {
    let isMounted = true;
    remoteConfigClient.fetchAndActivate()
      .then(() => {
        if (isMounted) {
          setIsEnabled(remoteConfigClient.getBooleanFast(gateKey));
        }
      })
      .catch(() => {
        // Network gagal: pertahankan nilai cache/default
      });

    return () => { isMounted = false; };
  }, [gateKey]);

  return isEnabled;
};
// skipped: dynamic socket updates, add when sub-minute instant kill required.

2. Wrapper: FeatureGateBoundary

Komponen boundary menangkap unhandled JavaScript error secara lokal agar modul lain tetap berjalan, sekaligus mengevaluasi kill switch.

import React, { Component, ErrorInfo, ReactNode } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { recordBoundaryError } from '../services/observability';

interface Props {
  gateKey: string;
  isEnabled: boolean;
  fallback?: ReactNode;
  children: ReactNode;
}

interface State {
  hasError: boolean;
}

export class FeatureGateBoundary extends Component<Props, State> {
  public state: State = { hasError: false };

  public static getDerivedStateFromError(_: Error): State {
    return { hasError: true };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    recordBoundaryError(this.props.gateKey, error, errorInfo);
  }

  public render() {
    const { isEnabled, fallback, children } = this.props;
    const defaultFallback = (
      <View style={styles.fallbackContainer}>
        <Text style={styles.fallbackText}>Fitur sedang dalam pemeliharaan.</Text>
      </View>
    );

    // Circuit Breaker aktif via remote switch ATAU component crash lokal
    if (!isEnabled || this.state.hasError) {
      return fallback !== undefined ? fallback : defaultFallback;
    }

    return children;
  }
}

const styles = StyleSheet.create({
  fallbackContainer: {
    padding: 16,
    alignItems: 'center',
    justifyContent: 'center',
  },
  fallbackText: {
    color: '#666',
    fontSize: 14,
  },
});

3. Penggunaan pada Modul Transaksi

import React from 'react';
import { useFeatureGate } from '../hooks/useFeatureGate';
import { FeatureGateBoundary } from '../components/FeatureGateBoundary';
import { NewCheckoutModule } from './NewCheckoutModule';
import { LegacyCheckoutModule } from './LegacyCheckoutModule';

export const CheckoutScreen = () => {
  const isNewCheckoutActive = useFeatureGate('killswitch_checkout_v2', false);

  return (
    <FeatureGateBoundary 
      gateKey="checkout_v2"
      isEnabled={isNewCheckoutActive}
      fallback={<LegacyCheckoutModule />}
    >
      <NewCheckoutModule />
    </FeatureGateBoundary>
  );
};

Observability: Monitoring Real-Time & Breadcrumbs

Remote switch tidak berguna tanpa instrumen deteksi cepat. Data crash harus dikelompokkan berdasarkan konteks fitur.

Breadcrumb dan Metadata Logging

Ketika exception tertangkap oleh FeatureGateBoundary, kirim payload terstruktur ke monitoring tool (misal: Sentry, Datadog):

// services/observability.ts
import * as Sentry from '@sentry/react-native';

export const recordBoundaryError = (gateKey: string, error: Error, errorInfo: React.ErrorInfo) => {
  Sentry.withScope((scope) => {
    scope.setTag('feature_gate', gateKey);
    scope.setTag('circuit_state', 'tripped');
    scope.setExtra('componentStack', errorInfo.componentStack);
    scope.setLevel('fatal');
    
    Sentry.captureException(error);
  });
};

Automated Trigger Kill Switch

Sistem ideal tidak menunggu manusia menekan tombol switch secara manual. Alur otomatisasi:

  1. Alert Manager memantau threshold crash: count() > 50 dalam 1 menit pada tag feature_gate: checkout_v2.
  2. Webhook memicu serverless function / script CI yang mengeksekusi Remote Config API.
  3. Nilai killswitch_checkout_v2 diubah menjadi false via REST API remote config provider. Client mengunduh flag terbaru pada polling terdekat.

Template Postmortem Insiden

Gunakan format ringkas ini untuk evaluasi teknis pasca-insiden:

# INCIDENT POSTMORTEM: [INC-2024-X] Crash Layar Checkout Pasca Rilis v3.4.0

## Ringkasan
- Tanggal: 2024-10-24
- Dampak: 12% user gagal menyelesaikan checkout (crash instan saat klik 'Bayar').
- Total Downtime / Degradasi: 8 menit hingga kill switch aktif.
- MTTR: 8 menit (vs estimasi 4-6 jam jika via rilis biner darurat).

## Timeline
- 14:00 - Deploy OTA Bundle v3.4.0-patch1 live.
- 14:03 - Lonjakan alert Sentry: 'TypeError: Cannot read property id of undefined'.
- 14:05 - On-call engineer mengidentifikasi crash terlokalisir di Checkout V2.
- 14:08 - Trigger kill switch: 'killswitch_checkout_v2' diset ke false.
- 14:09 - Crash rate kembali ke 0.01%. User dialihkan otomatis ke Legacy Checkout.

## Root Cause Analysis (5 Whys)
1. User mengalami crash saat submit: Objek 'paymentMethod' bernilai null.
2. Kenapa null: API gateway mengembalikan format payload baru untuk tipe e-wallet tertentu.
3. Kenapa client tidak handle: Null-check tidak diterapkan pada helper parsePaymentData.
4. Kenapa lolos QA: Integrasi test mock data hanya menguji response kartu kredit standard.
5. Kenapa tidak terdeteksi pre-release: Kurangnya automated test untuk edge-case backward compatibility.

## Action Items & Pencegahan
1. [P0] Tambahkan defensive parsing pada parsePaymentData (ETA: Hari ini).
2. [P1] Terapkan Canary Rollout bertahap (10% -> 25% -> 100%) untuk update remote config fitur baru.
3. [P1] Tambahkan automated unit test pada level boundary wrapper sebelum merge ke main.

Pencegahan: Canary Rollout & Automated Testing

Kill switch adalah jaring pengaman, bukan pengganti praktik rilis yang sehat. Terapkan strategi berikut:

  • Percentage-based Rollout: Jangan aktifkan fitur 100% langsung. Lepas ke 5% user internal/beta, amati crash metrics selama 2 jam, lalu naikkan bertahap.
  • Automated Feature Gate Testing: Jalankan unit test komponen React Native dalam dua kondisi state (enabled dan disabled) untuk memverifikasi bahwa fallback UI tetap berfungsi dan tidak ikut crash saat switch dibuka.