FirebaseSessions.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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. internal import FirebaseCoreExtension
  17. internal import FirebaseInstallations
  18. internal import GoogleDataTransport
  19. #if swift(>=6.0)
  20. internal import Promises
  21. #elseif swift(>=5.10)
  22. import Promises
  23. #else
  24. internal import Promises
  25. #endif
  26. private enum GoogleDataTransportConfig {
  27. static let sessionsLogSource = "1974"
  28. static let sessionsTarget = GDTCORTarget.FLL
  29. }
  30. @objc(FIRSessions) final class Sessions: NSObject, Library, SessionsProvider {
  31. // MARK: - Private Variables
  32. /// The Firebase App ID associated with Sessions.
  33. private let appID: String
  34. /// Top-level Classes in the Sessions SDK
  35. private let coordinator: SessionCoordinatorProtocol
  36. private let initiator: SessionInitiator
  37. private let sessionGenerator: SessionGenerator
  38. private let appInfo: ApplicationInfoProtocol
  39. private let settings: SettingsProtocol
  40. /// Subscribers
  41. /// `subscribers` are used to determine the Data Collection state of the Sessions SDK.
  42. /// If any Subscribers has Data Collection enabled, the Sessions SDK will send events
  43. private var subscribers: [SessionsSubscriber] = []
  44. /// `subscriberPromises` are used to wait until all Subscribers have registered
  45. /// themselves. Subscribers must have Data Collection state available upon registering.
  46. private var subscriberPromises: [SessionsSubscriberName: Promise<Void>] = [:]
  47. /// Notifications
  48. static let SessionIDChangedNotificationName = Notification
  49. .Name("SessionIDChangedNotificationName")
  50. let notificationCenter = NotificationCenter()
  51. // MARK: - Initializers
  52. // Initializes the SDK and top-level classes
  53. required convenience init(appID: String, installations: InstallationsProtocol) {
  54. let googleDataTransport = GoogleDataTransporter(
  55. mappingID: GoogleDataTransportConfig.sessionsLogSource,
  56. transformers: nil,
  57. target: GoogleDataTransportConfig.sessionsTarget
  58. )
  59. let fireLogger = EventGDTLogger(googleDataTransport: googleDataTransport)
  60. let appInfo = ApplicationInfo(appID: appID)
  61. let settings = SessionsSettings(
  62. appInfo: appInfo,
  63. installations: installations
  64. )
  65. let sessionGenerator = SessionGenerator(collectEvents: Sessions
  66. .shouldCollectEvents(settings: settings))
  67. let coordinator = SessionCoordinator(
  68. installations: installations,
  69. fireLogger: fireLogger
  70. )
  71. let initiator = SessionInitiator(settings: settings)
  72. self.init(appID: appID,
  73. sessionGenerator: sessionGenerator,
  74. coordinator: coordinator,
  75. initiator: initiator,
  76. appInfo: appInfo,
  77. settings: settings) { result in
  78. switch result {
  79. case .success(()):
  80. Logger.logInfo("Successfully logged Session Start event")
  81. case let .failure(sessionsError):
  82. switch sessionsError {
  83. case let .SessionInstallationsError(error):
  84. Logger.logError(
  85. "Error getting Firebase Installation ID: \(error). Skipping this Session Event"
  86. )
  87. case let .DataTransportError(error):
  88. Logger
  89. .logError(
  90. "Error logging Session Start event to GoogleDataTransport: \(error)."
  91. )
  92. case .NoDependenciesError:
  93. Logger
  94. .logError(
  95. "Sessions SDK did not have any dependent SDKs register as dependencies. Events will not be sent."
  96. )
  97. case .SessionSamplingError:
  98. Logger
  99. .logDebug(
  100. "Sessions SDK has sampled this session"
  101. )
  102. case .DisabledViaSettingsError:
  103. Logger
  104. .logDebug(
  105. "Sessions SDK is disabled via Settings"
  106. )
  107. case .DataCollectionError:
  108. Logger
  109. .logDebug(
  110. "Data Collection is disabled for all subscribers. Skipping this Session Event"
  111. )
  112. case .SessionInstallationsTimeOutError:
  113. Logger.logError(
  114. "Error getting Firebase Installation ID due to timeout. Skipping this Session Event"
  115. )
  116. }
  117. }
  118. }
  119. }
  120. // Initializes the SDK and begins the process of listening for lifecycle events and logging
  121. // events. `logEventCallback` is invoked on a global background queue.
  122. init(appID: String, sessionGenerator: SessionGenerator, coordinator: SessionCoordinatorProtocol,
  123. initiator: SessionInitiator, appInfo: ApplicationInfoProtocol, settings: SettingsProtocol,
  124. loggedEventCallback: @escaping @Sendable (Result<Void, FirebaseSessionsError>) -> Void) {
  125. self.appID = appID
  126. self.sessionGenerator = sessionGenerator
  127. self.coordinator = coordinator
  128. self.initiator = initiator
  129. self.appInfo = appInfo
  130. self.settings = settings
  131. super.init()
  132. let dependencies = SessionsDependencies.dependencies
  133. for subscriberName in dependencies {
  134. subscriberPromises[subscriberName] = Promise<Void>.pending()
  135. }
  136. Logger
  137. .logDebug(
  138. "Version \(FirebaseVersion()). Expecting subscriptions from: \(dependencies)"
  139. )
  140. self.initiator.beginListening {
  141. // Generating a Session ID early is important as Subscriber
  142. // SDKs will need to read it immediately upon registration.
  143. let sessionInfo = self.sessionGenerator.generateNewSession()
  144. // Post a notification so subscriber SDKs can get an updated Session ID
  145. self.notificationCenter.post(name: Sessions.SessionIDChangedNotificationName,
  146. object: nil)
  147. let event = SessionStartEvent(sessionInfo: sessionInfo, appInfo: self.appInfo)
  148. // If there are no Dependencies, then the Sessions SDK can't acknowledge
  149. // any products data collection state, so the Sessions SDK won't send events.
  150. guard !self.subscriberPromises.isEmpty else {
  151. loggedEventCallback(.failure(.NoDependenciesError))
  152. return
  153. }
  154. // Wait until all subscriber promises have been fulfilled before
  155. // doing any data collection.
  156. all(self.subscriberPromises.values).then(on: .global(qos: .background)) { _ in
  157. guard self.isAnyDataCollectionEnabled else {
  158. loggedEventCallback(.failure(.DataCollectionError))
  159. return
  160. }
  161. Logger.logDebug("Data Collection is enabled for at least one Subscriber")
  162. // Fetch settings if they have expired. This must happen after the check for
  163. // data collection because it uses the network, but it must happen before the
  164. // check for sessionsEnabled from Settings because otherwise we would permanently
  165. // turn off the Sessions SDK when we disabled it.
  166. self.settings.updateSettings()
  167. self.addSubscriberFields(event: event)
  168. event.setSamplingRate(samplingRate: self.settings.samplingRate)
  169. guard sessionInfo.shouldDispatchEvents else {
  170. loggedEventCallback(.failure(.SessionSamplingError))
  171. return
  172. }
  173. guard self.settings.sessionsEnabled else {
  174. loggedEventCallback(.failure(.DisabledViaSettingsError))
  175. return
  176. }
  177. self.coordinator.attemptLoggingSessionStart(event: event) { result in
  178. loggedEventCallback(result)
  179. }
  180. }
  181. }
  182. }
  183. // MARK: - Sampling
  184. static func shouldCollectEvents(settings: SettingsProtocol) -> Bool {
  185. // Calculate whether we should sample events using settings data
  186. // Sampling rate of 1 means we do not sample.
  187. let randomValue = Double.random(in: 0 ... 1)
  188. return randomValue <= settings.samplingRate
  189. }
  190. // MARK: - Data Collection
  191. var isAnyDataCollectionEnabled: Bool {
  192. for subscriber in subscribers {
  193. if subscriber.isDataCollectionEnabled {
  194. return true
  195. }
  196. }
  197. return false
  198. }
  199. func addSubscriberFields(event: SessionStartEvent) {
  200. for subscriber in subscribers {
  201. event.set(subscriber: subscriber.sessionsSubscriberName,
  202. isDataCollectionEnabled: subscriber.isDataCollectionEnabled,
  203. appInfo: appInfo)
  204. }
  205. }
  206. // MARK: - SessionsProvider
  207. var currentSessionDetails: SessionDetails {
  208. return SessionDetails(sessionId: sessionGenerator.currentSession?.sessionId)
  209. }
  210. // This type is not actually sendable, but works around an issue below.
  211. // It's safe only if executed on the main actor.
  212. private struct MainActorNotificationCallback: @unchecked Sendable {
  213. private let callback: (Notification) -> Void
  214. init(_ callback: @escaping (Notification) -> Void) {
  215. self.callback = callback
  216. }
  217. func invoke(notification: Notification) {
  218. dispatchPrecondition(condition: .onQueue(.main))
  219. callback(notification)
  220. }
  221. }
  222. func register(subscriber: SessionsSubscriber) {
  223. Logger
  224. .logDebug(
  225. "Registering Sessions SDK subscriber with name: \(subscriber.sessionsSubscriberName), data collection enabled: \(subscriber.isDataCollectionEnabled)"
  226. )
  227. // TODO(Firebase 12): After bumping to iOS 13, this hack should be replaced
  228. // with `Task { @MainActor in }`.
  229. let callback = MainActorNotificationCallback { notification in
  230. subscriber.onSessionChanged(self.currentSessionDetails)
  231. }
  232. // Guaranteed to execute its callback on the main queue because of the queue parameter.
  233. notificationCenter.addObserver(
  234. forName: Sessions.SessionIDChangedNotificationName,
  235. object: nil,
  236. queue: OperationQueue.main
  237. ) { notification in
  238. callback.invoke(notification: notification)
  239. }
  240. // Immediately call the callback because the Sessions SDK starts
  241. // before subscribers, so subscribers will miss the first Notification
  242. subscriber.onSessionChanged(currentSessionDetails)
  243. // Fulfil this subscriber's promise
  244. subscribers.append(subscriber)
  245. subscriberPromises[subscriber.sessionsSubscriberName]?.fulfill(())
  246. }
  247. // MARK: - Library conformance
  248. static func componentsToRegister() -> [Component] {
  249. return [Component(SessionsProvider.self,
  250. instantiationTiming: .alwaysEager) { container, isCacheable in
  251. // Sessions SDK only works for the default app
  252. guard let app = container.app, app.isDefaultApp else { return nil }
  253. isCacheable.pointee = true
  254. let installations = Installations.installations(app: app)
  255. return self.init(appID: app.options.googleAppID, installations: installations)
  256. }]
  257. }
  258. }