591 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			591 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Dart
		
	
	
| 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<String> included;
 | ||
|   final Set<String> 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<Tuple2<String, String>> 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<String> 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 = <String>{};
 | ||
|         final excluded = <String>{};
 | ||
|         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<String> 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<StrategyRule> priorityRules = [];
 | ||
|   final SplayTreeMap<String, StrategyRule> explicitRules = SplayTreeMap();
 | ||
|   final Map<String, AdjustEventConverter> iosAdjustEventConverters = {};
 | ||
|   final Map<String, AdjustEventConverter> androidAdjustEventConverts = {};
 | ||
| 
 | ||
|   bool loaded = false;
 | ||
| 
 | ||
|   final LinkedLruHashMap<String, StrategyRule> eventRules = LinkedLruHashMap(maximumSize: 128);
 | ||
| 
 | ||
|   GuruAnalyticsStrategy._();
 | ||
| 
 | ||
|   static final GuruAnalyticsStrategy instance = GuruAnalyticsStrategy._();
 | ||
| 
 | ||
|   void reset() {
 | ||
|     priorityRules.clear();
 | ||
|     explicitRules.clear();
 | ||
|   }
 | ||
| 
 | ||
|   static const guruAnalyticsStrategyExtension = ".gas"; // Guru Analytics Strategy
 | ||
| 
 | ||
|   Future<File?> 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<String> strategyTextStream = file!.openRead().transform(utf8.decoder);
 | ||
| 
 | ||
|       StrategyRule? newDefaultRule;
 | ||
|       final List<StrategyRule> newPriorityRules = [];
 | ||
|       final Map<String, StrategyRule> 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);
 | ||
|     }
 | ||
|   }
 | ||
| }
 |