guru_sdk/guru_app/lib/analytics/guru_analytics.dart

600 lines
19 KiB
Dart

/// Created by Haoyi on 2022/8/24
import 'dart:collection';
import 'dart:core';
import 'dart:io';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:guru_analytics_flutter/event_logger.dart';
import 'package:guru_analytics_flutter/event_logger_common.dart';
import 'package:guru_analytics_flutter/events_constants.dart';
import 'package:guru_analytics_flutter/guru/guru_event_logger.dart';
import 'package:guru_analytics_flutter/guru/guru_statistic.dart';
import 'package:guru_app/account/account_data_store.dart';
import 'package:guru_app/ads/ads_manager.dart';
import 'package:guru_app/ads/core/ads_config.dart';
import 'package:guru_app/aigc/bi/ai_bi.dart';
import 'package:guru_app/analytics/data/analytics_model.dart';
import 'package:guru_app/analytics/strategy/guru_analytics_strategy.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/property_keys.dart';
import 'package:guru_app/property/runtime_property.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/device/device_info.dart';
import 'package:guru_utils/device/device_utils.dart';
import 'package:guru_utils/analytics/analytics.dart';
import 'package:guru_utils/network/network_utils.dart';
import 'package:intl/intl.dart';
import 'package:guru_utils/extensions/extensions.dart';
import 'package:adjust_sdk/adjust_ad_revenue.dart';
import 'package:adjust_sdk/adjust_config.dart';
export 'package:adjust_sdk/adjust.dart';
part 'modules/ads_analytics.dart';
part 'modules/adjust_aware.dart';
class GuruAnalytics extends Analytics with AdjustAware {
bool get release => !_mock && _enabledAnalytics && kReleaseMode;
String appInstanceId = "";
static bool _mock = false;
static bool _enabledAnalytics = true;
static GuruAnalytics instance = GuruAnalytics._();
/// Name of virtual currency type.
static bool initialized = false;
static final Map<String, String> facebookEventMapping = {};
static String currentScreen = "";
static const errorEventCodes = {
14, // 上报事件失败
22, // 网络状态不可用
101, // 调用api出错
102, // api返回结果错误
103, // 设置cacheControl出错
104, // 删除过期事件出错
105, // 从数据库取事件以及更改事件状态为正在上报出错
106, // dns 错误
};
int latestFetchStatisticTs = 0;
final BehaviorSubject<GuruStatistic> guruEventStatistic =
BehaviorSubject.seeded(GuruStatistic.invalid);
Stream<GuruStatistic> get observableGuruEventStatistic => guruEventStatistic.stream;
final BehaviorSubject<UserIdentification> userIdentificationSubject =
BehaviorSubject.seeded(UserIdentification());
UserIdentification get userIdentification => userIdentificationSubject.value;
AppEventCapabilities get currentAppEventCapabilities => EventLogger.getCapabilities();
static void setMock() {
_mock = true;
}
static void disableAnalytics() {
_enabledAnalytics = false;
}
static void enableAnalytics() {
_enabledAnalytics = true;
}
GuruAnalytics._();
String? getProperty(String key) {
return Analytics.userProperties[key];
}
void init() async {
Log.d(
"AnalyticsUtil init### Platform.localeName :${Platform.localeName} ${Intl.getCurrentLocale()}");
if (!_mock && !initialized) {
final analyticsConfig = RemoteConfigManager.instance.getAnalyticsConfig();
EventLogger.setCapabilities(analyticsConfig.toAppEventCapabilities());
EventLogger.registerTransmitter(EventTransmitter({}, defaultHook: (name, parameters) {
recordEvents(name, parameters);
final fbEvent = facebookEventMapping[name];
if (fbEvent == null) {
return;
}
Log.d("transmit EVENT [$name] => [$fbEvent]");
EventLogger.facebookLogEvent(name: fbEvent);
}));
EventLogger.setGuruPriorityGetter((name, parameters) =>
GuruApp.instance.conversionEvents.contains(name)
? EventPriority.EMERGENCE
: EventPriority.DEFAULT);
String xDeviceInfo = '';
try {
final deviceId = await AppProperty.getInstance().getDeviceId();
final deviceInfo = await DeviceUtils.buildDeviceInfo(deviceId: deviceId);
xDeviceInfo = deviceInfo.toXDeviceInfo();
} catch (error, stacktrace) {
Log.e("init deviceInfo error: $error, $stacktrace");
}
await GuruAnalyticsStrategy.instance.load();
EventLogger.initialize(
appId: GuruApp.instance.appSpec.details.saasAppId,
deviceInfo: xDeviceInfo,
delayedInSeconds: analyticsConfig.delayedInSeconds,
eventExpiredInDays: analyticsConfig.expiredInDays,
callback: processAnalyticsCallback,
debug: true,
);
_initEnvProperties();
_logLocale();
_logDeviceType();
_logFirstOpen();
Future.delayed(const Duration(seconds: 1), () {
initAdjust();
initFbEventMapping();
Log.d("register transmitter");
});
initialized = true;
// if (Platform.isAndroid) {
// _logPeerApps();
// }
}
}
void processAnalyticsCallback(int code, String? errorInfo) {
if (!errorEventCodes.contains(code)) {
return;
}
final parameters = {
"item_category": "error_event",
"item_name": code.toString(),
"country": AccountDataStore.instance.countryCode,
"network": AdsManager.instance.connectivityStatus.toString(),
};
if (errorInfo != null) {
parameters["err"] = errorInfo.length > 32 ? errorInfo.substring(0, 32) : errorInfo;
}
logFirebaseEvent("dev_audit", parameters);
// Guru Analytics Event(GAE)
Log.d("[GAE]($code)=>$errorInfo $parameters", tag: "Analytics");
}
void updateUserIdentification(
{String? firebaseAppInstanceId, String? idfa, String? adId, String? gpsAdId}) {
final latestUserIdentification = userIdentificationSubject.value;
bool changed = false;
String? changedFirebaseInstanceId = latestUserIdentification.firebaseAppInstanceId;
String? changedIdfa = latestUserIdentification.idfa;
String? changedAdId = latestUserIdentification.adId;
String? changedGpsAdId = latestUserIdentification.gpsAdId;
if (firebaseAppInstanceId != null &&
latestUserIdentification.firebaseAppInstanceId != firebaseAppInstanceId) {
changedFirebaseInstanceId = firebaseAppInstanceId;
changed = true;
}
if (idfa != null && latestUserIdentification.idfa != idfa) {
changedIdfa = idfa;
changed = true;
}
if (adId != null && latestUserIdentification.adId != adId) {
changedAdId = adId;
changed = true;
}
if (gpsAdId != null && latestUserIdentification.gpsAdId != gpsAdId) {
changedGpsAdId = gpsAdId;
changed = true;
}
if (changed) {
final newUserIdentification = UserIdentification(
firebaseAppInstanceId: changedFirebaseInstanceId ?? '',
idfa: changedIdfa,
adId: changedAdId,
gpsAdId: changedGpsAdId);
userIdentificationSubject.add(newUserIdentification);
Log.d("updateUserIdentification: $newUserIdentification");
}
}
void parseFbEventMapping() {
final fbEventMappingString =
RemoteConfigManager.instance.getString(RemoteConfigReservedConstants.fbEventMapping);
Log.d("parseFbEventMapping first: $fbEventMappingString");
if (fbEventMappingString == null) {
return;
}
Map<String, String> result = {};
final eventEntries = fbEventMappingString.split(";");
for (String eventEntryString in eventEntries) {
final eventEntry = eventEntryString.split(":");
if (eventEntry.length == 2) {
result[eventEntry.first] = eventEntry.last;
}
}
facebookEventMapping.clear();
facebookEventMapping.addAll(result);
Log.d("parseFbEventMapping: $result");
}
void initFbEventMapping() {
RemoteConfigManager.instance.observeConfig().listen((config) {
parseFbEventMapping();
});
parseFbEventMapping();
}
@override
Future<String> getAppInstanceId() async {
if (appInstanceId.isNotEmpty != true) {
appInstanceId = await EventLogger.getAppInstanceId();
RuntimeProperty.instance.setString(PropertyKeys.appInstanceId, appInstanceId);
}
return appInstanceId;
}
void _initEnvProperties() async {
final bundle = await AppProperty.getInstance().loadValuesByTag(PropertyTags.analytics);
final userId = bundle.getString(PropertyKeys.analyticsUserId);
if (userId != null) {
setUserId(userId);
}
final adjustId = bundle.getString(PropertyKeys.analyticsAdjustId);
if (adjustId != null) {
setAdjustId(adjustId);
}
final adId = bundle.getString(PropertyKeys.analyticsAdId);
if (adId != null) {
setAdId(adId);
}
refreshEventStatistic();
String? firebaseId = await getAppInstanceId();
if (firebaseId.isEmpty) {
firebaseId = bundle.getString(PropertyKeys.analyticsFirebaseId);
}
if (firebaseId?.isNotEmpty == true) {
setFirebaseId(firebaseId!);
}
final abProperties = RemoteConfigManager.instance.getABProperties();
final PropertyBundle propertyBundle = PropertyBundle();
if (abProperties.isNotEmpty) {
for (var entry in abProperties.entries) {
setGuruUserProperty(entry.key, entry.value);
propertyBundle.setString(PropertyKeys.buildABTestProperty(entry.key), entry.value);
Log.d("setGuruUserProperty: ${entry.key} = ${entry.value}");
}
}
AppProperty.getInstance().setProperties(propertyBundle);
}
void _logFirstOpen() async {
int firstInstallTime =
RuntimeProperty.instance.getInt(PropertyKeys.firstInstallTime, defValue: -1);
if (firstInstallTime == -1) {
firstInstallTime = await AppProperty.getInstance()
.getOrCreateInt(PropertyKeys.firstInstallTime, DateTimeUtils.currentTimeInMillis());
}
setUserProperty("first_open_time", firstInstallTime.toString());
}
void _logLocale() {
if (Platform.localeName.isNotEmpty == true) {
String lanCode = "";
String countryCode = "";
final currentLocale = Platform.localeName.split('_');
if (currentLocale.isNotEmpty) {
setUserProperty("lang_code", currentLocale[0].toLowerCase());
lanCode = currentLocale[0].toLowerCase();
}
if (currentLocale.length > 1) {
setUserProperty("country_code", currentLocale.last.toLowerCase());
countryCode = currentLocale.last.toLowerCase();
}
Log.d("## locale: [$currentLocale]");
if (lanCode.isNotEmpty && countryCode.isNotEmpty) {
// CountryCodes.init(Locale(lanCode, countryCode));
} else {
// CountryCodes.init();
}
} else {
// CountryCodes.init();
}
}
void _logDeviceType() async {
setUserProperty("device_type", DeviceUtils.isTablet() ? "tablet" : "phone");
final deviceId = await AppProperty.getInstance().getDeviceId();
setDeviceId(deviceId);
}
@override
Future setUserProperty(String key, String value) async {
recordEvents("setUserProperty", {key: value});
recordProperty(key, value);
if (release) {
await EventLogger.setUserProperty(key, value);
}
}
void setDeviceId(String deviceId) {
Log.d("setDeviceId: $deviceId");
recordEvents("setDeviceId", {"userId": deviceId});
recordProperty("deviceId", deviceId);
if (deviceId.isNotEmpty) {
AppProperty.getInstance().setAnalyticsDeviceId(deviceId);
if (release) {
EventLogger.setUserProperty("device_id", deviceId);
EventLogger.setDeviceId(deviceId);
}
}
}
void setUserId(String userId) {
Log.d("setUserId: $userId");
recordEvents("setUserId", {"userId": userId});
recordProperty("userId", userId);
if (userId.isNotEmpty) {
AppProperty.getInstance().setUserId(userId);
if (release) {
EventLogger.setUserId(userId);
FirebaseCrashlytics.instance.setUserIdentifier(userId);
}
}
}
void setAdjustId(String adjustId) {
Log.d("setAdjustId: $adjustId");
recordEvents("setAdjustId", {"adjustId": adjustId});
recordProperty("adjustId", adjustId);
if (adjustId.isNotEmpty) {
AppProperty.getInstance().setAdjustId(adjustId);
updateUserIdentification(adId: adjustId);
if (release) {
EventLogger.setAdjustId(adjustId);
}
}
}
void setFirebaseId(String firebaseId) {
Log.d("setFirebaseId: $firebaseId");
recordEvents("setFirebaseId", {"firebaseId": firebaseId});
recordProperty("firebaseId", firebaseId);
if (firebaseId.isNotEmpty) {
AppProperty.getInstance().setFirebaseId(firebaseId);
updateUserIdentification(firebaseAppInstanceId: firebaseId);
if (release) {
EventLogger.setFirebaseId(firebaseId);
}
}
}
void setAdId(String adId) {
Log.d("setAdId: $adId");
recordEvents("setAdId", {"adId": adId});
recordProperty("adId", adId);
AppProperty.getInstance().setAdId(adId);
updateUserIdentification(gpsAdId: adId);
if (release) {
EventLogger.setAdId(adId);
}
}
void setIdfa(String idfa) {
Log.d("setIdfa: $idfa");
recordEvents("setIdfa", {"idfa": idfa});
recordProperty("idfa", idfa);
AppProperty.getInstance().setIdfa(idfa);
updateUserIdentification(idfa: idfa);
if (release) {
// 自打点中。idfa变是adId
EventLogger.setAdId(idfa);
}
}
void logScreen(String screenName) {
recordEvents("logScreen", {"name": screenName});
recordProperty("screen", screenName);
if (release) {
FirebaseCrashlytics.instance.log(screenName);
EventLogger.logScreen(screenName);
}
}
@override
void setScreen(String screenName) {
if (currentScreen != screenName) {
currentScreen = screenName;
logScreen(screenName);
}
}
@override
void logFirebase(String msg) async {
if (release) {
try {
FirebaseCrashlytics.instance.log(msg);
if (EventLogger.dumpLog) {
Log.d("[Firebase]: $msg");
}
} catch (error, stacktrace) {}
} else {
Log.d("[Firebase]: $msg");
}
}
AppEventOptions? getOptions(String eventName) {
return GuruAnalyticsStrategy.instance.getStrategyRule(eventName)?.getAppEventOptions();
}
@override
void logEvent(String eventName, Map<String, dynamic> parameters, {AppEventOptions? options}) {
refreshEventStatistic();
// Firebase Facebook log event
if (release) {
EventLogger.logEvent(eventName, parameters, options: options ?? getOptions(eventName));
_logAdjustEvent(eventName, parameters);
} else {
Log.d("logEvent: $eventName $parameters");
EventLogger.transmit(eventName, parameters);
}
}
@override
void logEventEx(String eventName,
{String? itemCategory,
String? itemName,
double? value,
Map<String, dynamic> parameters = const {},
AppEventOptions? options}) async {
Map<String, dynamic> map = Map<String, dynamic>.from(parameters);
if (itemCategory != null) {
map["item_category"] = itemCategory;
}
if (itemName != null) {
map["item_name"] = itemName;
}
if (value != null) {
map["value"] = value;
}
logEvent(eventName, map, options: options);
}
Future refreshEventStatistic({bool force = false}) async {
if (!GuruApp.instance.appSpec.deployment.enableAnalyticsStatistic) {
return;
}
final now = DateTimeUtils.currentTimeInMillis();
if (force || (now - latestFetchStatisticTs > DateTimeUtils.minuteInMillis * 2)) {
EventLogger.getStatistic().then((statistic) {
Log.d("Event Statistic:$statistic");
if (statistic != GuruStatistic.invalid && guruEventStatistic.addIfChanged(statistic)) {
setUserProperty("lgd", statistic.logged.toString());
setUserProperty("uld", statistic.uploaded.toString());
}
});
latestFetchStatisticTs = now;
}
}
Future<String> zipGuruLogs() {
return EventLogger.zipGuruLogs();
}
Map<String, dynamic> filterOutNulls(Map<String, dynamic> parameters) {
final Map<String, dynamic> filtered = <String, dynamic>{};
parameters.forEach((String key, dynamic value) {
if (value != null) {
filtered[key] = value;
}
});
return filtered;
}
@override
void logException(dynamic exception, {StackTrace? stacktrace}) async {
if (release) {
Log.d("exception! $exception");
FirebaseCrashlytics.instance.log(exception.toString());
FirebaseCrashlytics.instance
.recordError(exception, stacktrace, printDetails: EventLogger.dumpLog);
} else {
Log.w("Occur Error! $exception $stacktrace", stackTrace: stacktrace);
}
}
void logPurchase(double amount,
{String currency = "",
String contentId = "",
String adPlatform = "",
Map<String, dynamic> parameters = const <String, dynamic>{}}) {
EventLogger.logFbPurchase(amount,
currency: currency,
contentId: contentId,
adPlatform: adPlatform,
additionParameters: parameters);
}
void logEventShare({String? itemCategory, String? itemName}) {
logEvent("share", {
"item_category": itemCategory,
"item_name": itemName,
"content_type": itemCategory,
"item_id": itemName
});
}
void logSpendCredits(String contentId, String contentType, int price,
{required String virtualCurrencyName, required int balance, String scene = ''}) {
if (release) {
EventLogger.logSpendCredits(contentId, contentType, price,
virtualCurrencyName: virtualCurrencyName, balance: balance, scene: scene);
} else {
final parameters = <String, dynamic>{
"item_name": contentId,
"item_category": contentType,
"virtual_currency_name": virtualCurrencyName,
"value": price,
"balance": balance,
"scene": scene
};
Log.d("logEvent: spend_virtual_currency $parameters");
EventLogger.transmit("spend_virtual_currency", parameters);
}
AiBi.instance.spendVirtualCurrency(balance, price.toDouble(), contentType);
}
Future<void> logEarnVirtualCurrency({
required String virtualCurrencyName,
required String method,
required int balance,
required int value,
}) async {
logEvent("earn_virtual_currency", <String, dynamic>{
"virtual_currency_name": virtualCurrencyName,
"item_category": method,
"value": value,
"balance": balance
});
AiBi.instance.earnVirtualCurrency(balance, value.toDouble(), method);
}
Future<void> setGuruUserProperty(String key, String value) async {
return await EventLogger.setGuruUserProperty(key, value);
}
Future<void> logGuruEvent(String eventName, Map<String, dynamic> parameters) async {
EventLogger.guruLogEvent(name: eventName, parameters: parameters);
}
Future<void> logFirebaseEvent(String eventName, Map<String, dynamic> parameters) async {
if (release) {
EventLogger.firebaseLogEvent(name: eventName, parameters: parameters);
} else {
Log.d("logEvent: $eventName $parameters");
}
EventLogger.transmit(eventName, parameters);
}
}