chore(native): retire the transport-migration probes - #2737
chore(native): retire the transport-migration probes#2737innolope-dev wants to merge 166 commits into
Conversation
Resolves 8 conflicts. Where main reworded copy after it was extracted, the
catalog now follows main — the exit prompts had reversed meaning (the KYC
flow saves progress now, so 'you'll have to start over' was wrong in every
locale), and the withdraw title became 'You're withdrawing'.
Drops keys orphaned by main: the StartVerificationView interstitial and the
autoStart 'lose progress' variant are both gone.
Extracts copy main added into the localized surface: the /fix-card-signature
support page and the deposit-cancel warning. That warning's noun was
interpolated ('Cancel this {noun}?'), which can't be translated — es/pt need
gender agreement — so each kind carries its own sentence.
Four cooperating defects kept a dead session resurrecting itself and the webview reloading in a flash loop that only 'clear all app data' escaped: - CapacitorUpdater.set() reloads the webview IMMEDIATELY — the comment believed it deferred to next launch. Use next(), which actually defers. - MainActivity served pre-rendered HTML from APK assets even while an OTA bundle was active, so any hard navigation (logout goes to /setup) loaded a stale export whose chunks no longer exist. Resolve HTML against the active server base path first, assets only when no bundle is live. - logout cleared the JWT before cancelling queries, so an in-flight /users/me sliding refresh re-persisted the very token being wiped. Cancel + clear the query cache first. - the sliding refresh now checks a clear-epoch counter and drops the re-minted token when the session was cleared mid-flight; the 401/404 clearAuthToken is awaited so Preferences.remove lands before teardown.
The white-panel-above-the-inset layout is identical on both platforms, but the white fill was gated to isIOSNative — Android 15 edge-to-edge painted the nav-bar inset periwinkle under the white panel, reading as a stray blue strip on the onboarding screen.
The animated-WebP mascots drop frames in the Android WebView — the same symptom the GIF fallback fixed on legacy iOS — but the fallback was gated on isLegacyWebKit, which can never match an Android UA.
The scale/opacity enter tween hitched on first-frame rasterization in the Android WebView; transform-gpu + will-change-transform pre-promote the layer. Also drops the dead max-h-[] class.
The redirect keyed on isloginClicked && user, and isloginClicked is set before the ceremony — a stale user re-appearing mid-ceremony (the OS passkey sheet blur/refocus fires refetchOnWindowFocus) pushed /home with balance and activity rendered before any authentication. Gate the redirect on loginResolved as well.
…itial A valid, completed session (hasAppAccess) that cold-starts on /setup was stopped at 'You're already signed in — Continue as <user>' whose CTA just pushes /home. Route those straight home; keep the interstitial for the half-finished-signup case it was written for.
Calls the new POST /users/logout (peanut-api-ts tokenVersion bump) with the still-valid JWT before clearing local state, so logout now kills the session on every device instead of only wiping this one's storage. Best-effort: a dead backend falls through to local logout (and the forced-logout path skips it entirely, as before).
Release builds signed the App Store configuration with App.entitlements, which carries aps-environment=development. TestFlight/App Store binaries therefore registered against the APNs sandbox while OneSignal sends via production, so every push silently reached zero devices. Point the release config at a new AppRelease.entitlements with aps-environment=production. Android had the mirror-image hole: android-release.yml skipped google-services.json when ANDROID_GOOGLE_SERVICES_JSON was unset and build.gradle swallowed the missing file, so release AABs shipped with the google-services plugin silently omitted and no FCM token registration. Both now fail the build instead of shipping dead push, and the iOS workflow verifies aps-environment in the provisioning profile and in the exported IPA.
…gging A failed OneSignal login() left the device subscription without the external_id the backend targets, so pushes resolved to no recipients with nothing in Sentry. Capture the failure, and only commit lastLinkedExternalId on success so transient errors retry on the next sync rather than latching the device into a permanently unlinked state. Add opt-in verbose SDK logging (NEXT_PUBLIC_ONESIGNAL_DEBUG, baked in via the pushDebug workflow input, or localStorage.__onesignal_debug) for diagnosing release builds where NODE_ENV-gated tooling is eliminated.
Adapter imports conflicted: main gained notification-click handling after the push-fix branch was cut. Kept both sides — click listeners and the opt-in debug logging.
The earlier hard-fail was based on a wrong premise. OneSignal's Android SDK never reads google-services.json: PushRegistratorFCM builds FirebaseOptions in code from its own FCM_DEFAULT_* constants plus the sender ID served by the OneSignal dashboard, and com.onesignal:notifications pulls firebase-messaging in transitively. The google-services plugin only wires Firebase's automatic initialization, which OneSignal bypasses. The "Push Notifications won't work" comment that motivated the guard is Capacitor's stock boilerplate for @capacitor/push-notifications, which does need the file. This app uses OneSignal, which does not. Keep validating the file when the secret is set; skip it when unset. Android push depends on the OneSignal dashboard's FCM v1 config, not on this file.
OneSignal's Android SDK registers with FCM on its own — PushRegistratorFCM builds FirebaseOptions in code from the sender ID served by the OneSignal dashboard, and firebase-messaging arrives transitively via com.onesignal:notifications. The google-services plugin and its JSON only wire Firebase's automatic initialization, which OneSignal bypasses, so they were dead weight. Drop the com.google.gms:google-services classpath, the conditional plugin apply, and the CI "Decode google-services.json" step. Android push now depends solely on the OneSignal dashboard's FCM v1 config (a service-account key, which is a different Firebase file than google-services.json).
isCoveredByDisableList only consulted ITEMS_TO_DISABLE, so routes handled via P0_TRANSFORMS — whose replacements strip force-dynamic/generateMetadata — were reported as uncovered server-only routes and failed the native build. (mobile-ui)/claim/page.tsx is the case that broke it: it lives in P0_TRANSFORMS, not ITEMS_TO_DISABLE, so the guard never saw its transform. invite survived only because it also has a dir entry in ITEMS_TO_DISABLE. Treat a route as covered if it's in either list. This regressed native Android builds since ~v1.0.20; the bug is present on main too.
Google now rejects the Play Edit commit with "Changes are sent for review automatically. The query parameter changesNotSentForReview must not be set." once the app has a reviewed base. The flag was only needed for the very first release; with a reviewed base the edit must go to review automatically. The AAB itself uploaded fine — only the commit step failed on this flag.
…ugin cap sync ios silently omits sources/SumsubCordovaIdensicMobileSdkPlugin (exit 0, no folder) when node_modules isn't fully materialized, and the committed CapApp-SPM/Package.swift then fails the archive a minute later with a cryptic SwiftPM "folder doesn't exist" error. - native-ios-postsync.js: hard-fail with an actionable message when the sources dir is missing but the plugin is still installed or still referenced by CapApp-SPM/Package.swift; only skip on genuine removal. - ios-release.yml: verify the SumSub Cordova plugin materialized right after pnpm install, catching the partial-install root cause at the point where a job re-run recovers.
A bare `vX.Y.Z` tag push only set CURRENT_PROJECT_VERSION and left MARKETING_VERSION at the project default 1.0, so tag releases shipped to TestFlight as version 1.0 (flagged outdated). Fall back to the tag name (v1.0.31 -> 1.0.31) when no explicit versionName input is given, matching android-release.yml.
The suite was added on main while the component still used hardcoded strings; the i18n branch converted it to useTranslations, so the two only break when merged. Use the same IntlWrapper pattern as the rest of the localized suites.
Three changes that together make PEANUT-UI-R5F diagnosable in the field: - Split the single direct-fetch canary into three probes (unauthenticated GET, authorized GET, POST) so reachability, CORS preflight and the GET-vs-POST asymmetry seen on-device can be told apart. Probes run in parallel and report in a fixed order. - Tag the binary version/build on every native Sentry event. With OTA the JS `release` and the installed binary diverge, which made R5F look like it came from a build it didn't. - Actively unregister service workers inside the Capacitor WebView. Builds before 2026-04 registered the PWA SW there; the native bundle ships no sw.js, so those registrations can never self-update and sit frozen in front of all GET traffic.
… to production Also updates the src/content submodule pointer to its latest commit.
…is blocked The edge in front of api.peanut.me rejects Android WebView GETs at the TLS-fingerprint level (PEANUT-UI-R5F): the block response has no CORS headers, so fetch surfaces it as an instant opaque TypeError while POSTs (passkey verify) and Sentry ingest sail through. CapacitorHttp.request() uses the OS HTTP client — no browser fingerprint, no CORS — and is registered natively regardless of the CapacitorHttp.enabled flag, so the fallback is OTA-deliverable to existing 1.0.32 binaries. fetchWithSentry retries API requests over the native transport when the WebView fetch rejects; FormData bodies keep the WebView path. Canary v3: per-probe messages (v2's identical messages were collapsed by Sentry's dedupe integration — only the first probe ever arrived), transport detection compares against the pre-wrap fetch (the auth-capture wrapper made v2 report cap-http-proxy on proxy-off binaries), plus no-cors and native-transport probes to separate edge blocks from dead networks.
Demo mode is the app-store review sandbox — synthetic data, no real backend — so the beta-feedback, connectivity, and maintenance banners don't apply there.
… fallback (OTA download broken on affected devices)
Brings 53 commits from main into the release branch ahead of the next mobile build: push-tap routing, OneSignal failure reporting, biometric app lock, fresh-passkey step-up, report-only CSP, Manteca per-rail wallet hotfix, Rhino route minimums and the Sentry noise fixes. Nine conflicts, all main-adds-behaviour vs mobile-release-adds-i18n. Resolved to keep BOTH in every case: - ClientProviders: main's AppLockGate now wraps children INSIDE mobile-release's AppIntlProvider > ContextProvider nesting. Getting this backwards 500s the whole app and unit tests don't catch it. - authContext: took main's clearStepUpToken(); dropped its duplicate query cancel/clear, which already runs earlier here — deliberately BEFORE the token wipe, or an in-flight /users/me re-persists a sliding-refresh token and logout never sticks on Android. - PhysicalCardScreen: main's null-position fix expressed through the translation layer. Needed a new onListBodyNoPosition key in all three locales rather than interpolating '#null'. - withdraw: kept main's USD-pinned validation (no token-price scaling) with mobile-release's translated error copy; dropped the now-dead tokenSelectorContext and INSUFFICIENT_BALANCE_MESSAGE imports. - ApplicationStatusScreen: main's proof-of-address props with mobile-release's COPY_KEYS lookup. - notifications page: kept the localized date-group header, dropped main's hardcoded-English formatGroupHeaderDate. - src/content: took main's production-published pointer (ca7f9ca). The only legal-copy delta is a support@ contact address; the Rain-superset prohibited-activities expansion is present in both. Two suites from main rendered components that now call useTranslations; wrapped both in NextIntlClientProvider, same pattern as the existing qr-pay and SumsubKycWrapper tests. Full suite green: 162 suites, 2133 passed.
- card.ts: restore mobile's apiFetch transport (demo-mode routing +
tokenless native transport; the authHeaders() revert threw
'Authentication required' in the App Store demo wallet and for Android
legacy cookie-jar sessions) while keeping main's geoProhibited field
- PendingVerificationTasks: accept the legacy comma-joined string shape
of pendingVerificationTasksDismissed on read (pre-1.0.51 builds) —
without the guard a dismiss after OTA spreads it char-by-char
- CancelDepositActions: take main's ICU-select version after all — the
{kind, select} messages are full sentences per locale, the
'untranslatable' rationale was stale; restores cancelConfirm group,
drops mobile's five flat keys, avoids re-conflict on future syncs
- catalogs: remove dead twins onListBodyNoPosition / notAWalletAddress
- demo-api.test: collapse triplicated describe + doubled jest.mock
…lease-20260810 sync: back-merge main into mobile-release (466 commits, 75 conflicts resolved)
…sessions authenticate chargesApi.create posted FormData via fetchWithSentry for every create, even with no file attached. That bypasses apiFetch's preferNativeTransport, and FormData can't cross the native bridge — so a legacy cookie-jar session on Android POSTed /charges with neither Authorization header nor cookie, 4xx'd, and surfaced as the generic "contact support" copy (PEANUT-UI-RXP shows the blind spot: "Failed to create charge: " with an empty h2 statusText). Attachment-less creates now go as JSON through apiFetch (the backend route is content-type agnostic); real file uploads keep the multipart path. sendLinksApi.create's existing JSON branch routes through apiFetch the same way. Failures now throw ApiError carrying status+code, and friendlyError discriminates: 401/403 → new sessionExpired copy, 5xx → the retryable network-busy copy — an auth failure can no longer masquerade as a bug report.
…MERA before Crisp opens qr-scanner's setCamera tears the old stream down before the new one starts, so the front-camera switch painted a dead grey frame for the whole handover on Android. toggleCamera now drops isCameraReady around the swap (the existing "starting camera" placeholder covers the video) and guards against a second toggle racing overlapping stream restarts; the visibility-resume path gets the same treatment. ensureNativeCameraPermission moves to a shared util and runs before CapacitorCrisp.openMessenger(): the manifest declares CAMERA for the QR scanner, which makes Crisp's "Take a photo" throw a SecurityException while the permission is declared-but-ungranted — the SDK never requests it itself. A denied camera deliberately doesn't block the chat.
… links on every cold start Two boot regressions with one root: startup work lived in the wrong place. The login screen flashed on every logged-in cold start because the root redirect read the in-memory token synchronously (null until Preferences hydration) and bounced through /setup, while the splash plugin's 500ms auto-hide exposed the whole thing. The generated root page now awaits hasNativeSession(), capacitor.config.ts sets launchAutoHide:false, and a new useSplashGate hook in ClientProviders is the single owner of hide() — gated on the route settling off the root and the startup locale painting, with a hard 3s failsafe, and deliberately never on authReady(), which parks behind the biometric lock. App Links, push-tap routing and the lifecycle listeners split out of useNativePlugins into useNativeAppLinks, mounted in ClientProviders: the old mount point was the (mobile-ui) layout, so a cold start that landed on /setup (logged out) never registered getLaunchUrl/appUrlOpen and silently dropped the link. useNativePlugins keeps StatusBar/keyboard only — the (setup) layout styles the status bar itself and the two must not race. launchAutoHide:false is baked into the binary at cap sync; ship this JS via OTA before or with the binary that carries it (a late hide() on old binaries is a harmless no-op, the reverse order leaves /setup behind the splash until the Capgo appReadyTimeout).
A scanned or deep-linked peanut.me/<username> fell through the deep-link mapper to the raw web path — a route the static export doesn't ship — and dumped the user at home; in-app, every profileUrl() call site (history, rewards, invites, contributors) funnelled straight into the send form because the native build had no profile surface at all. New /profile/view?username= page renders the same ValidatedUsernameWrapper + PublicProfile pair as the web catch-all's profile branch (public route — the guest view exists for logged-out deep links). profileUrl() points at it, and the mapper's recipient block now resolves param-less recipient links: a bare username → the profile, payment shapes (amount segment, user@chain, address, ENS) → the send dispatcher. /invite?code= maps to /setup?step=signup with the params riding along — the invite landing page is stripped from the export — and the setup page persists ?code= as the same session inviteCode cookie the web InvitesPage and the deferred-install hand-off write. Registered in the Android intent filter in the companion android commit.
…the app origin, register /invite
Three binary-side fixes (store release required; none are OTA-able):
The manifest declared androidx.core.content.FileProvider directly with
tools:replace, and the merger — which keys providers by android:name —
folded Crisp's upload provider into ours and REJECTED its authority and
paths (blame report lines 11/470). Crisp's getUriForFile then threw
IllegalArgumentException, crashing "Take a photo" in support chat. The
entry is now an empty PeanutFileProvider subclass: distinct merge key, both
providers coexist (verified in the merged manifest — both authorities
present, Crisp keeps @xml/crisp_sdk_filepaths). The authority stays
${applicationId}.fileprovider because @capacitor/camera hardcodes it.
The SPA-fallback interceptor answered ANY dotless GET — including
cross-origin ones — with index.html, which is why the ipfs.io chain icons
(Base, Mantle, Avalanche) decoded as HTML and fell back to letter avatars.
It now only answers for the app's own host.
/invite joins the App Links intent filter, paired with the deep-link mapping
in the companion JS commit.
…ff ipfs.io Their chain-details URLs are dotless ipfs.io/ipfs/<CID> paths — fine on web where the next/image optimizer proxies them, broken natively where the unoptimized <img> hit the WebView's SPA fallback (fixed separately in MainActivity) and got HTML back. Override with CoinGecko rasters / the bundled mantle.svg, same precedent as Arbitrum and Linea, and drop the stale 'ipfs icons render fine' comment.
…laims paths:["*"] made iOS claim every peanut.me URL — marketing, help and locale pages included — while Android registers a 16-path allowlist. The AASA now lists the same routes as the Android intent filter (incl. the new /invite). iOS caches the AASA at install time, so this lands per-user on the next app install or update.
…e reload useStaleDeploymentReload's safety gate reads the global loading context, but the direct-send flow tracked its loading only in local state — so on a >12h-old document, the app-resume reload could fire mid-payment. Every Credential Manager prompt backgrounds the app, and the reload wipes the in-memory step-up token cache, forcing repeat passkey prompts (or reloading between the on-chain send and recordPayment). Mirror the flow's busy window into the loading context, like the link flow already does.
Charges/send-link native auth, QR camera flip, splash/login flash, in-app public profiles, /invite deep links, Crisp FileProvider un-hijack, WebView SPA-fallback origin gate, chain icon overrides, AASA alignment.
isPixEmvcoQr matched the payload GUI case-sensitively, but the BCB manual prints it as BR.GOV.BCB.PIX and most PSPs follow — so the canonical form of a "Pix copia e cola" failed the check. Scanning was unaffected (PIX_REGEX is /i and QRScannerOverlay lowercases before recognizeQr), which is why only the PASTE paths broke and why every existing fixture is lowercase. For an uppercase payload that meant validatePixKey returned "Invalid PIX key format", so PixKeySendView kept Continue disabled and the payment never started, and pixKeyToBRCode/pixKeyToQrPayUrl returned null. The quieter failure was extractPixKey: it fell through to substring search and returned the UUID found INSIDE the payload, swapping the user's code for a fragment of itself — dropping the amount and merchant a dynamic code encodes while still looking like a valid key. An EMV payload is now all-or-nothing: recognizable but unusable (bad length, recurring) returns null, never a piece of itself. isPixRecurringCode already normalized case; this brings isPixEmvcoQr in line.
…payloads Two ways the scanner's paste affordances refused work the scan path accepts. The iOS chip filtered the clipboard through extractPaymentValue(_, 'evmAddress') and rejected anything else, so a copied Pix code came back as "Copied text is not a wallet address" and the chip hid itself. hasStrings() reports only THAT the clipboard holds text, never what it is, so the chip was never entitled to filter: it now hands the raw text to the same scanValue path as "Click to paste" and lets recognizeQr classify it. Label follows (useCopiedCode), and the two address-only strings retire with it. Paste also rendered only inside a live viewfinder. Pasting needs no camera, but a denied or failed camera swapped the whole surface for the permission modal or the error view — and on native the OS grant is a sticky per-install decision, so declining it removed the app's only entry point for a copied code. Both states now offer it; the modal owns the screen in the denied case, so the action sits inside it as a second CTA rather than behind it.
Finishing Sumsub opened the isIssuing gate, which renders
ApplicationStatusScreen variant="pending" — header "Add card", body
"Setting up your card…", a spinner and nothing else. Three defects made
that a trap on Android: the branch passed no onPrev, so the screen
carried no control at all; the poll's finally released the gate only
when the controller was not aborted, and nothing else ever clears
isIssuing, so any non-unmount abort stranded the user permanently; and
pollUntilReady checks its 30s deadline only BETWEEN requests, while
getCardApplyReadiness inherited the 20s default timeout plus the silent
GET retry, so one stalled request (the usual state right after the app
resumes from a native activity) held the screen ~40s before the deadline
was even evaluated. Spinner still animating, no button to press, hardware
back the only way out.
pending now gets onPrev={onBack}; the gate is released on every exit path
including aborts, guarded by a mounted ref plus an ownership check so a
superseded poll cannot clear the gate its successor owns; and the
readiness read — one webhook-stamped flag — is capped at 8s so the poll
budget means what it says.
Unifies the native release line with main: main gains the full native layer (Android/iOS shells, capacitor config, OTA workflows) that shipped as v1.0.52, and mobile-release's post-1.0.52 JS fixes. Releases are cut from main from here on; mobile-release is frozen. Conflict resolution: src/content kept at main's newer pointer.
…n key (dark flag) The mixed path costs two passkey prompts — the Rain admin EIP-712 and the UserOp — which testers read as a duplicate request. Behind the dark SESSION_KEY_SPEND flag (build env + per-device localStorage, the harness two-gate pattern), a single tap now signs a permission-enable for a throwaway in-memory ECDSA key that silently signs both. Authority is enforced on-chain, not by app code: a CallPolicy pinned to the exact batch (targets, selectors, recipient EQUAL, amounts ≤ this spend), a SignatureCallerPolicy pinning who may consume its ERC-1271 signatures to Rain's contracts, a TimestampPolicy (180s), RateLimit count 1 — and the batch's last call uninstalls the permission itself, because the fork spike proved the TimestampPolicy does NOT expire the 1271 path: without the self-destruct a leaked key would keep admin-signature capability forever. With it, the key's authority does not outlive its own transaction. All load-bearing assumptions are fork-proven (scripts/spike-session-key-1271.mjs, 7/7 against an Arbitrum mainnet fork): enable-during-validation with a session-signed batch, CallPolicy scope rejection, 1271 caller pinning, and the self-uninstall killing the 1271 path post-op. Fallback is safe by construction: any failure — even ambiguous post-broadcast ones — retries the passkey path with the SAME prep, and the coordinator's adminNonce lets only one attempt execute (the loser's batch reverts atomically). A mined-but-reverted ephemeral op reports failure, which is exactly the shape an on-device disproof of the 1271 ordering would take. The enable signature binds to the live validator nonce and is injected via pluginEnableSignature — the SDK's internal enable path silently falls back to nonce 1 on read failures (the dead-approval class useGrantSessionKey already works around). Accounts needing migration or nonce repair throw pre-broadcast and fall back rather than spending extra taps.
The session-key grant gate in runCollateralSpendPreflight fired for mixed too, but the granted key is consumed only by the backend's collateral-only submit path (verifyRainWithdrawal 409s without it) — mixed broadcasts the user's own root-signed UserOp and never touches the stored approval, and /prepare has no approval requirement. First-time mixed spenders paid a third passkey tap for nothing. Gate on collateral-only.
…se branch Union resolutions of note: - native-routes: dev's null dead-end guard around the richer recipient mapping (profile view, request pots, charge context); /invite keeps its params AND writes the invite cookie - sentry.utils: reportNonOkResponse refactor + native-http fallback on top of dev's windowed-connectivity model (reportNetworkOk is gone) - SupportDrawer: unread-badge clearing inside the hardened native open - Drawer: scrollAreaClassName prop with the pb-safe-bottom token - capgo-deploy-ios.yml stays deleted; its --version-exists-ok already lives in the consolidated capgo-deploy.yml
Native reported 970 Sentry events in 24h, 691 of them level:info. Almost all of that is instrumentation written to watch the CapacitorHttp → direct-fetch switch (PEANUT-UI-R44). That switch has shipped; the probes have not. - native-canary: five captureMessage calls per app launch (~550 events/day), plus five extra API round-trips on every cold start, to answer a question we already answered. Deleted. - legacy-cookie native transport engaged / native http fallback engaged: once-per-session census notes for the same migration. Deleted. - onesignal subscription snapshot: still useful, but it's a state fact, not a fault — moved to PostHog as notification_subscription_snapshot. The failure variant stays in Sentry, since failing to read the state is a real error. Transport behaviour is unchanged; the fallback and prefer-native paths still work exactly as before, and their tests now assert the paths stay silent.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedToo many files! This PR contains 197 files, which is 47 over the limit of 150. To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to Pro+ to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (34)
📒 Files selected for processing (197)
You can disable this status message by setting the Comment |
|
@coderabbitai review |
|
|
|
Why
environment:nativeproduced 970 Sentry events in 24h. The level split:71% is instrumentation, not faults. Nearly all of it was written to watch the CapacitorHttp → direct-fetch transport switch (PEANUT-UI-R44). That switch shipped; the probes didn't get removed.
What goes
native-canary— deleted. FivecaptureMessagecalls per app launch (get-healthz,get-users-me,post-healthz,get-healthz-nocors,get-healthz-native), ~110 launches/day → ~550 events/day. It also fires five extra API requests on every cold start. Its own header comment states the purpose: watching CORS + credential behaviour "before and after the transport switch".legacy-cookie native transport engaged/native http fallback engaged— deleted. Once-per-session census notes (~25/day) for the same migration: how many users still sit on legacy cookie-jar auth, how often the WebView path gets rejected.onesignal subscription snapshot— moved to PostHog (~86/day). The data is genuinely useful (permission state, token presence, whetherlogin()linked the external_id), but it's a per-launch state snapshot, not an error — Sentry is the wrong store. NowANALYTICS_EVENTS.NOTIFICATION_SUBSCRIPTION_SNAPSHOT. Thesnapshot failedvariant stays in Sentry: failing to read subscription state is a real fault.What does not change
The transport logic itself.
preferNativeTransportstill tries the OS HTTP client first for tokenless native sessions, and the WebView-rejection fallback still rescues requests. Only the reporting is gone — both tests now assert those paths stay silent rather than asserting a notice count.Verification
sentry-fallback.test.ts,sentry-prefer-native.test.ts,native.adapter.test.ts— 11 passed, 3 suites.tsc --noEmit: 232 errors, all pre-existingCannot find module '@/assets/….svg'asset-declaration noise from a fresh worktree with no generatednext-env.d.ts. Zero in any touched file.prettier --check: clean.Base
Branched off
merge/mobile-release-into-main-20260817, because that's the only branch carrying this code — canary v3, the OneSignal snapshot, and both transport notes are absent fromdevandmain. Until #2702 lands, this PR's diff will also show #2702's changes. Review the two commits on top, or merge #2702 first and this reduces to the removal alone.Expected effect
~660 events/day removed from
environment:native, taking info-level volume to near zero and leaving the error tier as the actual signal.Related
beforeSendchained-exception bug and suppresses ~95/day of Capgo updater noise.disable_auto_update_under_nativecause (105/day) — deliberately not filtered, since it means OTA is dead for that binary.Update — the conflict banner is #2702's, not this change's
GitHub shows this PR as conflicting with
dev. The four conflicting files are:The eight files this PR's commit touches are:
Zero overlap.
git merge-tree origin/dev origin/merge/mobile-release-into-main-20260817conflicts on its own, so the banner is inherited wholesale from #2702's base — this branch is #2702's head plus one commit.I deliberately did not resolve them here: doing so would mean making reconciliation decisions for
native-routesandinstrumentation-clienton #2702's behalf, in files this change doesn't touch, and #2702 has to make those same decisions anyway. Resolving them in two places invites divergence.Unblocking order: land #2702 first, then this rebases onto
devas the removal alone. If #2702 is going to sit, say so and I'll re-cut this againstdevonce the probe code is there.