Signed-off-by: Haoyi <haoyi.zhang@castbox.fm>
main 0.3.5
Haoyi 2024-09-04 13:01:33 +08:00
commit de45dd6e81
38 changed files with 4139 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# macOS
.DS_Store
# Xcode
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
# Bundler
.bundle
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
#
# Note: if you ignore the Pods directory, make sure to uncomment
# `pod install` in .travis.yml
#
# Pods/
Example/Pods
Podfile.lock

150
CHANGELOG.md Normal file
View File

@ -0,0 +1,150 @@
## v0.3.5
- 接口更新:
- 日志打包方法eventsLogsArchive废弃使用eventsLogsDirectory获取文件夹URL
## v0.3.4
- 配置privacy manifest文件增加隐私项:
- 隐私API访问:
- System boot time APIs
- File timestamp APIs
- User defaults APIs
## v0.3.3
- feature
- 增加获取当前user properties接口
- getUserProperties() -> [String : String]
- 增加upload开关切换接口
- setEnableUpload(isOn: Bool = true) -> Void
## v0.3.2
- bugfix
- 修复一些字典访问的崩溃问题
## v0.3.1
- feature
- 重命名
- registerInternalEventObserver(reportCallback: @escaping (_ eventCode: Int, _ info: String) -> Void)
## v0.3.0
- feature
- 增加注册网络层错误监听接口
- registerErrorReporter(reportCallback: @escaping (_ errorCode: Int, _ info: String) -> Void)
## v0.2.17
- feature
- 增加清除user property接口
- removeUserProperties(forNames names: [String])
## v0.2.16
- feature
- 增加fg打点保证轮询上传的每批事件中包含一个fg事件。
## v0.2.15
- feature
- 初始化过程中延迟500ms打一个fg事件。
## v0.2.14
- bugfix
- 修复logEvent(_ name: String, parameters: [String : Any]?)parameters中int类型未解析的错误。
## v0.2.13
- feature
- 请求header中添加中台标准字段X-APP-IDX-DEVICE-INFO对应值由应用层从初始化方法中传入。
- initializeLib(..., **saasXAPPID: String, saasXDEVICEINFO: String,** ...)
## v0.2.12
- feature
- 上传服务器每个event增加eventId字段采用uuid4算法小写。
- bugfix
- 将放置到info内的字段从userProperty中移除。
## v0.2.11
- optimization
- fg计算逻辑调整为每间隔1s更新一次缓存累计时长切入后台后记录fg事件缓存累计时长置零。
## v0.2.10
- optimization
- app切入后台上传events任务等待fg事件入库后再执行。
## v0.2.9
- feature
- 增加设置events host name接口setEventsUploadEndPoint(host: String?)
## v0.2.8
- bugfix
- 修复fg时间异常问题将计算时间差所用起始系统时间和当前系统时间均替换为cpu时间。
- 修复当前服务器时间计算逻辑bug。
## v0.2.7
- bugfix
- 修复fg可能出现负值的情况
## v0.2.6
- bugfix
- 修复后台任务超时时未及时调用结束任务造成app被系统终止的问题
## v0.2.5
- bugfix
- 修复Manager成员userProperty多线程访问引起的崩溃
- 修复manager工作队列发生阻塞时block上传任务问题
## v0.2.4
- bugfix
- podspec文件回滚对subspec的配置lint无法通过。
## v0.2.3
- bugfix
- 修复Manager成员userProperty多线程访问引起的崩溃
## v0.2.2
- public接口适配objc可供OC调用
## v0.2.1
- 调整IP直联时服务器证书验证规则
- 上传任务适配后台运行
- 增加文件日志功能,提供日志文件打包导出接口
eventsLogsArchive(_ callback: @escaping (_ url: URL?) -> Void)
## v0.2.0
- 支持HTTPDNS
## v0.1.1
- 将原来设置成员变量方式替换为通过初始化接口传值
initializeLib(uploadPeriodInSecond: Double, batchLimit: Int, eventExpiredSeconds: Double, initializeTimeout: Double, loggerDebug: Bool)
- 间隔`uploadPeriodInSecond`秒后打包上传`batchLimit`个数据
- 支持延时记录event的逻辑初始化后等待user id/device id/firebase pseudo id等属性超时`initializeTimeout`秒
- 支持事件优先级
| ***NAME*** | ***PRIORITY*** |
| :-------- | :--------: |
| `EMERGENCE` | 0 |
| `HIGH` | 5 |
| `DEFAULT` | 10 |
| `LOW` | 15 |
- 首次初始化时自动上报first_open点并将该点以`EMERGENCE`优先级发送
## v0.1.0
- 支持App生命周期自打点`fg`,并根据前后台时间计算`duration`
- 首次初始化时记录first_open_time
- 提供下列接口
| ***Method*** | ***Description*** |
| :-------- | :--------: |
| `logEvent` | 所有event点都是通过该函数完成 |
| `setUserProperty` | 设置用户属性 |
| `setScreen` | 设置当前屏幕 |
| `setDeviceId` | 设置设备ID |
| `setUid` | 设置用户ID |
| `setAdjustId` | 设置AdjustId |
| `setAdId` | 设置Google Ad Id |
| `setFirebaseId` | 设置Firebase Id |

