guru_sdk/guru_app/lib/analytics/abtest/abtest_model.dart

389 lines
11 KiB
Dart
Raw Normal View History

import 'dart:io';
import 'package:guru_app/account/account_data_store.dart';
import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
import 'package:guru_app/property/settings/guru_settings.dart';
import 'package:guru_utils/device/device_utils.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_utils/random/random_utils.dart';
import 'package:json_annotation/json_annotation.dart';
part 'abtest_model.g.dart';
class ConditionOpt {
static const equals = "eq";
static const greaterThan = "gt";
static const greaterThanOrEquals = "gte";
static const lessThan = "lt";
static const lessThanOrEquals = "lte";
static const notEquals = "ne";
static bool evaluate<T extends Comparable>(T value, T target, String opt) {
final result = value.compareTo(target);
switch (opt) {
case ConditionOpt.equals:
return result == 0;
case ConditionOpt.greaterThan:
return result > 0;
case ConditionOpt.greaterThanOrEquals:
return result >= 0;
case ConditionOpt.lessThan:
return result < 0;
case ConditionOpt.lessThanOrEquals:
return result <= 0;
case ConditionOpt.notEquals:
return result != 0;
default:
return false;
}
}
}
abstract class ABTestFilter {
static const platform = 1;
static const version = 2;
static const country = 3;
static const newUser = 4;
final int type;
const ABTestFilter(this.type);
bool filter();
factory ABTestFilter.fromJson(Map<String, dynamic> json) {
final type = json["type"] = json["type"] ?? 0;
switch (type) {
case ABTestFilter.platform:
return PlatformFilter.fromJson(json);
case ABTestFilter.country:
return CountryFilter.fromJson(json);
case ABTestFilter.version:
return VersionFilter.fromJson(json);
case ABTestFilter.newUser:
return NewUserFilter.fromJson(json);
default:
throw UnimplementedError("Unknown ABTestFilter type: $type");
}
}
Map<String, dynamic> toJson() => toJson()..addAll({"type": type});
}
abstract class ABTestCondition {
bool validate();
}
/// 为了后面可以做一些定制,因此这里按平台进行区分
@JsonSerializable()
class AndroidCondition extends ABTestCondition {
@JsonKey(name: "opt")
final String? opt;
@JsonKey(name: "sdk")
final int? sdkInt;
AndroidCondition({this.opt, this.sdkInt});
factory AndroidCondition.fromJson(Map<String, dynamic> json) => _$AndroidConditionFromJson(json);
Map<String, dynamic> toJson() => _$AndroidConditionToJson(this);
@override
bool validate() {
final versionOpt = opt;
final targetVersion = sdkInt;
if (versionOpt != null && targetVersion != null) {
final versionCode = DeviceUtils.peekOSVersion();
// 操作系统版本号获取失败直接返回false
if (versionCode == -1) {
return false;
}
if (!ConditionOpt.evaluate<int>(versionCode, targetVersion, versionOpt)) {
return false;
}
}
// 方便后面扩展其它字段
return true;
}
}
@JsonSerializable()
class IosCondition extends ABTestCondition {
@JsonKey(name: "opt")
final String? opt;
@JsonKey(name: "ver")
final int? version; // 这里只记录大版本号
IosCondition({this.opt, this.version});
factory IosCondition.fromJson(Map<String, dynamic> json) => _$IosConditionFromJson(json);
Map<String, dynamic> toJson() => _$IosConditionToJson(this);
@override
bool validate() {
final versionOpt = opt;
final targetVersion = version;
if (versionOpt != null && targetVersion != null) {
final versionCode = DeviceUtils.peekOSVersion();
// 操作系统版本号获取失败直接返回false
if (versionCode == -1) {
return false;
}
if (!ConditionOpt.evaluate<int>(versionCode, targetVersion, versionOpt)) {
return false;
}
}
// 方便后面扩展其它字段
return true;
}
}
@JsonSerializable()
class PlatformFilter extends ABTestFilter {
@JsonKey(name: "ac")
final AndroidCondition? androidCondition;
@JsonKey(name: "ic")
final IosCondition? iosCondition;
PlatformFilter({this.androidCondition, this.iosCondition}) : super(ABTestFilter.platform);
@override
bool filter() {
// 如果配了 Platform Filter, 如果指定平台没有 condition, 则默认为true
if (Platform.isAndroid) {
return androidCondition?.validate() != false;
} else if (Platform.isIOS) {
return iosCondition?.validate() != false;
}
return false;
}
factory PlatformFilter.fromJson(Map<String, dynamic> json) => _$PlatformFilterFromJson(json);
@override
Map<String, dynamic> toJson() => _$PlatformFilterToJson(this);
}
@JsonSerializable(constructor: "_")
class VersionFilter extends ABTestFilter {
@JsonKey(name: "opt")
final String opt;
@JsonKey(name: "mmp")
final String mmp; // major.minor.patch
VersionFilter._(this.opt, this.mmp) : super(ABTestFilter.version);
VersionFilter.equals(this.mmp)
: opt = ConditionOpt.equals,
super(ABTestFilter.version);
VersionFilter.greaterThan(this.mmp)
: opt = ConditionOpt.greaterThan,
super(ABTestFilter.version);
VersionFilter.greaterThanOrEquals(this.mmp)
: opt = ConditionOpt.greaterThanOrEquals,
super(ABTestFilter.version);
VersionFilter.lessThan(this.mmp)
: opt = ConditionOpt.lessThan,
super(ABTestFilter.version);
VersionFilter.lessThanOrEquals(this.mmp)
: opt = ConditionOpt.lessThanOrEquals,
super(ABTestFilter.version);
VersionFilter.notEquals(this.mmp)
: opt = ConditionOpt.notEquals,
super(ABTestFilter.version);
@override
bool filter() {
final version = GuruSettings.instance.version.get();
Log.d("[$runtimeType] $version $opt $mmp");
return ConditionOpt.evaluate<String>(version, mmp, opt);
}
@override
String toString() {
return 'VersionValidator{opt: $opt, mmp: $mmp}';
}
factory VersionFilter.fromJson(Map<String, dynamic> json) => _$VersionFilterFromJson(json);
@override
Map<String, dynamic> toJson() => _$VersionFilterToJson(this);
}
@JsonSerializable(constructor: "_")
class CountryFilter extends ABTestFilter {
@JsonKey(name: "included", defaultValue: {})
final Set<String> included;
@JsonKey(name: "excluded", defaultValue: {})
final Set<String> excluded;
CountryFilter._(this.included, this.excluded) : super(ABTestFilter.country);
CountryFilter.included(this.included)
: excluded = {},
super(ABTestFilter.country);
CountryFilter.excluded(this.excluded)
: included = {},
super(ABTestFilter.country);
@override
bool filter() {
final String countryCode = Platform.localeName.split('_').safeLast?.toLowerCase() ?? "";
Log.d("[$runtimeType] $countryCode included: $included excluded: $excluded");
if (countryCode.isEmpty) {
return false;
}
// 如果excluded不为空证明存在排除选项该validate将只判断所有excluded中的逻辑
// 将不会在判断included中的逻辑
if (excluded.isNotEmpty) {
return !excluded.contains(countryCode);
}
if (included.contains(countryCode)) {
return true;
}
return false;
}
factory CountryFilter.fromJson(Map<String, dynamic> json) => _$CountryFilterFromJson(json);
@override
Map<String, dynamic> toJson() => _$CountryFilterToJson(this);
}
@JsonSerializable()
class NewUserFilter extends ABTestFilter {
NewUserFilter() : super(ABTestFilter.newUser);
@override
bool filter() {
final version = GuruSettings.instance.version.get();
final fiv = GuruSettings.instance.firstInstallVersion.get();
return fiv.startsWith(version);
}
factory NewUserFilter.fromJson(Map<String, dynamic> json) => _$NewUserFilterFromJson(json);
@override
Map<String, dynamic> toJson() => _$NewUserFilterToJson(this);
}
@JsonSerializable()
class ABTestAudience {
@JsonKey(name: "filters")
final List<ABTestFilter> filters;
@JsonKey(name: "variant", defaultValue: 2)
final int variant;
ABTestAudience({required this.filters, this.variant = 2});
factory ABTestAudience.fromJson(Map<String, dynamic> json) => _$ABTestAudienceFromJson(json);
Map<String, dynamic> toJson() => _$ABTestAudienceToJson(this);
@override
String toString() {
return 'ABTestAudience{filters: $filters, variant: $variant}';
}
bool validate() {
for (var filter in filters) {
if (!filter.filter()) {
return false;
}
}
return true;
}
}
@JsonSerializable()
class ABTestExperiment {
@JsonKey(name: "name")
final String name;
@JsonKey(name: "start_ts", defaultValue: 0)
final int startTs;
@JsonKey(name: "end_ts", defaultValue: 0)
final int endTs;
@JsonKey(name: "audience")
final ABTestAudience audience;
ABTestExperiment(
{required String name, required this.startTs, required this.endTs, required this.audience})
: name = _validExperimentName(name);
@override
String toString() {
return 'ABTestExperiment{name: $name, startTs: $startTs, endTs: $endTs, audience: $audience}';
}
static String _validExperimentName(String experimentName) {
if (experimentName.contains(RemoteConfigManager.invalidABKeyRegExp)) {
Log.w("abName($experimentName) use invalid key! $experimentName! replace invalid char to _");
experimentName = experimentName.replaceAll(RemoteConfigManager.invalidABKeyRegExp, "_");
} else {
if (experimentName.length > 20) {
experimentName = experimentName.substring(0, 20);
}
}
return experimentName;
}
factory ABTestExperiment.fromJson(Map<String, dynamic> json) => _$ABTestExperimentFromJson(json);
Map<String, dynamic> toJson() => _$ABTestExperimentToJson(this);
bool isExpired() {
final now = DateTime.now().millisecondsSinceEpoch;
return now < startTs || now > endTs;
}
bool isMatchAudience() {
return audience.validate();
}
@JsonKey(includeToJson: false)
String? _variantName;
String get variantName =>
(_variantName ??= _toVariantName(RandomUtils.nextInt(audience.variant)));
static const _originalVariant = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
String _toVariantName(int value) {
String codes = "";
int nv = value;
while (true) {
final nextNum = nv ~/ _originalVariant.length;
if (nextNum <= 0) {
break;
}
codes = "${_originalVariant[nv % _originalVariant.length]}$codes";
nv = nextNum;
}
final tailIndex = nv % _originalVariant.length;
if (tailIndex >= 0) {
codes = "${_originalVariant[tailIndex]}$codes";
}
return codes.toString();
}
}