feat: Upload JavaScript Source Maps (#1924)

* init

* fix paths

* add hermes flag
This commit is contained in:
Patrick Loser
2026-07-29 17:28:00 +02:00
committed by GitHub
parent ecc5b84a64
commit b7639ab55a
7 changed files with 155 additions and 6 deletions
+8
View File
@@ -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
+5
View File
@@ -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 |
+73
View File
@@ -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 <sourcemap.map> < trace.txt
```
Or pipe it directly:
```bash
adb logcat -d | npx metro-symbolicate <sourcemap.map>
```
## 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.
@@ -3,6 +3,10 @@ def releaseAbiSplitsEnabled = providers
.map { value -> value.toBoolean() }
.getOrElse(false)
react {
hermesFlags = ["-O", "-output-source-map"]
}
android {
splits {
abi {
@@ -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<ErrorFallbackProps> = ({
}) => {
const theme = useTheme();
const fallbackGetString = (
key: Parameters<typeof getString>[0],
fallback: string,
options?: Parameters<typeof getString>[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 (
<SafeAreaView
style={[styles.mainCtn, { backgroundColor: theme.background }]}
@@ -24,12 +53,16 @@ export const ErrorFallback: React.FC<ErrorFallbackProps> = ({
<StatusBar translucent={true} backgroundColor="transparent" />
<View style={styles.errorInfoCtn}>
<Text style={[styles.errorTitle, { color: theme.onSurface }]}>
An Unexpected Error Ocurred
{fallbackGetString(
'errorBoundary.title',
'An Unexpected Error Occurred',
)}
</Text>
<Text style={[styles.errorDesc, { color: theme.onSurface }]}>
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.',
)}
</Text>
<Text
style={[
@@ -45,9 +78,21 @@ export const ErrorFallback: React.FC<ErrorFallbackProps> = ({
</Text>
</View>
<List.Divider theme={theme} />
<Button
onPress={handleCopyStackTrace}
title={fallbackGetString(
'errorBoundary.copyStackTrace',
'Copy stack trace',
)}
style={styles.copyButtonCtn}
mode="outlined"
/>
<Button
onPress={resetError}
title={'Restart the application'}
title={fallbackGetString(
'errorBoundary.restart',
'Restart the application',
)}
style={styles.buttonCtn}
mode="contained"
/>
@@ -72,6 +117,10 @@ const styles = StyleSheet.create({
margin: 16,
marginBottom: 32,
},
copyButtonCtn: {
margin: 16,
marginBottom: 8,
},
errorCtn: {
borderRadius: 8,
lineHeight: 20,
+6
View File
@@ -364,6 +364,12 @@
"pluginNotFound": "Plugin not found!",
"removeDownloadsWarning": "Are you sure? All downloaded chapters will be deleted."
},
"errorBoundary": {
"title": "An Unexpected Error Occurred",
"description": "The application ran into an unexpected error. Please copy the stack trace below and share it on our Discord support channel.",
"copyStackTrace": "Copy stack trace",
"restart": "Restart the application"
},
"generalSettings": "General",
"generalSettingsScreen": {
"asc": "(Ascending)",
+4
View File
@@ -312,6 +312,10 @@ export interface StringMap {
'downloadScreen.noDownloads': 'string';
'downloadScreen.pluginNotFound': 'string';
'downloadScreen.removeDownloadsWarning': 'string';
'errorBoundary.title': 'string';
'errorBoundary.description': 'string';
'errorBoundary.copyStackTrace': 'string';
'errorBoundary.restart': 'string';
'generalSettings': 'string';
'generalSettingsScreen.asc': 'string';
'generalSettingsScreen.autoDownload': 'string';