feat: add device_info_plus, TagTemplates & UserInfo button
Flutter CI / analyze-and-test (push) Waiting to run Details

- Integrate device_info_plus for readable OS version and device model
- Add TagTemplates utility class with industry-standard custom tags:
  - businessContext, userSegmentation, forScreen, technicalContext
  - campaign (UTM), ecommerce (GA4), session, content
- Use snake_case naming convention for data warehouse compatibility
- Add Set UserInfo button to example app
- Update DeviceInfo to use Platform.operatingSystemVersion
This commit is contained in:
Max 2026-02-03 17:42:31 +08:00
parent 3277efd47c
commit ee9e6739b5
8 changed files with 404 additions and 4 deletions

View File

@ -228,10 +228,27 @@ class _DemoPageState extends State<DemoPage> {
url: 'https://example.com/demo', url: 'https://example.com/demo',
buttonId: 'demo_btn_01', buttonId: 'demo_btn_01',
), ),
customTags: const <String, dynamic>{'tenantId': 't1', 'feature': 'demo'}, customTags: TagTemplates.merge([
TagTemplates.businessContext(
tenantId: 't1',
appVersion: '1.0.0',
channel: 'internal_test',
),
TagTemplates.forScreen(screenName: 'DemoPage', featureModule: 'demo'),
]),
); );
} }
Future<void> _setUserInfo() async {
const userInfo = UserInfo(
userId: 10086,
userName: 'Test User',
account: 'test_account',
);
await Analytics.setUser(userInfo);
_addLog('Set user: ${userInfo.toJson()}');
}
Future<void> _flushNow() async { Future<void> _flushNow() async {
await Analytics.flush(force: true); await Analytics.flush(force: true);
} }
@ -510,6 +527,16 @@ class _DemoPageState extends State<DemoPage> {
), ),
child: const Text('Track Demo Event'), child: const Text('Track Demo Event'),
), ),
ElevatedButton(
onPressed: _isRunning('set_user')
? null
: () => _runAction(
'set_user',
'Set User Info',
_setUserInfo,
),
child: const Text('Set User Info'),
),
ElevatedButton( ElevatedButton(
onPressed: _isRunning('flush') onPressed: _isRunning('flush')
? null ? null

View File

@ -5,10 +5,12 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import device_info_plus
import path_provider_foundation import path_provider_foundation
import sqflite_darwin import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
} }

View File

@ -49,6 +49,22 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
device_info_plus:
dependency: transitive
description:
name: device_info_plus
sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "12.3.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.3"
dio: dio:
dependency: transitive dependency: transitive
description: description:
@ -81,6 +97,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.5" version: "2.1.5"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -99,6 +123,11 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -376,6 +405,22 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.15.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@ -7,7 +7,7 @@ class DeviceInfo {
required this.screenResolution, required this.screenResolution,
}); });
/// /// "Android 10" "Version 14.4"
final String os; final String os;
/// ///

View File

