feat(mobile/telemetry): collect real network type via connectivity_plus

- Add connectivity_plus: ^6.0.3 to pubspec.yaml
- DeviceInfoCollector._getNetworkType() calls Connectivity().checkConnectivity()
  and maps ConnectivityResult → wifi / mobile / ethernet / vpn / bluetooth / none / unknown
- networkType field in DeviceContext is now populated on every cold start
- All page_view and session events carry the accurate network type in device props

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-07 09:43:04 -08:00
parent 9b760c56ee
commit 9519e29b8f
2 changed files with 39 additions and 2 deletions

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../models/device_context.dart'; import '../models/device_context.dart';
@ -25,6 +26,7 @@ class DeviceInfoCollector {
final deviceInfo = DeviceInfoPlugin(); final deviceInfo = DeviceInfoPlugin();
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final networkType = await _getNetworkType();
DeviceContext result; DeviceContext result;
@ -49,7 +51,7 @@ class DeviceInfoCollector {
locale: Platform.localeName, locale: Platform.localeName,
timezone: DateTime.now().timeZoneName, timezone: DateTime.now().timeZoneName,
isDarkMode: mediaQuery.platformBrightness == Brightness.dark, isDarkMode: mediaQuery.platformBrightness == Brightness.dark,
networkType: 'unknown', // connectivity包 networkType: networkType,
collectedAt: DateTime.now(), collectedAt: DateTime.now(),
); );
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
@ -73,7 +75,7 @@ class DeviceInfoCollector {
locale: Platform.localeName, locale: Platform.localeName,
timezone: DateTime.now().timeZoneName, timezone: DateTime.now().timeZoneName,
isDarkMode: mediaQuery.platformBrightness == Brightness.dark, isDarkMode: mediaQuery.platformBrightness == Brightness.dark,
networkType: 'unknown', networkType: networkType,
collectedAt: DateTime.now(), collectedAt: DateTime.now(),
); );
} else { } else {
@ -84,6 +86,40 @@ class DeviceInfoCollector {
return result; return result;
} }
///
///
/// 'wifi' | 'mobile_2g' | 'mobile_3g' | 'mobile_4g' | 'mobile_5g' |
/// 'ethernet' | 'vpn' | 'bluetooth' | 'none' | 'unknown'
Future<String> _getNetworkType() async {
try {
final results = await Connectivity().checkConnectivity();
// checkConnectivity List<ConnectivityResult>
final result = results.isNotEmpty ? results.first : ConnectivityResult.none;
return _mapConnectivityResult(result);
} catch (e) {
return 'unknown';
}
}
String _mapConnectivityResult(ConnectivityResult result) {
switch (result) {
case ConnectivityResult.wifi:
return 'wifi';
case ConnectivityResult.mobile:
return 'mobile'; // 2G/3G/4G/5G
case ConnectivityResult.ethernet:
return 'ethernet';
case ConnectivityResult.vpn:
return 'vpn';
case ConnectivityResult.bluetooth:
return 'bluetooth';
case ConnectivityResult.none:
return 'none';
default:
return 'unknown';
}
}
/// ///
ScreenInfo _collectScreenInfo(MediaQueryData mediaQuery) { ScreenInfo _collectScreenInfo(MediaQueryData mediaQuery) {
final size = mediaQuery.size; final size = mediaQuery.size;

View File

@ -35,6 +35,7 @@ dependencies:
sign_in_with_apple: ^6.1.0 sign_in_with_apple: ^6.1.0
flutter_riverpod: ^2.5.1 flutter_riverpod: ^2.5.1
fpdart: ^1.1.0 fpdart: ^1.1.0
connectivity_plus: ^6.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: