| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- // Copyright 2022 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- import Foundation
- // Avoids exposing internal FirebaseCore APIs to Swift users.
- internal import FirebaseCoreExtension
- internal import FirebaseInstallations
- internal import GoogleDataTransport
- #if swift(>=6.0)
- internal import Promises
- #elseif swift(>=5.10)
- import Promises
- #else
- internal import Promises
- #endif
- private enum GoogleDataTransportConfig {
- static let sessionsLogSource = "1974"
- static let sessionsTarget = GDTCORTarget.FLL
- }
- @objc(FIRSessions) final class Sessions: NSObject, Library, SessionsProvider {
- // MARK: - Private Variables
- /// The Firebase App ID associated with Sessions.
- private let appID: String
- /// Top-level Classes in the Sessions SDK
- private let coordinator: SessionCoordinatorProtocol
- private let initiator: SessionInitiator
- private let sessionGenerator: SessionGenerator
- private let appInfo: ApplicationInfoProtocol
- private let settings: SettingsProtocol
- /// Subscribers
- /// `subscribers` are used to determine the Data Collection state of the Sessions SDK.
- /// If any Subscribers has Data Collection enabled, the Sessions SDK will send events
- private var subscribers: [SessionsSubscriber] = []
- /// `subscriberPromises` are used to wait until all Subscribers have registered
- /// themselves. Subscribers must have Data Collection state available upon registering.
- private var subscriberPromises: [SessionsSubscriberName: Promise<Void>] = [:]
- /// Notifications
- static let SessionIDChangedNotificationName = Notification
- .Name("SessionIDChangedNotificationName")
- let notificationCenter = NotificationCenter()
- // MARK: - Initializers
- // Initializes the SDK and top-level classes
- required convenience init(appID: String, installations: InstallationsProtocol) {
- let googleDataTransport = GoogleDataTransporter(
- mappingID: GoogleDataTransportConfig.sessionsLogSource,
- transformers: nil,
- target: GoogleDataTransportConfig.sessionsTarget
- )
- let fireLogger = EventGDTLogger(googleDataTransport: googleDataTransport)
- let appInfo = ApplicationInfo(appID: appID)
- let settings = SessionsSettings(
- appInfo: appInfo,
- installations: installations
- )
- let sessionGenerator = SessionGenerator(collectEvents: Sessions
- .shouldCollectEvents(settings: settings))
- let coordinator = SessionCoordinator(
- installations: installations,
- fireLogger: fireLogger
- )
- let initiator = SessionInitiator(settings: settings)
- self.init(appID: appID,
- sessionGenerator: sessionGenerator,
- coordinator: coordinator,
- initiator: initiator,
- appInfo: appInfo,
- settings: settings) { result in
- switch result {
- case .success(()):
- Logger.logInfo("Successfully logged Session Start event")
- case let .failure(sessionsError):
- switch sessionsError {
- case let .SessionInstallationsError(error):
- Logger.logError(
- "Error getting Firebase Installation ID: \(error). Skipping this Session Event"
- )
- case let .DataTransportError(error):
- Logger
- .logError(
- "Error logging Session Start event to GoogleDataTransport: \(error)."
- )
- case .NoDependenciesError:
- Logger
- .logError(
- "Sessions SDK did not have any dependent SDKs register as dependencies. Events will not be sent."
- )
- case .SessionSamplingError:
- Logger
- .logDebug(
- "Sessions SDK has sampled this session"
- )
- case .DisabledViaSettingsError:
- Logger
- .logDebug(
- "Sessions SDK is disabled via Settings"
- )
- case .DataCollectionError:
- Logger
- .logDebug(
- "Data Collection is disabled for all subscribers. Skipping this Session Event"
- )
- case .SessionInstallationsTimeOutError:
- Logger.logError(
- "Error getting Firebase Installation ID due to timeout. Skipping this Session Event"
- )
- }
- }
- }
- }
- // Initializes the SDK and begins the process of listening for lifecycle events and logging
- // events. `logEventCallback` is invoked on a global background queue.
- init(appID: String, sessionGenerator: SessionGenerator, coordinator: SessionCoordinatorProtocol,
- initiator: SessionInitiator, appInfo: ApplicationInfoProtocol, settings: SettingsProtocol,
- loggedEventCallback: @escaping @Sendable (Result<Void, FirebaseSessionsError>) -> Void) {
- self.appID = appID
- self.sessionGenerator = sessionGenerator
- self.coordinator = coordinator
- self.initiator = initiator
- self.appInfo = appInfo
- self.settings = settings
- super.init()
- let dependencies = SessionsDependencies.dependencies
- for subscriberName in dependencies {
- subscriberPromises[subscriberName] = Promise<Void>.pending()
- }
- Logger
- .logDebug(
- "Version \(FirebaseVersion()). Expecting subscriptions from: \(dependencies)"
- )
- self.initiator.beginListening {
- // Generating a Session ID early is important as Subscriber
- // SDKs will need to read it immediately upon registration.
- let sessionInfo = self.sessionGenerator.generateNewSession()
- // Post a notification so subscriber SDKs can get an updated Session ID
- self.notificationCenter.post(name: Sessions.SessionIDChangedNotificationName,
- object: nil)
- let event = SessionStartEvent(sessionInfo: sessionInfo, appInfo: self.appInfo)
- // If there are no Dependencies, then the Sessions SDK can't acknowledge
- // any products data collection state, so the Sessions SDK won't send events.
- guard !self.subscriberPromises.isEmpty else {
- loggedEventCallback(.failure(.NoDependenciesError))
- return
- }
- // Wait until all subscriber promises have been fulfilled before
- // doing any data collection.
- all(self.subscriberPromises.values).then(on: .global(qos: .background)) { _ in
- guard self.isAnyDataCollectionEnabled else {
- loggedEventCallback(.failure(.DataCollectionError))
- return
- }
- Logger.logDebug("Data Collection is enabled for at least one Subscriber")
- // Fetch settings if they have expired. This must happen after the check for
- // data collection because it uses the network, but it must happen before the
- // check for sessionsEnabled from Settings because otherwise we would permanently
- // turn off the Sessions SDK when we disabled it.
- self.settings.updateSettings()
- self.addSubscriberFields(event: event)
- event.setSamplingRate(samplingRate: self.settings.samplingRate)
- guard sessionInfo.shouldDispatchEvents else {
- loggedEventCallback(.failure(.SessionSamplingError))
- return
- }
- guard self.settings.sessionsEnabled else {
- loggedEventCallback(.failure(.DisabledViaSettingsError))
- return
- }
- self.coordinator.attemptLoggingSessionStart(event: event) { result in
- loggedEventCallback(result)
- }
- }
- }
- }
- // MARK: - Sampling
- static func shouldCollectEvents(settings: SettingsProtocol) -> Bool {
- // Calculate whether we should sample events using settings data
- // Sampling rate of 1 means we do not sample.
- let randomValue = Double.random(in: 0 ... 1)
- return randomValue <= settings.samplingRate
- }
- // MARK: - Data Collection
- var isAnyDataCollectionEnabled: Bool {
- for subscriber in subscribers {
- if subscriber.isDataCollectionEnabled {
- return true
- }
- }
- return false
- }
- func addSubscriberFields(event: SessionStartEvent) {
- for subscriber in subscribers {
- event.set(subscriber: subscriber.sessionsSubscriberName,
- isDataCollectionEnabled: subscriber.isDataCollectionEnabled,
- appInfo: appInfo)
- }
- }
- // MARK: - SessionsProvider
- var currentSessionDetails: SessionDetails {
- return SessionDetails(sessionId: sessionGenerator.currentSession?.sessionId)
- }
- // This type is not actually sendable, but works around an issue below.
- // It's safe only if executed on the main actor.
- private struct MainActorNotificationCallback: @unchecked Sendable {
- private let callback: (Notification) -> Void
- init(_ callback: @escaping (Notification) -> Void) {
- self.callback = callback
- }
- func invoke(notification: Notification) {
- dispatchPrecondition(condition: .onQueue(.main))
- callback(notification)
- }
- }
- func register(subscriber: SessionsSubscriber) {
- Logger
- .logDebug(
- "Registering Sessions SDK subscriber with name: \(subscriber.sessionsSubscriberName), data collection enabled: \(subscriber.isDataCollectionEnabled)"
- )
- // TODO(Firebase 12): After bumping to iOS 13, this hack should be replaced
- // with `Task { @MainActor in }`.
- let callback = MainActorNotificationCallback { notification in
- subscriber.onSessionChanged(self.currentSessionDetails)
- }
- // Guaranteed to execute its callback on the main queue because of the queue parameter.
- notificationCenter.addObserver(
- forName: Sessions.SessionIDChangedNotificationName,
- object: nil,
- queue: OperationQueue.main
- ) { notification in
- callback.invoke(notification: notification)
- }
- // Immediately call the callback because the Sessions SDK starts
- // before subscribers, so subscribers will miss the first Notification
- subscriber.onSessionChanged(currentSessionDetails)
- // Fulfil this subscriber's promise
- subscribers.append(subscriber)
- subscriberPromises[subscriber.sessionsSubscriberName]?.fulfill(())
- }
- // MARK: - Library conformance
- static func componentsToRegister() -> [Component] {
- return [Component(SessionsProvider.self,
- instantiationTiming: .alwaysEager) { container, isCacheable in
- // Sessions SDK only works for the default app
- guard let app = container.app, app.isDefaultApp else { return nil }
- isCacheable.pointee = true
- let installations = Installations.installations(app: app)
- return self.init(appID: app.options.googleAppID, installations: installations)
- }]
- }
- }
|