View File

@ -0,0 +1,430 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
63E4E6CEDD0672A6AB498174 /* Pods_GuruAnalytics_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B1F595BB712FF36838F4A125 /* Pods_GuruAnalytics_Example.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
267F05352936F18F00D72EFD /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = "<group>"; };
26BF34CF292C8122001CFE57 /* MyPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = MyPlayground.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
37274AD5F3F5B3AAD2714B34 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
407E9D4F744C1F686237011A /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
5CD9DCBF7AE700FB60660DB4 /* Pods_GuruAnalytics_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GuruAnalytics_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD01AFB9204008FA782 /* GuruAnalytics_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GuruAnalytics_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
B1F595BB712FF36838F4A125 /* Pods_GuruAnalytics_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GuruAnalytics_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D30C5441D6D5CBD750E76657 /* Pods-GuruAnalytics_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GuruAnalytics_Example.debug.xcconfig"; path = "Target Support Files/Pods-GuruAnalytics_Example/Pods-GuruAnalytics_Example.debug.xcconfig"; sourceTree = "<group>"; };
DC74B33D46880B782C485C13 /* GuruAnalyticsLib.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = GuruAnalyticsLib.podspec; path = ../GuruAnalyticsLib.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
F992AC1E7C4013032773C33F /* Pods-GuruAnalytics_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GuruAnalytics_Example.release.xcconfig"; path = "Target Support Files/Pods-GuruAnalytics_Example/Pods-GuruAnalytics_Example.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
607FACCD1AFB9204008FA782 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
63E4E6CEDD0672A6AB498174 /* Pods_GuruAnalytics_Example.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
607FACC71AFB9204008FA782 = {
isa = PBXGroup;
children = (
607FACF51AFB993E008FA782 /* Podspec Metadata */,
607FACD21AFB9204008FA782 /* Example for GuruAnalytics */,
607FACD11AFB9204008FA782 /* Products */,
8ABE29700505470322F45254 /* Pods */,
81BE026791FA842AE71ACD3B /* Frameworks */,
);
sourceTree = "<group>";
};
607FACD11AFB9204008FA782 /* Products */ = {
isa = PBXGroup;
children = (
607FACD01AFB9204008FA782 /* GuruAnalytics_Example.app */,
);
name = Products;
sourceTree = "<group>";
};
607FACD21AFB9204008FA782 /* Example for GuruAnalytics */ = {
isa = PBXGroup;
children = (
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
607FACD71AFB9204008FA782 /* ViewController.swift */,
607FACD91AFB9204008FA782 /* Main.storyboard */,
607FACDC1AFB9204008FA782 /* Images.xcassets */,
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
607FACD31AFB9204008FA782 /* Supporting Files */,
26BF34CF292C8122001CFE57 /* MyPlayground.playground */,
);
name = "Example for GuruAnalytics";
path = GuruAnalytics;
sourceTree = "<group>";
};
607FACD31AFB9204008FA782 /* Supporting Files */ = {
isa = PBXGroup;
children = (
607FACD41AFB9204008FA782 /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
607FACF51AFB993E008FA782 /* Podspec Metadata */ = {
isa = PBXGroup;
children = (
DC74B33D46880B782C485C13 /* GuruAnalyticsLib.podspec */,
407E9D4F744C1F686237011A /* README.md */,
267F05352936F18F00D72EFD /* CHANGELOG.md */,
37274AD5F3F5B3AAD2714B34 /* LICENSE */,
);
name = "Podspec Metadata";
sourceTree = "<group>";
};
81BE026791FA842AE71ACD3B /* Frameworks */ = {
isa = PBXGroup;
children = (
B1F595BB712FF36838F4A125 /* Pods_GuruAnalytics_Example.framework */,
5CD9DCBF7AE700FB60660DB4 /* Pods_GuruAnalytics_Tests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
8ABE29700505470322F45254 /* Pods */ = {
isa = PBXGroup;
children = (
D30C5441D6D5CBD750E76657 /* Pods-GuruAnalytics_Example.debug.xcconfig */,
F992AC1E7C4013032773C33F /* Pods-GuruAnalytics_Example.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
607FACCF1AFB9204008FA782 /* GuruAnalytics_Example */ = {
isa = PBXNativeTarget;
buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "GuruAnalytics_Example" */;
buildPhases = (
12A998D33B96E401B841E243 /* [CP] Check Pods Manifest.lock */,
607FACCC1AFB9204008FA782 /* Sources */,
607FACCD1AFB9204008FA782 /* Frameworks */,
607FACCE1AFB9204008FA782 /* Resources */,
86E7176C5034831947DC0310 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = GuruAnalytics_Example;
productName = GuruAnalytics;
productReference = 607FACD01AFB9204008FA782 /* GuruAnalytics_Example.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
607FACC81AFB9204008FA782 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastUpgradeCheck = 0830;
ORGANIZATIONNAME = CocoaPods;
TargetAttributes = {
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
LastSwiftMigration = 0900;
};
};
};
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "GuruAnalytics" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
Base,
);
mainGroup = 607FACC71AFB9204008FA782;
productRefGroup = 607FACD11AFB9204008FA782 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
607FACCF1AFB9204008FA782 /* GuruAnalytics_Example */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
607FACCE1AFB9204008FA782 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */,
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
12A998D33B96E401B841E243 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-GuruAnalytics_Example-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
86E7176C5034831947DC0310 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-GuruAnalytics_Example/Pods-GuruAnalytics_Example-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-GuruAnalytics_Example/Pods-GuruAnalytics_Example-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GuruAnalytics_Example/Pods-GuruAnalytics_Example-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
607FACCC1AFB9204008FA782 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
607FACD91AFB9204008FA782 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
607FACDA1AFB9204008FA782 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = {
isa = PBXVariantGroup;
children = (
607FACDF1AFB9204008FA782 /* Base */,
);
name = LaunchScreen.xib;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
607FACED1AFB9204008FA782 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
607FACEE1AFB9204008FA782 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
607FACF01AFB9204008FA782 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D30C5441D6D5CBD750E76657 /* Pods-GuruAnalytics_Example.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = 43U6TB4QK3;
INFOPLIST_FILE = GuruAnalytics/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
607FACF11AFB9204008FA782 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F992AC1E7C4013032773C33F /* Pods-GuruAnalytics_Example.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = 43U6TB4QK3;
INFOPLIST_FILE = GuruAnalytics/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "GuruAnalytics" */ = {
isa = XCConfigurationList;
buildConfigurations = (
607FACED1AFB9204008FA782 /* Debug */,
607FACEE1AFB9204008FA782 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "GuruAnalytics_Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
607FACF01AFB9204008FA782 /* Debug */,
607FACF11AFB9204008FA782 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 607FACC81AFB9204008FA782 /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:GuruAnalytics.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "GuruAnalytics_Example.app"
BlueprintName = "GuruAnalytics_Example"
ReferencedContainer = "container:GuruAnalytics.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACE41AFB9204008FA782"
BuildableName = "GuruAnalytics_Tests.xctest"
BlueprintName = "GuruAnalytics_Tests"
ReferencedContainer = "container:GuruAnalytics.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACE41AFB9204008FA782"
BuildableName = "GuruAnalytics_Tests.xctest"
BlueprintName = "GuruAnalytics_Tests"
ReferencedContainer = "container:GuruAnalytics.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "GuruAnalytics_Example.app"
BlueprintName = "GuruAnalytics_Example"
ReferencedContainer = "container:GuruAnalytics.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "GuruAnalytics_Example.app"
BlueprintName = "GuruAnalytics_Example"
ReferencedContainer = "container:GuruAnalytics.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "GuruAnalytics_Example.app"
BlueprintName = "GuruAnalytics_Example"
ReferencedContainer = "container:GuruAnalytics.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:GuruAnalytics.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,67 @@
//
// AppDelegate.swift
// GuruAnalytics
//
// Created by devSC on 11/16/2022.
// Copyright (c) 2022 devSC. All rights reserved.
//
import UIKit
import GuruAnalyticsLib
import RxSwift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// _ = NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification).take(1)
// .asSingle()
// .delay(.seconds(2), scheduler: MainScheduler.asyncInstance)
// .subscribe(onSuccess: { _ in
GuruAnalytics.initializeLib(uploadPeriodInSecond: 60,
batchLimit: 25,
initializeTimeout: 5,
saasXAPPID: "test_app_id",
saasXDEVICEINFO: "appIdentifier=test.app.example;appVersion=1.0;deviceType=apple;deviceCountry=CN")
GuruAnalytics.setUserID("100004")
GuruAnalytics.setAdjustId("xsdfal021sxasdfl")
GuruAnalytics.setDeviceId(UUID().uuidString)
GuruAnalytics.setScreen("home")
GuruAnalytics.registerInternalEventObserver { errorCode, info in
print("")
}
// })
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2015 CocoaPods. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<rect key="frame" x="20" y="439" width="441" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="GuruAnalytics_iOS" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
<rect key="frame" x="20" y="140" width="441" height="43"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC"/>
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk"/>
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l"/>
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0"/>
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9"/>
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="548" y="455"/>
</view>
</objects>
</document>

View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="ufC-wZ-h7g">
<objects>
<viewController id="vXZ-lx-hvc" customClass="ViewController" customModule="GuruAnalytics_Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="jyV-Pf-zRb"/>
<viewControllerLayoutGuide type="bottom" id="2fi-mo-0CV"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="kh9-bI-dsS">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UiT-xC-g9f">
<rect key="frame" x="150.5" y="125" width="74" height="35"/>
<constraints>
<constraint firstAttribute="height" constant="35" id="clk-M7-Fmx"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="Delete"/>
<connections>
<action selector="deleteItem:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="qxv-Dq-ok1"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="MFg-e1-QYB">
<rect key="frame" x="121.5" y="180" width="132" height="35"/>
<constraints>
<constraint firstAttribute="height" constant="35" id="hZX-Nz-pHw"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="set firebase id"/>
<connections>
<action selector="setFirebaseId:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="iaO-7l-pOx"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="P7y-en-hqa">
<rect key="frame" x="141" y="235" width="93" height="35"/>
<constraints>
<constraint firstAttribute="height" constant="35" id="q4X-qi-ks2"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="Get Logs"/>
<connections>
<action selector="getLogs:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="fKR-om-I4f"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="G3I-za-UMF">
<rect key="frame" x="132.5" y="290" width="110" height="35"/>
<constraints>
<constraint firstAttribute="height" constant="35" id="2Fo-sC-mX5"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="Start Timer"/>
<connections>
<action selector="startTimer:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="aj0-1v-Trc"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hDD-Ie-zVC">
<rect key="frame" x="133.5" y="345" width="108" height="35"/>
<constraints>
<constraint firstAttribute="height" constant="35" id="WAl-TP-kMI"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="Stop Timer"/>
<connections>
<action selector="stopTimer:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="s1z-ZZ-0bX"/>
</connections>
</button>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="wDL-gO-FHP">
<rect key="frame" x="24" y="400" width="327" height="31"/>
<connections>
<action selector="onSliderAction:" destination="vXZ-lx-hvc" eventType="valueChanged" id="Vhs-Q3-lRm"/>
</connections>
</slider>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6oL-kc-ZkC">
<rect key="frame" x="153.5" y="70" width="68" height="35"/>
<constraints>
<constraint firstAttribute="height" constant="35" id="IMO-pR-HdA"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="Insert"/>
<connections>
<action selector="create:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="Zut-Np-82g"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IoV-jb-3en">
<rect key="frame" x="100.5" y="450" width="174.5" height="31"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enable Upload" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="imo-Fc-VhE">
<rect key="frame" x="0.0" y="0.0" width="110.5" height="31"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="PzM-jf-3Zn">
<rect key="frame" x="125.5" y="0.0" width="51" height="31"/>
<connections>
<action selector="onEnableUploadSwitcher:" destination="vXZ-lx-hvc" eventType="valueChanged" id="b3h-gD-Cxk"/>
</connections>
</switch>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="PzM-jf-3Zn" secondAttribute="trailing" id="9wc-B1-IHF"/>
<constraint firstItem="PzM-jf-3Zn" firstAttribute="top" secondItem="IoV-jb-3en" secondAttribute="top" id="BoM-nX-inP"/>
<constraint firstItem="PzM-jf-3Zn" firstAttribute="leading" secondItem="imo-Fc-VhE" secondAttribute="trailing" constant="15" id="THp-97-usz"/>
<constraint firstAttribute="bottom" secondItem="PzM-jf-3Zn" secondAttribute="bottom" id="UJN-XS-pIl"/>
<constraint firstItem="imo-Fc-VhE" firstAttribute="leading" secondItem="IoV-jb-3en" secondAttribute="leading" id="fbI-2d-yHW"/>
<constraint firstAttribute="bottom" secondItem="imo-Fc-VhE" secondAttribute="bottom" id="nLV-ho-ab5"/>
<constraint firstItem="imo-Fc-VhE" firstAttribute="top" secondItem="IoV-jb-3en" secondAttribute="top" id="wTY-dv-3Tv"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="UiT-xC-g9f" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="0nd-lb-t1t"/>
<constraint firstItem="G3I-za-UMF" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="7yj-Lv-JX9"/>
<constraint firstItem="6oL-kc-ZkC" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" constant="50" id="8hA-7d-tDI"/>
<constraint firstItem="G3I-za-UMF" firstAttribute="top" secondItem="P7y-en-hqa" secondAttribute="bottom" constant="20" id="BGD-WM-vcu"/>
<constraint firstItem="wDL-gO-FHP" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leadingMargin" constant="10" id="DGc-Ts-Qth"/>
<constraint firstItem="MFg-e1-QYB" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="NdO-nP-Diu"/>
<constraint firstItem="P7y-en-hqa" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="P2P-Q3-Fz3"/>
<constraint firstItem="P7y-en-hqa" firstAttribute="top" secondItem="MFg-e1-QYB" secondAttribute="bottom" constant="20" id="RfT-JT-d4s"/>
<constraint firstItem="IoV-jb-3en" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="V09-Ub-KVG"/>
<constraint firstItem="hDD-Ie-zVC" firstAttribute="top" secondItem="G3I-za-UMF" secondAttribute="bottom" constant="20" id="VhL-0o-Kp4"/>
<constraint firstItem="IoV-jb-3en" firstAttribute="top" secondItem="wDL-gO-FHP" secondAttribute="bottom" constant="20" id="Yxs-CC-aBR"/>
<constraint firstAttribute="trailingMargin" secondItem="wDL-gO-FHP" secondAttribute="trailing" constant="10" id="ZlY-n5-meR"/>
<constraint firstItem="6oL-kc-ZkC" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="cOt-z8-AaJ"/>
<constraint firstItem="IoV-jb-3en" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="20" id="cwu-l4-u6z"/>
<constraint firstItem="UiT-xC-g9f" firstAttribute="top" secondItem="6oL-kc-ZkC" secondAttribute="bottom" constant="20" id="gpg-kk-w0q"/>
<constraint firstItem="hDD-Ie-zVC" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="n2v-EB-mYi"/>
<constraint firstItem="wDL-gO-FHP" firstAttribute="top" secondItem="hDD-Ie-zVC" secondAttribute="bottom" constant="20" id="nqa-66-evV"/>
<constraint firstItem="MFg-e1-QYB" firstAttribute="top" secondItem="UiT-xC-g9f" secondAttribute="bottom" constant="20" id="sSY-mT-dVD"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="136.80000000000001" y="-0.44977511244377816"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@ -0,0 +1,53 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,39 @@
<?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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,7 @@
import UIKit
var greeting = "Hello, playground"
let sss = ["1", "2", "3"].map({ "'\($0)'"
}).joined(separator: ",")
print("value in (\(sss))")

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='5.0' target-platform='ios' buildActiveScheme='true' importAppTypes='true'>
<timeline fileName='timeline.xctimeline'/>
</playground>

View File

@ -0,0 +1,112 @@
//
// ViewController.swift
// GuruAnalytics_iOS
//
// Created by mayue on 11/07/2022.
// Copyright (c) 2022 mayue. All rights reserved.
//
import UIKit
import GuruAnalyticsLib
import MessageUI
import RxSwift
class ViewController: UIViewController {
private var timer1: Disposable?
private var timer2: Disposable?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
@IBAction func setFirebaseId(_ sender: Any) {
GuruAnalytics.setFirebaseId("2312:3XSFA0211231")
}
@IBAction func create(_ sender: Any) {
GuruAnalytics.setScreen("home")
GuruAnalytics.logEvent("crate_clk_" + String(Int(Date().timeIntervalSince1970)),
parameters: ["category": "category_\(Int.random(in: 0...100000))",
"int_v_test": 2147483647, "double_v_test": 200.1,
"string_v_test": "400",
"long_v_test": Int64(1)])
}
@IBAction func deleteItem(_ sender: Any) {
}
@IBAction func getLogs(_ sender: UIButton) {
GuruAnalytics.eventsLogsArchive({ [weak self] url in
guard let `self` = self, let url = url else { return }
if MFMailComposeViewController.canSendMail() {
let controller = MFMailComposeViewController()
do {
let data = try Data(contentsOf: url)
controller.addAttachmentData(data, mimeType: "application/zip", fileName: url.lastPathComponent)
} catch {
NSLog("\(error)")
}
controller.mailComposeDelegate = self
self.present(controller, animated: true, completion: nil)
}
})
}
@IBAction func startTimer(_ sender: Any) {
timer1 = Observable<Int>.interval(.milliseconds(10), scheduler: SerialDispatchQueueScheduler(qos: .default))
.subscribe(onNext: { int in
if int % 2 == 0 {
GuruAnalytics.setUserProperty("\(int)", forName: "SerialDispatchQueueScheduler_interval_2")
}
GuruAnalytics.logEvent("SerialDispatchQueueScheduler_interval", parameters: ["value": int])
if int % 3 == 0 {
GuruAnalytics.setUserProperty("\(int)", forName: "SerialDispatchQueueScheduler_interval_3")
}
})
timer2 = Observable<Int>.interval(.milliseconds(20), scheduler: MainScheduler.instance)
.subscribe(onNext: { int in
if int % 2 == 0 {
GuruAnalytics.setUserProperty("\(int)", forName: "MainScheduler_interval")
}
GuruAnalytics.logEvent("MainScheduler_interval", parameters: ["value": int])
if int % 3 == 0 {
GuruAnalytics.setUserProperty("\(int)", forName: "MainScheduler_interval")
}
})
}
@IBAction func stopTimer(_ sender: Any) {
timer1?.dispose()
timer1 = nil
timer2?.dispose()
timer2 = nil
}
@IBAction func onSliderAction(_ sender: UISlider) {
UIScreen.main.brightness = CGFloat(sender.value);
}
@IBAction func onEnableUploadSwitcher(_ sender: UISwitch) {
GuruAnalytics.setEnableUpload(isOn: sender.isOn)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension ViewController: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.presentingViewController?.dismiss(animated: true, completion: nil)
}
}

29
Example/Podfile Normal file
View File

@ -0,0 +1,29 @@
source 'https://cdn.cocoapods.org/'
source 'git@github.com:castbox/GuruSpecs.git'
use_frameworks!
platform :ios, '11.0'
target 'GuruAnalytics_Example' do
pod 'GuruAnalyticsLib', :path => '../'
# pod 'GuruAnalyticsLib', '0.3.4'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
installer.generated_projects.each do |project|
project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings["DEVELOPMENT_TEAM"] = "FRX48476C2"
end
end
end
end

View File

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

53
GuruAnalyticsLib.podspec Normal file
View File

@ -0,0 +1,53 @@
#
# 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.5'
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@git.chengdu.pundit.company:castbox/GuruAnalytics_iOS.git', :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'
s.dependency 'Alamofire', '~> 5.0'
s.dependency 'FMDB'
s.dependency 'GzipSwift'
s.dependency 'CryptoSwift'
s.dependency 'SwiftyBeaver'
s.subspec 'Privacy' do |ss|
ss.resource_bundles = {
s.name => 'GuruAnalytics/Assets/PrivacyInfo.xcprivacy'
}
end
end

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2022 devSC <xiaochong2154@163.com>
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.

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# GuruAnalytics
[![CI Status](https://img.shields.io/travis/devSC/GuruAnalyticsLib.svg?style=flat)](https://travis-ci.org/devSC/GuruAnalyticsLib)
[![Version](https://img.shields.io/cocoapods/v/GuruAnalyticsLib.svg?style=flat)](https://cocoapods.org/pods/GuruAnalyticsLib)
[![License](https://img.shields.io/cocoapods/l/GuruAnalyticsLib.svg?style=flat)](https://cocoapods.org/pods/GuruAnalyticsLib)
[![Platform](https://img.shields.io/cocoapods/p/GuruAnalyticsLib.svg?style=flat)](https://cocoapods.org/pods/GuruAnalyticsLib)
## [CHANGELOG](https://github.com/castbox/GuruAnalytics_iOS/blob/master/CHANGELOG.md)
## Example
To run the example project, clone the repo, and run `pod install` from the Example directory first.
## Requirements
## Installation
GuruAnalytics is available through [CocoaPods](https://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'GuruAnalyticsLib'
```
## Author
devSC, xiaochong2154@163.com
## License
GuruAnalyticsLib is available under the MIT license. See the LICENSE file for more info.