FirebaseSessions.swift 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. // Copyright 2022 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. // Avoids exposing internal FirebaseCore APIs to Swift users.
  16. @_implementationOnly import FirebaseCoreExtension
  17. @_implementationOnly import FirebaseInstallations
  18. @_implementationOnly import GoogleDataTransport
  19. @_implementationOnly import Promises
  20. private enum GoogleDataTransportConfig {
  21. static let sessionsLogSource = "1974"
  22. static let sessionsTarget = GDTCORTarget.FLL
  23. }
  24. @objc(FIRSessions) final class Sessions: NSObject, Library, SessionsProvider {
  25. // MARK: - Private Variables
  26. /// The Firebase App ID associated with Sessions.
  27. private let appID: String
  28. /// Top-level Classes in the Sessions SDK
  29. private let coordinator: SessionCoordinatorProtocol
  30. private let initiator: SessionInitiator
  31. private let sessionGenerator: SessionGenerator
  32. private let appInfo: ApplicationInfoProtocol
  33. private let settings: SettingsProtocol
  34. /// Subscribers
  35. /// `subscribers` are used to determine the Data Collection state of the Sessions SDK.
  36. /// If any Subscribers has Data Collection enabled, the Sessions SDK will send events
  37. private var subscribers: [SessionsSubscriber] = []
  38. /// `subscriberPromises` are used to wait until all Subscribers have registered
  39. /// themselves. Subscribers must have Data Collection state available upon registering.
  40. private var subscriberPromises: [SessionsSubscriberName: Promise<Void>] = [:]
  41. /// Notifications
  42. static let SessionIDChangedNotificationName = Notification
  43. .Name("SessionIDChangedNotificationName")
  44. let notificationCenter = NotificationCenter()
  45. // MARK: - Initializers
  46. // Initializes the SDK and top-level classes
  47. required convenience init(appID: String, installations: InstallationsProtocol) {
  48. let googleDataTransport = GDTCORTransport(
  49. mappingID: GoogleDataTransportConfig.sessionsLogSource,
  50. transformers: nil,
  51. target: GoogleDataTransportConfig.sessionsTarget
  52. )
  53. let fireLogger = EventGDTLogger(googleDataTransport: googleDataTransport!)
  54. let appInfo = ApplicationInfo(appID: appID)
  55. let settings = SessionsSettings(
  56. appInfo: appInfo,
  57. installations: installations
  58. )
  59. let sessionGenerator = SessionGenerator(collectEvents: Sessions
  60. .shouldCollectEvents(settings: settings))
  61. let coordinator = SessionCoordinator(
  62. installations: installations,
  63. fireLogger: fireLogger
  64. )
  65. let initiator = SessionInitiator(settings: settings)
  66. self.init(appID: appID,
  67. sessionGenerator: sessionGenerator,
  68. coordinator: coordinator,
  69. initiator: initiator,
  70. appInfo: appInfo,
  71. settings: settings) { result in
  72. switch result {
  73. case .success(()):
  74. Logger.logInfo("Successfully logged Session Start event")
  75. case let .failure(sessionsError):
  76. switch sessionsError {
  77. case let .SessionInstallationsError(error):
  78. Logger.logError(
  79. "Error getting Firebase Installation ID: \(error). Skipping this Session Event"
  80. )
  81. case let .DataTransportError(error):
  82. Logger
  83. .logError(
  84. "Error logging Session Start event to GoogleDataTransport: \(error)."
  85. )
  86. case .NoDependenciesError:
  87. Logger
  88. .logError(
  89. "Sessions SDK did not have any dependent SDKs register as dependencies. Events will not be sent."
  90. )
  91. case .SessionSamplingError:
  92. Logger
  93. .logDebug(
  94. "Sessions SDK has sampled this session"
  95. )
  96. case .DisabledViaSettingsError:
  97. Logger
  98. .logDebug(
  99. "Sessions SDK is disabled via Settings"
  100. )
  101. case .DataCollectionError:
  102. Logger
  103. .logDebug(
  104. "Data Collection is disabled for all subscribers. Skipping this Session Event"
  105. )
  106. case .SessionInstallationsTimeOutError:
  107. Logger.logError(
  108. "Error getting Firebase Installation ID due to timeout. Skipping this Session Event"
  109. )
  110. }
  111. }
  112. }
  113. }
  114. // Initializes the SDK and begines the process of listening for lifecycle events and logging
  115. // events
  116. init(appID: String, sessionGenerator: SessionGenerator, coordinator: SessionCoordinatorProtocol,
  117. initiator: SessionInitiator, appInfo: ApplicationInfoProtocol, settings: SettingsProtocol,
  118. loggedEventCallback: @escaping (Result<Void, FirebaseSessionsError>) -> Void) {
  119. self.appID = appID
  120. self.sessionGenerator = sessionGenerator
  121. self.coordinator = coordinator
  122. self.initiator = initiator
  123. self.appInfo = appInfo
  124. self.settings = settings
  125. super.init()
  126. SessionsDependencies.dependencies.forEach { subscriberName in
  127. self.subscriberPromises[subscriberName] = Promise<Void>.pending()
  128. }
  129. Logger
  130. .logDebug(
  131. "Version \(FirebaseVersion()). Expecting subscriptions from: \(SessionsDependencies.dependencies)"
  132. )
  133. self.initiator.beginListening {
  134. // Generating a Session ID early is important as Subscriber
  135. // SDKs will need to read it immediately upon registration.
  136. let sessionInfo = self.sessionGenerator.generateNewSession()
  137. // Post a notification so subscriber SDKs can get an updated Session ID
  138. self.notificationCenter.post(name: Sessions.SessionIDChangedNotificationName,
  139. object: nil)
  140. let event = SessionStartEvent(sessionInfo: sessionInfo, appInfo: self.appInfo)
  141. // If there are no Dependencies, then the Sessions SDK can't acknowledge
  142. // any products data collection state, so the Sessions SDK won't send events.
  143. guard !self.subscriberPromises.isEmpty else {
  144. loggedEventCallback(.failure(.NoDependenciesError))
  145. return
  146. }
  147. // Wait until all subscriber promises have been fulfilled before
  148. // doing any data collection.
  149. all(self.subscriberPromises.values).then(on: .global(qos: .background)) { _ in
  150. guard self.isAnyDataCollectionEnabled else {
  151. loggedEventCallback(.failure(.DataCollectionError))
  152. return
  153. }
  154. Logger.logDebug("Data Collection is enabled for at least one Subscriber")
  155. // Fetch settings if they have expired. This must happen after the check for
  156. // data collection because it uses the network, but it must happen before the
  157. // check for sessionsEnabled from Settings because otherwise we would permanently
  158. // turn off the Sessions SDK when we disabled it.
  159. self.settings.updateSettings()
  160. self.addSubscriberFields(event: event)
  161. event.setSamplingRate(samplingRate: self.settings.samplingRate)
  162. guard sessionInfo.shouldDispatchEvents else {
  163. loggedEventCallback(.failure(.SessionSamplingError))
  164. return
  165. }
  166. guard self.settings.sessionsEnabled else {
  167. loggedEventCallback(.failure(.DisabledViaSettingsError))
  168. return
  169. }
  170. self.coordinator.attemptLoggingSessionStart(event: event) { result in
  171. loggedEventCallback(result)
  172. }
  173. }
  174. }
  175. }
  176. // MARK: - Sampling
  177. static func shouldCollectEvents(settings: SettingsProtocol) -> Bool {
  178. // Calculate whether we should sample events using settings data
  179. // Sampling rate of 1 means we do not sample.
  180. let randomValue = Double.random(in: 0 ... 1)
  181. return randomValue <= settings.samplingRate
  182. }
  183. // MARK: - Data Collection
  184. var isAnyDataCollectionEnabled: Bool {
  185. for subscriber in subscribers {
  186. if subscriber.isDataCollectionEnabled {
  187. return true
  188. }
  189. }
  190. return false
  191. }
  192. func addSubscriberFields(event: SessionStartEvent) {
  193. subscribers.forEach { subscriber in
  194. event.set(subscriber: subscriber.sessionsSubscriberName,
  195. isDataCollectionEnabled: subscriber.isDataCollectionEnabled,
  196. appInfo: self.appInfo)
  197. }
  198. }
  199. // MARK: - SessionsProvider
  200. var currentSessionDetails: SessionDetails {
  201. return SessionDetails(sessionId: sessionGenerator.currentSession?.sessionId)
  202. }
  203. func register(subscriber: SessionsSubscriber) {
  204. Logger
  205. .logDebug(
  206. "Registering Sessions SDK subscriber with name: \(subscriber.sessionsSubscriberName), data collection enabled: \(subscriber.isDataCollectionEnabled)"
  207. )
  208. notificationCenter.addObserver(
  209. forName: Sessions.SessionIDChangedNotificationName,
  210. object: nil,
  211. queue: nil
  212. ) { notification in
  213. subscriber.onSessionChanged(self.currentSessionDetails)
  214. }
  215. // Immediately call the callback because the Sessions SDK starts
  216. // before subscribers, so subscribers will miss the first Notification
  217. subscriber.onSessionChanged(currentSessionDetails)
  218. // Fulfil this subscriber's promise
  219. subscribers.append(subscriber)
  220. subscriberPromises[subscriber.sessionsSubscriberName]?.fulfill(())
  221. }
  222. // MARK: - Library conformance
  223. static func componentsToRegister() -> [Component] {
  224. return [Component(SessionsProvider.self,
  225. instantiationTiming: .alwaysEager,
  226. dependencies: []) { container, isCacheable in
  227. // Sessions SDK only works for the default app
  228. guard let app = container.app, app.isDefaultApp else { return nil }
  229. isCacheable.pointee = true
  230. let installations = Installations.installations(app: app)
  231. return self.init(appID: app.options.googleAppID, installations: installations)
  232. }]
  233. }
  234. }