Compare commits

..

1 Commits

Author SHA1 Message Date
胡宇飞 c3b77622d9 update: 更新 Adjust 的 DeepLink 功能
--story=1020917 --user=yufei.hu 【中台】【SDK】新增 Adjust Deeplink 接口 https://www.tapd.cn/33527076/s/1156644

Signed-off-by: huyufei <yufei.hu@castbox.fm>
2024-07-12 10:29:39 +08:00
149 changed files with 594 additions and 6749 deletions

View File

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

View File

@ -1,119 +0,0 @@
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

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

View File

@ -77,8 +77,6 @@ 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(2000)]
[PostProcessBuild(40)]
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,7 +6,6 @@
"MaxSdk",
"MaxSdk.Scripts",
"Amazon",
"Amazon.Scripts",
"OpenWrapSDK",
"UniWebView-CSharp",
"UnityEngine.Purchasing",
@ -18,9 +17,7 @@
"Google.Play.Review",
"Google.Play.Common",
"Guru.LitJson",
"Unity.Advertisement.IosSupport",
"Unity.Notifications.Android",
"Unity.Notifications.iOS"
"Unity.Advertisement.IosSupport"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,81 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

@ -1,80 +0,0 @@
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

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

View File

@ -20,11 +20,16 @@ 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" bitcodeEnabled="false" path="Packages/com.guru.unity.sdk.core/Runtime/GuruAnalytics/Plugins/iOS" />
<iosPod name="GuruAnalyticsLib" version="0.3.3" bitcodeEnabled="false">
<sources>
<source>git@github.com:castbox/GuruSpecs.git</source>
</sources>
</iosPod>
<iosPod name="JJException" bitcodeEnabled="false" />
</iosPods>
</dependencies>

View File

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

View File

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

View File

@ -1,32 +0,0 @@
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

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

View File

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

View File

@ -1,41 +0,0 @@
<?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

@ -1,85 +0,0 @@
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

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

View File

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

View File

@ -1,158 +0,0 @@
//
// 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

@ -1,85 +0,0 @@
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

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

View File

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

View File

@ -1,228 +0,0 @@
//
// 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

@ -1,85 +0,0 @@
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

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

View File

@ -1,391 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

@ -1,681 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

@ -1,65 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

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

View File

@ -1,36 +0,0 @@
//
// 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

@ -1,85 +0,0 @@
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

@ -1,51 +0,0 @@
//
// 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

@ -1,85 +0,0 @@
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

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

View File

@ -1,78 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

@ -1,419 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

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

View File

@ -1,160 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

@ -1,54 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

@ -1,25 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

@ -1,28 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

@ -1,158 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

@ -1,32 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

@ -1,158 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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

@ -1,54 +0,0 @@
#
# 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

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

View File

@ -2,42 +2,10 @@
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,11 +14,10 @@ namespace Guru
public class GuruAnalytics
{
// Plugin Version
public const string Version = "1.10.5";
public const string Version = "1.10.4";
public static readonly string Tag = "[ANU]";
private static readonly string ActionName = "logger_error";
internal const int EventPriorityDefault = 10;
private static IAnalyticsAgent _agent;
@ -86,8 +85,6 @@ 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);
@ -201,8 +198,7 @@ namespace Guru
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="data">INT类型的值</param>
/// <param name="priority"></param>
public static void LogEvent(string eventName, Dictionary<string, dynamic> data = null, int priority = -1)
public static void LogEvent(string eventName, Dictionary<string, dynamic> data = null)
{
if(_autoSyncProperties)
UpdateAllUserProperties(); // 每次打点更新用户属性
@ -212,9 +208,8 @@ namespace Guru
{
raw = BuildParamsJson(data);
}
if (priority < 0) priority = EventPriorityDefault;
Debug.Log($"{Tag} event:{eventName} | raw: {raw} | priority: {priority}");
Agent?.LogEvent(eventName, raw, priority);
Debug.Log($"{Tag} event:{eventName} | raw: {raw}");
Agent?.LogEvent(eventName, raw);
}
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, int priority = -1);
void LogEvent(string eventName, string parameters);
void ReportEventSuccessRate(); // 上报任务成功率
void SetTch02Value(double value); // 设置太极02数值
void InitCallback(string objName, string method); // 设置回调对象参数

View File

@ -80,8 +80,7 @@ namespace Guru
{
_isDebug = isDebug;
string bundleId = Application.identifier;
// 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); // 调用接口
CallStatic("init", appId, deviceInfo, bundleId, UseWorker, isDebug, UseCronet, BaseUrl); // 调用接口
}
public void SetScreen(string screenName)
@ -125,10 +124,7 @@ namespace Guru
}
public bool IsDebug => CallStatic<bool>("isDebug");
public void LogEvent(string eventName, string parameters, int priority = -1)
{
CallStatic("logEvent", eventName, parameters, priority);
}
public void LogEvent(string eventName, string parameters) => CallStatic("logEvent", eventName, parameters);
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, int priority = -1)
public void LogEvent(string eventName, string data)
{
#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, int priority = -1)
public void LogEvent(string eventName, string parameters)
{
if (_isShowLog)
{
@ -127,7 +127,7 @@ namespace Guru
}
}
Debug.Log($"{TAG} LogEvent: event:<color=orange>{eventName} ({priority})</color> Properties:\n{sb.ToString()}");
Debug.Log($"{TAG} LogEvent: event:<color=orange>{eventName}</color> Properties:\n{sb.ToString()}");
}
}
}

