Skip to content
Merged
57 changes: 57 additions & 0 deletions Sources/Common/Broadcaster.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Foundation

public actor Broadcaster<Value: Sendable> {
private var continuations = [Int: AsyncStream<Value>.Continuation]()
private var streamIdentifier = 0
private var finished = false

public init() { }

public func stream(
bufferingPolicy: AsyncStream<Value>.Continuation.BufferingPolicy = .unbounded
) -> AsyncStream<Value> {
let id = streamIdentifier
streamIdentifier &+= 1
var continuationRef: AsyncStream<Value>.Continuation?

let stream = AsyncStream<Value>(bufferingPolicy: bufferingPolicy) { continuation in
continuationRef = continuation
continuation.onTermination = { [weak self] _ in
guard let self else { return }
Task { await self.removeContinuation(id: id) }
}
}

if let continuation = continuationRef {
if finished {
continuation.finish()
} else {
continuations[id] = continuation
}
}

return stream
}

public func send(_ event: Value) {
guard !finished else { return }
for continuation in continuations.values {
_ = continuation.yield(event)
}
}

public func finish() {
guard !finished else { return }
finished = true
for continuation in continuations.values {
continuation.finish()
}
continuations.removeAll()
}

private func removeContinuation(id: Int) {
continuations.removeValue(forKey: id)
}
}


2 changes: 2 additions & 0 deletions Sources/Common/GraphQL/GraphQLClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public final class GraphQLClient {
return value
} catch let error as GraphQLClientError {
throw error
} catch let error as NetworkError {
throw error
} catch {
throw GraphQLClientError.decoding(error)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Common/Task+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ extension Task where Success == Never, Failure == Never {
/// try await Task.sleep(seconds: 3.0)
///
public static func sleep(seconds: TimeInterval) async throws {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000.0))
try await Task.sleep(nanoseconds: UInt64(seconds * Double(NSEC_PER_SEC)))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ public struct ObservabilityClientFactory {
withOptions options: Options,
mobileKey: String
) throws -> Observe {
let appLifecycleManager = AppLifecycleManager()
let sessionManager = SessionManager(
options: .init(
timeout: options.sessionBackgroundTimeout,
isDebug: options.isDebug,
log: options.log)
log: options.log),
appLifecycleManager: appLifecycleManager
)

/// Discuss adding autoInstrumentationSamplingInterval to options worth it
/// Maybe could be by instrument or single global sampling interval
let autoInstrumentationSamplingInterval: TimeInterval = 5.0
Expand Down Expand Up @@ -145,6 +148,7 @@ public struct ObservabilityClientFactory {
let context = ObservabilityContext(
sdkKey: mobileKey,
options: options,
appLifecycleManager: appLifecycleManager,
sessionManager: sessionManager,
transportService: transportService
)
Expand Down
21 changes: 12 additions & 9 deletions Sources/Observability/Client/ObservabilityContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ import OpenTelemetrySdk
import Common

/** Shared info between plugins */
public struct ObservabilityContext {
public class ObservabilityContext {
public let sdkKey: String
public let options: Options
public var sessionManager: SessionManaging
public var transportService: TransportServicing

public let sessionManager: SessionManaging
public let transportService: TransportServicing
public let appLifecycleManager: AppLifecycleManaging

public init(
sdkKey: String,
options: Options,
appLifecycleManager: AppLifecycleManaging,
sessionManager: SessionManaging,
transportService: TransportServicing) {
self.sdkKey = sdkKey
self.options = options
self.sessionManager = sessionManager
self.transportService = transportService
}
self.sdkKey = sdkKey
self.options = options
self.appLifecycleManager = appLifecycleManager
self.sessionManager = sessionManager
self.transportService = transportService
}
}
86 changes: 86 additions & 0 deletions Sources/Observability/Session/AppLifecycleManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import UIKit
import Common

public enum AppLifeCycleEvent {
case didFinishLaunching
case willEnterForeground
case didBecomeActive
case willResignActive
case didEnterBackground
case willTerminate
}

public protocol AppLifecycleManaging: AnyObject {
func events() async -> AsyncStream<AppLifeCycleEvent>
}

final class AppLifecycleManager: AppLifecycleManaging {
private let broadcaster = Broadcaster<AppLifeCycleEvent>()
private var observers = [NSObjectProtocol]()

init() {
observeLifecycleNotifications()
}

private func observeLifecycleNotifications() {
observers.append(NotificationCenter.default.addObserver(forName: UIApplication.didFinishLaunchingNotification, object: nil, queue: .main) { [weak self] _ in
self?.didFinishLaunching()
})
observers.append(NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
self?.handleDidBecomeActive()
})
observers.append(NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { [weak self] _ in
self?.handleWillResignActive()
})
observers.append(NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { [weak self] _ in
self?.handleDidEnterBackground()
})
observers.append(NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
self?.handleWillEnterForeground()
})
observers.append(NotificationCenter.default.addObserver(forName: UIApplication.willTerminateNotification, object: nil, queue: .main) { [weak self] _ in
self?.handleWillTerminate()
})
}

deinit {
observers.forEach {
NotificationCenter.default.removeObserver($0)
}
Task {
await broadcaster.finish()
}
}

func events() async -> AsyncStream<AppLifeCycleEvent> {
await broadcaster.stream()
}

private func send(_ event: AppLifeCycleEvent) {
Task { await broadcaster.send(event) }
}

private func didFinishLaunching() {
send(.didFinishLaunching)
}

private func handleDidBecomeActive() {
send(.didBecomeActive)
}

private func handleWillResignActive() {
send(.willResignActive)
}

private func handleDidEnterBackground() {
send(.didEnterBackground)
}

private func handleWillEnterForeground() {
send(.willEnterForeground)
}

private func handleWillTerminate() {
send(.willTerminate)
}
}
12 changes: 12 additions & 0 deletions Sources/Observability/Session/SessionInfo.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import Common

public struct SessionInfo: Sendable, Equatable {
public let id: String
Expand All @@ -8,4 +9,15 @@ public struct SessionInfo: Sendable, Equatable {
self.id = id
self.startTime = startTime
}

public init() {
self.init(id: SecureIDGenerator.generateSecureID(), startTime: Date())
}

var sessionAttributes: [String: AttributeValue] {
[
"session.id": .string(id),
"session.start_time": .string(String(format: "%.0f", startTime.timeIntervalSince1970))
]
}
}
Loading