guru_sdk/guru_app/lib/analytics/strategy/guru_analytics_strategy.dart

591 lines
18 KiB
Dart
Raw Normal View History

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);
}
}
}