View File

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

View File

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

View File

@ -1,40 +0,0 @@
#
# 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

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

View File

@ -1,21 +0,0 @@
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

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

View File

@ -1,282 +0,0 @@
# 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

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

View File

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

View File

@ -1,60 +0,0 @@
<?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

@ -1,85 +0,0 @@
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

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

View File

@ -1,121 +0,0 @@
//
// 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

@ -1,49 +0,0 @@
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:

View File

@ -1,204 +0,0 @@
//
// GuruConsent.swift
// GuruConsent
//
// Created by on 2022/11/11.
//
import UIKit
@objc
public class GuruConsent: NSObject {
/// CDPR
@objc(GuruConsentGDPRStatus)
public enum GDPRStatus: Int {
case unknown ///< Unknown consent status.
case required ///< User consent required but not yet obtained.
case notRequired ///< Consent not required.
case obtained ///< User consent obtained, personalized vs non-personalized undefined.
}
/// CDPR
internal enum GDPRFormStatus: Int {
case unknown
case available
case unavailable
}
/// GDPR
@objc(GuruConsentGDPRPrivacyOptionsRequirementStatus)
public enum GDPRPrivacyOptionsRequirementStatus: Int {
case unknown ///< Requirement unknown.
case required ///< A way must be provided for the user to modify their privacy options.
case notRequired ///< User does not need to modify their privacy options. Either consent is not required, or the consent type does not require modification.
}
///
@objc(GuruConsentDebugSettings)
public class DebugSettings: NSObject {
@objc(GuruConsentDebugSettingsGeography)
public enum Geography: Int {
case disabled
case EEA
case notEEA
}
/// ID
@objc
public var testDeviceIdentifiers: [String]
///
@objc
public var geography: Geography
@objc
public override init() {
testDeviceIdentifiers = []
geography = .disabled
}
}
///
@objc
public static var debug: DebugSettings?
/// false
@objc
public static var tagForUnderAgeOfConsent: Bool = false
/// (status.obtained)
@objc
public static var isObtained: Bool {
return status == .obtained
}
/// OC
/// ATT(EEA) ATT ATT
/// ATT(EEA) GDPR ATT
/// - Parameters:
/// - controller:
/// - completion:
@objc
public static func start(from controller: UIViewController, success: @escaping ((GDPRStatus) -> Void), failure: @escaping (Error) -> Void) {
start(from: controller) { result in
switch result {
case .success(let value):
success(value)
case .failure(let error):
failure(error)
}
}
}
/// Swift
/// ATT(EEA) ATT ATT
/// ATT(EEA) GDPR ATT
/// - Parameters:
/// - controller:
/// - completion:
public static func start(from controller: UIViewController, with completion: @escaping ((Swift.Result<GDPRStatus, Error>) -> Void)) {
request { result in
switch result {
case .success(let value):
if value == .available {
//
loadForm { result in
switch result {
case .success(let value):
switch value {
case .required:
//
openForm(from: controller, with: completion)
default:
completion(.success(value))
}
case .failure(let error):
//
completion(.failure(error))
}
}
} else {
//
completion(.success(.notRequired))
}
case .failure(let error):
//
completion(.failure(error))
}
}
}
/// ----------------------------------------
@objc
public static func prepare(success: @escaping ((GDPRStatus) -> Void), failure: @escaping (Error) -> Void) {
prepare { result in
switch result {
case .success(let value):
success(value)
case .failure(let error):
failure(error)
}
}
}
@objc
public static func present(from controller: UIViewController, success: @escaping ((GDPRStatus) -> Void), failure: @escaping (Error) -> Void) {
present(from: controller) { result in
switch result {
case .success(let value):
success(value)
case .failure(let error):
failure(error)
}
}
}
/// ( )
/// - Parameter completion:
public static func prepare(with completion: @escaping ((Swift.Result<GDPRStatus, Error>) -> Void)) {
request { result in
switch result {
case .success(let value):
if value == .available {
//
loadForm { result in
switch result {
case .success(let value):
//
completion(.success(value))
case .failure(let error):
//
completion(.failure(error))
}
}
} else {
//
completion(.success(.notRequired))
}
case .failure(let error):
//
completion(.failure(error))
}
}
}
/// status.required
/// - Parameters:
/// - controller:
/// - completion:
public static func present(from controller: UIViewController, with completion: @escaping ((Swift.Result<GDPRStatus, Error>) -> Void)) {
openForm(from: controller, with: completion)
}
}

View File

@ -1,49 +0,0 @@
fileFormatVersion: 2
guid: 0b11b5b1fa30a432eaf1b02602717dc9
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