Skip to content

fix(perps): deduplicate error view and analytics across connection providers#29655

Open
michalconsensys wants to merge 18 commits into
mainfrom
fix/perps-single-error-view
Open

fix(perps): deduplicate error view and analytics across connection providers#29655
michalconsensys wants to merge 18 commits into
mainfrom
fix/perps-single-error-view

Conversation

@michalconsensys
Copy link
Copy Markdown
Contributor

@michalconsensys michalconsensys commented May 4, 2026

Description

Multiple PerpsConnectionProvider instances (main stack, modal stack, close-position bottom-sheet stack) each independently rendered their own PerpsConnectionErrorView and fired their own PERPS_SCREEN_VIEWED analytics event. This caused duplicate error screens to stack on top of each other and duplicate analytics events to fire on every connection failure.

This PR introduces a centralized PerpsGlobalErrorGate component that wraps the entire PerpsScreenStack. When the connection manager reports an error, the gate renders a single PerpsConnectionErrorView and suppresses the error view in all nested PerpsConnectionProvider instances via the existing suppressErrorView prop.

Key changes:

  • New PerpsGlobalErrorGate component — polls PerpsConnectionManager.getConnectionState() and renders a single error view with retry logic when an error is present.
  • Debounced analyticsPERPS_SCREEN_VIEWED events are debounced (1-second window) to suppress rapid error→null→error flap cycles from emitting duplicate events.
  • Moved analytics out of PerpsConnectionErrorView — the removed useEffect in PerpsConnectionErrorView is now handled centrally by the gate.
  • Sentry breadcrumbs — added breadcrumbs for error view display and retry failures.
  • Nested providers use suppressErrorViewPerpsModalStack and PerpsClosePositionBottomSheetStack connection providers now pass suppressErrorView to defer error display to the gate.
  • Route cleanup — removed duplicate screen registrations and comment-only lines in PerpsScreenStack.
  • Comprehensive test coverage — 700+ lines of tests covering error rendering, retry logic, analytics debouncing, flap suppression, Sentry breadcrumbs, polling, and unmount cleanup.

Changelog

CHANGELOG entry: null

Related issues

Fixes: https://consensyssoftware.atlassian.net/browse/TAT-2988

Manual testing steps

Feature: Perps connection error handling

  Scenario: user sees a single error view when connection fails
    Given the user navigates to the Perps screen stack
    And the WebSocket connection to the perps backend fails

    When the error state is detected
    Then a single connection error view is displayed (not duplicated across providers)
    And the user can tap "Retry" to attempt reconnection

  Scenario: user retries and connection recovers
    Given the connection error view is displayed
    And retry attempt count shows 0

    When user taps the "Retry" button
    And the reconnection succeeds
    Then the error view disappears
    And the normal Perps screens are rendered

  Scenario: analytics are not duplicated on rapid error flaps
    Given the user is on the Perps screen stack
    And the connection rapidly toggles between error and connected states

    When the error clears within 1 second of appearing
    Then no PERPS_SCREEN_VIEWED analytics event is fired for that transient error

Screenshots/Recordings

N/A — no visual changes; this is a structural refactor of error handling logic.

Before

N/A

After

N/A

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Moderate risk because it changes how connection errors are surfaced across the entire Perps navigation stack and alters analytics/breadcrumb emission timing via polling and debouncing, which could affect user recovery flows and event accuracy.

Overview
Deduplicates Perps connection error handling across nested stacks. Introduces a new PerpsGlobalErrorGate that wraps PerpsScreenStack and renders a single PerpsConnectionErrorView whenever PerpsConnectionManager reports an error, with centralized retry + retry-attempt tracking.

Moves and debounces error analytics/breadcrumbs. Removes the PERPS_SCREEN_VIEWED tracking side effect from PerpsConnectionErrorView and instead emits it (plus a Sentry breadcrumb) from the gate with a 1s debounce to suppress rapid error flaps; retry failures also add a breadcrumb.

Suppresses provider-level error screens and breadcrumbs. All Perps PerpsConnectionProvider usages in the stack/modals now pass suppressErrorView, and provider breadcrumb emission is skipped when suppressed to avoid duplicates.

Adds comprehensive unit tests for the gate covering render/clear behavior, retry logic, polling cleanup, and analytics/breadcrumb deduping/debouncing.

