Sentry integration: Phase 1 + Phase 2a + Phase 2b#54
Merged
Conversation
Plans an opt-in, host-app-driven Sentry integration covering: - error capture across backend (Node), JS/RN, and native layers - RPC tracing via @comapeo/ipc onRequestHook (mirrors comapeo-mobile) - forwarding @comapeo/core OpenTelemetry spans (PR digidem/comapeo-core#1051) - app-specific gating so non-CoMapeo consumers ship no Sentry traffic https://claude.ai/code/session_01EcVXzczA1TVkhEkgUg9DKX
Closes the FGS-cold-start gap where the prior draft required RN to be alive before backend Sentry could initialize: - §4 reworked: Expo config plugin writes DSN/environment/release into Android manifest meta-data and iOS Info.plist at prebuild time. Native reads those at process start, no JS round-trip, before booting @sentry/node and @sentry/android. - §7.4 added: native telemetry data design mapped onto Sentry primitives (breadcrumbs for state transitions, transaction + spans for boot/shutdown phases, captureMessage for timeouts, tags/contexts for cross-process attribution). Categorizes captures as essential vs opt-in and documents a hard never-capture list for PII. - §9 added: persisted "capture application data" toggle with restart-to-activate semantics. Snapshot read at boot, embedded in the init frame; gates per-RPC spans, sync-session transactions, memory checkpoints, and storage-size sampling. Never unlocks the never-capture list. - §10 phasing and §13 file-change list updated. New open questions added for release tagging, plugin no-op behavior, toggle UI, and boot sample rate. https://claude.ai/code/session_01EcVXzczA1TVkhEkgUg9DKX
Three corrections after deeper review of the comapeo-mobile backend implementation: - §4.1: drop applicationId-suffix derivation. That convention is CoMapeo-specific and shouldn't be hard-coded in this module. Consumers feed `environment` via app.config.js + EAS build-profile env vars (SENTRY_ENVIRONMENT). The plugin requires `dsn` and `environment`; `release` defaults to versionName at runtime. - §4.5 + §5: replace control-socket init-frame transport with Node argv at spawn time. Sentry config in argv satisfies the auto-instrumentation order requirement (Sentry.init must run before any module loads so import-in-the-middle can patch them). The init frame stays focused on the rootkey, which we still deliberately keep out of argv. - §5.1-5.3: multi-entry rollup with separate `loader.mjs` (parses argv, conditionally inits Sentry, dynamically imports index.mjs), `importHook.js` (the import-in-the-middle hook, must be a separate file because it's loaded with module.register), and `lib/register.js` (a sub-dep that resolves at a hard-coded relative path). Includes the path-rewrite plugin from comapeo-mobile's rollup config. @sentry/node chunk loads only when --sentryDsn is passed; consumers without Sentry pay disk cost only, no runtime cost. - §9.5: toggle plumbing now goes via argv flag (--captureApplicationData), not the init frame. - §11 test plan: covers loader argv parsing, lazy chunk gating, rollup output shape, and the rewritten import-hook reference. - §12 open questions: add iOS lazy-chunk verification (--jitless), offline transport decision, sourcemap upload setup, and the capture-application-data default in non-production environments. https://claude.ai/code/session_01EcVXzczA1TVkhEkgUg9DKX
Replaces speculation with concrete decisions across 9 of 13 open questions from §12, leaving 3 verify-during-build items: - §6.2: replace Option A/B speculation with the concrete onRequestHook pattern from comapeo-mobile/src/frontend/lib/ createMapeoApi.ts. Confirmed @comapeo/ipc@^8 supports onRequestHook directly; uses Sentry.getActiveSpan() short-circuit for the no-op path. - §4.1 + §4.2: release defaults to versionName + "+" + versionCode on Android (longVersionCode) and CFBundleShortVersionString + "+" + CFBundleVersion on iOS. Successive EAS builds of the same marketing version produce distinct releases. - §5.1: pin @sentry/node@^8, @sentry/react-native@^6, @sentry/core@^8 — OpenTelemetry-first majors required for PR #1051 forwarding to work without glue. - §5.1: drop @sentry/rollup-plugin. Module ships sourcemaps in the npm package; consumer excludes from APK/IPA and runs sentry-cli sourcemaps upload in their own CI with their own credentials. Adds a README task for documenting the consumer workflow. - §9.7: capture-application-data default is per-environment via a new captureApplicationDataDefault plugin field. EAS pattern: default to true when environment !== "production". User toggle always overrides once set. - §12: collapsed into §12.1 Decided (9 entries) + §12.2 Still open / verify-during-build (3 entries: cross-process FGS tag scope, iOS jitless lazy chunk, offline transport deferred). https://claude.ai/code/session_01EcVXzczA1TVkhEkgUg9DKX
Implements the JS-side adapter handoff (Phase 1) and the Expo config
plugin + native config readers (Phase 2a) from
docs/sentry-integration-plan.md. The native-side direct-Sentry-SDK
init in the FGS process and the breadcrumb/span/event calls remain
deferred to Phase 2b — they need a Gradle dep on
io.sentry:sentry-android-core, scoped out of this PR.
Phase 1 (JS-side adapter):
- src/sentry-internal.ts holds the active SentryAdapter slot. Phase
3 RPC tracing reads from it.
- src/sentry.ts exposes configureSentry({ sentry }) as the public
API. SentryAdapter is hand-rolled (not Pick<typeof @sentry/...>)
so consumers without the optional peer dep don't get a typecheck
error. State listeners emit a comapeo.state breadcrumb on every
transition and a captureException tagged ComapeoError:<phase> on
ERROR. messageerror events capture as warning-level.
- package.json gains an exports field (./, ./sentry, ./app.plugin)
and lists @sentry/react-native@^6 as an optional peer dep.
Phase 2a (config plugin + native readers):
- app.plugin.js (root) is an ESM Expo plugin (the package is
type:module). withAndroidManifest upserts <meta-data> on the main
<application>; withInfoPlist upserts plist keys. Validates
dsn+environment at prebuild. No-op when registered without a
sentry argument.
- SentryConfig.kt + SentryConfigTest.kt land the typed manifest
reader. Pure load(metaString, defaultRelease) overload makes the
parser unit-testable on the JVM classpath without mocking
android.os.Bundle. Default release = versionName + "+" +
longVersionCode (API 28+) so successive EAS builds disambiguate.
- SentryConfig.swift + SentryConfigTests.swift mirror the Android
side. Default release = CFBundleShortVersionString + "+" +
CFBundleVersion. Accepts both string-coerced (the plugin's normal
output) and native plist types defensively.
Plan + README updated:
- docs/sentry-integration-plan.md §10.2 split into Phase 2a
(this PR) + Phase 2b (follow-up). §13 file list reflects what
actually shipped.
- README gains an Optional: Sentry integration section with the
app.config.js + configureSentry pattern.
https://claude.ai/code/session_01EcVXzczA1TVkhEkgUg9DKX
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Implements §10.2 Phase 2b for Android (the only platform that needs it — iOS is single-process and Phase 1's JS adapter already covers it; deferred as a soft follow-up). The FGS process (`:ComapeoCore`) gets its own Sentry SDK init because the host's `@sentry/react-native` only fires in the main process. Without this, FGS-originated errors carry no logcat tail / foreground-state context and the boot transaction is unobservable from the dashboard. What's new on Android: - `io.sentry:sentry-android-core:7.20.1` added as `compileOnly` + `testImplementation`. The runtime classes come transitively from `@sentry/react-native@^6` — consumers without Sentry pay nothing. - `SentryFgsBridge.kt` (guard) + `SentryFgsBridgeImpl.kt` (impl) split. The guard's `Class.forName` probe (with `initialize=false` so the SDK's `<clinit>` doesn't run on the JVM unit-test classpath) gates every public method. Impl freely imports `io.sentry.*` and is only loaded when the guard says the SDK is present. - `ComapeoCoreService.onCreate` calls `SentryFgsBridge.init(applicationContext, cfg)` from `SentryConfig.loadFromManifest(...)`. Sets `proc:fgs` and `layer:native` as process-level tags so dashboards can split FGS events from the main-process events that carry `proc:main` from `src/sentry.ts`. - `NodeJSService` opens `comapeo.boot` transaction in `start()`, closes on first STARTED (`ok`) / ERROR (`internal_error`). Phase spans (`boot.rootkey-load`, `boot.init-frame`) match the bench backend's taxonomy from `apps/benchmark/backend/lib/boot-spans.js` on `claude/benchmark-uds-rpc-bridge-1Zahz` so a single Sentry dashboard query charts both sides. - State-transition breadcrumbs in `applyAndEmit`, control-frame breadcrumbs (started/ready/stopping/error/ malformed) in `handleControlMessage`, FGS lifecycle breadcrumbs (`onCreate`/`onStartCommand`/`onDestroy`). - Timeout events (`comapeo: startup timeout fired`, `comapeo: FGS stop timeout fired`) tagged `timeout:startup` / `timeout:fgsStop`. - FGS-side `captureException` on rootkey-load failure tagged `comapeo.phase:rootkey`, `source:rootkey-store`. Fires before `sendErrorNativeFrame` so the FGS scope is intact; the same exception is re-broadcast and re-captured by the main-process JS adapter for the cross-process triple per §7.4.7. Phase 1 polish: added `proc:main` to the JS adapter's `captureException` calls so the pair with `proc:fgs` is filterable in the dashboard. Tests: - `SentryFgsBridgeTest.kt` pins the no-op guard contract: pre-init calls (addBreadcrumb / captureException / captureMessage / startBootTransaction / startBootSpan / finishSpan) all return cleanly; the `Class.forName` probe with `initialize=false` finds sentry-android on the test classpath and is idempotent. - `eslint.config.js` ignores `.claude/**/*` so leftover worktree artifacts don't break the lint cache. Verified locally: - `npm run lint` clean - `npx tsc --noEmit` clean - `./gradlew :comapeo-core-react-native:testDebugUnitTest` passes (54 tests, including 9 new SentryFgsBridge cases) - `./gradlew :comapeo-core-react-native:compileDebugKotlin` succeeds with sentry-android on the compile classpath Plan + README updated: - §10.2 Phase 2b reframed as "landed (Android only)" with full implementation list + iOS-deferred note. - §13 file-change list reflects what shipped. - README "Optional: Sentry integration" gains a "What gets captured automatically" section explaining the three event streams (JS/main, FGS/native, backend later) and the automatic FGS-process SDK init.
Plan §10 now opens with a status table marking each phase as
landed / pending so the reader can see at a glance what's
done (1, 2a, 2b) versus what's remaining (3, 4, 5, 6).
Per-phase headings get explicit "— landed" / "— pending"
suffixes for the same reason.
ARCHITECTURE.md gains §7 "Sentry observability (optional)" —
the high-level view of the integration without the design-doc
detail. Covers:
- the three event streams (proc:main / proc:fgs /
proc:backend) and how cross-process attribution converges
- build-time config flow via the Expo plugin → manifest /
Info.plist → native readers
- the Guard / Impl split and why it lets `compileOnly`
sentry-android work without breaking consumers who don't
use Sentry
- what the design intentionally doesn't attempt (sentry-
native inside nodejs-mobile, iOS multi-process, PII
capture)
§1 TL;DR gets a single-line mention so the entry point isn't
silent on the optional integration. §8 / §9 (Alternatives /
References) bumped to make room.
README cleanups:
- Step "1. Install `@sentry/react-native`" added — the
optional peer dep is also what brings sentry-android onto
the runtime classpath for the Android FGS bridge.
- Step numbering bumped (1 → 2 plugin, 2 → 3 handoff).
- Intro now says "native-side and JS-side lifecycle events"
rather than "lifecycle errors and (in later phases) RPC
tracing" — Phase 2b shipped, the framing was outdated.
- Cross-link to ARCHITECTURE.md §7 + plan for further detail.
- "EAS profile example" link moved into the plan rather
than inlining a slimmer copy in the README.
Addresses the high- and medium-priority issues from the review subagent's pass over Phase 1+2a+2b: Boot transaction lifecycle (was: review #1, #15) - applyAndEmit now closes bootTx and drains in-flight phase spans on STOPPING / STOPPED transitions too — not just STARTED / ERROR. stop()-from-STARTING transitions to STOPPING (rule 3 of deriveLifecycleState) and bypassed both terminals; destroy() forcing STOPPED-via-stopRequested did the same. Status mapped: STARTED→ok, ERROR→ internal_error, STOPPING/STOPPED→cancelled. - startBootTransaction now passes a TransactionContext carrying TracesSamplingDecision(true, 1.0). The previous TransactionOptions-only setup didn't actually force sampling, so with the SDK default tracesSampleRate=0.0 the boot transaction was dropped before reaching the wire. SentryConfig misconfig handling (was: review #3) - Both Kotlin and Swift readers used to crash on (DSN-set, environment-missing) — meant to be "fail loud" but a stale prebuild from before the validation was added would crash every cold start with no recovery. Now log loud (System.err on Android since android.util.Log isn't mocked on JVM tests; NSLog on iOS) and return null (Sentry off). Updated test renamed to assert "returns null, doesn't throw". Span op/description ordering (was: review #19) - transaction.startChild(op, description) — op is the indexed dashboard column. Was passing ("boot", "boot.<phase>"), swapped to ("boot.<phase>", human-readable description) so the dashboard groups by the phase taxonomy that matches the bench backend's boot-spans.js helper. Plugin idempotency (was: review #7) - Previously, dropping `props.sentry` from the plugin registration left stale meta-data / plist entries from a previous prebuild (with `expo prebuild --no-clean`). Plugin now passes through a no-Sentry cleanup mod that strips every key it owns; consumer-owned keys (e.g. io.sentry.* set by @sentry/react-native's plugin) are untouched. messageerror payload truncation (was: review #8) - src/sentry.ts now truncates the wrapped error message to 256 chars before forwarding to captureException. The control-frame parser surfaces offending input verbatim, which can include arbitrary bytes from a corrupted frame — truncating keeps Sentry events small and readable. IPC + SEND_ERROR_NATIVE breadcrumbs/events (was: review #9, #10) - NodeJSIPC's onConnectionStateChange callback wired in the FGS-side controlIpc construction; emits comapeo.ipc breadcrumbs at info (warning on Error). Per §7.4.5. - SEND_ERROR_NATIVE_TIMEOUT_MS firing now captures a level=warning event with timeout:errorNativeForward tag. Per §7.4.4. Logging swallowed surprises (was: review low-priority) - SentryFgsBridge's empty `catch (t: Throwable) {}` blocks now Log.w so debug builds notice swallowed bridge / SDK bugs. Post-init bridge tests (was: review #6) - New SentryFgsBridgeImplTest spins up a real Sentry hub via the cross-platform Sentry.init(SentryOptions) path with an in-memory ITransport. Covers: addBreadcrumb (no envelope on its own), captureException + captureMessage (envelope enqueued), startBootTransaction with global tracesSampleRate=0.0 (must still reach transport thanks to the TracesSamplingDecision override — regression test for the §15 bug above), boot span lifecycle, finishSpan with cancelled status, unknown level fallback to INFO. All Sentry-related tests pass: 25 cases across SentryConfigTest (8), SentryFgsBridgeTest (10), SentryFgsBridgeImplTest (7). Verified locally: - npm run lint clean - npx tsc --noEmit clean - ./gradlew :comapeo-core-react-native:testDebugUnitTest passes - ./gradlew :comapeo-core-react-native:compileDebugKotlin succeeds with sentry-android on the compile classpath
…, comments Addresses every point in the latest review pass: Auto-detected adapter (replaces configureSentry handoff) - src/sentry-internal.ts probes for @sentry/react-native via try/ require at module load. The host's existing Sentry.init(...) populates the global hub; our state listeners call into it directly. No double-init risk, no consumer call needed. - src/sentry.ts: dropped configureSentry and ComapeoSentryConfig. State listeners attach at module load. Tests can override via setSentryAdapterForTests(fake). - README "3. Hand off" step shrinks to a single side-effect import. proc/layer taxonomy fix - proc reflects the actual OS process: iOS = always main, Android = main (RN/native) or fgs (FGS process incl. embedded node). - Renamed layer:backend → layer:node throughout the plan and ARCHITECTURE.md §7. Phase 3's Node-side captures will tag layer:node, proc:fgs (Android) / proc:main (iOS) — not proc:backend (which doesn't exist). - ARCHITECTURE.md §7 redrawn as a 2D table of process × layer. iOS parity (was Android-only) - New ios/SentryNativeBridge.swift mirrors the Android bridge. Uses #if canImport(Sentry) so consumers without the Sentry pod (no @sentry/react-native) compile to no-ops. No separate init needed — host's @sentry/react-native already booted sentry-cocoa. - Wired into ios/NodeJSService.swift on the same hook points as Android: state-transition breadcrumbs, comapeo.boot transaction with phase spans (rootkey-load, init-frame), control-frame breadcrumbs, watchdog timeout (startup), shutdown timeout, rootkey-load captureException with proc:main + layer:native. Tag constants - New SentryTags.kt / SentryTags.swift / sentry-tags.ts. Every emit site uses constants for proc, layer, comapeo.phase, comapeo.state, source, timeout, timeoutMs. A typo can no longer silently route events to the wrong dashboard column. resetForTests cleanup + Impl type tightening - SentryFgsBridgeImpl.resetForTests was a documented no-op being called from SentryFgsBridge.resetForTests for nothing — dropped. - Impl method bodies use proper ITransaction / ISpan types internally; signatures stay Any/Any? at the boundary so the Guard's bytecode remains free of io.sentry.* references (the whole point of the Class.forName probe). startBootSpan now passes (op="boot.<phase>", description=human-readable), matching Sentry's startChild convention. Plugin idempotency on reset - app.plugin.js drops the early-return when sentry is absent; always passes through both withAndroidManifest / withInfoPlist so a `--no-clean` re-prebuild after disabling Sentry strips stale entries instead of leaving the previous DSN in place. Comment cleanup pass - Stripped plan-section refs (§4.x, §7.4.y, "Phase 2b") from inline comments — those go stale once the plan is implemented or restructured. Kept WHY comments where the logic isn't self-explanatory; trimmed verbose docstrings where the function name + signature already tells the story. - Same pass applied to: SentryConfig.kt, SentryConfig.swift, SentryFgsBridge.kt, SentryFgsBridgeImpl.kt, NodeJSService.kt (Android + iOS), ComapeoCoreService.kt, app.plugin.js. Verification - `npm run lint` clean - `npx tsc --noEmit` clean - `cd ios && swift build` succeeds; `swift test` passes 78 tests - `cd apps/example/android && ./gradlew :comapeo-core-react-native:testDebugUnitTest` passes (25 Sentry tests + existing suite)
Addresses the latest review pass:
logCrumb helper (deduplicates log + addBreadcrumb pairs)
- Researched Sentry's log/breadcrumb integration patterns: Sentry
Timber on Android and consoleLoggingIntegration on RN can
auto-route logs to breadcrumbs/structured-logs, but the
Timber bridge would lose our structured `data` payload and
doesn't apply to the iOS side. Pragmatic answer: a tiny
`logCrumb(category, message, level, data)` helper in
log.kt / Log.swift that emits one logcat/os_log line and one
Sentry breadcrumb in a single call. Replaces every
`log("foo"); SentryFgsBridge.addBreadcrumb(category, "foo", …)`
pair at the callsite. Sentry's structured-log feature stays
unused for now (different concept — standalone queryable
records vs ride-along event context).
Category constants
- New SentryCategories.{kt,swift,ts} hoists the dot-separated
category strings (`comapeo.state`, `comapeo.control`,
`comapeo.ipc`, `comapeo.fgs`, `comapeo.boot`) out of every
callsite. Mirrors the SentryTags pattern.
log() callsite audit
- Boot-progress log lines that were `log()` only now use
`logCrumb(SentryCategories.BOOT, …)`: start(), asset copy,
init frame sent, node thread exit (level=warning when
exitCode≠0).
- Stop / destroy lifecycle steps switched to logCrumb under
`comapeo.state`.
- iOS state-transition log moved out of `state.didSet` (was
duplicating with the new logCrumb in applyAndEmit).
- Node-runtime launch failure on Android now also
captureExceptions with phase=node-runtime,
source=startNodeWithArguments — was previously only an
ERROR-state derivation, lacked stack trace at the source.
- Verbose `log("Control IPC received: ...")` lines dropped —
the per-frame breadcrumbs already cover this and it was
spammy.
Metadata reference (ARCHITECTURE.md §7.5)
- Five tables: tags, breadcrumb categories, spans,
captureMessage events, captureException tag sets. Single
source of truth for what flows to the dashboard. Updates
to the constants files first, then this table.
- Fixed pre-existing numbering drift: §8 subsections were
mislabeled 7.1–7.4; now 8.1–8.4.
Verified locally: tsc clean, lint clean, swift build clean,
android testDebugUnitTest passes (25 Sentry tests).
Documents the three emission categories already in use: - logCrumb (both) — lifecycle events that describe meaningful app progress and would help debug a future error. - log only — debug noise / guard-rejection paths. - breadcrumb only — JS adapter on Android (FGS-side logCrumb already covers logcat). Calls out the iOS dual-crumb caveat (single process, both JS-adapter and native-bridge crumbs land on the same hub) and the Phase 3 forward-look for per-RPC volume. Adds a section on Sentry structured logs as a separate pipeline. Two ways to populate it (Sentry Android Gradle plugin's bytecode logcat instrumentation, or wrapping our helpers to call Sentry.logger.* directly). Documents that we do neither today, and why — explicit follow-up if cross- process timeline reconstruction becomes a need.
Symmetric with logCrumb. Each helper maps to a single Sentry primitive: - logCrumb — log + addBreadcrumb (lifecycle progress) - logException — log + captureException (caught throwables) - logCapture — log + captureMessage (notable non-error events) - log — log only (debug, guard rejections) Refactor the existing duplicated callsites: Android NodeJSService.kt: - Failed-to-load-rootkey: Log.e + captureException → logException - Error-starting-node: Log.e + captureException → logException - Startup-timeout: captureMessage → logCapture - Error-native-frame-dropped: Log.w + captureMessage → logCapture Android ComapeoCoreService.kt: - FGS-stop-timeout: captureMessage + logCrumb → logCapture (was double-logging — captureMessage carries the message forward via the addBreadcrumb side-effect of capture, so the separate logCrumb was redundant) iOS NodeJSService.swift: - Failed-to-load-rootkey: captureException → logException (gains a logcat line that wasn't there before) - Startup-timeout: captureMessage → logCapture - Stop-timeout: captureMessage → logCapture ARCHITECTURE.md §7.6: rewritten as a four-helper table (log / logCrumb / logException / logCapture). The captureException-vs-captureMessage rule is documented inline. Verified: tsc clean, lint clean, swift build clean, android testDebugUnitTest passes (25 Sentry tests).
…t-native@^7
Restructures the four log helpers so log(message, level, attributes,
throwable?) is the foundation that always writes to logcat / os_log AND
forwards to Sentry's structured-log pipeline. logCrumb / logException /
logCapture compose on log() and only add their specific Sentry primitive
(breadcrumb / captureException / captureMessage) — eliminating the
repeated level→Log.x mapping and Sentry.log call that lived in each.
To get a sentry-android with the structured-log API (Sentry.logger().*,
SentryLogLevel, SentryAttributes), bumps the @sentry/react-native peer
dep from ^6 (transitive sentry-android 7.20.x) to ^7 (transitive
sentry-android 8.32.x and sentry-cocoa 8.58.x). Audited the v6→v7 / v7→v8
migration guides against our actual call sites; no consumer-facing API
we use changed signatures, so no downstream code edits required. Notes
for host-app upgrade live in the README.
Adds an enableLogs plugin option that gates options.logs.isEnabled on the
Android FGS-process hub. Main-process Android and iOS are gated by the
host's own Sentry.init({ enableLogs: true }).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an opt-in Sentry integration for @comapeo/core-react-native, spanning JS (state listener adapter), build-time configuration via an Expo config plugin, and native-side config readers + telemetry emission (including Android FGS-process support).
Changes:
- Introduces a JS Sentry sub-export that emits lifecycle breadcrumbs and captures ERROR transitions / control-frame parse failures.
- Adds an Expo config plugin that writes/removes Sentry config in Android manifest meta-data and iOS Info.plist, plus native parsers and unit tests.
- Adds native telemetry emission helpers + Android FGS Sentry bridge and boot transaction/span instrumentation; updates docs and package exports.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/sentry.ts | JS sub-export that hooks state events to add breadcrumbs and capture errors via an optional Sentry adapter. |
| src/sentry-tags.ts | Centralizes JS-side Sentry tag keys and breadcrumb categories. |
| src/sentry-internal.ts | Auto-detects @sentry/react-native via guarded require and allows test overrides. |
| README.md | Documents the opt-in Sentry integration steps for consumers. |
| package.json | Adds exports for ./sentry and ./app.plugin, and declares @sentry/react-native as an optional peer dependency. |
| app.plugin.js | Expo config plugin that writes/strips Android/iOS Sentry config at prebuild time. |
| docs/sentry-integration-plan.md | Detailed design/phasing document for the integration. |
| docs/ARCHITECTURE.md | Adds an architectural overview of the optional Sentry observability design. |
| eslint.config.js | Ignores .claude/**/* for linting. |
| ios/SentryConfig.swift | iOS typed reader for Info.plist Sentry config with pure parsing helpers. |
| ios/SentryTags.swift | iOS constants for Sentry tags and breadcrumb categories. |
| ios/SentryNativeBridge.swift | iOS bridge that emits breadcrumbs/captures/spans to the host’s Sentry hub when available. |
| ios/Log.swift | Adds log helpers that also emit Sentry breadcrumbs/captures via the iOS bridge. |
| ios/NodeJSService.swift | Adds iOS-native Sentry boot transaction/spans + breadcrumbs + timeout captures. |
| ios/Package.swift | Includes new Sentry-related Swift sources in the Swift Package target. |
| ios/Tests/SentryConfigTests.swift | XCTest coverage for iOS SentryConfig parsing behavior. |
| android/build.gradle | Adds compileOnly + testImplementation dependency on io.sentry:sentry-android-core. |
| android/src/main/java/com/comapeo/core/SentryConfig.kt | Android typed reader for manifest meta-data Sentry config (unit-testable pure overload). |
| android/src/main/java/com/comapeo/core/SentryTags.kt | Android constants for Sentry tags and breadcrumb categories. |
| android/src/main/java/com/comapeo/core/SentryFgsBridge.kt | Guarded bridge that no-ops when Sentry classes aren’t on the runtime classpath. |
| android/src/main/java/com/comapeo/core/SentryFgsBridgeImpl.kt | Android implementation that directly calls the Sentry SDK (boot tx/spans, breadcrumb/capture helpers). |
| android/src/main/java/com/comapeo/core/log.kt | Android log helpers that also emit Sentry breadcrumbs/captures via the bridge. |
| android/src/main/java/com/comapeo/core/NodeJSService.kt | Adds Android FGS-process Sentry boot transaction/spans, breadcrumbs, and timeout/capture wiring. |
| android/src/main/java/com/comapeo/core/ComapeoCoreService.kt | Initializes FGS-process Sentry on service create; adds lifecycle breadcrumbs and stop-timeout capture. |
| android/src/test/java/com/comapeo/core/SentryConfigTest.kt | JVM tests for Android SentryConfig parsing. |
| android/src/test/java/com/comapeo/core/SentryFgsBridgeTest.kt | JVM tests asserting guarded no-op behavior before init and probe caching. |
| android/src/test/java/com/comapeo/core/SentryFgsBridgeImplTest.kt | JVM tests for post-init Sentry bridge behavior using an in-memory transport. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Plan §10.1: drop the stale `configureSentry({ sentry })` mention;
shipped JS auto-detects and only exposes `setSentryAdapterForTests`.
- ARCHITECTURE.md §7.4: rewrite the iOS bullet — JS adapter is no
longer the only iOS-side integration; native iOS code emits
against the host hub directly. The "doesn't attempt" claim is now
scoped to multi-process Sentry (moot on single-process iOS).
- src/sentry-internal.ts: docstring referenced a nonexistent
`resetOverrideAdapter`; reset is `setOverrideAdapter(null)`.
- src/sentry-tags.ts: add `timeoutMs` for parity with `SentryTags.kt`
/ `SentryTags.swift` (referenced in ARCHITECTURE.md §7.5).
- ComapeoCoreService.kt: `comapeo: FGS stop timeout fired` capture
was missing the `comapeo.phase: shutdown-timeout` tag (per §7.5).
- ios/NodeJSService.swift: lock-protect every read/write of
`bootTransaction` / `bootSpans`. The drain in `applyAndEmit` now
snapshots-and-clears under the lock, so concurrent terminal
transitions can't double-finish; `start()`, `sendInitFrame`, and
`handleControlMessage` take the lock around the field access too.
Bridge calls stay outside the lock per the no-callbacks-under-
lock discipline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cleanups from PR review.
1. Drop `timeoutMs` from the tag schema. Tags are for low-cardinality
categorical filtering, not numeric values; `timeoutMs` carried a
constant-per-release duration and overlapped semantically with the
`timeout` tag (which already names *which* timeout fired). Anyone
wanting the configured duration can read it from the release. Removed
from `SentryTags.{kt,swift}` / `src/sentry-tags.ts`, the three
emission sites in `NodeJSService.{kt,swift}`, and the §7.5 schema /
captureMessage tables in `ARCHITECTURE.md`.
2. Migrate iOS boot-Sentry state (`bootTransaction`, `bootSpans`) off
the existing state-machine `NSLock` and onto a dedicated serial
`bootSentryQueue`. Every read/write goes through `queue.sync`;
bridge calls happen outside the queue (same no-callbacks-under-sync
discipline). Replaces five small `lock.lock()/unlock()` pairs added
earlier in this PR with single `bootSentryQueue.sync { … }` calls,
so future contributors don't have to remember which sites are
racy. The drain in `applyAndEmit` also moves out of the
state-machine lock — terminal-status decision still happens under
`lock`, but the snapshot+clear of `bootTransaction` / `bootSpans`
happens on the boot-Sentry queue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
package.json:33
- The README now links to
./docs/ARCHITECTURE.mdand./docs/sentry-integration-plan.md, butdocs/is not included in the packagefileswhitelist. When published to npm those links will 404/miss unless you either adddocs/tofilesor change the README links to point at GitHub URLs.
"files": [
"expo-module.config.json",
"app.plugin.js",
"android/build.gradle",
"android/CMakeLists.txt",
"android/src/debug/",
"android/src/main/",
"android/libnode/",
"build/",
"ios/",
"src/"
],
Three review comments from Copilot:
1. Plan §4 still described an explicit `configureSentry({ adapter })`
handoff. Updated the configuration-vector table and the §4 intro
bullet about the state observability gap to refer to the
side-effect import (`@comapeo/core-react-native/sentry`) and
require-then-catch auto-detect that actually shipped.
2. Pinned-version notes referenced `@sentry/react-native@^6`,
`@sentry/core@^8`, and `sentry-android-core:7.20.1`. The module
now declares `@sentry/react-native@^7` and pins `8.32.0`. Updated
§5.1, §10 status row, §10.2 shipped section, §12.1 decisions
table, and the §13 file diff to match.
3. `SentryFgsBridgeTest.kt` comment quoted the old 7.20.1 pin.
Bumped to 8.32.0 to match `android/build.gradle`.
The HMR/listener double-registration comment from the same review
was not addressed: the suggested module-level `installed` flag
wouldn't survive a re-evaluation (each re-eval gets a fresh
top-level scope). A correct fix would have to live on a long-lived
singleton (globalThis), and the impact is a dev-only cosmetic noise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cleanups suggested by a code-review pass on SentryFgsBridgeImpl:
- `log()` had a 6-arm `when` over `SentryLogLevel` whenever
`attributes.isEmpty()`. The unified `Sentry.logger().log(level,
params, message)` overload accepts an empty `SentryAttributes`
perfectly well, so the branch was dead weight. Drops 13 lines.
- `captureException` and `captureMessage` both hand-rolled the same
`tags.forEach { (k, v) -> scope.setTag(k, v) }` ScopeCallback body.
Pulled into a single `applyTags(scope, tags)` helper. The scope
parameter is `IScope` (sentry-android v8's unified scope type
per `ScopeCallback.run(IScope)`).
Other findings (3 sibling `parseLevel`/`parseLogLevel`/`parseStatus`
mappers, the typed-factory `sentryAttribute` dispatch, and the
phase→description `when` in `startBootSpan`) were reviewed and
left as-is — unifying them would lose readability without
meaningful dedup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d type coercion; wire example app
Two changes captured in one commit because the example-app smoke
test is what surfaced the bug.
1. android/src/main/java/com/comapeo/core/SentryConfig.kt — switch
the manifest-metadata reader from `Bundle.getString` to
`Bundle.get(key)?.toString()`. Android's manifest XML parser
coerces `android:value="true"` to a Java Boolean and
`android:value="0.5"` to a Float before the Bundle is built;
`getString` returns null for anything that isn't a true String,
so `enableLogs`, `captureApplicationDataDefault`, and any
numeric value were all silently dropping to null on the FGS
process. Logcat (PID 22343 in the smoke test) showed:
`Bundle: Key com.comapeo.core.sentry.enableLogs expected
String but value was a java.lang.Boolean. The default value
<null> was returned.` After the fix the FGS-process bridge
now correctly observes `enableLogs=true` and forwards
structured logs to `Sentry.logger().*`.
The unit tests use a fake `metaString` lambda over a
`Map<String, String>`, so they don't exercise the Bundle's
coercion path — adding a Robolectric or instrumented test for
that is the obvious follow-up.
2. apps/example — wire `@comapeo/core-react-native` Sentry into
the example app:
- npm dep: `@sentry/react-native@^7`.
- app.json: register the comapeo plugin via the relative
`../../app.plugin.js` path (Expo couldn't resolve `../..`
because the package's `main` resolves first and isn't a
plugin) with `dsn`, `environment: "development"`,
`captureApplicationDataDefault: true`, `enableLogs: true`.
- index.ts: call `Sentry.init({ enableLogs: true,
tracesSampleRate: 1.0, debug: true })` before the comapeo
side-effect import, plus an explicit
`Sentry.captureMessage("...smoke test")` and
`Sentry.logger.info("...index.ts loaded")` so something
concrete shows up in Issues / Logs even on a clean boot
(without these the only RN-side captures fire on ERROR
transitions).
Verified end-to-end against the
`core-react-native-example` Sentry project: structured logs
from both the JS adapter and the FGS-process bridge flow into
the Logs UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… for app.start.cold Two cleanups from the smoke-test review of the core-react-native-example issue. 1. src/sentry.ts — set `proc:main` / `layer:rn` as scope-default tags on the JS hub when the sub-export attaches its listeners. Previously these tags only landed on the captures we made by hand (state-listener captureException / captureMessage); they were missing from the default RN-SDK events (JS errors, ANRs, app-start transactions, navigation transactions). Now every event from this layer carries them, so dashboards can filter by `layer:rn` consistently. Required adding `setTag` to the hand-rolled `SentryAdapter` interface. 2. apps/example/index.ts — wire `Sentry.wrap(App)` on the root component so the AppStart integration can mark the app-start *end* timestamp on first render. Without it the integration logs `'[AppStart] Last recorded app start end timestamp is before the app start timestamp. ... usually caused by missing Sentry.wrap(RootComponent) call'` and drops the span. The example app also has no navigation library, so the AppStart integration's default mode (attach app-start data as a child span on the first auto-instrumented transaction) never flushes — switched to `standalone: true` via integrations override so it emits its own `App Start` transaction. Real consumers (comapeo-mobile etc.) with react-navigation can drop the override. Verified end-to-end: the Sentry Logger now reports `[Tracing] Starting sampled root span op: ui.load name: App Start` with three `app.start.cold` child spans (Cold Start, JS Bundle Execution Before React Root, Process Initialization) and a 3.7s app-start measurement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Sentry.captureMessage` + `Sentry.logger.info` were added to the example app's `index.ts` to confirm end-to-end delivery during the Sentry integration smoke test. Now that delivery is verified (both Issues and Logs UIs received the events), the calls would just produce one noise event per build of the example app — drop. Sentry.init, Sentry.wrap, the AppStart standalone override, and the comapeo sub-export import all remain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ule.version tag
The bundled-backend deps (@comapeo/core, @comapeo/ipc, etc.) are
rolled into a single `nodejs-project/index.mjs`, so they never
appear in the consumer's `node_modules` and `@sentry/react-native`'s
`ModulesLoader` integration can't see them. Without this change,
`event.modules` is empty on every event from this module's hub
and there's no way to filter "issues since I bumped @comapeo/core
from 7.1.0 to 7.2.0" in Sentry.
This commit wires three pieces:
1. `scripts/write-version.mjs` — runs on `npm install` (via
`prepare`) and `npm pack` / `npm publish` (via `prepack`).
Generates `src/version.ts` with:
- `COMAPEO_MODULE_VERSION` from this package.json
- `COMAPEO_MODULE_GIT_SHA` from `git rev-parse --short=8 HEAD`
(empty when not in a git checkout — npm tarball install)
- `COMAPEO_MODULE_DIRTY` from `git status --porcelain`
- `COMAPEO_MODULE_VERSION_LABEL` — `<version>+git<sha>[-dirty]`
composite for the `comapeo.module.version` tag value
- `BACKEND_MODULES` — pinned `@comapeo/*` and `@mapeo/*` deps
from `backend/package.json` (filter excludes upstream peers
like fastify that aren't useful pivots)
`src/version.ts` is gitignored; the published tarball
embeds whatever values the publisher's `prepack` produced.
2. `src/sentry.ts` — when the sub-export attaches its listeners:
- `setTag("comapeo.module.version", LABEL)` for the primary
filter axis ("issues since 0.2.0+gitabc1234")
- `addEventProcessor` injecting our entries into
`event.modules` so the SDK's auto-populated map is augmented
with the bundled-backend dep versions
Required adding `addEventProcessor` to the hand-rolled
`SentryAdapter` interface and a minimal `SentryEvent` shape.
3. `.gitignore` — list `src/version.ts` so the auto-generated
file doesn't churn the working tree.
Smoke-tested on the core-react-native-example project: events
from PID 25005 (after rebuild) carry the new tag and the
populated modules map. `addEventProcessor` is re-exported from
`@sentry/core` via `@sentry/react-native@^7`'s public surface,
so the structural type check on the auto-detected adapter
passes without explicit registration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kers Two requested changes off the smoke-test pass. 1. scripts/write-version.mjs — replace the boolean `COMAPEO_MODULE_DIRTY` with an 8-char sha256-prefix digest of `git diff HEAD --binary`. Different in-flight changes now produce different version labels (`0.1.0+gitabc1234-dirty1f2e3d4c` vs `0.1.0+gitabc1234-dirty5a6b7c8d`), so a Sentry event from "fix attempt 1" is distinguishable from "fix attempt 2" without committing in between. Empty string when the tree is clean. Updated the `COMAPEO_MODULE_VERSION_LABEL` construction to embed the digest as `-dirty<hash>` instead of the bare `-dirty` flag. 2. apps/example/index.ts — re-add the temporary `Sentry.captureMessage` + `Sentry.logger.info` smoke-test calls so end-to-end verification produces something concrete in the Issues / Logs UI on each fresh build. Marked "TEMPORARY ... remove before merging" in a comment so it doesn't ship. Verified on both platforms after rebuild: - Android (`expo run:android` against Pixel 7a API 29 emulator): smoke-test envelope flushed at 15:06:05; Sentry SDK reported `Captured error event \`comapeo-core-react-native example smoke test\``. - iOS (`expo run:ios` against iPhone 16 sim, after `pod install --repo-update` to pick up Sentry/HybridSDK 8.58.0): same smoke-test event captured; AppStart integration emitted a standalone `App Start` transaction with `Cold Start` / `UIKit Init to JS Exec Start` / `Process Initialization` child spans (14443ms cold start in the simulator) and 15 total frames. Follow-up: expose `BACKEND_MODULES` as a public export so consumers can read the bundled-backend dep map without reaching into `src/version`. Will land separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ive consumer's Sentry.init
The previous round wired `proc:main` / `layer:rn` /
`comapeo.module.version` tags and the `event.modules` injector
via `adapter.setTag(...)` / `adapter.addEventProcessor(...)` —
the top-level `@sentry/core` helpers. Both target the
*current/isolation* scope, which the consumer's `Sentry.init`
can fork or replace. Combined with Metro's import hoisting
(and `inlineRequires: true` in the example's
`metro.config.js`), the comapeo sub-export's side-effect runs
BEFORE `Sentry.init({...})` even when the import line appears
below it in source — so all our scope writes were landing on
a doomed scope and then disappearing.
Symptom: the user's smoke-test event JSON had none of our
expected tags or modules entries.
Fix: write to `adapter.getGlobalScope()` instead. The global
scope is a module-level singleton from `@sentry/core` load
time; it isn't replaced by `Sentry.init` and writes survive
the pre-init / post-init handoff. Verified by inspecting
`@sentry/core/build/esm/currentScopes.js` — `getGlobalScope`
returns `globalScope ??= new ScopeClass()`.
The hand-rolled `SentryAdapter` interface gains a
`getGlobalScope()` method returning the minimum surface we
need (`setTag` + `addEventProcessor`), so the structural
match against `@sentry/react-native@^7` (which re-exports
`getGlobalScope` from `@sentry/core`) stays valid without
pulling in `Scope` as a dep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…re globals
The previous round's `getGlobalScope()` switch should have made
the tag + event-processor writes survive the consumer's
`Sentry.init`, but the smoke-test event still came back without
either. Two compounding causes; both fixed here.
1. `src/sentry-internal.ts` was using the
`try { const r: NodeRequire = require; r("@sentry/react-native") }`
pattern to make the peer dep optional. Metro's static bundler
doesn't reliably bind aliased dynamic requires — at runtime
the resolved module came back as `undefined`, so `detected`
stayed `null`, `activeAdapter()` returned `null`, and the
sub-export's whole side-effect block (tags, event processor)
silently skipped. State-listener captures still appeared to
work because the user was seeing FGS-side native bridge
output (Kotlin → sentry-android) and the example's own
smoke-test `Sentry.captureMessage` call (direct, not via our
adapter) — neither path runs through `activeAdapter()`.
Switched to a static
`import * as Sentry from "@sentry/react-native"`. Since the
sub-export (`@comapeo/core-react-native/sentry`) is opt-in
per its own import path, requiring `@sentry/react-native`
to be installed when this sub-export is imported is the
correct contract; the package.json keeps it as an optional
peer dep so the parent install doesn't fail when consumers
skip Sentry.
2. The static import has a follow-on risk: with
`@sentry/react-native` installed both at the example
(apps/example/node_modules/@sentry) and the package root
(added as a devDep so the package's tsc has type
declarations), Metro could resolve the SDK to two different
locations from two different bundle entry points and
instantiate `@sentry/core` twice — giving two separate
`getGlobalScope()` singletons. Same hazard as react /
react-native that the metro.config already guards against.
Extended the example's `blockList` to also exclude
`<repo>/node_modules/@sentry/*`, forcing every bundled file
to resolve through `apps/example/node_modules/@sentry` and
share one module instance.
`@sentry/react-native@^7.13.0` added to the package's
devDependencies so `npm run build`'s tsc finds type
declarations; runtime contract still goes through the optional
peer dep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o comapeo.rn
Two cleanups in response to PR feedback on the modules / tag
shape.
1. The previous round dumped both @comapeo/core-react-native and
the bundled-backend deps into `event.modules`. That conflated
two different runtimes: `@comapeo/core-react-native` runs in
the host's RN bundle, while the backend deps run in
nodejs-mobile (a different process / runtime). More
practically, anything the consumer imports from
`@comapeo/core` directly on the RN side (type/static exports)
would write a different version into
`event.modules["@comapeo/core"]` and one would silently
overwrite the other. Moved the bundled-backend deps to a
dedicated `comapeoBackend` context block via
`globalScope.setContext("comapeoBackend", BACKEND_MODULES)`;
`event.modules` now only carries
`@comapeo/core-react-native` itself.
2. Renamed the version tag `comapeo.module.version` ->
`comapeo.rn`. Three-level dot nesting was unusual against
Sentry's two-level `device.family` / `os.name`
convention; pairs cleanly with a future `comapeo.core` tag
for the backend's @comapeo/core version. The value is
unchanged (`<version>+git<sha>[-dirty<hash>]`) so both
release and source identity stay in one filterable string —
you can `git checkout` from any event.
`SentryGlobalScope` interface gains `setContext(name, context)`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bake content-hashed debug IDs into each per-platform/variant rollup output via @sentry/rollup-plugin (in disable-upload mode), relocate the *.map files to sibling nodejs-sourcemaps/ dirs so they ship in the npm tarball without being packaged into the APK/IPA, and add a `comapeo-rn-upload-sourcemaps` bin CLI consumers run from CI to push the maps to their own Sentry project. The relocate-sourcemaps rollup plugin also injects `debug_id`/`debugId` into each map JSON and appends the spec-compliant `//# debugId=` trailer to each bundle, since sentry-rollup-plugin's upload pipeline (which normally does both) is disabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address review feedback: - The header comment claimed `--debug-ids` mode; that flag doesn't exist in sentry-cli 2.x (debug-ID upload is the default). Reword. - Pass `--no-rewrite` so sentry-cli skips its `discover_sourcemaps_location` walk: the bundle's `//# sourceMappingURL=index.mjs.map` reference is dangling because we relocate the map to a sibling `nodejs-sourcemaps/` dir, and the rewrite step would warn and no-op. The debug IDs are already embedded in both files, which is exactly what `--no-rewrite` is documented for. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the regex over `_sentryDebugIdIdentifier` (an implementation detail of @sentry/bundler-plugin-core's runtime snippet) with a tiny `captureDebugIdsPlugin` that calls the same publicly-exported `stringToUUID` helper @sentry/rollup-plugin uses internally. Plugin order ensures both see the same `code` parameter in renderChunk, so the IDs are identical by construction; relocateSourcemapsPlugin reads the captured IDs from a per-config Map keyed by chunk filename. This swaps coupling-to-snippet-format for coupling-to-public-helper. Re-publishes still produce identical IDs to the previous regex-based build (verified: 580d8e47/4f24a4e4/f327fcbf for android-debug/main/ios). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t dep Four follow-ups from review: - Move captureDebugIdsPlugin + relocateSourcemapsPlugin into backend/rollup-plugins/rollup-plugin-sentry-debug-ids.js, matching the existing convention (JSDoc-typed JS like rollup-plugin-addon-loader). - Drop @sentry/bundler-plugin-core from devDependencies and import it transitively via @sentry/rollup-plugin. Always-same-version is a stronger guarantee than two pinned 5.2.1's that could drift. - Add a `bundleSource.includes(debugId)` sanity check in the relocate step. If the @sentry/bundler-plugin-core stringToUUID algorithm ever diverges from what sentry-rollup-plugin uses internally, or if plugin order regresses, the build fails loudly instead of silently shipping bundles whose snippet ID doesn't match the sourcemap's debug_id. The check is on the UUID string itself, not on the snippet shape, so it survives snippet renames. - Replace JSON.parse + JSON.stringify on the sourcemap with a string splice (find the trailing `}`, insert `,"debug_id":"…","debugId":"…"` before it). Avoids ~600ms-1s of CPU on the 24M map per build, and preserves rollup's original byte-for-byte output for everything else in the file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three review fixes: - Tighten @sentry/react-native peerDep to ^7.13.0 (matches the dev dep we test against and the version that ships sentry-android-core 8.32.x — the structured-log API SentryFgsBridgeImpl.log requires). Previous ^7 admits 7.0.x ships of sentry-android that lack the API. - Add SentryFgsBridge.flush(timeoutMillis) and call it from ComapeoCoreService.onDestroy before Process.killProcess. The "FGS stop timeout fired" capture is async-queued; without an explicit flush, the network send races the process kill. Bound at 2s so we don't stall shutdown noticeably. Bridge keeps the standard guard pattern — no-op when sentry-android isn't on the classpath. - Reorder NodeJSService.applyAndEmit to drain in-flight phase spans BEFORE finishing the boot transaction. Sentry closes the span tree when the parent transaction finishes — child spans finished after the parent are dropped. The single-owner getAndSet(null) pattern on the parent is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sub-export's side-effect import is enough to confirm end-to-end
delivery on cold start (state-transition breadcrumbs and any error
captures land on the host's Sentry hub). The two smoke-test calls
were marked TEMPORARY and only added noise.
DSN is intentionally kept hardcoded — Sentry docs explicitly say
DSNs are safe to keep public ("they only allow submission of new
events and related event data; they do not allow read access").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gmaclennan
added a commit
that referenced
this pull request
Jun 22, 2026
## Optic Release Automation This **draft** PR is opened by Github action [optic-release-automation-action](https://github.com/nearform-actions/optic-release-automation-action). A new **draft** GitHub release [v1.0.0-pre.2](https://github.com/digidem/comapeo-core-react-native/releases/tag/untagged-c499977757c9745e56b2) has been created. Release author: @gmaclennan #### If you want to go ahead with the release, please merge this PR. When you merge: - The GitHub release will be published - The npm package with tag pre will be published according to the publishing rules you have configured - No major or minor tags will be updated as configured #### If you close the PR - The new draft release will be deleted and nothing will change ## What's Changed * Android Testing Infrastructure & Bug Fixes by @gmaclennan in #3 * chore: prebuild example/android; harden instrumented tests by @gmaclennan in #10 * Integrate @comapeo/core via IPC over Unix sockets by @gmaclennan in #5 * chore: adjust repo setup by @achou11 in #12 * chore: minor fixes based on expo-doctor by @achou11 in #13 * Add iOS support & test infrastructure by @gmaclennan in #6 * chore: add architecture docs & plans by @gmaclennan in #11 * update some native deps used in backend by @achou11 in #14 * iOS Phase 1: unified JS bundle + smoke test (simulator-only) by @gmaclennan in #15 * iOS Phase 2: xcframework Embed & Sign for native addons by @gmaclennan in #16 * Phase 2 Android: jniLibs packaging + unified rollup loader plugin by @gmaclennan in #17 * chore: post-Phase-2 cleanup — comments, plan docs, agents.md by @gmaclennan in #33 * android: read abiFilters from reactNativeArchitectures (#30) by @gmaclennan in #35 * refactor: simplify build-backend.ts; rollup writes directly to native asset trees by @gmaclennan in #34 * chore: fix eslint configuration by @achou11 in #41 * android: audit 16 KB page alignment on every shipped .so by @gmaclennan in #43 * Add rootkey persistence and lifecycle state management by @gmaclennan in #36 * chore: move example app into apps directory by @achou11 in #18 * refactor: per-component lifecycle state with derived ComapeoState by @gmaclennan in #47 * android: fold waitForFile into connect retry loop by @gmaclennan in #52 * chore: add e2e testing app by @achou11 in #49 * fix(android): drop setUnlockedDeviceRequired from rootkey wrapper key by @gmaclennan in #57 * fix(backend): cache stopping/error frames for late joiners by @gmaclennan in #58 * fix(ios-tests): wait for STOPPING before signalling node exit by @gmaclennan in #59 * fix(android): drain JNI stdio pumps before returning from node::Start by @gmaclennan in #60 * Sentry integration: Phase 1 + Phase 2a + Phase 2b by @gmaclennan in #54 * feat(backend): polywasm-backed undici on iOS, re-enable maps plugin by @gmaclennan in #62 * ci: drop unreliable Android emulator snapshot caching by @gmaclennan in #64 * feat(sentry): land Phase 3 — backend loader + RPC tracing by @gmaclennan in #63 * fix(ios-tests): serialise STOPPING/STOPPED observers in testFullLifecycleStateTransitions by @gmaclennan in #71 * use npm list instead of custom traversal to get native module versions by @achou11 in #70 * feat(sentry): land Phases 6 + 7a — Android exit reasons & iOS MetricKit app-exit telemetry by @gmaclennan in #72 * fix(sentry): make exit telemetry lossless and stop cross-process clobbering by @gmaclennan in #84 * chore(e2e): add e2e tests on browserstack via Maestro by @achou11 in #56 * feat(sentry): migrate to @sentry/react-native v8; exit telemetry as Application Metrics by @gmaclennan in #73 * Map server integration by @gmaclennan in #86 * chore(deps): upgrade to Expo SDK 56 (React Native 0.85) by @gmaclennan in #87 * chore(ci): add release workflow by @gmaclennan in #90 * chore: fix npm script and release build script by @gmaclennan in #91 * chore(pack): don't try to package build files by @gmaclennan in #92 * fix: start fastify listening by @gmaclennan in #93 * perf(backend): switch bundler from rollup to rolldown by @gmaclennan in #94 * fix(ci): ignore-scripts in ios npm installs by @gmaclennan in #96 * fix(ci): replace --ignore-scripts with npm strict-allow-scripts allowlist by @gmaclennan in #106 * feat(config): let the consuming app supply the default project config by @gmaclennan in #95 * chore(release): merge prerelease branch. by @gmaclennan in #110 ## New Contributors * @achou11 made their first contribution in #12 **Full Changelog**: https://github.com/digidem/comapeo-core-react-native/commits/v1.0.0-pre.2 <!-- <release-meta>{"id":342868678,"version":"v1.0.0-pre.2","npmTag":"pre","opticUrl":"https://optic-zf3votdk5a-ew.a.run.app/api/generate/"}</release-meta> -->
gmaclennan
added a commit
that referenced
this pull request
Jun 22, 2026
## Optic Release Automation This **draft** PR is opened by Github action [optic-release-automation-action](https://github.com/nearform-actions/optic-release-automation-action). A new **draft** GitHub release [v1.0.0-pre.2](https://github.com/digidem/comapeo-core-react-native/releases/tag/untagged-352a6c41c12fd02dec37) has been created. Release author: @gmaclennan #### If you want to go ahead with the release, please merge this PR. When you merge: - The GitHub release will be published - The npm package with tag pre will be published according to the publishing rules you have configured - No major or minor tags will be updated as configured #### If you close the PR - The new draft release will be deleted and nothing will change <!-- Release notes generated using configuration in .github/release.yml at 7fe80b4 --> ## What's Changed ### 🚀 Features * Integrate @comapeo/core via IPC over Unix sockets by @gmaclennan in #5 * Add iOS support & test infrastructure by @gmaclennan in #6 * iOS Phase 1: unified JS bundle + smoke test (simulator-only) by @gmaclennan in #15 * iOS Phase 2: xcframework Embed & Sign for native addons by @gmaclennan in #16 * Phase 2 Android: jniLibs packaging + unified rollup loader plugin by @gmaclennan in #17 * android: read abiFilters from reactNativeArchitectures (#30) by @gmaclennan in #35 * Add rootkey persistence and lifecycle state management by @gmaclennan in #36 * Sentry integration: Phase 1 + Phase 2a + Phase 2b by @gmaclennan in #54 * feat(backend): polywasm-backed undici on iOS, re-enable maps plugin by @gmaclennan in #62 * feat(sentry): land Phase 3 — backend loader + RPC tracing by @gmaclennan in #63 * feat(sentry): land Phases 6 + 7a — Android exit reasons & iOS MetricKit app-exit telemetry by @gmaclennan in #72 * feat(sentry): migrate to @sentry/react-native v8; exit telemetry as Application Metrics by @gmaclennan in #73 * Map server integration by @gmaclennan in #86 * feat(config): let the consuming app supply the default project config by @gmaclennan in #95 ### 🐛 Bug Fixes * fix(android): drop setUnlockedDeviceRequired from rootkey wrapper key by @gmaclennan in #57 * fix(backend): cache stopping/error frames for late joiners by @gmaclennan in #58 * fix(ios-tests): wait for STOPPING before signalling node exit by @gmaclennan in #59 * fix(android): drain JNI stdio pumps before returning from node::Start by @gmaclennan in #60 * fix(ios-tests): serialise STOPPING/STOPPED observers in testFullLifecycleStateTransitions by @gmaclennan in #71 * fix(sentry): make exit telemetry lossless and stop cross-process clobbering by @gmaclennan in #84 * fix: start fastify listening by @gmaclennan in #93 * fix(ci): ignore-scripts in ios npm installs by @gmaclennan in #96 * fix(ci): replace --ignore-scripts with npm strict-allow-scripts allowlist by @gmaclennan in #106 * fix(release): stop `npm pack --dry-run` leaking dry-run into backend install by @gmaclennan in #129 ### ⚡ Performance * perf(backend): switch bundler from rollup to rolldown by @gmaclennan in #94 ### ⬆️ Dependencies * update some native deps used in backend by @achou11 in #14 * chore(deps): upgrade to Expo SDK 56 (React Native 0.85) by @gmaclennan in #87 ### 🏗️ Maintenance * Android Testing Infrastructure & Bug Fixes by @gmaclennan in #3 * chore: prebuild example/android; harden instrumented tests by @gmaclennan in #10 * chore: adjust repo setup by @achou11 in #12 * chore: minor fixes based on expo-doctor by @achou11 in #13 * chore: add architecture docs & plans by @gmaclennan in #11 * chore: post-Phase-2 cleanup — comments, plan docs, agents.md by @gmaclennan in #33 * refactor: simplify build-backend.ts; rollup writes directly to native asset trees by @gmaclennan in #34 * chore: fix eslint configuration by @achou11 in #41 * android: audit 16 KB page alignment on every shipped .so by @gmaclennan in #43 * chore: move example app into apps directory by @achou11 in #18 * refactor: per-component lifecycle state with derived ComapeoState by @gmaclennan in #47 * android: fold waitForFile into connect retry loop by @gmaclennan in #52 * chore: add e2e testing app by @achou11 in #49 * ci: drop unreliable Android emulator snapshot caching by @gmaclennan in #64 * use npm list instead of custom traversal to get native module versions by @achou11 in #70 * chore(e2e): add e2e tests on browserstack via Maestro by @achou11 in #56 * chore(ci): add release workflow by @gmaclennan in #90 * chore: fix npm script and release build script by @gmaclennan in #91 * chore(pack): don't try to package build files by @gmaclennan in #92 * chore(release): merge prerelease branch. by @gmaclennan in #110 * ci(e2e): retry BrowserStack builds on infra-class flakes by @gmaclennan in #113 ### Other Changes * ci: derive changelog labels from PR titles + add Dependabot by @gmaclennan in #114 ## New Contributors * @achou11 made their first contribution in #12 * @optic-release-automation[bot] made their first contribution in #112 **Full Changelog**: https://github.com/digidem/comapeo-core-react-native/commits/v1.0.0-pre.2 <!-- <release-meta>{"id":342970724,"version":"v1.0.0-pre.2","npmTag":"pre","opticUrl":"https://optic-zf3votdk5a-ew.a.run.app/api/generate/"}</release-meta> -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Phases 1, 2a, 2b, and the sourcemap-upload prerequisite for Phase 3 of the Sentry integration plan for
@comapeo/core-react-native. Sentry is opt-in — consumers who don't register the Expo plugin and don't import the@comapeo/core-react-native/sentrysub-export pay nothing (no DSN in their APK/IPA, noio.sentryclasses loaded at runtime, no captures fire from this module). Phases 4, 5, 6 remain pending and are described in the plan.Companion docs:
docs/sentry-integration-plan.md— full design + per-phase status (§10)docs/ARCHITECTURE.md§7 — architectural overview of the integrationWhat ships
Phase 1 — JS-side adapter
src/sentry.ts+src/sentry-internal.ts— public sub-export@comapeo/core-react-native/sentry. Importing the sub-export as a side effect attaches state listeners that emit acomapeo.statebreadcrumb on every transition and acaptureException(taggedproc:main,layer:rn,comapeo.phase:<phase>) on ERROR.messageerrorparse failures captured as warning-level with truncated payload. The module auto-detects the host's already-initialised@sentry/react-native— there's noconfigureSentryhandoff. Tests inject a fake adapter viasetSentryAdapterForTests(adapter).comapeo.rntag carries<version>+git<sha>[-dirty<hash>]andevent.modulescarries@comapeo/core-react-native. AcomapeoBackendcontext block carries the pinned versions of the rolled-up backend deps (@comapeo/coreetc.) so Sentry events are filterable by either bound.Sentry.init(...)even when their init runs after our side-effect import (Metro hoists ESM imports above non-import code).SentryAdapteris hand-rolled (notPick<typeof @sentry/...>) so consumers without the optional@sentry/react-nativepeer dep don't get a typecheck error from importing the sub-export.package.json—@sentry/react-native@^7.13.0declared as optional peer dep (matches the floor needed bySentryFgsBridgeImpl.log's structured-log API). Newexportsfield surfaces./sentryand./app.plugin.Phase 2a — Expo config plugin + native readers
app.plugin.js— root-level Expo plugin. Atexpo prebuildit writes Sentry config (DSN, environment, release, sample rates,captureApplicationDataDefault,enableLogs) into Android manifest meta-data and iOS Info.plist keys. Validatesdsn+environmentat prebuild time. Idempotent: droppingprops.sentryon a later prebuild strips stale entries (the original draft left them behind on--no-clean).SentryConfig.kt+SentryConfig.swift— typed readers. Default release falls back toversionName + "+" + versionCode(AndroidlongVersionCode) /CFBundleShortVersionString + "+" + CFBundleVersion(iOS) so successive EAS builds of the same marketing version produce distinct Sentry releases. Missing environment with DSN present logs and returnsnull(Sentry off) rather than crashing.load(metaString, defaultRelease)overload makes the parser unit-testable on the JVM classpath without mockingandroid.os.Bundle.Phase 2b — FGS-process Sentry SDK + native captures (Android only)
iOS doesn't need a Phase 2b — single-process app, host's
@sentry/react-nativealready covers it. On Android the:ComapeoCoreFGS process gets a freshApplicationand an empty Sentry hub;@sentry/react-native's init only fires in the main process. Without this, FGS-originated errors carry no logcat tail / foreground-state context and the boot transaction is unobservable.io.sentry:sentry-android-core:8.32.0added toandroid/build.gradleascompileOnlyso this module never pulls sentry-android into consumers who don't use Sentry. The runtime classes come transitively from@sentry/react-native@^7.13.0. Pin matches what@sentry/react-native@7.xships, and is the first line that has the structured-log API used bySentryFgsBridgeImpl.log.SentryFgsBridge.kt(Guard) +SentryFgsBridgeImpl.kt(Impl) split. The guard'sClass.forName(name, initialize=false, …)probe gates every public method; consumers without sentry-android on the runtime classpath get a clean no-op rather thanNoClassDefFoundError. Impl freely importsio.sentry.*and is only loaded after the guard passes.SentryAndroid.initinComapeoCoreService.onCreatekeyed offSentryConfig.loadFromManifest. Setsproc:fgsandlayer:nativeas process-level tags so the dashboard splits FGS captures from main-process captures (which carryproc:mainfromsrc/sentry.ts).comapeo.boottransaction opened inNodeJSService.start(), closed on first STARTED (ok) / ERROR (internal_error) / STOPPING|STOPPED (cancelled) per the lifecycle state machine. Phase spans drain BEFORE the parent transaction finishes (Sentry closes the span tree on parent finalization).boot.rootkey-load,boot.init-frame) — span operation names match the bench branch'sboot.<phase>taxonomy inapps/benchmark/backend/lib/boot-spans.jsso a single Sentry dashboard query charts both sides when the bench branch lands.TracesSamplingDecision(true, 1.0)on theTransactionContext, per plan §7.4.2 — boot is once-per-process and high value, so it reaches the wire even when the globaltracesSampleRateis 0.0.NodeJSService.applyAndEmit, control-frame breadcrumbs (started/ready/stopping/error/malformed) inhandleControlMessage, FGS-lifecycle breadcrumbs (onCreate/onStartCommand/onDestroy), IPC connection-state breadcrumbs fromNodeJSIPC.onConnectionStateChange.comapeo: startup timeout fired(timeout:startup),comapeo: FGS stop timeout fired(timeout:fgsStop, taggedcomapeo.phase:shutdown-timeout), andcomapeo: error-native frame dropped(timeout:errorNativeForward) per plan §7.4.4.Process.killProcessinComapeoCoreService.onDestroy. Bounded at 2s — long enough to deliver the FGS-stop-timeout capture under typical conditions, short enough not to noticeably stall shutdown. No-op when sentry-android isn't on the classpath.captureExceptionon rootkey-load failure taggedcomapeo.phase:rootkey,source:rootkey-store. Fires beforesendErrorNativeFrameso the FGS scope is intact; the same exception is re-broadcast to Node and re-captured by the main-process JS adapter for the cross-process triple per §7.4.7.Phase 3 prep — Backend bundle sourcemap upload
The Node backend bundle (
index.mjsper platform/variant, rolled up at ourprepack) ships minified, so without sourcemaps any Phase 3 captures would be unreadable in Sentry.@sentry/rollup-pluginindisable-uploadmode.captureDebugIdsPluginruns before sentry-rollup-plugin inrenderChunkand computes the same ID via the sharedstringToUUID(code)helper so the IDs match by construction without regexing on the runtime snippet's format.relocateSourcemapsPluginmoves*.mapfiles out of each per-platform output dir into a siblingnodejs-sourcemaps/dir before APK/IPA packaging. Maps stay in the npm tarball; APK is ~10 MB lighter per release install. The plugin also injectsdebug_id/debugId(snake_case + camelCase, for sentry-cli back-compat) into the map JSON via string splice (no JSON parse/stringify round-trip on the multi-MB map) and appends the spec-compliant//# debugId=trailer to each bundle.comapeo-rn-upload-sourcemapsCLI (newbininpackage.json). Consumers run it from their CI / EAS pipeline with their ownSENTRY_AUTH_TOKEN— the upload targets the consumer's Sentry project (debug-ID symbolication is independent of the consumer'srelease). Resolves@sentry/clivia the consumer's transitive@sentry/react-nativeinstall.Tests
26 JVM unit tests across:
SentryConfigTest(9) — manifest reader contract, default release fallback, numeric coercion, strict bool parsing, missing-environment fail-soft.SentryFgsBridgeTest(10) — pre-init no-op guard contract for every public method;Class.forNameprobe withinitialize=false.SentryFgsBridgeImplTest(7) — post-init paths against an in-memoryITransport. Covers breadcrumb queueing,captureException/captureMessageenvelope shape, the forced-sampling override, boot span lifecycle,cancelledstatus mapping, unknown-level fallback.Plus matching iOS XCTest suite (
ios/Tests/SentryConfigTests.swift).Documentation
docs/sentry-integration-plan.md— design plan with per-phase status table at §10. Phases 1, 2a, 2b marked landed; 4, 5, 6 marked pending.docs/ARCHITECTURE.md— §7 "Sentry observability (optional)" with the three-event-streams diagram, build-time config flow, Guard/Impl rationale, and what we deliberately don't attempt (nosentry-nativein nodejs-mobile; no PII capture).README.md— "Optional: Sentry integration" section covering@sentry/react-nativeinstall, Expo plugin registration with EAS env-var pattern, side-effect import of the sub-export, "What gets captured automatically", and the sourcemap-upload CI step.eslint.config.js— ignore.claude/**/*so Claude Code worktree artefacts don't pollute the lint cache (orthogonal to Sentry; one-line fix).Pending phases
Tracked in plan §10 and intentionally not in this PR:
loader.mjs+@sentry/node+ RPC tracing. Sourcemap-upload prereq landed in this PR.@comapeo/coreOpenTelemetry forwarding (blocked on@comapeo/corePR DO_NOT_MERGE: add instrumentation comapeo-core#1051).SharedPreferences/UserDefaultsstore, restart-to-activate semantics).Test plan
npm run lintcleannpx tsc --noEmitcleancd apps/example/android && ./gradlew :comapeo-core-react-native:testDebugUnitTestpasses./gradlew :comapeo-core-react-native:compileDebugKotlinsucceeds with sentry-android on the compile classpathnpm run backend:build— three rollup outputs each have matching debug IDs in bundle snippet, bundle trailer, and map keyscomapeo-rn-upload-sourcemaps --org x --project yinvokessentry-clicorrectly (verified end-to-end against a fake token, expected 401 from API)cd ios && swift test(requires macOS — run by reviewer)🤖 Generated with Claude Code