Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8cc22e0
feat(sentry): Phase 11 toggle rework, metrics layer + PII scrubbers
gmaclennan Jun 22, 2026
2089ef9
Merge branch 'main' into feat/sentry-metrics-phase11
gmaclennan Jun 22, 2026
a119f7e
fix(sentry): address Phase 11 review findings
gmaclennan Jun 24, 2026
874e5f6
Merge remote-tracking branch 'origin/main' into feat/sentry-metrics-p…
gmaclennan Jun 27, 2026
fd07edc
test(sentry): isolate notification-permissions test from metrics impo…
gmaclennan Jun 27, 2026
89e9b5f
perf(sentry): skip storage-size walk when Sentry is off
gmaclennan Jun 27, 2026
4bb57e9
fix(sentry): capture RPC errors regardless of debug flag
gmaclennan Jun 27, 2026
d6f5ebf
fix(sentry): disable broad base64-22 scrub rule pending narrower design
gmaclennan Jun 27, 2026
b83a3bb
refactor(sentry): stop capturing RPC errors in the request hooks
gmaclennan Jun 27, 2026
4b14ac2
refactor(sentry): rework applicationUsageData tier, drop feature-usag…
gmaclennan Jul 1, 2026
6784ac4
fix(sentry): address phase-11 review — scrubber leaks, load crash, me…
gmaclennan Jul 1, 2026
29cefd9
feat(sentry): live getters for the toggle prefs so the module owns th…
gmaclennan Jul 1, 2026
7effafb
refactor(sentry): one high-cardinality metric per measurement, restor…
gmaclennan Jul 1, 2026
89e50b9
docs(sentry): add "what it tells us" column to the metrics table + dr…
gmaclennan Jul 1, 2026
f08dae9
refactor(sentry): drop three low-signal metrics
gmaclennan Jul 1, 2026
3532a9c
refactor(sentry): rename launch-snapshot vs current prefs, cache the …
gmaclennan Jul 1, 2026
c92fc24
refactor(sentry): report event-loop delay as per-window max, not mean
gmaclennan Jul 1, 2026
c51793d
Merge branch 'main' into feat/sentry-metrics-phase11
gmaclennan Jul 1, 2026
c8c1e0c
feat(sentry): derive user.id from a permanent root user ID
gmaclennan Jul 2, 2026
a655e78
fix(sentry): scrub `lon` and JSON-quoted coordinate keys
gmaclennan Jul 2, 2026
1b8246a
fix(sentry): update the live-preferences cache only after the native …
gmaclennan Jul 2, 2026
51a8600
docs(sentry): reconcile the 72h debug window and the tracesSampleRate…
gmaclennan Jul 2, 2026
80aa4fd
feat(sentry): shorten the root user ID to a hand-copyable 12-char code
gmaclennan Jul 2, 2026
c2d038f
docs(sentry): mark unwired metric rows in the tier table
gmaclennan Jul 2, 2026
22067bf
Merge branch 'feat/sentry-metrics-phase11' of github.com:digidem/coma…
gmaclennan Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,11 @@ Info.plist keys at prebuild.
| `environment` | yes | Sentry environment (e.g. `production`, `staging`). |
| `release` | no | Release tag. Defaults to the app's version (`versionName`+`versionCode` / `CFBundleShortVersionString`+`CFBundleVersion`). |
| `sampleRate` | no | Error sample rate (0–1). |
| `tracesSampleRate` | no | Performance trace sample rate. Default 0.1 when capture-application-data is on; 0 when off. |
| `tracesSampleRate` | no | Performance trace sample rate (0–1) for the non-`debug` baseline. The `debug` toggle forces full (1.0) sampling while its window is on; day-to-day performance signal rides the always-on metrics layer, so this can stay 0. |
| `rpcArgsBytes` | no | Max bytes of RPC arguments captured on spans. |
| `diagnosticsEnabledDefault` | no | Fresh-install default for the diagnostics toggle. |
| `captureApplicationDataDefault` | no | Fresh-install default for the capture-application-data toggle. Keep off in production. |
| `applicationUsageDataDefault` | no | Fresh-install default for the application-usage-data toggle. Keep off in production. |
| `debugDefault` | no | Fresh-install default for the debug toggle (per-RPC traces + richer capture, auto-expires 72h after enable). Keep off in production. |
| `enableLogs` | no | Forward Sentry structured logs from the backend process. Pair with `enableLogs: true` in your host `Sentry.init` setup. |

