feat(sentry): Phase 11 toggle rework, metrics layer + PII scrubbers#111
Conversation
Implements the Sentry Phase 11 workblock (plan §11, §9b.1, §9b.5): #75 toggle rework - Rename captureApplicationData -> applicationUsageData across the JS API, native bridge methods, on-device stored key, Expo plugin field, and Node CLI flags. Deprecated aliases forward for one minor release; a one-shot stored-key migration runs on first prefs open (both platforms). - New `debug` toggle (get/setDebugEnabled, sentry.debug + sentry.debugEnabledAtMs slots, --debug argv, debugDefault plugin field) with a 24h auto-off enforced in the native debug reader. - Device classification (DeviceTags.{kt,swift}): low/mid/high by RAM + cores plus <platform>.<major>, plumbed to RN via sentryConfig.deviceTags and to Node via --deviceClass/--osMajor/ --platformTag. - tracesSampleRate now derives from debug (1.0/0). #76 metrics layer - New backend/lib/metrics.js + src/sentry-metrics.ts: wrappers that inject `platform` on every metric, attach device tags only on the .by_device mirrors, no-op when Sentry is off, and drop forbidden tags. RPC hooks split: always record the metric, span only under debug (recorded while active so it links to the trace). console integration moved behind debug; 60s memory/event-loop sampler, boot-phase durations, state transitions, storage-size bucket. #77 PII scrubbers - Shared regex list (rootKey, 22-char base64, lat/lng) in src/sentry-scrub.ts (RN) and backend/before-send.js (Node). RN wires the real beforeSend ahead of the host chain plus a host-only URL beforeBreadcrumb; Node registers the same scrub as an event processor. Walks message, exception, extra, contexts, breadcrumb message+data, span description+data; HTTP breadcrumb URLs reduce to host-only. Tests: backend metrics/before-send suites, extended sentry suites (debug branching + scrubber), native migration/24h-auto-off/device boundary tests. npm lint, tsc, jest (18), backend node --test (47) all green locally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scrubber (src/sentry-scrub.ts + backend/before-send.js, kept mirrored): - Redact numeric/string lat/lng held as object fields (key-aware). - Match base64url runs of 22+ chars so longer project keys/ids scrub. - RN scrubEvent now reduces breadcrumb URLs to host-only via scrubBreadcrumb (symmetric with Node). - Walk event.request (url→host, query_string/headers/cookies/data). Metrics: - Emit comapeo.rpc.client.send_ms on the always-on and debug paths. Native (review-only, not compiled here): - Android/iOS: read debug toggle before native Sentry init so the §11.5 auto_disabled breadcrumb is queued before the drain. - Document the main-process auto-off breadcrumb drop on Android. Tests: - New RN suites: sentry-scrub, sentry-metrics, rpc-client-hook. - Backend: real forbidden-NAME integration test through the wrappers; lat/lng-object, base64 43/52, and event.request regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 11 review fixes —
|
…hase11 * origin/main: (92 commits) docs(readme): document uploading sourcemaps, dSYMs, and native symbols to Sentry feat(e2e): forward RPC traces to Sentry, "e2e" environment for both test apps fix(e2e): drop tsconfig paths alias that double-loaded the module Release v1.0.0-pre.6 fix(android): wake blocked read in disconnect() and tidy close() teardown fix(android): shutdown IPC socket on JS reload so backend cleans up subscriptions docs: vendor relevant Expo skills into .claude/skills/ docs: drop stale issue list from AGENTS.md, fix web platform status docs: slim AGENTS.md to orientation, defer detail to docs/ docs: run local e2e against a Release build, drop the dev-client flow chore: add build:ios to match build:android in both test apps chore(deps): bump the minor-and-patch group across 1 directory with 14 updates docs: rename agents.md to AGENTS.md, add CLAUDE.md, and ship agent skills ci: align dependabot and action pinning with policy chore: add dev setup/test scripts and a Maestro e2e skill fix(android): stop dangling argv pointers from corrupting node args test(e2e): Maestro rootkey persistence across app restarts chore: keep package version in sync with origin/main fix(ios): ad-hoc-sign simulator tests with a keychain entitlement Release v1.0.0-pre.5 ... # Conflicts: # android/src/main/java/com/comapeo/core/ComapeoCoreService.kt # src/ComapeoCoreModule.ts
…rt cycle The post-merge ComapeoCoreModule imports the Phase 11 metrics layer, which eagerly pulls ./sentry and forms an import cycle back into this module. Stub ./sentry-metrics like the other transitive deps so the notification-wrapper test loads the real module without tripping the cycle's eager readSentryConfig() call.
sampleStorageSize() recursively stats the entire private storage tree at boot, but its only output is a bucket metric that no-ops when Sentry is disabled. Gate the walk on metrics.isEnabled() so opted-out users don't pay the disk I/O for a discarded sample.
Code review (xhigh, workflow-backed)I merged I've already pushed two fixes that needed no decision:
The rest below need your call or touch the PII scrubber, so I'm flagging rather than editing. 1. RPC error capture is now gated behind
|
captureException had moved into the debug-gated span path, so with debug off (the default, and it auto-expires after 24h) a throwing RPC handler produced no Sentry issue — only duration/error counters. Move capture to the always-on path in both the backend rpcHook and the RN client hook, gated only on Sentry being initialised. Add the missing trailing .catch on the backend chain so a throw from a metrics call can't escalate to unhandledRejection -> handleFatal -> process.exit.
The 22+-char URL-safe-base64 token rule over-matched Sentry's own identifiers: it redacted contexts.trace.trace_id (breaking crash-to-trace correlation and risking event rejection), collapsed long PascalCase exception type names to [redacted] (breaking issue grouping), and dropped metrics whose error_class tag was a long error name. Remove it from both the RN scrubber and the backend mirror, leaving the targeted root_key= and lat/lng rules. Bare tokens are unscrubbed until the team agrees a narrower rule; documented in-code and in the tests.
|
Update — findings addressed (pushed):
Still open for the team: #4 (duplicated scrubber/metrics modules), #10 (the metric-wrapper dedup), #9 (Android cross-process breadcrumb, already documented as deferred), and the eventual narrower token-scrub rule. |
The RPC hooks now observe errors for metrics + tracing only; neither the backend rpcHook nor the RN client hook calls captureException. An RPC rejection is often expected control flow (e.g. NotFound) that should not auto-create a Sentry issue — whether an error is worth reporting is the calling application's decision, made at the call site. The response and its rejection reach the JS caller through rpc-reflector's own channel independently of the hook, so not capturing here changes nothing about propagation. Genuine backend fatals are still caught by the global uncaughtException/unhandledRejection handlers (captureFatal).
|
Follow-up on the earlier review thread: RPC error capture — removed from the hooks entirely. Tracing of the actual flow confirmed the RN client hook sees every RPC failure (handler errors come back deserialized with the backend stack preserved, plus client-only Storage-size metric — the recursive-walk concern is filed as #178 (replace the JS walk with Android Still open for the team: the duplicated scrubber/metrics modules, the metric-wrapper dedup, and the eventual narrower token-scrub rule (broad base64-22 rule remains disabled). |
…e + error metrics Front-end feature-usage recording moves to PostHog, so drop the RN `recordUsage` API and the backend `comapeo.usage.screen`/`feature` counters. The `applicationUsageData` toggle stays as the opt-in tier and now gates the usage-revealing metric dimensions instead: the RPC `method` breakdown (client + server latency and `send_ms`) and the sync `peers_bucket`/`bytes_bucket` counts. Aggregate latency, sync duration/outcome, storage-size bucket, boot/health/exit signals, and device tags stay at the always-on diagnostic tier. The Android app-exit exact-ms durations remain gated, with the collector's flag renamed from `captureApplicationData` to `applicationUsageData`. Drop the `comapeo.rpc.server.errors` counter entirely: routine RPC rejections (NotFound and similar) aren't worth counting, and the latency metric's `status` tag already carries the error rate. Significant errors reach Sentry as issues via application code. Also remove the now-dead `captureApplicationData` rename-migration code and legacy aliases (the module is unreleased), and rewrite code comments that referenced the temporary planning doc so they read as permanent. Update docs/sentry-integration.md to the three-toggle model with a per-signal gating table and justifications.
| // Read the debug + usage prefs BEFORE SentryFgsBridge.init: | ||
| // readDebugEnabled() may queue the auto_disabled breadcrumb, and init | ||
| // drains the DebugAutoOff queue (SentryFgsBridge.init → | ||
| // DebugAutoOff.consume). If init ran first the crumb would be lost on | ||
| // the launch that performed the auto-off. These reads are independent | ||
| // of the diagnostics gate. |
| * the production constructor. | ||
| * the production constructor and runs the one-shot key migration. | ||
| */ | ||
| internal class ComapeoPrefs( |
There was a problem hiding this comment.
Is this the right abstraction at this point, now we have added more values to it? Take a step back and think through other abstractions over SharedPreferences that might involve less boilerplate and reduce code needed for each additional pref that is added.
| /** | ||
| * Holds the `comapeo.debug.auto_disabled` breadcrumb queued by the 24h | ||
| * auto-off. [ComapeoPrefs.readDebugEnabled] runs before | ||
| * `Sentry.init`, so the breadcrumb can't be added directly; it's drained | ||
| * by [SentryFgsBridge.init] / RN init once the SDK is up. | ||
| * | ||
| * Known gap: the main app process and the `:ComapeoCore` FGS process are | ||
| * separate JVMs with separate `DebugAutoOff` statics. On the common | ||
| * cold-start ordering the main-process `sentryPreferences` read | ||
| * ([ComapeoCoreModule]) can win the 24h flip; the FGS read then sees | ||
| * `debug` already false and is a no-op, and the main process has no | ||
| * native-side drain — so the crumb queued there is currently dropped. | ||
| * The auto-off behaviour itself is unaffected; only the timeline marker | ||
| * is lost. Delivering it would need cross-process plumbing (expose the | ||
| * pending flag to JS and drain in the RN `initSentry` path). | ||
| */ |
There was a problem hiding this comment.
Tighten this comment and make it clearer what the consequences of the current gap are.
| const val DEFAULT_DEBUG = false | ||
|
|
||
| /** 24h in milliseconds. */ | ||
| const val DEBUG_MAX_AGE_MS = 24L * 60 * 60 * 1000 |
There was a problem hiding this comment.
Change debug auto-off to 72h (on both android and ios)
| // (see `setDiagnosticsEnabled` / `setCaptureApplicationData`). | ||
| // (see `setDiagnosticsEnabled` / `setApplicationUsageData` / | ||
| // `setDebugEnabled`). | ||
| Constant("sentryPreferences") { () -> [String: Any] in |
There was a problem hiding this comment.
Same questions here as in the kotlin side
| /// | ||
| /// Constructor takes read/write closures so unit tests don't need a | ||
| /// real `UserDefaults`. | ||
| final class ComapeoPrefs { |
There was a problem hiding this comment.
similar to kotlin, take a step back and thing whether this remains the best abstraction for these preferences - as we add more values to the prefs it starts to become a bit unweildy.
| - `getDebugEnabled()` / `setDebugEnabled(value)` — opt-in debug mode (per-RPC | ||
| traces, `@comapeo/core` OTel spans, richer capture). Restart-to-activate and | ||
| auto-expires 24h after the most recent enable. | ||
| - `recordUsage.screen(name)` / `recordUsage.feature(name)` — record a |
There was a problem hiding this comment.
This can be removed from the readme I think. Does this API still exist?
…trics rework Correctness fixes surfaced by the code review: - Break the ComapeoCoreModule -> sentry-metrics -> sentry import cycle (sentry-metrics reads device tags from readSentryConfig, memoized) so importing the package entry no longer throws a TDZ ReferenceError at load. - Scrub the structured-log channel via beforeSendLog (RN + backend). - scrubUrlToHost drops the query/fragment of relative URLs instead of falling back to a no-op; scrubValue guards against cycles and depth; the rootKey pattern stops at field delimiters instead of eating the rest of the string. All mirrored in src/sentry-scrub.ts + backend/before-send.js. - Backend 60s sampler uses monitorEventLoopDelay and is skipped entirely when Sentry is off; rpcStatusFor never reports a failure as "ok". Review-driven reworks: - tracesSampleRate: native resolves the effective rate (debug window forces full, else the plugin cap) and forwards it; backend and RN mirror it. - Drop the misleading rss gauge (whole-process on iOS); keep heap_used + uptime. - ComapeoPrefs (Kotlin + Swift) persists through a Store abstraction instead of five positional lambdas; debug auto-off is 72h with a backward-clock guard. - metrics.js gains a by_device mirror helper; the server per-method RPC series is dropped (the client end-to-end metric owns the method breakdown). - Snapshot the applicationUsageData tier at boot rather than per RPC. Docs/tests: - Shared scrubber case table (test-support/) exercised by both the RN jest and backend node:test suites so the mirrored copies can't drift. - Metrics reference table in docs/ARCHITECTURE.md; drop stale capture-application-data / recordUsage references from README and docs.
…e state `getDiagnosticsEnabled` / `getApplicationUsageData` / `getDebugEnabled` read the current persisted value instead of the boot snapshot, so a settings screen reflects a `setX` made earlier in the same session and survives a JS reload — application code no longer has to keep its own app-wide copy in sync. - Add a synchronous native `getSentryPreferences()` (Kotlin + Swift) that reads the current SharedPreferences / UserDefaults values fresh (cheap in-memory reads). `debug` is the raw stored toggle via a new side-effect-free `ComapeoPrefs.readDebugStored()`, so a getter never triggers the launch-time auto-off write. - The `sentryPreferences` Constant stays the boot snapshot that the module's own session behaviour (initSentry, metrics tier, debug tracing) is pinned to — a live read there would be wrong, and reading it once keeps it off hot paths. - RN `readSentryPreferencesLive()` backs the public getters and falls back to the snapshot when the native live getter is absent (older native / tests). Reading live rather than caching boot state is what makes the reload case work: there is no cached snapshot to go stale.
…e server per-method
Sentry Application Metrics bills by ingested volume, not cardinality (high
cardinality is the default, not a limit), so the primary + `.by_device` split
was doubling emitted volume for no saving while making method × device queries
impossible. Collapse each pair into a single metric that carries every
dimension — status, phase, and the device tags all ride together — and slice
with group-by at query time.
- Drop every `.by_device` metric and the `distributionMirrored` helper; device
tags now attach to the single duration metric (RN + backend).
- `method` (and the sync collaboration/volume buckets) stay gated on
`applicationUsageData`, but as a privacy gate now, not a cardinality one:
when on, `method` is just another attribute on the one metric.
- Restore server per-method RPC: rpcServer(method, status, ms) →
`comapeo.rpc.server.duration_ms{method,status,device_class,os_major}`, so
handler-vs-IPC latency is separable per method (cheap now that cardinality
isn't billed).
- Update the metrics tests and the §7.5 metrics reference table to the
single-metric model and the volume-billing rationale.
Verified both SDKs emit to the GA Application Metrics product: @sentry/core
10.58.0 (under @sentry/react-native 8.15.1) and @sentry/node-core 10.53.1,
both above the metrics minimums.
…op candidates Each metric now states what we can actually learn from it, and honestly flags the ones whose aggregate is weak or redundant, with a "candidates to reconsider" list: fgs.uptime_s (sampled monotonic gauge, no actionable aggregate), rpc.client.send_ms (redundant with the debug-span rn.send.syncMs attribute), state.transitions (overlaps boot.outcome), and event_loop_delay_ms (60s mean is too blunt — p99 would be sharper).
Remove metrics whose aggregate carried no actionable signal: - comapeo.fgs.uptime_s — a 60s-sampled monotonic gauge; its average is meaningless and its max is just ≈ session length. - comapeo.rpc.client.send_ms — redundant with the rn.send.syncMs attribute already recorded on the debug-gated client span (which is kept). - comapeo.state.transitions — the started/error signal overlapped boot.outcome; the rest was routine lifecycle churn. Keeps the rn.send.syncMs span attribute and the event-loop-delay gauge (the latter's sampling strategy is being reworked separately).
…live read The two native primitives are both needed but were confusingly named. Rename to make the difference obvious, and stop the live read from running on every render. - Constant `sentryPreferences` → `sentryPreferencesAtLaunch`: the snapshot in effect this session (initSentry, metrics tier, debug tracing are pinned to it). - Function `getSentryPreferences` → `getCurrentSentryPreferences`: the current saved value (raw debug, no auto-off) for a settings screen to read back. - JS wrappers follow: readSentryPreferences → readSentryPreferencesAtLaunch, readSentryPreferencesLive → readCurrentSentryPreferences. The public getDiagnosticsEnabled/getApplicationUsageData/getDebugEnabled getters now read an in-memory cache seeded once from the current-value native call and kept in sync by the setters, so a preferences screen doesn't pay a synchronous SharedPreferences/UserDefaults read (Android also a PackageManager metadata read) on every render. A JS reload drops the cache and it re-seeds, so it still reflects changes made since the native process launched.
The 60s mean buried a brief stall in ~6000 samples. Report the window max (worst single stall per minute) as a distribution with device tags, so Sentry gives fleet percentiles of the per-minute worst stall, sliceable by device class. Drop resolution 20→10ms. monitorEventLoopDelay is pure libuv (no addon/worker/inspector) so it runs in nodejs-mobile; Sentry's stack-on-stall integrations don't (the deprecated anrIntegration needs node:inspector, which nodejs-mobile disables via its intl dependency; eventLoopBlockIntegration is a native addon with no mobile prebuild). A stack-on-stall event is left as a possible follow-up.
Anchor Sentry identity on a random per-install root user ID stored in
native prefs and never sent to Sentry. user.id is sha256("<root>|<salt>")
truncated to 16 hex chars: salt is the UTC YYYY-MM month (rotates
monthly) by default, or the constant "permanent" when the user opts in
to applicationUsageData. Both derivations are recomputable from a
user-shared root ID (exposed via getRootUserId() for a debug screen), so
historical events can be re-associated for support cases.
Native derives the value once per process start and distributes it to
all three SDKs: Sentry.setUser on RN (via the sentryConfig constant),
scope user on the native inits, and --sentryUserId argv to the backend's
initialScope. Kotlin/Swift derivations share pinned test vectors.
The scrubbers covered lat/lng/latitude/longitude but not `lon` — the field name @comapeo/schema observations actually use — and the marker regex missed JSON-serialized coordinates (`"lat":-12.3`) because it lacked the optional-quote handling the rootKey pattern already had. Shared regression cases added so the two mirrored copies can't drift.
…write lands The setters mutated the in-memory view before the native persist resolved, so a rejected write (e.g. native context not attached) left the getters reporting an opt-out that never happened — the UI would show diagnostics off while the on-disk value stayed on.
… policy The auto-off constant shipped as 72h but the breadcrumb text, JSDoc, and docs still said 24h — align them all on 72h. Document the actual trace-sampling policy (debug ? 1.0 : plugin rate, 0 when unset — a build-time consumer decision, not a per-user gate) in the three places that still described the old usage-toggle gate, and replace the stale "identity placeholder" / base64-22 claims in §8 with what the shipped scrubbers actually do.
Replace the UUID root user ID with XXXX-XXXX-XXXX from a Crockford-style base32 alphabet (no I/L/O/U): 60 bits of entropy — ample collision headroom across the install base — while short and unambiguous enough for a user to manually transcribe from a screen for a support case. Generated with a secure RNG on both platforms; stored and hashed exactly as formatted, hyphens included.
…peo-core-react-native into feat/sentry-metrics-phase11 * 'feat/sentry-metrics-phase11' of github.com:digidem/comapeo-core-react-native: chore: workflow cleanup & syntax highlight hints merge main feat(sentry): symbolicate backend errors in debug builds without uploading sourcemaps test(ios): fix shutdown-ordering race in IPC lifecycle tests Release v1.0.0-pre.8 chore(deps): bump the minor-and-patch group across 1 directory with 3 updates chore(deps): bump rpc-reflector fix(sentry): make initSentry idempotent across JS-bundle reloads chore(deps): bump @comapeo/ipc Release v1.0.0-pre.7 chore(deps): bump comapeocat chore(deps): bump @digidem/types chore(deps): bump the minor-and-patch group across 1 directory with 4 updates ci: reference PR bot app-id/user-id as vars, not secrets ci(deps): fast-track first-party dependency updates fix(android): address review — @volatile watchdog fields, stronger test, doc fix fix(android): graceful node drain before self-terminate kill test(android): debug-gated seam to exercise the FGS self-terminate watchdog feat(android): self-terminate FGS on terminal ERROR for uniform crash recovery
Implements the Sentry Phase 11 workblock (tracking #74) in one branch / one PR, plus the user.id half of #78 and the review fixes from the pre-merge review. Spec:
docs/sentry-integration-plan.md§11.1–§11.8, §9b.1, §9b.2, §9b.5.Part of #74 (tracking umbrella — remaining children stay open: #77, #78, #80–#83, #190, #191)
Closes #75
Closes #76
Closes #161
#75 — Toggle rework (§11.1, §11.5, §11.6, §11.7, §11.2.b)
captureApplicationData→applicationUsageDataeverywhere: JS get/set, native bridge methods, the on-device stored key, the Expo plugin field, and the Node CLI flags. Deprecated aliases forward for one minor release; a one-shot stored-key migration runs on first prefs open on both platforms.debugtoggle:get/setDebugEnabled(JS),setDebugEnabled(native),sentry.debug+sentry.debugEnabledAtMsslots,--debugargv,debugDefaultplugin field. 72h auto-off (§11.5 specced 24h; widened during review): at process start the debug reader flipsdebug=falseif switched on >72h ago and queues acomapeo.debug.auto_disabledbreadcrumb; re-enable refreshes the window.DeviceTags.{kt,swift}bucket the device low/mid/high by RAM + cores and compute<platform>.<major>. Passed to JS viasentryConfig.deviceTagsand to Node via--deviceClass/--osMajor/--platformTag.tracesSampleRateis now owned by native and mirrored by the other layers:1.0while thedebugwindow is on, otherwise the plugin-configured rate (0when unset — the expected production config). This resolves the four-init-site divergence documented in Sentry: captureApplicationData → tracesSampleRate gate diverges across the four init sites #161; the policy is documented indocs/sentry-integration.md§8/§9.#76 — Metrics layer (§11.2, §11.3, §11.6, §11.8)
backend/lib/metrics.js+src/sentry-metrics.ts: thin wrappers aroundSentry.metrics.*that injectplatform+ units, no-op when Sentry is off, and run a forbidden-tag filter on every emission..by_devicemirrors were collapsed — the new Sentry metrics product bills by volume, not cardinality, so device tags ride the primary metrics and group-by does the slicing). The RPCmethodattribute is gated onapplicationUsageDatafor privacy, not cardinality.comapeo.app.exit. Sync-session / shutdown / transport-health emitters ship unwired — call sites tracked in Sentry: sync-session transaction + bg/fg breadcrumbs (usage tier) #80 and Sentry: wire the remaining diagnostic metric emitters (shutdown phases, ipc.errors, telemetry.forwarding_failures) #190, and their rows in the §9.2 tier table are marked accordingly.debug, recording the metric while the span is active so it links to the trace. Hooks never capture issues — RPC rejections are the caller's decision. BackendconsoleIntegrationmoved behinddebug.PII scrubbers (§9b.1, §9b.5 — the bulk of #77)
src/sentry-scrub.ts(RNbeforeSend, ahead of the host's chain) andbackend/before-send.js(Node event processor), kept in sync by shared cases intest-support/scrubber-cases.js. Walks message, exception text, extra, contexts, breadcrumb message + data, span data, and structured logs.lat/lng/lon/latitude/longitude, in key-value, JSON-quoted, and object-key forms.error_classtags); Sentry: narrower scrub rule for bare rootkey-shaped tokens (base64-22) #77 is re-scoped to the narrower design, so it stays open.user.id — root user ID + monthly rotation (§9b.2 / §11.4, the user.id half of #78)
XXXX-XXXX-XXXX, base32 without I/L/O/U, 60 bits, secure RNG) generated lazily and stored in on-device settings so uninstall resets identity. Short and unambiguous so a user can hand-copy it from a screen.user.idis always a derived hash (sha256("<root>|<salt>"), 16 hex chars): salt is the UTCYYYY-MMmonth by default (rotates monthly, cross-month unlinkable) or"permanent"underapplicationUsageData(stable, cohort analysis works). The raw root ID is never sent.sentryConfig.userIdto JS (Sentry.setUser, locked), and passes--sentryUserIdto the backend'sinitialScope.getRootUserId()exposed from the/sentrysub-export for a debug/about screen; support can recompute historical monthly IDs from a shared root ID. Kotlin/Swift derivations share pinned test vectors.Review fixes
lonfield (@comapeo/schema's actual field name) and quote handling for JSON-serialized coordinates.tracesSampleRatepolicy in the three places that described the old gate, §8 rewritten to what the shipped scrubbers do, unwired metric rows flagged in the tier table.Checks run locally
npm run lint,npm run build— passnpm test(jest, src) — 69/69npm run backend:test— 73/73swift test(ios/) — 118/118./scripts/run-instrumented-tests.sh --unit-only(Android JVM) — pass (incl. newSentryUserIdTest, prefs root-ID tests)Deferred (tracked)
syncSessionmetric wiring, bg/fg breadcrumbs — Sentry: sync-session transaction + bg/fg breadcrumbs (usage tier) #80ipc.errors,telemetry.forwarding_failures) — Sentry: wire the remaining diagnostic metric emitters (shutdown phases, ipc.errors, telemetry.forwarding_failures) #190