/// Created by Haoyi on 2022/8/24 import 'dart:collection'; import 'dart:core'; import 'dart:io'; import 'package:adjust_sdk/adjust_third_party_sharing.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_remote_config/firebase_remote_config.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/abtest/abtest_model.dart'; import 'package:guru_app/analytics/data/analytics_model.dart'; import 'package:guru_app/analytics/strategy/guru_analytics_strategy.dart'; import 'package:device_info_plus/device_info_plus.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_app/property/settings/guru_settings.dart'; import 'package:guru_platform_data/guru_platform_data.dart'; import 'package:guru_utils/collection/collectionutils.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 facebookEventMapping = {}; static String currentScreen = ""; static final RegExp _consentPurposeRegExp = RegExp(r"^[01]+$"); static String? mockCountryCode; static const errorEventCodes = { 14, // 上报事件失败 22, // 网络状态不可用 101, // 调用api出错 102, // api返回结果错误 103, // 设置cacheControl出错 104, // 删除过期事件出错 105, // 从数据库取事件以及更改事件状态为正在上报出错 106, // dns 错误 }; int latestFetchStatisticTs = 0; final BehaviorSubject guruEventStatistic = BehaviorSubject.seeded(GuruStatistic.invalid); final BehaviorSubject> abTestExperimentVariant = BehaviorSubject.seeded({}); Stream get observableGuruEventStatistic => guruEventStatistic.stream; Stream> get observableABTestExperimentVariant => abTestExperimentVariant.stream; final BehaviorSubject 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]; } Future prepare() async { if (GuruApp.instance.appSpec.localABTestExperiments.isNotEmpty) { await initLocalExperiments(); } RemoteConfigManager.instance.observeConfig().listen((config) { Log.i( "GuruAnalytics observeConfig changed: ${config.lastFetchStatus} ${config.lastFetchTime}"); if (config.lastFetchStatus == RemoteConfigFetchStatus.success) { refreshABProperties(); } }); } 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(); refreshConsents(); Log.d("register transmitter"); }); initialized = true; // if (Platform.isAndroid) { // _logPeerApps(); // } } } Future switchSession(String oldToken, String newToken) async { _initEnvProperties(); _logLocale(); _logDeviceType(); } Future initLocalExperiments() async { final runningExperiments = await AppProperty.getInstance().loadRunningExperiments(); final experiments = GuruApp.instance.appSpec.localABTestExperiments; final validRunningExperimentKeys = runningExperiments.keys.toSet().intersection(experiments.keys.toSet()); for (var experiment in experiments.values) { // 如果在已经开始的实验中,但是不在当前的实验列表中,需要删除 final needRemove = runningExperiments.containsKey(experiment.name) && !validRunningExperimentKeys.contains(experiment.name); if (needRemove) { await removeExperiment(experiment.name); } else { await _applyExperiment(experiment); } } } Future refreshConsents({AnalyticsConfig? analyticsConfig}) async { final config = analyticsConfig ?? RemoteConfigManager.instance.getAnalyticsConfig(); final purposeConsents = await GuruPlatformData.getPurposeConsents(); Log.i("refreshConsents: '$purposeConsents'"); if (purposeConsents.isEmpty) { return ""; } /// 如果他不是完全使用 1,0 组成的字符串 if (!_consentPurposeRegExp.hasMatch(purposeConsents)) { Log.i("invalid consents $purposeConsents"); return ""; } /// 获取当前的 countryCode, 判断是否在 dma country的范围内 if (config.dmaCountry.isNotEmpty) { final countryCode = getCountryCode(); if (!config.dmaCountry.contains(countryCode)) { Log.i("invalid country $countryCode"); return ""; } } final length = min(purposeConsents.length, 32); int flags = 0; for (var i = 0; i < length; i++) { flags |= (((purposeConsents[i] == "1") ? 1 : 0) << i); } final consentsData = { ConsentFieldName.adStorage: config.googleDmaGranted(ConsentType.adStorage, flags), ConsentFieldName.analyticsStorage: config.googleDmaGranted(ConsentType.analyticsStorage, flags), ConsentFieldName.adPersonalization: config.googleDmaGranted(ConsentType.adPersonalization, flags), ConsentFieldName.adUserData: config.googleDmaGranted(ConsentType.adUserData, flags), }; String _flag(String key) { return consentsData[key] == true ? "1" : "0"; } Log.d("setConsents consentsData: $consentsData"); try { final result = await EventLogger.guru.setConsents(consentsData); Log.d("setConsents result: $result"); } catch (error, stacktrace) { Log.e("setConsents error! $error, $stacktrace"); } if (enabledAdjust) { AdjustThirdPartySharing adjustThirdPartySharing = AdjustThirdPartySharing(null); adjustThirdPartySharing.addGranularOption("google_dma", "eea", "1"); adjustThirdPartySharing.addGranularOption( "google_dma", "ad_personalization", _flag(ConsentFieldName.adPersonalization)); adjustThirdPartySharing.addGranularOption( "google_dma", "ad_user_data", _flag(ConsentFieldName.adUserData)); Adjust.trackThirdPartySharing(adjustThirdPartySharing); Log.d("setAdjust complete!"); } final result = "${_flag(ConsentFieldName.adStorage)}${_flag(ConsentFieldName.analyticsStorage)}${_flag(ConsentFieldName.adPersonalization)}${_flag(ConsentFieldName.adUserData)}"; final changed = await AppProperty.getInstance().refreshGoogleDma(result); if (changed || GuruSettings.instance.debugMode.get()) { logEventEx("dma_gg", parameters: {"purpose": purposeConsents, "result": result}); } return result; } 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 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 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!); } refreshABProperties(); } void refreshABProperties() { 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()); } String getCountryCode() { if (mockCountryCode != null) { return mockCountryCode!; } final currentLocale = Platform.localeName.split('_'); if (currentLocale.length > 1) { return currentLocale.last.toLowerCase(); } return ""; } 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); } } static String buildVariantKey(String experimentName) { return "ab_$experimentName"; } String getExperimentVariant(String experimentName) { return abTestExperimentVariant.value[experimentName] ?? "BASELINE"; } Future setLocalABTest(ABTestExperiment experiment, {PropertyBundle? bundle}) async { Log.d("setLocalABTest: $experiment"); String experimentName = experiment.name; final exp = await AppProperty.getInstance().getExperiment(experimentName, bundle: bundle); if (exp != null) { Log.w("Experiment already exists!"); experiment = exp; } return await _applyExperiment(experiment); } Future removeExperiment(String experimentName) async { await AppProperty.getInstance().removeExperiment(experimentName); final data = Map.of(abTestExperimentVariant.value); data.remove(experimentName); abTestExperimentVariant.addIfChanged(data); } Future _applyExperiment(ABTestExperiment experiment) async { final experimentName = experiment.name; if (experiment.isExpired()) { Log.w("Experiment($experimentName) is expired"); await removeExperiment(experimentName); return false; } if (!experiment.isMatchAudience()) { Log.i("NOT match audience! $experiment! INTO BASELINE"); return false; } String variantName = await AppProperty.getInstance().getExperimentVariant(experimentName); if (variantName.isEmpty) { variantName = await AppProperty.getInstance().setExperiment(experiment); } await setGuruUserProperty(buildVariantKey(experimentName), variantName); Log.i("==> Setup Local Experiment($experimentName) variantName: $variantName"); final data = Map.of(abTestExperimentVariant.value); data[experimentName] = variantName; abTestExperimentVariant.addIfChanged(data); return true; } 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); } } } Future setUserId(String userId) async { Log.d("setUserId: $userId"); recordEvents("setUserId", {"userId": userId}); recordProperty("userId", userId); if (userId.isNotEmpty) { await 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 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 parameters = const {}, AppEventOptions? options}) async { Map map = Map.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 zipGuruLogs() { return EventLogger.zipGuruLogs(); } Map filterOutNulls(Map parameters) { final Map filtered = {}; 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 parameters = const {}}) async { Log.i("logPurchase:$amount, $currency, $contentId, $adPlatform, $parameters"); try { await EventLogger.logFbPurchase(amount, currency: currency, contentId: contentId, adPlatform: adPlatform, additionParameters: parameters); } catch (error, stacktrace) { Log.w("logFbPurchase error$error, $stacktrace"); } } 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 = ''}) { final levelName = GuruApp.instance.protocol.getLevelName(); if (release) { EventLogger.logSpendCredits(contentId, contentType, price, virtualCurrencyName: virtualCurrencyName, balance: balance, scene: scene); } else { final parameters = { "item_name": contentId, "item_category": contentType, "virtual_currency_name": virtualCurrencyName, "value": price, "balance": balance, "scene": scene, "level_name": levelName }; Log.d("logEvent: spend_virtual_currency $parameters"); EventLogger.transmit("spend_virtual_currency", parameters); } AiBi.instance.spendVirtualCurrency(balance, price.toDouble(), contentType); } Future logEarnVirtualCurrency( {required String virtualCurrencyName, required String method, required int balance, required int value, String? specific, String? scene}) async { final levelName = GuruApp.instance.protocol.getLevelName(); logEvent( "earn_virtual_currency", filterOutNulls({ "virtual_currency_name": virtualCurrencyName, "item_category": method, "item_name": specific, "value": value, "balance": balance, "level_name": levelName, "scene": scene })); AiBi.instance.earnVirtualCurrency(balance, value.toDouble(), method); } String? peekUserProperty(String key) { return Analytics.userProperties[key]; } Future setGuruUserProperty(String key, String value) async { recordProperty(key, value); return await EventLogger.setGuruUserProperty(key, value); } Future logGuruEvent(String eventName, Map parameters) async { EventLogger.guruLogEvent(name: eventName, parameters: parameters); } Future logFirebaseEvent(String eventName, Map parameters) async { if (release) { EventLogger.firebaseLogEvent(name: eventName, parameters: parameters); } else { Log.d("logEvent: $eventName $parameters"); } EventLogger.transmit(eventName, parameters); } }