Skip to content

Sentry integration: Phase 1 + Phase 2a + Phase 2b#54

Merged
gmaclennan merged 37 commits into
mainfrom
claude/plan-sentry-integration-9dt0T
May 7, 2026
Merged

Sentry integration: Phase 1 + Phase 2a + Phase 2b#54
gmaclennan merged 37 commits into
mainfrom
claude/plan-sentry-integration-9dt0T

Conversation

@gmaclennan

@gmaclennan gmaclennan commented May 1, 2026

Copy link
Copy Markdown
Member

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/sentry sub-export pay nothing (no DSN in their APK/IPA, no io.sentry classes 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 integration

What 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 a comapeo.state breadcrumb on every transition and a captureException (tagged proc:main, layer:rn, comapeo.phase:<phase>) on ERROR. messageerror parse failures captured as warning-level with truncated payload. The module auto-detects the host's already-initialised @sentry/react-native — there's no configureSentry handoff. Tests inject a fake adapter via setSentryAdapterForTests(adapter).
  • Module-version bookkeeping: comapeo.rn tag carries <version>+git<sha>[-dirty<hash>] and event.modules carries @comapeo/core-react-native. A comapeoBackend context block carries the pinned versions of the rolled-up backend deps (@comapeo/core etc.) so Sentry events are filterable by either bound.
  • All scope writes go to the global scope (singleton from module load) rather than the current/isolation scope. Survives the consumer's Sentry.init(...) even when their init runs after our side-effect import (Metro hoists ESM imports above non-import code).
  • SentryAdapter is hand-rolled (not Pick<typeof @sentry/...>) so consumers without the optional @sentry/react-native peer dep don't get a typecheck error from importing the sub-export.
  • package.json@sentry/react-native@^7.13.0 declared as optional peer dep (matches the floor needed by SentryFgsBridgeImpl.log's structured-log API). New exports field surfaces ./sentry and ./app.plugin.

Phase 2a — Expo config plugin + native readers

  • app.plugin.js — root-level Expo plugin. At expo prebuild it writes Sentry config (DSN, environment, release, sample rates, captureApplicationDataDefault, enableLogs) into Android manifest meta-data and iOS Info.plist keys. Validates dsn + environment at prebuild time. Idempotent: dropping props.sentry on 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 to versionName + "+" + versionCode (Android longVersionCode) / CFBundleShortVersionString + "+" + CFBundleVersion (iOS) so successive EAS builds of the same marketing version produce distinct Sentry releases. Missing environment with DSN present logs and returns null (Sentry off) rather than crashing.
  • Pure load(metaString, defaultRelease) overload makes the parser unit-testable on the JVM classpath without mocking android.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-native already covers it. On Android the :ComapeoCore FGS process gets a fresh Application and 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.0 added to android/build.gradle as compileOnly so 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.x ships, and is the first line that has the structured-log API used by SentryFgsBridgeImpl.log.
  • SentryFgsBridge.kt (Guard) + SentryFgsBridgeImpl.kt (Impl) split. The guard's Class.forName(name, initialize=false, …) probe gates every public method; consumers without sentry-android on the runtime classpath get a clean no-op rather than NoClassDefFoundError. Impl freely imports io.sentry.* and is only loaded after the guard passes.
  • FGS-process SentryAndroid.init in ComapeoCoreService.onCreate keyed off SentryConfig.loadFromManifest. Sets proc:fgs and layer:native as process-level tags so the dashboard splits FGS captures from main-process captures (which carry proc:main from src/sentry.ts).
  • comapeo.boot transaction opened in NodeJSService.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).
  • Phase spans (boot.rootkey-load, boot.init-frame) — span operation names match the bench branch's boot.<phase> taxonomy in apps/benchmark/backend/lib/boot-spans.js so a single Sentry dashboard query charts both sides when the bench branch lands.
  • Forced 100% sampling for the boot transaction via TracesSamplingDecision(true, 1.0) on the TransactionContext, per plan §7.4.2 — boot is once-per-process and high value, so it reaches the wire even when the global tracesSampleRate is 0.0.
  • State-transition breadcrumbs in NodeJSService.applyAndEmit, control-frame breadcrumbs (started/ready/stopping/error/malformed) in handleControlMessage, FGS-lifecycle breadcrumbs (onCreate/onStartCommand/onDestroy), IPC connection-state breadcrumbs from NodeJSIPC.onConnectionStateChange.
  • Timeout eventscomapeo: startup timeout fired (timeout:startup), comapeo: FGS stop timeout fired (timeout:fgsStop, tagged comapeo.phase:shutdown-timeout), and comapeo: error-native frame dropped (timeout:errorNativeForward) per plan §7.4.4.
  • Synchronous flush before Process.killProcess in ComapeoCoreService.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.
  • 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 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.mjs per platform/variant, rolled up at our prepack) ships minified, so without sourcemaps any Phase 3 captures would be unreadable in Sentry.

  • Deterministic, content-hashed Sentry debug IDs baked into each bundle via @sentry/rollup-plugin in disable-upload mode. captureDebugIdsPlugin runs before sentry-rollup-plugin in renderChunk and computes the same ID via the shared stringToUUID(code) helper so the IDs match by construction without regexing on the runtime snippet's format.
  • relocateSourcemapsPlugin moves *.map files out of each per-platform output dir into a sibling nodejs-sourcemaps/ dir before APK/IPA packaging. Maps stay in the npm tarball; APK is ~10 MB lighter per release install. The plugin also injects debug_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-sourcemaps CLI (new bin in package.json). Consumers run it from their CI / EAS pipeline with their own SENTRY_AUTH_TOKEN — the upload targets the consumer's Sentry project (debug-ID symbolication is independent of the consumer's release). Resolves @sentry/cli via the consumer's transitive @sentry/react-native install.

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.forName probe with initialize=false.
  • SentryFgsBridgeImplTest (7) — post-init paths against an in-memory ITransport. Covers breadcrumb queueing, captureException/captureMessage envelope shape, the forced-sampling override, boot span lifecycle, cancelled status 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 (no sentry-native in nodejs-mobile; no PII capture).
  • README.md — "Optional: Sentry integration" section covering @sentry/react-native install, 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:

  • Phase 3 — backend loader.mjs + @sentry/node + RPC tracing. Sourcemap-upload prereq landed in this PR.
  • Phase 4@comapeo/core OpenTelemetry forwarding (blocked on @comapeo/core PR DO_NOT_MERGE: add instrumentation comapeo-core#1051).
  • Phase 5 — capture-application-data toggle (SharedPreferences / UserDefaults store, restart-to-activate semantics).
  • Phase 6 — sample-rate tuning from real data; optional dual bundle if size matters.

Test plan

  • npm run lint clean
  • npx tsc --noEmit clean
  • cd apps/example/android && ./gradlew :comapeo-core-react-native:testDebugUnitTest passes
  • ./gradlew :comapeo-core-react-native:compileDebugKotlin succeeds with sentry-android on the compile classpath
  • Backend builds: npm run backend:build — three rollup outputs each have matching debug IDs in bundle snippet, bundle trailer, and map keys
  • CLI smoke test: comapeo-rn-upload-sourcemaps --org x --project y invokes sentry-cli correctly (verified end-to-end against a fake token, expected 401 from API)
  • iOS Swift tests via cd ios && swift test (requires macOS — run by reviewer)
  • Manual smoke test on a real Android device with a test Sentry project DSN — exercise the rootkey-failure path, the watchdog timeout, and a graceful stop to verify all three event streams land in the dashboard
  • Real upload of sourcemaps to a test Sentry project + symbolicated event verification

🤖 Generated with Claude Code

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

socket-security Bot commented May 1, 2026

Copy link
Copy Markdown

gmaclennan added 5 commits May 1, 2026 11:47
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
@gmaclennan gmaclennan changed the title Phase 1: Sentry integration plan and JS-side adapter Sentry integration: Phase 1 + Phase 2a + Phase 2b May 7, 2026
gmaclennan added 4 commits May 7, 2026 10:30
…, 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).
gmaclennan and others added 2 commits May 7, 2026 12:20
…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/sentry-integration-plan.md
Comment thread docs/ARCHITECTURE.md Outdated
Comment thread src/sentry-internal.ts Outdated
Comment thread src/sentry-tags.ts
Comment thread android/src/main/java/com/comapeo/core/ComapeoCoreService.kt Outdated
Comment thread ios/NodeJSService.swift Outdated
gmaclennan and others added 2 commits May 7, 2026 12:36
- 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.md and ./docs/sentry-integration-plan.md, but docs/ is not included in the package files whitelist. When published to npm those links will 404/miss unless you either add docs/ to files or 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/"
  ],