@ -1,6 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:yx_tracking_flutter/src/model/device_info.dart'; import 'package:yx_tracking_flutter/src/model/device_info.dart';
@ -17,10 +19,53 @@ class DeviceUtil {
static Future<DeviceInfo> collectDeviceInfo() async { static Future<DeviceInfo> collectDeviceInfo() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
final os = Platform.operatingSystem; var os = 'unknown';
final model = Platform.operatingSystemVersion; var model = 'unknown';
final screenResolution = _screenResolution(); final screenResolution = _screenResolution();
try {
final deviceInfo = DeviceInfoPlugin();
if (kIsWeb) {
final webInfo = await deviceInfo.webBrowserInfo;
os = '${webInfo.browserName.name} ${webInfo.appVersion}';
model = webInfo.userAgent ?? 'unknown';
} else {
if (Platform.isAndroid) {
final androidInfo = await deviceInfo.androidInfo;
// : Android 12
os = 'Android ${androidInfo.version.release}';
// : Google Pixel 6
model = '${androidInfo.brand} ${androidInfo.model}';
} else if (Platform.isIOS) {
final iosInfo = await deviceInfo.iosInfo;
// : iOS 15.4
os = '${iosInfo.systemName} ${iosInfo.systemVersion}';
// : iPhone14,2
model = iosInfo.utsname.machine;
} else if (Platform.isMacOS) {
final macInfo = await deviceInfo.macOsInfo;
os = 'macOS ${macInfo.majorVersion}.${macInfo.minorVersion}';
model = macInfo.model;
} else if (Platform.isWindows) {
final windowsInfo = await deviceInfo.windowsInfo;
os =
'Windows ${windowsInfo.majorVersion}.${windowsInfo.minorVersion}';
model = 'PC';
} else if (Platform.isLinux) {
final linuxInfo = await deviceInfo.linuxInfo;
os = 'Linux ${linuxInfo.versionId}';
model = linuxInfo.name;
} else {
os = Platform.operatingSystemVersion;
}
}
} catch (e) {
// Fallback if plugin fails
if (!kIsWeb) {
os = Platform.operatingSystemVersion;
}
}
return DeviceInfo( return DeviceInfo(
os: os, os: os,
model: model, model: model,
@ -29,6 +74,7 @@ class DeviceUtil {
} }
@visibleForTesting @visibleForTesting
/// view /// view
static String screenResolutionForTesting({ static String screenResolutionForTesting({
Iterable<FlutterView> Function()? viewsProvider, Iterable<FlutterView> Function()? viewsProvider,
@ -37,6 +83,7 @@ class DeviceUtil {
} }
@visibleForTesting @visibleForTesting
/// size /// size
static String resolutionFromSizeForTesting(Size size) { static String resolutionFromSizeForTesting(Size size) {
return _resolutionFromSize(size); return _resolutionFromSize(size);

View File

@ -0,0 +1,277 @@
///
///
/// Firebase GA4AmplitudeMixpanel
/// 使 snake_case 便BigQuerySnowflake
///
/// ## / Design Principle
///
/// `TagTemplates` `Event` **** `Event`
///
/// | Event | |
/// |---|---|
/// | `systemCode` | |
/// | `eventType` | |
/// | `userInfo` | `Analytics.setUser` |
/// | `clientType` | iOS/Android/Web |
/// | `deviceInfo` | OS/Model/ |
/// | `timestamp` | |
///
/// `TagTemplates` `Event` ****
class TagTemplates {
///
TagTemplates._();
// ---------------------------------------------------------------------------
// / Business Context Tags
// ---------------------------------------------------------------------------
///
///
/// > ****: `platform` `Event.clientType`
///
/// - [tenantId]: ID
/// - [appVersion]: "1.2.3"
/// - [channel]: "appstore", "huawei", "xiaomi"
/// - [abTestGroup]: A/B "control", "variant_a"
/// - [environment]: "dev", "staging", "prod"
static Map<String, dynamic> businessContext({
required String tenantId,
String? appVersion,
String? channel,
String? abTestGroup,
String? environment,
}) =>
<String, dynamic>{
'tenant_id': tenantId,
if (appVersion != null) 'app_version': appVersion,
if (channel != null) 'channel': channel,
if (abTestGroup != null) 'ab_test_group': abTestGroup,
if (environment != null) 'environment': environment,
};
// ---------------------------------------------------------------------------
// / User Segmentation Tags
// ---------------------------------------------------------------------------
///
///
/// - [userType]: "free", "premium", "enterprise"
/// - [subscriptionTier]: "basic", "pro", "enterprise"
/// - [isFirstTimeUser]: 使
/// - [daysFromSignup]:
/// - [cohort]: "2024-01", "beta_testers"
/// - [acquisitionSource]: "organic", "paid", "referral"
/// - [lifetimeValueTier]: "high", "medium", "low"
static Map<String, dynamic> userSegmentation({
String? userType,
String? subscriptionTier,
bool? isFirstTimeUser,
int? daysFromSignup,
String? cohort,
String? acquisitionSource,
String? lifetimeValueTier,
}) =>
<String, dynamic>{
if (userType != null) 'user_type': userType,
if (subscriptionTier != null) 'subscription_tier': subscriptionTier,
if (isFirstTimeUser != null) 'is_first_time_user': isFirstTimeUser,
if (daysFromSignup != null) 'days_from_signup': daysFromSignup,
if (cohort != null) 'cohort': cohort,
if (acquisitionSource != null) 'acquisition_source': acquisitionSource,
if (lifetimeValueTier != null) 'lifetime_value_tier': lifetimeValueTier,
};
// ---------------------------------------------------------------------------
// / Feature Tracking Tags
// ---------------------------------------------------------------------------
/// /
///
/// - [screenName]:
/// - [featureModule]: "profile", "checkout", "search"
/// - [entrySource]: "deeplink", "push", "organic"
/// - [previousScreen]:
/// - [sessionDepth]:
static Map<String, dynamic> forScreen({
required String screenName,
String? featureModule,
String? entrySource,
String? previousScreen,
int? sessionDepth,
}) =>
<String, dynamic>{
'screen_name': screenName,
if (featureModule != null) 'feature_module': featureModule,
if (entrySource != null) 'entry_source': entrySource,
if (previousScreen != null) 'previous_screen': previousScreen,
if (sessionDepth != null) 'session_depth': sessionDepth,
};
// ---------------------------------------------------------------------------
// / Technical Context Tags
// ---------------------------------------------------------------------------
///
///
/// - [networkType]: "wifi", "4g", "5g"
/// - [isOffline]: 线
/// - [buildMode]: "debug", "release", "profile"
/// - [appInstallSource]: "play_store", "app_store"
/// - [locale]: "zh_CN", "en_US"
/// - [timezone]: "Asia/Shanghai", "America/New_York"
static Map<String, dynamic> technicalContext({
String? networkType,
bool? isOffline,
String? buildMode,
String? appInstallSource,
String? locale,
String? timezone,
}) =>
<String, dynamic>{
if (networkType != null) 'network_type': networkType,
if (isOffline != null) 'is_offline': isOffline,
if (buildMode != null) 'build_mode': buildMode,
if (appInstallSource != null) 'app_install_source': appInstallSource,
if (locale != null) 'locale': locale,
if (timezone != null) 'timezone': timezone,
};
// ---------------------------------------------------------------------------
// / Campaign Attribution Tags
// ---------------------------------------------------------------------------
/// UTM
///
/// - [campaignId]: ID
/// - [campaignName]:
/// - [campaignSource]: "google", "facebook", "tiktok"
/// - [campaignMedium]: "cpc", "email", "social"
/// - [campaignTerm]:
/// - [campaignContent]: 广/
static Map<String, dynamic> campaign({
String? campaignId,
String? campaignName,
String? campaignSource,
String? campaignMedium,
String? campaignTerm,
String? campaignContent,
}) =>
<String, dynamic>{
if (campaignId != null) 'campaign_id': campaignId,
if (campaignName != null) 'campaign_name': campaignName,
if (campaignSource != null) 'campaign_source': campaignSource,
if (campaignMedium != null) 'campaign_medium': campaignMedium,
if (campaignTerm != null) 'campaign_term': campaignTerm,
if (campaignContent != null) 'campaign_content': campaignContent,
};
// ---------------------------------------------------------------------------
// / E-commerce Tags (Firebase GA4 Compatible)
// ---------------------------------------------------------------------------
/// Firebase GA4
///
/// - [currency]: "CNY", "USD"
/// - [value]:
/// - [transactionId]: ID
/// - [paymentMethod]: "alipay", "wechat_pay", "credit_card"
/// - [coupon]:
/// - [shipping]:
/// - [tax]:
static Map<String, dynamic> ecommerce({
String? currency,
double? value,
String? transactionId,
String? paymentMethod,
String? coupon,
double? shipping,
double? tax,
}) =>
<String, dynamic>{
if (currency != null) 'currency': currency,
if (value != null) 'value': value,
if (transactionId != null) 'transaction_id': transactionId,
if (paymentMethod != null) 'payment_method': paymentMethod,
if (coupon != null) 'coupon': coupon,
if (shipping != null) 'shipping': shipping,
if (tax != null) 'tax': tax,
};
// ---------------------------------------------------------------------------
// / Session Context Tags
// ---------------------------------------------------------------------------
///
///
/// - [sessionId]:
/// - [sessionNumber]:
/// - [eventIndex]:
/// - [sessionDurationSeconds]:
static Map<String, dynamic> session({
String? sessionId,
int? sessionNumber,
int? eventIndex,
int? sessionDurationSeconds,
}) =>
<String, dynamic>{
if (sessionId != null) 'session_id': sessionId,
if (sessionNumber != null) 'session_number': sessionNumber,
if (eventIndex != null) 'event_index': eventIndex,
if (sessionDurationSeconds != null)
'session_duration_seconds': sessionDurationSeconds,
};
// ---------------------------------------------------------------------------
// / / Content/Media Tags
// ---------------------------------------------------------------------------
/// /
///
/// - [contentType]: "video", "article", "audio", "image"
/// - [contentId]:
/// - [contentName]: /
/// - [contentCategory]:
/// - [contentDurationSeconds]: /
/// - [contentAuthor]: /
static Map<String, dynamic> content({
String? contentType,
String? contentId,
String? contentName,
String? contentCategory,
int? contentDurationSeconds,
String? contentAuthor,
}) =>
<String, dynamic>{
if (contentType != null) 'content_type': contentType,
if (contentId != null) 'content_id': contentId,
if (contentName != null) 'content_name': contentName,
if (contentCategory != null) 'content_category': contentCategory,
if (contentDurationSeconds != null)
'content_duration_seconds': contentDurationSeconds,
if (contentAuthor != null) 'content_author': contentAuthor,
};
// ---------------------------------------------------------------------------
// / Merge Utility
// ---------------------------------------------------------------------------
/// Map
///
///
///
/// :
/// ```dart
/// final tags = TagTemplates.merge([
/// TagTemplates.businessContext(tenantId: 't1', appVersion: '1.0.0'),
/// TagTemplates.forScreen(screenName: 'HomePage'),
/// TagTemplates.campaign(campaignSource: 'google', campaignMedium: 'cpc'),
/// ]);
/// ```
static Map<String, dynamic> merge(List<Map<String, dynamic>> templates) {
final result = <String, dynamic>{};
for (final template in templates) {
result.addAll(template);
}
return result;
}
}

View File

@ -15,6 +15,7 @@ export 'src/model/device_info.dart';
export 'src/model/event.dart'; export 'src/model/event.dart';
export 'src/model/recent_event_summary.dart'; export 'src/model/recent_event_summary.dart';
export 'src/model/user_info.dart'; export 'src/model/user_info.dart';
export 'src/util/tag_templates.dart';
/// Facade /// Facade
class Analytics { class Analytics {

View File

@ -8,6 +8,7 @@ environment:
flutter: ">=3.22.0" flutter: ">=3.22.0"
dependencies: dependencies:
device_info_plus: ^12.3.0
dio: ^5.4.3+1 dio: ^5.4.3+1
flutter: flutter:
sdk: flutter sdk: flutter