Reviewed by Cursor Bugbot for commit c90bbdf. Bugbot is set up for automated code reviews on this repo. Configure here.

Three PerpsConnectionProvider instances (PerpsScreenStack, PerpsModalStack,
PerpsClosePositionBottomSheetStack) all rendered their own error view on
connection failure, stacking identical screens and firing PERPS_SCREEN_VIEWED
analytics 3x.

Introduce PerpsGlobalErrorGate — a single centralized error gate mounted at
the root of PerpsScreenStack that polls PerpsConnectionManager and renders
one PerpsConnectionErrorView when an error is set. All three route-level
providers now pass suppressErrorView so they never render their own error UI.
Analytics are debounced (1s) to suppress rapid error→null→error flap cycles.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbotv2 metamaskbotv2 Bot added the team-perps Perps team label May 4, 2026
Cover four previously untested code paths:
- isConnecting forwarded as isRetrying to the error view
- Polling picks up isConnecting changes
- Sentry breadcrumb emitted on retry failure
- Gate + suppressErrorView provider integration (error blocks
  children, no-error renders all children)
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.77778% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.97%. Comparing base (8437791) to head (371e4f0).
⚠️ Report is 103 commits behind head on main.

Files with missing lines Patch % Lines
...ents/PerpsGlobalErrorGate/PerpsGlobalErrorGate.tsx 80.32% 5 Missing and 7 partials ⚠️
app/components/UI/Perps/routes/index.tsx 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #29655      +/-   ##
==========================================
+ Coverage   81.86%   81.97%   +0.10%     
==========================================
  Files        5255     5294      +39     
  Lines      138980   140265    +1285     
  Branches    31518    31887     +369     
==========================================
+ Hits       113774   114979    +1205     
- Misses      17465    17471       +6     
- Partials     7741     7815      +74     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ionErrorView

PerpsGlobalErrorGate already emits a debounced PERPS_SCREEN_VIEWED event
when a connection error occurs. The useEffect in PerpsConnectionErrorView
was firing the same event immediately on mount, producing 2x analytics
per error occurrence. Remove it so analytics fires exactly once via the
gate's debounced path.
@michalconsensys michalconsensys marked this pull request as ready for review May 6, 2026 09:25
@michalconsensys michalconsensys requested a review from a team as a code owner May 6, 2026 09:25
Comment thread app/components/UI/Perps/routes/index.tsx
Comment thread app/components/UI/Perps/components/PerpsGlobalErrorGate/PerpsGlobalErrorGate.tsx Outdated
…ount point

PerpsModalStack is mounted independently in MainNavigator.js as its own
screen, separate from PerpsScreenStack. Adding suppressErrorView without
wrapping it in PerpsGlobalErrorGate caused a regression where no error UI
or retry mechanism would be displayed when a connection error occurs while
a Perps modal is accessed via the MainNavigator route.
Comment thread app/components/UI/Perps/routes/index.tsx Outdated
PerpsGlobalErrorGate was mounted in both PerpsModalStack and
PerpsScreenStack. Since both are registered as separate screens in
MainNavigator, opening a perps modal while the perps screen is active
results in two independent gate instances with duplicate polling,
error views, and analytics events. Remove the gate from
PerpsModalStack so only PerpsScreenStack owns the single error gate.
The closure in handleRetry captured the stale pre-increment value of
retryAttempts, causing the Sentry breadcrumb to always report a count
one behind the actual attempt number. Compute the incremented value
into a local variable so both setState and the breadcrumb use the
correct value.
…orGate

Lower debounce window so error analytics fire sooner while still
suppressing rapid error→null→error flap cycles.
Comment thread app/components/UI/Perps/routes/index.tsx
Three routes were registered twice in the same Stack.Navigator with
functionally identical options, causing React Navigation warnings and
dead code. Keep the first set (using shared constants) and remove the
redundant inline duplicates.
@abretonc7s
Copy link
Copy Markdown
Contributor

Automated Review — PR #29655

BETA — Automated review from the farmslot pipeline.

Recommendation REQUEST_CHANGES
Runner codex / gpt-5.5
Tier standard
Cost N/A (1.2M tokens)
Recipe 1/4 steps PASS

Summary

REQUEST_CHANGES. Live validation fails when entering Perps: React Navigation throws because PerpsScreenStack registers duplicate screen names, including PerpsClosePositionModals. This blocks the app before the PR's stated connection-error behavior can be validated.

