mattermost-mobile/docs/network-connectivity-observation.md

24 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

User Preference

Low Connectivity Monitor (User Setting):

  • Accessible in: Settings → Advanced Settings → Experimental Features
  • Controls real-time performance monitoring and banner display
  • Enables early detection and network performance tracking
  • Independent from CollectNetworkMetrics
  • Stored in app-level database via GLOBAL_IDENTIFIERS.LOW_CONNECTIVITY_MONITOR
  • Default: Enabled (true)
  • Observable via observeLowConnectivityMonitor() from @queries/app/global

Implementation Details

Actions (app/actions/app/global.ts):

export const storeLowConnectivityMonitor = async (enabled: boolean) => {
  return storeGlobal(GLOBAL_IDENTIFIERS.LOW_CONNECTIVITY_MONITOR, enabled, false);
};

Queries (app/queries/app/global.ts):

export const observeLowConnectivityMonitor = () => {
  const query = queryGlobalValue(GLOBAL_IDENTIFIERS.LOW_CONNECTIVITY_MONITOR);
  if (!query) {
    return of$(true);  // Default to enabled
  }
  return query.observe().pipe(
    switchMap((result) => (result.length ? result[0].observe() : of$(true))),
    switchMap((v) => {
      if (typeof v === 'boolean') {
        return of$(v);
      }
      return of$(v?.value ?? true);
    }),
  );
};

Integration in ClientTracking

class ClientTracking {
  private lowConnectivityMonitorEnabled = true;

  constructor(apiClient: APIClientInterface) {
    this.apiClient = apiClient;
    
    // Subscribe to user preference changes
    observeLowConnectivityMonitor().subscribe((enabled) => {
      this.lowConnectivityMonitorEnabled = enabled;
    });
  }

  startNetworkPerformanceTracking(url: string): string | undefined {
    if (!this.lowConnectivityMonitorEnabled) {
      return undefined;
    }
    return NetworkPerformanceManager.startRequestTracking(this.apiClient.baseUrl, url);
  }

  completeNetworkPerformanceTracking(
    requestId: string | undefined,
    url: string,
    metrics: ClientResponseMetrics | undefined
  ) {
    if (!this.lowConnectivityMonitorEnabled || !requestId || !metrics) {
      return;
    }
    NetworkPerformanceManager.completeRequestTracking(this.apiClient.baseUrl, requestId, metrics);
  }

  cancelNetworkPerformanceTracking(requestId: string | undefined) {
    if (!this.lowConnectivityMonitorEnabled || !requestId) {
      return;
    }
    NetworkPerformanceManager.cancelRequestTracking(this.apiClient.baseUrl, requestId);
  }
}

Network metrics collection (existing - for telemetry):

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

Network Manager Configuration

Metrics Collection (app/managers/network_manager.ts):

const config = {
  sessionConfiguration: {
    timeoutIntervalForRequest: managedConfig?.timeout ? parseInt(managedConfig.timeout, 10) : this.DEFAULT_CONFIG.sessionConfiguration?.timeoutIntervalForRequest,
    timeoutIntervalForResource: managedConfig?.timeoutVPN ? parseInt(managedConfig.timeoutVPN, 10) : this.DEFAULT_CONFIG.sessionConfiguration?.timeoutIntervalForResource,
    waitsForConnectivity: managedConfig?.useVPN === 'true',
    collectMetrics: true,  // Always enabled to support Low Connectivity Monitor
  },
  headers,
};

Note: collectMetrics is now always set to true to ensure network metrics are available for the Low Connectivity Monitor. The actual usage of these metrics is controlled dynamically by the user's Low Connectivity Monitor preference in ClientTracking.


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')