feat(kyc): watchdog + telemetry so a stuck Sumsub SDK can never be silent again#2443
Conversation
Prod Release Sprint 151 — KYC verdict rendering · Solana/Tron/Base withdrawals · profile fixes (2026-07-16)
…r late Card creation has been at zero since the #2407 release went live: the Sumsub WebSDK opens but nobody can finish. card_sumsub_opened is normal (137/day) while card_sumsub_completed went 49-85/day -> 0, kyc_submitted 27-76 -> 0 and kyc_approved 16-42 -> 0. Users sit on a spinner and give up (51 closes, zero completions today). Downstream that is a full card outage — no new verified users, and the 334 approved-but-cardless users are routed back through the same SDK for Rain's extra docs, so card_apply terms-required collapsed from 27-42% of outcomes to 1.4% and 0 cards were created in 15h+ (baseline 24-43). Root cause: #2407 replaced the StartVerificationView click gate with auto-init on `visible`. The init effect bails on `!sdkContainerRef.current`, but Modal is a headlessui <Transition>/<Dialog> that renders through a Portal — the portal target is created in the portal's own effect, so the container mounts a commit AFTER `visible` flips true. A ref is not reactive and was not in the dep array, so the effect read null on its only run and never re-ran: the SDK was never launched. The old click gate hid this by guaranteeing the container was mounted long before init. It only reproduces when the wrapper is already mounted (the real SumsubKycModals case) so `sdkLoaded` has settled before `visible` flips — mounting straight to visible lets the sdkLoaded flip re-run the effect. Fix: hold the container in state via a callback ref so attachment re-runs the init effect. Keeps #2407's intended no-interstitial UX rather than reverting. Test reproduces the portal's late mount and fails on the current prod code (launch called 0 times), passes with the fix.
Review pass on my own test: replace a tautological waitFor that did not actually guarantee sdkLoaded had settled (the premise the repro depends on), reset the mock's mounted flag so a close/re-open replays the late mount like the real portal, name the mock component so rules-of-hooks recognises it (it was adding 2 eslint errors), and stop leaking window.snsWebSdk.
…completes fix(kyc): launch Sumsub SDK when the modal portal mounts the container late
… only) #2440 was meant to fix the card outage but landed the regression test WITHOUT the code change: while proving the test failed against prod, a `git checkout origin/main -- SumsubKycWrapper.tsx` staged the reverted file, and the follow-up commit swept that staged revert in. Net effect: main got a test asserting behaviour that main does not have. Its unit job is red and the outage is still live. Re-applies the fix on top of main, unchanged from what #2440 intended: hold the SDK container in state via a callback ref so the init effect re-runs when the headlessui portal attaches the node a commit after `visible` flips. Without it the effect reads a null ref on its only run, never re-runs (a ref is not reactive and is not a dep), and the SDK is never launched — every user gets an infinite spinner (card_sumsub_opened normal, card_sumsub_completed 0). Verified the honest way this time: the test ALREADY on main fails against main's wrapper (launch called 0 times) and passes with this commit.
…llback-ref fix(kyc): actually apply the callback-ref fix (#2440 shipped the test only)
…lent again
The Jul-16/17 card outage ran 19h and was found by a customer in Dhaka, not by
us. The reason is structural, not bad luck: a WebSDK that never launches throws
nothing, requests nothing and logs nothing. Sentry cannot see code that does not
run, so the only trace was throughput quietly going to zero. This closes that
class of failure rather than the one instance of it.
Watchdog: if the modal is open and the SDK has not launched within 20s, show the
error UI instead of the infinite spinner. Deliberately keyed on `visible` ALONE —
every silent path (null container, token that never arrives, script that never
loads) ends with the init effect simply not running, so anything keyed on those
deps would also never run. Watching the one fact the user experiences ("I opened
it and nothing happened") catches the whole class, including causes we have not
thought of yet.
Telemetry: kyc_sdk_launched / kyc_sdk_launch_timeout / kyc_sdk_init_failed. The
timeout event carries hadAccessToken / sdkScriptLoaded / hadContainer, which are
the three facts that distinguish the silent stalls from each other. Makes
"opened but never launched" alertable instead of archaeology.
Also corrects the mechanism comments shipped in #2441: the one-commit delay comes
from headlessui's <Transition> promoting tree state inside an effect, NOT the
Portal (isolation-tested: dialog-only mounts the container synchronously;
transition-only does not). The fix was right, the explanation was not, and a
wrong comment misleads the next reader.
Tests: the watchdog fires and surfaces the error UI when the SDK never launches,
and does not fire once it has.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
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. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 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:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Code-analysis diffPainscore total: 6207.13 → 6207.44 (+0.31) 🆕 New findings (11)
✅ Resolved (11)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
Action item #9 from the outage post-mortem.
Why
The card outage ran 19 hours and was found by a customer in Dhaka, not by us. That wasn't bad luck — it's structural: a WebSDK that never launches throws nothing, requests nothing, and logs nothing. Sentry cannot see code that doesn't run. The only trace was throughput quietly going to zero, and nothing was watching throughput.
#2441 fixed the bug. This closes the class.
What
1. Watchdog — modal open + SDK not launched within 20s → show the error UI instead of an infinite spinner.
Keyed on
visiblealone, on purpose. Every silent path — null container, token that never arrives, script that never loads — ends with the init effect simply not running. Anything keyed on those deps would also never run. Watching the one fact the user actually experiences ("I opened it and nothing happened") catches the whole class, including causes we haven't thought of yet.2. Telemetry —
kyc_sdk_launched/kyc_sdk_launch_timeout/kyc_sdk_init_failed.The timeout event carries
hadAccessToken/sdkScriptLoaded/hadContainer— the three facts that tell the silent stalls apart. Turns "opened but never launched" into something alertable instead of archaeology.3. Corrects the mechanism comments shipped in #2441.
The delay comes from headlessui's
<Transition>promoting tree state inside an effect — not the Portal, which resolves synchronously. Isolation-tested:dialog-only(Portal, no Transition)transition-only(no Portal)transition+dialog(real Modal)The fix was right; my explanation was wrong. A wrong comment misleads the next reader, so it's corrected in both the component and the test.
Tests
kyc_sdk_launch_timeoutwithhadAccessToken: falsekyc_sdk_launchedis captured)Gates
eslint clean on changed files · typecheck clean · prettier clean · full suite: 4 pre-existing failures, byte-identical to clean
main(content-submodule/worktree env), none from this change.Risk
One component + consts. No API/contract change, no migration. Worst case the watchdog is too eager — 20s is generous for a slow script on a bad connection, and the failure mode is "user sees an actionable error" rather than "user stares at a spinner for 19 hours".