diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d68e0f9a..4c9ae11a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -146,6 +146,14 @@ jobs: app/build/outputs/apk/preRelease/app-preRelease.apk \ "app/build/outputs/apk/preRelease/$PRERELEASE_APK_NAME" + - name: Upload Preview Sourcemap + uses: actions/upload-artifact@v4 + with: + name: ${{ env.PRERELEASE_ARTIFACT_NAME }}-sourcemap + path: android/app/build/generated/sourcemaps/react/preRelease/index.android.bundle.map + if-no-files-found: error + retention-days: 14 + compression-level: 0 - name: Upload Preview APK id: upload-nightly uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4428d914..42c314c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -183,6 +183,11 @@ jobs: - name: Package Release APKs run: pnpm release:package "$RELEASE_VERSION" + - name: Upload Sourcemaps + run: | + cp android/app/build/generated/sourcemaps/react/release/index.android.bundle.map \ + "release-artifacts/LNReader-v${RELEASE_VERSION}-sourcemap.map" + - name: Verify APK Signatures run: | APKSIGNER="$(find "$ANDROID_HOME/build-tools" -type f -name apksigner | diff --git a/DEBUG_PRODUCTION.md b/DEBUG_PRODUCTION.md new file mode 100644 index 00000000..25e1f344 --- /dev/null +++ b/DEBUG_PRODUCTION.md @@ -0,0 +1,73 @@ +# Debugging Production Builds + +Symbolicate production stack traces from LNReader using uploaded sourcemaps. + +## Prerequisites + +- **Node.js** (v18 or later) +- The **sourcemap** matching the crashing APK version +- A **stack trace** from either `adb logcat` (device attached) or pasted from a Discord / GitHub issue report + +## Getting the Sourcemap + +### Release builds + +Sourcemaps are uploaded as GitHub release assets. Download the one matching the crashing version: + +``` +LNReader-v{version}-sourcemap.map +``` + +Example for `v2.5.0`: + +1. Go to the [releases page](https://github.com/lnreader/lnreader/releases) +2. Find release `v2.5.0` +3. Under **Assets**, download `LNReader-v2.5.0-sourcemap.map` + +### Preview / nightly builds + +Sourcemaps are uploaded as a separate artifact alongside the preview APK, named `LNReader-{sha}-sourcemap`. + +1. Go to the [Actions tab](https://github.com/lnreader/lnreader/actions) → **Build Preview** +2. Find the workflow run matching the build hash +3. Under **Artifacts**, download the one with the `-sourcemap` suffix + +## Symbolicating a Stack Trace + +Save the raw stack trace to a file (from `adb logcat`, or paste from Discord/GitHub into `trace.txt`), then run: + +```bash +npx metro-symbolicate < trace.txt +``` + +Or pipe it directly: + +```bash +adb logcat -d | npx metro-symbolicate +``` + +## Example + +Before (Hermes-obfuscated): + +``` +com.lnreader E Error: Unexpected token + at 93795 (address at index.js:1:729193) + at 28471 (address at index.js:1:219482) + at 10384 (address at index.js:1:81729) +``` + +After symbolication: + +``` +com.lnreader E Error: Unexpected token + at handleResponse (src/api/plugins.ts:120:12) + at fetchPlugin (src/hooks/usePlugins.ts:45:8) + at getPluginList (src/screens/browse/BrowseScreen.tsx:89:20) +``` + +## Troubleshooting + +- **Version mismatch**: the sourcemap must match the exact APK build. A mismatched version produces wrong file names and line numbers. +- **No sourcemap artifact**: verify the workflow that built the APK ran successfully and that the "Upload Sourcemaps" / "Upload Preview Sourcemap" step completed without errors. +- **Hermes bytecode**: Hermes compiles JS to bytecode. The sourcemap is generated during this step and maps bytecode offsets back to source. `metro-symbolicate` handles this transparently. diff --git a/plugins/android/android-build-types.gradle b/plugins/android/android-build-types.gradle index 4e3b8228..afe10204 100644 --- a/plugins/android/android-build-types.gradle +++ b/plugins/android/android-build-types.gradle @@ -3,6 +3,10 @@ def releaseAbiSplitsEnabled = providers .map { value -> value.toBoolean() } .getOrElse(false) +react { + hermesFlags = ["-O", "-output-source-map"] +} + android { splits { abi { diff --git a/src/components/AppErrorBoundary/AppErrorBoundary.tsx b/src/components/AppErrorBoundary/AppErrorBoundary.tsx index 95e3f407..9085ef56 100644 --- a/src/components/AppErrorBoundary/AppErrorBoundary.tsx +++ b/src/components/AppErrorBoundary/AppErrorBoundary.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { StyleSheet, View, Text, StatusBar } from 'react-native'; import ErrorBoundary from 'react-native-error-boundary'; - +import * as Clipboard from 'expo-clipboard'; +import { getString } from '@i18n/translations'; +import { showToast } from '@utils/showToast'; import { Button, List } from '@components'; import { useTheme } from '@hooks/persisted'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -17,6 +19,33 @@ export const ErrorFallback: React.FC = ({ }) => { const theme = useTheme(); + const fallbackGetString = ( + key: Parameters[0], + fallback: string, + options?: Parameters[1], + ) => { + try { + return getString(key, options); + } catch { + return fallback; + } + }; + + const handleCopyStackTrace = async () => { + try { + await Clipboard.setStringAsync(`${error.message}\n\n${error.stack}`); + showToast( + fallbackGetString( + 'common.copiedToClipboard', + 'Copied to clipboard: Stack trace', + { name: 'Stack trace' }, + ), + ); + } catch { + // clipboard failure is non-critical + } + }; + return ( = ({ - An Unexpected Error Ocurred + {fallbackGetString( + 'errorBoundary.title', + 'An Unexpected Error Occurred', + )} - The application ran into an unexpected error. We suggest you - screenshot this message and then share it in our support channel on - Discord. + {fallbackGetString( + 'errorBoundary.description', + 'The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.', + )} = ({ +