Comment thread docs/sentry-integration-plan.md Outdated
Comment thread docs/sentry-integration-plan.md Outdated
Comment thread android/src/test/java/com/comapeo/core/SentryFgsBridgeTest.kt
Comment thread src/sentry.ts
gmaclennan and others added 4 commits May 7, 2026 13:02
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>
gmaclennan and others added 12 commits May 7, 2026 14:15
… 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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 41 changed files in this pull request and generated 7 comments.

Files not reviewed (2)
  • apps/example/package-lock.json: Language not supported
  • backend/package-lock.json: Language not supported

Comment thread android/src/main/java/com/comapeo/core/NodeJSService.kt Outdated
Comment thread android/src/main/java/com/comapeo/core/ComapeoCoreService.kt Outdated
Comment thread apps/example/index.ts
Comment thread apps/example/index.ts Outdated
Comment thread apps/example/app.json
Comment thread package.json
Comment thread src/sentry.ts
gmaclennan and others added 3 commits May 7, 2026 21:25
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 gmaclennan enabled auto-merge (squash) May 7, 2026 20:38
@gmaclennan gmaclennan merged commit f67aa88 into main May 7, 2026
7 checks passed
@gmaclennan gmaclennan deleted the claude/plan-sentry-integration-9dt0T branch May 7, 2026 20:50
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 gmaclennan added the feature New feature (changelog) label Jun 22, 2026
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>
-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature (changelog)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants