Compare commits
	
		
			2 Commits 
		
	
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 8163f171d2 | |
|  | 0ad881ca66 | 
							
								
								
									
										70
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										70
									
								
								CHANGELOG.md
								
								
								
								
							|  | @ -1,3 +1,73 @@ | |||
| ## v0.4.3 | ||||
| - fix | ||||
|   - 事件参数值字符串中包含“‘”单引号sql报错 | ||||
| - feature | ||||
|   - 增加事件参数 | ||||
|     - deviceInfo.appsflyerId | ||||
|   - 增加设置appsflyerId接口 | ||||
|     - setAppFlyersId(_ appFlyersId: String?) -> Void | ||||
| 
 | ||||
| ## v0.4.2 | ||||
| - fix | ||||
|   - 临时回滚 0.4.0,排查solitare collection ATP DAU下降问题。 | ||||
| 
 | ||||
| ## v0.4.1 | ||||
| - fix | ||||
|   - x.0的数值类型转换成整型问题 | ||||
| 
 | ||||
| ## v0.4.0 | ||||
| - feature | ||||
|   - 支持 AppExtension 上报打点 | ||||
| - 如在AppExtension 中使用,需在Podfile 中添加以下代码: | ||||
| ```swift | ||||
| installer.pods_project.targets.each do |target| | ||||
|   flutter_additional_ios_build_settings(target) | ||||
|   target.build_configurations.each do |config| | ||||
|     if target.name == 'GuruAnalyticsLib' | ||||
|       config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO' | ||||
|     end | ||||
|   end | ||||
| end | ||||
| ``` | ||||
| 
 | ||||
| ## v0.3.9 | ||||
| - feature | ||||
|   - 增加事件参数 | ||||
|     - deviceInfo.guruAnalyticsVersion | ||||
|     - deviceInfo.gurusdkVersion | ||||
|   - 增加设置中台版本接口 | ||||
|     - 初始化方法增加guruSDKVersion参数 initializeLib(..., guruSDKVersion: String)  | ||||
|     - setGuruSDKVersion(_ version: String) -> Void | ||||
| 
 | ||||
| ## v0.3.8.1 | ||||
| - fix | ||||
|   - x.0的数值类型转换成整型问题 | ||||
| 
 | ||||
| ## v0.3.8 | ||||
| - fix | ||||
|   - 隐私文件增加divice id声明 | ||||
| 
 | ||||
| ## v0.3.7 | ||||
| - feature | ||||
|   - 增加事件 | ||||
|     - guru_sdk_init_start | ||||
|     - guru_sdk_init_complete | ||||
|     - session_start | ||||
|     - guru_engagement | ||||
|   - 增加事件参数 | ||||
|     - session_number | ||||
|     - session_id | ||||
|     - deviceInfo.sdkVersion | ||||
|     - info.vendorId | ||||
| 
 | ||||
| ## v0.3.6.1 | ||||
| - fix: | ||||
|   - 更新privacy manifest文件,移除tracking domains下的空条目。 | ||||
| 
 | ||||
| ## v0.3.6 | ||||
| - fix: | ||||
|   - 增加第三方依赖库版本约束 | ||||
| 
 | ||||
| ## v0.3.5 | ||||
| - 接口更新: | ||||
|   - 日志打包方法eventsLogsArchive废弃,使用eventsLogsDirectory获取文件夹URL | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| 	archiveVersion = 1; | ||||
| 	classes = { | ||||
| 	}; | ||||
| 	objectVersion = 51; | ||||
| 	objectVersion = 54; | ||||
| 	objects = { | ||||
| 
 | ||||
| /* Begin PBXBuildFile section */ | ||||
|  | @ -128,7 +128,7 @@ | |||
| 				607FACCC1AFB9204008FA782 /* Sources */, | ||||
| 				607FACCD1AFB9204008FA782 /* Frameworks */, | ||||
| 				607FACCE1AFB9204008FA782 /* Resources */, | ||||
| 				86E7176C5034831947DC0310 /* [CP] Embed Pods Frameworks */, | ||||
| 				65438729BAC744B5EACA2945 /* [CP] Embed Pods Frameworks */, | ||||
| 			); | ||||
| 			buildRules = ( | ||||
| 			); | ||||
|  | @ -210,7 +210,7 @@ | |||
| 			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 */ = { | ||||
| 		65438729BAC744B5EACA2945 /* [CP] Embed Pods Frameworks */ = { | ||||
| 			isa = PBXShellScriptBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
|  | @ -368,8 +368,10 @@ | |||
| 			baseConfigurationReference = D30C5441D6D5CBD750E76657 /* Pods-GuruAnalytics_Example.debug.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				DEVELOPMENT_TEAM = 43U6TB4QK3; | ||||
| 				DEVELOPMENT_TEAM = 69MW7VVKA9; | ||||
| 				INFOPLIST_FILE = GuruAnalytics/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = GuruAnalytics; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.6; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 11.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
|  | @ -388,8 +390,10 @@ | |||
| 			baseConfigurationReference = F992AC1E7C4013032773C33F /* Pods-GuruAnalytics_Example.release.xcconfig */; | ||||
| 			buildSettings = { | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				DEVELOPMENT_TEAM = 43U6TB4QK3; | ||||
| 				DEVELOPMENT_TEAM = 69MW7VVKA9; | ||||
| 				INFOPLIST_FILE = GuruAnalytics/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = GuruAnalytics; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.6; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 11.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
|  |  | |||
|  | @ -27,7 +27,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { | |||
|                                             batchLimit: 25, | ||||
|                                             initializeTimeout: 5, | ||||
|                                             saasXAPPID: "test_app_id", | ||||
|                                             saasXDEVICEINFO: "appIdentifier=test.app.example;appVersion=1.0;deviceType=apple;deviceCountry=CN") | ||||
|                                             saasXDEVICEINFO: "appIdentifier=test.app.example;appVersion=1.0;deviceType=apple;deviceCountry=CN", | ||||
|                                             guruSDKVersion: "1.0.0") | ||||
|                 GuruAnalytics.setUserID("100004") | ||||
|                 GuruAnalytics.setAdjustId("xsdfal021sxasdfl") | ||||
|                 GuruAnalytics.setDeviceId(UUID().uuidString) | ||||
|  |  | |||
|  | @ -1,7 +1,31 @@ | |||
| import UIKit | ||||
| import Foundation | ||||
| 
 | ||||
| var greeting = "Hello, playground" | ||||
| 
 | ||||
| let sss = ["1", "2", "3"].map({ "'\($0)'" | ||||
| }).joined(separator: ",") | ||||
| print("value in (\(sss))") | ||||
| 
 | ||||
| let number = NSNumber(7.0) | ||||
| let number2 = NSNumber(7) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| print("number is double: \(number is Double)") | ||||
| print("number is int: \(number is Int)") | ||||
| print("number value: \(number)") | ||||
| print("number type: \(number.objCType)") | ||||
|           extension NSNumber { | ||||
|               var type: CFNumberType { | ||||
|                   return CFNumberGetType(self as CFNumber) | ||||
|               } | ||||
|           } | ||||
| 
 | ||||
| print("number type: \(number.type)") | ||||
| print("number2 type: \(number2.type)") | ||||
| 
 | ||||
| let number3 = 7.0 | ||||
| 
 | ||||
| print("number3 is Double: \(number3 is Double)") | ||||
| print("number3 is Int: \(number3 is Int)") | ||||
| print("number3 is NSNumber: \(number3 is NSNumber)") | ||||
|  |  | |||
							
								
								
									
										7
									
								
								Example/GuruAnalytics/MyPlayground.playground/playground.xcworkspace/contents.xcworkspacedata
								
								
									generated
								
								
								
									Normal file
								
							
							
						
						
									
										7
									
								
								Example/GuruAnalytics/MyPlayground.playground/playground.xcworkspace/contents.xcworkspacedata
								
								
									generated
								
								
								
									Normal file
								
							|  | @ -0,0 +1,7 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <Workspace | ||||
|    version = "1.0"> | ||||
|    <FileRef | ||||
|       location = "self:"> | ||||
|    </FileRef> | ||||
| </Workspace> | ||||
|  | @ -23,15 +23,31 @@ class ViewController: UIViewController { | |||
|      | ||||
|     @IBAction func setFirebaseId(_ sender: Any) { | ||||
|         GuruAnalytics.setFirebaseId("2312:3XSFA0211231") | ||||
|         GuruAnalytics.setAppFlyersId("app_flyers_id:133323") | ||||
|     } | ||||
|      | ||||
|     @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, | ||||
|                                             "int_v_test": 2147483647, "double_v_test_1": 200.1, | ||||
|                                             "double_v_test_2": NSNumber(7.00), | ||||
|                                             "double_v_test_3": 7.00, | ||||
|                                             "string_v_test": "400", | ||||
|                                             "long_v_test":  Int64(1)]) | ||||
|                                             "long_v_test":  Int64(1), | ||||
|                                             "long_v_test2": Int64.max, | ||||
|                                             "long_v_test3": Int64.min, | ||||
|                                             "value": 0.0]) | ||||
|         GuruAnalytics.logEvent("spend_virtual_currency", | ||||
|                                parameters: ["theme_name": "theme_bg_0029", | ||||
|                                             "theme_category": "theme", | ||||
|                                             "virtural_currency_name": "coins", | ||||
|                                             "value": 100, | ||||
|                                             "balance": 33810, | ||||
|                                             "scene": "theme", | ||||
|                                             "level_name": "Instance of I'm 'SettingIntDat'", | ||||
|                                            ]) | ||||
| 
 | ||||