The PR also does not reference a Jira ticket or linked issue, so there is no external acceptance-criteria source. I am treating the PR body as review claims, not as ticket ACs.

Full review details

Review Claims

# Claim Status Evidence
1 The Perps connection error screen should render once across the Perps screen, modal, and close-position modal provider stacks. BLOCKED live, PASS unit Live Perps navigation crashes before this can be exercised; focused unit tests cover isolated gate behavior
2 Connection-error screen-view analytics should not be duplicated across those provider stacks. BLOCKED live, PASS unit Live Perps navigation crashes before this can be exercised; focused unit tests cover isolated analytics behavior

Prior Reviews

No prior CHANGES_REQUESTED reviews.

Live Validation

  • CDP/app state: available on mm-1.
  • Wallet state: unlocked before rerun.
  • Action: attempted PerpsHomeView navigation.
  • Result: FAIL.
  • App error: A navigator cannot contain multiple 'Screen' components with the same name (found duplicate screen named 'PerpsClosePositionModals').
  • Evidence: .task/review/29655-0507-172028/artifacts/live-validation-rerun.md and .task/review/29655-0507-172028/artifacts/metro-rerun.log.
  • Screenshots/video: not used.

Code Quality

  • Pattern adherence: centralizing the visible error screen is a reasonable shape, but the route stack now contains duplicate screen registrations.
  • Complexity: the gate is scoped, but the route changes introduce a runtime navigation failure.
  • Type safety: no new type issue seen in changed files.
  • Error handling: retry failure breadcrumb is reasonable.
  • PR hygiene: missing Jira/reference issue prevents validation against external acceptance criteria.

Fix Quality

  • Best approach: centralized error rendering is a good approach, but the current route implementation is not shippable because it crashes the Perps stack.
  • Would not ship: duplicate Stack.Screen registrations in PerpsScreenStack.
  • Test quality: focused gate tests pass, but there is no route/render test that catches duplicate navigator screen names.
  • Brittleness: route-level duplication is easy to miss in a large stack; add a focused route render or static assertion if practical.

Correctness

  • Diff vs stated goal: isolated gate logic aligns with the stated goal, but the route stack regression blocks Perps UI entry.
  • Edge cases: isolated error, clear, retry, analytics, and cleanup cases have unit coverage.
  • Race conditions: analytics debounce may not match the PR body's stated 1-second window.
  • Backward compatibility: broken for entering Perps on the validated iOS runtime.

Static Analysis

  • lint:tsc: FAIL — two existing-looking errors outside the changed files:
    • app/controllers/perps/PerpsController.ts(1920,11): Unused '@ts-expect-error' directive.
    • app/reducers/rewards/index.ts(571,7): Type instantiation is excessively deep and possibly infinite.
  • Tests: PASS — 18/18 in PerpsGlobalErrorGate.test.tsx

Architecture & Domain

The gate is architecturally sound, but PerpsScreenStack must not register duplicate screen names. React Navigation rejects this at runtime, so the centralized gate cannot safely wrap the stack until the duplicate screen declarations are removed.

Risk Assessment

  • HIGH — live validation shows entering Perps crashes into the root error boundary.

Recommended Action

REQUEST_CHANGES

Remove the duplicate Stack.Screen registrations in PerpsScreenStack, add the missing Jira/issue reference, rerun Perps navigation, and then rerun the connection-error validation.

Line comments (2 comments: 1 must_fix, 1 suggestion)
  • [must_fix] app/components/UI/Perps/routes/index.tsx:441: Live validation fails when entering Perps because this stack registers duplicate screen names. Routes.PERPS.MODALS.CLOSE_POSITION_MODALS is already registered earlier in the same navigator, and Routes.PERPS.MODALS.ROOT / Routes.CONFIRMATION_PAY_WITH_MODAL are duplicated as well. React Navigation throws A navigator cannot contain multiple 'Screen' components with the same name, sending the app to the root error boundary. Please remove the duplicate registrations and rerun Perps navigation.
  • [suggestion] app/components/UI/Perps/components/PerpsGlobalErrorGate/PerpsGlobalErrorGate.tsx:14: The PR body says rapid error flaps are debounced with a 1-second window, but this constant is 150 ms. Please either restore the 1-second window or update the PR description/tests if 150 ms is the intended behavior.
