From a8628b57f0ee486c3755fc38eb209177bff22868 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Fri, 10 Oct 2025 15:21:04 +0300 Subject: [PATCH 1/7] feat: add option to disable swizzling --- .../App Life Cycle/PostHogAppLifeCycleIntegration.swift | 2 ++ PostHog/Autocapture/PostHogAutocaptureIntegration.swift | 2 ++ PostHog/PostHogConfig.swift | 9 +++++++++ PostHog/PostHogIntegration.swift | 8 ++++++++ PostHog/PostHogSDK.swift | 6 ++++++ PostHog/Replay/PostHogReplayIntegration.swift | 2 ++ PostHog/Screen Views/PostHogScreenViewIntegration.swift | 2 ++ PostHog/Surveys/PostHogSurveyIntegration.swift | 2 ++ 8 files changed, 33 insertions(+) diff --git a/PostHog/App Life Cycle/PostHogAppLifeCycleIntegration.swift b/PostHog/App Life Cycle/PostHogAppLifeCycleIntegration.swift index 23ba24c76b..b423326c49 100644 --- a/PostHog/App Life Cycle/PostHogAppLifeCycleIntegration.swift +++ b/PostHog/App Life Cycle/PostHogAppLifeCycleIntegration.swift @@ -17,6 +17,8 @@ import Foundation - captures an `App Backgrounded` event when the app moves to the background */ final class PostHogAppLifeCycleIntegration: PostHogIntegration { + var requiresSwizzling: Bool { false } + private static var integrationInstalledLock = NSLock() private static var integrationInstalled = false private static var didCaptureAppInstallOrUpdate = false diff --git a/PostHog/Autocapture/PostHogAutocaptureIntegration.swift b/PostHog/Autocapture/PostHogAutocaptureIntegration.swift index 8e345bff34..fb3363e634 100644 --- a/PostHog/Autocapture/PostHogAutocaptureIntegration.swift +++ b/PostHog/Autocapture/PostHogAutocaptureIntegration.swift @@ -11,6 +11,8 @@ private let elementsChainDelimiter = ";" class PostHogAutocaptureIntegration: AutocaptureEventProcessing, PostHogIntegration { + var requiresSwizzling: Bool { true } + private static var integrationInstalledLock = NSLock() private static var integrationInstalled = false diff --git a/PostHog/PostHogConfig.swift b/PostHog/PostHogConfig.swift index 7a889e0b95..deca461c2e 100644 --- a/PostHog/PostHogConfig.swift +++ b/PostHog/PostHogConfig.swift @@ -54,6 +54,15 @@ public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? @objc public var captureApplicationLifecycleEvents: Bool = true @objc public var captureScreenViews: Bool = true + + /// Enable method swizzling for SDK functionality that depends on it + /// + /// When disabled, functionality that require swizzling (autocapture, screen views, session replay, surveys) will not be installed. + /// Note: The SDK may still use minimal internal swizzling for core functionality of managing sessions, but this will be kept to a minimum. + /// + /// Default: true + @objc public var enableSwizzling: Bool = true + #if os(iOS) || targetEnvironment(macCatalyst) /// Enable autocapture for iOS /// Default: false diff --git a/PostHog/PostHogIntegration.swift b/PostHog/PostHogIntegration.swift index 27a8df8876..85c4ae42d9 100644 --- a/PostHog/PostHogIntegration.swift +++ b/PostHog/PostHogIntegration.swift @@ -7,6 +7,14 @@ import Foundation protocol PostHogIntegration { + /** + * Indicates whether this integration requires method swizzling to function. + * + * When `enableSwizzling` is set to `false` in PostHogConfig, integrations + * that return `true` for this property will be skipped during installation. + */ + var requiresSwizzling: Bool { get } + /** * Installs and initializes the integration with a PostHogSDK instance. * diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index 1e1661ace2..3573a79391 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -1396,6 +1396,12 @@ let maxRetryDelay = 30.0 var installed: [PostHogIntegration] = [] for integration in integrations { + // Skip integrations that require swizzling when swizzling is disabled + if integration.requiresSwizzling, !config.enableSwizzling { + hedgeLog("Integration \(type(of: integration)) skipped. Integration requires swizzling but enableSwizzling is disabled in config") + continue + } + do { try integration.install(self) installed.append(integration) diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index 882aa502b2..abf1a929fb 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -14,6 +14,8 @@ import WebKit class PostHogReplayIntegration: PostHogIntegration { + var requiresSwizzling: Bool { true } + private static var integrationInstalledLock = NSLock() private static var integrationInstalled = false diff --git a/PostHog/Screen Views/PostHogScreenViewIntegration.swift b/PostHog/Screen Views/PostHogScreenViewIntegration.swift index df9028a056..9f5f6a370b 100644 --- a/PostHog/Screen Views/PostHogScreenViewIntegration.swift +++ b/PostHog/Screen Views/PostHogScreenViewIntegration.swift @@ -8,6 +8,8 @@ import Foundation final class PostHogScreenViewIntegration: PostHogIntegration { + var requiresSwizzling: Bool { true } + private static var integrationInstalledLock = NSLock() private static var integrationInstalled = false diff --git a/PostHog/Surveys/PostHogSurveyIntegration.swift b/PostHog/Surveys/PostHogSurveyIntegration.swift index 381682e1a4..40b80fd02a 100644 --- a/PostHog/Surveys/PostHogSurveyIntegration.swift +++ b/PostHog/Surveys/PostHogSurveyIntegration.swift @@ -13,6 +13,8 @@ #endif final class PostHogSurveyIntegration: PostHogIntegration { + var requiresSwizzling: Bool { true } + private static var integrationInstalledLock = NSLock() private static var integrationInstalled = false From 05404655dd838fd91fab3436bb5128f3c90ef3d7 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Mon, 13 Oct 2025 23:12:16 +0300 Subject: [PATCH 2/7] feat: migrate PostHogSessionManager to SDK instance member --- CHANGELOG.md | 6 +- PostHog/DI.swift | 2 - PostHog/PostHogConfig.swift | 3 +- PostHog/PostHogSDK.swift | 30 +++-- PostHog/PostHogSessionManager.swift | 15 ++- ...ostHogSessionReplayConsoleLogsPlugin.swift | 2 +- .../PostHogSessionReplayNetworkPlugin.swift | 5 +- .../Plugins/Network/URLSessionExtension.swift | 4 +- .../Network/URLSessionInterceptor.swift | 6 +- .../Plugins/Network/URLSessionSwizzler.swift | 5 +- PostHog/Replay/PostHogReplayIntegration.swift | 8 +- .../PostHogAutocaptureIntegrationSpec.swift | 2 +- PostHogTests/PostHogSDKTest.swift | 2 - PostHogTests/PostHogSessionManagerTest.swift | 124 +++++++++--------- 14 files changed, 117 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f896e2b36..dcec9b1e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ ## Next +- feat: add config option to disable swizzling ([#388](https://github.com/PostHog/posthog-ios/pull/388)) +- feat: SDK instance now manages its own session ([#388](https://github.com/PostHog/posthog-ios/pull/388)) +> Note: In case of multiple SDK instances, each instance will now report and manage its own $session_id + ## 3.33.0 - 2025-10-13 -- add evaluation tags to iOS SDK ([#387](https://github.com/PostHog/posthog-ios/pull/387)) +- feat: add evaluation tags to iOS SDK ([#387](https://github.com/PostHog/posthog-ios/pull/387)) ## 3.32.0 - 2025-10-03 diff --git a/PostHog/DI.swift b/PostHog/DI.swift index 5bf417134e..03c4a056cf 100644 --- a/PostHog/DI.swift +++ b/PostHog/DI.swift @@ -10,8 +10,6 @@ enum DI { static var main = Container() final class Container { - // manages session rotation - lazy var sessionManager: PostHogSessionManager = .init() // publishes global app lifecycle events lazy var appLifecyclePublisher: AppLifecyclePublishing = ApplicationLifecyclePublisher.shared // publishes global screen view events (UIViewController.viewDidAppear) diff --git a/PostHog/PostHogConfig.swift b/PostHog/PostHogConfig.swift index a9a38d8ebf..9696d35c23 100644 --- a/PostHog/PostHogConfig.swift +++ b/PostHog/PostHogConfig.swift @@ -57,8 +57,7 @@ public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? /// Enable method swizzling for SDK functionality that depends on it /// - /// When disabled, functionality that require swizzling (autocapture, screen views, session replay, surveys) will not be installed. - /// Note: The SDK may still use minimal internal swizzling for core functionality of managing sessions, but this will be kept to a minimum. + /// When disabled, functionality that require swizzling (like autocapture, screen views, session replay, surveys) will not be installed. /// /// Default: true @objc public var enableSwizzling: Bool = true diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index 3573a79391..734656b0fe 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -46,6 +46,7 @@ let maxRetryDelay = 30.0 private var context: PostHogContext? private static var apiKeys = Set() private var installedIntegrations: [PostHogIntegration] = [] + var sessionManager: PostHogSessionManager? #if os(iOS) private weak var replayIntegration: PostHogReplayIntegration? @@ -136,7 +137,9 @@ let maxRetryDelay = 30.0 replayQueue?.start(disableReachabilityForTesting: config.disableReachabilityForTesting, disableQueueTimerForTesting: config.disableQueueTimerForTesting) - PostHogSessionManager.shared.startSession() + // Create session manager instance for this PostHogSDK instance + sessionManager = PostHogSessionManager(config: config) + sessionManager?.startSession() if !config.optOut { // don't install integrations if in opt-out state @@ -170,7 +173,7 @@ let maxRetryDelay = 30.0 return nil } - return PostHogSessionManager.shared.getSessionId(readOnly: true) + return sessionManager?.getSessionId(readOnly: true) } @objc public func startSession() { @@ -178,7 +181,7 @@ let maxRetryDelay = 30.0 return } - PostHogSessionManager.shared.startSession() + sessionManager?.startSession() } @objc public func endSession() { @@ -186,7 +189,7 @@ let maxRetryDelay = 30.0 return } - PostHogSessionManager.shared.endSession() + sessionManager?.endSession() } // EVENT CAPTURE @@ -303,7 +306,7 @@ let maxRetryDelay = 30.0 // if not present, get a current or new session id at event timestamp let propSessionId = properties?["$session_id"] as? String let sessionId: String? = propSessionId.isNilOrEmpty - ? PostHogSessionManager.shared.getSessionId(at: timestamp ?? now()) + ? sessionManager?.getSessionId(at: timestamp ?? now()) : propSessionId if let sessionId { @@ -351,7 +354,8 @@ let maxRetryDelay = 30.0 flagCallReportedLock.withLock { flagCallReported.removeAll() } - PostHogSessionManager.shared.resetSession() + sessionManager?.resetSession() + sessionManager = nil // Clear person and group properties for flags remoteConfig?.resetPersonPropertiesForFlags() @@ -1270,7 +1274,7 @@ let maxRetryDelay = 30.0 flagCallReported.removeAll() } context = nil - PostHogSessionManager.shared.endSession() + sessionManager?.endSession() toggleHedgeLog(false) uninstallIntegrations() @@ -1328,8 +1332,8 @@ let maxRetryDelay = 30.0 } let sessionId = resumeCurrent - ? PostHogSessionManager.shared.getSessionId() - : PostHogSessionManager.shared.getNextSessionId() + ? sessionManager?.getSessionId() + : sessionManager?.getNextSessionId() guard let sessionId else { return hedgeLog("Could not start recording. Missing session id.") @@ -1370,12 +1374,12 @@ let maxRetryDelay = 30.0 return false } - guard let replayIntegration, let remoteConfig else { + guard let replayIntegration, let remoteConfig, let sessionManager else { return false } return replayIntegration.isActive() - && !PostHogSessionManager.shared.getSessionId(readOnly: true).isNilOrEmpty + && !sessionManager.getSessionId(readOnly: true).isNilOrEmpty && remoteConfig.isSessionReplayFlagActive() } #endif @@ -1475,6 +1479,10 @@ let maxRetryDelay = 30.0 } #endif + func getSessionManager() -> PostHogSessionManager? { + sessionManager + } + func getAppLifeCycleIntegration() -> PostHogAppLifeCycleIntegration? { installedIntegrations.compactMap { $0 as? PostHogAppLifeCycleIntegration diff --git a/PostHog/PostHogSessionManager.swift b/PostHog/PostHogSessionManager.swift index 59bfce5982..e1bc18c18b 100644 --- a/PostHog/PostHogSessionManager.swift +++ b/PostHog/PostHogSessionManager.swift @@ -20,12 +20,10 @@ import Foundation case customSessionId = "Custom session set" } - @objc public static var shared: PostHogSessionManager { - DI.main.sessionManager - } + private let config: PostHogConfig - // Private initializer to prevent multiple instances - override init() { + init(config: PostHogConfig) { + self.config = config super.init() registerNotifications() registerApplicationSendEvent() @@ -237,8 +235,11 @@ import Foundation private func registerApplicationSendEvent() { #if os(iOS) || os(tvOS) - let applicationEventPublisher = DI.main.applicationEventPublisher - applicationEventToken = applicationEventPublisher.onApplicationEvent { [weak self] _, _ in + guard config.enableSwizzling else { + return + } + + applicationEventToken = DI.main.applicationEventPublisher.onApplicationEvent { [weak self] _, _ in // update "last active" session // we want to keep track of the idle time, so we need to maintain a timestamp on the last interactions of the user with the app. UIEvents are a good place to do so since it means that the user is actively interacting with the app (e.g not just noise background activity) self?.queue.async { diff --git a/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift b/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift index e3e67dd6db..a89be4eb78 100644 --- a/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift +++ b/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift @@ -49,7 +49,7 @@ isActive, let postHog, postHog.isSessionReplayActive(), - let sessionId = PostHogSessionManager.shared.getSessionId(at: output.timestamp) + let sessionId = postHog.sessionManager?.getSessionId(at: output.timestamp) else { return } diff --git a/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift b/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift index bf252cc5f0..fc37d0d0fd 100644 --- a/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift +++ b/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift @@ -19,7 +19,10 @@ do { sessionSwizzler = try URLSessionSwizzler( shouldCapture: shouldCaptureNetworkSample, - onCapture: handleNetworkSample + onCapture: handleNetworkSample, + getSessionId: { [weak self] date in + self?.postHog?.sessionManager?.getSessionId(at: date) + } ) sessionSwizzler?.swizzle() hedgeLog("[Session Replay] Network telemetry plugin started") diff --git a/PostHog/Replay/Plugins/Network/URLSessionExtension.swift b/PostHog/Replay/Plugins/Network/URLSessionExtension.swift index cf907dce0c..431cfb08c1 100644 --- a/PostHog/Replay/Plugins/Network/URLSessionExtension.swift +++ b/PostHog/Replay/Plugins/Network/URLSessionExtension.swift @@ -26,7 +26,7 @@ let timestamp = Date() let startMillis = getMonotonicTimeInMilliseconds() var endMillis: UInt64? - let sessionId = PostHogSessionManager.shared.getSessionId(at: timestamp) + let sessionId = postHog?.sessionManager?.getSessionId(at: timestamp) do { let (data, response) = try await action() endMillis = getMonotonicTimeInMilliseconds() @@ -57,7 +57,7 @@ let timestamp = Date() let startMillis = getMonotonicTimeInMilliseconds() var endMillis: UInt64? - let sessionId = PostHogSessionManager.shared.getSessionId(at: timestamp) + let sessionId = postHog?.sessionManager?.getSessionId(at: timestamp) do { let (url, response) = try await action() endMillis = getMonotonicTimeInMilliseconds() diff --git a/PostHog/Replay/Plugins/Network/URLSessionInterceptor.swift b/PostHog/Replay/Plugins/Network/URLSessionInterceptor.swift index be87e39622..b073e80ed6 100644 --- a/PostHog/Replay/Plugins/Network/URLSessionInterceptor.swift +++ b/PostHog/Replay/Plugins/Network/URLSessionInterceptor.swift @@ -12,10 +12,12 @@ private let tasksLock = NSLock() private let shouldCapture: () -> Bool private let onCapture: (NetworkSample) -> Void + private let getSessionId: (Date) -> String? - init(shouldCapture: @escaping () -> Bool, onCapture: @escaping (NetworkSample) -> Void) { + init(shouldCapture: @escaping () -> Bool, onCapture: @escaping (NetworkSample) -> Void, getSessionId: @escaping (Date) -> String?) { self.shouldCapture = shouldCapture self.onCapture = onCapture + self.getSessionId = getSessionId } /// An internal queue for synchronising the access to `samplesByTask`. @@ -42,7 +44,7 @@ let date = now() - guard let sessionId = PostHogSessionManager.shared.getSessionId(at: date) else { + guard let sessionId = getSessionId(date) else { return } diff --git a/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift b/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift index edf8e6fa27..529f8e022e 100644 --- a/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift +++ b/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift @@ -25,10 +25,11 @@ private var hasSwizzled = false - init(shouldCapture: @escaping () -> Bool, onCapture: @escaping (NetworkSample) -> Void) throws { + init(shouldCapture: @escaping () -> Bool, onCapture: @escaping (NetworkSample) -> Void, getSessionId: @escaping (Date) -> String?) throws { interceptor = URLSessionInterceptor( shouldCapture: shouldCapture, - onCapture: onCapture + onCapture: onCapture, + getSessionId: getSessionId ) dataTaskWithURLAndCompletion = try DataTaskWithURLAndCompletion.build(interceptor: interceptor) diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index abf1a929fb..e623f829d3 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -142,7 +142,7 @@ isEnabled = true // reset views when session id changes (or is cleared) so we can re-send new metadata (or full snapshot in the future) - PostHogSessionManager.shared.onSessionIdChanged = { [weak self] in + postHog.sessionManager?.onSessionIdChanged = { [weak self] in self?.resetViews() } @@ -185,7 +185,7 @@ guard isEnabled else { return } isEnabled = false resetViews() - PostHogSessionManager.shared.onSessionIdChanged = {} + postHog?.sessionManager?.onSessionIdChanged = {} // stop listening to `UIApplication.sendEvent` applicationEventToken = nil @@ -251,7 +251,7 @@ PostHogReplayIntegration.dispatchQueue.async { [touchInfo, weak postHog = postHog] in // always make sure we have a fresh session id as early as possible - guard let sessionId = PostHogSessionManager.shared.getSessionId(at: date) else { + guard let sessionId = postHog?.sessionManager?.getSessionId(at: date) else { return } @@ -343,7 +343,7 @@ PostHogReplayIntegration.dispatchQueue.async { // always make sure we have a fresh session id at correct timestamp - guard let sessionId = PostHogSessionManager.shared.getSessionId(at: timestampDate) else { + guard let sessionId = postHog.sessionManager?.getSessionId(at: timestampDate) else { return } diff --git a/PostHogTests/PostHogAutocaptureIntegrationSpec.swift b/PostHogTests/PostHogAutocaptureIntegrationSpec.swift index f0fd03a3f3..591b783e3c 100644 --- a/PostHogTests/PostHogAutocaptureIntegrationSpec.swift +++ b/PostHogTests/PostHogAutocaptureIntegrationSpec.swift @@ -36,7 +36,7 @@ import Quick server.stop() server = nil integration.stop() - PostHogSessionManager.shared.endSession {} + posthog.endSession() posthog.close() deleteSafely(applicationSupportDirectoryURL()) } diff --git a/PostHogTests/PostHogSDKTest.swift b/PostHogTests/PostHogSDKTest.swift index d4da955c2c..3a54a73ace 100644 --- a/PostHogTests/PostHogSDKTest.swift +++ b/PostHogTests/PostHogSDKTest.swift @@ -104,14 +104,12 @@ class PostHogSDKTest: QuickSpec { server = MockPostHogServer(version: 4) server.start() - DI.main.sessionManager = PostHogSessionManager() DI.main.appLifecyclePublisher = mockAppLifecycle } afterEach { now = { Date() } server.stop() server = nil - DI.main.sessionManager.endSession {} } it("captures the capture event") { diff --git a/PostHogTests/PostHogSessionManagerTest.swift b/PostHogTests/PostHogSessionManagerTest.swift index 2b5ec32670..c0b44de7be 100644 --- a/PostHogTests/PostHogSessionManagerTest.swift +++ b/PostHogTests/PostHogSessionManagerTest.swift @@ -6,10 +6,8 @@ // import Foundation -import Testing - @testable import PostHog -import XCTest +import Testing @Suite(.serialized) enum PostHogSessionManagerTest { @@ -20,34 +18,39 @@ enum PostHogSessionManagerTest { init() { mockAppLifecycle = MockApplicationLifecyclePublisher() DI.main.appLifecyclePublisher = mockAppLifecycle - DI.main.sessionManager = PostHogSessionManager() + } + + func getSut() -> PostHogSDK { + let config = PostHogConfig(apiKey: "test-key") + return PostHogSDK.with(config) } @Test("Session id is cleared after 30 min of background time") func sessionClearedBackgrounded() throws { let mockNow = MockDate() now = { mockNow.date } + let posthog = getSut() - let originalSessionId = PostHogSessionManager.shared.getNextSessionId() + let originalSessionId = posthog.getSessionManager()?.getNextSessionId() try #require(originalSessionId != nil) - PostHogSessionManager.shared.touchSession() + posthog.getSessionManager()?.touchSession() var newSessionId: String? - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == originalSessionId) mockAppLifecycle.simulateAppDidEnterBackground() // user backgrounds app mockNow.date.addTimeInterval(60 * 30) // +30 minutes (session should not rotate) - newSessionId = PostHogSessionManager.shared.getSessionId() // background activity + newSessionId = posthog.getSessionManager()?.getSessionId() // background activity #expect(newSessionId == originalSessionId) mockNow.date.addTimeInterval(60 * 1) // past 30 minutes (session should clear) - newSessionId = PostHogSessionManager.shared.getSessionId() // background activity, session should be cleared + newSessionId = posthog.getSessionManager()?.getSessionId() // background activity, session should be cleared #expect(newSessionId == nil) } @@ -56,29 +59,30 @@ enum PostHogSessionManagerTest { func sessionClearedWhenMovingBetweenBackgroundAndForeground() throws { let mockNow = MockDate() now = { mockNow.date } + let posthog = getSut() - let originalSessionId = PostHogSessionManager.shared.getNextSessionId() + let originalSessionId = posthog.getSessionManager()?.getNextSessionId() try #require(originalSessionId != nil) - PostHogSessionManager.shared.touchSession() + posthog.getSessionManager()?.touchSession() var newSessionId: String? - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == originalSessionId) mockAppLifecycle.simulateAppDidEnterBackground() // user backgrounds app mockNow.date.addTimeInterval(60 * 29) // waits 29 mins mockAppLifecycle.simulateAppDidBecomeActive() // user foregrounds app - newSessionId = PostHogSessionManager.shared.getSessionId() // should not rotate + newSessionId = posthog.getSessionManager()?.getSessionId() // should not rotate #expect(newSessionId == originalSessionId) mockAppLifecycle.simulateAppDidEnterBackground() // user backgrounds app mockNow.date.addTimeInterval(60 * 31) // waits 30+ mins mockAppLifecycle.simulateAppDidBecomeActive() // user foregrounds app - newSessionId = PostHogSessionManager.shared.getSessionId() // *should* rotate + newSessionId = posthog.getSessionManager()?.getSessionId() // *should* rotate #expect(newSessionId != originalSessionId) } @@ -87,26 +91,27 @@ enum PostHogSessionManagerTest { func sessionRotatedWhenInactive() throws { let mockNow = MockDate() now = { mockNow.date } + let posthog = getSut() // session start - let originalSessionId = PostHogSessionManager.shared.getNextSessionId() + let originalSessionId = posthog.getSessionManager()?.getNextSessionId() // app foregrounded mockAppLifecycle.simulateAppDidBecomeActive() try #require(originalSessionId != nil) // activity - PostHogSessionManager.shared.touchSession() + posthog.getSessionManager()?.touchSession() var newSessionId: String? // inactivity mockNow.date.addTimeInterval(60 * 30) // 30 minutes inactivity (session should not rotate) - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == originalSessionId) mockNow.date.addTimeInterval(20) // past 30 minutes of inactivity (session should rotate) - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId != nil) #expect(newSessionId != originalSessionId) @@ -116,9 +121,10 @@ enum PostHogSessionManagerTest { func sessionRotatedWhenPastMaxSessionLength() throws { let mockNow = MockDate() now = { mockNow.date } + let posthog = getSut() // session start - let originalSessionId = PostHogSessionManager.shared.getNextSessionId() + let originalSessionId = posthog.getSessionManager()?.getNextSessionId() // app foregrounded mockAppLifecycle.simulateAppDidBecomeActive() @@ -129,20 +135,20 @@ enum PostHogSessionManagerTest { for _ in 0 ..< 49 { // activity mockNow.date.addTimeInterval(60 * 29) // +23 hours, 40 minutes (session should not rotate) - PostHogSessionManager.shared.touchSession() + posthog.getSessionManager()?.touchSession() } - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == originalSessionId) mockNow.date.addTimeInterval(60 * 10) // +10 minutes (session should not rotate) - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == originalSessionId) mockNow.date.addTimeInterval(60 * 10) // +10 minutes (session should rotate) - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId != originalSessionId) } @@ -156,7 +162,6 @@ enum PostHogSessionManagerTest { init() { mockAppLifecycle = MockApplicationLifecyclePublisher() DI.main.appLifecyclePublisher = mockAppLifecycle - DI.main.sessionManager = PostHogSessionManager() server = MockPostHogServer() server.start() @@ -169,7 +174,6 @@ enum PostHogSessionManagerTest { now = { Date() } server.stop() server = nil - PostHogSessionManager.shared.endSession {} } func getSut( @@ -210,7 +214,7 @@ enum PostHogSessionManagerTest { mockAppLifecycle.simulateAppDidBecomeActive() // some activity - PostHogSessionManager.shared.touchSession() + sut.getSessionManager()?.touchSession() sut.capture("event captured", timestamp: mockNow.date) // background app @@ -244,7 +248,7 @@ enum PostHogSessionManagerTest { mockAppLifecycle.simulateAppDidBecomeActive() // some activity - PostHogSessionManager.shared.touchSession() + sut.getSessionManager()?.touchSession() sut.capture("event captured") mockNow.date.addTimeInterval(60 * 31) // +31 mins of inactivity @@ -283,28 +287,28 @@ enum PostHogSessionManagerTest { mockAppLifecycle.simulateAppDidBecomeActive() // activity - PostHogSessionManager.shared.touchSession() + sut.getSessionManager()?.touchSession() sut.capture("event 0 captured", timestamp: mockNow.date) - let originalSessionId = PostHogSessionManager.shared.getSessionId(readOnly: true) + let originalSessionId = sut.getSessionManager()?.getSessionId(readOnly: true) // 23 hours, 41 minutes worth of activity for i in 0 ..< 49 { // activity compoundedTime += 60 * 29 mockNow.date.addTimeInterval(60 * 29) - PostHogSessionManager.shared.touchSession() + sut.getSessionManager()?.touchSession() sut.capture("event \(i) captured", timestamp: mockNow.date) } compoundedTime += 60 * 10 mockNow.date.addTimeInterval(60 * 10) - PostHogSessionManager.shared.touchSession() + sut.getSessionManager()?.touchSession() sut.capture("event 51 captured", timestamp: mockNow.date) compoundedTime += 60 * 10 mockNow.date.addTimeInterval(60 * 10) - PostHogSessionManager.shared.touchSession() + sut.getSessionManager()?.touchSession() sut.capture("event 52 captured", timestamp: mockNow.date) let events = try await getServerEvents(server) @@ -372,12 +376,14 @@ enum PostHogSessionManagerTest { @Suite("Test React Native session management") struct ReactNativeTests { let mockAppLifecycle: MockApplicationLifecyclePublisher + let posthog: PostHogSDK init() { postHogSdkName = "posthog-react-native" mockAppLifecycle = MockApplicationLifecyclePublisher() DI.main.appLifecyclePublisher = mockAppLifecycle - DI.main.sessionManager = PostHogSessionManager() + let config = PostHogConfig(apiKey: "test-key") + posthog = PostHogSDK.with(config) } @Test("Session id is NOT cleared after 30 min of background time") @@ -387,23 +393,23 @@ enum PostHogSessionManagerTest { // RN sets custom session id let rnSessionId = UUID().uuidString - PostHogSessionManager.shared.setSessionId(rnSessionId) + posthog.getSessionManager()?.setSessionId(rnSessionId) - PostHogSessionManager.shared.touchSession() + posthog.getSessionManager()?.touchSession() var newSessionId: String? - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) mockAppLifecycle.simulateAppDidEnterBackground() mockNow.date.addTimeInterval(60 * 30) // +30 minutes (session should not rotate) - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) mockNow.date.addTimeInterval(60 * 1) // past 30 minutes (session should clear) - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) } @@ -415,23 +421,23 @@ enum PostHogSessionManagerTest { // RN sets custom session id let rnSessionId = UUID().uuidString - PostHogSessionManager.shared.setSessionId(rnSessionId) + posthog.getSessionManager()?.setSessionId(rnSessionId) // app foregrounded mockAppLifecycle.simulateAppDidBecomeActive() // activity - PostHogSessionManager.shared.touchSession() + posthog.getSessionManager()?.touchSession() var newSessionId: String? // inactivity mockNow.date.addTimeInterval(60 * 30) // 30 minutes inactivity (session should not rotate) - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) mockNow.date.addTimeInterval(20) // past 30 minutes of inactivity (session should rotate) - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) } @@ -443,7 +449,7 @@ enum PostHogSessionManagerTest { // RN sets custom session id let rnSessionId = UUID().uuidString - PostHogSessionManager.shared.setSessionId(rnSessionId) + posthog.getSessionManager()?.setSessionId(rnSessionId) // app foregrounded mockAppLifecycle.simulateAppDidBecomeActive() @@ -453,20 +459,20 @@ enum PostHogSessionManagerTest { for _ in 0 ..< 49 { // activity mockNow.date.addTimeInterval(60 * 29) // +23 hours, 40 minutes (session should not rotate) - PostHogSessionManager.shared.touchSession() + posthog.getSessionManager()?.touchSession() } - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) mockNow.date.addTimeInterval(60 * 10) // +10 minutes (session should not rotate) - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) mockNow.date.addTimeInterval(60 * 10) // +10 minutes (session should rotate) - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) } @@ -478,15 +484,15 @@ enum PostHogSessionManagerTest { // RN sets custom session id let rnSessionId = UUID().uuidString - PostHogSessionManager.shared.setSessionId(rnSessionId) + posthog.getSessionManager()?.setSessionId(rnSessionId) - var newSessionId = PostHogSessionManager.shared.getSessionId() + var newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) - PostHogSessionManager.shared.startSession() + posthog.getSessionManager()?.startSession() - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) } @@ -498,15 +504,15 @@ enum PostHogSessionManagerTest { // RN sets custom session id let rnSessionId = UUID().uuidString - PostHogSessionManager.shared.setSessionId(rnSessionId) + posthog.getSessionManager()?.setSessionId(rnSessionId) - var newSessionId = PostHogSessionManager.shared.getSessionId() + var newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) - PostHogSessionManager.shared.endSession() + posthog.getSessionManager()?.endSession() - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) } @@ -518,15 +524,15 @@ enum PostHogSessionManagerTest { // RN sets custom session id let rnSessionId = UUID().uuidString - PostHogSessionManager.shared.setSessionId(rnSessionId) + posthog.getSessionManager()?.setSessionId(rnSessionId) - var newSessionId = PostHogSessionManager.shared.getSessionId() + var newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) - PostHogSessionManager.shared.resetSession() + posthog.getSessionManager()?.resetSession() - newSessionId = PostHogSessionManager.shared.getSessionId() + newSessionId = posthog.getSessionManager()?.getSessionId() #expect(newSessionId == rnSessionId) } From d43d48a8cabb404bdd688291220457d43c916db8 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Mon, 13 Oct 2025 23:20:57 +0300 Subject: [PATCH 3/7] fix: do not clear session manager on reset --- PostHog/PostHogSDK.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index 734656b0fe..e13f923aaf 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -355,7 +355,6 @@ let maxRetryDelay = 30.0 flagCallReported.removeAll() } sessionManager?.resetSession() - sessionManager = nil // Clear person and group properties for flags remoteConfig?.resetPersonPropertiesForFlags() From 89c085122f1b6eb40e92d0624255e7ab01a2a708 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Tue, 14 Oct 2025 16:13:07 +0300 Subject: [PATCH 4/7] fix: add note --- PostHog/PostHogConfig.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PostHog/PostHogConfig.swift b/PostHog/PostHogConfig.swift index 9696d35c23..7a13576b9f 100644 --- a/PostHog/PostHogConfig.swift +++ b/PostHog/PostHogConfig.swift @@ -59,6 +59,9 @@ public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? /// /// When disabled, functionality that require swizzling (like autocapture, screen views, session replay, surveys) will not be installed. /// + /// Note: Disabling swizzling will limit session rotation logic to only detect application open and background events. + /// Session rotation will still work, just with reduced granularity for detecting user activity. + /// /// Default: true @objc public var enableSwizzling: Bool = true From 32ac83308a236786f75af232e03876a5631fbb7d Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Tue, 14 Oct 2025 16:36:01 +0300 Subject: [PATCH 5/7] fix: make session manage non-optional --- PostHog/PostHogConfig.swift | 2 +- PostHog/PostHogSDK.swift | 24 +++++++++---------- PostHog/PostHogSessionManager.swift | 24 +++++++++++++++---- ...ostHogSessionReplayConsoleLogsPlugin.swift | 2 +- .../PostHogSessionReplayNetworkPlugin.swift | 2 +- .../Plugins/Network/URLSessionExtension.swift | 4 ++-- PostHog/Replay/PostHogReplayIntegration.swift | 8 +++---- 7 files changed, 40 insertions(+), 26 deletions(-) diff --git a/PostHog/PostHogConfig.swift b/PostHog/PostHogConfig.swift index 7a13576b9f..2542efdbdf 100644 --- a/PostHog/PostHogConfig.swift +++ b/PostHog/PostHogConfig.swift @@ -59,7 +59,7 @@ public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? /// /// When disabled, functionality that require swizzling (like autocapture, screen views, session replay, surveys) will not be installed. /// - /// Note: Disabling swizzling will limit session rotation logic to only detect application open and background events. + /// Note: Disabling swizzling will limit session rotation logic to only detect application open and background events. /// Session rotation will still work, just with reduced granularity for detecting user activity. /// /// Default: true diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index e13f923aaf..182ac026cc 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -46,7 +46,7 @@ let maxRetryDelay = 30.0 private var context: PostHogContext? private static var apiKeys = Set() private var installedIntegrations: [PostHogIntegration] = [] - var sessionManager: PostHogSessionManager? + let sessionManager = PostHogSessionManager() #if os(iOS) private weak var replayIntegration: PostHogReplayIntegration? @@ -138,8 +138,8 @@ let maxRetryDelay = 30.0 disableQueueTimerForTesting: config.disableQueueTimerForTesting) // Create session manager instance for this PostHogSDK instance - sessionManager = PostHogSessionManager(config: config) - sessionManager?.startSession() + sessionManager.setup(config: config) + sessionManager.startSession() if !config.optOut { // don't install integrations if in opt-out state @@ -173,7 +173,7 @@ let maxRetryDelay = 30.0 return nil } - return sessionManager?.getSessionId(readOnly: true) + return sessionManager.getSessionId(readOnly: true) } @objc public func startSession() { @@ -181,7 +181,7 @@ let maxRetryDelay = 30.0 return } - sessionManager?.startSession() + sessionManager.startSession() } @objc public func endSession() { @@ -189,7 +189,7 @@ let maxRetryDelay = 30.0 return } - sessionManager?.endSession() + sessionManager.endSession() } // EVENT CAPTURE @@ -306,7 +306,7 @@ let maxRetryDelay = 30.0 // if not present, get a current or new session id at event timestamp let propSessionId = properties?["$session_id"] as? String let sessionId: String? = propSessionId.isNilOrEmpty - ? sessionManager?.getSessionId(at: timestamp ?? now()) + ? sessionManager.getSessionId(at: timestamp ?? now()) : propSessionId if let sessionId { @@ -354,7 +354,7 @@ let maxRetryDelay = 30.0 flagCallReportedLock.withLock { flagCallReported.removeAll() } - sessionManager?.resetSession() + sessionManager.reset() // Clear person and group properties for flags remoteConfig?.resetPersonPropertiesForFlags() @@ -1273,7 +1273,7 @@ let maxRetryDelay = 30.0 flagCallReported.removeAll() } context = nil - sessionManager?.endSession() + sessionManager.endSession() toggleHedgeLog(false) uninstallIntegrations() @@ -1331,8 +1331,8 @@ let maxRetryDelay = 30.0 } let sessionId = resumeCurrent - ? sessionManager?.getSessionId() - : sessionManager?.getNextSessionId() + ? sessionManager.getSessionId() + : sessionManager.getNextSessionId() guard let sessionId else { return hedgeLog("Could not start recording. Missing session id.") @@ -1373,7 +1373,7 @@ let maxRetryDelay = 30.0 return false } - guard let replayIntegration, let remoteConfig, let sessionManager else { + guard let replayIntegration, let remoteConfig else { return false } diff --git a/PostHog/PostHogSessionManager.swift b/PostHog/PostHogSessionManager.swift index e1bc18c18b..f38b3a5276 100644 --- a/PostHog/PostHogSessionManager.swift +++ b/PostHog/PostHogSessionManager.swift @@ -20,15 +20,30 @@ import Foundation case customSessionId = "Custom session set" } - private let config: PostHogConfig + @objc public static var shared: PostHogSessionManager { + PostHogSDK.shared.sessionManager + } - init(config: PostHogConfig) { - self.config = config + private var config: PostHogConfig? + + override init() { super.init() + } + + func setup(config: PostHogConfig) { + self.config = config + reset() registerNotifications() registerApplicationSendEvent() } + func reset() { + resetSession() + didBecomeActiveToken = nil + didEnterBackgroundToken = nil + applicationEventToken = nil + } + private let queue = DispatchQueue(label: "com.posthog.PostHogSessionManager", target: .global(qos: .utility)) private var sessionId: String? private var sessionStartTimestamp: TimeInterval? @@ -235,10 +250,9 @@ import Foundation private func registerApplicationSendEvent() { #if os(iOS) || os(tvOS) - guard config.enableSwizzling else { + guard let config, config.enableSwizzling else { return } - applicationEventToken = DI.main.applicationEventPublisher.onApplicationEvent { [weak self] _, _ in // update "last active" session // we want to keep track of the idle time, so we need to maintain a timestamp on the last interactions of the user with the app. UIEvents are a good place to do so since it means that the user is actively interacting with the app (e.g not just noise background activity) diff --git a/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift b/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift index a89be4eb78..a7f39ade98 100644 --- a/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift +++ b/PostHog/Replay/Plugins/Console Logs/PostHogSessionReplayConsoleLogsPlugin.swift @@ -49,7 +49,7 @@ isActive, let postHog, postHog.isSessionReplayActive(), - let sessionId = postHog.sessionManager?.getSessionId(at: output.timestamp) + let sessionId = postHog.sessionManager.getSessionId(at: output.timestamp) else { return } diff --git a/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift b/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift index fc37d0d0fd..af317903ad 100644 --- a/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift +++ b/PostHog/Replay/Plugins/Network/PostHogSessionReplayNetworkPlugin.swift @@ -21,7 +21,7 @@ shouldCapture: shouldCaptureNetworkSample, onCapture: handleNetworkSample, getSessionId: { [weak self] date in - self?.postHog?.sessionManager?.getSessionId(at: date) + self?.postHog?.sessionManager.getSessionId(at: date) } ) sessionSwizzler?.swizzle() diff --git a/PostHog/Replay/Plugins/Network/URLSessionExtension.swift b/PostHog/Replay/Plugins/Network/URLSessionExtension.swift index 431cfb08c1..fa43ed6209 100644 --- a/PostHog/Replay/Plugins/Network/URLSessionExtension.swift +++ b/PostHog/Replay/Plugins/Network/URLSessionExtension.swift @@ -26,7 +26,7 @@ let timestamp = Date() let startMillis = getMonotonicTimeInMilliseconds() var endMillis: UInt64? - let sessionId = postHog?.sessionManager?.getSessionId(at: timestamp) + let sessionId = postHog?.sessionManager.getSessionId(at: timestamp) do { let (data, response) = try await action() endMillis = getMonotonicTimeInMilliseconds() @@ -57,7 +57,7 @@ let timestamp = Date() let startMillis = getMonotonicTimeInMilliseconds() var endMillis: UInt64? - let sessionId = postHog?.sessionManager?.getSessionId(at: timestamp) + let sessionId = postHog?.sessionManager.getSessionId(at: timestamp) do { let (url, response) = try await action() endMillis = getMonotonicTimeInMilliseconds() diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index e623f829d3..d3511fc835 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -142,7 +142,7 @@ isEnabled = true // reset views when session id changes (or is cleared) so we can re-send new metadata (or full snapshot in the future) - postHog.sessionManager?.onSessionIdChanged = { [weak self] in + postHog.sessionManager.onSessionIdChanged = { [weak self] in self?.resetViews() } @@ -185,7 +185,7 @@ guard isEnabled else { return } isEnabled = false resetViews() - postHog?.sessionManager?.onSessionIdChanged = {} + postHog?.sessionManager.onSessionIdChanged = {} // stop listening to `UIApplication.sendEvent` applicationEventToken = nil @@ -251,7 +251,7 @@ PostHogReplayIntegration.dispatchQueue.async { [touchInfo, weak postHog = postHog] in // always make sure we have a fresh session id as early as possible - guard let sessionId = postHog?.sessionManager?.getSessionId(at: date) else { + guard let sessionId = postHog?.sessionManager.getSessionId(at: date) else { return } @@ -343,7 +343,7 @@ PostHogReplayIntegration.dispatchQueue.async { // always make sure we have a fresh session id at correct timestamp - guard let sessionId = postHog.sessionManager?.getSessionId(at: timestampDate) else { + guard let sessionId = postHog.sessionManager.getSessionId(at: timestampDate) else { return } From 671e6a2e51185920d5ec78fbb2de02397c1c7d83 Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Tue, 14 Oct 2025 16:43:18 +0300 Subject: [PATCH 6/7] chore: improve changelog note --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcec9b1e17..1688d35d93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ - feat: add config option to disable swizzling ([#388](https://github.com/PostHog/posthog-ios/pull/388)) - feat: SDK instance now manages its own session ([#388](https://github.com/PostHog/posthog-ios/pull/388)) -> Note: In case of multiple SDK instances, each instance will now report and manage its own $session_id +> **Note**: A potentially breaking change for users with multiple SDK instances. Each SDK instance now manages its own `$session_id` instead of sharing a global session across all instances. +> This aligns with PostHog JS SDK behavior and ensures proper session isolation when using multiple SDK instances. +> For single-instance usage (the common case), this change has no impact. ## 3.33.0 - 2025-10-13 From a7169a380ac1d1093d6b9e3e6e76105f4cb6e14e Mon Sep 17 00:00:00 2001 From: Ioannis J Date: Tue, 14 Oct 2025 19:45:21 +0300 Subject: [PATCH 7/7] fix: session setup --- PostHog/PostHogSessionManager.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PostHog/PostHogSessionManager.swift b/PostHog/PostHogSessionManager.swift index f38b3a5276..07de4bc4d0 100644 --- a/PostHog/PostHogSessionManager.swift +++ b/PostHog/PostHogSessionManager.swift @@ -32,7 +32,9 @@ import Foundation func setup(config: PostHogConfig) { self.config = config - reset() + didBecomeActiveToken = nil + didEnterBackgroundToken = nil + applicationEventToken = nil registerNotifications() registerApplicationSendEvent() }