mattermost-mobile/docs/network-connectivity-observation.md
Rahim Rahman 597e03d7d8
feat(MM-65145): Network connectivity/performance observer (#9173)
* feat: implement floating banner system

- Add FloatingBanner component with gesture support and keyboard awareness
- Implement BannerManager singleton for banner lifecycle management
- Create floating banner screen with SafeAreaProvider integration
- Add comprehensive banner configuration types and positioning
- Update Banner component to use modern gesture handling
- Enhance BannerItem with improved typography and spacing (40px height)
- Add ConnectionBanner improvements with better sizing
- Remove ConnectionBanner from channel list (moved to floating system)
- Update screens constants (remove FLOATING_BANNER - handled as overlay)
- Add i18n support for limited network connection message

The system provides:
- Auto-hide functionality with customizable duration
- Position-aware rendering (top/bottom with keyboard adjustment)
- Tablet-specific offset handling
- Swipe-to-dismiss with configurable thresholds
- Custom component support alongside default banner items
- Comprehensive test coverage with device-specific scenarios

* docs: add floating banner system documentation and cleanup

- Add comprehensive floating-banner.md with architecture diagrams
- Remove incompatible connection_banner/index.ts file
- Update device.ts hooks for better keyboard handling
- Simplify screens/index.tsx floating banner registration
- Update test/setup.ts to remove deprecated keyboard mocks
- Clean up keyboard height logic and ESLint issues

The documentation covers:
- System architecture and component relationships
- API reference and usage patterns
- Performance considerations and best practices
- Integration points and troubleshooting guide
- Comprehensive testing strategy

All tests now pass with the updated setup.

* fix issue with translation file

* some self cleanup.

* renamed index.tsx => Banner.tsx

* creaete meaningful tests for Banner component and all the hooks.

* fix tests

* cleanup based on initial review by AI

* dismissible was set to true, changing to what was configured.

* making title and message optional

* feat(MM-65145): Network connectivity/performance observer

* add MONITOR_NETWORK_PERFORMANCE

* i18n stuff

* remove unused props

* Undo some AI changes that caught by another AI

* network connectivity observation md

* addressed some comments in PR

* more fixes based on PR review.

* added future enhancement

* dismissOverlay will be awaited
* delay dismissing overlay so we don't have to show a new one all the time

* make the banner stackable

* Fix issue with last banner dismissal delayed by 2s

* update floating-banner test

* put back an extra space

* add a todo to use namespace
* add comment on priority order for connectivity performance
* use static const vs magic number

* add a guard against adding performanceSubject when server has been removed
* fix failing tests

* clean-up based on review by @enahum

* fix failing test

* fix failiing tests

* rename confusing var

* update changes to types

* fixed issue with swipe not working on android

* performance and connectivity use the same id so that 2 banners won't appear

* fix issue w/ android not registering touch events behind the overlay

* fix failing test

* hideBanner now needs id.

* Connection status unknown added in en.json

* update the doc

* animate the banner moving up when bottom banner first appear.

* removed unused functions and update tests

* add useMemo and useCallback

* update jsdoc to say dismissable is default true

* fix failing test
2025-10-12 10:10:05 -06:00

21 KiB

Network Connectivity Observation System

Table of Contents

Overview

The Network Connectivity Observation System is a comprehensive monitoring solution that tracks network performance and connectivity status in real-time, providing contextual feedback to users through an intelligent banner system.

Key Features

  • Real-time Performance Monitoring: Tracks request latency to detect network degradation
  • Early Detection System: Identifies slow requests before completion (2s threshold)
  • Smart Banner Management: Context-aware banner display with anti-spam mechanisms
  • Multi-Server Support: Independent tracking per server URL
  • Lifecycle Management: Handles app state transitions (background/foreground)

Architecture

System Diagram

graph TB
    subgraph NCSM["NetworkConnectivitySubscriptionManager<br/>Manages 5 Subscriptions"]
        direction LR
        SUB1[1. AppState]
        SUB2[2. NetInfo]
        SUB3[3. Active Servers]
        SUB4[4. WebSocket State]
        SUB5[5. Performance State]
    end
    
    NPM["NetworkPerformanceManager<br/>• Request Tracking<br/>• Performance Detection<br/>• Observable Streams<br/>• Sliding Window 20 requests"]
    
    NCM["NetworkConnectivityManager<br/>• updateState()<br/>• updatePerformanceState()<br/>• Banner Priority Logic<br/>• Suppression Control"]
    
    BM[BannerManager<br/>• Show/Hide Banner<br/>• Auto-Hide<br/>• Cleanup]
    
    CB[ConnectionBanner<br/>FloatingBanner]
    
    NCSM -->|updates| NCM
    NCM -->|controls| BM
    BM -->|renders| CB
    
    style NCSM fill:#e1f5ff
    style NCM fill:#fff4e1
    style NPM fill:#f0e1ff
    style BM fill:#d4edda

System Components

1. NetworkPerformanceManager

Purpose: Monitors request performance and detects network degradation

Key Responsibilities:

  • Track active requests with unique IDs
  • Detect slow requests (≥2000ms) early via timers
  • Maintain sliding window of request outcomes (20 requests)
  • Calculate performance state based on slow request percentage
  • Emit performance state changes via RxJS observables

Public API:

class NetworkPerformanceManager {
  // Start tracking a request, returns unique ID
  startRequestTracking(serverUrl: string, url: string): string
  
  // Complete tracking with metrics
  completeRequestTracking(serverUrl: string, requestId: string, metrics: ClientResponseMetrics): void
  
  // Cancel tracking on failure
  cancelRequestTracking(serverUrl: string, requestId: string): void
  
  // Observe performance state changes
  observePerformanceState(serverUrl: string): Observable<NetworkPerformanceState>
  
  // Get current state
  getCurrentPerformanceState(serverUrl: string): NetworkPerformanceState
  
  // Cleanup
  removeServer(serverUrl: string): void
}

2. NetworkConnectivityManager

Purpose: Orchestrates banner display based on connectivity and performance states

Key Responsibilities:

  • Maintain current WebSocket, network, and performance state
  • Implement banner display priority logic
  • Handle suppression mechanisms
  • Manage first connection vs reconnection scenarios

Public API:

class NetworkConnectivityManager {
  // Initialize with server URL
  init(serverUrl: string | null): void
  
  // Update connection status
  setServerConnectionStatus(connected: boolean, serverUrl: string | null): void
  
  // Update WebSocket/network state
  updateState(
    websocketState: WebsocketConnectedState,
    netInfo: {isInternetReachable: boolean | null},
    appState: string
  ): void
  
  // Update performance state
  updatePerformanceState(performanceState: NetworkPerformanceState): void
  
  // Cleanup
  cleanup(): void
  shutdown(): void
}

3. NetworkConnectivitySubscriptionManager

Purpose: Coordinates all network-related subscriptions and server lifecycle

Key Responsibilities:

  • Subscribe to app state changes (active/background)
  • Subscribe to network info changes
  • Subscribe to active server changes
  • Subscribe to WebSocket state per server
  • Subscribe to performance state per server

Public API:

class NetworkConnectivitySubscriptionManager {
  // Initialize all subscriptions
  init(): void
  
  // Stop subscriptions (preserves AppState listener)
  stop(): void
  
  // Complete shutdown
  shutdown(): void
}

4. ClientTracking (Enhanced)

Purpose: Integrate performance tracking into network requests

Integration Points:

class ClientTracking {
  doFetchWithTracking = async (url: string, options: ClientOptions) => {
    // 1. Start performance tracking
    const performanceRequestId = this.startNetworkPerformanceTracking(url);
    
    try {
      // 2. Execute request
      response = await request(url, this.buildRequestOptions(options));
      
      // 3. Complete tracking with metrics
      this.completeNetworkPerformanceTracking(performanceRequestId, url, response.metrics);
    } catch (error) {
      // 4. Cancel tracking on error
      this.cancelNetworkPerformanceTracking(performanceRequestId);
      throw error;
    }
  }
}

Data Flow

1. Request Performance Tracking Flow

flowchart TD
    Start[HTTP Request Starts] --> CT[ClientTracking.doFetchWithTracking]
    CT -->|startNetworkPerformanceTracking| NPM[NetworkPerformanceManager]
    
    NPM -->|1. Generate unique requestId<br/>2. Set 2s timer<br/>3. Store in activeRequests| Decision{Request Outcome}
    
    Decision -->|Completes < 2s| Complete[Record Outcome<br/>wasEarly: false]
    Decision -->|Timer Triggers @ 2s| Early[Early Detection<br/>Record Outcome<br/>wasEarly: true]
    Decision -->|Fails| Cancel[Cancel Tracking<br/>No outcome recorded]
    
    Complete --> Update[Update Performance State]
    Early --> Update
    
    Update --> Steps[1. Add to outcomes window max 20<br/>2. Calculate slow %<br/>3. Determine state<br/>4. Emit if changed]
    
    style NPM fill:#f0e1ff
    style Update fill:#e1f5ff
    style Complete fill:#d4edda
    style Early fill:#fff3cd
    style Cancel fill:#f8d7da

2. Subscription and State Flow

sequenceDiagram
    participant Init as app/init/app.ts
    participant NCSM as NetworkConnectivitySubscriptionManager
    participant NCM as NetworkConnectivityManager
    participant WSM as WebsocketManager
    participant NPM as NetworkPerformanceManager
    participant DB as Active Servers DB
    
    Init->>NCM: 1. initialize()<br/>init(activeServerUrl)
    Init->>NCSM: 2. start()<br/>init()
    
    NCSM->>NCSM: AppState.addEventListener()
    NCSM->>NCSM: NetInfo.addEventListener()
    NCSM->>DB: subscribeActiveServers()
    
    DB->>NCSM: Active Server Changed<br/>findMostRecentServer() → serverUrl
    
    NCSM->>NCM: setServerConnectionStatus(true, serverUrl)
    NCSM->>WSM: observeWebsocketState(serverUrl)
    NCSM->>NPM: observePerformanceState(serverUrl)
    
    loop State Updates
        WSM-->>NCSM: Emit WebSocket State
        NCSM->>NCM: updateState(websocketState, netInfo, appState)
        NCM->>NCM: updateBanner()<br/>Priority Order:<br/>1. Disconnected<br/>2. Performance<br/>3. Connecting<br/>4. Reconnection<br/>5. Connected
        
        NPM-->>NCSM: Emit Performance State
        NCSM->>NCM: updatePerformanceState(performanceState)
        NCM->>NCM: updateBanner()
    end

Technical Specifications

Performance Detection Algorithm

Constants

const SLOW_REQUEST_THRESHOLD = 2000;                    // 2 seconds
const SLOW_REQUEST_PERCENTAGE_THRESHOLD = 0.7;          // 70%
const REQUEST_OUTCOME_WINDOW_SIZE = 20;                 // Last 20 requests
const MINIMUM_REQUESTS_FOR_INITIAL_DETECTION = 4;       // First detection
const MINIMUM_REQUESTS_FOR_SUBSEQUENT_DETECTION = 10;   // Later detections

State Calculation Logic

function calculatePerformanceStateFromOutcomes(
  outcomes: RequestOutcome[], 
  isInitialDetection: boolean
): NetworkPerformanceState {
  const minimumRequests = isInitialDetection 
    ? MINIMUM_REQUESTS_FOR_INITIAL_DETECTION    // 4 requests
    : MINIMUM_REQUESTS_FOR_SUBSEQUENT_DETECTION; // 10 requests

  // Not enough data yet for initial detection
  if (isInitialDetection && outcomes.length < minimumRequests) {
    return 'normal';
  }

  const slowRequestCount = outcomes.filter(o => o.isSlow).length;
  const slowPercentage = slowRequestCount / outcomes.length;

  // Slow if ≥70% of requests are slow
  return slowPercentage >= SLOW_REQUEST_PERCENTAGE_THRESHOLD 
    ? 'slow' 
    : 'normal';
}

Early Detection System

// When request tracking starts
startRequestTracking(serverUrl: string, url: string): string {
  const requestId = generateUniqueId();
  
  // Set timer for early detection
  const checkTimer = setTimeout(() => {
    // If still active after 2s, mark as slow
    if (activeRequests[requestId]) {
      recordRequestOutcome({
        timestamp: Date.now(),
        isSlow: true,
        wasEarlyDetection: true  // Flag for early detection
      });
      clearActiveRequest(requestId);
    }
  }, SLOW_REQUEST_THRESHOLD); // 2000ms
  
  activeRequests[requestId] = { id, url, startTime, checkTimer };
  return requestId;
}

// When request completes
completeRequestTracking(serverUrl: string, requestId: string, metrics: ClientResponseMetrics) {
  const wasEarlyDetected = !activeRequests[requestId]; // Already removed by timer
  
  clearActiveRequest(requestId);
  
  // Only record if not already recorded by early detection
  if (!wasEarlyDetected) {
    recordRequestOutcome({
      timestamp: Date.now(),
      isSlow: metrics.latency >= SLOW_REQUEST_THRESHOLD,
      wasEarlyDetection: false
    });
  }
}

Sliding Window Management

recordRequestOutcome(serverUrl: string, outcome: RequestOutcome) {
  if (!requestOutcomes[serverUrl]) {
    requestOutcomes[serverUrl] = [];
  }
  
  requestOutcomes[serverUrl].push(outcome);
  
  // Keep only last 20 requests
  if (requestOutcomes[serverUrl].length > REQUEST_OUTCOME_WINDOW_SIZE) {
    requestOutcomes[serverUrl] = requestOutcomes[serverUrl].slice(-REQUEST_OUTCOME_WINDOW_SIZE);
  }
  
  // Recalculate and emit new state
  const newState = calculatePerformanceStateFromOutcomes(
    requestOutcomes[serverUrl],
    isInitialDetection[serverUrl]
  );
  
  performanceSubject.next(newState);
}

Banner Display Logic

Priority Order (First Match Wins)

private updateBanner() {
  // 0. Hide banner if no server or app in background
  if (!currentServerUrl || appState === 'background') {
    BannerManager.hideBanner();
    return;
  }

  // 1. HIGHEST PRIORITY: Disconnected state
  if (shouldShowDisconnectedBanner(websocketState, isOnAppStart)) {
    showConnectivity(false);  // Persistent banner
    return;
  }

  // 2. Performance degradation
  if (shouldShowPerformanceBanner(performanceState, performanceSuppressed)) {
    showPerformance(10000);   // Auto-hide after 10s
    return;
  }

  // 3. Connecting state
  if (shouldShowConnectingBanner(websocketState, isOnAppStart)) {
    showConnectivity(false);  // Persistent banner
    return;
  }

  // 4. Reconnection success
  if (shouldShowReconnectionBanner(websocketState, previousState, isOnAppStart)) {
    showConnectivity(true, 3000);  // Auto-hide after 3s
    return;
  }

  // 5. LOWEST PRIORITY: Connected (hide banner)
  BannerManager.hideBanner();
}

Banner Decision Functions

Disconnected Banner

shouldShowDisconnectedBanner(
  websocketState: WebsocketConnectedState | null,
  isOnAppStart: boolean
): boolean {
  return websocketState === 'not_connected' && !isOnAppStart;
}
  • Shows: When disconnected after initial connection
  • Hides: On first connection (app start)
  • Duration: Persistent until state changes
  • Messages (internationalized):
    • "The server is not reachable" (connection_banner.not_reachable) - when internet is reachable but server is not
    • "Unable to connect to network" (connection_banner.not_connected) - when internet is not reachable
    • "Connection status unknown" (connection_banner.status_unknown) - when websocketState or netInfo is null

Performance Banner

shouldShowPerformanceBanner(
  performanceState: NetworkPerformanceState | null,
  performanceSuppressed: boolean
): boolean {
  return performanceState === 'slow' && !performanceSuppressed;
}
  • Shows: When performance degrades to 'slow'
  • Suppression: User can dismiss; suppressed until performance returns to 'normal'
  • Duration: Auto-hide after 10 seconds
  • Message: "Limited network connection" (connection_banner.limited_network_connection)

Connecting Banner

shouldShowConnectingBanner(
  websocketState: WebsocketConnectedState | null,
  isOnAppStart: boolean
): boolean {
  return websocketState === 'connecting' && !isOnAppStart;
}
  • Shows: When reconnecting after initial connection
  • Hides: During first connection
  • Duration: Persistent until state changes
  • Message: "Connecting..." (connection_banner.connecting)

Reconnection Banner

shouldShowReconnectionBanner(
  websocketState: WebsocketConnectedState | null,
  previousWebsocketState: WebsocketConnectedState | null,
  isOnAppStart: boolean
): boolean {
  return websocketState === 'connected' &&
         previousWebsocketState !== 'connected' && 
         !isOnAppStart;
}
  • Shows: When connection restored after being disconnected/connecting
  • Hides: On first connection
  • Duration: Auto-hide after 3 seconds
  • Message: "Connection restored" (connection_banner.connected)

Message Internationalization

All banner messages are internationalized using formatMessage() for proper localization:

private getConnectionMessage(): string {
  // Guard against uninitialized state
  if (!this.websocketState || !this.netInfo) {
    return this.intl.formatMessage({
      id: 'connection_banner.status_unknown', 
      defaultMessage: 'Connection status unknown'
    });
  }

  return getConnectionMessageText(
    this.websocketState, 
    this.netInfo.isInternetReachable, 
    this.intl.formatMessage
  );
}

Message Keys:

  • connection_banner.status_unknown - When websocketState or netInfo is null
  • connection_banner.connected - Connection restored
  • connection_banner.connecting - Connecting...
  • connection_banner.not_reachable - Server not reachable (but internet is)
  • connection_banner.not_connected - Unable to connect to network
  • connection_banner.limited_network_connection - Performance degraded

Suppression Mechanisms

Performance Suppression

// User dismisses performance banner
onDismiss: () => {
  this.performanceSuppressedUntilNormal = true;
}

// Reset when performance returns to normal
updatePerformanceState(performanceState: NetworkPerformanceState) {
  if (this.performanceSuppressedUntilNormal && performanceState === 'normal') {
    this.performanceSuppressedUntilNormal = false;
  }
}

First Connection Logic

// isOnAppStart flag prevents banners on initial connection
updateState(websocketState, netInfo, appState) {
  this.previousWebsocketState = this.websocketState;
  this.websocketState = websocketState;
  
  this.updateBanner();
  
  // Clear flag only after first successful connection
  if (websocketState === 'connected' && this.isOnAppStart) {
    this.isOnAppStart = false;
  }
}

Configuration

Config Flags

assets/base/config.json

{
  "CollectNetworkMetrics": false,
  "MonitorNetworkPerformance": true
}

CollectNetworkMetrics (Existing):

  • Controls telemetry/analytics collection
  • Used by PerformanceMetricsManager
  • Tracks request groups, parallel requests, etc.

MonitorNetworkPerformance (New):

  • Controls real-time performance monitoring
  • Used by NetworkPerformanceManager
  • Enables early detection and banner display
  • Independent from CollectNetworkMetrics

Integration in ClientTracking

// Network metrics collection (existing)
if (groupLabel && CollectNetworkMetrics) {
  this.incrementRequestCount(groupLabel);
  this.trackRequest(groupLabel, url, response.metrics);
  this.decrementRequestCount(groupLabel);
}

// Performance monitoring (new)
if (MonitorNetworkPerformance) {
  const requestId = NetworkPerformanceManager.startRequestTracking(baseUrl, url);
  
  try {
    response = await request(url, options);
    NetworkPerformanceManager.completeRequestTracking(baseUrl, requestId, response.metrics);
  } catch (error) {
    NetworkPerformanceManager.cancelRequestTracking(baseUrl, requestId);
  }
}

Lifecycle Management

App State Transitions

// AppState: active → background
handleAppStateChange(nextAppState: AppStateStatus) {
  if (nextAppState !== 'active') {
    // 1. Clear all active request timers
    clearAllActiveRequests();
    
    // 2. Reset performance state to normal
    performanceSubjects.forEach(subject => subject.next('normal'));
    
    // 3. Hide all banners
    BannerManager.hideBanner();
    
    // 4. Stop subscriptions (except AppState listener)
    NetworkConnectivitySubscriptionManager.stop();
  }
}

// AppState: background → active
handleAppStateChange(nextAppState: AppStateStatus) {
  if (nextAppState === 'active') {
    // Restart all subscriptions
    NetworkConnectivitySubscriptionManager.init();
  }
}

Server Lifecycle

// Server added/activated
handleActiveServersChange(servers: Server[]) {
  const activeServer = findMostRecentServer(servers);
  
  if (activeServer) {
    // 1. Set connection status
    NetworkConnectivityManager.setServerConnectionStatus(true, activeServer.url);
    
    // 2. Subscribe to WebSocket state
    websocketSubscription = WebsocketManager
      .observeWebsocketState(activeServer.url)
      .subscribe(state => {
        NetworkConnectivityManager.updateState(state, netInfo, appState);
      });
    
    // 3. Subscribe to performance state
    performanceSubscription = NetworkPerformanceManager
      .observePerformanceState(activeServer.url)
      .subscribe(state => {
        NetworkConnectivityManager.updatePerformanceState(state);
      });
  }
}

// Server removed/logout
terminateSession(serverUrl: string) {
  // 1. Clear connection status
  NetworkConnectivityManager.setServerConnectionStatus(false, serverUrl);
  
  // 2. Remove performance tracking
  NetworkPerformanceManager.removeServer(serverUrl);
  
  // 3. Stop subscriptions
  NetworkConnectivitySubscriptionManager.stop();
}

Performance Characteristics

Memory Usage

  • Request Outcomes: Max 20 per server (sliding window)
  • Active Requests: Cleared on completion or 2s timeout
  • Timers: One per active request, auto-cleared

Detection Speed

  • Early Detection: 2 seconds (timer-based)
  • Initial Detection: 4-8 slow requests (4 minimum)
  • Subsequent Detection: 10-20 requests (10 minimum with 70% threshold)

Banner Timing

  • Disconnected: Immediate, persistent
  • Connecting: Immediate, persistent
  • Performance: After threshold met, auto-hide 10s
  • Reconnected: Immediate, auto-hide 3s

Error Handling

Request Failure

try {
  response = await request(url, options);
  completeRequestTracking(requestId, response.metrics);
} catch (error) {
  // Cancel tracking - no outcome recorded
  cancelRequestTracking(requestId);
  throw error;
}

Missing Metrics

completeRequestTracking(requestId, metrics) {
  if (!metrics) {
    // Silently skip - no outcome recorded
    clearActiveRequest(requestId);
    return;
  }
  // ... record outcome
}

Subscription Cleanup

// Safe to call multiple times
stop() {
  websocketSubscription?.unsubscribe();
  performanceSubscription?.unsubscribe();
  netInfoSubscription?.();
  activeServersUnsubscriber?.unsubscribe();
  
  // Clear references
  websocketSubscription = undefined;
  performanceSubscription = undefined;
  // ...
}

Testing Strategy

Unit Tests

  • NetworkPerformanceManager: Request tracking, state calculation, sliding window
  • NetworkConnectivityManager: Banner decision functions, state transitions
  • NetworkConnectivitySubscriptionManager: Subscription lifecycle, server changes

Test Coverage

  • Early detection (timer triggers)
  • Request completion before/after threshold
  • State transitions (normal ↔ slow)
  • Banner priority order
  • Suppression mechanisms
  • App lifecycle (background/foreground)
  • Server lifecycle (add/remove)
  • Error scenarios (missing metrics, failed requests)

Future Enhancements

Potential Improvements

  1. Adaptive Thresholds: Adjust SLOW_REQUEST_THRESHOLD based on historical performance
  2. Network Type Awareness: Different thresholds for WiFi vs Cellular
  3. Exponential Backoff: For repeated performance banners
  4. Metrics Reporting: Send detection events to analytics
  5. User Preferences: Allow users to configure sensitivity

Known Limitations

  1. Single Server Focus: Only monitors most recently active server
  2. Fixed Thresholds: 2s threshold may not suit all use cases
  3. No Geographical Context: Doesn't account for server location
  4. Binary States: Only 'normal' vs 'slow' (no 'excellent' or 'poor')