Initial commit: 91dj android client dev_1.0.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user