import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:guru_analytics_flutter/events_constants.dart'; import 'package:guru_analytics_flutter/events_constants.dart'; import 'package:guru_app/account/account_data_store.dart'; import 'package:guru_app/analytics/guru_analytics.dart'; import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart'; import 'package:guru_app/guru_app.dart'; import 'package:guru_app/property/app_property.dart'; import 'package:guru_app/property/settings/guru_settings.dart'; import 'package:guru_app/utils/guru_file_utils_extension.dart'; import 'package:guru_utils/core/ext.dart'; import 'package:guru_utils/file/file_utils.dart'; import 'package:guru_utils/log/log.dart'; import 'package:guru_utils/property/runtime_property.dart'; import 'package:guru_utils/quiver/cache.dart'; import 'package:guru_utils/quiver/collection.dart'; import 'package:guru_utils/settings/settings.dart'; import 'package:guru_utils/tuple/tuple.dart'; abstract class EventMatcher { bool match(String eventName); } class UniversalMatcher extends EventMatcher { @override bool match(String eventName) => true; @override String toString() { return 'UniversalMatcher'; } } class RegexMatcher extends EventMatcher { final RegExp re; RegexMatcher(String pattern) : re = RegExp(pattern); @override bool match(String eventName) => re.hasMatch(eventName); @override String toString() { return 'RegexMatcher:${re.pattern}'; } } class WildcardMatcher extends RegexMatcher { final String wildcard; WildcardMatcher(this.wildcard) : super("^${wildcard.replaceAll("*", ".*")}\$"); @override bool match(String eventName) => super.match(eventName); @override String toString() { return 'WildcardMatcher:$wildcard => ${re.pattern}'; } } abstract class StrategyValidator { bool get alwaysVerify => false; const StrategyValidator(); bool validate(); } class UnlimitedValidator extends StrategyValidator { const UnlimitedValidator(); @override bool validate() => true; @override String toString() { return 'UnlimitedValidator'; } } class DisabledValidator extends StrategyValidator { @override bool validate() => false; @override String toString() { return 'DisabledValidator'; } } class PlatformValidator extends StrategyValidator { final String platform; PlatformValidator(this.platform); @override bool validate() => Platform.isAndroid ? platform == "android" : platform == "ios"; @override String toString() { return 'PlatformValidator($platform)'; } } class CountryCodeValidator extends StrategyValidator { final Set included; final Set excluded; CountryCodeValidator(this.included, this.excluded); @override bool validate() { final countryCode = AccountDataStore.instance.countryCode; // 如果excluded不为空,证明存在排除选项,该validate将只判断所有excluded中的逻辑, // 将不会在判断included中的逻辑 if (excluded.isNotEmpty) { return !excluded.contains(countryCode); } if (included.contains(countryCode)) { return true; } return false; } @override String toString() { return 'CountryCodeValidator{included: $included, excluded: $excluded}'; } } class UserPropertyValidator extends StrategyValidator { @override bool get alwaysVerify => true; final List> validProperties; UserPropertyValidator(this.validProperties); @override bool validate() { for (var tuple in validProperties) { if (GuruAnalytics.instance.getProperty(tuple.item1) != tuple.item2) { return false; } } return true; } @override String toString() { return 'UserPropertyValidator{validProperties: $validProperties}'; } } class RandomValidator extends StrategyValidator { final int percent; RandomValidator(int percent) : percent = percent.clamp(10, 90); @override bool validate() { final firstInstallTime = RuntimeProperty.instance.getInt(UtilsSettingsKeys.firstInstallTime, defValue: -1); return (firstInstallTime % 9) >= (percent ~/ 10 - 1); } @override String toString() { return 'RandomValidator{percent: $percent}'; } } class VersionValidator extends StrategyValidator { final String opt; final String buildId; VersionValidator(this.opt, this.buildId); @override bool validate() { final buildNumber = GuruSettings.instance.buildNumber.get(); switch (opt) { case "ve": return buildNumber == buildId; case "vg": return buildNumber.compareTo(buildId) > 0; case "vge": return buildNumber.compareTo(buildId) >= 0; case "vl": return buildNumber.compareTo(buildId) < 0; case "vle": return buildNumber.compareTo(buildId) <= 0; default: return false; } } @override String toString() { return 'VersionValidator{opt: $opt, buildId: $buildId}'; } } class StrategyRuleTypeException implements Exception { final String message; StrategyRuleTypeException([this.message = "Type mismatch: Expected a StrategyRuleItem."]); @override String toString() => "StrategyRuleTypeException: $message"; } class StrategyRule { final EventMatcher? matcher; final StrategyValidator validator; final AppEventCapabilities appEventCapabilities; final String? adjustToken; AppEventOptions? _options; StrategyRule(this.validator, this.appEventCapabilities, {this.matcher, this.adjustToken}); AppEventOptions? getAppEventOptions() { if ((_options != null && !validator.alwaysVerify) || validator.validate()) { return _options ??= AppEventOptions(capabilities: appEventCapabilities); } return null; } @override String toString() { return 'StrategyRule{matcher: $matcher, validator: $validator, appEventCapabilities: $appEventCapabilities, adjustToken: $adjustToken}'; } } class StrategyRuleParser { static final invalidWildcardReg = RegExp(r'[^a-zA-Z=0-9_*]'); static final adjustTokenReg = RegExp(r'^[a-z0-9]{6}$'); static final randomStrategyReg = RegExp(r'^r([1-9]0)$'); static final userPropertyStrategyReg = RegExp(r'^up:(.+)=(.+)$'); static final versionStrategyReg = RegExp(r'^(ve|vg|vl|vge|vle)(\d{8})$'); static final countryStrategyReg = RegExp(r'^cc:(.+)$'); static final countryCodeValidReg = RegExp(r'^[a-z]{2}|\![a-z]{2}$'); final List fields; StrategyRuleParser(this.fields); EventMatcher? createEventMatcher(String event) { if (event == "_all_") { return UniversalMatcher(); } else if (!invalidWildcardReg.hasMatch(event)) { if (event.contains("*")) { return WildcardMatcher(event); } else { // 返回空的话,表示精确匹配,无需提供matcher return null; } } else { return RegexMatcher(event); } } StrategyValidator? createStrategyValidator(String strategy) { if (strategy == "unlimited") { return const UnlimitedValidator(); } else if (strategy == "disabled") { return DisabledValidator(); } else if (strategy == "android" || strategy == "ios") { return PlatformValidator(strategy); } else { final randomMatch = randomStrategyReg.firstMatch(strategy); final randomPercent = randomMatch?.group(1); if (!DartExt.isBlank(randomPercent)) { return RandomValidator(int.parse(randomPercent!)); } final userPropertyMatch = userPropertyStrategyReg.firstMatch(strategy); final userPropertyKey = userPropertyMatch?.group(1); final userPropertyValue = userPropertyMatch?.group(2); if (!DartExt.isBlank(userPropertyKey) && !DartExt.isBlank(userPropertyValue)) { return UserPropertyValidator([Tuple2(userPropertyKey!, userPropertyValue!)]); } final versionMatch = versionStrategyReg.firstMatch(strategy); final versionOpt = versionMatch?.group(1); final versionBuildId = versionMatch?.group(2); if (!DartExt.isBlank(versionOpt) && !DartExt.isBlank(versionBuildId)) { return VersionValidator(versionOpt!, versionBuildId!); } final countryCodeMatch = countryStrategyReg.firstMatch(strategy); final countryCodeExpression = countryCodeMatch?.group(1); if (!DartExt.isBlank(countryCodeExpression)) { final included = {}; final excluded = {}; final countryCodes = countryCodeExpression! .split("|") .where((cc) => countryCodeValidReg.hasMatch(cc)) .toSet(); for (var cc in countryCodes) { if (cc.startsWith("!")) { excluded.add(cc.substring(1)); } else { included.add(cc); } } return CountryCodeValidator(included, excluded); } } return null; } StrategyRuleItem? fromData(List data) { if (data.length != fields.length) { return null; } String? event; EventMatcher? eventMatcher; StrategyValidator? validator; int appEventCapabilitiesFlag = 0; String? adjustToken; for (int i = 0; i < fields.length; ++i) { final field = fields[i]; final value = data[i]; if (field == "event") { event = value; if (event.isEmpty) { return null; } try { eventMatcher = createEventMatcher(value); Log.d("eventMatcher:$eventMatcher"); } catch (error, stacktrace) { Log.w("createEventMatcher error! $error", stackTrace: stacktrace); return null; } } else if (field == "guru") { if (value == '1') { appEventCapabilitiesFlag |= AppEventCapabilities.guru; } } else if (field == "firebase") { if (value == '1') { appEventCapabilitiesFlag |= AppEventCapabilities.firebase; } } else if (field == "facebook") { if (value == '1') { appEventCapabilitiesFlag |= AppEventCapabilities.facebook; } } else if (field == "adjust") { if (value == '1') {} } else if (field == "strategy") { validator = createStrategyValidator(value); } else if ((Platform.isAndroid && field == "ata") || (Platform.isIOS && field == "ati")) { if (value.isNotEmpty && adjustTokenReg.hasMatch(value)) { adjustToken = value; } } } if (event != null && validator != null) { return StrategyRuleItem( event, StrategyRule(validator, AppEventCapabilities(appEventCapabilitiesFlag), matcher: eventMatcher, adjustToken: adjustToken)); } return null; } } class StrategyRuleItem extends Comparable { final String eventName; final StrategyRule rule; StrategyRuleItem(this.eventName, this.rule); @override int compareTo(other) { if (other is StrategyRuleItem) { return eventName.compareTo(other.eventName); } throw StrategyRuleTypeException(); } } class GuruAnalyticsStrategy { static const String tag = "GuruAnalyticsStrategy"; final List priorityRules = []; final SplayTreeMap explicitRules = SplayTreeMap(); final Map iosAdjustEventConverters = {}; final Map androidAdjustEventConverts = {}; bool loaded = false; final LinkedLruHashMap eventRules = LinkedLruHashMap(maximumSize: 128); GuruAnalyticsStrategy._(); static final GuruAnalyticsStrategy instance = GuruAnalyticsStrategy._(); void reset() { priorityRules.clear(); explicitRules.clear(); } static const guruAnalyticsStrategyExtension = ".gas"; // Guru Analytics Strategy Future checkAndCreateLocalStrategyFile() async { final currentLocalStrategy = "${GuruSettings.instance.buildNumber.get()}$guruAnalyticsStrategyExtension"; final file = await FileUtils.instance.getGuruConfigFile("analytics", currentLocalStrategy); if (!file.existsSync()) { try { final data = await rootBundle.loadString("assets/guru/analytics_strategy.csv"); file.writeAsStringSync(data); Log.i("load local strategy success! [$currentLocalStrategy]", tag: tag); return file; } catch (error, stacktrace) { Log.w("not config local strategy!", tag: tag); } } return null; } Future load() async { try { final analyticsConfig = RemoteConfigManager.instance.getAnalyticsConfig(); if (!GuruApp.instance.appSpec.deployment.enabledGuruAnalyticsStrategy || !analyticsConfig.enabledStrategy) { Log.w("analytics strategy disabled!", tag: tag); return; } final String remoteAnalyticsStrategy = analyticsConfig.strategy; final latestAnalyticsStrategy = await AppProperty.getInstance().getLatestAnalyticsStrategy(); if (remoteAnalyticsStrategy != latestAnalyticsStrategy) { loaded = false; } if (loaded) { Log.w("already loaded! ignore!", tag: tag); return; } File? file; // 如果remoteAnalyticsStrategy非空表示云控配置了strategy if (!DartExt.isBlank(remoteAnalyticsStrategy)) { file = await FileUtils.instance.getGuruConfigFile("analytics", remoteAnalyticsStrategy); if (!file.existsSync()) { try { await FileUtils.instance.downloadFile( "${GuruApp.instance.details.storagePrefix}/guru%2Fanalytics%2F$remoteAnalyticsStrategy?alt=media", file); Log.i("download analytics strategy[$remoteAnalyticsStrategy] success", tag: tag); } catch (error, stacktrace) { Log.w("downloadFile error! $error try to fallback", tag: tag); // 这里没有使用上一次的strategy做回滚的原因, // 主要是考虑到上一次的云端strategy可能没有本地的strategy可靠, // SDK假设本地的strategy比Firebase Storage中配置的strategy更可靠 // 因此这里在出现下载异常的情况下,会回滚到本地strategy上 // 如果不想使用这个机制,可以在自己的项目中不配置任何strategy } } if (remoteAnalyticsStrategy != latestAnalyticsStrategy) { AppProperty.getInstance().setLatestAnalyticsStrategy(remoteAnalyticsStrategy); final latestStrategyFile = await FileUtils.instance.getGuruConfigFile("analytics", latestAnalyticsStrategy); if (latestStrategyFile.existsSync()) { FileUtils.instance.deleteFile(latestStrategyFile); } } } // 如果当前文件为空或是不存在,证明有可能相应的strategy下载失败,或是没有设置 // 因此这种情况下尝试使用本地的strategy进行加载 if (file?.existsSync() != true) { file = await checkAndCreateLocalStrategyFile(); if (file?.existsSync() != true) { return; } } final Stream strategyTextStream = file!.openRead().transform(utf8.decoder); StrategyRule? newDefaultRule; final List newPriorityRules = []; final Map newExplicitRules = {}; StrategyRuleParser? parser; int lineNum = 0; await for (var line in strategyTextStream.transform(const LineSplitter())) { final list = line.split(","); Log.d("[${lineNum++}] $list", tag: tag); if (parser == null) { parser = StrategyRuleParser(list); } else { final ruleItem = parser.fromData(list); if (ruleItem == null) { continue; } if (ruleItem.eventName == "_all_") { newDefaultRule = ruleItem.rule; } else if (ruleItem.rule.matcher != null) { newPriorityRules.add(ruleItem.rule); } else { newExplicitRules[ruleItem.eventName] = ruleItem.rule; } if (ruleItem.rule.adjustToken != null) { if (Platform.isAndroid) { androidAdjustEventConverts[ruleItem.eventName] = (_) => AdjustEvent(ruleItem.rule.adjustToken!); } else if (Platform.isIOS) { iosAdjustEventConverters[ruleItem.eventName] = (_) => AdjustEvent(ruleItem.rule.adjustToken!); } } } } reset(); priorityRules.addAll(newPriorityRules.reversed); if (newDefaultRule != null) { priorityRules.add(newDefaultRule); } explicitRules.addAll(newExplicitRules); loaded = true; Log.d( "analytics strategy loaded! ${eventRules.length} ${explicitRules.length} ${priorityRules.length}", tag: tag); } catch (error, stacktrace) {} } StrategyRule? getStrategyRule(String eventName) { Log.d( "[$loaded]getStrategyRule:$eventName ${eventRules.length} ${explicitRules.length} ${priorityRules.length}"); if (!loaded) { return null; } final rule = eventRules[eventName]; if (rule != null) { return rule; } final explicitRule = explicitRules[eventName]; if (explicitRule != null) { return explicitRule; } for (var rule in priorityRules) { Log.d("matcher: ${rule.matcher} eventName: $eventName ${rule.matcher?.match(eventName)}", tag: tag); if (rule.matcher?.match(eventName) == true) { return rule; } } // 如果没有启用strategy,默认按之前逻辑处理 return null; } AdjustEventConverter? getAdjustEventConverter(String eventName) { if (Platform.isAndroid) { return androidAdjustEventConverts[eventName]; } else if (Platform.isIOS) { return iosAdjustEventConverters[eventName]; } return null; } void testRule(String eventName) { final rule = getStrategyRule(eventName); if (rule?.matcher?.match(eventName) != false) { Log.d("testMatch: $eventName => $rule success!", tag: tag); } else { Log.d("testMatch: $eventName => $rule error!", tag: tag); } } }