From d10ac7b1b1fb4e4491ca837f9603a2c10912a170 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 23 Oct 2023 12:04:27 +0200 Subject: [PATCH] add captureApplicationLifecycleEvents --- .swiftlint.yml | 3 +- PostHog/PostHogConfig.swift | 4 +- PostHog/PostHogSDK.swift | 111 +++++++++++++++++++++++--- PostHog/PostHogSessionManager.swift | 39 --------- PostHog/PostHogStorage.swift | 1 - PostHogExample/AppDelegate.swift | 4 +- PostHogTests/PostHogTest.swift | 4 - PostHogTests/SessionManagerTest.swift | 21 ----- 8 files changed, 105 insertions(+), 82 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index d22824421..cf3bbe72b 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -8,6 +8,7 @@ excluded: # case-sensitive paths to ignore during linting. Takes precedence over - PostHog/Utils/NSData+PHGGZIP.h - PostHog/Utils/NSData+PHGGZIP.m - .build + - Pods disabled_rules: - force_cast @@ -17,7 +18,7 @@ disabled_rules: line_length: 160 file_length: - warning: 600 + warning: 1000 error: 1200 function_body_length: diff --git a/PostHog/PostHogConfig.swift b/PostHog/PostHogConfig.swift index df32d7a5f..40da7f1ff 100644 --- a/PostHog/PostHogConfig.swift +++ b/PostHog/PostHogConfig.swift @@ -22,11 +22,11 @@ import Foundation @objc public var dataMode: PostHogDataMode = .any @objc public var sendFeatureFlagEvent: Bool = true @objc public var preloadFeatureFlags: Bool = true + @objc public var captureApplicationLifecycleEvents: Bool = true + @objc public var captureScreenViews: Bool = true @objc public var debug: Bool = false @objc public var optOut: Bool = false public static let defaultHost: String = "https://app.posthog.com" - // TODO: encryption, captureApplicationLifecycleEvents, recordScreenViews, captureInAppPurchases, - // capturePushNotifications, captureDeepLinks, launchOptions public init( apiKey: String, diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index 54a31772e..40cdcad45 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -37,6 +37,7 @@ let maxRetryDelay = 30.0 private var featureFlags: PostHogFeatureFlags? private var context: PostHogContext? private static var apiKeys = Set() + private var capturedAppInstalled = false @objc public static let shared: PostHogSDK = { let instance = PostHogSDK(PostHogConfig(apiKey: "")) @@ -90,11 +91,10 @@ let maxRetryDelay = 30.0 queue = PostHogQueue(config, theStorage, theApi, reachability) - // TODO: Decide if we definitely want to reset the session on load or not - sessionManager?.resetSession() - queue?.start() + registerNotifications() + DispatchQueue.main.async { NotificationCenter.default.post(name: PostHogSDK.didStartNotification, object: nil) } @@ -121,20 +121,11 @@ let maxRetryDelay = 30.0 return sessionManager?.getAnonymousId() ?? "" } - @objc public func getSessionId() -> String? { - if !isEnabled() { - return nil - } - - return sessionManager?.getSessionId() - } - // EVENT CAPTURE private func dynamicContext() -> [String: Any] { var properties: [String: Any] = [:] - properties["$session_id"] = getSessionId() var groups: [String: String]? groupsLock.withLock { groups = getGroups() @@ -515,4 +506,100 @@ let maxRetryDelay = 30.0 postHog.setup(config) return postHog } + + private func registerNotifications() { + let defaultCenter = NotificationCenter.default + + #if os(iOS) || os(tvOS) + let didFinishLaunchingNotification = UIApplication.didFinishLaunchingNotification + + defaultCenter.addObserver(self, selector: #selector(captureAppLifecycle), name: didFinishLaunchingNotification, object: nil) + #endif + } + + private func captureAppInstalled() { + let bundle = Bundle.main + + let versionName = bundle.infoDictionary?["CFBundleShortVersionString"] as? String + let versionCode = bundle.infoDictionary?["CFBundleVersion"] as? String + + // capture app installed/updated + if !capturedAppInstalled { + let userDefaults = UserDefaults.standard + + let previousVersion = userDefaults.string(forKey: "PHGVersionKey") + let previousVersionCode = userDefaults.string(forKey: "PHGBuildKeyV2") + + var props: [String: Any] = [:] + var event: String + if previousVersionCode == nil { + // installed + event = "Application Installed" + } else { + event = "Application Updated" + + // Do not send version updates if its the same + if previousVersionCode == versionCode { + return + } + + if previousVersion != nil { + props["previous_version"] = previousVersion + } + props["previous_build"] = previousVersionCode + } + + var syncDefaults = false + if versionName != nil { + props["version"] = versionName + userDefaults.setValue(versionName, forKey: "PHGVersionKey") + syncDefaults = true + } + + if versionCode != nil { + props["build"] = versionCode + userDefaults.setValue(versionCode, forKey: "PHGBuildKeyV2") + syncDefaults = true + } + + if syncDefaults { + userDefaults.synchronize() + } + + capture(event, properties: props) + + capturedAppInstalled = true + } + } + + private func captureAppOpened() { + let bundle = Bundle.main + + let versionName = bundle.infoDictionary?["CFBundleShortVersionString"] as? String + let versionCode = bundle.infoDictionary?["CFBundleVersion"] as? String + + var props: [String: Any] = [:] + + if versionName != nil { + props["version"] = versionName + } + if versionCode != nil { + props["build"] = versionCode + } +// TODO: detect info dynamically + props["from_background"] = false +// props["referring_application"] = launchOptions[UIApplicationLaunchOptionsSourceApplicationKey] +// props["url"] = launchOptions[UIApplicationLaunchOptionsURLKey] + + capture("Application Opened", properties: props) + } + + @objc private func captureAppLifecycle() { + if !config.captureApplicationLifecycleEvents { + return + } + + captureAppInstalled() + captureAppOpened() + } } diff --git a/PostHog/PostHogSessionManager.swift b/PostHog/PostHogSessionManager.swift index 9e5f7d540..619ef9d2e 100644 --- a/PostHog/PostHogSessionManager.swift +++ b/PostHog/PostHogSessionManager.swift @@ -12,14 +12,10 @@ class PostHogSessionManager { private let anonLock = NSLock() private let distinctLock = NSLock() - private let sessionLock = NSLock() - init(config: PostHogConfig) { storage = PostHogStorage(config) } - let sessionChangeThreshold: Double = 1800 - public func getAnonymousId() -> String { var anonymousId: String? anonLock.withLock { @@ -57,39 +53,4 @@ class PostHogSessionManager { storage.setString(forKey: .distinctId, contents: id) } } - - public func getSessionId() -> String { - var sessionId: String? - sessionLock.withLock { - sessionId = getSesssionId() - } - return sessionId ?? "" - } - - // Load the sessionId, ensuring it is rotated if expired - private func getSesssionId(timestamp: TimeInterval? = nil) -> String { - var sessionId = storage.getString(forKey: .sessionId) - let sessionLastTimestamp = storage.getNumber(forKey: .sessionlastTimestamp) ?? 0 - let newTimestamp = Double(timestamp ?? Date().timeIntervalSince1970) - - if sessionId == nil || sessionLastTimestamp == 0 || (newTimestamp - sessionLastTimestamp) > sessionChangeThreshold { - sessionId = UUID().uuidString - storage.setString(forKey: .sessionId, contents: sessionId!) - storage.setNumber(forKey: .sessionlastTimestamp, contents: newTimestamp) - - hedgeLog("Session expired - creating new session '\(sessionId!)'") - DispatchQueue.main.async { - NotificationCenter.default.post(name: PostHogSDK.didResetSessionNotification, object: sessionId) - } - } - - return sessionId ?? "" - } - - public func resetSession() { - sessionLock.withLock { - storage.remove(key: .sessionId) - storage.remove(key: .sessionlastTimestamp) - } - } } diff --git a/PostHog/PostHogStorage.swift b/PostHog/PostHogStorage.swift index 86f4f4c13..20b89e166 100644 --- a/PostHog/PostHogStorage.swift +++ b/PostHog/PostHogStorage.swift @@ -27,7 +27,6 @@ class PostHogStorage { case enabledFeatureFlags = "posthog.enabledFeatureFlags" case enabledFeatureFlagPayloads = "posthog.enabledFeatureFlagPayloads" case groups = "posthog.groups" - case sessionId = "posthog.sessionId" case sessionlastTimestamp = "posthog.sessionlastTimestamp" case registerProperties = "posthog.registerProperties" case optOut = "posthog.optOut" diff --git a/PostHogExample/AppDelegate.swift b/PostHogExample/AppDelegate.swift index f05dee37d..4eaa8c035 100644 --- a/PostHogExample/AppDelegate.swift +++ b/PostHogExample/AppDelegate.swift @@ -16,8 +16,8 @@ class AppDelegate: NSObject, UIApplicationDelegate { ) PostHogSDK.shared.setup(config) - PostHogSDK.shared.debug() - PostHogSDK.shared.capture("App started!") +// PostHogSDK.shared.debug() +// PostHogSDK.shared.capture("App started!") // DispatchQueue.global(qos: .utility).async { // let task = Api().failingRequest() diff --git a/PostHogTests/PostHogTest.swift b/PostHogTests/PostHogTest.swift index c482638ab..e75d066d6 100644 --- a/PostHogTests/PostHogTest.swift +++ b/PostHogTests/PostHogTest.swift @@ -37,21 +37,17 @@ class PostHogTest: QuickSpec { it("setups default IDs") { expect(posthog.getAnonymousId()).toNot(beNil()) expect(posthog.getDistinctId()) == posthog.getAnonymousId() - expect(posthog.getSessionId()).toNot(beNil()) } it("persits IDs but resets the session ID on load") { let anonymousId = posthog.getAnonymousId() let distinctId = posthog.getDistinctId() - let sessionId = posthog.getSessionId() let config = PostHogConfig(apiKey: "test-api-key") let otherPostHog = PostHogSDK.with(config) let otherAnonymousId = otherPostHog.getAnonymousId() let otherDistinctId = otherPostHog.getDistinctId() - let otherSessionId = otherPostHog.getSessionId() - let refreshedSessionId = posthog.getSessionId() expect(anonymousId) == otherAnonymousId expect(distinctId) == otherDistinctId diff --git a/PostHogTests/SessionManagerTest.swift b/PostHogTests/SessionManagerTest.swift index e91a610b1..0d19dd740 100644 --- a/PostHogTests/SessionManagerTest.swift +++ b/PostHogTests/SessionManagerTest.swift @@ -45,26 +45,5 @@ class SessionManagerTest: QuickSpec { expect(newAnonymousId) != newDistinctId expect(newDistinctId) == idToSet } - - it("Generates a session id") { - let sessionId = sessionManager.getSessionId() - expect(sessionId).toNot(beNil()) - - let secondSessionId = sessionManager.getSessionId() - expect(secondSessionId) == sessionId - } - - it("Generates a new session id if last timestamp is older") { - let sessionId = sessionManager.getSessionId() - expect(sessionId).toNot(beNil()) - - changeSessionLastTimestamp(timeToAdd: TimeInterval(0 - sessionManager.sessionChangeThreshold + 100)) - let sameSessionId = sessionManager.getSessionId() - expect(sameSessionId) == sessionId - - changeSessionLastTimestamp(timeToAdd: TimeInterval(0 - sessionManager.sessionChangeThreshold - 100)) - let newSessionId = sessionManager.getSessionId() - expect(newSessionId) != sessionId - } } }