|     } | ||||
|      | ||||
|     @IBAction func deleteItem(_ sender: Any) { | ||||
|  | @ -39,7 +55,7 @@ class ViewController: UIViewController { | |||
|     } | ||||
| 
 | ||||
|     @IBAction func getLogs(_ sender: UIButton) { | ||||
|         GuruAnalytics.eventsLogsArchive({ [weak self] url in | ||||
|         GuruAnalytics.eventsLogsDirectory({ [weak self] url in | ||||
|             guard let `self` = self, let url = url else { return } | ||||
|              | ||||
|             if MFMailComposeViewController.canSendMail() { | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ platform :ios, '11.0' | |||
| 
 | ||||
| target 'GuruAnalytics_Example' do | ||||
|   pod 'GuruAnalyticsLib', :path => '../' | ||||
| #  pod 'GuruAnalyticsLib', '0.3.4' | ||||
| #  pod 'GuruAnalyticsLib', '0.4.2' | ||||
| end | ||||
| 
 | ||||
| post_install do |installer| | ||||
|  |  | |||
|  | @ -2,6 +2,21 @@ | |||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||
| <plist version="1.0"> | ||||
| <dict> | ||||
| 	<key>NSPrivacyCollectedDataTypes</key> | ||||
| 	<array> | ||||
| 		<dict> | ||||
| 			<key>NSPrivacyCollectedDataType</key> | ||||
| 			<string>NSPrivacyCollectedDataTypeDeviceID</string> | ||||
| 			<key>NSPrivacyCollectedDataTypeLinked</key> | ||||
| 			<false/> | ||||
| 			<key>NSPrivacyCollectedDataTypeTracking</key> | ||||
| 			<false/> | ||||
| 			<key>NSPrivacyCollectedDataTypePurposes</key> | ||||
| 			<array> | ||||
| 				<string>NSPrivacyCollectedDataTypePurposeAnalytics</string> | ||||
| 			</array> | ||||
| 		</dict> | ||||
| 	</array> | ||||
| 	<key>NSPrivacyAccessedAPITypes</key> | ||||
| 	<array> | ||||
| 		<dict> | ||||
|  | @ -34,8 +49,6 @@ | |||
| 	<key>NSPrivacyTracking</key> | ||||
| 	<false/> | ||||
| 	<key>NSPrivacyTrackingDomains</key> | ||||
| 	<array> | ||||
| 		<string></string> | ||||
| 	</array> | ||||
| 	<array/> | ||||
| </dict> | ||||
| </plist> | ||||
|  |  | |||
|  | @ -34,7 +34,8 @@ public class GuruAnalytics: NSObject { | |||
|                                     initializeTimeout: Double = 5, | ||||
|                                     saasXAPPID: String, | ||||
|                                     saasXDEVICEINFO: String, | ||||
|                                     loggerDebug: Bool = true) { | ||||
|                                     loggerDebug: Bool = true, | ||||
|                                     guruSDKVersion: String) { | ||||
|         Self.uploadPeriodInSecond = uploadPeriodInSecond | ||||
|         Self.batchLimit = batchLimit | ||||
|         Self.eventExpiredSeconds = eventExpiredSeconds | ||||
|  | @ -42,6 +43,7 @@ public class GuruAnalytics: NSObject { | |||
|         Self.saasXAPPID = saasXAPPID | ||||
|         Self.saasXDEVICEINFO = saasXDEVICEINFO | ||||
|         Self.loggerDebug = loggerDebug | ||||
|         Constants.guruSDKVersion = guruSDKVersion | ||||
|         _ = Manager.shared | ||||
|     } | ||||
|      | ||||
|  | @ -81,6 +83,12 @@ public class GuruAnalytics: NSObject { | |||
|         setUserProperty(firebaseId, forName: .firebaseId) | ||||
|     } | ||||
|      | ||||
|     /// 设置appsflyerId | ||||
|     @objc | ||||
|     public class func setAppFlyersId(_ appFlyersId: String?) { | ||||
|         setUserProperty(appFlyersId, forName: .appsflyerId) | ||||
|     } | ||||
|      | ||||
|     /// screen name | ||||
|     @objc | ||||
|     public class func setScreen(_ name: String) { | ||||
|  | @ -155,4 +163,9 @@ public class GuruAnalytics: NSObject { | |||
|         enableUpload = isOn | ||||
|     } | ||||
| 
 | ||||
|     /// 设置中台库版本 | ||||
|     @objc | ||||
|     public class func setGuruSDKVersion(_ version: String) -> Void { | ||||
|         Constants.guruSDKVersion = version | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -124,8 +124,16 @@ extension Entity { | |||
|          | ||||
|         static func normalizeParameters(_ parameters: [String : Any]) -> [String : EventValue] { | ||||
|             var params = [String : EventValue]() | ||||
|             var allParams = parameters; | ||||
|              | ||||
|             GuruAnalytics.BuiltinParametersKeys.allCases.forEach { paramKey in | ||||
|                 if let value = allParams.removeValue(forKey: paramKey.rawValue) { | ||||
|                     params[paramKey.rawValue] = normalizeValue(value); | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             var count = 0 | ||||
|             parameters.sorted(by: { $0.key < $1.key }).forEach({ key, value in | ||||
|             allParams.sorted(by: { $0.key < $1.key }).forEach({ key, value in | ||||
|                  | ||||
|                 guard count < maxParametersCount else { | ||||
|                     cdPrint("too many parameters") | ||||
|  | @ -140,17 +148,7 @@ extension Entity { | |||
|                     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))) | ||||
|                 } | ||||
|                 params[normalizedKey] = normalizeValue(value) | ||||
|                  | ||||
|                 count += 1 | ||||
|             }) | ||||
|  | @ -158,6 +156,36 @@ extension Entity { | |||
|             return params | ||||
|         } | ||||
|          | ||||
|         static func normalizeValue(_ value: Any) -> EventValue { | ||||
|              | ||||
|             var preprocessedValue = value | ||||
|              | ||||
|             if let val = preprocessedValue as? NSNumber { | ||||
|                 preprocessedValue = val.numricValue | ||||
|             } | ||||
|              | ||||
|             let eventValue: EventValue | ||||
|             if let value = preprocessedValue as? String { | ||||
|                 eventValue = Entity.EventValue(stringValue: normalizeStringValue(String(value.prefix(maxParameterStringValueLength)))) | ||||
|             } else if let value = preprocessedValue as? Int { | ||||
|                 eventValue = Entity.EventValue(longValue: Int64(value)) | ||||
|             } else if let value = preprocessedValue as? Int64 { | ||||
|                 eventValue = Entity.EventValue(longValue: value) | ||||
|             } else if let value = preprocessedValue as? Double { | ||||
|                 eventValue = Entity.EventValue(doubleValue: value) | ||||
|             } else { | ||||
|                 eventValue = Entity.EventValue(stringValue: normalizeStringValue(String("\(value)".prefix(maxParameterStringValueLength)))) | ||||
|             } | ||||
|             return eventValue | ||||
|         } | ||||
|          | ||||
|         static func normalizeStringValue(_ value: String) -> String { | ||||
|             let normalizedString = value.replacingOccurrences(of: "'", with: "''") | ||||
|             cdPrint("original string value: \(value)") | ||||
|             cdPrint("normalized string value: \(normalizedString)") | ||||
|             return normalizedString | ||||
|         } | ||||
|          | ||||
|         static func normalizeKey(_ key: String) -> String? { | ||||
|             var mutableKey = key | ||||
|              | ||||
|  | @ -191,6 +219,11 @@ extension Entity { | |||
|         let adId: String? | ||||
|         ///用户的pseudo_id | ||||
|         let firebaseId: String? | ||||
|         ///appsFlyerId | ||||
|         let appsflyerId: String? | ||||
|          | ||||
|         ///IDFV | ||||
|         let vendorId: String? = UIDevice().identifierForVendor?.uuidString | ||||
|          | ||||
|         enum CodingKeys: String, CodingKey { | ||||
|             case deviceId | ||||
|  | @ -198,6 +231,8 @@ extension Entity { | |||
|             case adjustId | ||||
|             case adId | ||||
|             case firebaseId | ||||
|             case vendorId | ||||
|             case appsflyerId | ||||
|         } | ||||
|     } | ||||
|      | ||||
|  | @ -226,3 +261,23 @@ extension Entity { | |||
|         let data: Int64 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| extension Entity { | ||||
|     struct Session { | ||||
|         let id: UUID = UUID() | ||||
|          | ||||
|         var sessionId: Int { | ||||
|             return id.uuidString.hash | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     struct SessionNumber: Codable { | ||||
|         var number: Int | ||||
|         let createdAtMs: Int64 | ||||
|          | ||||
|         static func createNumber() -> SessionNumber { | ||||
|             return SessionNumber(number: 0, createdAtMs: Date().msSince1970) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -176,6 +176,40 @@ WHERE \(Entity.EventRecord.CodingKeys.timestamp.rawValue) < \(earlierThan) | |||
|         }) | ||||
|     } | ||||
|      | ||||
|     func fetchOutdatedEventRecords(earlierThan: Int64) -> Single<[Entity.EventRecord]> { | ||||
|         return mapTransactionToSingle { (db) in | ||||
|             let querySQL: String = | ||||
| """ | ||||
| SELECT * FROM \(TableName.event.rawValue) | ||||
| WHERE \(Entity.EventRecord.CodingKeys.timestamp.rawValue) < \(earlierThan) | ||||
| """ | ||||
|             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() | ||||
|             return t | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func resetTransitionStatus(for recordIds: [String]) -> Single<Void> { | ||||
|         guard !recordIds.isEmpty else { | ||||
|             return .just(()) | ||||
|  |  | |||
|  | @ -54,6 +54,7 @@ internal class Manager { | |||
|     // MARK: - private members | ||||
|      | ||||
|     private typealias PropertyName = GuruAnalytics.PropertyName | ||||
|     private typealias BuiltinParametersKeys = GuruAnalytics.BuiltinParametersKeys | ||||
|      | ||||
|     private let bag = DisposeBag() | ||||
|      | ||||
|  | @ -170,8 +171,23 @@ internal class Manager { | |||
|     private typealias InternalEventReporter = ((_ eventCode: Int, _ info: String) -> Void) | ||||
|     private var internalEventReporter: InternalEventReporter? | ||||
|      | ||||
|     private lazy var session = Entity.Session() | ||||
|     private lazy var sessionNumber: Entity.SessionNumber = { | ||||
|         var sessionNumber = UserDefaults.sessionNumber | ||||
|         let dayGaps = Calendar.current.dateComponents([.day], from:  Date(timeIntervalSince1970: Double(sessionNumber.createdAtMs) / 1000), to: Date()).day ?? 0 | ||||
|         if (dayGaps > 0) { | ||||
|             sessionNumber = Entity.SessionNumber.createNumber() | ||||
|         } | ||||
|         sessionNumber.number += 1 | ||||
|         UserDefaults.sessionNumber = sessionNumber | ||||
|         return sessionNumber | ||||
|     }() | ||||
|      | ||||
|     private init() { | ||||
|          | ||||
|         // | ||||
|         logSDKInitStart() | ||||
|          | ||||
|         // first open | ||||
|         logFirstOpenIfNeeded() | ||||
|          | ||||
|  | @ -188,6 +204,12 @@ internal class Manager { | |||
|         logFirstFgEvent() | ||||
|          | ||||
|         ntwkMgr.networkErrorReporter = self | ||||
|          | ||||
|         // | ||||
|         logSDKInitComplete() | ||||
|          | ||||
|         // | ||||
|         logSessionStart() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -229,7 +251,7 @@ internal extension Manager { | |||
|                                 priority: Entity.EventRecord.Priority) -> Single<Entity.EventRecord> { | ||||
|          | ||||
|         return userProperty.take(1).observe(on: rxWorkScheduler).asSingle().flatMap { p in | ||||
|                 .create { subscriber in | ||||
|                 .create { [weak self] subscriber in | ||||
|                     do { | ||||
|                         debugPrint("userProperty thread queueName: \(Thread.current.queueName) count: \(p.count)") | ||||
|                         var userProperty = p | ||||
|  | @ -240,12 +262,16 @@ internal extension Manager { | |||
|                             eventParam[PropertyName.screen.rawValue] = screen | ||||
|                         } | ||||
|                          | ||||
|                         eventParam[BuiltinParametersKeys.sessionId.rawValue] = self?.session.sessionId | ||||
|                         eventParam[BuiltinParametersKeys.sessionNo.rawValue] = self?.sessionNumber.number | ||||
|                          | ||||
|                         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) | ||||
|                             firebaseId: userProperty.removeValue(forKey: PropertyName.firebaseId.rawValue), | ||||
|                             appsflyerId: userProperty.removeValue(forKey: PropertyName.appsflyerId.rawValue) | ||||
|                         ) | ||||
|                          | ||||
|                         let event = try Entity.Event(timestamp: timestamp, | ||||
|  | @ -361,19 +387,33 @@ private extension Manager { | |||
|          | ||||
|         /// 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) | ||||
|             .flatMap({ [weak self] serverNowMs -> Single<[Entity.EventRecord]> in | ||||
|                 guard let `self` = self else { | ||||
|                     return .error(NSError(domain: "com.guru.analytics.manager", | ||||
|                                           code: 0, | ||||
|                                           userInfo: [NSLocalizedDescriptionKey : "Manager released"] | ||||
|                                          )) | ||||
|                 } | ||||
|                 let earlierThan: Int64 = serverNowMs - self.eventExpiredIntervel.int64Ms | ||||
|                 return self.db.fetchOutdatedEventRecords(earlierThan: earlierThan) | ||||
|             }) | ||||
|             .flatMap({ [weak self] records in | ||||
|                 return self?.db.deleteEventRecords(records.map { $0.recordId }) | ||||
|                     .map { _ in records.count } ??  | ||||
|                     .error(NSError(domain: "com.guru.analytics.manager", | ||||
|                                    code: 0, | ||||
|                                    userInfo: [NSLocalizedDescriptionKey : "Manager released"] | ||||
|                                   )) | ||||
|             }) | ||||
|             .catch({ error in | ||||
|                 cdPrint("remove outdated records error: \(error)") | ||||
|                 return .just(0) | ||||
|             }) | ||||
|             .subscribe(onSuccess: { [weak self] deletedCount in | ||||
|                 UserDefaults.deletedEventsCount += deletedCount | ||||
|                 self?.outdatedEventsCleared.onNext(true) | ||||
|             }) | ||||
|             .disposed(by: bag) | ||||
|     } | ||||
|      | ||||
|     func logFirstOpenIfNeeded() { | ||||
|  | @ -417,6 +457,7 @@ private extension Manager { | |||
|                 .flatMap { self.db.addEventRecords($0) } | ||||
|                 .do(onSuccess: { _ in | ||||
|                     self.accumulateLoggedEventsCount(1) | ||||
|                     UserDefaults.totalEventsCount += 1 | ||||
|                     self.eventsLogger.verbose("log event success") | ||||
|                 }, onError: { error in | ||||
|                     self.eventsLogger.error("log event error: \(error)") | ||||
|  | @ -424,6 +465,28 @@ private extension Manager { | |||
|         }() | ||||
|     } | ||||
| 
 | ||||
|     func logSDKInitStart() { | ||||
|         _logEvent(GuruAnalytics.sdkInitStartEvent.name, parameters: [ | ||||
|             GuruAnalytics.sdkInitStartEvent.paramKeyType.totalEvents.rawValue : UserDefaults.totalEventsCount, | ||||
|             GuruAnalytics.sdkInitStartEvent.paramKeyType.deletedEvents.rawValue : UserDefaults.deletedEventsCount, | ||||
|             GuruAnalytics.sdkInitStartEvent.paramKeyType.uploadedEvents.rawValue : UserDefaults.uploadedEventsCount, | ||||
|         ], priority: .HIGH) | ||||
|         .subscribe() | ||||
|         .disposed(by: bag) | ||||
|     } | ||||
|      | ||||
|     func logSDKInitComplete() { | ||||
|         _logEvent(GuruAnalytics.sdkInitCompleteEvent.name, parameters: [ | ||||
|             GuruAnalytics.sdkInitCompleteEvent.paramKeyType.duration.rawValue : Date().msSince1970 - startAt.msSince1970, | ||||
|         ], priority: .HIGH) | ||||
|         .subscribe() | ||||
|         .disposed(by: bag) | ||||
|     } | ||||
|     func logSessionStart() { | ||||
|         _logEvent(GuruAnalytics.sessionStartEvent.name, parameters: nil, priority: .HIGH) | ||||
|             .subscribe() | ||||
|             .disposed(by: bag) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // MARK: - 轮询上传相关 | ||||
|  | @ -536,6 +599,7 @@ private extension Manager { | |||
|                 } | ||||
|                 .flatMap { recordIDs -> Single<Void> in | ||||
|                     self.accumulateUploadedEventsCount(recordIDs.count) | ||||
|                     UserDefaults.uploadedEventsCount += recordIDs.count | ||||
|                     /// step4:  删除数据库中对应记录 | ||||
|                     return self.db.deleteEventRecords(recordIDs) | ||||
|                         .catch { error in | ||||
|  |  | |||
|  | @ -39,6 +39,48 @@ internal enum UserDefaults { | |||
|         } | ||||
|     } | ||||
|      | ||||
|     static var totalEventsCount: Int { | ||||
|         get { | ||||
|             return defaults?.value(forKey: totalEventsCountKey) as? Int ?? 0 | ||||
|         } | ||||
|          | ||||
|         set { | ||||
|             defaults?.set(newValue, forKey: totalEventsCountKey) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     static var deletedEventsCount: Int { | ||||
|         get { | ||||
|             return defaults?.value(forKey: deletedEventsCountKey) as? Int ?? 0 | ||||
|         } | ||||
|          | ||||
|         set { | ||||
|             defaults?.set(newValue, forKey: deletedEventsCountKey) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     static var uploadedEventsCount: Int { | ||||
|         get { | ||||
|             return defaults?.value(forKey: uploadedEventsCountKey) as? Int ?? 0 | ||||
|         } | ||||
|          | ||||
|         set { | ||||
|             defaults?.set(newValue, forKey: uploadedEventsCountKey) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     static var sessionNumber: Entity.SessionNumber { | ||||
|         get { | ||||
|             let jsonString = defaults?.value(forKey: sessionNumberKey) as? String ?? "" | ||||
|             let sessionNumber = JSONDecoder().decodeObject(Entity.SessionNumber.self, from: jsonString) | ||||
|             ?? Entity.SessionNumber.createNumber() | ||||
|             return sessionNumber | ||||
|         } | ||||
|         set { | ||||
|             let jsonString = newValue.asString | ||||
|             defaults?.setValue(jsonString, forKey: sessionNumberKey) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| extension UserDefaults { | ||||
|  | @ -62,4 +104,21 @@ extension UserDefaults { | |||
|     static var fgDurationKey: String { | ||||
|         return "fg.duration.ms" | ||||
|     } | ||||
|      | ||||
|     static var totalEventsCountKey: String { | ||||
|         return "events.recorded.total.count" | ||||
|     } | ||||
|      | ||||
|     static var deletedEventsCountKey: String { | ||||
|         return "events.deleted.count" | ||||
|     } | ||||
|      | ||||
|     static var uploadedEventsCountKey: String { | ||||
|         return "events.uploaded.count" | ||||
|     } | ||||
|      | ||||
|     static var sessionNumberKey: String { | ||||
|         return "session.number" | ||||
|     } | ||||
|      | ||||
| } | ||||
|  |  | |||
|  | @ -18,11 +18,12 @@ internal extension GuruAnalytics { | |||
|         case firebaseId | ||||
|         case screen = "screen_name" | ||||
|         case firstOpenTime = "first_open_time" | ||||
|         case appsflyerId | ||||
|     } | ||||
|      | ||||
|     ///built-in events | ||||
|     static let fgEvent: EventProto = { | ||||
|         var e = EventProto(paramKeyType: FgEventParametersKeys.self, name: "fg") | ||||
|         var e = EventProto(paramKeyType: FgEventParametersKeys.self, name: "guru_engagement") | ||||
|         return e | ||||
|     }() | ||||
|      | ||||
|  | @ -31,6 +32,21 @@ internal extension GuruAnalytics { | |||
|         return e | ||||
|     }() | ||||
|      | ||||
|     static let sdkInitStartEvent: EventProto = { | ||||
|         var e = EventProto(paramKeyType: SDKEventParametersKeys.self, name: "guru_sdk_init_start") | ||||
|         return e | ||||
|     }() | ||||
|      | ||||
|     static let sdkInitCompleteEvent: EventProto = { | ||||
|         var e = EventProto(paramKeyType: SDKEventParametersKeys.self, name: "guru_sdk_init_complete") | ||||
|         return e | ||||
|     }() | ||||
|      | ||||
|     static let sessionStartEvent: EventProto = { | ||||
|         var e = EventProto(paramKeyType: DefaultEventParametersKeys.self, name: "session_start") | ||||
|         return e | ||||
|     }() | ||||
|      | ||||
|     class func setUserProperty(_ value: String?, forName name: PropertyName) { | ||||
|         setUserProperty(value, forName: name.rawValue) | ||||
|     } | ||||
|  | @ -48,4 +64,23 @@ internal extension GuruAnalytics { | |||
|         case duration | ||||
|     } | ||||
|      | ||||
|     enum SDKEventParametersKeys: String { | ||||
|         case totalEvents = "total_events" | ||||
|         case deletedEvents = "deleted_events" | ||||
|         case uploadedEvents = "uploaded_events" | ||||
|         case duration | ||||
|     } | ||||
|      | ||||
|     enum DefaultEventParametersKeys { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| internal extension GuruAnalytics { | ||||
|      | ||||
|     ///built-in event parameters | ||||
|     enum BuiltinParametersKeys: String, CaseIterable { | ||||
|         case screenName = "screen_name" | ||||
|         case sessionNo = "session_number" | ||||
|         case sessionId = "session_id" | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -26,6 +26,17 @@ internal struct Constants { | |||
|         return shortVersion | ||||
|     }() | ||||
|      | ||||
|     private static let guruAnalyticsSDKVersion: String = { | ||||
|         guard let infoDict = Bundle(for: Manager.self).infoDictionary, | ||||
|               let currentVersion = infoDict["CFBundleShortVersionString"] as? String else { | ||||
|             return "" | ||||
|         } | ||||
|         return currentVersion | ||||
|     }() | ||||
|      | ||||
|     /// 中台库版本,由外部更新 | ||||
|     static var guruSDKVersion: String = ""; | ||||
|      | ||||
|     private static let preferredLocale: Locale = { | ||||
|         guard let preferredIdentifier = Locale.preferredLanguages.first else { | ||||
|             return Locale.current | ||||
|  | @ -83,7 +94,9 @@ internal struct Constants { | |||
|             "screenH": Int(screenSize.h), | ||||
|             "screenW": Int(screenSize.w), | ||||
|             "osVersion": systemVersion, | ||||
|             "language" : languageCode | ||||
|             "language" : languageCode, | ||||
|             "guruAnalyticsVersion" : guruAnalyticsSDKVersion, | ||||
|             "gurusdkVersion" : guruSDKVersion, | ||||
|         ] | ||||
|     } | ||||
|      | ||||
|  | @ -93,6 +106,50 @@ internal struct Constants { | |||
|     /// - returns: raw `String` of device type, e.g. iPhone5,1 | ||||
|     /// | ||||
|     private static func hardwareString() -> String { | ||||
|         var name: [Int32] = [CTL_HW, HW_MACHINE] | ||||
|         var size: size_t = 0 | ||||
|          | ||||
|         // 🛡️ 安全检查1:获取缓冲区大小 | ||||
|         guard sysctl(&name, 2, nil, &size, nil, 0) == 0, | ||||
|               size > 0 && size < 256 else { | ||||
|             return "iPhone14,1" // 安全默认值 | ||||
|         } | ||||
|          | ||||
|         // 🛡️ 安全检查2:创建缓冲区 | ||||
|         let bufferSize = Int(size) + 1 | ||||
|         var hw_machine = [CChar](repeating: 0, count: bufferSize) | ||||
|         var actualSize = size | ||||
|          | ||||
|         // 🛡️ 安全检查3:获取数据 | ||||
|         guard sysctl(&name, 2, &hw_machine, &actualSize, nil, 0) == 0 else { | ||||
|             return "iPhone14,1" // 安全默认值 | ||||
|         } | ||||
|          | ||||
|         // 🛡️ 安全检查4:确保null终止(防止越界) | ||||
|         let safeIndex = min(Int(actualSize), bufferSize - 1) | ||||
|         hw_machine[safeIndex] = 0 | ||||
|          | ||||
|         var hardware: String = String(cString: hw_machine) | ||||
|          | ||||
|         // 🛡️ 安全检查5:验证结果 | ||||
|         if hardware.isEmpty { | ||||
|             return "iPhone14,1" // 安全默认值 | ||||
|         } | ||||
|          | ||||
|         // Check for simulator | ||||
|         if hardware == "x86_64" || hardware == "i386" || hardware == "arm64" { | ||||
|             if let deviceID = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] { | ||||
|                 hardware = deviceID | ||||
|             } else { | ||||
|                 hardware = "Simulator" | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         return hardware | ||||
|     } | ||||
|      | ||||
|     //这里不可用,有崩溃!!!用上边的方法!! | ||||
|     private static func hardwareStringError() -> String { | ||||
|         var name: [Int32] = [CTL_HW, HW_MACHINE] | ||||
|         var size: Int = 2 | ||||
|         sysctl(&name, 2, nil, &size, nil, 0) | ||||
|  | @ -107,10 +164,12 @@ internal struct Constants { | |||
|                 hardware = deviceID | ||||
|             } | ||||
|         } | ||||
|          | ||||
|      | ||||
|         return hardware | ||||
|     } | ||||
|      | ||||
| 
 | ||||
|      | ||||
|     /// This method returns the Platform enum depending upon harware string | ||||
|     /// | ||||
|     /// | ||||
|  |  | |||
|  | @ -0,0 +1,43 @@ | |||
| // | ||||
| //  Untitled.swift | ||||
| //  Pods | ||||
| // | ||||
| //  Created by mayue on 2025/1/14. | ||||
| // | ||||
| 
 | ||||
| extension NSNumber { | ||||
|     var valueType: CFNumberType { | ||||
|         return CFNumberGetType(self as CFNumber) | ||||
|     } | ||||
|      | ||||
|     var numricValue: Any { | ||||
|         switch valueType { | ||||
|         case .sInt8Type, | ||||
|                 .sInt16Type, | ||||
|                 .sInt32Type, | ||||
|                 .charType, | ||||
|                 .shortType, | ||||
|                 .intType, | ||||
|                 .longType, | ||||
|                 .cfIndexType, | ||||
|                 .nsIntegerType: | ||||
|             return intValue; | ||||
|              | ||||
|         case | ||||
|                 .sInt64Type, | ||||
|                 .longLongType: | ||||
|             return int64Value; | ||||
|              | ||||
|         case .float32Type, | ||||
|                 .float64Type, | ||||
|                 .floatType, | ||||
|                 .doubleType, | ||||
|                 .cgFloatType, | ||||
|                 .maxType: | ||||
|             return doubleValue; | ||||
|              | ||||
|         @unknown default: | ||||
|             return doubleValue; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -25,4 +25,38 @@ internal extension JSONDecoder { | |||
|         } | ||||
|         return try decode(type, from: unwrappedData) | ||||
|     } | ||||
|      | ||||
|     private func decodeObject<T>(_ type: T.Type, from data: Data) -> T? where T: Decodable { | ||||
|          | ||||
|         guard data.count > 0 else { return nil } | ||||
|          | ||||
|         var object: T? = nil | ||||
|          | ||||
|         do { | ||||
|             object = try decode(type, from: data) | ||||
|         } catch { | ||||
|             cdPrint("JSONDecoder decode error: \(error)") | ||||
|         } | ||||
|          | ||||
|         return object | ||||
|     } | ||||
|      | ||||
|     func decodeObject<T>(_ type: T.Type, from jsonString: String) -> T? where T: Decodable { | ||||
|         guard let jsonData = jsonString.data(using: .utf8) else { return nil } | ||||
|         return decodeObject(type, from: jsonData) | ||||
|     } | ||||
|      | ||||
|     func decodeObject<T>(_ type: T.Type, from dictionary: [String : Any]) -> T? where T: Decodable { | ||||
|          | ||||
|         var data: Data? | ||||
|          | ||||
|         do { | ||||
|             data = try JSONSerialization.data(withJSONObject: dictionary, options: .prettyPrinted) | ||||
|         } catch let error { | ||||
|             cdPrint(error) | ||||
|         } | ||||
|          | ||||
|         guard let jsonData = data else { return nil } | ||||
|         return decodeObject(type, from: jsonData) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
| 
 | ||||
| Pod::Spec.new do |s| | ||||
|   s.name             = 'GuruAnalyticsLib' | ||||
|   s.version          = '0.3.5' | ||||
|   s.version          = '0.4.4' | ||||
|   s.summary          = 'A short description of GuruAnalytics.' | ||||
| 
 | ||||
| # This description is used to generate tags and improve search results. | ||||
|  | @ -38,12 +38,12 @@ TODO: Add long description of the pod here. | |||
|   # 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.dependency 'RxCocoa', '~> 6.7.0' | ||||
|   s.dependency 'Alamofire', '~> 5.9' | ||||
|   s.dependency 'FMDB', '~> 2.0' | ||||
|   s.dependency 'GzipSwift', '~> 5.0' | ||||
|   s.dependency 'CryptoSwift', '~> 1.0' | ||||
|   s.dependency 'SwiftyBeaver', '~> 1.0' | ||||
|    | ||||
|   s.subspec 'Privacy' do |ss| | ||||
|       ss.resource_bundles = { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue