Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ ios/.swiftpm/
backend/dist
nodejs-assets
android/src/debug/assets/
android/src/debug/nodejs-sourcemaps/
android/src/main/assets/
android/src/main/jniLibs/
android/src/main/nodejs-sourcemaps/
Expand Down
17 changes: 17 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ Re-run `npm run setup` after pulling changes that bump the nodejs-mobile version
or touch the backend. The individual steps are also available on their own
(`npm run download:nodejs-mobile`, `npm run backend:build`).

### Backend sourcemaps

`backend:build` symbolicates backend errors two ways, split by build type so
release artifacts stay lean:

- **Debug builds** ship the bundle's sourcemap colocated with it and pass Node's
`--enable-source-maps` flag (Android `BuildConfig.DEBUG`, iOS `#if DEBUG`), so
errors are remapped to original positions in-process and reach Sentry already
symbolicated — no upload or Sentry auth token needed. On Android the map rides
the `src/debug/` asset tree (debug variants only); on iOS the config plugin
adds a Debug-only `ComapeoCoreSourcemaps` companion pod
(`:configurations => ['Debug']`) whose maps merge next to the bundle in the
app — no custom build phase.
- **Release builds** keep the map out of the app entirely; the consuming app
uploads it to Sentry from CI with `comapeo-rn-upload-sourcemaps` (debug-ID
matched, symbolicated server-side).

## Repository layout

This is a single published npm package with its build tooling and test apps in
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,12 +355,15 @@ Re-uploading is idempotent (Sentry de-dupes by debug ID). The CLI finds
`@sentry/cli` via `@sentry/react-native`'s dependency chain — if you don't have
`@sentry/react-native` installed, add `@sentry/cli` to your devDependencies.
`--targets <list>` restricts the upload to a subset of
`android-debug, android-main, ios`; `--url` points at self-hosted Sentry;
`android-main, ios`; `--url` points at self-hosted Sentry;
`SENTRY_ORG` / `SENTRY_PROJECT` work in place of the flags.

The backend sourcemaps live in sibling `nodejs-sourcemaps/` directories (not
under the bundled `nodejs-project/` assets), so they are **not** shipped inside
your APK/IPA — no exclusion step is needed to keep them off the device.
Only **release** builds need this upload. Debug builds ship the backend
sourcemap alongside the bundle and enable Node's `--enable-source-maps`, so
backend errors are symbolicated in-process — no upload or Sentry auth token
required. In release builds the maps live in sibling `nodejs-sourcemaps/`
directories (not under the bundled `nodejs-project/` assets), so they are
**not** shipped inside your release APK/IPA.

#### JS bundle sourcemaps and native debug symbols (your app)

Expand Down
13 changes: 11 additions & 2 deletions android/src/main/java/com/comapeo/core/NodeJSService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,17 @@ class NodeJSService(
// 5th positional: consumer's online map style URL, or "" when unset.
val defaultOnlineStyleUrl =
SentryConfig.readApplicationMetaDataString(this, META_DEFAULT_ONLINE_STYLE_URL) ?: ""
val args = mutableListOf(
"node",
val args = mutableListOf("node")
// Debug builds ship the backend's `.map` colocated with the bundle
// (src/debug only). `--enable-source-maps` (a Node runtime flag, so
// it must precede the script path) makes Node remap stacks to
// original positions in-process, so Sentry events are symbolicated
// without a map upload. Release builds omit it and rely on
// consumer-uploaded maps (debug-ID matched, symbolicated by Sentry).
if (BuildConfig.DEBUG) {
args += "--enable-source-maps"
}
args += listOf(
entryPath,
comapeoSocketFile.absolutePath,
controlSocketFile.absolutePath,
Expand Down
40 changes: 39 additions & 1 deletion app.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import configPlugins from "@expo/config-plugins";
import { createRequire } from "node:module";
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { isAbsolute, join } from "node:path";
import { dirname, isAbsolute, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
const { withAndroidManifest } = configPlugins;
const { withMainApplication } = configPlugins;
const { withInfoPlist } = configPlugins;
Expand Down Expand Up @@ -95,6 +96,12 @@ function withComapeoCore(config, props) {
config = withSentryAndroid(config, sentry, moduleIdent);
config = withSentryIos(config, sentry);
config = withSentryLibraryEvolution(config);
// iOS Debug builds: ship the backend sourcemaps next to the bundle so
// Node's `--enable-source-maps` (passed by NodeJSService under #if DEBUG)
// symbolicates backend errors in-process — no Sentry upload needed.
// Release builds skip the copy (the bundle's map stays out of the IPA).
// Android handles the debug case via its `src/debug/` asset tree instead.
config = withDebugSourcemapsIos(config);
// Optional default project config (presets/categories) supplied by the
// consuming app. The module no longer ships @comapeo/default-categories;
// when this prop is absent, new projects get no default config.
Expand Down Expand Up @@ -413,6 +420,37 @@ function withSentryLibraryEvolution(config) {
});
}

// Adds the Debug-only `ComapeoCoreSourcemaps` companion pod to the consumer's
// Podfile. `:configurations => ['Debug']` makes CocoaPods copy its resources
// (the backend `.map` files, shaped as a `nodejs-project` dir) only in Debug
// builds — they merge next to the bundle in the `.app`, where Node resolves
// them via `--enable-source-maps` (passed by NodeJSService under #if DEBUG).
// Release builds never include the pod, so the IPA stays lean.
//
// The pod is added explicitly here, never autolinked — see
// `ComapeoCoreSourcemaps.podspec` and the pinned `apple.podspecPath` in
// `expo-module.config.json`. `:path` is computed per-consumer so it survives
// monorepo node_modules hoisting.
function withDebugSourcemapsIos(config) {
return withPodfile(config, (cfg) => {
const podDir = join(dirname(fileURLToPath(import.meta.url)), "ios");
const relPodPath =
relative(cfg.modRequest.platformProjectRoot, podDir) || ".";
const podLine =
` pod 'ComapeoCoreSourcemaps', :path => '${relPodPath}', ` +
`:configurations => ['Debug']`;
cfg.modResults.contents = mergeContents({
tag: "comapeo-core-debug-sourcemaps",
src: cfg.modResults.contents,
newSrc: podLine,
anchor: /use_expo_modules!/,
offset: 1,
comment: "#",
}).contents;
return cfg;
});
}

/**
* Module version label + bundled-backend dep map — the same values
* `src/version.ts` exposes to the RN-side `initSentry`. Used only on
Expand Down
19 changes: 11 additions & 8 deletions backend/rolldown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ const IOS_OUT = process.env.OUTPUT_DIR_IOS ?? path.join(__dirname, "dist/ios");
* For the production build these are passed by `scripts/build-backend.ts`;
* the fallbacks keep the standalone `cd backend && npm run build` case
* working — maps land in `<outDir>-sourcemaps/` next to the bundle dir.
*
* Note: the Android *debug* output is deliberately absent — its map stays
* colocated with the bundle (shipped in debug-only `src/debug/` for
* in-process symbolication), so there's nothing to relocate.
*/
const ANDROID_SOURCEMAPS_DEBUG =
process.env.SOURCEMAPS_DIR_ANDROID_DEBUG ?? `${ANDROID_OUT_DEBUG}-sourcemaps`;

const ANDROID_SOURCEMAPS_MAIN =
process.env.SOURCEMAPS_DIR_ANDROID_MAIN ?? `${ANDROID_OUT_MAIN}-sourcemaps`;

Expand Down Expand Up @@ -316,11 +317,13 @@ const config: RolldownOptions[] = [
outDir: ANDROID_OUT_DEBUG,
debugIdMap: androidDebugDebugIds,
}),
relocateSourcemapsPlugin(
ANDROID_OUT_DEBUG,
ANDROID_SOURCEMAPS_DEBUG,
androidDebugDebugIds,
),
// Debug output keeps its `.map` colocated with the bundle (no
// relocate). `src/debug/` is merged only into debug variants, so the
// map ships in debug APKs but never in release. With native passing
// `--enable-source-maps` under `BuildConfig.DEBUG`, Node remaps
// backend stacks in-process — symbolicated errors reach Sentry with
// no upload or auth token. Release uses `src/main` (relocated +
// consumer-uploaded).
],
},
{
Expand Down
1 change: 1 addition & 0 deletions expo-module.config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"platforms": ["apple", "android"],
"apple": {
"podspecPath": "ios/ComapeoCore.podspec",
"modules": [
"ComapeoCoreModule"
],
Expand Down
37 changes: 37 additions & 0 deletions ios/ComapeoCoreSourcemaps.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require 'json'

package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))

# Debug-only companion pod: ships the embedded backend bundle's sourcemaps.
#
# The config plugin (app.plugin.js) adds this to the consumer's Podfile with
# `:configurations => ['Debug']`, so CocoaPods copies these resources only in
# Debug builds — Release IPAs never carry the (multi-MB) maps. Native passes
# Node's `--enable-source-maps` under `#if DEBUG`, so backend errors are
# remapped to original positions in-process and reach Sentry symbolicated with
# no upload. Release builds instead rely on the consumer uploading the same
# maps via `comapeo-rn-upload-sourcemaps` (debug-ID matched, server-side).
#
# Not autolinked: `expo-module.config.json` pins `apple.podspecPath` to the
# main `ComapeoCore.podspec`, so Expo only links that one and this pod is only
# ever added via the explicit, config-scoped Podfile entry above.
Pod::Spec.new do |s|
s.name = 'ComapeoCoreSourcemaps'
s.version = package['version']
s.summary = 'Debug-only backend sourcemaps for in-process symbolication'
s.description = 'Ships @comapeo/core-react-native backend sourcemaps as a Debug-only resource.'
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platforms = { :ios => '16.4' }
s.source = { git: 'https://github.com/digidem/comapeo-core-react-native' }

# Maps are laid out as a `nodejs-project/` dir (mirroring the bundle: the
# `relocateSourcemapsPlugin` writes `index.mjs.map`, `loader.mjs.map`,
# `chunks/*.map`). CocoaPods copies `nodejs-project` into the app bundle
# root, where it MERGES with the main pod's `nodejs-project` (the resource
# rsync runs without `--delete`, so the bundle files survive). Each map then
# sits next to its bundle file, so Node resolves the relative
# `//# sourceMappingURL=index.mjs.map` at runtime.
s.resources = ['nodejs-sourcemaps/nodejs-project']
end
16 changes: 12 additions & 4 deletions ios/NodeJSService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -628,16 +628,24 @@ class NodeJSService {
let defaultConfigPath = resolveDefaultConfigPath() ?? ""
// 5th positional: consumer's online map style URL, or "" when unset.
let defaultOnlineStyleUrl = resolveDefaultOnlineStyleUrl() ?? ""
var args: [String] = [
"node",
"--no-experimental-fetch",
var args: [String] = ["node", "--no-experimental-fetch"]
// Debug builds ship the backend's `.map` next to the bundle via the
// Debug-only `ComapeoCoreSourcemaps` companion pod. `--enable-source-maps`
// (a Node runtime flag, so it precedes the script path) makes Node
// remap stacks to original positions in-process, so Sentry events
// are symbolicated without a map upload. Release omits it and relies
// on consumer-uploaded maps (debug-ID matched, symbolicated by Sentry).
#if DEBUG
args.append("--enable-source-maps")
#endif
args.append(contentsOf: [
jsPath,
comapeoSocketPath,
controlSocketPath,
privateStorageDir,
defaultConfigPath,
defaultOnlineStyleUrl,
]
])
args.append(contentsOf: buildSentryArgs())
let exitCode = nodeEntryPoint(args)
logCrumb(
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"ios/*.{h,m,mm,swift,hpp,cpp}",
"!ios/Package.swift",
"ios/ComapeoCore.podspec",
"ios/ComapeoCoreSourcemaps.podspec",
"ios/NodeMobile.xcframework",
"ios/Frameworks/",
"ios/nodejs-project/",
Expand Down
19 changes: 13 additions & 6 deletions scripts/build-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,23 @@ const IOS_NODEJS_PROJECT_DIR = join(PROJECT_ROOT, "ios/nodejs-project");
// APK/IPA. The relocate-sourcemaps rolldown plugin moves `*.map` here
// after writeBundle. Consumed by `comapeo-rn-upload-sourcemaps` at
// the consumer's CI step.
const ANDROID_DEBUG_SOURCEMAPS_DIR = join(
PROJECT_ROOT,
"android/src/debug/nodejs-sourcemaps",
);
//
// The Android *debug* output has no relocation target: its map stays
// colocated with the bundle in `src/debug/assets/nodejs-project/` (shipped
// only in debug variants) so Node can remap stacks in-process. See
// `backend/rolldown.config.ts`.
const ANDROID_MAIN_SOURCEMAPS_DIR = join(
PROJECT_ROOT,
"android/src/main/nodejs-sourcemaps",
);
const IOS_SOURCEMAPS_DIR = join(PROJECT_ROOT, "ios/nodejs-sourcemaps");
// iOS maps are laid out under a `nodejs-project/` dir so the Debug-only
// `ComapeoCoreSourcemaps` companion pod can ship them as a `nodejs-project`
// resource that merges next to the bundle in the app (see that podspec and
// `app.plugin.js`'s `withDebugSourcemapsIos`).
const IOS_SOURCEMAPS_DIR = join(
PROJECT_ROOT,
"ios/nodejs-sourcemaps/nodejs-project",
);
// One xcframework per native module instance. CocoaPods picks them up
// via `vendored_frameworks` in ComapeoCore.podspec; Xcode's standard
// Embed & Sign phase places + codesigns them into <App>.app/Frameworks/
Expand Down Expand Up @@ -90,7 +98,6 @@ await $({
OUTPUT_DIR_ANDROID_DEBUG: ANDROID_DEBUG_NODEJS_PROJECT_DIR,
OUTPUT_DIR_ANDROID_MAIN: ANDROID_MAIN_NODEJS_PROJECT_DIR,
OUTPUT_DIR_IOS: IOS_NODEJS_PROJECT_DIR,
SOURCEMAPS_DIR_ANDROID_DEBUG: ANDROID_DEBUG_SOURCEMAPS_DIR,
SOURCEMAPS_DIR_ANDROID_MAIN: ANDROID_MAIN_SOURCEMAPS_DIR,
SOURCEMAPS_DIR_IOS: IOS_SOURCEMAPS_DIR,
},
Expand Down
24 changes: 11 additions & 13 deletions src/cli/upload-sourcemaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
// of the release pipeline). Re-uploading is idempotent — Sentry de-dupes
// by debug ID.
//
// Three targets ship with the package:
// Only the *release* bundles are uploaded — debug builds ship their map
// alongside the bundle and symbolicate in-process via Node's
// `--enable-source-maps` (see `backend/rolldown.config.ts` and the native
// NodeJSService), so there's nothing to upload for them.
//
// Two targets ship with the package:
//
// android-debug → android/src/debug/assets/nodejs-project/index.mjs
// android/src/debug/nodejs-sourcemaps/index.mjs.map
// android-main → android/src/main/assets/nodejs-project/index.mjs
// android/src/main/nodejs-sourcemaps/index.mjs.map
// ios → ios/nodejs-project/index.mjs
// ios/nodejs-sourcemaps/index.mjs.map
// ios/nodejs-sourcemaps/nodejs-project/index.mjs.map
//
// Each (bundle, map) pair is keyed by a deterministic debug ID embedded
// at build time (`stringToUUID(chunk.code)`); see
Expand Down Expand Up @@ -44,11 +47,6 @@ interface Target {
const PKG_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));

const TARGETS: Record<string, Target> = {
"android-debug": {
name: "android-debug",
bundleDir: join(PKG_ROOT, "android/src/debug/assets/nodejs-project"),
sourcemapDir: join(PKG_ROOT, "android/src/debug/nodejs-sourcemaps"),
},
"android-main": {
name: "android-main",
bundleDir: join(PKG_ROOT, "android/src/main/assets/nodejs-project"),
Expand All @@ -57,11 +55,11 @@ const TARGETS: Record<string, Target> = {
ios: {
name: "ios",
bundleDir: join(PKG_ROOT, "ios/nodejs-project"),
sourcemapDir: join(PKG_ROOT, "ios/nodejs-sourcemaps"),
sourcemapDir: join(PKG_ROOT, "ios/nodejs-sourcemaps/nodejs-project"),
},
};

const DEFAULT_TARGETS = "android-debug,android-main,ios";
const DEFAULT_TARGETS = "android-main,ios";

const USAGE = `\
Usage: comapeo-rn-upload-sourcemaps [options]
Expand All @@ -73,8 +71,8 @@ Options:
--org <slug> Sentry org slug (or SENTRY_ORG env var).
--project <slug> Sentry project slug (or SENTRY_PROJECT env var).
--url <url> Sentry instance URL (default sentry.io; or SENTRY_URL).
--targets <list> Comma-separated subset of: android-debug, android-main,
ios. Default: all three.
--targets <list> Comma-separated subset of: android-main, ios.
Default: both.
-h, --help Show this help.

Required env:
Expand Down