Compare commits

...

62 Commits

Author SHA1 Message Date
胡宇飞 5c1f73fc18 update: 完善 AdjustId 的缓存机制和二次上报机制
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-17 09:57:44 +08:00
胡宇飞 8a81ed78b4 update: 修正 AdjustID 获取的回调和时机
--story=1020639 --user=yufei.hu 【Unity】-【BI】Firebase 数据新增上报用户属性 adjust_id https://www.tapd.cn/58098289/s/1157505

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-16 21:04:46 +08:00
胡宇飞 76fc4f5c26 update: 上报Firebase用户属性 adjust_id
--story=1020972 --user=yufei.hu 【中台】【SDK】新增 Firebase 用户属性打点: adjust_id https://www.tapd.cn/33527076/s/1157507

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-16 19:19:04 +08:00
胡宇飞 72bf076537 fix: 广告 SDK 优化广告重试时间 从最高间隔 8 秒 -> 64 秒, 减少无网时高频请求
--story=1020971 --user=yufei.hu 【中台】【ADS】优化 IV 和 RV 广告加载失败后的重试等待时间 https://www.tapd.cn/33527076/s/1157502

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-16 18:46:16 +08:00
胡宇飞 19a46fff1e fix: 为 iap_purchase 添加 sandbox 参数
--story=1020884 --user=yufei.hu 【中台】【BUG】补齐 iap_purchase 打点的 sandbox 参数 https://www.tapd.cn/33527076/s/1156167

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-10 17:12:57 +08:00
胡宇飞 4989926a47 fix: 修复广告 TCH 打点携带 sandbox 属性的BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-03 17:09:53 +08:00
胡宇飞 fe691235d6 update:新增 INTER 和 RV 广告关闭的回调参数
--story=1020788 --user=yufei.hu 【中台】【SDK】新增INTER 和 RV 广告关闭的回调参数 https://www.tapd.cn/33527076/s/1154205

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-02 14:08:09 +08:00
胡宇飞 dc47cec8bd fix: 修复一个 Json 解析的错误问题
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-01 20:41:24 +08:00
胡宇飞 2075f676b9 fix: 删除 RemoteConfigBase 内OnChange 回调的JSON 属性
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-01 18:27:47 +08:00
胡宇飞 660303e45d update: 添加 Amazon 测试接口
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-01 18:21:41 +08:00
胡宇飞 2a895b370e Merge branch 'hotfix/1.0.13' into dev
Signed-off-by: huyufei <yufei.hu@castbox.fm>

# Conflicts:
#	Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs
2024-07-01 14:25:18 +08:00
胡宇飞 c0c557b34e Merge branch 'hotfix/1.0.12.1' into hotfix/1.0.13
Signed-off-by: huyufei <yufei.hu@castbox.fm>

# Conflicts:
#	Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs
2024-07-01 13:46:27 +08:00
胡宇飞 edcc533d33 fix: Adjust 打点测试支付也会上报
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-01 12:36:55 +08:00
胡宇飞 3d9d027e89 fix: 修复 Adjust 事件重复上报以及为调用接口导致 revenue 没有正确上报的 BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-01 09:49:52 +08:00
胡宇飞 1256880b22 update: 更新 IOS 内的打包管线逻辑. UnityFramework 设置ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES ->NO
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-28 12:25:52 +08:00
胡宇飞 e77994d811 update: 更新 DeviceInfo 上报日志
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-27 21:13:57 +08:00
胡宇飞 85dc4a7ddc update: 新增打印 DeviceData 的日志
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-27 18:53:57 +08:00
胡宇飞 b7aacb61e4 fix: 修复 AdStatus 的显示 BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-27 18:53:48 +08:00
胡宇飞 8cc083410d update: 新增打印 DeviceData 的日志
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-27 18:53:24 +08:00
胡宇飞 81f37625c1 fix: 修复 AdStatus 的显示 BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-27 18:47:36 +08:00
胡宇飞 1a9481b094 update: Tch 001 和 Tch 02 打点, 添加 sandbox 参数
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-26 16:17:23 +08:00
胡宇飞 2174bcf1a3 fix: 修复 noti_perm 获取Allow 返回值
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-26 13:09:08 +08:00
胡宇飞 602662881c fix: 对齐 SDK 接口参数
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-26 13:09:04 +08:00
胡宇飞 e36f7483a3 fix: 修复打点时打日志报空的 BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-26 13:09:00 +08:00
胡宇飞 7cbc5ac148 fix: 修复打开 Channel 跳转的 BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-26 13:08:56 +08:00
胡宇飞 e8c17f4cf4 fix: 对齐 SDK 接口参数
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-25 10:35:03 +08:00
胡宇飞 e8b3112cc5 【中台】【广告】修复 Amazon 升级 Adapter 至 9.9.5.0 无法加载广告的问题
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-24 10:19:26 +08:00
胡宇飞 e0e78da9a3 update: 【中台】【广告】添加 MAX 平台的黑屏修复配置
--story=1020664 --user=yufei.hu 【中台】【广告】添加 MAX 平台的黑屏修复配置 https://www.tapd.cn/33527076/s/1152218

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 17:05:03 +08:00
胡宇飞 9e7e94ef36 update: 添加 iOS 标志位缓存, 请求时直接返回状态
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 16:46:42 +08:00
胡宇飞 80e38bf85d update: 优化 iOS Noti 打点逻辑
--story=1020629 --user=yufei.hu 【中台】【SDK】加入消息弹框管理,中台 noti_perm 打点逻辑优化 https://www.tapd.cn/33527076/s/1152197

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 16:18:18 +08:00
胡宇飞 cfe81b5583 fix: 修复提示 BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 14:13:58 +08:00
胡宇飞 34ae9e3f0b update: 更新Status默认值获取
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 14:01:33 +08:00
胡宇飞 eba48e4a75 update: 完善 Android 端的Noti 请求缓存逻辑
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 13:15:50 +08:00
胡宇飞 a53c153338 update: 更新 Android 请求状态
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 11:28:57 +08:00
胡宇飞 ffcb846a64 update: 添加构建管线, 修复授权回调BUG 逻辑
--story=1020629 --user=yufei.hu 【中台】【BUG】修复 noti_perm 属性设置不正确的问题 https://www.tapd.cn/33527076/s/1152099
2024-06-21 11:25:50 +08:00
胡宇飞 dbb56e5a32 update: 更新 Notification 的Android 实现
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-21 09:45:26 +08:00
胡宇飞 b6e038a027 update: 更新库引用
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-20 18:52:53 +08:00
胡宇飞 4e24169b25 update: 新增 Notification 服务和对一个的 noti 查询功能
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-20 18:20:38 +08:00
胡宇飞 9024b8171c Merge tag '1.0.13' into dev
Signed-off-by: huyufei <yufei.hu@castbox.fm>

# Conflicts:
#	Runtime/GuruCore/Runtime/Analytics/Analytics.TemplateDefine.cs
2024-06-19 19:31:28 +08:00
胡宇飞 f5747977ec Merge branch 'hotfix/1.0.13' 2024-06-19 19:30:40 +08:00
胡宇飞 d3028d9e3b update: 更新 Adjust 打点 Tag
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-18 17:00:15 +08:00
胡宇飞 a9a438b288 update: 更新自打点 Native库
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-18 12:44:31 +08:00
胡宇飞 523115bdb9 update: 优化代码结构
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-18 10:14:14 +08:00
胡宇飞 f360529552 update: 升级 GuruAnalytic 至 1.12.0, 新增 priority 参数, Android 端升级至 1.1.1
--story=1020598 --user=yufei.hu 【中台】【BI】升级自打点插件至 1.12.0, Native 接口添加 priority 参数和功能 https://www.tapd.cn/33527076/s/1151108

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-18 09:47:23 +08:00
胡宇飞 f96e506a19 update: 完善数据打印逻辑
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-17 19:10:53 +08:00
胡宇飞 c59f76aead update: 添加续订打点修改逻辑
--story=1020368 --user=yufei.hu 【中台】【IAP】续订打点修改逻辑 https://www.tapd.cn/33527076/s/1150863

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-17 15:01:09 +08:00
胡宇飞 ba90a32195 update: remove old firebase version hotfix
--story=1020565 --user=yufei.hu 【Unity】-【BI】IOS设备 升级Firebase/Analytics SDK 版本大于10.23.0 https://www.tapd.cn/33527076/s/1150717

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-15 08:27:58 +08:00
胡宇飞 4a5229780a fix: 修复 iap_clk打点没有product_id参数
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-04 21:05:52 +08:00
胡宇飞 9ec1038a00 update: 调整合并方法,转为统一的工具类
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-03 09:58:40 +08:00
胡宇飞 b1ee76ef0e fix: 打点逻辑修复合并 Extra 数据BUG
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-06-03 09:46:48 +08:00
胡宇飞 85dc66dc2d fix: 修复 打点 null parames 报错
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-31 17:25:43 +08:00
胡宇飞 6512ed327c update: 更新 SDK 注入逻辑
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-31 16:19:58 +08:00
胡宇飞 225197c867 update: 更新 Adujust Signature V3 原生库的部署路基,可选部署对应的文件
--story=1020267 --user=yufei.hu 【中台】【变现】添加 Adjust SDK 签名 https://www.tapd.cn/33527076/s/1148070

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-31 16:03:02 +08:00
胡宇飞 6df7530aa8 update: GuruAnalytics 双端升级 a:1.0.3 i:3.6.0, iOS 内置 POD 库
--story=1020280 --user=yufei.hu 【中台】【发行】将 GuruAnalytics 库升级到最新的版本, 将线上的 Pods 依赖改为 UPM 内部文件依赖 https://www.tapd.cn/33527076/s/1147993

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-31 13:33:59 +08:00
胡宇飞 d5c02418c7 update: 更新和完善 GuruConsent 本地 iOS 库的文件和 podspec 配置
--story=1020278 --user=yufei.hu 【中台】【发行】【iOS】 将 GuruConsent 库升级到最新的版本, 将线上的 Pods 依赖改为 UPM 内部文件依赖 https://www.tapd.cn/33527076/s/1147977

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-31 12:59:52 +08:00
胡宇飞 5882d213c3 update: 完善标准化打点接口, 补全自定义事件封装接口
--story=1020273 --user=yufei.hu 【中台】【BI】 中台打点标准化, 更新原有的打点和用户属性上报逻辑 https://www.tapd.cn/33527076/s/1147868

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-30 20:49:15 +08:00
胡宇飞 aca914ab02 fix: 更新打包管线的注入时机
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-30 13:49:23 +08:00
胡宇飞 7f4beb6e3f update: 新增 Firebase 依赖修复工具
Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-30 13:13:51 +08:00
胡宇飞 f641f1828c update: 升级 GuruConsent iOS 版本 -> 1.4.6
--story=1020278 --user=yufei.hu 【中台】【发行】【iOS】 将 GuruConsent 库升级到最新的版本, 将线上的 Pods 依赖改为 UPM 内部文件依赖 https://www.tapd.cn/33527076/s/1147539

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-30 10:56:14 +08:00
胡宇飞 073c0ef7ac update: 添加 Adjust SDK 签名校验需要的 .aar 和 .a 库
--story=1020267 --user=yufei.hu 【中台】【变现】添加 Adjust SDK 签名 https://www.tapd.cn/33527076/s/1147519

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-30 09:57:58 +08:00
胡宇飞 5974cb163a update: 添加 Adjust preinstall Tracker
--story=1020232 --user=yufei.hu 【中台】【SDK】添加 Adjust Preinstall Tracker 功能 https://www.tapd.cn/33527076/s/1147510

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-30 09:41:37 +08:00
胡宇飞 beb027bf76 update: 更新打点参数和打点库逻辑
--story=1020273 --user=yufei.hu 【中台】【BI】 中台打点标准化, 更新原有的打点和用户属性上报逻辑 https://www.tapd.cn/33527076/s/1147487

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-05-29 21:00:29 +08:00
149 changed files with 6747 additions and 587 deletions

View File

