Skip to content

Commit 077091a

Browse files
authored
Add Sessions Internal API (#10665)
1 parent b2a4ccd commit 077091a

10 files changed

+247
-26
lines changed

FirebaseSessions.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Pod::Spec.new do |s|
4545
s.dependency 'GoogleDataTransport', '~> 9.2'
4646
s.dependency 'GoogleUtilities/Environment', '~> 7.10'
4747
s.dependency 'nanopb', '>= 2.30908.0', '< 2.30910.0'
48+
s.dependency 'PromisesSwift', '~> 2.1'
4849

4950
s.pod_target_xcconfig = {
5051
'GCC_C_LANGUAGE_STANDARD' => 'c99',

FirebaseSessions/Sources/FirebaseSessions.swift

Lines changed: 106 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,13 @@ import Foundation
1818
@_implementationOnly import FirebaseCoreExtension
1919
@_implementationOnly import FirebaseInstallations
2020
@_implementationOnly import GoogleDataTransport
21+
@_implementationOnly import Promises
2122

2223
private enum GoogleDataTransportConfig {
2324
static let sessionsLogSource = "1974"
2425
static let sessionsTarget = GDTCORTarget.FLL
2526
}
2627

27-
@objc(FIRSessionsProvider)
28-
protocol SessionsProvider {
29-
@objc static func sessions() -> Void
30-
}
31-
3228
@objc(FIRSessions) final class Sessions: NSObject, Library, SessionsProvider {
3329
// MARK: - Private Variables
3430

@@ -42,6 +38,19 @@ protocol SessionsProvider {
4238
private let appInfo: ApplicationInfo
4339
private let settings: SessionsSettings
4440

41+
/// Subscribers
42+
/// `subscribers` are used to determine the Data Collection state of the Sessions SDK.
43+
/// If any Subscribers has Data Collection enabled, the Sessions SDK will send events
44+
private var subscribers: [SessionsSubscriber] = []
45+
/// `subscriberPromises` are used to wait until all Subscribers have registered
46+
/// themselves. Subscribers must have Data Collection state available upon registering.
47+
private var subscriberPromises: [SessionsSubscriberName: Promise<Void>] = [:]
48+
49+
/// Notifications
50+
static let SessionIDChangedNotificationName = Notification
51+
.Name("SessionIDChangedNotificationName")
52+
let notificationCenter = NotificationCenter()
53+
4554
// MARK: - Initializers
4655

4756
// Initializes the SDK and top-level classes
@@ -89,23 +98,104 @@ protocol SessionsProvider {
8998

9099
super.init()
91100

101+
SessionsDependencies.dependencies.forEach { subscriberName in
102+
self.subscriberPromises[subscriberName] = Promise<Void>.pending()
103+
}
104+
105+
Logger.logDebug("Expecting subscriptions from: \(SessionsDependencies.dependencies)")
106+
92107
self.initiator.beginListening {
93-
// On each session start, first update Settings if expired
94-
self.settings.updateSettings()
95-
let session = self.sessionGenerator.generateNewSession()
96-
// Generate a session start event only when session data collection is enabled and if the session is allowed to dispatch events
97-
if self.settings.sessionsEnabled, session.shouldDispatchEvents {
98-
let event = SessionStartEvent(sessionInfo: session, appInfo: self.appInfo)
99-
DispatchQueue.global().async {
100-
self.coordinator.attemptLoggingSessionStart(event: event) { result in
101-
}
108+
// Generating a Session ID early is important as Subscriber
109+
// SDKs will need to read it immediately upon registration.
110+
let sessionInfo = self.sessionGenerator.generateNewSession()
111+
112+
// Post a notification so subscriber SDKs can get an updated Session ID
113+
self.notificationCenter.post(name: Sessions.SessionIDChangedNotificationName,
114+
object: nil)
115+
116+
let event = SessionStartEvent(sessionInfo: sessionInfo, appInfo: self.appInfo)
117+
118+
// Wait until all subscriber promises have been fulfilled before
119+
// doing any data collection.
120+
all(self.subscriberPromises.values).then(on: .global(qos: .background)) { _ in
121+
guard self.isAnyDataCollectionEnabled else {
122+
Logger
123+
.logDebug(
124+
"Data Collection is disabled for all subscribers. Skipping this Session Event"
125+
)
126+
return
127+
}
128+
129+
Logger.logDebug("Data Collection is enabled for at least one Subscriber")
130+
131+
// Fetch settings if they have expired. This must happen after the check for
132+
// data collection because it uses the network, but it must happen before the
133+
// check for sessionsEnabled from Settings because otherwise we would permanently
134+
// turn off the Sessions SDK when we disabled it.
135+
self.settings.updateSettings()
136+
137+
self.addEventDataCollectionState(event: event)
138+
139+
if !(self.settings.sessionsEnabled && sessionInfo.shouldDispatchEvents) {
140+
Logger
141+
.logDebug(
142+
"Session Event logging is disabled sessionsEnabled: \(self.settings.sessionsEnabled), shouldDispatchEvents: \(sessionInfo.shouldDispatchEvents)"
143+
)
144+
return
145+
}
146+
147+
self.coordinator.attemptLoggingSessionStart(event: event) { result in
102148
}
103-
} else {
104-
Logger.logDebug("Session logging is disabled by configuration settings.")
105149
}
106150
}
107151
}
108152

153+
// MARK: - Data Collection
154+
155+
var isAnyDataCollectionEnabled: Bool {
156+
for subscriber in subscribers {
157+
if subscriber.isDataCollectionEnabled {
158+
return true
159+
}
160+
}
161+
return false
162+
}
163+
164+
func addEventDataCollectionState(event: SessionStartEvent) {
165+
subscribers.forEach { subscriber in
166+
event.set(subscriber: subscriber.sessionsSubscriberName,
167+
isDataCollectionEnabled: subscriber.isDataCollectionEnabled)
168+
}
169+
}
170+
171+
// MARK: - SessionsProvider
172+
173+
var currentSessionDetails: SessionDetails {
174+
return SessionDetails(sessionId: sessionGenerator.currentSession?.sessionId)
175+
}
176+
177+
func register(subscriber: SessionsSubscriber) {
178+
Logger
179+
.logDebug(
180+
"Registering Sessions SDK subscriber with name: \(subscriber.sessionsSubscriberName), data collection enabled: \(subscriber.isDataCollectionEnabled)"
181+
)
182+
183+
notificationCenter.addObserver(
184+
forName: Sessions.SessionIDChangedNotificationName,
185+
object: nil,
186+
queue: nil
187+
) { notification in
188+
subscriber.onSessionChanged(self.currentSessionDetails)
189+
}
190+
// Immediately call the callback because the Sessions SDK starts
191+
// before subscribers, so subscribers will miss the first Notification
192+
subscriber.onSessionChanged(currentSessionDetails)
193+
194+
// Fulfil this subscriber's promise
195+
subscribers.append(subscriber)
196+
subscriberPromises[subscriber.sessionsSubscriberName]?.fulfill(())
197+
}
198+
109199
// MARK: - Library conformance
110200

111201
static func componentsToRegister() -> [Component] {
@@ -119,8 +209,4 @@ protocol SessionsProvider {
119209
return self.init(appID: app.options.googleAppID, installations: installations)
120210
}]
121211
}
122-
123-
// MARK: - SessionsProvider conformance
124-
125-
static func sessions() {}
126212
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
16+
import Foundation
17+
18+
// Sessions Dependencies determines when a dependent SDK is
19+
// installed in the app. The Sessions SDK uses this to figure
20+
// out which dependencies to wait for to getting the data
21+
// collection state.
22+
//
23+
// This is important because the Sessions SDK starts up before
24+
// dependent SDKs
25+
@objc(FIRSessionsDependencies)
26+
public class SessionsDependencies: NSObject {
27+
static var dependencies: Set<SessionsSubscriberName> = .init()
28+
29+
@objc public static func addDependency(name: SessionsSubscriberName) {
30+
SessionsDependencies.dependencies.insert(name)
31+
}
32+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
16+
import Foundation
17+
18+
// Sessions Provider is the Session SDK's internal
19+
// interface for other 1P SDKs to talk to.
20+
@objc(FIRSessionsProvider)
21+
public protocol SessionsProvider {
22+
@objc func register(subscriber: SessionsSubscriber)
23+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
16+
import Foundation
17+
18+
/// Sessions Subscriber is an interface that dependent SDKs
19+
/// must implement.
20+
@objc(FIRSessionsSubscriber)
21+
public protocol SessionsSubscriber {
22+
func onSessionChanged(_ session: SessionDetails)
23+
var isDataCollectionEnabled: Bool { get }
24+
var sessionsSubscriberName: SessionsSubscriberName { get }
25+
}
26+
27+
/// Session Payload is a container for Session Data passed to Subscribers
28+
/// whenever the Session changes
29+
@objc(FIRSessionDetails)
30+
public class SessionDetails: NSObject {
31+
var sessionId: String?
32+
33+
public init(sessionId: String?) {
34+
self.sessionId = sessionId
35+
super.init()
36+
}
37+
}
38+
39+
/// Session Subscriber Names are used for identifying subscribers
40+
@objc(FIRSessionsSubscriberName)
41+
public enum SessionsSubscriberName: Int, CustomStringConvertible {
42+
case Unknown
43+
case Crashlytics
44+
case Performance
45+
46+
public var description: String {
47+
switch self {
48+
case .Crashlytics:
49+
return "Crashlytics"
50+
case .Performance:
51+
return "Performance"
52+
default:
53+
return "Unknown"
54+
}
55+
}
56+
}

FirebaseSessions/Sources/SessionGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class SessionGenerator {
6262
return newSession
6363
}
6464

65-
func currentSession() -> SessionInfo? {
65+
var currentSession: SessionInfo? {
6666
return thisSession
6767
}
6868
}

FirebaseSessions/Sources/SessionStartEvent.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ class SessionStartEvent: NSObject, GDTCOREventDataObject {
6565
proto.application_info.apple_app_info.mcc_mnc = makeProtoString(appInfo.mccMNC)
6666

6767
proto.session_data.data_collection_status
68-
.crashlytics = firebase_appquality_sessions_DataCollectionState_COLLECTION_UNKNOWN
68+
.crashlytics = firebase_appquality_sessions_DataCollectionState_COLLECTION_SDK_NOT_INSTALLED
6969
proto.session_data.data_collection_status
70-
.performance = firebase_appquality_sessions_DataCollectionState_COLLECTION_UNKNOWN
70+
.performance = firebase_appquality_sessions_DataCollectionState_COLLECTION_SDK_NOT_INSTALLED
7171
}
7272

7373
func setInstallationID(installationId: String) {
@@ -78,6 +78,19 @@ class SessionStartEvent: NSObject, GDTCOREventDataObject {
7878
proto.session_data.data_collection_status.session_sampling_rate = samplingRate
7979
}
8080

81+
func set(subscriber: SessionsSubscriberName, isDataCollectionEnabled: Bool) {
82+
let dataCollectionState = makeDataCollectionProto(isDataCollectionEnabled)
83+
switch subscriber {
84+
case .Crashlytics:
85+
proto.session_data.data_collection_status.crashlytics = dataCollectionState
86+
case .Performance:
87+
proto.session_data.data_collection_status.performance = dataCollectionState
88+
default:
89+
Logger
90+
.logWarning("Attempted to set Data Collection status for unknown Subscriber: \(subscriber)")
91+
}
92+
}
93+
8194
// MARK: - GDTCOREventDataObject
8295

8396
func transportBytes() -> Data {
@@ -96,6 +109,15 @@ class SessionStartEvent: NSObject, GDTCOREventDataObject {
96109

97110
// MARK: - Data Conversion
98111

112+
func makeDataCollectionProto(_ isDataCollectionEnabled: Bool)
113+
-> firebase_appquality_sessions_DataCollectionState {
114+
if isDataCollectionEnabled {
115+
return firebase_appquality_sessions_DataCollectionState_COLLECTION_ENABLED
116+
} else {
117+
return firebase_appquality_sessions_DataCollectionState_COLLECTION_DISABLED
118+
}
119+
}
120+
99121
private func makeProtoStringOrNil(_ string: String?) -> UnsafeMutablePointer<pb_bytes_array_t>? {
100122
guard let string = string else {
101123
return nil

FirebaseSessions/Tests/Unit/SessionGeneratorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class SessionGeneratorTests: XCTestCase {
7676
// with the Sessions SDK, we may want to move to a lazy solution where
7777
// sessionID can never be empty
7878
func test_sessionID_beforeGenerateReturnsNothing() throws {
79-
XCTAssertNil(generator.currentSession())
79+
XCTAssertNil(generator.currentSession)
8080
}
8181

8282
func test_generateNewSessionID_generatesValidID() throws {

FirebaseSessions/Tests/Unit/SessionStartEventTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,11 @@ class SessionStartEventTests: XCTestCase {
208208
testProtoAndDecodedProto(sessionEvent: event) { proto in
209209
XCTAssertEqual(
210210
proto.session_data.data_collection_status.performance,
211-
firebase_appquality_sessions_DataCollectionState_COLLECTION_UNKNOWN
211+
firebase_appquality_sessions_DataCollectionState_COLLECTION_SDK_NOT_INSTALLED
212212
)
213213
XCTAssertEqual(
214214
proto.session_data.data_collection_status.crashlytics,
215-
firebase_appquality_sessions_DataCollectionState_COLLECTION_UNKNOWN
215+
firebase_appquality_sessions_DataCollectionState_COLLECTION_SDK_NOT_INSTALLED
216216
)
217217
}
218218
}

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,7 @@ let package = Package(
10891089
"FirebaseInstallations",
10901090
"FirebaseCoreExtension",
10911091
"FirebaseSessionsObjC",
1092+
.product(name: "Promises", package: "Promises"),
10921093
.product(name: "GoogleDataTransport", package: "GoogleDataTransport"),
10931094
.product(name: "GULEnvironment", package: "GoogleUtilities"),
10941095
],

0 commit comments

Comments
 (0)