Masalah Un-symbolicated Stack Trace di Production

Saat aplikasi React Native berjalan di environment production Android, penelusuran akar masalah (root cause analysis) dari crash log sering terhambat oleh dua lapisan transformasi kode: optimasi bytecode Hermes pada layer JavaScript dan obfuskasi ProGuard/R8 pada layer native Android.

Ketika crash terjadi pada runtime JavaScript, Sentry hanya menerima referensi alamat memori bytecode (misalnya bytecode offset 0x0001a2b4) atau satu baris bundle terkompresi (index.android.bundle:1:24510) alih-alih file sumber TypeScript asli beserta nomor barisnya. Di sisi lain, pengecualian native Android menampilkan stack trace dengan nama kelas dan method terpotong, seperti a.b.c.d(SourceFile:1). Tanpa artefak pemetaan (mapping files), proses deobfuskasi dan simbolikasi log menjadi mustahil dilakukan.

Arsitektur Source Map Hermes: Komposisi Dua Tahap

Hermes tidak mengeksekusi plain JavaScript secara langsung di runtime. Metro bundler terlebih dahulu mengompilasi kode TypeScript/ES6 menjadi JavaScript bundle tunggal, kemudian Hermes Compiler (hermesc) mengompilasi JavaScript bundle tersebut menjadi Hermes Bytecode (HBC). Proses ini menghasilkan dua map terpisah:

  1. Packager Source Map: Memetakan kode sumber TypeScript/JavaScript asli ke plain JavaScript bundle Metro.
  2. Hermes Compiler Source Map: Memetakan plain JavaScript bundle Metro ke bytecode Hermes.

Agar Sentry dapat melacak error dari bytecode Hermes kembali ke file TypeScript orisinal, kedua file tersebut harus digabungkan menggunakan script bawaan React Native: compose-source-maps.js.

# Alur komposisi source map manual/headless
node node_modules/react-native/scripts/compose-source-maps.js \
  /tmp/index.android.bundle.packager.map \
  /tmp/index.android.bundle.hbc.map \
  -o /tmp/index.android.bundle.map

Konfigurasi Pipeline GitHub Actions

Berikut adalah workflow GitHub Actions yang mengompilasi aplikasi Android release, menghasilkan Hermes source maps komposit, dan mengunggah artefak tersebut bersama file ProGuard mapping.txt ke Sentry.

name: Android Release & Sentry Upload

on:
  push:
    tags:
      - 'v*'

jobs:
  build-android:
    runs-on: ubuntu-latest
    env:
      SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
      SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
      SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'yarn'

      - name: Setup Java JDK
        uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: 17

      - name: Install Dependencies
        run: yarn install --frozen-lockfile

      - name: Install Sentry CLI
        run: curl -sL https://sentry.io/get-cli/ | bash

      - name: Set Release Version Name
        run: |
          VERSION_NAME=$(node -p "require('./package.json').version")
          echo "RELEASE_VERSION=my.app.package@${VERSION_NAME}" >> $GITHUB_ENV
          echo "DIST_VERSION=${{ github.run_number }}" >> $GITHUB_ENV

      - name: Build Android Bundle (AAB) & Mapping
        working-directory: android
        run: ./gradlew bundleRelease --no-daemon

      - name: Upload ProGuard / R8 Mapping to Sentry
        run: |
          sentry-cli upload-proguard \
            --org "$SENTRY_ORG" \
            --project "$SENTRY_PROJECT" \
            --app-id "my.app.package" \
            --version "$RELEASE_VERSION" \
            android/app/build/outputs/mapping/release/mapping.txt

      - name: Bundle JS and Hermes Source Maps
        run: |
          mkdir -p build/sentry
          
          # 1. Generate Metro JS Bundle & Packager Map
          npx react-native bundle \
            --platform android \
            --dev false \
            --entry-file index.js \
            --bundle-output build/sentry/index.android.bundle \
            --sourcemap-output build/sentry/index.android.bundle.packager.map

          # 2. Compile ke Hermes Bytecode dan generate Hermes Map
          HERMESC_BIN="node_modules/react-native/sdks/hermesc/linux64-bin/hermesc"
          $HERMESC_BIN -emit-binary \
            -out build/sentry/index.android.bundle.hbc \
            build/sentry/index.android.bundle \
            -O -output-source-map

          # 3. Gabungkan kedua Source Map
          node node_modules/react-native/scripts/compose-source-maps.js \
            build/sentry/index.android.bundle.packager.map \
            build/sentry/index.android.bundle.hbc.map \
            -o build/sentry/index.android.bundle.map

      - name: Create Sentry Release & Upload Source Maps
        run: |
          sentry-cli releases new "$RELEASE_VERSION"
          sentry-cli releases set-commits "$RELEASE_VERSION" --auto
          
          sentry-cli sourcemaps upload \
            --org "$SENTRY_ORG" \
            --project "$SENTRY_PROJECT" \
            --release "$RELEASE_VERSION" \
            --dist "$DIST_VERSION" \
            --strip-prefix "$PWD" \
            build/sentry/index.android.bundle build/sentry/index.android.bundle.map

          sentry-cli releases finalize "$RELEASE_VERSION"

Opsi Alternatif: Sentry Android Gradle Plugin

Penggunaan script manual pada pipeline di atas memberikan kontrol penuh dan dependensi minimal terhadap plugin native. Namun, ada opsi yang lebih ringkas: menggunakan @sentry/react-native yang terintegrasi dengan Gradle plugin.

Alternatif Ringkas: Aktifkan sentry-android-gradle-plugin di android/app/build.gradle. Plugin ini secara otomatis mengaitkan task bundleReleaseJsAndAssets dan minifyReleaseWithR8 untuk mengunggah ProGuard dan Hermes map langsung selama proses kompilasi Gradle.

Gunakan pendekatan skrip manual jika Anda menggunakan custom build pipeline, monorepo dengan isolated build container, atau memisahkan tahapan kompilasi native dan JavaScript bundle untuk efisiensi caching CI.

Validasi Simbolikasi Crash Log

Untuk memastikan setup berhasil sebelum merilis ke publik, lakukan pengujian crash sintetis di sisi TypeScript dan Java/Kotlin:

// Test Crash JavaScript (App.tsx)
import * as Sentry from '@sentry/react-native';

export function CrashButton() {
  return (
    <Button
      title="Trigger JS Crash"
      onPress={() => {
        throw new Error('Test Hermes Symbolication Error');
      }}
    />
  );
}

Periksa hasil event di dashboard Sentry:

  • Buka menu Issues > pilih event error yang ditrigger.
  • Periksa tab Tags: pastikan release dan dist sesuai dengan variabel CI yang dikirimkan.
  • Periksa stack trace: jika simbolikasi berhasil, Sentry menampilkan referensi file App.tsx:8 dengan cuplikan baris kode asli, bukan index.android.bundle atau representasi bytecode mentah.
  • Buka Project Settings > Source Maps untuk memverifikasi bahwa file index.android.bundle.map telah terindeks dengan status valid.
  • Buka Project Settings > ProGuard untuk memastikan UUID mapping file ProGuard terdaftar.