Skip to content

Commit 5d9f06a

Browse files
authored
[Sessions] Setup Sessions SDK Class Structure and Tests (#10297)
1 parent 6bda67a commit 5d9f06a

12 files changed

+571
-7
lines changed

.github/workflows/sessions.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: sessions
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'FirebaseSessions**'
7+
- 'FirebaseSessions.podspec'
8+
- '.github/workflows/sessions.yml'
9+
- 'Gemfile*'
10+
schedule:
11+
# Run every day at 9am (PST) - cron uses UTC times
12+
- cron: '0 1 * * *'
13+
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
20+
pod-lib-lint:
21+
# Don't run on private repo unless it is a PR.
22+
if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request'
23+
24+
runs-on: macos-12
25+
26+
strategy:
27+
matrix:
28+
target: [ios, tvos, macos, watchos --skip-tests]
29+
steps:
30+
- uses: actions/checkout@v2
31+
- uses: ruby/setup-ruby@v1
32+
- name: Setup Bundler
33+
run: scripts/setup_bundler.sh
34+
- name: Build and test
35+
run: |
36+
scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseSessions.podspec --platforms=${{ matrix.target }}
37+
38+
#
39+
# Uncomment below when Swift Package Manager is implemented
40+
#
41+
# spm:
42+
# # Don't run on private repo unless it is a PR.
43+
# if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request'
44+
# runs-on: macos-12
45+
# strategy:
46+
# matrix:
47+
# target: [iOS, tvOS, macOS, catalyst, watchOS]
48+
# steps:
49+
# - uses: actions/checkout@v2
50+
# - uses: mikehardy/buildcache-action@50738c6c77de7f34e66b870e4f8ede333b69d077
51+
# with:
52+
# cache_key: ${{ matrix.os }}
53+
# - name: Initialize xcodebuild
54+
# run: scripts/setup_spm_tests.sh
55+
# - name: Unit Tests
56+
# run: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseSessions ${{ matrix.target }} spmbuildonly
57+
58+
59+
catalyst:
60+
# Don't run on private repo unless it is a PR.
61+
if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request'
62+
63+
runs-on: macos-12
64+
steps:
65+
- uses: actions/checkout@v2
66+
- uses: mikehardy/buildcache-action@50738c6c77de7f34e66b870e4f8ede333b69d077
67+
with:
68+
cache_key: ${{ matrix.os }}
69+
- uses: ruby/setup-ruby@v1
70+
- name: Setup Bundler
71+
run: scripts/setup_bundler.sh
72+
- name: Setup project and Build for Catalyst
73+
run: scripts/test_catalyst.sh FirebaseSessions test FirebaseSessions-Unit-unit

FirebaseSessions.podspec

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ Pod::Spec.new do |s|
3333
s.cocoapods_version = '>= 1.4.0'
3434
s.prefix_header_file = false
3535

36-
s.source_files = "FirebaseSessions/Sources/**/*.swift"
36+
base_dir = "FirebaseSessions/"
37+
s.source_files = [
38+
base_dir + 'Sources/**/*.swift',
39+
]
3740

3841
s.dependency 'FirebaseCore', '~> 10.0'
3942
s.dependency 'FirebaseCoreExtension', '~> 10.0'
@@ -43,4 +46,18 @@ Pod::Spec.new do |s|
4346
'GCC_C_LANGUAGE_STANDARD' => 'c99',
4447
'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"'
4548
}
49+
50+
s.test_spec 'unit' do |unit_tests|
51+
unit_tests.scheme = { :code_coverage => true }
52+
unit_tests.platforms = {
53+
:ios => ios_deployment_target,
54+
:osx => osx_deployment_target,
55+
:tvos => tvos_deployment_target,
56+
# https://github.com/CocoaPods/CocoaPods/issues/8283
57+
# :watchos => watchos_deployment_target,
58+
}
59+
unit_tests.source_files = base_dir + 'Tests/Unit/**/*.swift'
60+
unit_tests.resources = base_dir + 'Tests/Fixtures/**/*'
61+
unit_tests.requires_app_host = true
62+
end
4663
end

FirebaseSessions/Sources/FirebaseSessions.swift

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
// limitations under the License.
1414

1515
import Foundation
16+
1617
import FirebaseCore
18+
import FirebaseInstallations
1719

1820
// Avoids exposing internal FirebaseCore APIs to Swift users.
1921
@_implementationOnly import FirebaseCoreExtension
@@ -23,16 +25,44 @@ protocol SessionsProvider {
2325
@objc static func sessions() -> Void
2426
}
2527

26-
@objc(FIRSessions) class Sessions: NSObject, Library, SessionsProvider {
28+
@objc(FIRSessions) final class Sessions: NSObject, Library, SessionsProvider {
2729
// MARK: - Private Variables
2830

29-
/// The app associated with all sessions.
30-
private let googleAppID: String
31+
/// The Firebase App ID associated with Sessions.
32+
private let appID: String
33+
34+
/// Top-level Classes in the Sessions SDK
35+
private let coordinator: SessionCoordinator
36+
private let initiator: SessionInitiator
37+
private let identifiers: Identifiers
3138

3239
// MARK: - Initializers
3340

34-
required init(app: FirebaseApp) {
35-
googleAppID = app.options.googleAppID
41+
required convenience init(appID: String, installations: InstallationsProtocol) {
42+
let identifiers = Identifiers(installations: installations)
43+
let coordinator = SessionCoordinator(identifiers: identifiers)
44+
let initiator = SessionInitiator()
45+
46+
self.init(appID: appID,
47+
identifiers: identifiers,
48+
coordinator: coordinator,
49+
initiator: initiator)
50+
}
51+
52+
init(appID: String, identifiers: Identifiers, coordinator: SessionCoordinator,
53+
initiator: SessionInitiator) {
54+
self.appID = appID
55+
56+
self.identifiers = identifiers
57+
self.coordinator = coordinator
58+
self.initiator = initiator
59+
60+
super.init()
61+
62+
self.initiator.beginListening {
63+
self.identifiers.generateNewSessionID()
64+
self.coordinator.runMain()
65+
}
3666
}
3767

3868
// MARK: - Library conformance
@@ -44,7 +74,8 @@ protocol SessionsProvider {
4474
// Sessions SDK only works for the default app
4575
guard let app = container.app, app.isDefaultApp else { return nil }
4676
isCacheable.pointee = true
47-
return self.init(app: app)
77+
let installations = Installations.installations(app: app)
78+
return self.init(appID: app.options.googleAppID, installations: installations)
4879
}]
4980
}
5081

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
import FirebaseInstallations
18+
19+
let sessionIDUserDefaultsKey = "com.firebase.sessions.sessionID"
20+
let lastSessionIDUserDefaultsKey = "com.firebase.sessions.lastSessionID"
21+
22+
protocol IdentifierProvider {
23+
var installationID: String {
24+
get
25+
}
26+
27+
var sessionID: String {
28+
get
29+
}
30+
31+
var lastSessionID: String {
32+
get
33+
}
34+
}
35+
36+
///
37+
/// Identifiers is responsible for:
38+
/// 1) Getting the Installation ID from Installations
39+
/// 2) Generating the Session ID
40+
/// 3) Persisting and reading the Session ID from the last session
41+
/// (Maybe) 4) Persisting, reading, and incrementing an increasing index
42+
///
43+
class Identifiers: IdentifierProvider {
44+
private let installations: InstallationsProtocol
45+
46+
private var uuid: UUID
47+
48+
init(installations: InstallationsProtocol) {
49+
self.installations = installations
50+
uuid = UUID()
51+
}
52+
53+
func generateNewSessionID() {
54+
uuid = UUID()
55+
56+
let lastStoredSessionID = UserDefaults.standard.string(forKey: sessionIDUserDefaultsKey) ?? ""
57+
UserDefaults.standard.set(lastStoredSessionID, forKey: lastSessionIDUserDefaultsKey)
58+
59+
let newSessionID = uuid.uuidString.replacingOccurrences(of: "-", with: "").lowercased()
60+
UserDefaults.standard.set(newSessionID, forKey: sessionIDUserDefaultsKey)
61+
}
62+
63+
// This method must be run on a background thread due to how Firebase Installations
64+
// handles threading.
65+
var installationID: String {
66+
if Thread.isMainThread {
67+
Logger
68+
.logError(
69+
"Error: Identifiers.installationID getter must be called on a background thread. Using an empty ID"
70+
)
71+
return ""
72+
}
73+
74+
var localInstallationID = ""
75+
76+
let semaphore = DispatchSemaphore(value: 0)
77+
78+
installations.installationID { result in
79+
switch result {
80+
case let .success(fiid):
81+
localInstallationID = fiid
82+
case let .failure(error):
83+
Logger
84+
.logError(
85+
"Error getting Firebase Installation ID: \(error). Using an empty ID"
86+
)
87+
}
88+
89+
semaphore.signal()
90+
}
91+
92+
switch semaphore.wait(timeout: DispatchTime.now() + 1.0) {
93+
case .success:
94+
break
95+
case .timedOut:
96+
Logger.logError("Error: took too long to get the Firebase Installation ID. Using an empty ID")
97+
}
98+
99+
return localInstallationID
100+
}
101+
102+
var sessionID: String {
103+
return UserDefaults.standard.string(forKey: sessionIDUserDefaultsKey) ?? ""
104+
}
105+
106+
var lastSessionID: String {
107+
return UserDefaults.standard.string(forKey: lastSessionIDUserDefaultsKey) ?? ""
108+
}
109+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
import FirebaseInstallations
18+
19+
protocol InstallationsProtocol {
20+
func installationID(completion: @escaping (Result<String, Error>) -> Void)
21+
}
22+
23+
extension Installations: InstallationsProtocol {
24+
func installationID(completion: @escaping (Result<String, Error>) -> Void) {
25+
installationID { (installationID: String?, error: Error?) in
26+
if let installationID = installationID {
27+
completion(.success(installationID))
28+
} else if let error = error {
29+
completion(.failure(error))
30+
}
31+
}
32+
}
33+
}

FirebaseSessions/Sources/Logger.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
@_implementationOnly import FirebaseCoreExtension
19+
20+
enum Logger {
21+
private static let logServiceTag = "[FirebaseSessions]"
22+
private static let logCode = "I-SES000000"
23+
24+
static func logInfo(_ message: String) {
25+
FirebaseLogger.log(
26+
level: .info,
27+
service: logServiceTag,
28+
code: logCode,
29+
message: message
30+
)
31+
}
32+
33+
static func logDebug(_ message: String) {
34+
FirebaseLogger.log(
35+
level: .debug,
36+
service: logServiceTag,
37+
code: logCode,
38+
message: message
39+
)
40+
}
41+
42+
static func logWarning(_ message: String) {
43+
FirebaseLogger.log(
44+
level: .warning,
45+
service: logServiceTag,
46+
code: logCode,
47+
message: message
48+
)
49+
}
50+
51+
static func logError(_ message: String) {
52+
FirebaseLogger.log(
53+
level: .error,
54+
service: logServiceTag,
55+
code: logCode,
56+
message: message
57+
)
58+
}
59+
}

0 commit comments

Comments
 (0)