@ -1,8 +1,8 @@
{
"name": "Guru.Editor",
"rootNamespace": "",
"references": [
"Guru.Runtime"
"Guru.Runtime",
"Guru.Notification"
],
"includePlatforms": [
"Editor"

View File

@ -0,0 +1,119 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
#if UNITY_IOS
namespace Guru.Editor
{
using UnityEditor;
using System;
using System.IO;
using UnityEditor.Callbacks;
public class IOSPostBuild_Firebase_VersionFix
{
private static string[] fixedLibs = new string[]
{
"Firebase/Core",
"Firebase/Firestore",
"Firebase/Analytics",
"Firebase/Storage",
"Firebase/Auth",
"Firebase/Messaging",
"Firebase/Crashlytics",
"Firebase/DynamicLinks",
"Firebase/RemoteConfig",
};
private static string[] additionLibs = new string[]
{
"FirebaseFirestoreInternal",
};
private static string fixedVersion = "10.22.0";
private static string minTargetSdk = "8.0";
// <iosPod name="Firebase/Core" version="10.22.0" minTargetSdk="8.0" />
// Firebase 10.20.0 fixed to 10.22.0. BUT higher version do not open this ATTRIBUTE !!
// [PostProcessBuild(47)] // MAX POD Process Order
public static void PostBuildFixPodDeps(BuildTarget target, string projPath)
{
if (target != BuildTarget.iOS) return;
string podfile = Path.Combine(projPath, "Podfile");
if (!File.Exists(podfile)) return;
FixFirebasePodVersion(podfile);
}
private static void FixFirebasePodVersion(string podfile)
{
var lines = File.ReadAllLines(podfile).ToList();
string line = "";
int idx = 0;
bool isDirty = false;
List<string> needAdded = new List<string>(additionLibs);
for (int i = 0; i < lines.Count; i++)
{
line = lines[i];
if (line.Contains("pod '"))
{
idx = i;
}
foreach (var libName in fixedLibs)
{
if (line.Contains(libName))
{
lines[i] = FixOneFirebaseLibVersion(line, fixedVersion);
isDirty = true;
}
}
foreach (var libName in additionLibs)
{
if (line.Contains(libName))
{
needAdded.Remove(libName);
lines[i] = FixOneFirebaseLibVersion(line, fixedVersion);
isDirty = true;
}
}
}
if (needAdded.Count > 0)
{
// pod 'Firebase/DynamicLinks', '10.20.0'
foreach (var libName in needAdded)
{
idx++;
idx = Mathf.Min(idx, lines.Count - 1);
lines.Insert(idx, $"\tpod '{libName}', '{fixedVersion}'");
isDirty = true;
}
}
if(isDirty) File.WriteAllLines(podfile, lines);
}
private static string FixOneFirebaseLibVersion(string line, string fixedVersion)
{
if(!line.Contains("', '") || !line.Contains("pod")) return line;
string fixedLine = line.Substring(0, line.IndexOf("', '") + 4) + $"{fixedVersion}'";
return fixedLine;
}
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2aab28d86c8346e581c650a86cad060f
timeCreated: 1717039005

View File

@ -77,6 +77,8 @@ namespace Guru.Editor
var workdir = GetWorkdir();
var source = $"{workdir}/{SourceFileName}";
var toDir = Directory.GetParent(IosPrivacyInfoPath);
if (!toDir.Exists) toDir.Create();
if (File.Exists(source))
{
FileUtil.ReplaceFile(source, IosPrivacyInfoPath);

View File

@ -11,7 +11,7 @@ namespace Guru
/// </summary>
public class IOSPostBuildSwift
{
[PostProcessBuild(40)]
[PostProcessBuild(2000)]
public static void OnPostProcessBuild(BuildTarget target, string buildPath)
{
if (target != BuildTarget.iOS) return;
@ -43,7 +43,7 @@ namespace Guru
// 设置主项目的SWIFT构建支持
project.SetBuildProperty(mainTargetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
project.SetBuildProperty(frameworkTargetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "NO");
project.WriteToFile(projectPath);
}

View File

@ -6,6 +6,7 @@
"MaxSdk",
"MaxSdk.Scripts",
"Amazon",
"Amazon.Scripts",
"OpenWrapSDK",
"UniWebView-CSharp",
"UnityEngine.Purchasing",
@ -17,7 +18,9 @@
"Google.Play.Review",
"Google.Play.Common",
"Guru.LitJson",
"Unity.Advertisement.IosSupport"
"Unity.Advertisement.IosSupport",
"Unity.Notifications.Android",
"Unity.Notifications.iOS"
],
"includePlatforms": [],
"excludePlatforms": [],

3
Runtime/GuruAdjust.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2b993d660d0a48b1a098f5d42611b464
timeCreated: 1717034031

View File

@ -1,11 +1,14 @@
namespace Guru
{
using UnityEngine;
using com.adjust.sdk;
using System;
using System.Collections;
public static class AdjustService
public class AdjustService
{
public const string Version = "1.6.1";
public const string AdjustVersion = "4.38.0"; // Adjust SDK Version
@ -14,6 +17,8 @@ namespace Guru
public const string K_IAP_PURCHASE = "iap_purchase"; // 固定点位事件
public const string K_SUB_PURCHASE = "sub_purchase"; // 固定点位事件
private static Action<string> _onSessionSuccessCallback;
private static string _adId = "";
@ -45,7 +50,7 @@ namespace Guru
/// </summary>
/// <param name="appToken"></param>
/// <param name="fbAppId">MIR 追踪 AppID</param>
public static void StartService(string appToken, string fbAppId = "")
public static void StartService(string appToken, string fbAppId = "", Action<string> onSessionSuccess = null)
{
if (string.IsNullOrEmpty(appToken))
{
@ -53,12 +58,17 @@ namespace Guru
return;
}
_onSessionSuccessCallback = onSessionSuccess;
InstallEvent(IPMConfig.FIREBASE_ID, IPMConfig.IPM_DEVICE_ID); // 注入启动参数
AdjustEnvironment environment = GetAdjustEnvironment();
AdjustConfig config = new AdjustConfig(appToken, environment);
config.setLogLevel(GetAdjustLogLevel());
config.setDelayStart(DelayTime);
config.setPreinstallTrackingEnabled(true); // Adjust Preinstall
config.setSessionSuccessDelegate(OnSessionSuccessCallback); // SessionSuccess
#if UNITY_ANDROID
if (!string.IsNullOrEmpty(fbAppId)) config.setFbAppId(fbAppId); // 注入 MIR ID
@ -69,7 +79,7 @@ namespace Guru
config.setLogDelegate(log => LogI(LOG_TAG, log));
config.setEventSuccessDelegate(OnEventSuccessCallback);
config.setEventFailureDelegate(OnEventFailureCallback);
config.setSessionSuccessDelegate(OnSessionSuccessCallback);
config.setSessionFailureDelegate(OnSessionFailureCallback);
config.setAttributionChangedDelegate(OnAttributionChangedCallback);
#endif
@ -270,27 +280,10 @@ namespace Guru
private static void OnSessionSuccessCallback(AdjustSessionSuccess sessionSuccessData)
{
LogI(LOG_TAG,"Session tracked successfully!");
LogI(LOG_TAG,$"{LOG_TAG} --- Session tracked successfully!");
if (sessionSuccessData.Message != null)
{
LogI(LOG_TAG,"Message: " + sessionSuccessData.Message);
}
if (sessionSuccessData.Timestamp != null)
{
LogI(LOG_TAG,"Timestamp: " + sessionSuccessData.Timestamp);
}
if (sessionSuccessData.Adid != null)
{
LogI(LOG_TAG, "Adid: " + sessionSuccessData.Adid);
}
if (sessionSuccessData.JsonResponse != null)
{
LogI(LOG_TAG, "JsonResponse: " + sessionSuccessData.GetJsonResponse());
}
var adid = sessionSuccessData.Adid;
_onSessionSuccessCallback?.Invoke(adid);
}
private static void OnSessionFailureCallback(AdjustSessionFailure sessionFailureData)

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 43aa7a523e0141d4a7f64e16e5294d2d
timeCreated: 1717034046

View File

@ -0,0 +1,6 @@
{
"name": "GuruAdjust.Editor",
"includePlatforms": [
"Editor"
]
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 82f90ccbb33b42e9ad29f5f5a861dc4a
timeCreated: 1717137351

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 522c3aca8edd4e5bb3c57f54460df356
timeCreated: 1717137307

View File

@ -0,0 +1,81 @@
using System.IO;
using UnityEditor;
using UnityEngine;
namespace Guru
{
public class AdjustSignatureHelper
{
private static readonly string AndroidLib = "adjust-android-signature-3.13.1.aar";
private static readonly string iOSLib = "AdjustSigSdk.a";
public static void DeployFiles()
{
var dir = GetParentDir();
var files = $"{dir}/Files";
if (Directory.Exists(files))
{
string from, to;
bool res;
from = $"{files}/{AndroidLib}.f";
to = $"{Application.dataPath}/Plugins/Android/{AndroidLib}";
res = CopyFile(from, to);
if (res) Debug.Log($"Copy <color=#88ff00>{AndroidLib} to {to}</color> success...");
from = $"{files}/{AndroidLib}.f.meta";
to = $"{Application.dataPath}/Plugins/Android/{AndroidLib}.meta";
CopyFile(from, to);
from = $"{files}/{iOSLib}.f";
to = $"{Application.dataPath}/Plugins/iOS/{iOSLib}";
res = CopyFile(from, to);
if (res) Debug.Log($"Copy <color=#88ff00>{iOSLib} to {to}</color> success...");
from = $"{files}/{iOSLib}.f.meta";
to = $"{Application.dataPath}/Plugins/iOS/{iOSLib}.meta";
CopyFile(from, to);
AssetDatabase.Refresh();
}
else
{
Debug.Log($"<color=red>Files not found: {files}</color>");
}
}
private static string GetParentDir()
{
var guids = AssetDatabase.FindAssets(nameof(AdjustSignatureHelper));
if (guids != null && guids.Length > 0)
{
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
var dir = Directory.GetParent(Path.GetFullPath(path));
return dir.FullName;
}
return Path.GetFullPath($"{Application.dataPath}/../Packages/com.guru.unity.sdk.core/Runtime/GuruAdjust/Editor/Signature");
}
private static bool CopyFile(string source, string dest)
{
if (File.Exists(source))
{
if (!File.Exists(dest))
{
File.Delete(dest);
}
else
{
var destDir = Directory.GetParent(dest);
if(!destDir.Exists) destDir.Create();
}
File.Copy(source, dest, true);
return true;
}
Debug.Log($"<colo=red>File not found: {source}...</color>");
return false;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fcbb67d0a48d4b88bc8fd1430c4bbda4
timeCreated: 1717137470

View File

@ -0,0 +1,14 @@
namespace Guru
{
using UnityEditor;
public class AdjustSignatureMenuItem
{
[MenuItem("Guru/Adjust/SignatureV3/Deploy Libs")]
private static void CopyLibsToPlugins()
{
AdjustSignatureHelper.DeployFiles();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5f624e98bef44a399cf808a6aa7f5499
timeCreated: 1717137523

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f0f3f1cbb077474882bf4725c274efa9
timeCreated: 1717034059

View File

@ -0,0 +1,80 @@
fileFormatVersion: 2
guid: 7dfc92df774d347749c60047aaa3da41
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 0
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: None
- first:
iPhone: iOS
second:
enabled: 1
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 07cf2335bd298401b8015718fca55265
guid: 07dda3e28d25446c3bb0924d7fc21cb4
PluginImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -79,7 +79,7 @@ namespace Guru
/// <summary>
/// 初始化平台
/// </summary>
public void Initialize()
public void Initialize(bool isDebug = false)
{
#if UNITY_EDITOR
Debug.Log($"<color=orange>=== Amazon will not init on Editor ===</color>");
@ -93,11 +93,9 @@ namespace Guru
// 初始化Amazon
Amazon.Initialize (AmazonAppID);
Amazon.SetAdNetworkInfo(new AdNetworkInfo(DTBAdNetwork.MAX));
#if UNITY_EDITOR || DEBUG
Amazon.EnableTesting (true); // Make sure to take this off when going live.
#else
Amazon.EnableLogging (false);
#endif
Debug.Log($"[Ads] --- Amazon init start isDebug:{isDebug}, AmazonID:{AmazonAppID}");
Amazon.EnableTesting (isDebug); // Make sure to take this off when going live.
Amazon.EnableLogging (isDebug);
#if UNITY_IOS
Amazon.SetAPSPublisherExtendedIdFeatureEnabled(true);

View File

@ -50,7 +50,7 @@ namespace Guru
* before it can request an ad using OpenWrap SDK.
* The storeURL is the URL where users can download your app from the App Store/Google Play Store.
*/
public void Initialize()
public void Initialize(bool isDebug = false)
{
#if UNITY_EDITOR
Debug.Log($"<color=orange>=== PubMatic will not init on Editor ===</color>");

View File

@ -20,16 +20,11 @@ Sample Dependencies.xml:
<androidPackage spec="com.squareup.retrofit2:converter-gson:2.7.1" />
<androidPackage spec="com.squareup.retrofit2:adapter-rxjava2:2.7.1" />
<androidPackage spec="com.squareup.okhttp3:okhttp:4.9.3" />
<!-- <androidPackage spec="com.mapzen:on-the-road:0.8.1" />-->
<!-- <androidPackage spec="com.squareup.retrofit2:retrofit:2.7.1" />-->
</androidPackages>
<iosPods>
<iosPod name="GuruAnalyticsLib" version="0.3.3" bitcodeEnabled="false">
<sources>
<source>git@github.com:castbox/GuruSpecs.git</source>
</sources>
</iosPod>
<iosPod name="GuruAnalyticsLib" bitcodeEnabled="false" path="Packages/com.guru.unity.sdk.core/Runtime/GuruAnalytics/Plugins/iOS" />
<iosPod name="JJException" bitcodeEnabled="false" />
</iosPods>
</dependencies>

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: b0e58ca75957d470cbc4951b34f31bf8
guid: 32eda01e213614348899eefe856392d3
PluginImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -0,0 +1,32 @@
fileFormatVersion: 2
guid: 66c5f430ab9654ef4a2376e71aa04bca
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings: {}
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e53e2bfca0fd949559d383674081f737
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 502f707bde2a24fadb6ec09ac5a3593f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
</dict>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
</dict>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
<string>8FFB.1</string>
<string>3D61.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
</dict>
</array>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array>
<string></string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,85 @@
fileFormatVersion: 2
guid: 63885139be48c43ae8cd3b1c403a686f
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d30316515c87a4421bc7032194f888e1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e0086576c1ac64707b788bef25dc9316
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,158 @@
//
// GuruAnalytics.swift
// GuruAnalytics_iOS
//
// Created by mayue on 2022/11/4.
//
import Foundation
public class GuruAnalytics: NSObject {
internal static var uploadPeriodInSecond: Double = 60
internal static var batchLimit: Int = 25
internal static var eventExpiredSeconds: Double = 7 * 24 * 60 * 60
internal static var initializeTimeout: Double = 5
internal static var saasXAPPID = ""
internal static var saasXDEVICEINFO = ""
internal static var loggerDebug = true
internal static var enableUpload = true
///
/// - Parameters:
/// - uploadPeriodInSecond:
/// - batchLimit:
/// - eventExpiredSeconds:
/// - initializeTimeout: user id/device id/firebase pseudo id
/// - saasXAPPID: headerX-APP-ID
/// - saasXDEVICEINFO: headerX-DEVICE-INFO
/// - loggerDebug: debug
@objc
public class func initializeLib(uploadPeriodInSecond: Double = 60,
batchLimit: Int = 25,
eventExpiredSeconds: Double = 7 * 24 * 60 * 60,
initializeTimeout: Double = 5,
saasXAPPID: String,
saasXDEVICEINFO: String,
loggerDebug: Bool = true) {
Self.uploadPeriodInSecond = uploadPeriodInSecond
Self.batchLimit = batchLimit
Self.eventExpiredSeconds = eventExpiredSeconds
Self.initializeTimeout = initializeTimeout
Self.saasXAPPID = saasXAPPID
Self.saasXDEVICEINFO = saasXDEVICEINFO
Self.loggerDebug = loggerDebug
_ = Manager.shared
}
/// event
@objc
public class func logEvent(_ name: String, parameters: [String : Any]?) {
Manager.shared.logEvent(name, parameters: parameters)
}
/// IDuid
@objc
public class func setUserID(_ userID: String?) {
setUserProperty(userID, forName: .uid)
}
/// IDIDiOSIDFVUUIDAndroidandroidID
@objc
public class func setDeviceId(_ deviceId: String?) {
setUserProperty(deviceId, forName: .deviceId)
}
/// adjust_idadjust
@objc
public class func setAdjustId(_ adjustId: String?) {
setUserProperty(adjustId, forName: .adjustId)
}
/// 广 ID/广 (IDFA)
@objc
public class func setAdId(_ adId: String?) {
setUserProperty(adId, forName: .adId)
}
/// pseudo_id
@objc
public class func setFirebaseId(_ firebaseId: String?) {
setUserProperty(firebaseId, forName: .firebaseId)
}
/// screen name
@objc
public class func setScreen(_ name: String) {
Manager.shared.setScreen(name)
}
/// userproperty
@objc
public class func setUserProperty(_ value: String?, forName name: String) {
Manager.shared.setUserProperty(value ?? "", forName: name)
}
/// userproperty
@objc
public class func removeUserProperties(forNames names: [String]) {
Manager.shared.removeUserProperties(forNames: names)
}
/// eventszip
/// zipCastbox123
@available(*, deprecated, renamed: "eventsLogsDirectory", message: "废弃使用eventsLogsDirectory方法获取日志文件目录URL")
@objc
public class func eventsLogsArchive(_ callback: @escaping (_ url: URL?) -> Void) {
Manager.shared.eventsLogsArchive(callback)
}
/// events
@objc
public class func eventsLogsDirectory(_ callback: @escaping (_ url: URL?) -> Void) {
Manager.shared.eventsLogsDirURL(callback)
}
/// events
/// host: abc.bbb.com, "https://abc.bbb.com", "http://abc.bbb.com"
@objc
public class func setEventsUploadEndPoint(host: String?) {
UserDefaults.eventsServerHost = host
}
/// events
/// - Parameter callback:
/// - callback parameters:
/// - uploadedEventsCount: event
/// - loggedEventsCount: event
@objc
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
public class func debug_eventsStatistics(_ callback: @escaping (_ uploadedEventsCount: Int, _ loggedEventsCount: Int) -> Void) {
Manager.shared.debug_eventsStatistics(callback)
}
///
/// - Parameter reportCallback:
/// - callback parameters:
/// - eventCode:
/// - info:
@objc
public class func registerInternalEventObserver(reportCallback: @escaping (_ eventCode: Int, _ info: String) -> Void) {
Manager.shared.registerInternalEventObserver(reportCallback: reportCallback)
}
/// user property
@objc
public class func getUserProperties() -> [String : String] {
return Manager.shared.getUserProperties()
}
/// true
/// true -
/// false -
@objc
public class func setEnableUpload(isOn: Bool = true) -> Void {
enableUpload = isOn
}
}

View File

@ -0,0 +1,85 @@
fileFormatVersion: 2
guid: 669b744f21d994fd3b6fb7aeb95b0669
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c6cbae57da78c46c7918b2bfd24d7335
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a9b9cc55c438041a7ae3ce46bd896d8d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,228 @@
//
// DBEntities.swift
// Alamofire
//
// Created by mayue on 2022/11/4.
//
import Foundation
import CryptoSwift
internal enum Entity {
}
extension Entity {
struct EventRecord: Codable {
enum Priority: Int, Codable {
case EMERGENCE = 0
case HIGH = 5
case DEFAULT = 10
case LOW = 15
}
enum TransitionStatus: Int, Codable {
case idle = 0
case instransition = 1
}
let recordId: String
let eventName: String
let eventJson: String
///
let timestamp: Int64
let priority: Priority
let transitionStatus: TransitionStatus
init(eventName: String, event: Entity.Event, priority: Priority = .DEFAULT, transitionStatus: TransitionStatus = .idle) {
let now = Date()
let eventJson = event.asString ?? ""
if eventJson.isEmpty {
cdPrint("[WARNING] error for convert event to json")
}
self.recordId = "\(eventName)\(eventJson)\(now.timeIntervalSince1970)\(Int.random(in: Int.min...Int.max))".md5()
self.eventName = eventName
self.eventJson = eventJson
self.timestamp = event.timestamp
self.priority = priority
self.transitionStatus = transitionStatus
}
init(recordId: String, eventName: String, eventJson: String, timestamp: Int64, priority: Int, transitionStatus: Int) {
self.recordId = recordId
self.eventName = eventName
self.eventJson = eventJson
self.timestamp = timestamp
self.priority = .init(rawValue: priority) ?? .DEFAULT
self.transitionStatus = .init(rawValue: transitionStatus) ?? .idle
}
enum CodingKeys: String, CodingKey {
case recordId
case eventName
case eventJson
case timestamp
case priority
case transitionStatus
}
static func createTableSql(with name: String) -> String {
return """
CREATE TABLE IF NOT EXISTS \(name)(
\(CodingKeys.recordId.rawValue) TEXT UNIQUE NOT NULL PRIMARY KEY,
\(CodingKeys.eventName.rawValue) TEXT NOT NULL,
\(CodingKeys.eventJson.rawValue) TEXT NOT NULL,
\(CodingKeys.timestamp.rawValue) INTEGER,
\(CodingKeys.priority.rawValue) INTEGER,
\(CodingKeys.transitionStatus.rawValue) INTEGER);
"""
}
func insertSql(to tableName: String) -> String {
return "INSERT INTO \(tableName) VALUES ('\(recordId)', '\(eventName)', '\(eventJson)', '\(timestamp)', '\(priority.rawValue)', '\(transitionStatus.rawValue)')"
}
}
}
extension Entity {
struct Event: Codable {
///
let timestamp: Int64
let event: String
let userInfo: UserInfo
let param: [String: EventValue]
let properties: [String: String]
let eventId: String
enum CodingKeys: String, CodingKey {
case timestamp
case userInfo = "info"
case event, param, properties
case eventId
}
init(timestamp: Int64, event: String, userInfo: UserInfo, parameters: [String : Any], properties: [String : String]) throws {
guard let normalizedEvent = Self.normalizeKey(event),
normalizedEvent == event else {
cdPrint("drop event because of illegal event name: \(event)")
cdPrint("standard: https://developers.google.com/android/reference/com/google/firebase/analytics/FirebaseAnalytics.Event")
throw NSError(domain: "cunstrcting event error", code: 0, userInfo: [NSLocalizedDescriptionKey : "illegal event name: \(event)"])
}
self.eventId = UUID().uuidString.lowercased()
self.timestamp = timestamp
self.event = normalizedEvent
self.userInfo = userInfo
self.param = Self.normalizeParameters(parameters)
self.properties = properties
}
static let maxParametersCount = 25
static let maxKeyLength = 40
static let maxParameterStringValueLength = 128
static func normalizeParameters(_ parameters: [String : Any]) -> [String : EventValue] {
var params = [String : EventValue]()
var count = 0
parameters.sorted(by: { $0.key < $1.key }).forEach({ key, value in
guard count < maxParametersCount else {
cdPrint("too many parameters")
cdPrint("standard: https://developers.google.com/android/reference/com/google/firebase/analytics/FirebaseAnalytics.Event")
return
}
guard let normalizedKey = normalizeKey(key),
normalizedKey == key else {
cdPrint("drop event parameter because of illegal key: \(key)")
cdPrint("standard: https://developers.google.com/android/reference/com/google/firebase/analytics/FirebaseAnalytics.Event")
return
}
if let value = value as? String {
params[normalizedKey] = Entity.EventValue(stringValue: String(value.prefix(maxParameterStringValueLength)))
} else if let value = value as? Int {
params[normalizedKey] = Entity.EventValue(longValue: Int64(value))
} else if let value = value as? Int64 {
params[normalizedKey] = Entity.EventValue(longValue: value)
} else if let value = value as? Double {
params[normalizedKey] = Entity.EventValue(doubleValue: value)
} else {
params[normalizedKey] = Entity.EventValue(stringValue: String("\(value)".prefix(maxParameterStringValueLength)))
}
count += 1
})
return params
}
static func normalizeKey(_ key: String) -> String? {
var mutableKey = key
while let first = mutableKey.first,
!first.isLetter {
_ = mutableKey.removeFirst()
}
var normalizedKey = ""
var count = 0
mutableKey.forEach { c in
guard count < maxKeyLength,
c.isAlphabetic || c.isDigit || c == "_" else { return }
normalizedKey.append(c)
count += 1
}
return normalizedKey.isEmpty ? nil : normalizedKey
}
}
///
struct UserInfo: Codable {
///IDuid
let uid: String?
///IDIDiOSIDFVUUIDAndroidandroidID
let deviceId: String?
///adjust_idadjust
let adjustId: String?
///广 ID/广 (IDFA)
let adId: String?
///pseudo_id
let firebaseId: String?
enum CodingKeys: String, CodingKey {
case deviceId
case uid
case adjustId
case adId
case firebaseId
}
}
//
struct EventValue: Codable {
let stringValue: String? //
let longValue: Int64? //
let doubleValue: Double? // APPJSON
init(stringValue: String? = nil, longValue: Int64? = nil, doubleValue: Double? = nil) {
self.stringValue = stringValue
self.longValue = longValue
self.doubleValue = doubleValue
}
enum CodingKeys: String, CodingKey {
case stringValue = "s"
case longValue = "i"
case doubleValue = "d"
}
}
}
extension Entity {
struct SystemTimeResult: Codable {
let data: Int64
}
}

View File

@ -0,0 +1,85 @@
fileFormatVersion: 2
guid: f63b0ff90afd0409781c1266dc618875
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bb7dde11f0ad6496ca231330136b7b61
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,391 @@
//
// Database.swift
// GuruAnalytics_iOS
//
// Created by mayue on 2022/11/4.
// Copyright © 2022 Guru Network Limited. All rights reserved.
//
import Foundation
import RxSwift
import RxCocoa
import FMDB
internal class Database {
typealias PropertyName = GuruAnalytics.PropertyName
enum TableName: String, CaseIterable {
case event = "event"
}
private let dbIOQueue = DispatchQueue.init(label: "com.guru.analytics.db.io.queue", qos: .userInitiated)
private let dbQueueRelay = BehaviorRelay<FMDatabaseQueue?>(value: nil)
private let bag = DisposeBag()
///
private let currentDBVersion = DBVersionHistory.v_3
private var dbVersion: Database.DBVersionHistory {
get {
if let v = UserDefaults.defaults?.value(forKey: UserDefaults.dbVersionKey) as? String,
let dbV = Database.DBVersionHistory.init(rawValue: v) {
return dbV
} else {
return .initialVersion
}
}
set {
UserDefaults.defaults?.set(newValue.rawValue, forKey: UserDefaults.dbVersionKey)
}
}
internal init() {
dbIOQueue.async { [weak self] in
guard let `self` = self else { return }
let applicationSupportPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory,
.userDomainMask,
true).last! + "/GuruAnalytics"
if !FileManager.default.fileExists(atPath: applicationSupportPath) {
do {
try FileManager.default.createDirectory(atPath: applicationSupportPath, withIntermediateDirectories: true)
} catch {
assertionFailure("create db path error: \(error)")
}
}
let dbPath = applicationSupportPath + "/analytics.db"
let queue = FMDatabaseQueue(url: URL(fileURLWithPath: dbPath))!
cdPrint("database path: \(queue.path ?? "")")
self.createEventTable(in: queue)
.filter { $0 }
.flatMap { _ in
self.migrateDB(in: queue).asMaybe()
}
.flatMap({ _ in
self.resetAllTransitionStatus(in: queue).asMaybe()
})
.subscribe(onSuccess: { _ in
self.dbQueueRelay.accept(queue)
})
.disposed(by: self.bag)
}
}
}
internal extension Database {
func addEventRecords(_ events: Entity.EventRecord) -> Single<Void> {
cdPrint(#function)
return mapTransactionToSingle { (db) in
try db.executeUpdate(events.insertSql(to: TableName.event.rawValue), values: nil)
}
.do(onSuccess: { [weak self] (_) in
guard let `self` = self else { return }
NotificationCenter.default.post(name: self.tableUpdateNotification(TableName.event.rawValue), object: nil)
})
}
func fetchEventRecordsToUpload(limit: Int) -> Single<[Entity.EventRecord]> {
return mapTransactionToSingle { (db) in
let querySQL: String =
"""
SELECT * FROM \(TableName.event.rawValue)
WHERE \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) IS NULL
OR \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) != \(Entity.EventRecord.TransitionStatus.instransition.rawValue)
ORDER BY \(Entity.EventRecord.CodingKeys.priority.rawValue) ASC, \(Entity.EventRecord.CodingKeys.timestamp.rawValue) ASC
LIMIT \(limit)
"""
cdPrint(#function + "query sql: \(querySQL)")
let results = try db.executeQuery(querySQL, values: nil) //[ASC | DESC]
var t: [Entity.EventRecord] = []
while results.next() {
guard let recordId = results.string(forColumnIndex: 0),
let eventName = results.string(forColumnIndex: 1),
let eventJson = results.string(forColumnIndex: 2) else {
continue
}
let priority: Int = results.columnIsNull(Entity.EventRecord.CodingKeys.priority.rawValue) ?
Entity.EventRecord.Priority.DEFAULT.rawValue : Int(results.int(forColumn: Entity.EventRecord.CodingKeys.priority.rawValue))
let ts: Int = results.columnIsNull(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) ?
Entity.EventRecord.TransitionStatus.idle.rawValue : Int(results.int(forColumn: Entity.EventRecord.CodingKeys.transitionStatus.rawValue))
let record = Entity.EventRecord(recordId: recordId, eventName: eventName, eventJson: eventJson,
timestamp: results.longLongInt(forColumn: Entity.EventRecord.CodingKeys.timestamp.rawValue),
priority: priority, transitionStatus: ts)
t.append(record)
}
results.close()
try t.forEach { record in
let updateSQL =
"""
UPDATE \(TableName.event.rawValue)
SET \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) = \(Entity.EventRecord.TransitionStatus.instransition.rawValue)
WHERE \(Entity.EventRecord.CodingKeys.recordId.rawValue) = '\(record.recordId)'
"""
try db.executeUpdate(updateSQL, values: nil)
}
return t
}
}
func deleteEventRecords(_ recordIds: [String]) -> Single<Void> {
guard !recordIds.isEmpty else {
return .just(())
}
cdPrint(#function + "\(recordIds)")
return mapTransactionToSingle { db in
try recordIds.forEach { item in
try db.executeUpdate("DELETE FROM \(TableName.event.rawValue) WHERE \(Entity.EventRecord.CodingKeys.recordId.rawValue) = '\(item)'", values: nil)
}
}
.do(onSuccess: { [weak self] (_) in
guard let `self` = self else { return }
NotificationCenter.default.post(name: self.tableUpdateNotification(TableName.event.rawValue), object: nil)
}, onError: { error in
cdPrint("\(#function) error: \(error)")
})
}
func removeOutdatedEventRecords(earlierThan: Int64) -> Single<Void> {
return mapTransactionToSingle { db in
let sql = """
DELETE FROM \(TableName.event.rawValue)
WHERE \(Entity.EventRecord.CodingKeys.timestamp.rawValue) < \(earlierThan)
"""
try db.executeUpdate(sql, values: nil)
}
.do(onSuccess: { [weak self] (_) in
guard let `self` = self else { return }
NotificationCenter.default.post(name: self.tableUpdateNotification(TableName.event.rawValue), object: nil)
}, onError: { error in
cdPrint("\(#function) error: \(error)")
})
}
func resetTransitionStatus(for recordIds: [String]) -> Single<Void> {
guard !recordIds.isEmpty else {
return .just(())
}
cdPrint(#function + "\(recordIds)")
return mapTransactionToSingle { db in
try recordIds.forEach { item in
let updateSQL =
"""
UPDATE \(TableName.event.rawValue)
SET \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) = \(Entity.EventRecord.TransitionStatus.idle.rawValue)
WHERE \(Entity.EventRecord.CodingKeys.recordId.rawValue) = '\(item)'
"""
try db.executeUpdate(updateSQL, values: nil)
}
}
.do(onSuccess: { [weak self] (_) in
guard let `self` = self else { return }
NotificationCenter.default.post(name: self.tableUpdateNotification(TableName.event.rawValue), object: nil)
}, onError: { error in
cdPrint("\(#function) error: \(error)")
})
}
func uploadableEventRecordCount() -> Single<Int> {
return mapTransactionToSingle { db in
let querySQL =
"""
SELECT count(*) as Count FROM \(TableName.event.rawValue)
WHERE \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) IS NULL
OR \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) != \(Entity.EventRecord.TransitionStatus.instransition.rawValue)
"""
let result = try db.executeQuery(querySQL, values: nil)
var count = 0
while result.next() {
count = Int(result.int(forColumn: "Count"))
}
result.parentDB = nil
result.close()
return count
}
}
func uploadableEventRecordCountOb() -> Observable<Int> {
return NotificationCenter.default.rx.notification(tableUpdateNotification(TableName.event.rawValue))
.startWith(Notification(name: tableUpdateNotification(TableName.event.rawValue)))
.flatMap({ [weak self] (_) -> Observable<Int> in
guard let `self` = self else {
return Observable.empty()
}
return self.uploadableEventRecordCount().asObservable()
})
}
func hasFgEventRecord() -> Single<Bool> {
return mapTransactionToSingle { db in
let querySQL =
"""
SELECT count(*) as Count FROM \(TableName.event.rawValue)
WHERE \(Entity.EventRecord.CodingKeys.eventName.rawValue) == '\(GuruAnalytics.fgEvent.name)'
"""
let result = try db.executeQuery(querySQL, values: nil)
var count = 0
while result.next() {
count = Int(result.int(forColumn: "Count"))
}
result.parentDB = nil
result.close()
return count > 0
}
}
}
private extension Database {
func createEventTable(in queue: FMDatabaseQueue) -> Single<Bool> {
return mapTransactionToSingle(queue: queue) { db in
db.executeStatements(Entity.EventRecord.createTableSql(with: TableName.event.rawValue))
}
.do(onSuccess: { result in
cdPrint("createEventTable result: \(result)")
}, onError: { error in
cdPrint("createEventTable error: \(error)")
})
}
func mapTransactionToSingle<T>(_ transaction: @escaping ((FMDatabase) throws -> T)) -> Single<T> {
return dbQueueRelay.compactMap({ $0 })
.take(1)
.asSingle()
.flatMap { [unowned self] queue -> Single<T> in
return self.mapTransactionToSingle(queue: queue, transaction)
}
}
func mapTransactionToSingle<T>(queue: FMDatabaseQueue, _ transaction: @escaping ((FMDatabase) throws -> T)) -> Single<T> {
return Single<T>.create { [weak self] (subscriber) -> Disposable in
self?.dbIOQueue.async {
queue.inDeferredTransaction { (db, rollback) in
do {
let data = try transaction(db)
subscriber(.success(data))
} catch {
rollback.pointee = true
cdPrint("inDeferredTransaction failed: \(error.localizedDescription)")
subscriber(.failure(error))
}
}
}
return Disposables.create()
}
}
func tableUpdateNotification(_ tableName: String) -> Notification.Name {
return Notification.Name("Guru.Analytics.DB.Table.update-\(tableName)")
}
func migrateDB(in queue: FMDatabaseQueue) -> Single<Void> {
return mapTransactionToSingle(queue: queue) { [weak self] db in
guard let `self` = self else { return }
while let nextVersion = self.dbVersion.nextVersion,
self.dbVersion < self.currentDBVersion {
switch nextVersion {
case .v_1:
()
case .v_2:
/// v_1 -> v_2
/// eventpriority
if !db.columnExists(Entity.EventRecord.CodingKeys.priority.rawValue, inTableWithName: TableName.event.rawValue) {
db.executeStatements("""
ALTER TABLE \(TableName.event.rawValue)
ADD \(Entity.EventRecord.CodingKeys.priority.rawValue) Integer DEFAULT \(Entity.EventRecord.Priority.DEFAULT.rawValue)
""")
}
case .v_3:
/// v_2 -> v_3
/// eventtransitionStatus
if !db.columnExists(Entity.EventRecord.CodingKeys.transitionStatus.rawValue, inTableWithName: TableName.event.rawValue) {
db.executeStatements("""
ALTER TABLE \(TableName.event.rawValue)
ADD \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) Integer DEFAULT \(Entity.EventRecord.TransitionStatus.idle.rawValue)
""")
}
}
self.dbVersion = nextVersion
}
}
.do(onError: { error in
cdPrint("migrate db error: \(error)")
})
}
func resetAllTransitionStatus(in queue: FMDatabaseQueue) -> Single<Void> {
return mapTransactionToSingle(queue: queue) { db in
let updateSQL =
"""
UPDATE \(TableName.event.rawValue)
SET \(Entity.EventRecord.CodingKeys.transitionStatus.rawValue) = \(Entity.EventRecord.TransitionStatus.idle.rawValue)
"""
try db.executeUpdate(updateSQL, values: nil)
}
.do(onSuccess: { [weak self] (_) in
guard let `self` = self else { return }
NotificationCenter.default.post(name: self.tableUpdateNotification(TableName.event.rawValue), object: nil)
}, onError: { error in
cdPrint("\(#function) error: \(error)")
})
}
}
fileprivate extension Array where Element == String {
var joinedStringForSQL: String {
return self.map { "'\($0)'" }.joined(separator: ",")
}
}
private extension Database {
enum DBVersionHistory: String, Comparable {
case v_1
case v_2
case v_3
}
}
extension Database.DBVersionHistory {
static func < (lhs: Database.DBVersionHistory, rhs: Database.DBVersionHistory) -> Bool {
return lhs.versionNumber < rhs.versionNumber
}
var versionNumber: Int {
return Int(String(self.rawValue.split(separator: "_")[1])) ?? 1
}
var nextVersion: Self? {
return .init(rawValue: "v_\(versionNumber + 1)")
}
static let initialVersion: Self = .v_1
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: cc72ba03590e04e4db8e3d9eb675d0d2
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,681 @@
//
// Manager.swift
// GuruAnalytics_iOS
//
// Created by on 16/11/22.
//
import Foundation
import RxCocoa
import RxSwift
internal class Manager {
// MARK: - temporary, will be removed soon
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
private var loggedEventsCount: Int = 0
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
private func accumulateLoggedEventsCount(_ count: Int) {
loggedEventsCount += count
}
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
private var uploadedEventsCount: Int = 0
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
private func accumulateUploadedEventsCount(_ count: Int) {
uploadedEventsCount += count
}
@available(*, deprecated, message: "used for debug, will be removed on any future released versions")
internal func debug_eventsStatistics(_ callback: @escaping (_ uploadedEventsCount: Int, _ loggedEventsCount: Int) -> Void) {
callback(uploadedEventsCount, loggedEventsCount)
}
// MARK: - internal members
internal static let shared = Manager()
/// 11
private var scheduleInterval: TimeInterval = GuruAnalytics.uploadPeriodInSecond
/// 251
private var numberOfCountPerConsume: Int = GuruAnalytics.batchLimit
/// event7
private var eventExpiredIntervel: TimeInterval = GuruAnalytics.eventExpiredSeconds
private var initializeTimeout: Double = GuruAnalytics.initializeTimeout
///
internal var serverNowMs: Int64 { serverInitialMs + (Date.absoluteTimeMs - serverSyncedAtAbsoluteMs)}
// MARK: - private members
private typealias PropertyName = GuruAnalytics.PropertyName
private let bag = DisposeBag()
private let db = Database()
private let ntwkMgr = NetworkManager()
/// background key disposeable
private var taskKeyDisposableMap: [Int: Disposable] = [:]
///
private var maxEventFetchingCount: Int = 100
///
private let workQueue = DispatchQueue.init(label: "com.guru.analytics.manager.work.queue", qos: .userInitiated)
///
private lazy var rxNetworkScheduler = SerialDispatchQueueScheduler(qos: .default, internalSerialQueueName: "com.guru.analytics.manager.rx.network.queue")
private lazy var rxConsumeScheduler = SerialDispatchQueueScheduler(qos: .default, internalSerialQueueName: "com.guru.analytics.manager.rx.consume.queue")
private lazy var rxWorkScheduler = SerialDispatchQueueScheduler.init(queue: workQueue, internalSerialQueueName: "com.guru.analytics.manager.rx.work.queue")
private let bgWorkQueue = DispatchQueue.init(label: "com.guru.analytics.manager.background.work.queue", qos: .background)
private lazy var rxBgWorkScheduler = SerialDispatchQueueScheduler.init(queue: bgWorkQueue, internalSerialQueueName: "com.guru.analytics.manager.background.work.queue")
/// event
private let outdatedEventsCleared = BehaviorSubject(value: false)
///
private var serverInitialMs = Date().msSince1970 {
didSet {
serverSyncedAtAbsoluteMs = Date.absoluteTimeMs
}
}
private var serverSyncedAtAbsoluteMs = Date.absoluteTimeMs
private let startAt = Date()
///
private let _serverTimeSynced = BehaviorRelay(value: false)
private var serverNowMsSingle: Single<Int64> {
guard _serverTimeSynced.value == false else {
return .just(serverNowMs)
}
return _serverTimeSynced.observe(on: rxNetworkScheduler)
.filter { $0 }
.take(1).asSingle()
.timeout(.seconds(10), scheduler: rxNetworkScheduler)
.catchAndReturn(false)
.map({ [weak self] _ in
return self?.serverNowMs ?? 0
})
}
/// fg
private var fgStartAtAbsoluteMs = Date.absoluteTimeMs
private var fgAccumulateTimer: Disposable? = nil
/// user property
private var userProperty: Observable<[String : String]> {
let p = userPropertyUpdated.startWith(()).observe(on: rxWorkScheduler).flatMap { [weak self] _ -> Observable<[String : String]> in
guard let `self` = self else { return .just([:]) }
return .create({ subscriber in
subscriber.onNext(self._userProperty)
subscriber.onCompleted()
// debugPrint("userProperty thread queueName: \(Thread.current.queueName)")
return Disposables.create()
})
}
let latency = self.initializeTimeout - Date().timeIntervalSince(self.startAt)
let intLatency = Int(latency)
guard latency > 0 else {
return p
}
return p.filter({ property in
/// userproperty
/// PropertyName.deviceId
/// PropertyName.uid
/// PropertyName.firebaseId
guard let deviceId = property[PropertyName.deviceId.rawValue], !deviceId.isEmpty,
let uid = property[PropertyName.uid.rawValue], !uid.isEmpty,
let firebaseId = property[PropertyName.firebaseId.rawValue], !firebaseId.isEmpty else {
return false
}
return true
})
.timeout(.milliseconds(intLatency), scheduler: rxNetworkScheduler)
.catch { _ in
return p
}
}
private var _userProperty: [String : String] = [:] {
didSet {
userPropertyUpdated.onNext(())
}
}
private var userPropertyUpdated = PublishSubject<Void>()
///
private let syncServerTrigger = PublishSubject<Void>()
/// event
private var pollingUploadTask: Disposable?
///
private let reschedulePollingTrigger = BehaviorSubject(value: ())
/// eventslogger
private lazy var eventsLogger: LoggerManager = {
let l = LoggerManager(logCategoryName: "eventLogs")
return l
}()
///
private typealias InternalEventReporter = ((_ eventCode: Int, _ info: String) -> Void)
private var internalEventReporter: InternalEventReporter?
private init() {
// first open
logFirstOpenIfNeeded()
//
setupOberving()
//
clearOutdatedEventsIfNeeded()
//
setupPollingUpload()
// fg
logFirstFgEvent()
ntwkMgr.networkErrorReporter = self
}
}
// MARK: - internal functions
internal extension Manager {
func logEvent(_ eventName: String, parameters: [String : Any]?, priority: Entity.EventRecord.Priority = .DEFAULT) {
_ = _logEvent(eventName, parameters: parameters, priority: priority)
.subscribe()
.disposed(by: bag)
}
func setUserProperty(_ value: String, forName name: String) {
eventsLogger.verbose(#function + "name: \(name) value: \(value)")
workQueue.async { [weak self] in
self?._userProperty[name] = value
}
}
func removeUserProperties(forNames names: [String]) {
eventsLogger.verbose(#function + "names: \(names)")
workQueue.async { [weak self] in
guard let `self` = self else { return }
var temp = self._userProperty
for name in names {
temp.removeValue(forKey: name)
}
self._userProperty = temp
}
}
func setScreen(_ name: String) {
setUserProperty(name, forName: PropertyName.screen.rawValue)
}
private func constructEvent(_ eventName: String,
parameters: [String : Any]?,
timestamp: Int64,
priority: Entity.EventRecord.Priority) -> Single<Entity.EventRecord> {
return userProperty.take(1).observe(on: rxWorkScheduler).asSingle().flatMap { p in
.create { subscriber in
do {
debugPrint("userProperty thread queueName: \(Thread.current.queueName) count: \(p.count)")
var userProperty = p
var eventParam = parameters ?? [:]
// append screen
if let screen = userProperty.removeValue(forKey: PropertyName.screen.rawValue) {
eventParam[PropertyName.screen.rawValue] = screen
}
let userInfo = Entity.UserInfo(
uid: userProperty.removeValue(forKey: PropertyName.uid.rawValue),
deviceId: userProperty.removeValue(forKey: PropertyName.deviceId.rawValue),
adjustId: userProperty.removeValue(forKey: PropertyName.adjustId.rawValue),
adId: userProperty.removeValue(forKey: PropertyName.adId.rawValue),
firebaseId: userProperty.removeValue(forKey: PropertyName.firebaseId.rawValue)
)
let event = try Entity.Event(timestamp: timestamp,
event: eventName,
userInfo: userInfo,
parameters: eventParam,
properties: userProperty)
let eventRecord = Entity.EventRecord(eventName: event.event, event: event, priority: priority)
subscriber(.success(eventRecord))
} catch {
subscriber(.failure(error))
}
return Disposables.create()
}
}
}
func eventsLogsArchive(_ callback: @escaping (URL?) -> Void) {
eventsLogger.logFilesZipArchive()
.subscribe(onSuccess: { url in
callback(url)
}, onFailure: { error in
callback(nil)
cdPrint("events logs archive error: \(error)")
})
.disposed(by: bag)
}
func eventsLogsDirURL(_ callback: @escaping (URL?) -> Void) {
eventsLogger.logFilesDirURL()
.subscribe(onSuccess: { url in
callback(url)
}, onFailure: { error in
callback(nil)
cdPrint("events logs archive error: \(error)")
})
.disposed(by: bag)
}
func registerInternalEventObserver(reportCallback: @escaping (_ eventCode: Int, _ info: String) -> Void) {
self.internalEventReporter = reportCallback
}
func getUserProperties() -> [String : String] {
return _userProperty
}
}
// MARK: - private functions
private extension Manager {
func setupOberving() {
syncServerTrigger
.debounce(.seconds(1), scheduler: rxConsumeScheduler)
.subscribe(onNext: { [weak self] _ in
self?.syncServerTime()
})
.disposed(by: bag)
var activeNoti = NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification)
if UIApplication.shared.applicationState == .active {
activeNoti = activeNoti.startWith(.init(name: UIApplication.didBecomeActiveNotification))
}
activeNoti
.subscribe(onNext: { [weak self] _ in
self?.syncServerTrigger.onNext(())
// fg
self?.setupFgAccumulateTimer()
})
.disposed(by: bag)
NotificationCenter.default.rx.notification(UIApplication.didEnterBackgroundNotification)
.subscribe(onNext: { [weak self] _ in
guard let `self` = self else { return }
//log fgevents
_ = self.logForegroundDuration()
.catchAndReturn(())
.map { self.consumeEvents() }
.subscribe()
self._serverTimeSynced.accept(false)
self.invalidFgAccumulateTimer()
})
.disposed(by: bag)
}
func syncServerTime() {
//
ntwkMgr.reachableObservable.filter { $0 }.map { _ in }.take(1).asSingle()
.flatMap { [weak self] _ -> Single<Int64> in
guard let `self` = self else { return Observable.empty().asSingle()}
return self.ntwkMgr.syncServerTime()
}
.observe(on: rxNetworkScheduler)
.subscribe(onSuccess: { [weak self] ms in
self?.serverInitialMs = ms
self?._serverTimeSynced.accept(true)
})
.disposed(by: bag)
}
func logForegroundDuration() -> Single<Void> {
return _logEvent(GuruAnalytics.fgEvent.name, parameters: [GuruAnalytics.fgEvent.paramKeyType.duration.rawValue : fgDurationMs()])
.observe(on: MainScheduler.asyncInstance)
.do(onSuccess: { _ in
UserDefaults.fgAccumulatedDuration = 0
})
}
func clearOutdatedEventsIfNeeded() {
/// 1.
serverNowMsSingle
.flatMap({ [weak self] serverNowMs -> Single<Void> in
guard let `self` = self else { return .just(()) }
let earlierThan: Int64 = serverNowMs - self.eventExpiredIntervel.int64Ms
return self.db.removeOutdatedEventRecords(earlierThan: earlierThan)
})
.catch({ error in
cdPrint("remove outdated records error: \(error)")
return .just(())
})
.subscribe(onSuccess: { [weak self] _ in
self?.outdatedEventsCleared.onNext(true)
})
.disposed(by: bag)
}
func logFirstOpenIfNeeded() {
if let t = UserDefaults.defaults?.value(forKey: UserDefaults.firstOpenTimeKey),
let firstOpenTimeMs = t as? Int64 {
setUserProperty("\(firstOpenTimeMs)", forName: PropertyName.firstOpenTime.rawValue)
} else {
/// log first open event
logEvent(GuruAnalytics.firstOpenEvent.name, parameters: nil, priority: .EMERGENCE)
/// save first open time
/// set to userProperty
let firstOpenAt = Date()
let saveFirstOpenTime = { [weak self] (ms: Int64) -> Void in
UserDefaults.defaults?.set(ms, forKey: UserDefaults.firstOpenTimeKey)
self?.setUserProperty("\(ms)", forName: PropertyName.firstOpenTime.rawValue)
}
serverNowMsSingle
.subscribe(onSuccess: { _ in
let latency = Date().timeIntervalSince(firstOpenAt)
let adjustedFirstOpenTimeMs = self.serverInitialMs - latency.int64Ms
saveFirstOpenTime(adjustedFirstOpenTimeMs)
}, onFailure: { error in
cdPrint("waiting for server time syncing error: \(error)")
saveFirstOpenTime(firstOpenAt.timeIntervalSince1970.int64Ms)
})
.disposed(by: bag)
}
}
func _logEvent(_ eventName: String, parameters: [String : Any]?, priority: Entity.EventRecord.Priority = .DEFAULT) -> Single<Void> {
eventsLogger.verbose(#function + " eventName: \(eventName)" + " params: \(parameters?.jsonString() ?? "")")
return { [weak self] () -> Single<Void> in
guard let `self` = self else { return Observable<Void>.empty().asSingle() }
return self.serverNowMsSingle
.flatMap { self.constructEvent(eventName, parameters: parameters, timestamp: $0, priority: priority) }
.flatMap { self.db.addEventRecords($0) }
.do(onSuccess: { _ in
self.accumulateLoggedEventsCount(1)
self.eventsLogger.verbose("log event success")
}, onError: { error in
self.eventsLogger.error("log event error: \(error)")
})
}()
}
}
// MARK: -
private extension Manager {
typealias TaskCallback = (() -> Void)
typealias Task = ((@escaping TaskCallback, Int) -> Void)
func performBackgroundTask(task: @escaping Task) -> Single<Void> {
return Single.create { [weak self] subscriber in
var backgroundTaskID: UIBackgroundTaskIdentifier?
let stopTaskHandler = {
/// dispose
guard let taskId = backgroundTaskID,
let disposable = self?.taskKeyDisposableMap[taskId.rawValue] else {
return
}
cdPrint("[performBackgroundTask] performBackgroundTask expired: \(backgroundTaskID?.rawValue ?? -1)")
disposable.dispose()
}
// Request the task assertion and save the ID.
backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "com.guru.analytics.manager.background.task", expirationHandler: {
// End the task if time expires.
self?.eventsLogger.verbose("performBackgroundTask expirationHandler: \(backgroundTaskID?.rawValue ?? -1)")
stopTaskHandler()
})
self?.eventsLogger.verbose("performBackgroundTask start: \(backgroundTaskID?.rawValue ?? -1)")
if let taskID = backgroundTaskID {
task({
self?.eventsLogger.verbose("performBackgroundTask finish: \(taskID.rawValue)")
subscriber(.success(()))
}, taskID.rawValue)
}
return Disposables.create {
if var taskID = backgroundTaskID {
self?.eventsLogger.verbose("performBackgroundTask dispose: \(taskID.rawValue)")
UIApplication.shared.endBackgroundTask(taskID)
taskID = .invalid
backgroundTaskID = nil
}
}
}
.subscribe(on: rxBgWorkScheduler)
}
/// event
func consumeEvents() {
guard GuruAnalytics.enableUpload else {
return
}
self.eventsLogger.verbose("consumeEvents start")
performBackgroundTask { [weak self] callback, taskId in
guard let `self` = self else { return }
cdPrint("consumeEvents start background task")
//
let disposable = outdatedEventsCleared
.filter { $0 }
.take(1)
.observe(on: rxBgWorkScheduler)
.asSingle()
.flatMap { _ -> Single<[Entity.EventRecord]> in
self.eventsLogger.verbose("consumeEvents fetchEventRecordsToUpload")
///step1:
return self.db.fetchEventRecordsToUpload(limit: self.maxEventFetchingCount)
}
.map { records -> [[Entity.EventRecord]] in
/// step2: eventnumberOfCountPerConsume
/// self.eventsLogger.verbose("consumeEvents fetchEventRecordsToUpload")
self.eventsLogger.verbose("consumeEvents fetchEventRecordsToUpload result: \(records.count)")
return records.chunked(into: self.numberOfCountPerConsume)
}
.flatMap({ batches -> Single<[[Entity.EventRecord]]> in
guard batches.count > 0 else { return .just([]) }
///
return self.ntwkMgr.reachableObservable.filter { $0 }
.take(1).asSingle()
.map { _ in batches }
})
.map { batches -> [Single<[String]>] in
/// step3:
self.eventsLogger.verbose("consumeEvents uploadEvents")
return batches.map { records in
return self.ntwkMgr.uploadEvents(records)
.do(onSuccess: { t in
self.eventsLogger.verbose("consumeEvents upload events succeed: \(t.eventsJson)")
})
.catch({ error in
self.eventsLogger.error("consumeEvents upload events error: \(error)")
// ID
let recordIds = records.map { $0.recordId }
return self.db.resetTransitionStatus(for: recordIds)
.map { _ in ([], "") }
})
.map { $0.recordIDs }
}
}
.flatMap { uploadBatches -> Single<[String]> in
guard uploadBatches.count > 0 else { return .just([]) }
///
return Observable.from(uploadBatches)
.merge()
.toArray().map { batches -> [String] in batches.flatMap { $0 } }
}
.flatMap { recordIDs -> Single<Void> in
self.accumulateUploadedEventsCount(recordIDs.count)
/// step4:
return self.db.deleteEventRecords(recordIDs)
.catch { error in
cdPrint("consumeEvents delete events from DB error: \(error)")
return .just(())
}
}
.observe(on: self.rxBgWorkScheduler)
.subscribe(onFailure: { error in
cdPrint("consumeEvents error: \(error)")
}, onDisposed: { [weak self] in
self?.taskKeyDisposableMap.removeValue(forKey: taskId)
cdPrint("consumeEvents onDisposed")
callback()
})
taskKeyDisposableMap[taskId] = disposable
}
.subscribe()
.disposed(by: bag)
}
func startPollingUpload() {
pollingUploadTask?.dispose()
pollingUploadTask = nil
// scheduleInterval
let timer = Observable<Int>.timer(.seconds(0), period: .milliseconds(Int(scheduleInterval.int64Ms)),
scheduler: rxConsumeScheduler)
.do(onNext: { _ in
cdPrint("consumeEvents timer")
})
// numberOfCountPerConsume
let counter = db.uploadableEventRecordCountOb()
.distinctUntilChanged()
.compactMap({ [weak self] count -> Int? in
cdPrint("consumeEvents uploadableEventRecordCountOb count: \(count) numberOfCountPerConsume: \(self?.numberOfCountPerConsume)")
guard let `self` = self,
count >= self.numberOfCountPerConsume else { return nil }
return count
})
.map { _ in }
.startWith(())
pollingUploadTask = Observable.combineLatest(timer, counter)
.throttle(.seconds(1), scheduler: rxConsumeScheduler)
.flatMap({ [weak self] t -> Single<(Int, Void)> in
guard let `self` = self else { return .just(t) }
return Observable.combineLatest(self.db.hasFgEventRecord().asObservable(), self.db.uploadableEventRecordCount().asObservable())
.take(1).asSingle()
.flatMap({ (hasFgEventInDb, eventsCount) -> Single<(Int, Void)> in
guard !hasFgEventInDb, eventsCount > 0 else {
return .just(t)
}
return self.logForegroundDuration().catchAndReturn(()).map({ _ in t })
})
})
.subscribe(onNext: { [weak self] (timer, counter) in
self?.consumeEvents()
})
}
func setupPollingUpload() {
reschedulePollingTrigger
.debounce(.seconds(1), scheduler: rxConsumeScheduler)
.subscribe(onNext: { [weak self] _ in
self?.startPollingUpload()
})
.disposed(by: bag)
}
func logFirstFgEvent() {
_ = Single.just(()).delay(.milliseconds(500), scheduler: MainScheduler.asyncInstance)
.flatMap({ [weak self] _ in
self?.logForegroundDuration() ?? .just(())
})
.subscribe()
}
}
// MARK: - fg
private extension Manager {
func setupFgAccumulateTimer() {
invalidFgAccumulateTimer()
fgStartAtAbsoluteMs = Date.absoluteTimeMs
fgAccumulateTimer = Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self] _ in
guard let `self` = self else { return }
UserDefaults.fgAccumulatedDuration = self.fgDurationMs()
}, onDisposed: {
cdPrint("fg accumulate timer disposed")
})
}
func invalidFgAccumulateTimer() {
fgAccumulateTimer?.dispose()
fgAccumulateTimer = nil
}
///
func fgDurationMs() -> Int64 {
let slice = Date.absoluteTimeMs - fgStartAtAbsoluteMs
fgStartAtAbsoluteMs = Date.absoluteTimeMs
// cdPrint("accumulate fg duration: \(slice)")
let totalDuration = UserDefaults.fgAccumulatedDuration + slice
// cdPrint("total fg duration: \(totalDuration)")
return totalDuration
}
}
extension Manager: GuruAnalyticsNetworkErrorReportDelegate {
func reportError(networkError: GuruAnalyticsNetworkError) {
enum UserInfoKey: String, Encodable {
case httpCode = "h_c"
case errorCode = "e_c"
case url, msg
}
let errorCode = networkError.internalErrorCategory.rawValue
let userInfo = (networkError.originError as NSError).userInfo
var info: [UserInfoKey : String] = [
.url : (userInfo[NSURLErrorFailingURLStringErrorKey] as? String) ?? "",
.msg : networkError.originError.localizedDescription,
]
if let httpCode = networkError.httpStatusCode {
info[.httpCode] = "\(httpCode)"
} else {
info[.errorCode] = "\((networkError.originError as NSError).code)"
}
info = info.compactMapValues { $0.isEmpty ? nil : $0 }
let jsonString = info.asString ?? ""
DispatchQueue.main.async { [weak self] in
self?.internalEventReporter?(errorCode, jsonString)
}
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: dab7243d39b964205a4fa4446e95403b
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,65 @@
//
// UserDefaults.swift
// GuruAnalytics
//
// Created by mayue on 2022/11/21.
//
import Foundation
internal enum UserDefaults {
static let defaults = Foundation.UserDefaults(suiteName: "com.guru.guru_analytics_lib")
static var eventsServerHost: String? {
get {
return defaults?.value(forKey: eventsServerHostKey) as? String
}
set {
var host = newValue
let h_sch = "http://"
let hs_sch = "https://"
host?.deletePrefix(h_sch)
host?.deletePrefix(hs_sch)
host?.trimmed(in: .whitespacesAndNewlines.union(.init(charactersIn: "/")))
defaults?.set(host, forKey: eventsServerHostKey)
}
}
static var fgAccumulatedDuration: Int64 {
get {
return defaults?.value(forKey: fgDurationKey) as? Int64 ?? 0
}
set {
defaults?.set(newValue, forKey: fgDurationKey)
}
}
}
extension UserDefaults {
static var firstOpenTimeKey: String {
return "app.first.open.timestamp"
}
static var dbVersionKey: String {
return "db.version"
}
static var hostsMapKey: String {
return "hosts.map"
}
static var eventsServerHostKey: String {
return "events.server.host"
}
static var fgDurationKey: String {
return "fg.duration.ms"
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: e022f21a2d3a94edb87e7479aab2cfb9
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b2af081c64bfe4af7b232b8132d01544
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,36 @@
//
// GuruAnalyticsErrorHandleDelegate.swift
// Alamofire
//
// Created by mayue on 2023/10/27.
//
import Foundation
internal enum GuruAnalyticsNetworkLayerErrorCategory: Int {
case unknown = -100
case serverAPIError = 101
case responseParsingError = 102
case googleDNSServiceError = 106
}
@objc internal protocol GuruAnalyticsNetworkErrorReportDelegate {
func reportError(networkError: GuruAnalyticsNetworkError) -> Void
}
internal class GuruAnalyticsNetworkError: NSError {
private(set) var httpStatusCode: Int?
private(set) var originError: Error
private(set) var internalErrorCategory: GuruAnalyticsNetworkLayerErrorCategory
init(httpStatusCode: Int? = nil, internalErrorCategory: GuruAnalyticsNetworkLayerErrorCategory, originError: Error) {
self.httpStatusCode = httpStatusCode
self.originError = originError
self.internalErrorCategory = internalErrorCategory
super.init(domain: "com.guru.analytics.network.layer", code: internalErrorCategory.rawValue, userInfo: (originError as NSError).userInfo)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,85 @@
fileFormatVersion: 2
guid: 77961084d08bf4074afb847d866d89e0
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,51 @@
//
// GuruAnalytics+Internal.swift
// Pods
//
// Created by mayue on 2022/11/18.
//
import Foundation
internal extension GuruAnalytics {
///built-in user property keys
enum PropertyName: String {
case deviceId
case uid
case adjustId
case adId
case firebaseId
case screen = "screen_name"
case firstOpenTime = "first_open_time"
}
///built-in events
static let fgEvent: EventProto = {
var e = EventProto(paramKeyType: FgEventParametersKeys.self, name: "fg")
return e
}()
static let firstOpenEvent: EventProto = {
var e = EventProto(paramKeyType: FgEventParametersKeys.self, name: "first_open")
return e
}()
class func setUserProperty(_ value: String?, forName name: PropertyName) {
setUserProperty(value, forName: name.rawValue)
}
}
internal extension GuruAnalytics {
struct EventProto<ParametersKeys> {
var paramKeyType: ParametersKeys.Type
var name: String
}
enum FgEventParametersKeys: String {
case duration
}
}

View File

@ -0,0 +1,85 @@
fileFormatVersion: 2
guid: 2623be1999d104830a332ddf24de490a
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ef02d71253914417f9d007629ed0eb1d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,78 @@
//
// APIService.swift
// GuruAnalytics_iOS
//
// Created by mayue on 2022/11/8.
//
import Foundation
import Alamofire
internal enum APIService {}
extension APIService {
enum Backend: CaseIterable {
case event
case systemTime
}
}
extension APIService.Backend {
var scheme: String {
return "https"
}
var host: String {
switch self {
case .systemTime:
return "saas.castbox.fm"
case .event:
return UserDefaults.eventsServerHost ?? "collect.saas.castbox.fm"
}
}
var urlComponents: URLComponents {
var urlC = URLComponents()
urlC.host = self.host
urlC.scheme = self.scheme
urlC.path = self.path
return urlC
}
var path: String {
switch self {
case .event:
return "/event"
case .systemTime:
return "/tool/api/v1/system/time"
}
}
var method: HTTPMethod {
switch self {
case .event:
return .post
case .systemTime:
return .get
}
}
var headers: HTTPHeaders {
HTTPHeaders(
["Content-Type": "application/json",
"Content-Encoding": "gzip",
"x_event_type": "event"]
)
}
var version: Int {
///
switch self {
case .event:
return 10
case .systemTime:
return 0
}
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: 09cfaaabf31cb4a73ab8d83aa65eb2de
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,419 @@
//
// Network.swift
// GuruAnalytics_iOS
//
// Created by mayue on 2022/11/3.
// Copyright © 2022 Guru Network Limited. All rights reserved.
//
import Foundation
import Alamofire
import RxSwift
import RxRelay
import Gzip
internal class NetworkManager {
private static let ipErrorUserInfoKey = "failed_ip"
internal var isReachable: Bool {
return _reachableObservable.value
}
internal var reachableObservable: Observable<Bool> {
return _reachableObservable.asObservable()
}
private let _reachableObservable = BehaviorRelay(value: false)
private let reachablity = NetworkReachabilityManager()
private let networkQueue = DispatchQueue.init(label: "com.guru.analytics.network.queue", qos: .userInitiated)
private lazy var rxWorkScheduler = SerialDispatchQueueScheduler.init(queue: networkQueue, internalSerialQueueName: "com.guru.analytics.network.rx.work.queue")
private lazy var session: Session = {
let trustManager = CertificatePinnerServerTrustManager()
trustManager.evaluator.hostWhiteList = hostsMap
return Session(serverTrustManager: trustManager)
}()
private var hostsMap: [String : [String]] {
get {
return UserDefaults.defaults?.value(forKey: UserDefaults.hostsMapKey) as? [String : [String]] ?? [:]
}
set {
UserDefaults.defaults?.set(newValue, forKey: UserDefaults.hostsMapKey)
(session.serverTrustManager as? CertificatePinnerServerTrustManager)?.evaluator.hostWhiteList = newValue
checkHostMap(newValue)
}
}
internal weak var networkErrorReporter: GuruAnalyticsNetworkErrorReportDelegate?
internal init() {
reachablity?.startListening(onQueue: networkQueue, onUpdatePerforming: { [weak self] status in
var reachable: Bool
switch status {
case .reachable(_):
reachable = true
case .notReachable, .unknown:
reachable = false
}
self?._reachableObservable.accept(reachable)
})
APIService.Backend.allCases.forEach({ service in
_ = lookupHostRemote(service.host).subscribe()
})
}
/// event
/// - Parameter events: event record
/// - Returns: event record ID
internal func uploadEvents(_ events: [Entity.EventRecord]) -> Single<(recordIDs: [String], eventsJson: String)> {
guard !events.isEmpty else {
return .just(([], ""))
}
let service = APIService.Backend.event
return lookupHostLocal(service.host)
.flatMap { ip in
Single.create { [weak self] subscriber in
guard let `self` = self else {
subscriber(.failure(
NSError(domain: "networkManager", code: 0, userInfo: [NSLocalizedDescriptionKey : "manager is released"])
))
return Disposables.create()
}
var postJson = [String : Any]()
postJson["version"] = service.version
postJson["deviceInfo"] = Constants.deviceInfo
let eventJsonArray = events.compactMap { $0.eventJson.jsonObject() }
postJson["events"] = eventJsonArray
do {
let jsonData = try JSONSerialization.data(withJSONObject: postJson)
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
let gzippedJsonData = try jsonData.gzipped()
let httpBody = gzippedJsonData
var urlRequest: URLRequest
var urlC = service.urlComponents
let session: Session
if let ip = ip {
session = self.session
urlC.host = ip
urlRequest = try URLRequest(url: urlC, method: service.method, headers: service.headers)
urlRequest.setValue(service.host, forHTTPHeaderField: "host")
} else {
session = AF
urlRequest = try URLRequest(url: urlC, method: service.method, headers: service.headers)
}
urlRequest.setValue(GuruAnalytics.saasXAPPID, forHTTPHeaderField: "X-APP-ID")
urlRequest.setValue(GuruAnalytics.saasXDEVICEINFO, forHTTPHeaderField: "X-DEVICE-INFO")
urlRequest.httpBody = httpBody
var emptyResponseCodes = DataResponseSerializer.defaultEmptyResponseCodes
emptyResponseCodes.insert(200)
let request = session.request(urlRequest).validate(statusCode: [200])
.responseData(
queue: self.networkQueue,
emptyResponseCodes: emptyResponseCodes,
completionHandler: { response in
cdPrint("\(#function): request: \(urlRequest) \nheader:\(urlRequest.headers) \nhttpbody: \(jsonString) \nresponse: \(response)")
switch response.result {
case .failure(let error):
subscriber(.failure(self.mapError(error, for: ip)))
cdPrint("\(#function) error: \(error)")
case .success:
subscriber(.success((events.map { $0.recordId }, jsonString)))
}
})
return Disposables.create {
request.cancel()
}
} catch {
cdPrint("construct request failed: \(error)")
subscriber(.failure(error))
return Disposables.create()
}
}
}
.do(onError: { [weak self] error in
self?.reportError(error: error, internalErrorCategory: .serverAPIError)
})
.catch { [weak self] error in
guard let `self` = self else { throw error }
return try self.errorCatcher(error, for: service.host) {
self.uploadEvents(events)
}
}
.subscribe(on: rxWorkScheduler)
}
///
/// - Returns:
internal func syncServerTime() -> Single<Int64> {
let service = APIService.Backend.systemTime
return lookupHostLocal(service.host)
.flatMap { ip in
Single.create { [weak self] subscriber in
guard let `self` = self else {
subscriber(.failure(
NSError(domain: "networkManager", code: 0, userInfo: [NSLocalizedDescriptionKey : "manager is released"])
))
return Disposables.create()
}
do {
let start = Date()
var urlC = service.urlComponents
let session: Session
var urlReq: URLRequest
if let ip = ip {
session = self.session
urlC.host = ip
urlReq = try URLRequest(url: urlC, method: service.method, headers: service.headers)
urlReq.setValue(service.host, forHTTPHeaderField: "host")
} else {
session = AF
urlReq = try URLRequest(url: urlC, method: service.method, headers: service.headers)
}
urlReq.setValue(GuruAnalytics.saasXAPPID, forHTTPHeaderField: "X-APP-ID")
urlReq.setValue(GuruAnalytics.saasXDEVICEINFO, forHTTPHeaderField: "X-DEVICE-INFO")
let request = session.request(urlReq).validate(statusCode: [200])
.responseDecodable(of: Entity.SystemTimeResult.self,
queue: self.networkQueue,
completionHandler: { response in
cdPrint("\(#function): request: \(urlReq) \nheaders:\(urlReq.headers) \nresponse: \(response)")
switch response.result {
case .success(let data):
let timespan = Date().timeIntervalSince(start).int64Ms
let systemTime = data.data - timespan / 2
subscriber(.success(systemTime))
case .failure(let error):
cdPrint("\(#function) error: \(error)")
subscriber(.failure(self.mapError(error, for: ip)))
}
})
return Disposables.create {
request.cancel()
}
} catch {
cdPrint("construct request failed: \(error)")
subscriber(.failure(error))
return Disposables.create()
}
}
}
.do(onError: { [weak self] error in
self?.reportError(error: error, internalErrorCategory: .serverAPIError)
})
.catch { [weak self] error in
guard let `self` = self else { throw error }
return try self.errorCatcher(error, for: service.host) {
self.syncServerTime()
}
}
.subscribe(on: rxWorkScheduler)
}
private func _lookupHostRemote(_ host: String) -> Single<[IpAdress]> {
return Single.create { subscriber in
do {
var urlC = URLComponents()
urlC.scheme = "https"
urlC.host = "dns.google"
urlC.path = "/resolve"
urlC.queryItems = [.init(name: "name", value: "\(host)")]
let urlReq = try URLRequest(url: urlC, method: .get)
let request = AF.request(urlReq)
.validate(statusCode: [200])
.responseData(completionHandler: { response in
switch response.result {
case .success(let data):
do {
guard let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String : Any],
let answerDictArr = dict["Answer"] as? [[String : Any]] else {
let customError = NSError(domain: "com.guru.analytics.network.layer", code: 0,
userInfo: [NSLocalizedDescriptionKey : "dns.google service returned unexpected data"])
subscriber(.failure(customError))
return
}
let ips = try JSONDecoder().decodeAnyData([IpAdress].self, from: answerDictArr)
subscriber(.success(ips))
cdPrint("\(#function) success request: \(urlReq) \nresponse: \(ips)")
} catch {
subscriber(.failure(error))
}
case .failure(let error):
cdPrint("\(#function) error: \(error) request: \(urlReq)")
subscriber(.failure(error))
}
})
return Disposables.create {
request.cancel()
}
} catch {
cdPrint("construct request failed: \(error)")
subscriber(.failure(error))
return Disposables.create()
}
}
.subscribe(on: rxWorkScheduler)
}
private func lookupHostRemote(_ host: String) -> Single<[String]> {
return _lookupHostRemote(host)
.map { ipList -> [String] in
ipList.compactMap { ip in
guard ip.type == 1 else { return nil }
return ip.data
}
}
.do(onSuccess: { [weak self] ipList in
self?.hostsMap[host] = ipList
}, onError: { [weak self] error in
self?.reportError(error: error, internalErrorCategory: .googleDNSServiceError)
})
}
private func lookupHostLocal(_ host: String) -> Single<String?> {
return Single.create { [weak self] subscriber in
guard let `self` = self else {
subscriber(.failure(
NSError(domain: "networkManager", code: 0, userInfo: [NSLocalizedDescriptionKey : "manager is released"])
))
return Disposables.create()
}
subscriber(.success(self.hostsMap[host]?.first))
return Disposables.create()
}
.subscribe(on: rxWorkScheduler)
}
private func mapError(_ error: AFError, for ip: String?) -> Error {
guard let ip = ip else { return error }
var e = (error.underlyingError ?? error) as NSError
var userInfo = e.userInfo
userInfo[Self.ipErrorUserInfoKey] = ip
e = NSError(domain: e.domain, code: e.code, userInfo: userInfo)
return e
}
private func errorCatcher<T>(_ error: Error, for host: String, returnValue: (() -> Single<T>) ) throws -> Single<T> {
let e = error as NSError
guard let ip = e.userInfo[Self.ipErrorUserInfoKey] as? String else {
throw error
}
//FIX: https://console.firebase.google.com/u/1/project/ball-sort-dd4d0/crashlytics/app/ios:ball.sort.puzzle.color.sorting.bubble.games/issues/c1f6d36aeb7c105a32015504776adff5?time=last-ninety-days&sessionEventKey=27d699688a594f96a7b17003a3c49c84_1900062047348716162
if var hosts = hostsMap[host] {
hosts.removeAll(where: { $0 == ip })
hostsMap[host] = hosts
}
return returnValue()
}
private func checkHostMap(_ hostMap: [String : [String]]) {
hostMap.forEach { key, value in
guard value.count <= 0 else { return }
_ = lookupHostRemote(key).subscribe()
}
}
private func reportError(error: Error, internalErrorCategory: GuruAnalyticsNetworkLayerErrorCategory) {
let customError: GuruAnalyticsNetworkError
if let aferror = error.asAFError {
if case let AFError.responseValidationFailed(reason) = aferror,
case let AFError.ResponseValidationFailureReason.unacceptableStatusCode(httpStatusCode) = reason {
customError = GuruAnalyticsNetworkError(httpStatusCode: httpStatusCode, internalErrorCategory: internalErrorCategory, originError: aferror.underlyingError ?? error)
} else {
customError = GuruAnalyticsNetworkError(internalErrorCategory: internalErrorCategory, originError: aferror.underlyingError ?? error)
}
} else {
customError = GuruAnalyticsNetworkError(internalErrorCategory: internalErrorCategory, originError: error)
}
networkErrorReporter?.reportError(networkError: customError)
}
}
internal final class CertificatePinnerTrustEvaluator: ServerTrustEvaluating {
private let dftEvaluator = DefaultTrustEvaluator()
init() {}
var hostWhiteList: [String : [String]] = [:]
func evaluate(_ trust: SecTrust, forHost host: String) throws {
let originHostName: String = hostWhiteList.first { _, value in
value.contains { $0 == host }
}?.key ?? host
try dftEvaluator.evaluate(trust, forHost: originHostName)
cdPrint(#function + " \(trust) forHost: \(host) originHostName: \(originHostName)")
}
}
internal class CertificatePinnerServerTrustManager: ServerTrustManager {
let evaluator = CertificatePinnerTrustEvaluator()
init() {
super.init(allHostsMustBeEvaluated: true, evaluators: [:])
}
override func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
return evaluator
}
}
extension NetworkManager {
struct IpAdress: Codable {
let name: String
let type: Int
let TTL: Int
let data: String
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: f197036e5438741cea961bc00c23c775
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8675c92422b8d45f49ced2e0f1602f32
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,160 @@
//
// Constants.swift
// AgoraChatRoom
//
// Created by LXH on 2019/11/27.
// Copyright © 2019 CavanSu. All rights reserved.
//
import UIKit
internal struct Constants {
private static let appVersion: String = {
guard let infoDict = Bundle.main.infoDictionary,
let currentVersion = infoDict["CFBundleShortVersionString"] as? String else {
return ""
}
return currentVersion
}()
private static let appBundleIdentifier: String = {
guard let infoDictionary = Bundle.main.infoDictionary,
let shortVersion = infoDictionary["CFBundleIdentifier"] as? String else {
return ""
}
return shortVersion
}()
private static let preferredLocale: Locale = {
guard let preferredIdentifier = Locale.preferredLanguages.first else {
return Locale.current
}
return Locale(identifier: preferredIdentifier)
}()
private static let countryCode: String = {
return preferredLocale.regionCode?.uppercased() ?? ""
}()
private static let timeZone: String = {
return TimeZone.current.identifier
}()
private static let languageCode: String = {
return preferredLocale.languageCode ?? ""
}()
private static let localeCode: String = {
return preferredLocale.identifier
}()
private static let modelName: String = {
return platform().deviceType.rawValue
}()
private static let model: String = {
return hardwareString()
}()
private static let systemVersion: String = {
return UIDevice.current.systemVersion
}()
private static let screenSize: (w: CGFloat, h: CGFloat) = {
return (UIScreen.main.bounds.width, UIScreen.main.bounds.height)
}()
///
private static let tzOffset: Int64 = {
return Int64(TimeZone.current.secondsFromGMT(for: Date())) * 1000
}()
static var deviceInfo: [String : Any] {
return [
"country": countryCode,
"platform": "IOS",
"appId" : appBundleIdentifier,
"version" : appVersion,
"tzOffset": tzOffset,
"deviceType" : modelName,
"brand": "Apple",
"model": model,
"screenH": Int(screenSize.h),
"screenW": Int(screenSize.w),
"osVersion": systemVersion,
"language" : languageCode
]
}
/// This method returns the hardware type
///
///
/// - returns: raw `String` of device type, e.g. iPhone5,1
///
private static func hardwareString() -> String {
var name: [Int32] = [CTL_HW, HW_MACHINE]
var size: Int = 2
sysctl(&name, 2, nil, &size, nil, 0)
var hw_machine = [CChar](repeating: 0, count: Int(size))
sysctl(&name, 2, &hw_machine, &size, nil, 0)
var hardware: String = String(cString: hw_machine)
// Check for simulator
if hardware == "x86_64" || hardware == "i386" || hardware == "arm64" {
if let deviceID = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] {
hardware = deviceID
}
}
return hardware
}
/// This method returns the Platform enum depending upon harware string
///
///
/// - returns: `Platform` type of the device
///
static func platform() -> Platform {
let hardware = hardwareString()
if (hardware.hasPrefix("iPhone")) { return .iPhone }
if (hardware.hasPrefix("iPod")) { return .iPodTouch }
if (hardware.hasPrefix("iPad")) { return .iPad }
if (hardware.hasPrefix("Watch")) { return .appleWatch }
if (hardware.hasPrefix("AppleTV")) { return .appleTV }
return .unknown
}
enum Platform {
case iPhone
case iPodTouch
case iPad
case appleWatch
case appleTV
case unknown
enum DeviceType: String {
case mobile, tablet, desktop, smartTV, watch, other
}
var deviceType: DeviceType {
switch self {
case .iPad:
return .tablet
case .iPhone, .iPodTouch:
return .mobile
case .appleTV:
return .smartTV
case .appleWatch:
return .watch
case .unknown:
return .other
}
}
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: b7b38f09cfd4b47dc8ffd0e05fd14523
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,54 @@
//
// EncodableExtension.swift
// Runner
//
// Created by on 2020/5/19.
// Copyright © 2020 Guru. All rights reserved.
//
import Foundation
internal extension Encodable {
func asDictionary() throws -> [String: Any] {
let data = try JSONEncoder().encode(self)
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
throw NSError()
}
return dictionary
}
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
var asString: String? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return String(data: data, encoding: .utf8)
}
}
internal extension String {
func jsonObject() -> [String: Any]? {
guard let data = data(using: .utf8) else {
return nil
}
guard let jsonData = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [String: Any] else {
return nil
}
return jsonData
}
func jsonArrayObject() -> [[String: Any]]? {
guard let data = data(using: .utf8) else {
return nil
}
guard let jsonData = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [[String: Any]] else {
return nil
}
return jsonData
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: 8225294b372d84b579cecb97daca5100
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,25 @@
//
// Helper.swift
// GuruAnalytics_iOS
//
// Created by mayue on 2022/11/4.
//
import Foundation
internal func cdPrint(_ items: Any..., context: String? = nil, separator: String = " ", terminator: String = "\n") {
#if DEBUG
guard GuruAnalytics.loggerDebug else { return }
let date = Date()
let df = DateFormatter()
df.dateFormat = "HH:mm:ss.SSSS"
let dateString = df.string(from: date)
print("\(dateString) [GuruAnalytics] Thread: \(Thread.current.queueName) \(context ?? "") ", terminator: "")
for item in items {
print(item, terminator: " ")
}
print("", terminator: terminator)
#else
#endif
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: bf62f6d630bd84c75834df5c964dee4f
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,28 @@
//
// JSONDecoder.Extension.swift
// Moya-Cuddle
//
// Created by Wilson-Yuan on 2019/12/25.
// Copyright © 2019 Guru. All rights reserved.
//
import Foundation
internal extension JSONDecoder {
func decodeAnyData<T>(_ type: T.Type, from data: Any) throws -> T where T: Decodable {
var unwrappedData = Data()
if let data = data as? Data {
unwrappedData = data
}
else if let data = data as? [String: Any] {
unwrappedData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
}
else if let data = data as? [[String: Any]] {
unwrappedData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
}
else {
fatalError("error format of data ")
}
return try decode(type, from: unwrappedData)
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: a45044c2a092c455881ad4f39fdeec2b
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,158 @@
//
// Logger.swift
// GuruAnalyticsLib
//
// Created by mayue on 2022/12/21.
//
import Foundation
import SwiftyBeaver
import CryptoSwift
import RxSwift
internal class LoggerManager {
private static let password: String = "Castbox123"
private lazy var logger: SwiftyBeaver.Type = {
let logger = SwiftyBeaver.self
logger.addDestination(consoleOutputDestination)
logger.addDestination(fileOutputDestination)
return logger
}()
private lazy var logFileDir: URL = {
let baseDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
return baseDir.appendingPathComponent("GuruAnalytics/Logs/\(logCategoryName)/", isDirectory: true)
}()
private lazy var consoleOutputDestination: ConsoleDestination = {
let d = ConsoleDestination()
return d
}()
private lazy var fileOutputDestination: FileDestination = {
let file = FileDestination()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateString = dateFormatter.string(from: Date())
file.logFileURL = logFileDir.appendingPathComponent("\(dateString).log", isDirectory: false)
file.asynchronously = true
return file
}()
private let logCategoryName: String
internal init(logCategoryName: String) {
self.logCategoryName = logCategoryName
}
}
internal extension LoggerManager {
func logFilesZipArchive() -> Single<URL?> {
return Single.create { subscriber in
subscriber(.success(nil))
return Disposables.create()
}
.observe(on: MainScheduler.asyncInstance)
}
func logFilesDirURL() -> Single<URL?> {
return Single.create { subscriber in
DispatchQueue.global().async { [weak self] in
guard let `self` = self else {
subscriber(.failure(
NSError(domain: "loggerManager", code: 0, userInfo: [NSLocalizedDescriptionKey : "manager is released"])
))
return
}
do {
let filePaths = try FileManager.default.contentsOfDirectory(at: self.logFileDir,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles])
.filter { $0.pathExtension == "log" }
.map { $0.path }
guard filePaths.count > 0 else {
subscriber(.success(nil))
return
}
subscriber(.success(self.logFileDir))
} catch {
subscriber(.failure(error))
}
}
return Disposables.create()
}
.observe(on: MainScheduler.asyncInstance)
}
func clearAllLogFiles() {
DispatchQueue.global().async { [weak self] in
guard let `self` = self else { return }
if let files = try? FileManager.default.contentsOfDirectory(at: self.logFileDir, includingPropertiesForKeys: [], options: [.skipsHiddenFiles]) {
files.forEach { url in
do {
try FileManager.default.removeItem(at: url)
} catch {
cdPrint("remove file: \(url.path) \n error: \(error)")
}
}
}
}
}
func verbose(_ message: Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
guard GuruAnalytics.loggerDebug else { return }
logger.verbose(message, file, function, line: line, context: context)
}
func debug(_ message: Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
guard GuruAnalytics.loggerDebug else { return }
logger.debug(message, file, function, line: line, context: context)
}
func info(_ message: Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
guard GuruAnalytics.loggerDebug else { return }
logger.info(message, file, function, line: line, context: context)
}
func warning(_ message: Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
guard GuruAnalytics.loggerDebug else { return }
logger.warning(message, file, function, line: line, context: context)
}
func error(_ message: Any,
_ file: String = #file,
_ function: String = #function,
line: Int = #line,
context: Any? = nil) {
guard GuruAnalytics.loggerDebug else { return }
logger.error(message, file, function, line: line, context: context)
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: c0c260bf3d64e48688db3309a7ea46c3
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,32 @@
//
// ThreadExtension.swift
// GuruAnalyticsLib
//
// Created by on 17/02/23.
//
import Foundation
extension Thread {
var threadName: String {
if isMainThread {
return "main"
} else if let threadName = Thread.current.name, !threadName.isEmpty {
return threadName
} else {
return description
}
}
var queueName: String {
if let queueName = String(validatingUTF8: __dispatch_queue_get_label(nil)) {
return queueName
} else if let operationQueueName = OperationQueue.current?.name, !operationQueueName.isEmpty {
return operationQueueName
} else if let dispatchQueueName = OperationQueue.current?.underlyingQueue?.label, !dispatchQueueName.isEmpty {
return dispatchQueueName
} else {
return "n/a"
}
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: 7ae00a082cf8149ea808b748124345a4
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,158 @@
//
// Utilities.swift
// GuruAnalytics_iOS
//
// Created by mayue on 2022/11/4.
//
import Foundation
import RxSwift
internal extension TimeInterval {
var int64Ms: Int64 {
return Int64(self * 1000)
}
}
internal extension Date {
var msSince1970: Int64 {
timeIntervalSince1970.int64Ms
}
static var absoluteTimeMs: Int64 {
return CACurrentMediaTime().int64Ms
}
}
internal extension Dictionary {
func jsonString(prettify: Bool = false) -> String? {
guard JSONSerialization.isValidJSONObject(self) else { return nil }
let options = (prettify == true) ? JSONSerialization.WritingOptions.prettyPrinted : JSONSerialization.WritingOptions()
guard let jsonData = try? JSONSerialization.data(withJSONObject: self, options: options) else { return nil }
return String(data: jsonData, encoding: .utf8)
}
}
internal extension String {
func convertToDictionary() -> [String: Any]? {
if let data = data(using: .utf8) {
return (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any]
}
return nil
}
mutating func deletePrefix(_ prefix: String) {
guard hasPrefix(prefix) else { return }
if #available(iOS 16.0, *) {
trimPrefix(prefix)
} else {
removeFirst(prefix.count)
}
}
mutating func trimmed(in set: CharacterSet) {
self = trimmingCharacters(in: set)
}
}
internal extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
internal class SafeValue<T> {
private var _value: T
private let queue = DispatchQueue(label: "com.guru.analytics.safe.value.reader.writer.queue", attributes: .concurrent)
private let group = DispatchGroup()
internal init(_ value: T) {
_value = value
}
internal func setValue(_ value: T) {
queue.async(group: group, execute: .init(flags: .barrier, block: { [weak self] in
self?._value = value
}))
}
internal func getValue(_ valueBlock: @escaping ((T) -> Void)) {
queue.async(group: group, execute: .init(block: { [weak self] in
guard let `self` = self else { return }
valueBlock(self._value)
}))
}
internal var singleValue: Single<T> {
return Single.create { [weak self] subscriber in
self?.getValue { value in
subscriber(.success(value))
}
return Disposables.create()
}
}
}
internal extension SafeValue where T == Dictionary<String, String> {
func mergeValue(_ value: T) -> Single<Void> {
return .create { [weak self] subscriber in
guard let `self` = self else {
subscriber(.failure(
NSError(domain: "safevalue", code: 0, userInfo: [NSLocalizedDescriptionKey : "safevalue object is released"])
))
return Disposables.create()
}
self.getValue { currentValue in
let newValue = currentValue.merging(value) { _, new in new }
self.setValue(newValue)
subscriber(.success(()))
}
return Disposables.create()
}
}
}
internal extension SafeValue where T == Array<String> {
func appendValue(_ value: T) {
getValue { [weak self] v in
var currentValue = v
currentValue.append(contentsOf: value)
self?.setValue(currentValue)
}
}
func removeAll(where shouldBeRemoved: @escaping (Array<String>.Element) -> Bool) {
getValue { [weak self] v in
var currentValue = v
currentValue.removeAll(where: shouldBeRemoved)
self?.setValue(currentValue)
}
}
}
internal extension Character {
var isAlphabetic: Bool {
return (self >= "a" && self <= "z") || (self >= "A" && self <= "Z")
}
var isDigit: Bool {
return self >= "0" && self <= "9"
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: 1f70e4103a1d748d4af94d840c55a281
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,54 @@
#
# Be sure to run `pod lib lint GuruAnalytics.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'GuruAnalyticsLib'
s.version = '0.3.6'
s.summary = 'A short description of GuruAnalytics.'
# This description is used to generate tags and improve search results.
# * Think: What does it do? Why did you write it? What is the focus?
# * Try to keep it short, snappy and to the point.
# * Write the description between the DESC delimiters below.
# * Finally, don't worry about the indent, CocoaPods strips it!
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://github.com/castbox/GuruAnalytics_iOS'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'devSC' => 'xiaochong2154@163.com' }
# s.source = { :git => 'git@github.com:castbox/GuruAnalytics_iOS.git', :tag => s.version.to_s }
s.source = { :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '11.0'
s.swift_version = '5'
s.source_files = 'GuruAnalytics/Classes/**/*'
# s.resource_bundles = {
# 'GuruAnalytics' => ['GuruAnalytics/Assets/*.png']
# }
# s.public_header_files = 'Pod/Classes/**/*.h'
# s.frameworks = 'UIKit', 'MapKit'
# s.dependency 'AFNetworking', '~> 2.3'
s.dependency 'RxCocoa', '~> 6.7.0'
s.dependency 'Alamofire', '~> 5.9'
s.dependency 'FMDB', '~> 2.0'
s.dependency 'GzipSwift', '~> 5.0'
s.dependency 'CryptoSwift', '~> 1.0'
s.dependency 'SwiftyBeaver', '~> 1.0'
s.subspec 'Privacy' do |ss|
ss.resource_bundles = {
s.name => 'GuruAnalytics/Assets/PrivacyInfo.xcprivacy'
}
end
end

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 09c4804bc0744da2bcc1f3fa6479b8b6
timeCreated: 1717115698

View File

@ -2,10 +2,42 @@
GuruAnalyticsLib 的 Unity 插件库
## 研发注意:
- **Android**
- 插件库内的 .aar 通过 [guru_analytics](https://github.com/castbox/guru_analytics) 项目直接构建 ( 命令 `gradle publishToMavenLocal` )
- 构建后请改名为 `guru-analytics-{version}.aar`
- 请将 .aar 文件放置于 `./Runtime/GuruAnalytics/Plugins/Android` 目录下
- **iOS**
- 插件库内的文件 通过 [GuruAnalytics_iOS](https://github.com/castbox/GuruAnalytics_iOS) 项目
- (1) 请将 repo 内的两个文件夹 `Assets``Classses` 拷贝至 `./Runtime/GuruAnalytics/Plugins/iOS/GuruAnalytics` 目录下:
- (2) 请将部署到 Unity 内所有的 `.swift` 文件的 meta 属性内, 取消 iOS 文件属性. (因为打包时会按照 POD 导入)
- 注意及时更新 `GuruAnalyticsLib.podspec`文件内的更新内容
```ruby
# 将 source 内的 git 属性删除, 只保留 tag 属性
# s.source = { :git => 'git@github.com:castbox/GuruAnalytics_iOS.git', :tag => s.version.to_s }
s.source = { :tag => s.version.to_s }
```
---
## Change Logs
### 1.12.0
- Android 端对齐 `1.1.1`
> Hash: bdb41ae118dcf438e8efe4f27d0ec856bc3147b0
- iOS 端对齐 `0.3.6`
> Hash: 0cd5ce7aa64e12caa7413c938a3164687b973843
- Pod 库改为 本地文件引用 (配合外部发行项目)
### 1.11.0
- Android 端对齐 `1.0.3`
> Hash: 1978686dbcba38b7b0421d8b6b2bef111356366b
- iOS 端对齐 `0.3.6`
> Hash: 0cd5ce7aa64e12caa7413c938a3164687b973843
- Pod 库改为 本地文件引用 (配合外部发行项目)
### 1.9.0
- Android 端对齐 0.3.1+.
> Hash: 0457eba963a9049fb6a16708b921573ef36c99b1

View File

@ -14,10 +14,11 @@ namespace Guru
public class GuruAnalytics
{
// Plugin Version
public const string Version = "1.10.4";
public const string Version = "1.10.5";
public static readonly string Tag = "[ANU]";
private static readonly string ActionName = "logger_error";
internal const int EventPriorityDefault = 10;
private static IAnalyticsAgent _agent;
@ -85,6 +86,8 @@ namespace Guru
public static void Init(string appId, string deviceInfo, bool isDebug = false,
bool enableErrorLog = false, bool syncProperties = false)
{
Debug.Log($"{Tag} --- Guru Analytics [{Version}] initialing...");
_autoSyncProperties = syncProperties;
_enableErrorLog = enableErrorLog;
Agent?.Init(appId, deviceInfo, isDebug);
@ -198,7 +201,8 @@ namespace Guru
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="data">INT类型的值</param>
public static void LogEvent(string eventName, Dictionary<string, dynamic> data = null)
/// <param name="priority"></param>
public static void LogEvent(string eventName, Dictionary<string, dynamic> data = null, int priority = -1)
{
if(_autoSyncProperties)
UpdateAllUserProperties(); // 每次打点更新用户属性
@ -208,8 +212,9 @@ namespace Guru
{
raw = BuildParamsJson(data);
}
Debug.Log($"{Tag} event:{eventName} | raw: {raw}");
Agent?.LogEvent(eventName, raw);
if (priority < 0) priority = EventPriorityDefault;
Debug.Log($"{Tag} event:{eventName} | raw: {raw} | priority: {priority}");
Agent?.LogEvent(eventName, raw, priority);
}
private static string BuildParamsString(Dictionary<string, dynamic> data)

View File

@ -15,7 +15,7 @@ namespace Guru
void SetUid(string uid);
bool IsDebug { get; }
bool EnableErrorLog { get; set; }
void LogEvent(string eventName, string parameters);
void LogEvent(string eventName, string parameters, int priority = -1);
void ReportEventSuccessRate(); // 上报任务成功率
void SetTch02Value(double value); // 设置太极02数值
void InitCallback(string objName, string method); // 设置回调对象参数

View File

@ -80,7 +80,8 @@ namespace Guru
{
_isDebug = isDebug;
string bundleId = Application.identifier;
CallStatic("init", appId, deviceInfo, bundleId, UseWorker, isDebug, UseCronet, BaseUrl); // 调用接口
// public static void init(String appId, String deviceInfo, String bundleId, boolean isDebug, boolean useWorker, boolean useCronet, String baseUrl)
CallStatic("init", appId, deviceInfo, bundleId, isDebug, UseWorker, UseCronet, BaseUrl); // 调用接口
}
public void SetScreen(string screenName)
@ -124,7 +125,10 @@ namespace Guru
}
public bool IsDebug => CallStatic<bool>("isDebug");
public void LogEvent(string eventName, string parameters) => CallStatic("logEvent", eventName, parameters);
public void LogEvent(string eventName, string parameters, int priority = -1)
{
CallStatic("logEvent", eventName, parameters, priority);
}
public void ReportEventSuccessRate() => CallStatic("reportEventRate");
public void SetTch02Value(double value) => CallStatic("setTch02Value", value);
public void InitCallback(string objName, string method) => CallStatic("initCallback", objName, method);

View File

@ -122,7 +122,7 @@ namespace Guru
public bool IsDebug => _isDebug;
public void LogEvent(string eventName, string data)
public void LogEvent(string eventName, string data, int priority = -1)
{
#if UNITY_IOS
unityLogEvent(eventName, data);

View File

@ -87,7 +87,7 @@ namespace Guru
public bool IsDebug => _isDebug;
public void LogEvent(string eventName, string parameters)
public void LogEvent(string eventName, string parameters, int priority = -1)
{
if (_isShowLog)
{
@ -127,7 +127,7 @@ namespace Guru
}
}
Debug.Log($"{TAG} LogEvent: event:<color=orange>{eventName}</color> Properties:\n{sb.ToString()}");
Debug.Log($"{TAG} LogEvent: event:<color=orange>{eventName} ({priority})</color> Properties:\n{sb.ToString()}");
}
}
}

View File

@ -10,10 +10,6 @@ Sample Dependencies.xml:
<androidPackage spec="com.mapzen:on-the-road:0.8.1" />
</androidPackages>
<iosPods>
<iosPod name="GuruConsent" bitcodeEnabled="false">
<sources>
<source>git@github.com:castbox/GuruSpecs.git</source>
</sources>
</iosPod>>
<iosPod name="GuruConsent" bitcodeEnabled="false" path="Packages/com.guru.unity.sdk.core/Runtime/GuruConsent/Plugins/iOS" />
</iosPods>
</dependencies>

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 78bd1ca99d374a1f9f31be5cbf38b03c
timeCreated: 1717118600

View File

@ -0,0 +1,40 @@
#
# Be sure to run `pod lib lint CastboxNetwork.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'GuruConsent'
s.version = '1.4.6'
s.summary = 'Google GDPR'
s.description = 'Google GDPR'
s.homepage = 'https://github.com/castbox/GuruConsent-iOS'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'LEE' => 'xiang.li@castbox.fm' }
# s.source = { :git => 'git@github.com:castbox/GuruConsent-iOS.git', :tag => s.version }
s.source = { :tag => s.version }
s.frameworks = 'UIKit', 'AppTrackingTransparency'
s.swift_version = '5.0'
s.ios.deployment_target = '12.0'
s.source_files = "GuruConsent/Sources/**/*.swift"
s.requires_arc = true
s.static_framework = true
s.default_subspec = 'Privacy'
s.dependency 'GoogleUserMessagingPlatform', '2.3.0'
s.subspec 'Privacy' do |ss|
ss.resource_bundles = {
s.name => 'GuruConsent/Resources/PrivacyInfo.xcprivacy'
}
end
end

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4d451434a71243edb55e61e8fd0040a0
timeCreated: 1717035350

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 CastBox Dev Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0819b6a57c25e4ef0bd2f83c97a353fd
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,282 @@
# GuruConsent-iOS
![Swift](https://img.shields.io/badge/Swift-5.0-orange.svg)&nbsp;
## 特性
- [x] 支持系统ATT权限引导弹窗.
- [x] 支持多国语言显示.
- [x] 支持调试EEA地理设置等.
- [x] 支持结果状态回调.
## 准备
将应用 ID 添加到 Info.plist 中:
```
<key>GADApplicationIdentifier</key>
<string>YOUR-APP-ID</string>
```
将ATT跟踪权限添加到 Info.plist 中:
```
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>
```
## 安装
GuruConsent 仅支持CocoaPods.
**CocoaPods - Podfile**
```ruby
source 'git@github.com:castbox/GuruSpecs.git'
pod 'GuruConsent'
```
## 使用
首先导入framework:
Swift:
```swift
import GuruConsent
```
Objective-C:
```objc
#import <GuruConsent/GuruConsent-Swift.h>
```
下面是一些简单示例. 支持所有设备和模拟器:
### 方式一: 自动
如果满足显示条件 自动显示弹窗 具有一定的延迟效果 但具体显示时机不定, 受网络影响.
__建议在应用启动后 延后一些调用开始 请务必确保首次启动后已授权网络请求权限再调用__
Swift:
```swift
// 开始请求
GuruConsent.start(from: controller) { result in
switch result {
case .success(let status):
if #available(iOS 14, *) {
print("ATT 结果: \(ATTrackingManager.trackingAuthorizationStatus)")
}
print("GDPR 结果: \(status)")
case .failure(let error):
print("失败: \(error)")
}
}
```
Objective-C:
```objc
// 开始请求
[GuruConsent startFrom:self success:^(enum GuruConsentGDPRStatus status) {
if (@available(iOS 14, *)) {
NSLog(@"ATT 结果: %lu", (unsigned long)ATTrackingManager.trackingAuthorizationStatus);
}
switch (status) {
case GuruConsentGDPRStatusUnknown:
break;
case GuruConsentGDPRStatusRequired:
break;
case GuruConsentGDPRStatusNotRequired:
break;
case GuruConsentGDPRStatusObtained:
break;
default:
break;
}
NSLog(@"GDPR 结果: %ld", (long)status);
} failure:^(NSError * _Nonnull error) {
NSLog(@"失败: %@", error);
}];
```
### 方式二: 手动
先调用准备, 准备完成后在合适的时机手动调用弹窗显示.
__建议在应用启动后 延后一些调用准备 请务必确保首次启动后已授权网络请求权限再调用__
Swift:
```swift
// 准备
GuruConsent.prepare { result in
switch result {
case .success(let status):
print("GDPR 结果: \(status)")
case .failure(let error):
print("失败: \(error)")
}
}
// 显示 请确保status为.required 否则无法显示
GuruConsent.present(from: self) { result in
switch result {
case .success(let status):
if #available(iOS 14, *) {
print("ATT 结果: \(ATTrackingManager.trackingAuthorizationStatus)")
}
print("GDPR 结果: \(status)")
case .failure(let error):
print("失败: \(error)")
}
}
```
Objective-C:
```objc
// 准备
[GuruConsent prepareWithSuccess:^(enum GuruConsentGDPRStatus status) {
switch (status) {
case GuruConsentGDPRStatusUnknown:
break;
case GuruConsentGDPRStatusRequired:
break;
case GuruConsentGDPRStatusNotRequired:
break;
case GuruConsentGDPRStatusObtained:
break;
default:
break;
}
NSLog(@"GDPR 结果: %ld", (long)status);
} failure:^(NSError * _Nonnull error) {
NSLog(@"失败: %@", error);
}];
// 显示 请确保status为.required 否则无法显示
[GuruConsent presentFrom:self success:^(enum GuruConsentGDPRStatus status) {
if (@available(iOS 14, *)) {
NSLog(@"ATT 结果: %lu", (unsigned long)ATTrackingManager.trackingAuthorizationStatus);
}
switch (status) {
case GuruConsentGDPRStatusUnknown:
break;
case GuruConsentGDPRStatusRequired:
break;
case GuruConsentGDPRStatusNotRequired:
break;
case GuruConsentGDPRStatusObtained:
break;
default:
break;
}
NSLog(@"GDPR 结果: %ld", (long)status);
} failure:^(NSError * _Nonnull error) {
NSLog(@"%@", error);
}];
```
### 调试设置
`testDeviceIdentifiers`获取方式: 当传入空置, 运行调用`GuruConsent.start(from:)` Xcode控制台会输出如下:
```
<UMP SDK> To enable debug mode for this device, set: UMPDebugSettings.testDeviceIdentifiers = @[ @"8C5E8576-5090-4C41-8FC4-A5A80FF77D9E" ];
```
将控制台的`8C5E8576-5090-4C41-8FC4-A5A80FF77D9E` 复制粘贴到代码中, 再次运行即可进行调试.
Swift:
```swift
// 设置调试配置
let debug = GuruConsent.DebugSettings()
debug.testDeviceIdentifiers = ["8C5E8576-5090-4C41-8FC4-A5A80FF77D9E"]
debug.geography = .EEA
GuruConsent.debug = debug
```
Objective-C:
```objc
// 设置调试配置
GuruConsentDebugSettings *debug = [[GuruConsentDebugSettings alloc] init];
debug.testDeviceIdentifiers = @[@"8C5E8576-5090-4C41-8FC4-A5A80FF77D9E"];
debug.geography = GuruConsentDebugSettingsGeographyEEA;
GuruConsent.debug = debug;
```
重置状态
```swift
GuruConsent.reset()
```
## 运行
### 未授权过ATT权限 (非EEA地区):
![IMG_4886](https://user-images.githubusercontent.com/13112992/201629493-3b95e3e8-ca02-41b6-9a64-1acd11ea4261.PNG)
点击`Continue`按钮弹出ATT授权弹窗
![IMG_4887](https://user-images.githubusercontent.com/13112992/201629676-3ca39406-513a-46ec-b79e-60df4fd7cc88.PNG)
### EEA地区:
![IMG_4888](https://user-images.githubusercontent.com/13112992/201629612-b736c439-54fe-4c59-97ee-71a6a449f23a.PNG)
未授权过ATT权限 点击同意等按钮弹出ATT授权弹窗
![Simulator Screen Shot - iPhone 14 Pro - 2022-11-14 at 16 40 48](https://user-images.githubusercontent.com/13112992/201629990-ec3776b2-6bd0-4c3e-aba4-48f093216e11.png)
## 参考
[官方文档](https://developers.google.com/admob/ump/ios/quick-start)
## 协议
GuruConsent 使用 MIT 协议. 有关更多信息,请参阅 [LICENSE](LICENSE) 文件.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2591a1ab95ce94235adc38c94f28f029
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 30771cebf05b44ad19739c67642b61ce
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeCoarseLocation</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypePerformanceData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
</array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
</dict>
</array>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,85 @@
fileFormatVersion: 2
guid: 71f33675e1c99450686a549ba5f04505
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9c2ff42f8bf73454290c2c13d6de943d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,121 @@
//
// GuruConsent.GDPR.swift
// GuruConsent
//
// Created by on 2022/11/11.
//
import UIKit
import UserMessagingPlatform
public extension GuruConsent {
///
@objc
public static var status: GDPRStatus {
return .init(
rawValue: UMPConsentInformation.sharedInstance.consentStatus.rawValue
) ?? .unknown
}
/// 广 (status.obtained.notRequired)
@objc
public static var canRequestAds: Bool {
return UMPConsentInformation.sharedInstance.canRequestAds
}
///
/// .unknown status.notRequired .notRequired
/// status.obtained() .required,
/// `func openPrivacyOptions(from: with:)`
/// , .unknown
@objc
public static var privacyOptionsRequirementStatus: GDPRPrivacyOptionsRequirementStatus {
return .init(
rawValue: UMPConsentInformation.sharedInstance.privacyOptionsRequirementStatus.rawValue
) ?? .unknown
}
private static var form: UMPConsentForm?
internal static func request(with completion: @escaping ((Swift.Result<GDPRFormStatus, Error>) -> Void)) {
let parameters = UMPRequestParameters()
// false
parameters.tagForUnderAgeOfConsent = tagForUnderAgeOfConsent
//
if let debug = GuruConsent.debug {
let debugSettings = UMPDebugSettings()
debugSettings.testDeviceIdentifiers = debug.testDeviceIdentifiers
debugSettings.geography = .init(rawValue: debug.geography.rawValue) ?? .disabled
parameters.debugSettings = debugSettings
}
//
UMPConsentInformation.sharedInstance.requestConsentInfoUpdate(
with: parameters,
completionHandler: { error in
if let error = error {
//
completion(.failure(error))
} else {
let status = UMPConsentInformation.sharedInstance.formStatus
completion(.success(.init(rawValue: status.rawValue) ?? .unknown))
}
}
)
}
static func loadForm(with completion: @escaping ((Swift.Result<GDPRStatus, Error>) -> Void)) {
//
UMPConsentForm.load { form, error in
if let error = error {
//
completion(.failure(error))
} else {
self.form = form
let status = UMPConsentInformation.sharedInstance.consentStatus
completion(.success(.init(rawValue: status.rawValue) ?? .unknown))
}
}
}
static func openForm(from controller: UIViewController, with completion: @escaping ((Swift.Result<GDPRStatus, Error>) -> Void)) {
guard let form = form else {
completion(.failure(NSError(domain: "Form Empty.", code: -1)))
return
}
//
form.present(
from: controller,
completionHandler: { error in
if let error = error {
//
completion(.failure(error))
} else {
//
completion(.success(status))
}
}
)
}
///
@objc
public static func reset() {
UMPConsentInformation.sharedInstance.reset()
}
/// (privacyOptionsRequirementStatus.required)
/// - Parameters:
/// - controller:
/// - completion:
@objc
public static func openPrivacyOptions(from controller: UIViewController, with completion: @escaping ((Error?) -> Void)) {
UMPConsentForm.presentPrivacyOptionsForm(from: controller) { error in
completion(error)
}
}
}

View File

@ -0,0 +1,49 @@
fileFormatVersion: 2
guid: 604558f3813844f3eaa13f84fde7e522
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
iPhone: iOS
second:
enabled: 0
settings: {}
- first:
tvOS: tvOS
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More