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

803 lines
24 KiB
Markdown

# Network Connectivity Observation System
## Table of Contents
- [Overview](#overview)
- [Architecture](#architecture)
- [System Components](#system-components)
- [Data Flow](#data-flow)
- [Technical Specifications](#technical-specifications)
- [Banner Display Logic](#banner-display-logic)
- [Performance Detection Algorithm](#performance-detection-algorithm)
- [Configuration](#configuration)
## 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
```mermaid
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**:
```typescript
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**:
```typescript
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**:
```typescript
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**:
```typescript
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
```mermaid
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
```mermaid
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
```typescript
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
```typescript
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
```typescript
// 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
```typescript
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)
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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:
```typescript
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
```typescript
// 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
```typescript
// 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`):
```typescript
export const storeLowConnectivityMonitor = async (enabled: boolean) => {
return storeGlobal(GLOBAL_IDENTIFIERS.LOW_CONNECTIVITY_MONITOR, enabled, false);
};
```
**Queries** (`app/queries/app/global.ts`):
```typescript
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
```typescript
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):
```typescript
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`):
```typescript
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
```typescript
// 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
```typescript
// 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
```typescript
try {
response = await request(url, options);
completeRequestTracking(requestId, response.metrics);
} catch (error) {
// Cancel tracking - no outcome recorded
cancelRequestTracking(requestId);
throw error;
}
```
### Missing Metrics
```typescript
completeRequestTracking(requestId, metrics) {
if (!metrics) {
// Silently skip - no outcome recorded
clearActiveRequest(requestId);
return;
}
// ... record outcome
}
```
### Subscription Cleanup
```typescript
// 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')