Files
51dj_android/analytics_sdk/lib/manager/sdk_context.dart
T
2026-10-08 11:37:25 +07:00

440 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'package:analytics_sdk/utils/device_fingerprint_util.dart';
import 'package:analytics_sdk/utils/logger.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
/// SDK 全局上下文:持有所有公共字段、提供 event_id 计算。
///
/// 作为 SDK 内部的单一状态来源(SSOT),所有事件构造时从此处读取公共字段。
/// 不对外暴露,接入方无需直接使用。
class SdkContext {
/// SDK 版本号(写死,与 pubspec.yaml 保持一致)
static const String kSdkVersion = '1.4.6';
/// appVersion 公共字段默认值。
/// 接入方未传 `appVersion`、或 release 模式下传入非法值时均使用该值,
/// 保证事件 payload 与 `AnalyticsSdk.getParams()` 返回值保持一致。
static const String kDefaultAppVersion = '1.0.0';
/// 客户端事件签名规则版本。
static const String kEventSignatureVersion = '2.0.0';
static String _eventSignatureHmacKey = '';
static String _eventSignatureVersion = kEventSignatureVersion;
static String? _appId;
static String? _channel;
static String? _uid;
static String? _appVersion;
static String? _device;
static String? _deviceId;
static String? _deviceBrand;
static String? _deviceModel;
static String? _userAgent;
static String? _systemName;
static String? _systemVersion;
static String? _deviceFingerprint;
static String _fingerprintVersion = DeviceFingerprintUtil.kVersion;
static Map<String, dynamic> _environmentFields = _defaultEnvironmentFields();
/// SID 提供者(由 AnalyticsSdk.init() 注入)。
/// 调用时同时返回当前 SID 并记录用户行为,避免 entity 层直接依赖 SessionManager。
static String Function()? sidProvider;
/// 配置公共字段
///
/// 所有参数均为可选,非 null 值才会覆盖现有值。
/// 通常在 [AnalyticsSdk.init] 中调用,也可在 init 之后按需更新。
/// deviceFingerprint / fingerprintVersion 不再支持从外部传入。
static void configure({
String? appId,
String? channel,
String? uid,
String? appVersion,
String? device,
String? deviceId,
String? deviceBrand,
String? deviceModel,
String? userAgent,
String? systemName,
String? systemVersion,
String? deviceFingerprint,
String? fingerprintVersion,
Map<String, dynamic>? environmentFields,
}) {
if (appId != null) _appId = appId;
if (channel != null) _channel = channel;
if (uid != null) _uid = uid;
if (appVersion != null) {
if (RegExp(r'^\d+\.\d+\.\d+$').hasMatch(appVersion)) {
_appVersion = appVersion;
} else if (kDebugMode) {
Logger.analyticsSdk('appVersion 格式不正确,应为 x.y.z,当前值:$appVersion,已忽略',
level: LogLevel.error);
} else {
_appVersion = kDefaultAppVersion;
}
}
if (device != null) _device = device;
if (deviceId != null) _deviceId = deviceId;
if (deviceBrand != null) _deviceBrand = deviceBrand;
if (deviceModel != null) _deviceModel = deviceModel;
if (userAgent != null) _userAgent = userAgent;
if (systemName != null) _systemName = systemName;
if (systemVersion != null) _systemVersion = systemVersion;
if (environmentFields != null) {
_environmentFields = {
..._defaultEnvironmentFields(),
..._normalizeEnvironmentFields(environmentFields),
};
}
}
/// SDK 内部调用:重置自动生成的设备指纹状态。
static void resetGeneratedDeviceFingerprint() {
_deviceFingerprint = null;
_fingerprintVersion = DeviceFingerprintUtil.kVersion;
}
/// SDK 内部调用:写入已生成的设备指纹。
static void configureGeneratedDeviceFingerprint(String deviceFingerprint) {
final normalized = deviceFingerprint.trim();
_deviceFingerprint = normalized.isEmpty ? null : normalized;
_fingerprintVersion = DeviceFingerprintUtil.kVersion;
}
/// 更新用户 ID(登录或切换账号时调用)
static void setUid(String newUid) {
_uid = newUid;
}
/// 更新渠道(渠道变更时调用)
static void setChannel(String channel) {
_channel = channel;
}
/// 增量更新系统、网络、硬件和浏览器环境快照字段。
///
/// 用于网络、电量、充电状态等运行期可能变化的字段刷新。
static void updateEnvironmentFields(Map<String, dynamic> fields) {
if (fields.isEmpty) return;
_environmentFields = {
..._environmentFields,
..._normalizeEnvironmentFields(fields),
};
}
/// 配置客户端事件签名密钥。
///
/// 密钥来自加密配置解密后的 `signKey`,不写入事件、payload、日志或缓存。
static void configureEventSignature({
String? signKey,
String? version,
}) {
if (signKey != null) {
_eventSignatureHmacKey = signKey.trim();
}
final normalizedVersion = version?.trim();
if (normalizedVersion != null && normalizedVersion.isNotEmpty) {
_eventSignatureVersion = normalizedVersion;
}
}
/// 重置所有公共字段为 null(SDK 重新初始化时使用)
///
/// 与 [configure] 不同,此方法会将所有字段清为 null,
/// 而 configure 中传 null 表示"不修改"。
static void reset() {
_appId = null;
_channel = null;
_uid = null;
_appVersion = null;
_device = null;
_deviceId = null;
_deviceBrand = null;
_deviceModel = null;
_userAgent = null;
_systemName = null;
_systemVersion = null;
_deviceFingerprint = null;
_fingerprintVersion = DeviceFingerprintUtil.kVersion;
_environmentFields = _defaultEnvironmentFields();
_eventSignatureHmacKey = '';
_eventSignatureVersion = kEventSignatureVersion;
sidProvider = null;
}
// ──────── Getters(调试 / 外部复用) ────────────────────────────────────
static String? get appId => _appId;
static String? get channel => _channel;
static String? get uid => _uid;
static String? get appVersion => _appVersion;
static String? get deviceId => _deviceId;
static String? get deviceBrand => _deviceBrand;
static String? get deviceModel => _deviceModel;
static String get deviceFingerprint => _deviceFingerprint ?? '';
static String get fingerprintVersion => _fingerprintVersion;
static Map<String, dynamic> get environmentFields =>
Map<String, dynamic>.from(_environmentFields);
/// 获取设备公共字段(供产品方在服务端上报事件时复用)
///
/// 返回的 Map 包含:sid、device、device_id、device_brand、device_model、
/// user_agent、system_name、system_version、device_fingerprint、fp_version。
/// 未配置的字段返回空字符串。
static Map<String, String> getDeviceCommonFields() {
String sid = '';
try {
sid = sidProvider?.call() ?? '';
} catch (_) {
// 未初始化或 SID 获取异常时保持为空,确保该读取 API 不抛异常。
}
return {
'sid': sid,
'device': _device ?? '',
'device_id': _deviceId ?? '',
'device_brand': _deviceBrand ?? '',
'device_model': _deviceModel ?? '',
'user_agent': _userAgent ?? '',
'system_name': _systemName ?? '',
'system_version': _systemVersion ?? '',
'device_fingerprint': _deviceFingerprint ?? '',
'fp_version': _fingerprintVersion,
};
}
/// 获取系统、网络、硬件和浏览器环境快照字段。
///
/// 仅供 SDK 内部构建 `sdk_init` 启动快照和少量汇总事件使用,普通事件公共字段
/// 不再重复携带这些环境快照。
static Map<String, dynamic> getEnvironmentSnapshotFields() {
return Map<String, dynamic>.from(_environmentFields);
}
/// 给 SDK 客户端事件补充外层签名字段。
///
/// 历史版本曾把轻量环境签名写入 payload;这里会清理这些保留字段,
/// 避免同一事件同时出现新旧两套签名。
static Map<String, dynamic> attachDeviceEnvIfNeeded(
Map<String, dynamic> event,
) {
final eventName = event['event']?.toString() ?? '';
final isServerOnlyEvent = _serverOnlyEvents.contains(eventName);
final result = Map<String, dynamic>.from(event);
final existingPayload = result['payload'];
final existingDeviceEnv = result.remove('device_env');
if (existingPayload is Map || existingPayload == null) {
final payload = <String, dynamic>{
if (existingPayload is Map)
...Map<String, dynamic>.from(existingPayload),
if (existingDeviceEnv is Map)
...Map<String, dynamic>.from(existingDeviceEnv),
};
payload.removeWhere(
(key, _) => _reservedPayloadSignatureFields.contains(key));
result['payload'] = payload;
}
if (!isServerOnlyEvent && _eventSignatureHmacKey.isNotEmpty) {
final eventForSignature = Map<String, dynamic>.from(result);
result['event_sig_version'] = _eventSignatureVersion;
result['event_sig'] = _eventSignature(eventForSignature);
}
return result;
}
/// 生成公共字段 Map
///
/// 包含所有客户端事件上报所需的公共字段:
/// event、channel、app_id、uid、sid、client_ts、
/// device、device_id、device_brand、device_model、
/// user_agent、system_name、system_version、
/// device_fingerprint、fp_version、sdk_version、app_version。
///
/// 本方法永远不会抛出异常。
static Map<String, dynamic> generateCommonFields(String event) {
final now = DateTime.now();
final clientTs = now.millisecondsSinceEpoch ~/ 1000;
String sid;
try {
sid = sidProvider?.call() ?? 'fallback_${now.millisecondsSinceEpoch}';
} catch (_) {
sid = 'fallback_${now.millisecondsSinceEpoch}';
}
return {
'event': event,
'channel': _channel ?? '',
'app_id': _appId ?? '',
'uid': _uid ?? '',
'sid': sid,
'client_ts': clientTs,
'device': _device ?? '',
'device_id': _deviceId ?? '',
'device_brand': _deviceBrand ?? '',
'device_model': _deviceModel ?? '',
'user_agent': _userAgent ?? '',
'system_name': _systemName ?? '',
'system_version': _systemVersion ?? '',
'device_fingerprint': _deviceFingerprint ?? '',
'fp_version': _fingerprintVersion,
'sdk_version': kSdkVersion,
'app_version': _appVersion ?? kDefaultAppVersion,
};
}
/// 根据字段列表生成 event_id
///
/// 计算规则:
/// 1. 对每个字段值单独做 MD5
/// 2. 将所有 MD5 用 `|` 拼接
/// 3. 对拼接结果再做一次 MD5,作为最终 event_id
///
/// **注意**:`device_fingerprint` 不参与 event_id 计算。
/// 本方法永远不会抛出异常,失败时返回基于时间戳的备选 ID。
static String generateEventId(List<String> fields) {
try {
final fieldDigests = <String>[];
for (final value in fields) {
final v = value;
final bytes = utf8.encode(v);
final digest = md5.convert(bytes);
fieldDigests.add(digest.toString());
}
final joined = fieldDigests.join('|');
final finalDigest = md5.convert(utf8.encode(joined));
return finalDigest.toString();
} catch (e) {
try {
final content = fields.join('|');
return 'h${(content.hashCode & 0x7FFFFFFF).toRadixString(16)}';
} catch (_) {
return 't${DateTime.now().millisecondsSinceEpoch}';
}
}
}
static Map<String, dynamic> _defaultEnvironmentFields() => {
'language': '',
'timezone': '',
'package_name': '',
'screen_width': -1,
'screen_height': -1,
'screen_dpi': -1,
'device_pixel_ratio': -1,
'orientation': '',
'screen_brightness': -1,
'volume': -1,
'is_screen_on': -1,
'network_type': 'unknown',
'battery_level': -1,
'is_charging': -1,
'battery_state': 'unknown',
'cpu_cores': -1,
'device_memory_gb': -1,
'browser_name': '',
'browser_version': '',
'cookie_enabled': -1,
'local_storage_enabled': -1,
'touch_support': -1,
'max_touch_points': -1,
'color_depth': -1,
'is_pwa': -1,
};
static Map<String, dynamic> _normalizeEnvironmentFields(
Map<String, dynamic> fields,
) {
final normalized = Map<String, dynamic>.from(fields);
if (normalized.containsKey('device_pixel_ratio')) {
normalized['device_pixel_ratio'] =
_normalizeDevicePixelRatio(normalized['device_pixel_ratio']);
}
return normalized;
}
static int _normalizeDevicePixelRatio(dynamic value) {
if (value == null) return -1;
final ratio =
value is num ? value.toDouble() : double.tryParse(value.toString());
if (ratio == null || !ratio.isFinite) return -1;
if (ratio > 10) return ratio.round();
return (ratio * 100).round();
}
static const Set<String> _serverOnlyEvents = {
'user_register',
'user_login',
'realtime_online',
'order_created',
'order_paid',
'coin_consume',
'video_status_change',
};
static const Set<String> _reservedPayloadSignatureFields = {
'sdk_type',
'client_type',
'id_type',
'event_sig',
'event_sig_version',
'security_key_version',
'sign_algorithm',
'sign_ts',
};
static String _eventSignature(
Map<String, dynamic> event,
) {
try {
if (_eventSignatureHmacKey.isEmpty) return '';
final factors = [
event['event']?.toString() ?? '',
event['event_id']?.toString() ?? '',
event['app_id']?.toString() ?? '',
event['client_ts']?.toString() ?? '',
event['device_fingerprint']?.toString() ?? '',
];
final signatureInput = factors.join('|');
final signature = Hmac(sha256, utf8.encode(_eventSignatureHmacKey))
.convert(utf8.encode(signatureInput))
.toString();
Logger.analyticsSdk(
'event_sig input="$signatureInput" '
'event_sig=$signature '
'event_sig_version=$_eventSignatureVersion',
);
return signature;
} catch (_) {
return '';
}
}
/// 从公共字段 Map 和业务特有字段列表生成 event_id。
///
/// 本方法永远不会抛出异常。
static String generateEventIdFromCommonFields(
Map<String, dynamic> commonFields, List<String> extraFields) {
return generateEventId([
commonFields['event'] as String? ?? '',
commonFields['app_id'] as String? ?? '',
commonFields['channel'] as String? ?? '',
commonFields['uid'] as String? ?? '',
commonFields['device'] as String? ?? '',
commonFields['device_id'] as String? ?? '',
commonFields['device_brand'] as String? ?? '',
commonFields['device_model'] as String? ?? '',
(commonFields['client_ts'] ?? '').toString(),
commonFields['sid'] as String? ?? '',
...extraFields,
]);
}
}