com.guru.unity.sdk.core/Runtime/GuruAnalytics/Runtime/Script/GuruAnalytics.cs

644 lines
22 KiB
C#
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.

// ReSharper disable ReplaceSubstringWithRangeIndexer
namespace Guru
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
using System.Diagnostics.CodeAnalysis;
public class GuruAnalytics
{
// Plugin Version
private const string Version = "1.12.0";
public static readonly string Tag = "[ANU]";
private static readonly string ActionName = "logger_error";
private const int EventPriorityDefault = 10;
private static GuruAnalytics _instance;
public static GuruAnalytics Instance
{
get
{
if (_instance == null)
{
_instance = new GuruAnalytics();
}
return _instance;
}
}
private static bool _isReady = false;
public static bool IsReady => _isReady;
private IAnalyticsAgent _agent;
private IAnalyticsAgent Agent
{
get
{
if (_agent == null)
{
#if UNITY_EDITOR
_agent = new AnalyticsAgentMock();
#elif UNITY_ANDROID
_agent = new AnalyticsAgentAndroid();
#elif UNITY_IOS
_agent = new AnalyticsAgentIOS();
#endif
}
if (_agent == null)
{
throw new NotImplementedException("You Should Implement IAnalyticsAgent on platform first.");
}
return _agent;
}
}
private Dictionary<string, string> _userProperties;
/// <summary>
/// 用户属性缓存字典
/// </summary>
private Dictionary<string, string> UserProperties
{
get
{
if (_userProperties == null)
{
_userProperties = new Dictionary<string, string>(10);
}
return _userProperties;
}
}
private bool _enableErrorLog;
private string _experimentGroupId;
public string ExperimentGroupId => _experimentGroupId;
private DateTime _lastReportTime;
/// <summary>
/// 启动日志错误上报
/// </summary>
public bool EnableErrorLog
{
get => _enableErrorLog;
set
{
_enableErrorLog = value;
if (_enableErrorLog) InitCallbacks(); // 激活错误日志回调
if (Agent != null) Agent.EnableErrorLog = _enableErrorLog;
}
}
#region 公用接口
/// <summary>
/// 初始化接口
/// </summary>
public void Init(string appId, string deviceInfo, Action onInitComplete, bool isDebug = false, string firebaseId = "")
{
Debug.Log($"{Tag} --- Guru Analytics [{Version}] initialing...");
if (_isReady) return;
if (Agent == null)
{
// Agent 不存在则抛异常
throw new NotImplementedException($"{Tag} Agent is null, please check your implementation of IAnalyticsAgent.");
}
string groupId = "not_set";
string baseUrl = "";
string[] uploadIpAddress = null;
bool enabelErrorLog = true;
// 获取云控参数
// TODO: 针对 GuruSDK 整体的云控值做一个分组的解决方案
var guruInitParams = GuruAnalyticsConfigManager.GetInitParams();
if (guruInitParams != null)
{
// 如果分组实验打开
groupId = guruInitParams.groupId;
baseUrl = guruInitParams.baseUrl;
uploadIpAddress = guruInitParams.uploadIpAddress;
enabelErrorLog = guruInitParams.enableErrorLog;
}
if (!string.IsNullOrEmpty(firebaseId))
Agent.SetFirebaseId(firebaseId); // 需要提前设置 Firebase ID
// 分组ID赋值
_experimentGroupId = groupId;
EnableErrorLog = enabelErrorLog;
// 初始化参数
Agent.Init(appId, deviceInfo, baseUrl, uploadIpAddress, onInitComplete, isDebug);
_lastReportTime = new DateTime(1970, 1, 1);
_isReady = true;
Debug.Log($"{Tag} --- Guru Analytics [{Version}] initialized.");
Debug.Log($"{Tag} --- GroupId: {groupId}");
}
/// <summary>
/// 设置视图名称
/// </summary>
/// <param name="screenName"></param>
public void SetScreen(string screenName)
{
if (!_isReady) return;
if (string.IsNullOrEmpty(screenName)) return;
// CacheUserProperty($"screen_name", screenName);
Agent.SetScreen(screenName);
}
/// <summary>
/// 设置广告ID
/// </summary>
/// <param name="id"></param>
public void SetAdId(string id)
{
if (!_isReady) return;
if (string.IsNullOrEmpty(id)) return;
// CacheUserProperty($"ad_id", id);
Agent.SetAdId(id);
}
/// <summary>
/// 设置用户属性
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void SetUserProperty(string key, string value)
{
if (!_isReady)
{
Debug.LogWarning($"{Tag} --- is Not Ready SetUserProperty: [{key}, {value}] failed");
return;
}
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) return;
// CacheUserProperty(key, value); // 添加用户属性
// ReSharper disable once Unity.PerformanceCriticalCodeInvocation
Debug.Log($"{Tag} --- SetUserProperty: [{key}, {value}]");
Agent.SetUserProperty(key, value);
}
/// <summary>
/// 设置Firebase ID
/// </summary>
/// <param name="id"></param>
public void SetFirebaseId(string id)
{
if (!_isReady) return;
if (string.IsNullOrEmpty(id)) return;
// CacheUserProperty($"firebase_id", id);
Agent.SetFirebaseId(id);
}
/// <summary>
/// 设置Adjust ID
/// </summary>
/// <param name="id"></param>
public void SetAdjustId(string id)
{
if (!_isReady) return;
if (string.IsNullOrEmpty(id)) return;
// CacheUserProperty($"adjust_id", id);
Agent.SetAdjustId(id);
}
/// <summary>
/// 设置设备ID
/// </summary>
/// <param name="deviceId"></param>
public void SetDeviceId(string deviceId)
{
if (!_isReady) return;
if (string.IsNullOrEmpty(deviceId)) return;
// CacheUserProperty($"device_id", deviceId);
Agent.SetDeviceId(deviceId);
}
public void SetAndroidId(string androidId)
{
if (!_isReady) return;
if (string.IsNullOrEmpty(androidId)) return;
// CacheUserProperty(Analytics.PropertyAndroidID, androidId);
}
public void SetIDFV(string idfv)
{
if (!_isReady) return;
if (string.IsNullOrEmpty(idfv)) return;
// CacheUserProperty(Analytics.PropertyIDFV, idfv);
}
public void SetIDFA(string idfa)
{
if (!_isReady) return;
if (string.IsNullOrEmpty(idfa)) return;
// CacheUserProperty(Analytics.PropertyIDFA, idfa);
}
/// <summary>
/// 设置用户ID
/// </summary>
/// <param name="uid"></param>
public void SetUid(string uid)
{
if (!_isReady) return;
if (string.IsNullOrEmpty(uid)) return;
// CacheUserProperty($"uid", uid);
Agent.SetUid(uid);
}
/// <summary>
/// 上报事件成功率
/// </summary>
public void ReportEventSuccessRate() => Agent.ReportEventSuccessRate();
/// <summary>
/// 上报打点事件
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="data">INT类型的值</param>
/// <param name="priority"></param>
[SuppressMessage("ReSharper", "Unity.PerformanceCriticalCodeInvocation")]
public void LogEvent(string eventName, Dictionary<string, dynamic> data = null, int priority = -1)
{
string raw = "";
if (data != null && data.Count > 0)
{
raw = BuildParamsJson(data);
}
if (priority < 0) priority = EventPriorityDefault;
Debug.Log($"{Tag} --- LogEvent GuruAnalytics:{eventName} | raw: {raw} | priority: {priority}");
Agent.LogEvent(eventName, raw, priority);
}
/*
private static string BuildParamsString(Dictionary<string, dynamic> data)
{
string raw = "";
List<string> strList = new List<string>(data.Count);
foreach (var kvp in data)
{
strList.Add(BuildStringValue(kvp));
raw = string.Join(",", strList);
}
return raw;
}
*/
[SuppressMessage("ReSharper", "Unity.PerformanceCriticalCodeInvocation")]
private static string BuildParamsJson(Dictionary<string, dynamic> data)
{
try
{
// 强制转换加入国家设置
return JsonConvert.SerializeObject(data, new JsonSerializerSettings()
{
Culture = new CultureInfo("en-US"),
});
}
catch (Exception e)
{
Debug.LogError(e);
}
return "";
}
/*
/// <summary>
/// 构建带有类型格式的Str值
/// </summary>
/// <param name="kvp"></param>
/// <returns></returns>
private static string BuildStringValue(KeyValuePair<string, dynamic> kvp)
{
if (kvp.Value is int || kvp.Value is long)
{
return $"{kvp.Key}:i{kvp.Value}";
}
if (kvp.Value is double || kvp.Value is float)
{
double dValue = (double)((object)kvp.Value);
return $"{kvp.Key}:d{dValue.ToString("F11", new CultureInfo("en-US"))}"; // 保留精度进行转换
}
return $"{kvp.Key}:s{kvp.Value}";
}
*/
/// <summary>
/// 设置太极02值
/// </summary>
/// <param name="value"></param>
public void SetTch02Value(double value)
{
Debug.Log($"{Tag} set tch_02_value:{value}");
Agent.SetTch02Value(value);
}
#endregion
#region iOS独有接口
#if UNITY_IOS
// 触发测试崩溃埋点
public static void TestCrash() => AnalyticsAgentIOS.TestCrashEvent();
#endif
#endregion
#region 用户属性
/// <summary>
/// 记录用户属性
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
private void CacheUserProperty(string key, string value)
{
// bool needUpdate = !UserProperties.ContainsKey(key) || UserProperties[key] != value;
UserProperties[key] = value;
// if (needUpdate) UpdateAllUserProperties();
}
#endregion
#region 日志回调
private void InitCallbacks()
{
try
{
GuruSDKCallback.RemoveCallback(OnSDKCallback);
GuruSDKCallback.AddCallback(OnSDKCallback);
if (Agent != null)
Agent.InitCallback(GuruSDKCallback.ObjectName, GuruSDKCallback.MethodName);
}
catch (Exception ex)
{
Analytics.LogCrashlytics(ex);
}
}
/// <summary>
/// 获取SDK回调
/// </summary>
/// <param name="raw"></param>
private void OnSDKCallback(string raw)
{
if (string.IsNullOrEmpty(raw)) return;
if (!raw.Contains($"\"{ActionName}\"")) return; // 不对其他行为的日志进行过滤
ParseJsonAndSendEvent(raw);
}
/// <summary>
/// 上报错误信息
/// </summary>
/// <param name="code"></param>
/// <param name="errorInfo"></param>
/// <param name="category"></param>
/// <param name="extra"></param>
private void ReportDevAuditEvent(int code, string errorInfo = "", string category = "", Dictionary<string, object> extra = null)
{
// Debug.Log($"{Tag} --- OnLoggerErrorEvent: code:{code}\t info:{errorInfo}");
var codeString = ((AnalyticsCode)code).ToString();
if (string.IsNullOrEmpty(codeString)) codeString = $"ErrorCode:{code}";
if (string.IsNullOrEmpty(errorInfo)) errorInfo = "Empty";
var parameters = new Dictionary<string, dynamic>()
{
{"item_name", codeString},
{"country", IPMConfig.IPM_COUNTRY_CODE},
{"network", Application.internetReachability.ToString()},
{"exp", _experimentGroupId}
};
if (!string.IsNullOrEmpty(category))
{
parameters[Analytics.ParameterItemCategory] = category;
}
int len = 96;
if (errorInfo.Length > len)
errorInfo = errorInfo.TrimStart().Substring(0, len);
if (!string.IsNullOrEmpty(errorInfo))
parameters["err"] = errorInfo;
if (extra != null)
{
foreach (var kvp in extra)
{
parameters[kvp.Key] = kvp.Value;
}
}
// Only for firebase GA
Analytics.TrackEvent("dev_audit", parameters, new Analytics.EventSetting() { EnableFirebaseAnalytics = true });
}
private void ParseJsonAndSendEvent(string json)
{
Debug.Log($"{Tag} ------ ParseWithJson: json:\n{json}");
int code = (int)AnalyticsCode.UNITY_INTERNAL_ERROR;
string info = json;
try
{
var dict = JsonConvert.DeserializeObject<JObject>(json);
if (dict == null || !dict.TryGetValue("data", out var jData)) return;
var j = jData.Value<JObject>();
if (j == null || !j.TryGetValue("code", out var jCode)) return;
code = jCode.Value<int>();
if (!j.TryGetValue("msg", out var jMsg)) return;
info = jMsg.Value<string>();
ReportWithCodeAndInfo(code, info);
}
catch (Exception)
{
string p = "\"msg\":\"";
string m = json;
if (json.Contains(p)) m = json.Substring(json.IndexOf(p, StringComparison.Ordinal) + p.Length);
info = $"JsonEX:{m}";
// Debug.Log($"{Tag} --- {info}");
Analytics.LogCrashlytics(json, false);
Analytics.LogCrashlytics(info);
ReportWithCodeAndInfo(code, info);
}
}
/// <summary>
/// 上报异常
/// 上报条件:上报总数 > 30 条, 上报成功率小于 0.7, 且间隔 5 分钟
/// Native 已经处理数量和成功率判断
/// </summary>
/// <param name="code"></param>
/// <param name="info"></param>
private void ReportWithCodeAndInfo(int code, string info)
{
if (Agent == null) return;
if (Application.internetReachability == NetworkReachability.NotReachable) return; // 网络不可用时不上报
ReportAnalyticsAudit(); // 上报
// 源码https://github.com/castbox/flutter_jigsort/blob/3.2.0V2/lib/data/jigsort_compliance_protocol.dart
var ac = (AnalyticsCode)code;
Debug.Log($"{Tag} ------ Get Code And Info: code:{code}[{ac}] \tinfo:{info}");
switch (ac)
{
case AnalyticsCode.UNITY_INTERNAL_ERROR: // -1
ReportUnityErrorEvent(code, info);
break;
// case AnalyticsCode.DELETE_EXPIRED:
case AnalyticsCode.UPLOAD_FAIL: //14
ReportUploadFailEvent(code, info);
break;
// case AnalyticsCode.NETWORK_LOST:
// case AnalyticsCode.CRONET_INIT_FAIL:
// case AnalyticsCode.CRONET_INIT_EXCEPTION:
// case AnalyticsCode.ERROR_API:
// case AnalyticsCode.ERROR_RESPONSE:
// case AnalyticsCode.ERROR_CACHE_CONTROL:
// case AnalyticsCode.ERROR_DELETE_EXPIRED:
case AnalyticsCode.ERROR_LOAD_MARK: // 105
ReportRuntimeErrorEvent(code, info);
break;
// case AnalyticsCode.ERROR_DNS:
// case AnalyticsCode.ERROR_ZIP:
// case AnalyticsCode.ERROR_DNS_CACHE:
// case AnalyticsCode.CRONET_INTERCEPTOR:
// case AnalyticsCode.ERROR_SESSION_START_ERROR:
case AnalyticsCode.EVENT_LOOKUP: // 1003
ReportDNSErrorEvent(code, info);
break;
case AnalyticsCode.EVENT_SESSION_ACTIVE: // 1004
ReportSessionActiveErrorEvent(code, info);
break;
}
}
private int _reportUploadFailCount = 0;
/// <summary>
/// 上报失败事件 (14)
/// </summary>
/// <param name="code"></param>
/// <param name="info"></param>
private void ReportUploadFailEvent(int code, string info)
{
if (Agent.GetEventCountTotal() < 50) return; // 数量太少不报
if ((float)Agent.GetEventCountUploaded() / Agent.GetEventCountTotal() > 0.6f) return; // 成功率太高也不报
if (_reportUploadFailCount >= 5) return; // N 次之后不再上报
ReportDevAuditEvent(code, info);
_reportUploadFailCount++;
}
private int _reportRuntimeExceptionTimes = 0;
// 105
private void ReportRuntimeErrorEvent(int code, string info)
{
if (_reportRuntimeExceptionTimes >= 5) return; // N 次之后不再上报
ReportDevAuditEvent(code, info);
_reportRuntimeExceptionTimes++;
}
// 1003
private void ReportDNSErrorEvent(int code, string info)
{
ReportDevAuditEvent(code, info, "ga_dns");
}
// 1004
private void ReportSessionActiveErrorEvent(int code, string info)
{
ReportDevAuditEvent(code, info, "session_active");
}
// -1
private void ReportUnityErrorEvent(int code, string info)
{
ReportDevAuditEvent(code, info, "unity");
}
// 上报 Snapshot 数据
private void ReportAnalyticsAudit()
{
if(DateTime.UtcNow - _lastReportTime < TimeSpan.FromMinutes(5)) // 5 分钟内只上报一次
return;
var snapshot = Agent.GetAuditSnapshot();
if (string.IsNullOrEmpty(snapshot)) return; // 空字段不报
var data = JsonParser.ToObject<Dictionary<string, object>>(snapshot);
if (data == null) return; // 解析失败不报
// 上报事件
ReportDevAuditEvent(0, "","analytics_audit", data);
_lastReportTime = DateTime.UtcNow;
}
#endregion
#region UNIT_TEST
#if UNITY_EDITOR
public static void TestOnCallback(string msg)
{
Instance.OnSDKCallback(msg);
}
#endif
#endregion
}
/// <summary>
/// 网络状态枚举
/// 详见 guru_analytics 库 guru.core.analytics.handler.AnalyticsCode 类
/// </summary>
public enum AnalyticsCode
{
UNITY_INTERNAL_ERROR = -1, // unity 内部错误
DELETE_EXPIRED = 12, // 删除过期事件
UPLOAD_FAIL = 14, // 上报事件失败
NETWORK_LOST = 22, // 网络状态不可用
CRONET_INIT_FAIL = 26, // 开启Cronet失败
CRONET_INIT_EXCEPTION = 27, // 开启Cronet报错
ERROR_API = 101, // 调用api出错
ERROR_RESPONSE = 102, // api返回结果错误
ERROR_CACHE_CONTROL = 103, // 设置cacheControl出错
ERROR_DELETE_EXPIRED = 104, // 删除过期事件出错
ERROR_LOAD_MARK = 105, // 从数据库取事件以及更改事件状态为正在上报出错
ERROR_DNS = 106, // dns 错误
ERROR_ZIP = 107, // zip 错误
ERROR_DNS_CACHE = 108, // zip 错误
CRONET_INTERCEPTOR = 109, // cronet拦截器
ERROR_SESSION_START_ERROR = 110,
EVENT_LOOKUP = 1003,
EVENT_SESSION_ACTIVE = 1004,
}
}