Aplikasi monolitik yang mengadopsi stack modern seperti Laravel dengan Inertia.js dan Vite memadukan arsitektur server-driven dengan routing berbasis single-page application (SPA). Saat pengguna bernavigasi antar-halaman, Inertia memuat komponen tampilan secara asynchronous melalui dynamic import (JavaScript code-splitting). Masalah kritis muncul ketika deployment versi baru menghapus file chunk versi sebelumnya saat pengguna masih aktif membuka aplikasi: navigasi klien seketika gagal karena aset yang diminta menghasilkan HTTP 404.

Akar Masalah: In-Place Deployment dan Broken Dynamic Imports

Saat bundler seperti Vite atau Webpack melakukan proses build, nama file aset diinjeksi dengan content hash unik, misalnya Dashboard-B3a9f1.js. Pola rilis in-place (seperti menjalankan git pull && npm run build langsung di direktori aktif server produksi) secara otomatis membersihkan direktori public/build lama sebelum meletakkan aset ber-hash baru, misalnya Dashboard-C8x2d0.js.

Jika klien membuka aplikasi pada build v1, DOM klien masih menyimpan manifest atau referensi chunk v1. Ketika pengguna mengklik link navigasi yang mengarah ke komponen baru:

  1. Router Inertia memanggil dynamic import browser: import('/build/assets/Dashboard-B3a9f1.js').
  2. Server/CDN mengembalikan respons HTTP 404 Not Found karena file tersebut telah terhapus oleh proses build baru (v2).
  3. Engine JavaScript browser melemparkan exception fatal: TypeError: Failed to fetch dynamically imported module atau ChunkLoadError.
  4. Aplikasi terhenti (freeze) tanpa transisi halaman, menurunkan pengalaman pengguna secara drastis.

Observasi Real-Time: Deteksi Spike 404 dan Exception

Mendeteksi kegagalan transisi rilis menuntut visibilitas pada dua sisi: edge/web server dan client runtime monitoring.

1. Filter Log Nginx / CDN

Spike HTTP 404 pada path aset statis adalah indikator primer kegagalan chunk loading. Pisahkan logging aset dari log aplikasi umum untuk menghindari noise log bot/crawler:

# /etc/nginx/conf.d/app.conf
map $status $is_asset_404 {
    ~^404 1;
    default 0;
}

server {
    listen 80;
    server_name example.com;
    root /var/www/app/public;

    location /build/assets/ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        try_files $uri =404;
        
        # Log khusus request aset gagal
        access_log /var/log/nginx/chunk_misses.log combined if=$is_asset_404;
    }
}

Monitor menggunakan CLI secara real-time saat deployment berlangsung:

tail -f /var/log/nginx/chunk_misses.log | grep -E "GET /build/assets/.*\.js"

2. Telemetri Frontend via Sentry

Konfigurasikan client-side reporting untuk menangkap kegagalan import modul. Vite melemparkan event khusus vite:preloadError ketika browser gagal memuat chunk dinamis:

// resources/js/app.js
import * as Sentry from "@sentry/browser";

window.addEventListener('vite:preloadError', (event) => {
    Sentry.captureException(event, {
        tags: {
            type: 'chunk_load_failure',
            current_url: window.location.href,
        },
        extra: {
            payload: event.payload?.message || 'Failed module fetch'
        }
    });
});

Mitigasi Runtime: Client Fallback Handler

Penanganan runtime mencegah pengguna terjebak di state rusak. Ketika chunk gagal dimuat, eksekusi reload penuh (hard refresh) agar browser memuat index HTML dan manifest build terbaru dari server.

Gunakan pengaman sessionStorage agar tidak terjadi infinite reload loop jika server memang sedang offline atau mengalami down secara menyeluruh:

// resources/js/app.js
import { router } from '@inertiajs/vue3'; // atau @inertiajs/react

// 1. Tangkap error preload Vite
window.addEventListener('vite:preloadError', (event) => {
    event.preventDefault();
    handleStaleAssets();
});

// 2. Tangkap invalid response dari event router Inertia
router.on('invalid', (event) => {
    const status = event.detail.response?.status;
    if (status === 404 || status === 409) {
        handleStaleAssets();
    }
});

function handleStaleAssets() {
    const key = 'chunk_reload_lock';
    const lastAttempt = sessionStorage.getItem(key);
    const now = Date.now();

    // Izinkan hard reload sekali per 10 detik
    if (!lastAttempt || now - parseInt(lastAttempt, 10) > 10000) {
        sessionStorage.setItem(key, now.toString());
        window.location.reload();
    } else {
        console.error('Asset retrieval permanently failed. Manual refresh required.');
    }
}

