SessionStartEvent.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. //
  2. // Copyright 2022 Google LLC
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. import Foundation
  16. @_implementationOnly import GoogleDataTransport
  17. #if SWIFT_PACKAGE
  18. import FirebaseSessionsObjC
  19. #endif // SWIFT_PACKAGE
  20. #if SWIFT_PACKAGE
  21. @_implementationOnly import GoogleUtilities_Environment
  22. #else
  23. @_implementationOnly import GoogleUtilities
  24. #endif // SWIFT_PACKAGE
  25. ///
  26. /// SessionStartEvent is responsible for:
  27. /// 1) Writing fields to the Session proto
  28. /// 2) Synthesizing itself for persisting to disk and logging to GoogleDataTransport
  29. ///
  30. class SessionStartEvent: NSObject, GDTCOREventDataObject {
  31. var proto: firebase_appquality_sessions_SessionEvent
  32. init(sessionInfo: SessionInfo, appInfo: ApplicationInfoProtocol,
  33. time: TimeProvider = Time()) {
  34. proto = firebase_appquality_sessions_SessionEvent()
  35. super.init()
  36. // Note: If you add a proto string field here, remember to free it in the deinit.
  37. proto.event_type = firebase_appquality_sessions_EventType_SESSION_START
  38. proto.session_data.session_id = makeProtoString(sessionInfo.sessionId)
  39. proto.session_data.first_session_id = makeProtoString(sessionInfo.firstSessionId)
  40. proto.session_data.session_index = sessionInfo.sessionIndex
  41. proto.session_data.event_timestamp_us = time.timestampUS
  42. proto.application_info.app_id = makeProtoString(appInfo.appID)
  43. proto.application_info.session_sdk_version = makeProtoString(appInfo.sdkVersion)
  44. proto.application_info.os_version = makeProtoString(appInfo.osDisplayVersion)
  45. proto.application_info.log_environment = convertLogEnvironment(environment: appInfo.environment)
  46. proto.application_info.device_model = makeProtoString(appInfo.deviceModel)
  47. // proto.application_info.development_platform_name;
  48. // proto.application_info.development_platform_version;
  49. // `which_platform_info` tells nanopb which oneof we're choosing to fill in for our proto
  50. proto.application_info.which_platform_info = FIRSESGetAppleApplicationInfoTag()
  51. proto.application_info.apple_app_info
  52. .bundle_short_version = makeProtoString(appInfo.appDisplayVersion)
  53. proto.application_info.apple_app_info
  54. .app_build_version = makeProtoString(appInfo.appBuildVersion)
  55. proto.application_info.apple_app_info.os_name = convertOSName(osName: appInfo.osName)
  56. // Set network info to base values but don't fill them in with the real
  57. // value because these are only tracked when Performance is installed
  58. proto.application_info.apple_app_info.mcc_mnc = makeProtoString("")
  59. proto.application_info.apple_app_info.network_connection_info
  60. .network_type = convertNetworkType(networkType: .none)
  61. proto.application_info.apple_app_info.network_connection_info
  62. .mobile_subtype = convertMobileSubtype(mobileSubtype: "")
  63. proto.session_data.data_collection_status
  64. .crashlytics = firebase_appquality_sessions_DataCollectionState_COLLECTION_SDK_NOT_INSTALLED
  65. proto.session_data.data_collection_status
  66. .performance = firebase_appquality_sessions_DataCollectionState_COLLECTION_SDK_NOT_INSTALLED
  67. }
  68. deinit {
  69. let garbage: [UnsafeMutablePointer<pb_bytes_array_t>?] = [
  70. proto.application_info.app_id,
  71. proto.application_info.apple_app_info.app_build_version,
  72. proto.application_info.apple_app_info.bundle_short_version,
  73. proto.application_info.apple_app_info.mcc_mnc,
  74. proto.application_info.development_platform_name,
  75. proto.application_info.development_platform_version,
  76. proto.application_info.device_model,
  77. proto.application_info.os_version,
  78. proto.application_info.session_sdk_version,
  79. proto.session_data.session_id,
  80. proto.session_data.firebase_installation_id,
  81. proto.session_data.first_session_id,
  82. ]
  83. for pointer in garbage {
  84. nanopb_free(pointer)
  85. }
  86. }
  87. func setInstallationID(installationId: String) {
  88. let oldID = proto.session_data.firebase_installation_id
  89. proto.session_data.firebase_installation_id = makeProtoString(installationId)
  90. nanopb_free(oldID)
  91. }
  92. func setSamplingRate(samplingRate: Double) {
  93. proto.session_data.data_collection_status.session_sampling_rate = samplingRate
  94. }
  95. func set(subscriber: SessionsSubscriberName, isDataCollectionEnabled: Bool,
  96. appInfo: ApplicationInfoProtocol) {
  97. let dataCollectionState = makeDataCollectionProto(isDataCollectionEnabled)
  98. switch subscriber {
  99. case .Crashlytics:
  100. proto.session_data.data_collection_status.crashlytics = dataCollectionState
  101. case .Performance:
  102. proto.session_data.data_collection_status.performance = dataCollectionState
  103. default:
  104. Logger
  105. .logWarning("Attempted to set Data Collection status for unknown Subscriber: \(subscriber)")
  106. }
  107. // Only set restricted fields if Data Collection is enabled. If it's disabled,
  108. // we're treating that as if the product isn't installed.
  109. if isDataCollectionEnabled {
  110. setRestrictedFields(subscriber: subscriber,
  111. appInfo: appInfo)
  112. }
  113. }
  114. /// This method should be called for every subscribed Subscriber. This is for cases where
  115. /// fields should only be collected if a specific SDK is installed.
  116. private func setRestrictedFields(subscriber: SessionsSubscriberName,
  117. appInfo: ApplicationInfoProtocol) {
  118. switch subscriber {
  119. case .Performance:
  120. let oldString = proto.application_info.apple_app_info.mcc_mnc
  121. proto.application_info.apple_app_info.mcc_mnc = makeProtoString("")
  122. nanopb_free(oldString)
  123. proto.application_info.apple_app_info.network_connection_info
  124. .network_type = convertNetworkType(networkType: appInfo.networkInfo.networkType)
  125. proto.application_info.apple_app_info.network_connection_info
  126. .mobile_subtype = convertMobileSubtype(mobileSubtype: appInfo.networkInfo.mobileSubtype)
  127. default:
  128. break
  129. }
  130. }
  131. // MARK: - GDTCOREventDataObject
  132. func transportBytes() -> Data {
  133. var fields = firebase_appquality_sessions_SessionEvent_fields
  134. var error: NSError?
  135. let data = FIRSESEncodeProto(&fields.0, &proto, &error)
  136. if error != nil {
  137. Logger
  138. .logError("Session Event failed to encode as proto with error: \(error.debugDescription)")
  139. }
  140. guard let data = data else {
  141. Logger.logError("Session Event generated nil transportBytes. Returning empty data.")
  142. return Data()
  143. }
  144. return data
  145. }
  146. // MARK: - Data Conversion
  147. func makeDataCollectionProto(_ isDataCollectionEnabled: Bool)
  148. -> firebase_appquality_sessions_DataCollectionState {
  149. if isDataCollectionEnabled {
  150. return firebase_appquality_sessions_DataCollectionState_COLLECTION_ENABLED
  151. } else {
  152. return firebase_appquality_sessions_DataCollectionState_COLLECTION_DISABLED
  153. }
  154. }
  155. private func makeProtoStringOrNil(_ string: String?) -> UnsafeMutablePointer<pb_bytes_array_t>? {
  156. guard let string = string else {
  157. return nil
  158. }
  159. return FIRSESEncodeString(string)
  160. }
  161. private func makeProtoString(_ string: String) -> UnsafeMutablePointer<pb_bytes_array_t>? {
  162. return FIRSESEncodeString(string)
  163. }
  164. private func convertOSName(osName: String) -> firebase_appquality_sessions_OsName {
  165. switch osName.lowercased() {
  166. case "macos":
  167. return firebase_appquality_sessions_OsName_MACOS
  168. case "maccatalyst":
  169. return firebase_appquality_sessions_OsName_MACCATALYST
  170. case "ios_on_mac":
  171. return firebase_appquality_sessions_OsName_IOS_ON_MAC
  172. case "ios":
  173. return firebase_appquality_sessions_OsName_IOS
  174. case "tvos":
  175. return firebase_appquality_sessions_OsName_TVOS
  176. case "watchos":
  177. return firebase_appquality_sessions_OsName_WATCHOS
  178. case "ipados":
  179. return firebase_appquality_sessions_OsName_IPADOS
  180. default:
  181. Logger.logWarning("Found unknown OSName: \"\(osName)\" while converting.")
  182. return firebase_appquality_sessions_OsName_UNKNOWN_OSNAME
  183. }
  184. }
  185. /// Encodes the proto in this SessionStartEvent to Data, and then decodes the Data back into
  186. /// the proto object and returns the decoded proto. This is used for validating encoding works
  187. /// and should not be used in production code.
  188. func encodeDecodeEvent() -> firebase_appquality_sessions_SessionEvent {
  189. let transportBytes = self.transportBytes()
  190. var proto = firebase_appquality_sessions_SessionEvent()
  191. var fields = firebase_appquality_sessions_SessionEvent_fields
  192. let bytes = (transportBytes as NSData).bytes
  193. var istream: pb_istream_t = pb_istream_from_buffer(bytes, transportBytes.count)
  194. if !pb_decode(&istream, &fields.0, &proto) {
  195. let errorMessage = FIRSESPBGetError(istream)
  196. if errorMessage.count > 0 {
  197. Logger.logError("Session Event failed to decode transportBytes: \(errorMessage)")
  198. }
  199. }
  200. return proto
  201. }
  202. /// Converts the provided log environment to its Proto format.
  203. private func convertLogEnvironment(environment: DevEnvironment)
  204. -> firebase_appquality_sessions_LogEnvironment {
  205. switch environment {
  206. case .prod:
  207. return firebase_appquality_sessions_LogEnvironment_LOG_ENVIRONMENT_PROD
  208. case .staging:
  209. return firebase_appquality_sessions_LogEnvironment_LOG_ENVIRONMENT_STAGING
  210. case .autopush:
  211. return firebase_appquality_sessions_LogEnvironment_LOG_ENVIRONMENT_AUTOPUSH
  212. }
  213. }
  214. private func convertNetworkType(networkType: GULNetworkType)
  215. -> firebase_appquality_sessions_NetworkConnectionInfo_NetworkType {
  216. switch networkType {
  217. case .WIFI:
  218. return firebase_appquality_sessions_NetworkConnectionInfo_NetworkType_WIFI
  219. case .mobile:
  220. return firebase_appquality_sessions_NetworkConnectionInfo_NetworkType_MOBILE
  221. case .none:
  222. return firebase_appquality_sessions_NetworkConnectionInfo_NetworkType_DUMMY
  223. @unknown default:
  224. return firebase_appquality_sessions_NetworkConnectionInfo_NetworkType_DUMMY
  225. }
  226. }
  227. private func convertMobileSubtype(mobileSubtype: String)
  228. -> firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype {
  229. var subtype: firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype
  230. #if os(iOS) && !targetEnvironment(macCatalyst)
  231. switch mobileSubtype {
  232. case CTRadioAccessTechnologyGPRS:
  233. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_GPRS
  234. case CTRadioAccessTechnologyEdge:
  235. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_EDGE
  236. case CTRadioAccessTechnologyWCDMA:
  237. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_CDMA
  238. case CTRadioAccessTechnologyCDMA1x:
  239. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_CDMA
  240. case CTRadioAccessTechnologyHSDPA:
  241. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_HSDPA
  242. case CTRadioAccessTechnologyHSUPA:
  243. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_HSUPA
  244. case CTRadioAccessTechnologyCDMAEVDORev0:
  245. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_EVDO_0
  246. case CTRadioAccessTechnologyCDMAEVDORevA:
  247. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_EVDO_A
  248. case CTRadioAccessTechnologyCDMAEVDORevB:
  249. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_EVDO_B
  250. case CTRadioAccessTechnologyeHRPD:
  251. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_EHRPD
  252. case CTRadioAccessTechnologyLTE:
  253. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_LTE
  254. default:
  255. subtype =
  256. firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_UNKNOWN_MOBILE_SUBTYPE
  257. }
  258. if #available(iOS 14.1, *) {
  259. if mobileSubtype == CTRadioAccessTechnologyNRNSA || mobileSubtype ==
  260. CTRadioAccessTechnologyNR {
  261. subtype = firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_NR
  262. }
  263. }
  264. #else // os(iOS) && !targetEnvironment(macCatalyst)
  265. subtype =
  266. firebase_appquality_sessions_NetworkConnectionInfo_MobileSubtype_UNKNOWN_MOBILE_SUBTYPE
  267. #endif // os(iOS) && !targetEnvironment(macCatalyst)
  268. return subtype
  269. }
  270. }