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

591 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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