Pencegahan di Sisi Build & Deployment

Client reload hanyalah mekanisme penyelamat (failsafe). Masalah struktural harus diselesaikan dengan mempertahankan file chunk lintas rilis (multi-build retention).

1. Nonaktifkan emptyOutDir pada Vite

Secara default, Vite mengosongkan folder public/build saat kompilasi. Ubah konfigurasi agar chunk lama tetap tersimpan di disk, lalu bersihkan chunk lama secara periodik melalui retensi terencana:

// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
    build: {
        emptyOutDir: false, // Pertahankan chunk lama di public/build
        manifest: 'manifest.json',
        rollupOptions: {
            output: {
                chunkFileNames: 'assets/[name]-[hash].js',
                entryFileNames: 'assets/[name]-[hash].js',
                assetFileNames: 'assets/[name]-[hash].[ext]'
            }
        }
    },
});

2. Strategi Retensi Cleanup CLI

Gunakan cron job atau pipeline CI/CD untuk menghapus chunk yang sudah berumur lebih dari 48 jam, memberikan waktu transisi yang cukup bagi sesi pengguna aktif:

find /var/www/app/public/build/assets -type f -name "*.js" -mtime +2 -delete

SOP Safe Rollback: Sinkronisasi Backend & Frontend

Rollback monolit Inertia berisiko tinggi jika frontend dan backend tidak di-revert secara atomik. Jika backend kembali ke skema lama sementara manifest frontend tertinggal di versi baru, Inertia Version Conflict akan memicu force reload terus-menerus.

Struktur Atomic Symlink (Release Directories)

Terapkan release flow menggunakan struktur symlink (standar Deployer/Capistrano):

/var/www/app/
├── releases/
│   ├── 20260330100000/  (v1)
│   └── 20260330120000/  (v2 - rusak)
├── shared/
│   ├── .env
│   └── storage/
└── current -> /var/www/app/releases/20260330120000/

Checklist Prosedur Rollback

  1. Identifikasi Status Migrasi DB: Pastikan rollback kode tidak merusak skema database aktif. Jika rilis mengandung migration non-destructive (misal: penambahan kolom), database tidak perlu di-rollback seketika.
  2. Alihkan Symlink Current:
    ln -nfs /var/www/app/releases/20260330100000 /var/www/app/current
  3. Sinkronkan Inertia Version Hash: Inertia menggunakan asset versioning untuk mendeteksi perbedaan versi. Jalankan pembersihan cache framework di release target:
    php /var/www/app/current/artisan optimize:clear
    php /var/www/app/current/artisan view:cache
    sudo systemctl reload php8.x-fpm
    sudo systemctl reload nginx
  4. Verifikasi Endpoint: Pastikan header X-Inertia-Version yang dikembalikan oleh server cocok dengan manifest aset pada rilis v1.

Postmortem Insiden: Transisi Rilis v2.4.0

Ringkasan: Terjadi lonjakan ChunkLoadError selama 18 menit pasca-deployment rilis v2.4.0 pada 30 Maret 2026, menyebabkan 12% sesi pengguna aktif mengalami kegagalan navigasi SPA.

Timeline Insiden

  • 14:00 UTC: Pipeline CI/CD menjalankan build langsung pada folder public/build server produksi. Direktori dibersihkan oleh Vite.
  • 14:02 UTC: Sentry memicu peringatan lonjakan exception Failed to fetch dynamically imported module.
  • 14:05 UTC: Nginx log mencatat 1.840 request berstatus 404 pada endpoint /build/assets/OrderIndex-*.js.
  • 14:11 UTC: Tim DevOps mendiagnosis in-place build mengeliminasi file chunk build v2.3.9.
  • 14:18 UTC: Diterapkan hotfix sync: menyalin aset folder build v2.3.9 dari artefak CI kembali ke folder public/build/assets/ server. Metrik 404 kembali ke baseline (0%).

Action Items

  • Build Pipeline: Set build.emptyOutDir: false pada Vite config untuk mempertahankan minimal 3 versi bundle terakhir di disk.
  • Application Layer: Pasang global event listener vite:preloadError di app.js dengan fallback hard reload terproteksi limit sesi.
  • Infrastructure: Migrasi sistem deployment dari in-place git pull ke symlink-based deployment zero-downtime.