feat(sentry): land Phase 3 — backend loader + RPC tracing#63
Open
gmaclennan wants to merge 12 commits into
Open
feat(sentry): land Phase 3 — backend loader + RPC tracing#63gmaclennan wants to merge 12 commits into
gmaclennan wants to merge 12 commits into
Conversation
Wire the Node-backend into Sentry: argv-driven loader spawns Sentry
before importing the app, RPC method-level errors capture with full
stack traces, and JS-side IPC spans propagate sentry-trace/baggage
to the backend so both halves of an RPC land on the same trace.
Backend
- New loader.mjs spawn target parses --sentryDsn/etc and conditionally
inits @sentry/node before dynamically importing ./index.js. Required
ordering for OpenTelemetry's import-in-the-middle hook to instrument
modules.
- rollup.config.ts now emits multi-entry output (loader/index/importHook
/lib/register) with a code-split chunks/ subdir for @sentry/node and
transitive deps. .js extension forced for importHook/lib/register so
import-in-the-middle's hardcoded relative paths resolve.
- New rollup-plugin-import-hook.mjs rewrites
module.register('import-in-the-middle/hook.mjs', ...) to point at
the bundled ./importHook.js. Ported from comapeo-mobile.
- relocate-sourcemaps walks subdirs recursively (lib/, chunks/) and
no-ops gracefully on auto-emitted helper chunks that bypass
captureDebugIdsPlugin.
- index.js reads globalThis.__comapeoSentry / __comapeoSentryConfig
set by the loader; wires handleFatal to Sentry.captureException
(with source:native tag for FGS-forwarded errors) and a 100ms flush
before process.exit. ComapeoRpcServer accepts {sentry,rpcArgsBytes}
and registers an onRequestHook that wraps each RPC in a transaction
with continueTrace from request metadata. rpc.args is gated behind
an opt-in byte cap (PII risk).
JS/RN module
- sentry-internal no longer statically imports @sentry/react-native.
The sub-export now calls registerAdapter(Sentry) at module load,
so consumers without the optional peer dep don't break when the
main barrel imports sentry-internal for RPC tracing.
- ComapeoCoreModule.ts registers an onRequestHook on the
createMapeoClient call. Inert when no adapter is registered or no
active root span; otherwise startSpan + getTraceData populates
request.metadata for the backend hook to continueTrace.
- SentryAdapter gains getTraceData (lives on @sentry/core, not
re-exported by RN@7); the sub-export attaches it at registration.
Native
- Android NodeJSService takes an optional SentryConfig and passes
it to loader.mjs as --sentry* flags via buildBackendArgs.
ComapeoCoreService threads the manifest-loaded config through.
NODEJS_PROJECT_INDEX_FILENAME → loader.mjs.
- iOS NodeJSService gains a sentryConfig parameter (defaults to
SentryConfig.loadFromMainBundle()) and a buildSentryArgs helper.
AppLifecycleDelegate resolves loader.mjs from the app bundle.
The capture-application-data toggle (Phase 5) is wired through the
loader's tracesSampleRate gating but always reads as off until the
SharedPreferences/UserDefaults store and JS API land.
Trim verbose explanations down to non-obvious why-only across all files touched by Phase 3. No behavior change.
Errors captured by `@sentry/node` (handleFatal, RPC, future OTel
spans) used to land in Sentry without the rich device/os/app/culture
context that `@sentry/react-native` and `sentry-android` attach
automatically. Native now builds a static-only `sentryContext` blob
at FGS / app startup and ships it on the existing init control frame;
the loader's event processor merges those fields into every outgoing
event so Node-side captures look the same as RN-side ones.
- `SentryNativeContext.{kt,swift}` build the static device/os/app/
culture/tags blob from platform APIs. Only fields that don't change
during a session — battery/network/orientation/foreground state
live with Phase 5's update frame.
- `NodeJSService.{kt,swift}` splice the JSON into the init frame
alongside the existing `rootKey`. Best-effort: a builder failure
doesn't block boot.
- `backend/index.js`'s init handler calls a setter exposed by the
loader; malformed shapes drop silently rather than crash boot.
- `loader.mjs` registers `Sentry.addEventProcessor` that merges
native context onto every event field-by-field. Field merge
preserves `runtime.{name,version}` and `app.app_start_time` from
`nodeContextIntegration` while letting native overwrite the
Linux/Darwin-libnode view of os/device/culture.
- `loader.mjs` switches `integrations` from array form to function
form so SDK defaults (inboundFilters, linkedErrors, nodeContext,
etc.) are preserved alongside our `consoleIntegration` addition.
Previously the array form silently replaced the entire default
list.
Release alignment:
- New `getSentryRelease()` export from `@comapeo/core-react-native/sentry`
returns the release the plugin baked into the native config — the
same value the backend already gets via `--sentryRelease`. Consumers
pass it to `Sentry.init({ release })` so RN and Node hubs use the
same identifier and Sentry can correlate cross-side.
- README documents the recommended init pattern.
User-id forwarding for cross-side issue/trace correlation, plus the
dynamic update frame for memory/network/foreground, are deferred to
Phase 5 alongside the capture-application-data toggle.
…eline
Two fixes from the review:
C1 — `@sentry/node@8`'s `maybeInitializeEsmLoader` is gated on
`typeof require === 'undefined'`, but `@rollup/plugin-esm-shim` injects
`createRequire` into every output module so the gate is always truthy
and the SDK's `module.register('import-in-the-middle/hook.mjs', ...)`
call is dead code. OTel auto-instrumentation never patches anything.
Fix: register the iitm hook explicitly in `loader.mjs` before importing
`@sentry/node`. The string is rewritten to `./importHook.js` by
`rollup-plugin-import-hook.mjs` like the SDK's own call. Phase 4's
`@comapeo/core` OTel forwarding now has a path to actually flow through.
C2 — bundled `lib/register.js` only exported `default` (rollup +
commonjs-plugin can't recover the source's named CJS exports through
the entry pipeline), so iitm's loader-thread `import { register } from
<url>` would throw `SyntaxError: ... does not provide an export named
'register'` on every patched module. Fix: drop `lib/register` from
`sharedInput`, copy upstream's CJS file verbatim into `<outDir>/lib/
register.js`, and write a sibling `lib/package.json` with
`{"type":"commonjs"}` so Node treats it as CJS despite the parent
package's `"type":"module"`. ESM-import from the loader thread reads
the named exports via Node's CJS-interop layer.
Boot tracing — measure the four pipeline stages as Sentry spans inside
the existing `comapeo.boot` transaction:
comapeo.boot [FGS]
├─ boot.fgs-launch (startService → onStartCommand)[FGS, A]
├─ boot.node-spawn (spawn → started frame) [FGS, B]
│ ├─ boot.loader-init (retroactive, from process start)[Node, C]
│ ├─ boot.import-index [Node, C]
│ └─ boot.listen-control [Node, C]
├─ boot.rootkey-load [FGS, existing]
├─ boot.init-frame [FGS, existing]
└─ boot.manager-init (rootkey → ready) [Node, D]
Cross-process plumbing:
- `ComapeoCoreReactActivityLifecycleListener` stamps the start-service
intent with `SystemClock.elapsedRealtime()` so FGS can compute the
fgs-launch duration. New `EXTRA_SERVICE_START_ELAPSED_MS` constant.
- `ComapeoCoreService.onStartCommand` reads the stamp and forwards it
to `NodeJSService.serviceStartElapsedMs`.
- `NodeJSService` opens `comapeo.boot` with backdated start =
serviceStartElapsedMs, records `boot.fgs-launch` retroactively,
then opens `boot.node-spawn` around the JNI call.
- iOS gets `boot.node-spawn` (no fgs-launch — single-process app).
- Both platforms forward the transaction's `sentry-trace` (and baggage
on Android — sentry-cocoa@8's public Span API doesn't expose baggage)
via new `--sentryTrace` / `--sentryBaggage` argv flags.
- `SentryFgsBridge` / `SentryFgsBridgeImpl` gain optional backdated-
start parameters on `startBootTransaction` / `startBootSpan` plus a
new `getTraceData(transaction)` returning `(trace, baggage)`.
- `SentryNativeBridge.swift` gains the same `getTraceData`.
Node side (`loader.mjs` + `index.js`):
- Captures `loaderStartDate` at first line.
- After `Sentry.init`, records `boot.loader-init` retroactively via
`startInactiveSpan({ startTime })`.
- Wraps `import("./index.js")` in `Sentry.continueTrace(...)` →
`Sentry.startSpan({ name: "boot.import-index" })`. AsyncLocalStorage
carries the trace context through index.js's IIFE so its child spans
attach to the same trace.
- `index.js` adds `boot.listen-control` and `boot.manager-init` via a
small `bootSpan(name, fn)` helper that's a no-op when Sentry is off.
Backdating uses `SystemClock.elapsedRealtime()` (monotonic, survives
NTP clock jumps mid-boot) translated to wall-clock `SentryDate` only at
span creation time. iOS passes Date instances directly; Sentry's
`SpanTimeInput` accepts both.
…peo.boot
The C-stage spans (loader-init, import-index, listen-control) all
happen during node-spawn (between the JNI call and the 'started'
frame), so they should be its children — not siblings. Two changes:
1. Native (Android + iOS) forwards the node-spawn *span*'s trace
header to Node instead of the transaction's. iOS reorders to open
the span before `buildSentryArgs` so the trace flag carries it.
Both fall back to the transaction trace if the span isn't open
(defensive — the span is always opened first in practice).
2. `loader.mjs` swaps `Sentry.startSpan` for `startInactiveSpan`
around `import("./index.js")`. With the active form, index.js's
IIFE inherited boot.import-index via AsyncLocalStorage and its
spans (listen-control, manager-init) parented to a finished
import-index. `startInactiveSpan` records the timing without
activating, so the IIFE inherits the continueTrace context
(parent = node-spawn) directly.
End-state tree:
comapeo.boot
├─ boot.fgs-launch
├─ boot.node-spawn
│ ├─ boot.loader-init
│ ├─ boot.import-index
│ ├─ boot.listen-control
│ └─ boot.manager-init
├─ boot.rootkey-load
└─ boot.init-frame
Caveat: `boot.manager-init` happens after node-spawn ends (the FGS
sees 'started' before Node receives the init frame), so its bar
extends past node-spawn's in the time view despite being a child in
the tree. The cure (forwarding both span IDs and switching context
inside index.js) costs more complexity than the visual oddness.
gmaclennan
commented
May 12, 2026
Replaces the narrow `getSentryRelease()` JS helper with a
spreadable `sentryConfig` object covering every plugin-baked
option that maps to `Sentry.init` (dsn, environment, release,
sampleRate, tracesSampleRate, enableLogs). Always-defined so
`Sentry.init({ ...sentryConfig, ...mine })` is safe whether or
not the plugin is registered.
Adds a `boot.kind` tag (`user-foreground` | `system-restart`)
to the FGS-side `comapeo.boot` transaction so the two
populations stay separable in Sentry — system-restart boots
lack the activity stamp and the `boot.fgs-launch` span, so
their timelines aren't comparable to user-initiated boots.
Drive-by: `SentryNativeContext.applicationInfo` is nullable on
API 33+; guard the label read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gmaclennan
commented
May 12, 2026
…exit) Adds two follow-up phases to the Sentry integration plan covering process-death observability — the gap nothing in the current integration answers: - Phase 6 (Android, API 30+): subscribe to `ActivityManager.getHistoricalProcessExitReasons()` on next cold start; tag OEM-aggressive-killer cohort (`oem.killer.suspected`), intentional-vs-system kills (`exit.intentional`), and FGS-killed-in-background. Derives per-event "backgrounded for / alive for" durations from wall-clock anchors stamped in SharedPreferences via `ProcessLifecycleOwner`. - Phase 7 (iOS, 14+): subscribe to `MXMetricPayload` and forward `MXAppExitMetric` buckets (memory pressure, background-task assertion timeout, watchdog, etc.) as Sentry events. Sentry-cocoa explicitly doesn't subscribe to `MXMetricPayload` — verified against current docs and issue #1661. Optional 7b sub-phase layers a `UserDefaults`-anchored heuristic on top for per-event "killed in background" inference, since `MXAppExitMetric` itself has no per-event timestamps. Both phases use events, not metrics: Sentry's standalone metrics beta was sunset Oct 2024, and the span-attribute replacement requires a live trace context that cold-start post-mortems don't have. Numeric durations get a dual emission — exact value as `extra` for drill-down, coarse bucket as a string tag for cohort aggregation in Discover. Phase 7 emits one event per individual exit (rather than per-bucket-per-window) so dashboard queries stay trivial `count(*)`s; iOS volume makes the duplication free. Refinements phase bumped to Phase 8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… toggles Adds the "off" tier to the privacy model. The module now owns the RN-side `Sentry.init` call via a new `initSentry()` export — host calls it once at app entry with allowlisted extensions (integrations, beforeSend, beforeBreadcrumb, tags); locked options (dsn, release, sampleRate, sendDefaultPii=false, etc.) come from the plugin and can't be overridden. When `diagnosticsEnabled=false`, `Sentry.init` runs nowhere — not on RN, not on the Android FGS, not in the Node loader. Both toggles (`diagnosticsEnabled`, `captureApplicationData`) persist in a shared prefs file (`com.comapeo.core.prefs` on Android, `UserDefaults.standard` on iOS). Setters are async; on a `true → false` flip they wipe sentry-android's `<cacheDir>/sentry/` / sentry-cocoa's `<NSCachesDirectory>/io.sentry/` so events queued in the current session never ship. Restart-to-activate semantics match the planned capture-app-data toggle. `backend/index.js` now attaches `memory_size` / `free_memory` / `storage_size` / `free_storage` to fatal captures via `os.freemem` / `os.totalmem` / `fs.statfsSync` — `@sentry/node` doesn't synthesise device context the way sentry-cocoa / sentry-android do, so OOM and disk-full backend crashes were previously silent on those fields. Subsequent-phase work (PII scrubber, user.id monthly rotation, context field reclassification, Phase 5/6 update-frame, network URL scrubbing, consoleIntegration gating) is planned in docs/sentry-integration-plan.md §9.8.5. Follow-up issues: #65 (Android open() factory test via Robolectric), #66 (iOS open() factory test with isolated UserDefaults suite). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sentry/node has no offline transport, so on mobile envelopes
captured while the device is offline are lost. Replace it with a
custom transport that forwards every captured payload to the native
side via the control socket; sentry-android and sentry-cocoa already
queue and retry under connectivity-aware send windows.
Two wire shapes:
- {type:"sentry-event",payload} for single-item error events —
deserialised via SentryEvent.Deserializer (Android) /
SentryEventDecoder (iOS, @_spi(Private)) and captured via
Sentry.captureEvent / SentrySDK.capture(event:), so native scope
(device, OS, app, user, breadcrumbs) merges at capture time.
- {type:"sentry-envelope",data} for transactions, sessions,
check-ins, profiles, multi-item payloads — passed straight to
InternalSentrySdk.captureEnvelope / PrivateSentrySDKOnly's
envelope-capture entrypoint.
Two 100-frame ring buffers (loader.mjs + SimpleRpcServer) cover the
boot-sequence gaps; first client to connect drains both. Delete
SentryNativeContext.{kt,swift} and the sentryContext field on the
init frame — native applies its own scope on the event path now.
Plan-doc §5.7 documents the architecture; §10.10 table row and §13
file-changes list updated; the "offline transport: deferred"
limitation in §12 is now landed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR lands “Phase 3” of the Sentry integration by bootstrapping Sentry inside the embedded Node backend before app imports (to enable OTel instrumentation), adding end-to-end RPC tracing with trace header propagation across RN ↔ Node, and introducing persisted user privacy toggles (diagnosticsEnabled, captureApplicationData) that gate initialization and data capture.
Changes:
- Add a Node backend
loader.mjswhich parses--sentry*argv, initializes@sentry/node(with an offline-forwarding transport to native), and then dynamically importsindex.js. - Implement distributed tracing for RPCs: RN injects
sentry-trace/baggageinto request metadata; backend continues the trace and wraps RPCs in Sentry spans/transactions. - Add native+JS plumbing for persisted Sentry preferences (incl. outbox wipe on opt-out) and update build tooling/rollup output to support multi-entry bundling and sourcemap relocation.
Reviewed changes
Copilot reviewed 44 out of 46 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/sentry.ts | Introduces initSentry() ownership, preference gating, adapter registration, and host extension hooks. |
| src/sentry-internal.ts | Converts to a “registered adapter” holder to avoid forcing @sentry/react-native as a hard dependency for non-Sentry consumers. |
| src/ComapeoCoreModule.ts | Adds native config/preference readers and injects trace headers into RPC requests via onRequestHook. |
| src/tests/sentry.test.js | Unit tests for initSentry() merge/gating/throw behavior with mocked Sentry + native module. |
| README.md | Updates Sentry documentation (but currently still describes host-managed Sentry.init in the new section). |
| package-lock.json | Adds bin entry and bumps @sentry/react-native peer range to ^7.13.0. |
| ios/Tests/SentryConfigTests.swift | Adds coverage for diagnosticsEnabledDefault parsing. |
| ios/Tests/ControlFrameTests.swift | Adds tests for sentry-event and sentry-envelope control frames. |
| ios/Tests/ComapeoPrefsTests.swift | Adds unit tests for persisted Sentry prefs + outbox wipe semantics. |
| ios/SentryNativeBridge.swift | Adds SPI-based event decode/capture + envelope capture + trace header extraction. |
| ios/SentryConfig.swift | Adds diagnosticsEnabledDefault and toSentryInitMap() for JS-exposed init options. |
| ios/Package.swift | Includes new ComapeoPrefs.swift in SwiftPM sources. |
| ios/NodeJSService.swift | Threads Sentry config into loader argv; adds node-spawn boot span and Sentry frame handling. |
| ios/ControlFrame.swift | Adds parsing for sentry-event and sentry-envelope frames. |
| ios/ComapeoPrefs.swift | Implements persisted prefs + outbox wipe on iOS. |
| ios/ComapeoCoreModule.swift | Exposes sentryConfig + sentryPreferences constants and toggle setters to JS. |
| ios/AppLifecycleDelegate.swift | Resolves loader entrypoint and gates backend Sentry argv on diagnosticsEnabled. |
| docs/sentry-integration-plan.md | Updates plan status/details for Phase 3/10 and adds three-tier privacy model spec. |
| docs/ARCHITECTURE.md | Documents build-time config flow and tags/spans (currently still references host Sentry.init spread). |
| backend/rollup.config.ts | Switches to multi-entry build (loader/index/importHook) and copies iitm CJS register for correct ESM/CJS interop. |
| backend/rollup-plugins/rollup-plugin-sentry-debug-ids.js | Makes sourcemap relocation recursive and resilient to chunks without debug IDs. |
| backend/rollup-plugins/rollup-plugin-import-hook.mjs | Rewrites iitm hook registration to point at the bundled hook entry. |
| backend/package.json | Adds @sentry/node, @sentry/core, and import-in-the-middle. |
| backend/package-lock.json | Locks new backend dependencies pulled in by Sentry/Otel and iitm. |
| backend/loader.mjs | New backend spawn target: argv parsing, Sentry init, offline forwarding transport, boot spans, and dynamic import of index.js. |
| backend/lib/simple-rpc.js | Buffers Sentry frames until first client connects (pre-client boot window). |
| backend/lib/comapeo-rpc.js | Adds server-side RPC span/transaction wrapper with continueTrace and optional arg capture cap. |
| backend/index.js | Reads Sentry globals from loader, wires fatal capture+flush, forwards Sentry frames to native, and wraps boot phases in spans. |
| babel.config.cjs | Adds root Babel config for Jest to handle TS imports in tests. |
| apps/example/index.ts | Migrates example app to call initSentry() instead of Sentry.init directly. |
| apps/example/app.json | Enables tracesSampleRate via plugin config for the example app. |
| app.plugin.js | Adds diagnosticsEnabledDefault plumbing for Android/iOS config outputs. |
| android/src/test/java/com/comapeo/core/SentryFgsBridgeImplTest.kt | Adds test for boot.kind tag behavior on boot transaction. |
| android/src/test/java/com/comapeo/core/SentryConfigTest.kt | Adds diagnosticsEnabledDefault parse tests. |
| android/src/test/java/com/comapeo/core/ControlFrameTest.kt | Adds tests for sentry-event/sentry-envelope parsing. |
| android/src/test/java/com/comapeo/core/ComapeoPrefsTest.kt | Adds JVM unit tests for prefs defaults, persistence, and outbox wipe. |
| android/src/main/java/com/comapeo/core/SentryTags.kt | Adds boot.kind tag and values. |
| android/src/main/java/com/comapeo/core/SentryFgsBridgeImpl.kt | Adds backdated boot timestamps, trace header extraction, and Node event/envelope capture helpers. |
| android/src/main/java/com/comapeo/core/SentryFgsBridge.kt | Exposes new boot timing args, trace header getter, and capture helpers while keeping Sentry imports behind the guard. |
| android/src/main/java/com/comapeo/core/SentryConfig.kt | Adds diagnosticsEnabledDefault and toSentryInitMap() for JS constants. |
| android/src/main/java/com/comapeo/core/NodeJSService.kt | Switches spawn target to loader.mjs, forwards --sentry* argv, adds boot spans + trace propagation, and handles Sentry frames. |
| android/src/main/java/com/comapeo/core/ControlFrame.kt | Adds Sentry frame variants and parsers. |
| android/src/main/java/com/comapeo/core/ComapeoPrefs.kt | Adds persisted Sentry prefs with restart-to-activate semantics and outbox wiping. |
| android/src/main/java/com/comapeo/core/ComapeoCoreService.kt | Gates FGS Sentry init + backend argv forwarding on persisted diagnosticsEnabled. |
| android/src/main/java/com/comapeo/core/ComapeoCoreReactActivityLifecycleListener.kt | Stamps elapsedRealtime into the service intent for backdated boot spans. |
| android/src/main/java/com/comapeo/core/ComapeoCoreModule.kt | Exposes sentryConfig + sentryPreferences constants and toggle setters; ignores Node Sentry frames in main process. |
Files not reviewed (1)
- backend/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+150
to
+166
| ### 3a. Share the plugin-baked options with your host `Sentry.init` | ||
|
|
||
| The Node-backend hub (`@sentry/node` running inside nodejs-mobile), | ||
| the Android FGS-process hub (`sentry-android`), and the host RN hub | ||
| (`@sentry/react-native`) are independent — for cross-side correlation | ||
| they must use the same `release`, `environment`, etc. Spread the | ||
| plugin-baked options into your host init so everything lines up: | ||
|
|
||
| ```ts | ||
| import { sentryConfig } from "@comapeo/core-react-native/sentry"; | ||
| import * as Sentry from "@sentry/react-native"; | ||
|
|
||
| Sentry.init({ | ||
| ...sentryConfig, | ||
| // your own options here override anything the plugin set | ||
| }); | ||
| ``` |
Comment on lines
+650
to
+654
| sentry`, so the host's `Sentry.init({ ...sentryConfig, ...mine })` | ||
| aligns RN-side events with the FGS hub and the Node backend without | ||
| duplicating values in app code. Plugin-internal fields | ||
| (`rpcArgsBytes`, `captureApplicationDataDefault`) stay on the | ||
| native-side `SentryConfig` only. |
Comment on lines
+60
to
+66
| /** | ||
| * Sentry options baked into the native config by the Expo plugin. | ||
| * Re-exported as `sentryConfig` from the `/sentry` sub-export. | ||
| * | ||
| * Always-defined: an empty object when the plugin isn't registered, | ||
| * so `Sentry.init({ ...sentryConfig, ...mine })` is always safe. | ||
| */ |
PR review (Copilot) pointed out that README.md, docs/ARCHITECTURE.md,
and src/ComapeoCoreModule.ts still steered hosts toward
`Sentry.init({ ...sentryConfig, ...mine })`, but the previous
commit on this branch landed `initSentry()` as the module-owned
init lifecycle (with `Sentry.init`-before-`initSentry` now an
explicit error). Update the three call sites to recommend
`initSentry({ integrations, beforeSend, tags, ... })` and clarify
that `sentryConfig` is for read-only inspection, not a spread
into a separate `Sentry.init`.
Also call `initSentry()` from the example app's App.tsx so the
smoke-test path actually exercises the supported init pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tests The control-socket Sentry-frame buffer in SimpleRpcServer drained once on the first client connect, then cleared. That assumed the FGS process connects before the main-app process on Android — but ComapeoCoreModule's OnCreate creates its NodeJSIPC during MainApplication startup, before ComapeoCoreReactActivityLifecycleListener's onResume even fires startForegroundService, so the main-app IPC has been polling for the socket file well before the FGS process exists. Connect order is a poll-timing race, not an ordering guarantee — main-app could connect first, drain the buffer to a client that ignores Sentry frames, and the FGS would never see the boot captures. Fix: keep the buffer as a rolling 100-frame window, replay to every new client on connect, accept duplicate delivery on transient reconnect (Sentry dedups server-side by event_id). Memory cost is bounded. Also: - Extract `envelopeToFrame` (the event-vs-envelope routing) from loader.mjs into backend/lib/sentry-frame.js so it's testable as a pure function. Add 7 `node --test` cases covering single-item events, transactions, sessions, check-ins, multi-item event + attachment, Uint8Array serialiser output, and the empty-envelope edge case. Run via `npm test` in backend/. - Add apps/example/tests/ios/SentryEventDecoderSpiTest.swift that exercises `SentryEventDecoder.decodeEvent(jsonData:)` against a known-good payload. Catches a future sentry-cocoa bump silently removing or renaming the SPI'd symbol that `SentryNativeBridge.captureEventJson` depends on — without this, the SDK still builds but forwarding silently drops events. Follow-up #67 tracks the heavier integration test (round-trip native captureEventJson against a stub-transport Sentry SDK). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Wire the Node-backend into Sentry: argv-driven loader spawns Sentry
before importing the app, RPC method-level errors capture with full
stack traces, and JS-side IPC spans propagate sentry-trace/baggage
to the backend so both halves of an RPC land on the same trace.
Backend
inits @sentry/node before dynamically importing ./index.js. Required
ordering for OpenTelemetry's import-in-the-middle hook to instrument
modules.
/lib/register) with a code-split chunks/ subdir for @sentry/node and
transitive deps. .js extension forced for importHook/lib/register so
import-in-the-middle's hardcoded relative paths resolve.
module.register('import-in-the-middle/hook.mjs', ...) to point at
the bundled ./importHook.js. Ported from comapeo-mobile.
no-ops gracefully on auto-emitted helper chunks that bypass
captureDebugIdsPlugin.
set by the loader; wires handleFatal to Sentry.captureException
(with source:native tag for FGS-forwarded errors) and a 100ms flush
before process.exit. ComapeoRpcServer accepts {sentry,rpcArgsBytes}
and registers an onRequestHook that wraps each RPC in a transaction
with continueTrace from request metadata. rpc.args is gated behind
an opt-in byte cap (PII risk).
JS/RN module
The sub-export now calls registerAdapter(Sentry) at module load,
so consumers without the optional peer dep don't break when the
main barrel imports sentry-internal for RPC tracing.
createMapeoClient call. Inert when no adapter is registered or no
active root span; otherwise startSpan + getTraceData populates
request.metadata for the backend hook to continueTrace.
re-exported by RN@7); the sub-export attaches it at registration.
Native
it to loader.mjs as --sentry* flags via buildBackendArgs.
ComapeoCoreService threads the manifest-loaded config through.
NODEJS_PROJECT_INDEX_FILENAME → loader.mjs.
SentryConfig.loadFromMainBundle()) and a buildSentryArgs helper.
AppLifecycleDelegate resolves loader.mjs from the app bundle.
The capture-application-data toggle (Phase 5) is wired through the
loader's tracesSampleRate gating but always reads as off until the
SharedPreferences/UserDefaults store and JS API land.