CoMapeo Core for React Native
For managed Expo projects, please follow the installation instructions in the API documentation for the latest stable release. If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
For bare React Native projects, you must ensure that you have installed and configured the expo package before continuing.
npm install @comapeo/core-react-native
The library's AndroidManifest.xml sets two <application> attributes
that exclude the rootkey-bearing SharedPreferences from cloud backup
and device-to-device transfer:
<application
android:dataExtractionRules="@xml/comapeo_data_extraction_rules"
android:fullBackupContent="@xml/comapeo_backup_rules">If your host app's AndroidManifest.xml already declares either
attribute (a fairly common case in shipping apps), the manifest merger
will fail at build time with a "different value declared" error. The
fix is two steps:
-
Merge our exclusions into your existing rules XML. Add an
<exclude domain="sharedpref" path="comapeo-core.xml" />entry under both<cloud-backup>and<device-transfer>in yourdataExtractionRulesresource, and the same<exclude>under<full-backup-content>in yourfullBackupContentresource. The library's defaults are atandroid/src/main/res/xml/comapeo_data_extraction_rules.xmlandandroid/src/main/res/xml/comapeo_backup_rules.xmlfor reference. -
Tell the merger that your manifest wins. Add
tools:replaceto your app's<application>tag:<application xmlns:tools="http://schemas.android.com/tools" android:dataExtractionRules="@xml/your_app_extraction_rules" android:fullBackupContent="@xml/your_app_backup_rules" tools:replace="android:dataExtractionRules,android:fullBackupContent">
The rootkey is encrypted with a wrapper key from AndroidKeyStore.
That wrapper key is device-bound and non-exportable, so a backed-up
envelope is useless on any other device. The exclusion is
defense-in-depth (the encrypted blob shouldn't sit in cloud backups
even when it's useless to attackers) and UX (without the exclusion,
restore-to-new-device flows appear to succeed but then fail at first
launch with RootKeyException("Wrapper key alias missing"), which
is a confusing state to end up in).
Run npx pod-install after installing the npm package.
This module can forward its native-side and JS-side lifecycle events
into the host app's @sentry/react-native. Sentry is opt-in — if you
don't register the plugin and don't import the sub-export, no Sentry
code path is exercised and no DSN ends up in your APK/IPA. See
docs/ARCHITECTURE.md §7 for the
architectural overview and
docs/sentry-integration-plan.md
for the design plan and per-phase status.
@sentry/react-native is an optional peer dep of this module. Install
it in the host app and run Sentry.init(...) once at startup as
documented at https://docs.sentry.io/platforms/react-native/. The
runtime classes shipped with @sentry/react-native also satisfy the
Android FGS-process bridge — no extra Android dependency to declare.
In app.config.js (must be .js, not app.json, to read process.env):
export default {
expo: {
plugins: [
["@comapeo/core-react-native", {
sentry: {
dsn: process.env.SENTRY_DSN,
environment: process.env.SENTRY_ENVIRONMENT ?? "production",
// Optional: opt internal/test builds into the §9 capture-application-data
// toggle by default. Production stays off-by-default.
captureApplicationDataDefault:
(process.env.SENTRY_ENVIRONMENT ?? "production") !== "production",
// Optional: opt into Sentry structured logs on the
// Android FGS process. Pair with `enableLogs: true` in
// your host-app `Sentry.init(...)` (covers main-process
// Android + iOS).
enableLogs: process.env.SENTRY_ENVIRONMENT !== "production",
},
}],
],
},
};The plugin runs at expo prebuild and bakes the DSN, environment, and other
options into AndroidManifest meta-data and Info.plist keys. Sourcing values
from process.env lets EAS build profiles produce different builds without
code changes — see
docs/sentry-integration-plan.md §4.1
for the matching eas.json example with per-profile env vars.
import "@comapeo/core-react-native/sentry";That's it — importing the sub-export attaches the lifecycle listeners
to the host's already-initialised Sentry hub. No explicit handoff
call. As long as the host has run Sentry.init(...) (the
@sentry/react-native SDK reads its DSN from the same Info.plist /
manifest values your plugin wrote), errors and breadcrumbs flow
automatically. ERROR state transitions surface tagged with the
relevant phase (rootkey, starting-timeout,
node-runtime-unexpected, etc.); state transitions show up as
breadcrumbs that ride along on the next event.
Once the plugin is registered with a dsn, the module captures
events from three layers, tagged for filtering in the dashboard:
layer:rn(JS adapter, auto-attached when the sub-export is imported) — state-machine ERROR transitions andmessageerrorparse failures; every state transition rides along as a breadcrumb.layer:native(Kotlin / Swift) —comapeo.boottransaction with phase spans (boot.rootkey-load,boot.init-frame), state-transition breadcrumbs, control-frame breadcrumbs, watchdog/shutdown timeout events, rootkey-loadcaptureException. On Android adds FGS-lifecycle breadcrumbs.layer:node(Phase 3, not yet shipped) — RPC method spans andhandleFatalexceptions from the embedded nodejs-mobile.
Each event also carries a proc tag for the actual OS process:
proc:main for everything on iOS (single-process), and
proc:main (RN code) or proc:fgs (anything in
:ComapeoCore — both the Kotlin FGS service and the embedded
nodejs-mobile) on Android.
The FGS-process Sentry SDK is initialised automatically in
ComapeoCoreService.onCreate from the manifest meta-data your
config plugin wrote. There's no extra configuration required for
multi-process Android apps using this module — that's the
SentryFgsBridge doing the work behind the scenes. If
@sentry/react-native isn't installed (so io.sentry.* isn't on
the runtime classpath), the bridge stays inert and the module
continues to function unchanged.
The Node-backend bundle (the index.mjs that runs in nodejs-mobile)
ships rolled-up + minified, so without sourcemaps stack traces in
Sentry are unreadable. The bundle's sourcemaps ship inside the npm
tarball with deterministic, content-hashed Sentry debug IDs baked
in at build time — symbolication is keyed off the IDs, so you do
not have to align this module's version with your app's release.
Add one step to your release pipeline (after eas build, or as part
of the build's post-publish phase):
SENTRY_AUTH_TOKEN=… npx comapeo-rn-upload-sourcemaps \
--org your-org \
--project your-projectRe-uploading is idempotent: Sentry de-dupes by debug ID. The CLI
finds @sentry/cli via the transitive @sentry/react-native →
@sentry/cli chain in your node_modules; if you don't use
@sentry/react-native, add @sentry/cli to your devDeps yourself.
--targets <list> (default: all) restricts the upload to a subset of
android-debug, android-main, ios. --url points at a self-hosted
Sentry. SENTRY_ORG / SENTRY_PROJECT env vars work in place of the
flags.
Contributions are very welcome! Please refer to guidelines described in the contributing guide.
The repo is a single npm package with two consumers:
src/,android/,ios/— the published@comapeo/core-react-nativemodule. This is what installs into a consumer's app vianpm install @comapeo/core-react-native.example/— an Expo app that doubles as the integration-test harness for the module. It is not published and is not something a consumer of this package would set up.
The example/ios/ and example/android/ trees are gitignored — they're regenerated on each npx expo prebuild. The source-of-truth integration tests live under example/tests/{ios,android}/ and are re-injected into each generated project by two example-app-only Expo config plugins:
example/plugins/with-ios-tests/— copies the Swift test sources, registers an XCTest target in the Xcode project, and idempotently adds the corresponding CocoaPods test target stanza to the generatedPodfile.example/plugins/with-android-tests/— copies the Kotlin test sources and adds theandroidTestdependencies + instrumentation runner to the generatedapp/build.gradle.
Both plugins are registered in example/app.json and only run during expo prebuild of the example app. Consumers of @comapeo/core-react-native do not install or register them — they're internal to the example.
iOS:
- Fast, runs on macOS without a simulator:
cd ios && swift test— exercises the framing, IPC, service lifecycle, and waitForFile helpers against mocks. - Full integration with real Node.js mobile:
cd example && npx expo prebuild --platform ios && cd ios && pod install && xcodebuild test ...— see.github/workflows/ios-tests.ymlfor the exact invocation.
Android:
- See
e2e/run-instrumented-tests.shfor the local emulator-based run, and.github/workflows/android-tests.ymlfor CI.
Native binaries (downloaded on first use):
npm run download:nodejs-mobilefetchesNodeMobile.xcframework(iOS) andlibnode.soper ABI + headers (Android) into the right places.