Omitting `sentry` (or removing it on a re-prebuild) strips all keys this plugin
Expand Down Expand Up @@ -315,8 +316,12 @@ From `@comapeo/core-react-native/sentry`:
- `getDiagnosticsEnabled()` / `setDiagnosticsEnabled(value)` — the diagnostics
opt-out toggle. Restart-to-activate; setting `false` also wipes the on-disk
envelope cache.
- `getCaptureApplicationData()` / `setCaptureApplicationData(value)` — the
capture-application-data toggle (gates traces and richer payloads).
- `getApplicationUsageData()` / `setApplicationUsageData(value)` — the
opt-in application-usage telemetry toggle (default off). Restart-to-activate;
setting `false` also wipes the on-disk envelope cache.
- `getDebugEnabled()` / `setDebugEnabled(value)` — opt-in debug mode (per-RPC
traces, `@comapeo/core` OTel spans, richer capture). Restart-to-activate and
auto-expires 72h after the most recent enable.

### Uploading artifacts to Sentry

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class ComapeoCoreApplicationLifecycleListener : ApplicationLifecycleListener {
context = application,
processName = application.packageName,
procKey = SentryTags.PROC_MAIN,
captureApplicationData = prefs.readCaptureApplicationData(),
applicationUsageData = prefs.readApplicationUsageData(),
snapshot = snapshot,
)
}
Expand Down
76 changes: 65 additions & 11 deletions android/src/main/java/com/comapeo/core/ComapeoCoreModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -173,27 +173,72 @@ class ComapeoCoreModule : Module() {

// `sentryConfig` — baked-in by app.plugin.js at prebuild; spread into
// `Sentry.init(...)` by the JS `/sentry` sub-export. Empty map when the
// plugin isn't registered so spreading is always safe.
// plugin isn't registered so spreading is always safe. `userId` is
// derived with the same launch snapshot the FGS uses, so both
// processes report the same Sentry user.id.
Constant("sentryConfig") {
appContext.reactContext?.let {
SentryConfig.loadFromManifest(it)?.toSentryInitMap()
appContext.reactContext?.let { ctx ->
val prefs = ComapeoPrefs.open(ctx)
SentryConfig.loadFromManifest(ctx)?.toSentryInitMap(
DeviceTags.compute(ctx),
prefs.deriveSentryUserId(prefs.readApplicationUsageData()),
)
} ?: emptyMap<String, Any>()
}

// `sentryPreferences` — snapshot-at-boot; toggle changes take effect on next
// launch. Returns baked-in defaults pre-attach so JS can spread unconditionally.
Constant("sentryPreferences") {
// The permanent root user ID (lazily generated on first read). Local
// debugging aid only — Sentry sees derived hashes, never this value.
// The host app may show it in a debug/about screen so a user can share
// it and support can recompute their historical monthly user.ids.
Function("getSentryRootUserId") {
val ctx = appContext.reactContext
?: throw IllegalStateException(
"getSentryRootUserId called before native context attached",
)
ComapeoPrefs.open(ctx).readRootUserId()
}

// `sentryPreferencesAtLaunch` — the snapshot in effect this session; toggle
// changes take effect on next launch. Returns baked-in defaults pre-attach so
// JS can spread unconditionally. For the current saved value use
// `getCurrentSentryPreferences`.
Constant("sentryPreferencesAtLaunch") {
val ctx = appContext.reactContext
if (ctx == null) {
mapOf(
"diagnosticsEnabled" to ComapeoPrefs.DEFAULT_DIAGNOSTICS_ENABLED,
"applicationUsageData" to ComapeoPrefs.DEFAULT_APPLICATION_USAGE_DATA,
"debug" to ComapeoPrefs.DEFAULT_DEBUG,
)
Comment thread
gmaclennan marked this conversation as resolved.
} else {
val prefs = ComapeoPrefs.open(ctx)
Comment thread
gmaclennan marked this conversation as resolved.
mapOf(
"diagnosticsEnabled" to prefs.readDiagnosticsEnabled(),
"applicationUsageData" to prefs.readApplicationUsageData(),
"debug" to prefs.readDebugEnabled(),
)
}
}

// Live read of the current persisted values — reflects a `setX` made this
// session and survives a JS reload (unlike the `sentryPreferencesAtLaunch`
// Constant), so a settings screen can read the user's choice without keeping
// its own copy. Raw `debug` (no 72h auto-off side effect — that's applied by
// readDebugEnabled at launch).
Function("getCurrentSentryPreferences") {
val ctx = appContext.reactContext
if (ctx == null) {
mapOf(
"diagnosticsEnabled" to ComapeoPrefs.DEFAULT_DIAGNOSTICS_ENABLED,
"captureApplicationData" to ComapeoPrefs.DEFAULT_CAPTURE_APPLICATION_DATA,
"applicationUsageData" to ComapeoPrefs.DEFAULT_APPLICATION_USAGE_DATA,
"debug" to ComapeoPrefs.DEFAULT_DEBUG,
)
} else {
val prefs = ComapeoPrefs.open(ctx)
mapOf(
"diagnosticsEnabled" to prefs.readDiagnosticsEnabled(),
"captureApplicationData" to prefs.readCaptureApplicationData(),
"applicationUsageData" to prefs.readApplicationUsageData(),
"debug" to prefs.readDebugStored(),
)
}
}
Expand All @@ -210,12 +255,21 @@ class ComapeoCoreModule : Module() {
if (!value) ComapeoPrefs.wipeSentryOutbox(ctx)
}

AsyncFunction("setCaptureApplicationData") { value: Boolean ->
AsyncFunction("setApplicationUsageData") { value: Boolean ->
Comment thread
gmaclennan marked this conversation as resolved.
val ctx = appContext.reactContext
?: throw IllegalStateException(
"setApplicationUsageData called before native context attached",
)
ComapeoPrefs.open(ctx).writeApplicationUsageData(value)
if (!value) ComapeoPrefs.wipeSentryOutbox(ctx)
}

AsyncFunction("setDebugEnabled") { value: Boolean ->
val ctx = appContext.reactContext
?: throw IllegalStateException(
"setCaptureApplicationData called before native context attached",
"setDebugEnabled called before native context attached",
)
ComapeoPrefs.open(ctx).writeCaptureApplicationData(value)
ComapeoPrefs.open(ctx).writeDebugEnabled(value)
if (!value) ComapeoPrefs.wipeSentryOutbox(ctx)
}

Expand Down
24 changes: 19 additions & 5 deletions android/src/main/java/com/comapeo/core/ComapeoCoreService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ class ComapeoCoreService : Service() {
// Snapshotted in onCreate, consumed when the Node backend is built lazily in
// ensureBackendInitialized() after startForeground().
private var effectiveSentryConfig: SentryConfig? = null
private var captureApplicationData: Boolean = false
private var applicationUsageData: Boolean = false
private var debug: Boolean = false
private var deviceTags: DeviceTags? = null
private var sentryUserId: String? = null
private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob())

/** Active self-terminate watchdog (see [onNodeStateChange]). Armed at most once
Expand Down Expand Up @@ -88,14 +91,21 @@ class ComapeoCoreService : Service() {
val sentryConfig = SentryConfig.loadFromManifest(applicationContext)
val prefs = ComapeoPrefs.open(applicationContext)
effectiveSentryConfig = if (prefs.readDiagnosticsEnabled()) sentryConfig else null
captureApplicationData = prefs.readCaptureApplicationData()

// Read debug + usage before SentryFgsBridge.init: readDebugEnabled()
// may queue the auto_disabled breadcrumb, which init drains — read
// first or that crumb is lost on the auto-off launch. Independent of
// the diagnostics gate.
applicationUsageData = prefs.readApplicationUsageData()
debug = prefs.readDebugEnabled()

// Init the Sentry bridge here, not after startForeground: breadcrumbs no-op
// until it's initialised, so deferring it would drop the pre-start trail.
// It's cheap relative to the Node backend (libnode load), which is the only
// part deferred to ensureBackendInitialized() to keep off the FGS deadline.
effectiveSentryConfig?.let { cfg ->
SentryFgsBridge.init(applicationContext, cfg)
sentryUserId = prefs.deriveSentryUserId(applicationUsageData)
SentryFgsBridge.init(applicationContext, cfg, sentryUserId)
}

logCrumb(SentryCategories.FGS, "ComapeoCoreService.onCreate")
Expand All @@ -119,6 +129,7 @@ class ComapeoCoreService : Service() {
)
}

deviceTags = DeviceTags.compute(applicationContext)
serviceScope.launch(Dispatchers.IO) {
// Snapshot the previous FGS session's anchors before stamping
// this run's — the decoder must see what was true at the old exit.
Expand All @@ -131,7 +142,7 @@ class ComapeoCoreService : Service() {
// android:process — a rename can't silently break the filter.
processName = currentProcessName(applicationContext),
procKey = SentryTags.PROC_FGS,
captureApplicationData = captureApplicationData,
applicationUsageData = applicationUsageData,
snapshot = snapshot,
)
}
Expand All @@ -145,7 +156,10 @@ class ComapeoCoreService : Service() {
nodeJSService = NodeJSService(
applicationContext,
sentryConfig = effectiveSentryConfig,
captureApplicationData = captureApplicationData,
applicationUsageData = applicationUsageData,
debug = debug,
deviceTags = deviceTags,
sentryUserId = sentryUserId,
)
}

Expand Down
Loading