Recipe (1/4 steps PASS)
{
  "title": "PR 29655 runtime validation - Perps stack enters without navigator crash",
  "schema_version": 1,
  "pr": 29655,
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "entry": "ac7-navigate-perps-home",
      "nodes": {
        "ac7-navigate-perps-home": {
          "action": "navigate",
          "target": "PerpsHomeView",
          "next": "ac7-assert-perps-route"
        },
        "ac7-assert-perps-route": {
          "action": "wait_for",
          "route": "PerpsMarketListView",
          "timeout_ms": 8000,
          "next": "ac7-assert-no-duplicate-screen-error"
        },
        "ac7-assert-no-duplicate-screen-error": {
          "action": "log_watch",
          "window_seconds": 5,
          "must_not_appear": [
            "A navigator cannot contain multiple 'Screen' components with the same name",
            "found duplicate screen named 'PerpsClosePositionModals'"
          ],
          "next": "done"
        },
        "done": {
          "action": "end",
          "status": "pass"
        }
      }
    }
  }
}

No video evidence recorded.

Copy link
Copy Markdown
Contributor

@abretonc7s abretonc7s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review — see comment above for full details.

import { ensureError } from '../../../../../util/errorUtils';
import { isE2E } from '../../../../../util/test/utils';

const ANALYTICS_DEBOUNCE_MS = 150;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must fix — The PR body says rapid error flaps are debounced with a 1-second window, but this constant is 150 ms. That means an error that clears after 200 ms and reappears before 1 second can still emit two PERPS_SCREEN_VIEWED events, which violates the stated behavior. Please either restore the 1-second window or update the PR/tests/acceptance criteria if 150 ms is the intended product behavior.


