Conversation
The parse-error catch logged the whole frame:
console.error('Error parsing WebSocket message:', error, event.data)
console.error is not local. instrumentation-client.ts and
sentry.client.config.ts both register
captureConsoleIntegration({ levels: ['error', 'warn'] }), so every
console.error becomes a Sentry event. beforeSendHandler scrubs
request.headers, request.data, extra, contexts and breadcrumb data by key
name - it never touches event.message, and key-name redaction does
nothing to a raw serialized blob anyway.
The frames this handler receives are kyc_status_update,
sumsub_kyc_status_update, manteca_kyc_status_update, history_entry,
rain_card_balance_changed and user_rail_status_changed - user KYC state
and financial data. A malformed one carried all of it to Sentry.
Log the byte length instead. That still separates a truncated frame from
a malformed one, which is the only thing this catch ever needed.
The test pins it: it fails against the old line and passes against this
one. CodeQL alert #145 (js/log-injection, medium).
…ation fix(ci): consolidate UI workflow reliability
Second half of TASK-21141. The backend now writes an in-app notification row for every support reply; this shows it. The Support icon in the mobile nav gets a pink dot while support has replied and the user has not opened the chat. Opening the drawer clears it. The count is server truth, read from /notifications/unread-count?category=support — the Crisp widget is a sandboxed iframe on web and an event-less plugin on native, so the client cannot work this out for itself. Clearing hangs off isSupportModalOpen, which is the one flag every entry sets before anything opens — the nav tap, openSupportWithMessage(), the push deep link and the Capacitor path. One effect covers all four. SupportDeepLink handles /home?support=open, the link a support push carries. The pink dot was copy-pasted in three places and the badge would have made a fourth, so it is now one IndicatorDot component. The three call sites render the same as before — twMerge resolves the size and animation overrides. The name is deliberately neutral: on a transaction card the dot means pending, on the perk carousel it means claimable. Do not merge before the backend PR is deployed. An old backend ignores the category param and would light the badge for any unread notification.
jest.fn(async () => …) infers a zero-arg function, so calling it with the category failed tsc. Local typecheck predated this mock and missed it.
The freshness check compared backend timestamps against the DEVICE clock with
a five-minute tolerance. A phone more than five minutes out rejected every
response the backend could send — permanently, and there is no local fallback
left to catch it. Device-clock comparisons now allow six hours; the checks
that actually bound staleness compare two backend timestamps and stay tight.
Sentry now skips 503 on /fx/rate. It means a provider leg is momentarily
absent, which peanut-api already reports with the upstream cause attached;
reporting it client-side multiplies one incident by every mounted hook and
its retries, and buries the signal that can be acted on.
Also gives /fx/rate a real demo-api handler. Without one a failed passthrough
fell to defaultShape and answered 200 {} — a contract violation dressed as a
success. A canned rate is not possible (handlers never see the query string,
and the validator rejects a mismatched pair), so it answers 503.
Retargeted from main to dev, so dev's locale redirect and native authReady gate had to land alongside the FX changes. proxy.ts: kept dev's locale block and Vary reasoning, carried over the /api/exchange-rate cache exemption. api-fetch.ts: authReady() now runs only when includeAuth is true. A public rate read sends no token either way, so making it queue behind auth hydration on a native cold start would delay it for nothing.
The three migrated call sites are only safe if twMerge wins their size and animation overrides instead of emitting both. Asserting the resolved class string pins that more precisely than a screenshot of a 10px dot.
String.length is UTF-16 code units. Getting a true byte count means running the whole frame through a TextEncoder inside an error path, and only the magnitude matters for telling a truncated frame from a malformed one — so relabel rather than pay for the encode.
Five findings from /code-review on the badge lifecycle. Opening the drawer is not the same as reading the reply. When the Crisp bundle fails to load, this same component shows the email fallback instead — the badge used to clear anyway and bury a reply nobody saw. The web path now waits for CRISP_READY. The native path has no such signal, so it clears right after openMessenger() instead. A reply arriving while the drawer is open — the normal case in a live conversation — used to light the badge with nothing new behind it and leave it lit until the user opened support again. Clearing now also fires on the closing edge. Concurrent refreshes could resurrect a cleared badge: tapping a push fires a foreground refetch (count 1), the deep link then clears and refetches (count 0), and if the first response lands last it wins. With no polling nothing corrected it. Responses now carry a request id and stale ones are dropped. Guests reach this drawer through claim and pay links, and were sending an unauthenticated mark-read on every open. Gated on a resolved userId. The nav badge announced nothing: aria-label on a bare span is ignored by assistive tech and is an aria-prohibited-attr violation. It now carries role="status" with a translated label. es-AR has no navigation block at all and falls back, so only the three locales that do were touched.
feat(support): unread badge on the Support nav icon
refactor(fx): consume shared backend rate policy
fix(websocket): stop shipping raw frames to Sentry on a parse error
…into-dev-20260807
…o-dev-20260807 chore: back-merge main → dev (pre-release 2026-08-07)
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughChangesFX rate flow
Support unread state
API contracts and diagnostics
Release and repository updates
Estimated code review effort: 5 (Critical) | ~90 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ExchangeRateHook
participant ExchangeRateRoute
participant FxApi
Client->>ExchangeRateHook: request exchange rate
ExchangeRateHook->>FxApi: GET /fx/rate
FxApi-->>ExchangeRateHook: rate or structured error
ExchangeRateHook-->>Client: rate or cleared conversion state
Client->>ExchangeRateRoute: request compatibility route
ExchangeRateRoute->>FxApi: fetch validated rate
FxApi-->>ExchangeRateRoute: status and rate data
ExchangeRateRoute-->>Client: cache or no-store response
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
| // TextEncoder inside an error path, and only the magnitude | ||
| // matters here. | ||
| const size = typeof event.data === 'string' ? event.data.length : 'non-string' | ||
| console.error('Error parsing WebSocket message:', error, `(frame length: ${size})`) |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/hooks/__tests__/useSupportUnread.test.ts (1)
16-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd an out-of-order response regression test.
The suite does not verify
latestRequestId. Create two controlled requests. Resolve the newer request with{ count: 0 }, then resolve the older request with{ count: 1 }. Assert that the hook remainsfalse.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/__tests__/useSupportUnread.test.ts` around lines 16 - 72, Add a regression test for latestRequestId in the useSupportUnread suite using two controlled unread-count requests. Resolve the newer request with count 0 before resolving the older request with count 1, then assert the hook remains false so stale responses cannot update the state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/Profile/components/ProfileMenuItem.tsx`:
- Line 59: Use one semantic accessibility contract for labelled indicator dots:
in src/components/Profile/components/ProfileMenuItem.tsx lines 59-59, add an
appropriate role with a localized accessible name or mark the visual dot
decorative; in src/components/Home/HomeCarouselCTA/CarouselCTA.tsx lines 84-84,
place the claimable label on a role-bearing wrapper or IndicatorDot and hide the
other element; in
src/components/Global/IndicatorDot/__tests__/IndicatorDot.test.tsx lines 32-37,
assert the intended role/name or hidden state and cover ProfileMenuItem
integration if required.
---
Nitpick comments:
In `@src/hooks/__tests__/useSupportUnread.test.ts`:
- Around line 16-72: Add a regression test for latestRequestId in the
useSupportUnread suite using two controlled unread-count requests. Resolve the
newer request with count 0 before resolving the older request with count 1, then
assert the hook remains false so stale responses cannot update the state.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 564cd269-dfb5-4ce7-89dd-67d84ced437a
⛔ Files ignored due to path filters (1)
src/types/api.generated.tsis excluded by!**/*.generated.*
📒 Files selected for processing (41)
.github/workflows/capgo-deploy-ios.yml.github/workflows/capgo-deploy.yml.github/workflows/content-publish-automerge.ymldocs/api-types.mdnext.config.jssrc/__tests__/proxy.test.tssrc/app/(mobile-ui)/add-money/[country]/bank/page.tsxsrc/app/(mobile-ui)/layout.tsxsrc/app/api/exchange-rate/__tests__/route.test.tssrc/app/api/exchange-rate/route.tssrc/app/m/[slug]/MerchantLandingPage.tsxsrc/components/Global/IndicatorDot/__tests__/IndicatorDot.test.tsxsrc/components/Global/IndicatorDot/index.tsxsrc/components/Global/SupportDeepLink/index.tsxsrc/components/Global/SupportDrawer/__tests__/SupportDrawer.test.tsxsrc/components/Global/SupportDrawer/index.tsxsrc/components/Global/WalletNavigation/index.tsxsrc/components/Home/HomeCarouselCTA/CarouselCTA.tsxsrc/components/Profile/components/ProfileMenuItem.tsxsrc/components/TransactionDetails/TransactionCard.tsxsrc/contentsrc/hooks/__tests__/useExchangeRate.test.tsxsrc/hooks/__tests__/useSupportUnread.test.tssrc/hooks/useExchangeRate.tssrc/hooks/useSupportUnread.tssrc/i18n/app/messages/en.jsonsrc/i18n/app/messages/es-419.jsonsrc/i18n/app/messages/pt-BR.jsonsrc/proxy.tssrc/services/__tests__/websocket-parse-error-pii.test.tssrc/services/notifications.tssrc/services/websocket.tssrc/types/api.openapi.jsonsrc/utils/__tests__/api-fetch.test.tssrc/utils/__tests__/demo-api.test.tssrc/utils/__tests__/fx.utils.test.tssrc/utils/__tests__/sentry.utils.test.tssrc/utils/api-fetch.tssrc/utils/demo-api.tssrc/utils/fx.utils.tssrc/utils/sentry.utils.ts
💤 Files with no reviewable changes (1)
- next.config.js
| <div className="h-2.5 w-2.5 rounded-full bg-primary-1" /> | ||
| </div> | ||
| )} | ||
| {highlight && <IndicatorDot className="animate-pulse" aria-label="highlight-indicator" />} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use one semantic accessibility contract for labelled dots.
The carousel and profile call sites preserve aria-label but do not provide a semantic role. The test checks only the attribute. A generic span or div does not expose the intended status to assistive technology.
src/components/Profile/components/ProfileMenuItem.tsx#L59-L59: add an appropriate role and localized accessible name, or mark the visual dot as decorative.src/components/Home/HomeCarouselCTA/CarouselCTA.tsx#L84-L84: move the claimable label to a role-bearing wrapper orIndicatorDot, and hide the other element.src/components/Global/IndicatorDot/__tests__/IndicatorDot.test.tsx#L32-L37: assert the intended role/name or hidden state, and coverProfileMenuItemif integration behavior is required.
📍 Affects 3 files
src/components/Profile/components/ProfileMenuItem.tsx#L59-L59(this comment)src/components/Home/HomeCarouselCTA/CarouselCTA.tsx#L84-L84src/components/Global/IndicatorDot/__tests__/IndicatorDot.test.tsx#L32-L37
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Profile/components/ProfileMenuItem.tsx` at line 59, Use one
semantic accessibility contract for labelled indicator dots: in
src/components/Profile/components/ProfileMenuItem.tsx lines 59-59, add an
appropriate role with a localized accessible name or mark the visual dot
decorative; in src/components/Home/HomeCarouselCTA/CarouselCTA.tsx lines 84-84,
place the claimable label on a role-bearing wrapper or IndicatorDot and hide the
other element; in
src/components/Global/IndicatorDot/__tests__/IndicatorDot.test.tsx lines 32-37,
assert the intended role/name or hidden state and cover ProfileMenuItem
integration if required.
…w skip reason (#2645) Without this the Explorer shows a balance-held user as 'due now', the exact opposite of the truth in the tool an operator uses to verify api#1309.
Payload
FX — consume the shared backend rate policy (#2607)
Replaces the local conversion implementation with
GET /fx/rate, so the wallet and Peanut Split share one contract.503added to the Sentry skip list for/fx/rate— peanut-api already reports the upstream cause; reporting it client-side multiplies one incident by every mounted hook and its retries./fx/rategets a real demo-api handler; without one a failed passthrough answered200 {}.Support — unread badge (#2639) — FE half of peanut-api-ts #1303. Ships together.
Observability (#2637) — stop shipping raw websocket frames to Sentry on a parse error.
CI (#2636) — workflow consolidation. No runtime effect.
Order — merge this AFTER the backend
peanut-api-ts #1308 must be deployed first. This PR drops
api.frankfurter.appfrom CSP, so if the FE ships before/fx/*is live on prod, every rate surface blanks and the old path cannot be hot-restored.Verification
FX verified against staging (which tracks
dev): 43/43 on the shadow-compare, every Manteca pair resolving from Manteca rather than the reference feed.Summary by CodeRabbit
New Features
Bug Fixes