feat: Upload JavaScript Source Maps (#1924)
* init * fix paths * add hermes flag
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user