return (
<PerpsConnectionProvider isFullScreen>
<PerpsConnectionProvider isFullScreen suppressErrorView>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion — Because PerpsConnectionProvider still records its PerpsConnectionErrorView shown Sentry breadcrumb whenever connectionState.error changes, passing suppressErrorView here suppresses the UI but not the provider breadcrumb. With the screen, modal, and close-position providers mounted, that can still duplicate and mislabel Sentry breadcrumbs even though the global gate owns rendering. Consider moving that breadcrumb to PerpsGlobalErrorGate or guarding it with !suppressErrorView.

…rendered

Guard the "PerpsConnectionErrorView shown" breadcrumb with
!suppressErrorView so that provider instances mounted with
suppressErrorView no longer emit misleading breadcrumbs when the
error UI is not actually displayed.
The provider's breadcrumb is suppressed when suppressErrorView is true,
but the gate never emitted a replacement. Add an addBreadcrumb call
inside the debounced analytics callback so the initial error-view
display produces Sentry context for crash-report debugging.
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 20e26e5. Configure here.

Comment thread app/components/UI/Perps/components/PerpsGlobalErrorGate/PerpsGlobalErrorGate.tsx Outdated
@michalconsensys michalconsensys changed the title fix(perps): render PerpsConnectionErrorView once across all provider instances fix(perps): deduplicate error view and analytics across connection providers May 8, 2026
… criteria

The debounce window was 150ms but the requirement specifies a 1-second
window to suppress rapid error→null→error flap cycles. At 150ms, transient
errors clearing after ~200ms could still emit duplicate PERPS_SCREEN_VIEWED
events.
@michalconsensys
Copy link
Copy Markdown
Contributor Author

@abretonc7s that is now updated

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePerps, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: @PerformancePreps
  • Risk Level: medium
  • AI Confidence: 88%
click to see 🤖 AI reasoning details

E2E Test Selection:
The changes are scoped entirely to the Perps feature's error handling and connection management architecture:

  1. PerpsGlobalErrorGate (new component): A centralized error gate that wraps the entire PerpsScreenStack. It polls PerpsConnectionManager every 100ms for connection state and shows a single error view when connection fails. Critically, it uses isE2E flag to skip polling in E2E tests, which means E2E test behavior is explicitly considered.

  2. routes/index.tsx: The PerpsScreenStack now wraps all content in <PerpsGlobalErrorGate>, and both PerpsModalStack and PerpsClosePositionBottomSheetStack now use suppressErrorView on PerpsConnectionProvider. This is a structural routing change that could affect how Perps screens render and navigate.

  3. PerpsConnectionProvider.tsx: Minor fix to also suppress breadcrumb logging when suppressErrorView is true.

  4. PerpsConnectionErrorView.tsx: Analytics tracking removed from this component (moved to PerpsGlobalErrorGate).

Tag Selection Rationale:

  • SmokePerps: Directly affected - the Perps routing structure and error handling have changed. The Add Funds flow and balance verification tests need to validate the new error gate doesn't interfere with normal operation.
  • SmokeWalletPlatform: Per tag description, Perps is a section inside the Trending tab. Changes to Perps views (including routing/error handling) affect Trending. The Trending tab integration needs validation.
  • SmokeConfirmations: Per SmokePerps tag description, "Add Funds deposits are on-chain transactions" - when selecting SmokePerps, also select SmokeConfirmations.

The risk is medium because while the changes are well-scoped to Perps, the routing structure change (new wrapper component around the entire PerpsScreenStack) could potentially affect navigation flows if the error gate has any unexpected behavior.

Performance Test Selection:
The routing structure change wraps the entire PerpsScreenStack in a new PerpsGlobalErrorGate component that polls PerpsConnectionManager every 100ms. While the isE2E flag bypasses polling in test environments, the 100ms polling interval in production could have a minor performance impact on Perps screen loading and rendering. The @PerformancePreps tag covers perps market loading, position management, add funds flow, and order execution - all of which now go through the new error gate wrapper.

View GitHub Actions results

@sonarqubecloud
Copy link
Copy Markdown

@abretonc7s
Copy link
Copy Markdown
Contributor

Automated Review — PR #29655

BETA — Automated review from the farmslot pipeline.

Recommendation REQUEST_CHANGES
Runner codex / gpt-5.5
Tier standard
Cost N/A (N/A tokens)
Recipe 1/4 steps PASS

Summary

Line comments (1 comments: 1 must_fix)
  • [must_fix] app/components/UI/Perps/routes/index.tsx:137: I think this should be wrapped for the direct MainNavigator route. PerpsModalStack is still registered at top level for Routes.PERPS.MODALS.ROOT, so this suppressErrorView is only safe when the stack is mounted under PerpsScreenStack's PerpsGlobalErrorGate. A small concrete fix would be to export a PerpsModalStackWithErrorGate wrapper, use that from MainNavigator, and keep the plain PerpsModalStack for the nested route that is already under the gate.
Recipe (1/4 steps PASS)
{
  "title": "PR 29655 runtime validation - Perps stack enters without navigator crash",
  "schema_version": 1,
  "pr": 29655,
  "validate": {
    "workflow": {
      "pre_conditions": [
        "wallet.unlocked",
        "perps.feature_enabled"
      ],
      "entry": "ac7-navigate-perps-home",
      "nodes": {
        "ac7-navigate-perps-home": {
          "action": "navigate",
          "target": "PerpsHomeView",
          "next": "ac7-assert-perps-route"
        },
        "ac7-assert-perps-route": {
          "action": "wait_for",
          "route": "PerpsMarketListView",
          "timeout_ms": 8000,
          "next": "ac7-assert-no-duplicate-screen-error"
        },
        "ac7-assert-no-duplicate-screen-error": {
          "action": "log_watch",
          "window_seconds": 5,
          "must_not_appear": [
            "A navigator cannot contain multiple 'Screen' components with the same name",
            "found duplicate screen named 'PerpsClosePositionModals'"
          ],
          "next": "done"
        },
        "done": {
          "action": "end",
          "status": "pass"
        }
      }
    }
  }
}

review.mp4 (100K)

Copy link
Copy Markdown
Contributor

@abretonc7s abretonc7s left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review — see comment above for full details.


return (
<PerpsConnectionProvider isFullScreen>
<PerpsConnectionProvider isFullScreen suppressErrorView>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must fix — I think this should be wrapped for the direct MainNavigator route. PerpsModalStack is still registered at top level for Routes.PERPS.MODALS.ROOT, so this suppressErrorView is only safe when the stack is mounted under PerpsScreenStack's PerpsGlobalErrorGate. A small concrete fix would be to export a PerpsModalStackWithErrorGate wrapper, use that from MainNavigator, and keep the plain PerpsModalStack for the nested route that is already under the gate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants