Skip to content
Merged
9 changes: 9 additions & 0 deletions .github/workflows/sessions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,17 @@ jobs:
- os: macos-14
xcode: Xcode_16.2
tests:
swift_version: 5.9
# Flaky tests on CI
- os: macos-15
xcode: Xcode_16.3
tests: --skip-tests
swift_version: 5.9
# Flaky tests on CI
- os: macos-15
xcode: Xcode_16.2
tests: --skip-tests
swift_version: 6.0
runs-on: ${{ matrix.build-env.os }}
steps:
- uses: actions/checkout@v4
Expand All @@ -51,6 +58,8 @@ jobs:
run: scripts/setup_bundler.sh
- name: Xcode
run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer
- name: Set Swift swift_version
run: sed -i "" "s/s.swift_version[[:space:]]*=[[:space:]]*'5.9'/s.swift_version = '${{ matrix.build-env.swift_version }}'/" FirebaseSessions.podspec
- uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3
with:
timeout_minutes: 120
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ typedef void (^FIRInstallationsTokenHandler)(
* as the ability to delete it. A Firebase Installation is unique by `FirebaseApp.name` and
* `FirebaseApp.options.googleAppID` .
*/
NS_SWIFT_NAME(Installations)
@interface FIRInstallations : NSObject
NS_SWIFT_NAME(Installations) NS_SWIFT_SENDABLE @interface FIRInstallations : NSObject

- (instancetype)init NS_UNAVAILABLE;

Expand Down
9 changes: 6 additions & 3 deletions FirebaseSessions/Sources/ApplicationInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ enum DevEnvironment: String {
case autopush // Autopush environment
}

protocol ApplicationInfoProtocol {
protocol ApplicationInfoProtocol: Sendable {
/// Google App ID / GMP App ID
var appID: String { get }

Expand Down Expand Up @@ -62,12 +62,15 @@ protocol ApplicationInfoProtocol {
var osDisplayVersion: String { get }
}

class ApplicationInfo: ApplicationInfoProtocol {
final class ApplicationInfo: ApplicationInfoProtocol {
let appID: String

private let networkInformation: NetworkInfoProtocol
private let envParams: [String: String]
private let infoDict: [String: Any]?

// Used to hold bundle info, so the `Any` params should also
// be Sendable.
private nonisolated(unsafe) let infoDict: [String: Any]?

init(appID: String, networkInfo: NetworkInfoProtocol = NetworkInfo(),
envParams: [String: String] = ProcessInfo.processInfo.environment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Foundation
import FirebaseSessionsObjC
#endif // SWIFT_PACKAGE

class DevEventConsoleLogger: EventGDTLoggerProtocol {
final class DevEventConsoleLogger: EventGDTLoggerProtocol {
private let commandLineArgument = "-FIRSessionsDebugEvents"

func logEvent(event: SessionStartEvent, completion: @escaping (Result<Void, Error>) -> Void) {
Expand Down
4 changes: 2 additions & 2 deletions FirebaseSessions/Sources/EventGDTLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Foundation

internal import GoogleDataTransport

protocol EventGDTLoggerProtocol {
protocol EventGDTLoggerProtocol: Sendable {
func logEvent(event: SessionStartEvent, completion: @escaping (Result<Void, Error>) -> Void)
}

Expand All @@ -26,7 +26,7 @@ protocol EventGDTLoggerProtocol {
/// 1) Creating GDT Events and logging them to the GoogleDataTransport SDK
/// 2) Handling debugging situations (eg. running in Simulator or printing the event to console)
///
class EventGDTLogger: EventGDTLoggerProtocol {
final class EventGDTLogger: EventGDTLoggerProtocol {
let googleDataTransport: GoogleDataTransportProtocol
let devEventConsoleLogger: EventGDTLoggerProtocol

Expand Down
34 changes: 28 additions & 6 deletions FirebaseSessions/Sources/FirebaseSessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ private enum GoogleDataTransportConfig {

// Initializes the SDK and top-level classes
required convenience init(appID: String, installations: InstallationsProtocol) {
let googleDataTransport = GDTCORTransport(
let googleDataTransport = GoogleDataTransporter(
mappingID: GoogleDataTransportConfig.sessionsLogSource,
transformers: nil,
target: GoogleDataTransportConfig.sessionsTarget
)

let fireLogger = EventGDTLogger(googleDataTransport: googleDataTransport!)
let fireLogger = EventGDTLogger(googleDataTransport: googleDataTransport)

let appInfo = ApplicationInfo(appID: appID)
let settings = SessionsSettings(
Expand Down Expand Up @@ -135,10 +135,10 @@ private enum GoogleDataTransportConfig {
}

// Initializes the SDK and begins the process of listening for lifecycle events and logging
// events
// events. `logEventCallback` is invoked on a global background queue.
init(appID: String, sessionGenerator: SessionGenerator, coordinator: SessionCoordinatorProtocol,
initiator: SessionInitiator, appInfo: ApplicationInfoProtocol, settings: SettingsProtocol,
loggedEventCallback: @escaping (Result<Void, FirebaseSessionsError>) -> Void) {
loggedEventCallback: @escaping @Sendable (Result<Void, FirebaseSessionsError>) -> Void) {
self.appID = appID

self.sessionGenerator = sessionGenerator
Expand Down Expand Up @@ -247,18 +247,40 @@ private enum GoogleDataTransportConfig {
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: nil
queue: OperationQueue.main
) { notification in
subscriber.onSessionChanged(self.currentSessionDetails)
callback.invoke(notification: notification)
}
// Immediately call the callback because the Sessions SDK starts
// before subscribers, so subscribers will miss the first Notification
Expand Down
2 changes: 1 addition & 1 deletion FirebaseSessions/Sources/FirebaseSessionsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import Foundation

/// Contains the list of errors that are localized for Firebase Sessions Library
enum FirebaseSessionsError: Error {
enum FirebaseSessionsError: Error, Sendable {
/// Event sampling related error
case SessionSamplingError
/// Firebase Installation ID related error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,31 @@

import Foundation

internal import GoogleDataTransport
@preconcurrency internal import GoogleDataTransport

enum GoogleDataTransportProtocolErrors: Error {
case writeFailure
}

protocol GoogleDataTransportProtocol {
protocol GoogleDataTransportProtocol: Sendable {
func logGDTEvent(event: GDTCOREvent, completion: @escaping (Result<Void, Error>) -> Void)
func eventForTransport() -> GDTCOREvent
}

extension GDTCORTransport: GoogleDataTransportProtocol {
func logGDTEvent(event: GDTCOREvent, completion: @escaping (Result<Void, Error>) -> Void) {
sendDataEvent(event) { wasWritten, error in
/// Workaround in combo with preconcurrency import of GDT. When GDT's
/// `GDTCORTransport`type conforms to Sendable within the GDT module,
/// this can be removed.
final class GoogleDataTransporter: GoogleDataTransportProtocol {
private let transporter: GDTCORTransport

init(mappingID: String,
transformers: [any GDTCOREventTransformer]?,
target: GDTCORTarget) {
transporter = GDTCORTransport(mappingID: mappingID, transformers: transformers, target: target)!
}

func logGDTEvent(event: GDTCOREvent, completion: @escaping (Result<Void, any Error>) -> Void) {
transporter.sendDataEvent(event) { wasWritten, error in
if let error {
completion(.failure(error))
} else if !wasWritten {
Expand All @@ -38,4 +49,8 @@ extension GDTCORTransport: GoogleDataTransportProtocol {
}
}
}

func eventForTransport() -> GDTCOREvent {
transporter.eventForTransport()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Foundation

internal import FirebaseInstallations

protocol InstallationsProtocol {
protocol InstallationsProtocol: Sendable {
var installationsWaitTimeInSecond: Int { get }

/// Override Installation function for testing
Expand Down
4 changes: 2 additions & 2 deletions FirebaseSessions/Sources/NetworkInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ import Foundation
internal import GoogleUtilities
#endif // SWIFT_PACKAGE

protocol NetworkInfoProtocol {
protocol NetworkInfoProtocol: Sendable {
var networkType: GULNetworkType { get }

var mobileSubtype: String { get }
}

class NetworkInfo: NetworkInfoProtocol {
final class NetworkInfo: NetworkInfoProtocol {
var networkType: GULNetworkType {
return GULNetworkInfo.getNetworkType()
}
Expand Down
4 changes: 2 additions & 2 deletions FirebaseSessions/Sources/Public/SessionsSubscriber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import Foundation
/// Sessions Subscriber is an interface that dependent SDKs
/// must implement.
@objc(FIRSessionsSubscriber)
public protocol SessionsSubscriber {
public protocol SessionsSubscriber: Sendable {
func onSessionChanged(_ session: SessionDetails)
var isDataCollectionEnabled: Bool { get }
var sessionsSubscriberName: SessionsSubscriberName { get }
Expand All @@ -38,7 +38,7 @@ public class SessionDetails: NSObject {

/// Session Subscriber Names are used for identifying subscribers
@objc(FIRSessionsSubscriberName)
public enum SessionsSubscriberName: Int, CustomStringConvertible {
public enum SessionsSubscriberName: Int, CustomStringConvertible, Sendable {
case Unknown
case Crashlytics
case Performance
Expand Down
5 changes: 3 additions & 2 deletions FirebaseSessions/Sources/SessionCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import Foundation

protocol SessionCoordinatorProtocol {
protocol SessionCoordinatorProtocol: Sendable {
func attemptLoggingSessionStart(event: SessionStartEvent,
callback: @escaping (Result<Void, FirebaseSessionsError>) -> Void)
}
Expand All @@ -23,8 +23,9 @@ protocol SessionCoordinatorProtocol {
/// SessionCoordinator is responsible for coordinating the systems in this SDK
/// involved with sending a Session Start event.
///
class SessionCoordinator: SessionCoordinatorProtocol {
final class SessionCoordinator: SessionCoordinatorProtocol {
let installations: InstallationsProtocol

let fireLogger: EventGDTLoggerProtocol

init(installations: InstallationsProtocol,
Expand Down
6 changes: 3 additions & 3 deletions FirebaseSessions/Sources/SessionInitiator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ import Foundation
///
class SessionInitiator {
let currentTime: () -> Date
var settings: SettingsProtocol
var backgroundTime = Date.distantFuture
var initiateSessionStart: () -> Void = {}
let settings: SettingsProtocol
private var backgroundTime = Date.distantFuture
private var initiateSessionStart: () -> Void = {}

init(settings: SettingsProtocol, currentTimeProvider: @escaping () -> Date = Date.init) {
currentTime = currentTimeProvider
Expand Down
Loading
Loading