Skip to content

Phase 1: Sentry integration plan and JS-side adapter#54

Open
gmaclennan wants to merge 6 commits intomainfrom
claude/plan-sentry-integration-9dt0T
Open

Phase 1: Sentry integration plan and JS-side adapter#54
gmaclennan wants to merge 6 commits intomainfrom
claude/plan-sentry-integration-9dt0T

Conversation

@gmaclennan
Copy link
Copy Markdown
Member

Summary

This PR implements Phase 1 of the Sentry integration plan for @comapeo/core-react-native, establishing the foundation for opt-in error reporting and RPC tracing. The integration is designed to be app-driven and zero-cost for consumers who don't use it.

Key Changes

Documentation & Architecture

  • docs/sentry-integration-plan.md (2073 lines): Comprehensive design document covering:
    • Goals and non-goals for the integration
    • Layered architecture across JS, Node backend, and native layers
    • Configuration strategy (Expo plugin, native config, JS adapter handoff)
    • Backend instrumentation patterns
    • RPC tracing design
    • Native error capture and telemetry

JS-Side Implementation

  • src/sentry.ts: Public sub-export (@comapeo/core-react-native/sentry) providing:

    • SentryAdapter interface: hand-rolled type contract matching @sentry/react-native@^6 methods without hard dependency
    • configureSentry() function: one-shot handoff for the host app's already-initialized Sentry client
    • No Sentry SDK imports; purely structural typing to avoid forcing installation
  • src/sentry-internal.ts: Internal state management for the active Sentry adapter

Expo Config Plugin

  • app.plugin.js (217 lines): Build-time plugin that:
    • Reads consumer's Sentry config (DSN, environment, release, sample rates)
    • Writes Android manifest meta-data (com.comapeo.core.sentry.* keys)
    • Writes iOS Info.plist keys (ComapeoCore* prefixed to avoid collisions)
    • Validates required fields at prebuild time
    • Is a no-op when not registered (zero Sentry footprint for non-adopters)

Native Configuration Types

  • android/src/main/java/com/comapeo/core/SentryConfig.kt: Typed data class and loader for Android manifest meta-data

    • Reads from PackageManager.getApplicationInfo().metaData
    • Shared across main process and :ComapeoCore FGS process
    • Defaults release to versionName+versionCode when not explicitly set
  • ios/SentryConfig.swift: Equivalent iOS implementation reading from Bundle.main.infoDictionary

    • Defaults release to CFBundleShortVersionString+CFBundleVersion

Test Coverage

  • android/src/test/java/com/comapeo/core/SentryConfigTest.kt: JVM unit tests for Android config deserialization
  • ios/Tests/SentryConfigTests.swift: Swift unit tests for iOS config deserialization
  • Both test the seam between plugin writes and native consumers

Package Configuration

  • package.json: Added conditional exports for the new ./sentry sub-export
  • ios/Package.swift: Added SentryConfig.swift to the library target
  • README.md: Added optional Sentry integration section with setup instructions

Implementation Details

Design Principles

  1. Opt-in and inert by default: No Sentry code path executes unless the consumer registers the plugin with a DSN
  2. App-driven configuration: The host app controls DSN, environment, and sample rates via the Expo plugin
  3. No hard SDK dependency: This module never imports @sentry/react-native or @sentry/node; it accepts a Sentry-shaped adapter object
  4. Build-time config: DSN and environment are baked into native config at prebuild time, enabling FGS cold-start without JS round-trip

Configuration Flow

  • Consumer registers plugin in app.config.js with DSN and environment from build-time sources (EAS env vars)
  • Plugin writes values to Android manifest and iOS plist at expo prebuild
  • Native reads config at process start (before any IPC)
  • JS calls `configureS

https://claude.ai/code/session_01EcVXzczA1TVkhEkgUg9DKX

claude added 5 commits May 1, 2026 00:08
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
@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 1, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​sentry/​react-native@​6.22.0971009996100

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants