diff --git a/PostHog/AppLifeCycle/PostHogAppLifeCycleIntegration.swift b/PostHog/AppLifeCycle/PostHogAppLifeCycleIntegration.swift index 8fffb79e10..2bc198a21f 100644 --- a/PostHog/AppLifeCycle/PostHogAppLifeCycleIntegration.swift +++ b/PostHog/AppLifeCycle/PostHogAppLifeCycleIntegration.swift @@ -19,8 +19,7 @@ import Foundation final class PostHogAppLifeCycleIntegration: PostHogIntegration { var requiresSwizzling: Bool { false } - private static var integrationInstalledLock = NSLock() - private static var integrationInstalled = false + private static let integrationInstallState = PostHogIntegrationInstallState() private static var didCaptureAppInstallOrUpdate = false private weak var postHog: PostHogSDK? @@ -35,33 +34,19 @@ final class PostHogAppLifeCycleIntegration: PostHogIntegration { private var didFinishLaunchingToken: RegistrationToken? func install(_ postHog: PostHogSDK) -> PostHogIntegrationInstallResult { - let didInstall = PostHogAppLifeCycleIntegration.integrationInstalledLock.withLock { - if PostHogAppLifeCycleIntegration.integrationInstalled { - return false - } - PostHogAppLifeCycleIntegration.integrationInstalled = true - return true - } + installIfNeeded(using: Self.integrationInstallState) { + self.postHog = postHog - guard didInstall else { - return .skipped(.alreadyInstalled) + start() + captureAppInstallOrUpdated() } - - self.postHog = postHog - - start() - captureAppInstallOrUpdated() - return .installed } func uninstall(_ postHog: PostHogSDK) { - // uninstall only for integration instance - if self.postHog === postHog || self.postHog == nil { + uninstallIfNeeded(from: postHog, installedPostHog: self.postHog, state: Self.integrationInstallState) { + // uninstall only for integration instance stop() self.postHog = nil - PostHogAppLifeCycleIntegration.integrationInstalledLock.withLock { - PostHogAppLifeCycleIntegration.integrationInstalled = false - } } } @@ -214,9 +199,7 @@ final class PostHogAppLifeCycleIntegration: PostHogIntegration { extension PostHogAppLifeCycleIntegration { static func clearInstalls() { PostHogAppLifeCycleIntegration.didCaptureAppInstallOrUpdate = false - integrationInstalledLock.withLock { - integrationInstalled = false - } + integrationInstallState.clear() } } #endif diff --git a/PostHog/Autocapture/PostHogAutocaptureIntegration.swift b/PostHog/Autocapture/PostHogAutocaptureIntegration.swift index 7674917890..b7524aacc1 100644 --- a/PostHog/Autocapture/PostHogAutocaptureIntegration.swift +++ b/PostHog/Autocapture/PostHogAutocaptureIntegration.swift @@ -11,39 +11,24 @@ class PostHogAutocaptureIntegration: AutocaptureEventProcessing, PostHogIntegration { var requiresSwizzling: Bool { true } - private static var integrationInstalledLock = NSLock() - private static var integrationInstalled = false + private static let integrationInstallState = PostHogIntegrationInstallState() private weak var postHog: PostHogSDK? private var debounceTimers: [Int: Timer] = [:] func install(_ postHog: PostHogSDK) -> PostHogIntegrationInstallResult { - let didInstall = PostHogAutocaptureIntegration.integrationInstalledLock.withLock { - if PostHogAutocaptureIntegration.integrationInstalled { - return false - } - PostHogAutocaptureIntegration.integrationInstalled = true - return true - } + installIfNeeded(using: Self.integrationInstallState) { + self.postHog = postHog - guard didInstall else { - return .skipped(.alreadyInstalled) + start() } - - self.postHog = postHog - - start() - return .installed } func uninstall(_ postHog: PostHogSDK) { - // uninstall only for integration instance - if self.postHog === postHog || self.postHog == nil { + uninstallIfNeeded(from: postHog, installedPostHog: self.postHog, state: Self.integrationInstallState) { + // uninstall only for integration instance stop() self.postHog = nil - PostHogAutocaptureIntegration.integrationInstalledLock.withLock { - PostHogAutocaptureIntegration.integrationInstalled = false - } } } @@ -140,9 +125,7 @@ #if TESTING extension PostHogAutocaptureIntegration { static func clearInstalls() { - integrationInstalledLock.withLock { - integrationInstalled = false - } + integrationInstallState.clear() } } #endif diff --git a/PostHog/Autocapture/PostHogRageClickIntegration.swift b/PostHog/Autocapture/PostHogRageClickIntegration.swift index 178235d8dc..35214156ee 100644 --- a/PostHog/Autocapture/PostHogRageClickIntegration.swift +++ b/PostHog/Autocapture/PostHogRageClickIntegration.swift @@ -11,41 +11,26 @@ final class PostHogRageClickIntegration: PostHogIntegration { var requiresSwizzling: Bool { true } - private static var integrationInstalledLock = NSLock() - private static var integrationInstalled = false + private static let integrationInstallState = PostHogIntegrationInstallState() private weak var postHog: PostHogSDK? private var rageClickDetector: RageClickDetector? private var applicationEventToken: RegistrationToken? func install(_ postHog: PostHogSDK) -> PostHogIntegrationInstallResult { - let didInstall = PostHogRageClickIntegration.integrationInstalledLock.withLock { - if PostHogRageClickIntegration.integrationInstalled { - return false - } - PostHogRageClickIntegration.integrationInstalled = true - return true - } + installIfNeeded(using: Self.integrationInstallState) { + self.postHog = postHog + rageClickDetector = RageClickDetector(config: postHog.config.rageClickConfig) - guard didInstall else { - return .skipped(.alreadyInstalled) + start() } - - self.postHog = postHog - rageClickDetector = RageClickDetector(config: postHog.config.rageClickConfig) - - start() - return .installed } func uninstall(_ postHog: PostHogSDK) { - // uninstall only for integration instance - if self.postHog === postHog || self.postHog == nil { + uninstallIfNeeded(from: postHog, installedPostHog: self.postHog, state: Self.integrationInstallState) { + // uninstall only for integration instance stop() self.postHog = nil - PostHogRageClickIntegration.integrationInstalledLock.withLock { - PostHogRageClickIntegration.integrationInstalled = false - } } } @@ -150,9 +135,7 @@ #if TESTING extension PostHogRageClickIntegration { static func clearInstalls() { - integrationInstalledLock.withLock { - integrationInstalled = false - } + integrationInstallState.clear() } func processTapForTesting( diff --git a/PostHog/ErrorTracking/PostHogErrorTrackingAutoCaptureIntegration.swift b/PostHog/ErrorTracking/PostHogErrorTrackingAutoCaptureIntegration.swift index b7a2bd719d..d8f405d351 100644 --- a/PostHog/ErrorTracking/PostHogErrorTrackingAutoCaptureIntegration.swift +++ b/PostHog/ErrorTracking/PostHogErrorTrackingAutoCaptureIntegration.swift @@ -12,8 +12,7 @@ import Foundation @_implementationOnly import PHPLCrashReporter class PostHogErrorTrackingAutoCaptureIntegration: PostHogIntegration { - private static let integrationInstalledLock = NSLock() - private static var integrationInstalled = false + private static let integrationInstallState = PostHogIntegrationInstallState() var requiresSwizzling: Bool { false } @@ -25,37 +24,22 @@ import Foundation return .skipped(.disabledByRemoteConfig) } - let installed = PostHogErrorTrackingAutoCaptureIntegration.integrationInstalledLock.withLock { - if PostHogErrorTrackingAutoCaptureIntegration.integrationInstalled { - return false + return installIfNeeded(using: Self.integrationInstallState) { + if let crashReporter = setupCrashReporter() { + self.crashReporter = crashReporter + self.postHog = postHog + // Note: Order here matters, we need to process any pending crash report before enabling the crash reporter + processPendingCrashReportIfNeeded(reporter: crashReporter) + enableCrashReporter(reporter: crashReporter) } - PostHogErrorTrackingAutoCaptureIntegration.integrationInstalled = true - return true } - - guard installed else { - return .skipped(.alreadyInstalled) - } - - if let crashReporter = setupCrashReporter() { - self.crashReporter = crashReporter - self.postHog = postHog - // Note: Order here matters, we need to process any pending crash report before enabling the crash reporter - processPendingCrashReportIfNeeded(reporter: crashReporter) - enableCrashReporter(reporter: crashReporter) - } - - return .installed } func uninstall(_ postHog: PostHogSDK) { - if self.postHog === postHog || self.postHog == nil { + uninstallIfNeeded(from: postHog, installedPostHog: self.postHog, state: Self.integrationInstallState) { stop() crashReporter = nil self.postHog = nil - PostHogErrorTrackingAutoCaptureIntegration.integrationInstalledLock.withLock { - PostHogErrorTrackingAutoCaptureIntegration.integrationInstalled = false - } } } diff --git a/PostHog/ErrorTracking/PostHogExceptionProcessor.swift b/PostHog/ErrorTracking/PostHogExceptionProcessor.swift index 4069119410..677cc574ef 100644 --- a/PostHog/ErrorTracking/PostHogExceptionProcessor.swift +++ b/PostHog/ErrorTracking/PostHogExceptionProcessor.swift @@ -30,10 +30,6 @@ enum PostHogExceptionProcessor { mechanismType: String = "generic", config: PostHogErrorTrackingConfig ) -> [String: Any] { - var properties: [String: Any] = [:] - - properties["$exception_level"] = "error" - let exceptions = buildExceptionList( from: error, handled: handled, @@ -41,9 +37,7 @@ enum PostHogExceptionProcessor { config: config ) - attachExceptionsAndDebugImages(exceptions, to: &properties) - - return properties + return buildProperties(exceptions: exceptions) } /// Convert NSException to properties @@ -63,9 +57,6 @@ enum PostHogExceptionProcessor { mechanismType: String = "generic", config: PostHogErrorTrackingConfig ) -> [String: Any] { - var properties: [String: Any] = [:] - properties["$exception_level"] = "error" // TODO: figure this out from error wrapped type - let exceptions = buildExceptionList( from: exception, handled: handled, @@ -73,9 +64,7 @@ enum PostHogExceptionProcessor { config: config ) - attachExceptionsAndDebugImages(exceptions, to: &properties) - - return properties + return buildProperties(exceptions: exceptions) } /// Convert a message string to properties @@ -90,10 +79,6 @@ enum PostHogExceptionProcessor { mechanismType: String = "generic", config: PostHogErrorTrackingConfig ) -> [String: Any] { - var properties: [String: Any] = [:] - - properties["$exception_level"] = "error" - var exception: [String: Any] = [:] exception["type"] = "Message" exception["value"] = message @@ -109,14 +94,19 @@ enum PostHogExceptionProcessor { exception["stacktrace"] = stacktrace } - let exceptions = [exception] - attachExceptionsAndDebugImages(exceptions, to: &properties) - - return properties + return buildProperties(exceptions: [exception]) } // MARK: - Internal Exception Building + private static func buildProperties(exceptions: [[String: Any]]) -> [String: Any] { + var properties: [String: Any] = [ + "$exception_level": "error", // TODO: figure this out from error wrapped type + ] + attachExceptionsAndDebugImages(exceptions, to: &properties) + return properties + } + /// Build list of exceptions from NSException chain /// /// Walks the NSException chain via NSUnderlyingErrorKey to capture all related exceptions. diff --git a/PostHog/Models/Surveys/PostHogSurveyEnums.swift b/PostHog/Models/Surveys/PostHogSurveyEnums.swift index 614bdcfe1a..fcc2484829 100644 --- a/PostHog/Models/Surveys/PostHogSurveyEnums.swift +++ b/PostHog/Models/Surveys/PostHogSurveyEnums.swift @@ -7,6 +7,16 @@ import Foundation +private func decodeSurveyStringValue( + from decoder: any Decoder, + values: [String: T], + unknown: (String) -> T +) throws -> T { + let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) + return values[value] ?? unknown(value) +} + // MARK: - Supporting Types enum PostHogSurveyType: Decodable, Equatable { @@ -16,19 +26,14 @@ enum PostHogSurveyType: Decodable, Equatable { case unknown(type: String) init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let typeString = try container.decode(String.self) - - switch typeString { - case "popover": - self = .popover - case "api": - self = .api - case "widget": - self = .widget - default: - self = .unknown(type: typeString) - } + self = try decodeSurveyStringValue( + from: decoder, + values: [ + "popover": .popover, + "api": .api, + "widget": .widget, + ] + ) { .unknown(type: $0) } } } @@ -41,23 +46,16 @@ enum PostHogSurveyQuestionType: Decodable, Equatable { case unknown(type: String) init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let typeString = try container.decode(String.self) - - switch typeString { - case "open": - self = .open - case "link": - self = .link - case "rating": - self = .rating - case "multiple_choice": - self = .multipleChoice - case "single_choice": - self = .singleChoice - default: - self = .unknown(type: typeString) - } + self = try decodeSurveyStringValue( + from: decoder, + values: [ + "open": .open, + "link": .link, + "rating": .rating, + "multiple_choice": .multipleChoice, + "single_choice": .singleChoice, + ] + ) { .unknown(type: $0) } } } @@ -67,17 +65,13 @@ enum PostHogSurveyTextContentType: Decodable, Equatable { case unknown(type: String) init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let typeString = try container.decode(String.self) - - switch typeString { - case "html": - self = .html - case "text": - self = .text - default: - self = .unknown(type: typeString) - } + self = try decodeSurveyStringValue( + from: decoder, + values: [ + "html": .html, + "text": .text, + ] + ) { .unknown(type: $0) } } } @@ -93,29 +87,19 @@ enum PostHogSurveyMatchType: Decodable, Equatable { case unknown(value: String) init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let valueString = try container.decode(String.self) - - switch valueString { - case "regex": - self = .regex - case "not_regex": - self = .notRegex - case "exact": - self = .exact - case "is_not": - self = .isNot - case "icontains": - self = .iContains - case "not_icontains": - self = .notIContains - case "gt": - self = .gt - case "lt": - self = .lt - default: - self = .unknown(value: valueString) - } + self = try decodeSurveyStringValue( + from: decoder, + values: [ + "regex": .regex, + "not_regex": .notRegex, + "exact": .exact, + "is_not": .isNot, + "icontains": .iContains, + "not_icontains": .notIContains, + "gt": .gt, + "lt": .lt, + ] + ) { .unknown(value: $0) } } } @@ -132,31 +116,20 @@ enum PostHogSurveyAppearancePosition: Decodable, Equatable { case unknown(position: String) init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let positionString = try container.decode(String.self) - - switch positionString { - case "top_left": - self = .topLeft - case "top_center": - self = .topCenter - case "top_right": - self = .topRight - case "middle_left": - self = .middleLeft - case "middle_center": - self = .middleCenter - case "middle_right": - self = .middleRight - case "left": - self = .left - case "right": - self = .right - case "center": - self = .center - default: - self = .unknown(position: positionString) - } + self = try decodeSurveyStringValue( + from: decoder, + values: [ + "top_left": .topLeft, + "top_center": .topCenter, + "top_right": .topRight, + "middle_left": .middleLeft, + "middle_center": .middleCenter, + "middle_right": .middleRight, + "left": .left, + "right": .right, + "center": .center, + ] + ) { .unknown(position: $0) } } } @@ -167,19 +140,14 @@ enum PostHogSurveyAppearanceWidgetType: Decodable, Equatable { case unknown(type: String) init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let typeString = try container.decode(String.self) - - switch typeString { - case "button": - self = .button - case "tab": - self = .tab - case "selector": - self = .selector - default: - self = .unknown(type: typeString) - } + self = try decodeSurveyStringValue( + from: decoder, + values: [ + "button": .button, + "tab": .tab, + "selector": .selector, + ] + ) { .unknown(type: $0) } } } @@ -189,17 +157,13 @@ enum PostHogSurveyRatingDisplayType: Decodable, Equatable { case unknown(type: String) init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let typeString = try container.decode(String.self) - - switch typeString { - case "number": - self = .number - case "emoji": - self = .emoji - default: - self = .unknown(type: typeString) - } + self = try decodeSurveyStringValue( + from: decoder, + values: [ + "number": .number, + "emoji": .emoji, + ] + ) { .unknown(type: $0) } } } @@ -272,19 +236,14 @@ enum PostHogSurveySchedule: Decodable, Equatable { case unknown(schedule: String) init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let scheduleString = try container.decode(String.self) - - switch scheduleString { - case "once": - self = .once - case "recurring": - self = .recurring - case "always": - self = .always - default: - self = .unknown(schedule: scheduleString) - } + self = try decodeSurveyStringValue( + from: decoder, + values: [ + "once": .once, + "recurring": .recurring, + "always": .always, + ] + ) { .unknown(schedule: $0) } } } @@ -296,20 +255,14 @@ enum PostHogSurveyQuestionBranchingType: Decodable, Equatable { case unknown(type: String) init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let typeString = try container.decode(String.self) - - switch typeString { - case "next_question": - self = .nextQuestion - case "end": - self = .end - case "response_based": - self = .responseBased - case "specific_question": - self = .specificQuestion - default: - self = .unknown(type: typeString) - } + self = try decodeSurveyStringValue( + from: decoder, + values: [ + "next_question": .nextQuestion, + "end": .end, + "response_based": .responseBased, + "specific_question": .specificQuestion, + ] + ) { .unknown(type: $0) } } } diff --git a/PostHog/PostHogIntegration.swift b/PostHog/PostHogIntegration.swift index 809723365e..1da5ec79d7 100644 --- a/PostHog/PostHogIntegration.swift +++ b/PostHog/PostHogIntegration.swift @@ -19,6 +19,27 @@ enum PostHogIntegrationInstallResult: Equatable { case skipped(PostHogIntegrationInstallSkipReason) } +final class PostHogIntegrationInstallState { + private let lock = NSLock() + private var installed = false + + func markInstalled() -> Bool { + lock.withLock { + if installed { + return false + } + installed = true + return true + } + } + + func clear() { + lock.withLock { + installed = false + } + } +} + protocol PostHogIntegration { /** * Indicates whether this integration requires method swizzling to function. @@ -83,6 +104,32 @@ protocol PostHogIntegration { } extension PostHogIntegration { + func installIfNeeded( + using state: PostHogIntegrationInstallState, + _ install: () -> Void + ) -> PostHogIntegrationInstallResult { + guard state.markInstalled() else { + return .skipped(.alreadyInstalled) + } + + install() + return .installed + } + + func uninstallIfNeeded( + from postHog: PostHogSDK, + installedPostHog: PostHogSDK?, + state: PostHogIntegrationInstallState, + _ uninstall: () -> Void + ) { + guard installedPostHog === postHog || installedPostHog == nil else { + return + } + + uninstall() + state.clear() + } + func contextDidChange(_: [String: Any]) { // Default empty implementation since most integrations won't need this } diff --git a/PostHog/PostHogRemoteConfig.swift b/PostHog/PostHogRemoteConfig.swift index c9ad81f330..755c56601d 100644 --- a/PostHog/PostHogRemoteConfig.swift +++ b/PostHog/PostHogRemoteConfig.swift @@ -57,13 +57,13 @@ class PostHogRemoteConfig { var lastRequestId: String? { featureFlagsLock.withLock { - requestId ?? storage.getString(forKey: .requestId) + getCachedValue(\.requestId, key: .requestId) { storage.getString(forKey: $0) } } } var lastEvaluatedAt: Int? { featureFlagsLock.withLock { - evaluatedAt ?? storage.getInt(forKey: .evaluatedAt) + getCachedValue(\.evaluatedAt, key: .evaluatedAt) { storage.getInt(forKey: $0) } } } @@ -570,18 +570,17 @@ class PostHogRemoteConfig { } func getFeatureFlag(_ key: String) -> Any? { - var flags: [String: Any]? - featureFlagsLock.withLock { - flags = self.getCachedFeatureFlags() - } - - return flags?[key] + getFeatureFlagValue(key) { self.getCachedFeatureFlags() } } func getFeatureFlagDetails(_ key: String) -> Any? { + getFeatureFlagValue(key) { self.getCachedFlags() } + } + + private func getFeatureFlagValue(_ key: String, from getCachedValues: () -> [String: Any]?) -> Any? { var flags: [String: Any]? featureFlagsLock.withLock { - flags = self.getCachedFlags() + flags = getCachedValues() } return flags?[key] @@ -589,44 +588,51 @@ class PostHogRemoteConfig { // To be called after acquiring `featureFlagsLock` private func getCachedFeatureFlagPayload() -> [String: Any]? { - if featureFlagPayloads == nil { - featureFlagPayloads = storage.getDictionary(forKey: .enabledFeatureFlagPayloads) as? [String: Any] - } - return featureFlagPayloads + getCachedDictionary(\.featureFlagPayloads, forKey: .enabledFeatureFlagPayloads) } // To be called after acquiring `featureFlagsLock` private func setCachedFeatureFlagPayload(_ featureFlagPayloads: [String: Any]) { - self.featureFlagPayloads = featureFlagPayloads - storage.setDictionary(forKey: .enabledFeatureFlagPayloads, contents: featureFlagPayloads) + setCachedDictionary(featureFlagPayloads, cache: \.featureFlagPayloads, forKey: .enabledFeatureFlagPayloads) } // To be called after acquiring `featureFlagsLock` private func getCachedFeatureFlags() -> [String: Any]? { - if featureFlags == nil { - featureFlags = storage.getDictionary(forKey: .enabledFeatureFlags) as? [String: Any] - } - return featureFlags + getCachedDictionary(\.featureFlags, forKey: .enabledFeatureFlags) } // To be called after acquiring `featureFlagsLock` private func setCachedFeatureFlags(_ featureFlags: [String: Any]) { - self.featureFlags = featureFlags - storage.setDictionary(forKey: .enabledFeatureFlags, contents: featureFlags) + setCachedDictionary(featureFlags, cache: \.featureFlags, forKey: .enabledFeatureFlags) } // To be called after acquiring `featureFlagsLock` private func setCachedFlags(_ flags: [String: Any]) { - self.flags = flags - storage.setDictionary(forKey: .flags, contents: flags) + setCachedDictionary(flags, cache: \.flags, forKey: .flags) } // To be called after acquiring `featureFlagsLock` private func getCachedFlags() -> [String: Any]? { - if flags == nil { - flags = storage.getDictionary(forKey: .flags) as? [String: Any] + getCachedDictionary(\.flags, forKey: .flags) + } + + private func getCachedDictionary( + _ cache: ReferenceWritableKeyPath, + forKey key: PostHogStorage.StorageKey + ) -> [String: Any]? { + if self[keyPath: cache] == nil { + self[keyPath: cache] = storage.getDictionary(forKey: key) as? [String: Any] } - return flags + return self[keyPath: cache] + } + + private func setCachedDictionary( + _ value: [String: Any], + cache: ReferenceWritableKeyPath, + forKey key: PostHogStorage.StorageKey + ) { + self[keyPath: cache] = value + storage.setDictionary(forKey: key, contents: value) } func setPersonPropertiesForFlags(_ properties: [String: Any]) { @@ -773,21 +779,37 @@ class PostHogRemoteConfig { // To be called after acquiring `featureFlagsLock` private func setCachedRequestId(_ value: String?) { - requestId = value - if let value { - storage.setString(forKey: .requestId, contents: value) - } else { - storage.remove(key: .requestId) + setCachedValue(value, cache: \.requestId, key: .requestId) { key, value in + storage.setString(forKey: key, contents: value) } } // To be called after acquiring `featureFlagsLock` private func setCachedEvaluatedAt(_ value: Int?) { - evaluatedAt = value + setCachedValue(value, cache: \.evaluatedAt, key: .evaluatedAt) { key, value in + storage.setInt(forKey: key, contents: value) + } + } + + private func getCachedValue( + _ cache: KeyPath, + key: PostHogStorage.StorageKey, + load: (PostHogStorage.StorageKey) -> T? + ) -> T? { + self[keyPath: cache] ?? load(key) + } + + private func setCachedValue( + _ value: T?, + cache: ReferenceWritableKeyPath, + key: PostHogStorage.StorageKey, + persist: (PostHogStorage.StorageKey, T) -> Void + ) { + self[keyPath: cache] = value if let value { - storage.setInt(forKey: .evaluatedAt, contents: value) + persist(key, value) } else { - storage.remove(key: .evaluatedAt) + storage.remove(key: key) } } @@ -882,10 +904,7 @@ class PostHogRemoteConfig { } private func getCachedRemoteConfig() -> [String: Any]? { - if remoteConfig == nil { - remoteConfig = storage.getDictionary(forKey: .remoteConfig) as? [String: Any] - } - return remoteConfig + getCachedDictionary(\.remoteConfig, forKey: .remoteConfig) } } diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index 2342fda23a..5003419b6b 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -231,22 +231,14 @@ let maxRetryDelay = 30.0 /// /// - Returns: The current distinct ID, or an empty string when the SDK is not set up. @objc public func getDistinctId() -> String { - if !isEnabled() { - return "" - } - - return config.storageManager?.getDistinctId() ?? "" + getStorageManagerValue { $0.getDistinctId() } } /// Returns the anonymous ID generated for this install. /// /// - Returns: The anonymous ID, or an empty string when the SDK is not set up. @objc public func getAnonymousId() -> String { - if !isEnabled() { - return "" - } - - return config.storageManager?.getAnonymousId() ?? "" + getStorageManagerValue { $0.getAnonymousId() } } /// Returns the stable device identifier used for device-level feature flag bucketing. @@ -256,11 +248,15 @@ let maxRetryDelay = 30.0 /// /// - Returns: The stable device ID, or an empty string when the SDK is not set up. @objc public func getDeviceId() -> String { - if !isEnabled() { + getStorageManagerValue { $0.getDeviceId() } + } + + private func getStorageManagerValue(_ value: (PostHogStorageManager) -> String) -> String { + guard isEnabled(), let storageManager = config.storageManager else { return "" } - return config.storageManager?.getDeviceId() ?? "" + return value(storageManager) } /// Returns the current session ID without rotating or creating a session. @@ -276,20 +272,24 @@ let maxRetryDelay = 30.0 /// Starts or resumes the current analytics session. @objc public func startSession() { - if !isEnabled() { - return + performWhenEnabled { + sessionManager.startSession() } - - sessionManager.startSession() } /// Ends the current analytics session. @objc public func endSession() { - if !isEnabled() { + performWhenEnabled { + sessionManager.endSession() + } + } + + private func performWhenEnabled(_ action: () -> Void) { + guard isEnabled() else { return } - sessionManager.endSession() + action() } // DEEP LINKS @@ -1359,38 +1359,32 @@ let maxRetryDelay = 30.0 elementsChain: String, properties: [String: Any] ) { - if !isEnabled() { - return - } - - if isOptOutState() { - return - } - - guard let queue else { - return - } - - let props = [ - "$event_type": eventType, - "$elements_chain": elementsChain, - ].merging(sanitizeDictionary(properties) ?? [:]) { prop, _ in prop } - - let distinctId = getDistinctId() - - let properties = buildProperties(distinctId: distinctId, properties: props) - - guard let event = buildEvent(event: "$autocapture", distinctId: distinctId, properties: properties) else { - return - } - - queueEvent(event, queue: queue) + captureAutocaptureEvent( + "$autocapture", + eventType: eventType, + elementsChain: elementsChain, + properties: properties + ) } func rageclick( eventType: String, elementsChain: String, properties: [String: Any] + ) { + captureAutocaptureEvent( + "$rageclick", + eventType: eventType, + elementsChain: elementsChain, + properties: properties + ) + } + + private func captureAutocaptureEvent( + _ eventName: String, + eventType: String, + elementsChain: String, + properties: [String: Any] ) { if !isEnabled() { return @@ -1413,7 +1407,7 @@ let maxRetryDelay = 30.0 let properties = buildProperties(distinctId: distinctId, properties: props) - guard let event = buildEvent(event: "$rageclick", distinctId: distinctId, properties: properties) else { + guard let event = buildEvent(event: eventName, distinctId: distinctId, properties: properties) else { return } @@ -1864,37 +1858,11 @@ let maxRetryDelay = 30.0 /// - flagVariant: The variant of the feature flag being viewed. If `nil`, the SDK /// looks up the current flag value and skips capture when no value is available. @objc public func captureFeatureView(flag: String, flagVariant: String?) { - if !isEnabled() { - return - } - - if isOptOutState() { - return - } - - // Get the variant value — prefer the explicitly passed variant, then fall back to a flag lookup. - // If neither is available, there is no meaningful variant to record, so we skip the event. - guard let variant: Any = flagVariant ?? getFeatureFlag(flag, sendEvent: false) else { - hedgeLog("captureFeatureView called for flag '\(flag)' but no variant value is available. Event will not be captured.") - return - } - - var props: [String: Any] = [ - "feature_flag": flag, - ] - - if let variantStr = variant as? String { - props["feature_flag_variant"] = variantStr - } - - let userProps: [String: Any] = [ - "$feature_view/\(flag)": variant, - ] - - capture( + captureFeatureEvent( "$feature_view", - properties: props, - userProperties: userProps + flag: flag, + flagVariant: flagVariant, + logName: "captureFeatureView" ) } @@ -1907,6 +1875,20 @@ let maxRetryDelay = 30.0 @objc public func captureFeatureInteraction( flag: String, flagVariant: String? + ) { + captureFeatureEvent( + "$feature_interaction", + flag: flag, + flagVariant: flagVariant, + logName: "captureFeatureInteraction" + ) + } + + private func captureFeatureEvent( + _ event: String, + flag: String, + flagVariant: String?, + logName: String ) { if !isEnabled() { return @@ -1919,7 +1901,7 @@ let maxRetryDelay = 30.0 // Get the variant value — prefer the explicitly passed variant, then fall back to a flag lookup. // If neither is available, there is no meaningful variant to record, so we skip the event. guard let variant: Any = flagVariant ?? getFeatureFlag(flag, sendEvent: false) else { - hedgeLog("captureFeatureInteraction called for flag '\(flag)' but no variant value is available. Event will not be captured.") + hedgeLog("\(logName) called for flag '\(flag)' but no variant value is available. Event will not be captured.") return } @@ -1932,11 +1914,11 @@ let maxRetryDelay = 30.0 } let userProps: [String: Any] = [ - "$feature_interaction/\(flag)": variant, + "\(event)/\(flag)": variant, ] capture( - "$feature_interaction", + event, properties: props, userProperties: userProps ) @@ -2390,10 +2372,7 @@ let maxRetryDelay = 30.0 config: config.errorTrackingConfig ) - var mergedProperties = errorProperties - properties?.forEach { mergedProperties[$0.key] = $0.value } - - capture("$exception", properties: mergedProperties) + captureExceptionEvent(errorProperties, additionalProperties: properties) } /// Capture a Swift Error or NSError without additional properties @@ -2440,10 +2419,7 @@ let maxRetryDelay = 30.0 config: config.errorTrackingConfig ) - var mergedProperties = exceptionProperties - properties?.forEach { mergedProperties[$0.key] = $0.value } - - capture("$exception", properties: mergedProperties) + captureExceptionEvent(exceptionProperties, additionalProperties: properties) } /// Capture an NSException without additional properties @@ -2459,6 +2435,16 @@ let maxRetryDelay = 30.0 captureException(exception, properties: nil) } + private func captureExceptionEvent( + _ exceptionProperties: [String: Any], + additionalProperties: [String: Any]? + ) { + var mergedProperties = exceptionProperties + additionalProperties?.forEach { mergedProperties[$0.key] = $0.value } + + capture("$exception", properties: mergedProperties) + } + private func installIntegrations() { guard installedIntegrations.isEmpty else { hedgeLog("Integrations already installed. Call uninstallIntegrations() first.") @@ -2567,23 +2553,17 @@ let maxRetryDelay = 30.0 extension PostHogSDK { #if os(iOS) || targetEnvironment(macCatalyst) func getAutocaptureIntegration() -> PostHogAutocaptureIntegration? { - installedIntegrations.compactMap { - $0 as? PostHogAutocaptureIntegration - }.first + getIntegration() } func getRageClickIntegration() -> PostHogRageClickIntegration? { - installedIntegrations.compactMap { - $0 as? PostHogRageClickIntegration - }.first + getIntegration() } #endif #if os(iOS) func getReplayIntegration() -> PostHogReplayIntegration? { - installedIntegrations.compactMap { - $0 as? PostHogReplayIntegration - }.first + getIntegration() } #endif @@ -2592,15 +2572,15 @@ let maxRetryDelay = 30.0 } func getAppLifeCycleIntegration() -> PostHogAppLifeCycleIntegration? { - installedIntegrations.compactMap { - $0 as? PostHogAppLifeCycleIntegration - }.first + getIntegration() } func getScreenViewIntegration() -> PostHogScreenViewIntegration? { - installedIntegrations.compactMap { - $0 as? PostHogScreenViewIntegration - }.first + getIntegration() + } + + private func getIntegration() -> T? { + installedIntegrations.compactMap { $0 as? T }.first } } #endif diff --git a/PostHog/PostHogStorage.swift b/PostHog/PostHogStorage.swift index 43a3e2b8bd..00dc52854c 100644 --- a/PostHog/PostHogStorage.swift +++ b/PostHog/PostHogStorage.swift @@ -426,13 +426,7 @@ class PostHogStorage { } func getString(forKey key: StorageKey) -> String? { - let value = getJson(forKey: key) - if let stringValue = value as? String { - return stringValue - } else if let dictValue = value as? [String: String] { - return dictValue[key.rawValue] - } - return nil + getTypedValue(forKey: key) } func setString(forKey key: StorageKey, contents: String) { @@ -448,10 +442,14 @@ class PostHogStorage { } func getBool(forKey key: StorageKey) -> Bool? { + getTypedValue(forKey: key) + } + + private func getTypedValue(forKey key: StorageKey) -> T? { let value = getJson(forKey: key) - if let boolValue = value as? Bool { - return boolValue - } else if let dictValue = value as? [String: Bool] { + if let typedValue = value as? T { + return typedValue + } else if let dictValue = value as? [String: T] { return dictValue[key.rawValue] } return nil diff --git a/PostHog/Replay/Plugins/Network/URLSessionExtension.swift b/PostHog/Replay/Plugins/Network/URLSessionExtension.swift index d0ab18623d..40016a4d40 100644 --- a/PostHog/Replay/Plugins/Network/URLSessionExtension.swift +++ b/PostHog/Replay/Plugins/Network/URLSessionExtension.swift @@ -18,16 +18,16 @@ return nanoTime / 1_000_000 } - private func executeRequest(request: URLRequest? = nil, - action: () async throws -> (Data, URLResponse), - postHog: PostHogSDK?) async throws -> (Data, URLResponse) + private func executeRequest(request: URLRequest? = nil, + action: () async throws -> (Result, URLResponse), + postHog: PostHogSDK?) async throws -> (Result, URLResponse) { let timestamp = Date() let startMillis = getMonotonicTimeInMilliseconds() var endMillis: UInt64? let sessionId = postHog?.sessionManager.getSessionId(at: timestamp) do { - let (data, response) = try await action() + let (result, response) = try await action() endMillis = getMonotonicTimeInMilliseconds() captureData(request: request, response: response, @@ -36,38 +36,7 @@ start: startMillis, end: endMillis, postHog: postHog) - return (data, response) - } catch { - captureData(request: request, - response: nil, - sessionId: sessionId, - timestamp: timestamp, - start: startMillis, - end: endMillis, - postHog: postHog) - throw error - } - } - - private func executeRequest(request: URLRequest? = nil, - action: () async throws -> (URL, URLResponse), - postHog: PostHogSDK?) async throws -> (URL, URLResponse) - { - let timestamp = Date() - let startMillis = getMonotonicTimeInMilliseconds() - var endMillis: UInt64? - let sessionId = postHog?.sessionManager.getSessionId(at: timestamp) - do { - let (url, response) = try await action() - endMillis = getMonotonicTimeInMilliseconds() - captureData(request: request, - response: response, - sessionId: sessionId, - timestamp: timestamp, - start: startMillis, - end: endMillis, - postHog: postHog) - return (url, response) + return (result, response) } catch { captureData(request: request, response: nil, diff --git a/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift b/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift index 8528fe1ec6..746fc8a085 100644 --- a/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift +++ b/PostHog/Replay/Plugins/Network/URLSessionSwizzler.swift @@ -118,22 +118,27 @@ } private func notifyTaskCreated(task: URLSessionTask, session: URLSession?) { - let handlers = lock.withLock { - registrations.values.compactMap(\.taskCreated) - } - - for handler in handlers { + notifyHandlers(\.taskCreated) { handler in handler(task, session) } } private func notifyTaskCompleted(task: URLSessionTask, error: Error?) { + notifyHandlers(\.taskCompleted) { handler in + handler(task, error) + } + } + + private func notifyHandlers( + _ keyPath: KeyPath, + invoke: (Handler) -> Void + ) { let handlers = lock.withLock { - registrations.values.compactMap(\.taskCompleted) + registrations.values.compactMap { $0[keyPath: keyPath] } } for handler in handlers { - handler(task, error) + invoke(handler) } } } diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index 7a91d399fe..2c2b0f9711 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -16,8 +16,7 @@ class PostHogReplayIntegration: PostHogIntegration { var requiresSwizzling: Bool { true } - private static var integrationInstalledLock = NSLock() - private static var integrationInstalled = false + private static let integrationInstallState = PostHogIntegrationInstallState() private var config: PostHogConfig? { postHog?.config @@ -145,54 +144,40 @@ } func install(_ postHog: PostHogSDK) -> PostHogIntegrationInstallResult { - let didInstall = PostHogReplayIntegration.integrationInstalledLock.withLock { - if PostHogReplayIntegration.integrationInstalled { - return false - } - PostHogReplayIntegration.integrationInstalled = true - return true - } - - guard didInstall else { - return .skipped(.alreadyInstalled) - } + installIfNeeded(using: Self.integrationInstallState) { + self.postHog = postHog + replayQueue = postHog.replayQueue - self.postHog = postHog - replayQueue = postHog.replayQueue + // Wire up as buffer delegate for the replay queue + replayQueue?.bufferDelegate = self - // Wire up as buffer delegate for the replay queue - replayQueue?.bufferDelegate = self + // Resolve event triggers and minimum duration from cached remote config (if available) + if let cachedRemoteConfig = postHog.remoteConfig?.getRemoteConfig() { + updateEventTriggers(from: cachedRemoteConfig) + } + updateCachedMinimumDuration() - // Resolve event triggers and minimum duration from cached remote config (if available) - if let cachedRemoteConfig = postHog.remoteConfig?.getRemoteConfig() { - updateEventTriggers(from: cachedRemoteConfig) - } - updateCachedMinimumDuration() + // Subscribe to remote config changes (needed before start to update triggers) + remoteConfigLoadedToken = postHog.remoteConfig?.onRemoteConfigLoaded.subscribe { [weak self] config in + self?.applyRemoteConfig(remoteConfig: config) + } - // Subscribe to remote config changes (needed before start to update triggers) - remoteConfigLoadedToken = postHog.remoteConfig?.onRemoteConfigLoaded.subscribe { [weak self] config in - self?.applyRemoteConfig(remoteConfig: config) - } + // Subscribe to event captures for trigger matching (needed before start to detect triggers) + eventCapturedToken = postHog.onEventCaptured.subscribe { [weak self] event in + self?.handleEventCaptured(event: event.event) + } - // Subscribe to event captures for trigger matching (needed before start to detect triggers) - eventCapturedToken = postHog.onEventCaptured.subscribe { [weak self] event in - self?.handleEventCaptured(event: event.event) + start() } - - start() - return .installed } func uninstall(_ postHog: PostHogSDK) { - if self.postHog === postHog || self.postHog == nil { + uninstallIfNeeded(from: postHog, installedPostHog: self.postHog, state: Self.integrationInstallState) { stop() // Clear the pre-start listeners remoteConfigLoadedToken = nil eventCapturedToken = nil self.postHog = nil - PostHogReplayIntegration.integrationInstalledLock.withLock { - PostHogReplayIntegration.integrationInstalled = false - } // Clear buffer delegate replayQueue?.bufferDelegate = nil @@ -432,16 +417,17 @@ } private func pauseAllPlugins() { - let pluginsToPause = installedPluginsLock.withLock { installedPlugins } - for plugin in pluginsToPause { - plugin.pause() - } + updateAllPlugins { $0.pause() } } private func resumeAllPlugins() { - let pluginsToResume = installedPluginsLock.withLock { installedPlugins } - for plugin in pluginsToResume { - plugin.resume() + updateAllPlugins { $0.resume() } + } + + private func updateAllPlugins(_ update: (PostHogSessionReplayPlugin) -> Void) { + let plugins = installedPluginsLock.withLock { installedPlugins } + for plugin in plugins { + update(plugin) } } @@ -1377,9 +1363,7 @@ #if TESTING extension PostHogReplayIntegration { static func clearInstalls() { - integrationInstalledLock.withLock { - integrationInstalled = false - } + integrationInstallState.clear() } } #endif diff --git a/PostHog/Replay/PostHogReplayQueue.swift b/PostHog/Replay/PostHogReplayQueue.swift index c5d086b380..0fc99bee19 100644 --- a/PostHog/Replay/PostHogReplayQueue.swift +++ b/PostHog/Replay/PostHogReplayQueue.swift @@ -90,14 +90,9 @@ class PostHogReplayQueue { /// If called from the main thread, migration is offloaded to a utility queue /// to avoid blocking rendering. On background threads migration executes inline. func migrateBufferToQueue() { - if Thread.isMainThread { - bufferIOQueue.async { [weak self] in - self?.migrateBufferToQueueNow() - } - return + performBufferOperation { [weak self] in + self?.migrateBufferToQueueNow() } - - migrateBufferToQueueNow() } /// Discards all buffered replay events. @@ -105,14 +100,18 @@ class PostHogReplayQueue { /// If called from the main thread, clear is offloaded to a utility queue to /// avoid blocking rendering. On background threads clear executes inline. func clearBuffer() { + performBufferOperation { [weak self] in + self?.clearBufferNow() + } + } + + private func performBufferOperation(_ operation: @escaping () -> Void) { if Thread.isMainThread { - bufferIOQueue.async { [weak self] in - self?.clearBufferNow() - } + bufferIOQueue.async(execute: operation) return } - clearBufferNow() + operation() } private func migrateBufferToQueueNow() { diff --git a/PostHog/Replay/UIImage+Util.swift b/PostHog/Replay/UIImage+Util.swift index 33ce1f0f39..53ea3cab7b 100644 --- a/PostHog/Replay/UIImage+Util.swift +++ b/PostHog/Replay/UIImage+Util.swift @@ -17,15 +17,15 @@ } private func toWebPBase64(_ compressionQuality: CGFloat) -> String? { - webpData(compressionQuality: compressionQuality).map { data in - "data:image/webp;base64,\(data.base64EncodedString())" - } + toImageBase64(mimeType: "webp", data: webpData(compressionQuality: compressionQuality)) } private func toJpegBase64(_ compressionQuality: CGFloat) -> String? { - jpegData(compressionQuality: compressionQuality).map { data in - "data:image/jpeg;base64,\(data.base64EncodedString())" - } + toImageBase64(mimeType: "jpeg", data: jpegData(compressionQuality: compressionQuality)) + } + + private func toImageBase64(mimeType: String, data: Data?) -> String? { + data.map { "data:image/\(mimeType);base64,\($0.base64EncodedString())" } } } diff --git a/PostHog/ScreenViews/PostHogScreenViewIntegration.swift b/PostHog/ScreenViews/PostHogScreenViewIntegration.swift index 5802bf88da..cfa432eb50 100644 --- a/PostHog/ScreenViews/PostHogScreenViewIntegration.swift +++ b/PostHog/ScreenViews/PostHogScreenViewIntegration.swift @@ -10,38 +10,23 @@ import Foundation final class PostHogScreenViewIntegration: PostHogIntegration { var requiresSwizzling: Bool { true } - private static var integrationInstalledLock = NSLock() - private static var integrationInstalled = false + private static let integrationInstallState = PostHogIntegrationInstallState() private weak var postHog: PostHogSDK? func install(_ postHog: PostHogSDK) -> PostHogIntegrationInstallResult { - let didInstall = PostHogScreenViewIntegration.integrationInstalledLock.withLock { - if PostHogScreenViewIntegration.integrationInstalled { - return false - } - PostHogScreenViewIntegration.integrationInstalled = true - return true - } + installIfNeeded(using: Self.integrationInstallState) { + self.postHog = postHog - guard didInstall else { - return .skipped(.alreadyInstalled) + start() } - - self.postHog = postHog - - start() - return .installed } func uninstall(_ postHog: PostHogSDK) { - // uninstall only for integration instance - if self.postHog === postHog || self.postHog == nil { + uninstallIfNeeded(from: postHog, installedPostHog: self.postHog, state: Self.integrationInstallState) { + // uninstall only for integration instance stop() self.postHog = nil - PostHogScreenViewIntegration.integrationInstalledLock.withLock { - PostHogScreenViewIntegration.integrationInstalled = false - } } } @@ -63,9 +48,7 @@ final class PostHogScreenViewIntegration: PostHogIntegration { #if TESTING extension PostHogScreenViewIntegration { static func clearInstalls() { - integrationInstalledLock.withLock { - integrationInstalled = false - } + integrationInstallState.clear() } } #endif diff --git a/PostHog/Surveys/PostHogSurveyIntegration.swift b/PostHog/Surveys/PostHogSurveyIntegration.swift index 051fe942de..af39ea88b7 100644 --- a/PostHog/Surveys/PostHogSurveyIntegration.swift +++ b/PostHog/Surveys/PostHogSurveyIntegration.swift @@ -15,8 +15,7 @@ final class PostHogSurveyIntegration: PostHogIntegration { var requiresSwizzling: Bool { true } - private static var integrationInstalledLock = NSLock() - private static var integrationInstalled = false + private static let integrationInstallState = PostHogIntegrationInstallState() typealias SurveyCallback = (_ surveys: [PostHogSurvey]) -> Void @@ -53,30 +52,16 @@ private var activeSurveyQuestionIndex: Int = 0 func install(_ postHog: PostHogSDK) -> PostHogIntegrationInstallResult { - let didInstall = PostHogSurveyIntegration.integrationInstalledLock.withLock { - if PostHogSurveyIntegration.integrationInstalled { - return false - } - PostHogSurveyIntegration.integrationInstalled = true - return true - } - - guard didInstall else { - return .skipped(.alreadyInstalled) + installIfNeeded(using: Self.integrationInstallState) { + self.postHog = postHog + start() } - - self.postHog = postHog - start() - return .installed } func uninstall(_ postHog: PostHogSDK) { - if self.postHog === postHog || self.postHog == nil { + uninstallIfNeeded(from: postHog, installedPostHog: self.postHog, state: Self.integrationInstallState) { stop() self.postHog = nil - PostHogSurveyIntegration.integrationInstalledLock.withLock { - PostHogSurveyIntegration.integrationInstalled = false - } } } @@ -208,12 +193,12 @@ forceReload: Bool = false, callback: (([String: Any]?) -> Void)? = nil ) { - let cached = remoteConfig.getRemoteConfig() - if cached == nil || forceReload { - remoteConfig.reloadRemoteConfig(callback: callback) - } else { - callback?(cached) - } + getCachedOrReload( + getCached: remoteConfig.getRemoteConfig, + reload: { remoteConfig.reloadRemoteConfig(callback: $0) }, + forceReload: forceReload, + callback: callback + ) } private func getFeatureFlags( @@ -221,9 +206,23 @@ forceReload: Bool = false, callback: (([String: Any]?) -> Void)? = nil ) { - let cached = remoteConfig.getFeatureFlags() + getCachedOrReload( + getCached: remoteConfig.getFeatureFlags, + reload: { remoteConfig.reloadFeatureFlags(callback: $0) }, + forceReload: forceReload, + callback: callback + ) + } + + private func getCachedOrReload( + getCached: () -> [String: Any]?, + reload: ((([String: Any]?) -> Void)?) -> Void, + forceReload: Bool, + callback: (([String: Any]?) -> Void)? + ) { + let cached = getCached() if cached == nil || forceReload { - remoteConfig.reloadFeatureFlags(callback: callback) + reload(callback) } else { callback?(cached) } @@ -1149,9 +1148,7 @@ } static func clearInstalls() { - integrationInstalledLock.withLock { - integrationInstalled = false - } + integrationInstallState.clear() } } #endif diff --git a/PostHog/Surveys/QuestionTypes.swift b/PostHog/Surveys/QuestionTypes.swift index fc9add6cc3..e6ac7fc5df 100644 --- a/PostHog/Surveys/QuestionTypes.swift +++ b/PostHog/Surveys/QuestionTypes.swift @@ -203,13 +203,11 @@ } private var hasOpenChoiceSelected: Bool { - guard let openChoice else { return false } - return selectedChoices.contains(openChoice) + isOpenChoiceSelected(openChoice, in: selectedChoices) } private var openChoice: String? { - guard question.hasOpenChoice == true else { return nil } - return question.choices.last + openChoiceOption(for: question) } } @@ -253,13 +251,21 @@ } private var hasOpenChoiceSelected: Bool { - guard let openChoice else { return false } - return selectedChoices.contains(openChoice) + isOpenChoiceSelected(openChoice, in: selectedChoices) } private var openChoice: String? { - guard question.hasOpenChoice == true else { return nil } - return question.choices.last + openChoiceOption(for: question) } } + + private func isOpenChoiceSelected(_ openChoice: String?, in selectedChoices: Set) -> Bool { + guard let openChoice else { return false } + return selectedChoices.contains(openChoice) + } + + private func openChoiceOption(for question: PostHogDisplayChoiceQuestion) -> String? { + guard question.hasOpenChoice == true else { return nil } + return question.choices.last + } #endif diff --git a/PostHog/Surveys/Utils/EmojiRating.swift b/PostHog/Surveys/Utils/EmojiRating.swift index d3bda39170..ca03214e96 100644 --- a/PostHog/Surveys/Utils/EmojiRating.swift +++ b/PostHog/Surveys/Utils/EmojiRating.swift @@ -86,11 +86,11 @@ } private func foregroundColor(selected: Bool) -> Color { - if selected { - return ratingButtonActiveColor.getContrastingTextColor() - } else { - return inputTextColor.opacity(0.5) - } + surveyRatingForegroundColor( + selected: selected, + activeColor: ratingButtonActiveColor, + inputTextColor: inputTextColor + ) } private var ratingButtonActiveColor: Color { diff --git a/PostHog/Surveys/Utils/NumberRating.swift b/PostHog/Surveys/Utils/NumberRating.swift index 0346542d9c..b5abbf2cd5 100644 --- a/PostHog/Surveys/Utils/NumberRating.swift +++ b/PostHog/Surveys/Utils/NumberRating.swift @@ -65,11 +65,11 @@ } private func foregroundTextColor(selected: Bool) -> Color { - if selected { - return ratingButtonActiveColor.getContrastingTextColor() - } else { - return inputTextColor.opacity(0.5) - } + surveyRatingForegroundColor( + selected: selected, + activeColor: ratingButtonActiveColor, + inputTextColor: inputTextColor + ) } private var ratingButtonColor: Color { diff --git a/PostHog/Surveys/Utils/SwiftUI+Util.swift b/PostHog/Surveys/Utils/SwiftUI+Util.swift index 5773836e55..530974af30 100644 --- a/PostHog/Surveys/Utils/SwiftUI+Util.swift +++ b/PostHog/Surveys/Utils/SwiftUI+Util.swift @@ -41,6 +41,15 @@ } } + @available(iOS 15.0, *) + func surveyRatingForegroundColor(selected: Bool, activeColor: Color, inputTextColor: Color) -> Color { + if selected { + return activeColor.getContrastingTextColor() + } else { + return inputTextColor.opacity(0.5) + } + } + @available(iOS 14.0, *) private struct ReadFrameModifier: ViewModifier { /// Helper for notifying parents for child view frame changes diff --git a/PostHog/SwiftUI/PostHogTagViewModifier.swift b/PostHog/SwiftUI/PostHogTagViewModifier.swift index 81ebac0bf0..910ece7f2f 100644 --- a/PostHog/SwiftUI/PostHogTagViewModifier.swift +++ b/PostHog/SwiftUI/PostHogTagViewModifier.swift @@ -307,8 +307,7 @@ } func updateUIView(_ uiView: PostHogTagUIView, context _: Context) { - uiView.postHogView = true - uiView.superview?.postHogView = true + markPostHogView(uiView) } static func dismantleUIView(_ uiView: PostHogTagUIView, coordinator: Coordinator) { @@ -452,11 +451,15 @@ } func updateUIView(_ uiView: UIViewType, context _: Context) { - uiView.postHogView = true - uiView.superview?.postHogView = true + markPostHogView(uiView) } } + private func markPostHogView(_ view: UIView) { + view.postHogView = true + view.superview?.postHogView = true + } + private class PostHogTagAnchorUIView: UIView { let id: UUID diff --git a/PostHog/Tracing/PostHogTracingHeadersIntegration.swift b/PostHog/Tracing/PostHogTracingHeadersIntegration.swift index 74e0223b9f..296f7ebede 100644 --- a/PostHog/Tracing/PostHogTracingHeadersIntegration.swift +++ b/PostHog/Tracing/PostHogTracingHeadersIntegration.swift @@ -48,40 +48,25 @@ final class PostHogTracingHeadersIntegration: PostHogIntegration { var requiresSwizzling: Bool { true } - private static let integrationInstalledLock = NSLock() - private static var integrationInstalled = false + private static let integrationInstallState = PostHogIntegrationInstallState() private weak var postHog: PostHogSDK? private var registrationId: UUID? private var normalizedHostnames = Set() func install(_ postHog: PostHogSDK) -> PostHogIntegrationInstallResult { - let didInstall = Self.integrationInstalledLock.withLock { - if Self.integrationInstalled { - return false - } - Self.integrationInstalled = true - return true + installIfNeeded(using: Self.integrationInstallState) { + self.postHog = postHog + normalizedHostnames = PostHogTracingHeaders.normalizeHostnames(postHog.config.tracingHeaders ?? []) + start() } - - guard didInstall else { - return .skipped(.alreadyInstalled) - } - - self.postHog = postHog - normalizedHostnames = PostHogTracingHeaders.normalizeHostnames(postHog.config.tracingHeaders ?? []) - start() - return .installed } func uninstall(_ postHog: PostHogSDK) { - if self.postHog === postHog || self.postHog == nil { + uninstallIfNeeded(from: postHog, installedPostHog: self.postHog, state: Self.integrationInstallState) { stop() self.postHog = nil normalizedHostnames = [] - Self.integrationInstalledLock.withLock { - Self.integrationInstalled = false - } } } @@ -129,9 +114,7 @@ #if TESTING extension PostHogTracingHeadersIntegration { static func clearInstalls() { - integrationInstalledLock.withLock { - integrationInstalled = false - } + integrationInstallState.clear() } } #endif