diff --git a/Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift b/Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift index 2f7021b..901b5ac 100644 --- a/Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift +++ b/Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift @@ -13,13 +13,9 @@ import Foundation import SystemConfiguration import UIKit -#if !os(tvOS) -import WebKit -#endif internal class iOSVendorSystem: VendorSystem { private let device = UIDevice.current - @Atomic private static var asyncUserAgent: String? = nil override var manufacturer: String { return "Apple" @@ -72,23 +68,7 @@ internal class iOSVendorSystem: VendorSystem { } override var userAgent: String? { - #if !os(tvOS) - // BKS: It was discovered that on some platforms there can be a delay in retrieval. - // It has to be fetched on the main thread, so we've spun it off - // async and cache it when it comes back. - // Note that due to how the `@Atomic` wrapper works, this boolean check may pass twice or more - // times before the value is updated, fetching the user agent multiple times as the result. - // This is not a big deal as the `userAgent` value is not expected to change often. - if Self.asyncUserAgent == nil { - DispatchQueue.main.async { - Self.asyncUserAgent = WKWebView().value(forKey: "userAgent") as? String - } - } - return Self.asyncUserAgent - #else - // webkit isn't on tvos - return "unknown" - #endif + return UserAgent.value } override var connection: ConnectionStatus { @@ -156,7 +136,7 @@ internal class watchOSVendorSystem: VendorSystem { } override var userAgent: String? { - return nil + return UserAgent.value } override var connection: ConnectionStatus { @@ -207,7 +187,6 @@ import WebKit internal class MacOSVendorSystem: VendorSystem { private let device = ProcessInfo.processInfo - @Atomic private static var asyncUserAgent: String? = nil override var manufacturer: String { return "Apple" @@ -248,18 +227,7 @@ internal class MacOSVendorSystem: VendorSystem { } override var userAgent: String? { - // BKS: It was discovered that on some platforms there can be a delay in retrieval. - // It has to be fetched on the main thread, so we've spun it off - // async and cache it when it comes back. - // Note that due to how the `@Atomic` wrapper works, this boolean check may pass twice or more - // times before the value is updated, fetching the user agent multiple times as the result. - // This is not a big deal as the `userAgent` value is not expected to change often. - if Self.asyncUserAgent == nil { - DispatchQueue.main.async { - Self.asyncUserAgent = WKWebView().value(forKey: "userAgent") as? String - } - } - return Self.asyncUserAgent + return UserAgent.value } override var connection: ConnectionStatus { diff --git a/Sources/Segment/Plugins/Platforms/Vendors/LinuxUtils.swift b/Sources/Segment/Plugins/Platforms/Vendors/LinuxUtils.swift index 7475016..b2beef5 100644 --- a/Sources/Segment/Plugins/Platforms/Vendors/LinuxUtils.swift +++ b/Sources/Segment/Plugins/Platforms/Vendors/LinuxUtils.swift @@ -43,7 +43,7 @@ class LinuxVendorSystem: VendorSystem { } override var userAgent: String? { - return "unknown" + return UserAgent.value } override var connection: ConnectionStatus { diff --git a/Sources/Segment/Utilities/UserAgent.swift b/Sources/Segment/Utilities/UserAgent.swift new file mode 100644 index 0000000..60ab8a5 --- /dev/null +++ b/Sources/Segment/Utilities/UserAgent.swift @@ -0,0 +1,86 @@ +// +// UserAgent.swift +// +// +// Created by Brandon Sneed on 5/6/24. +// + +import Foundation + +#if os(iOS) || os(visionOS) +import UIKit +#endif + +// macOS: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)" +// iOS: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" +// iPad: "Mozilla/5.0 (iPad; CPU OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" +// visionOS: "Mozilla/5.0 (iPad; CPU OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" +// catalyst: "Mozilla/5.0 (iPad; CPU OS 14_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" +// appleTV: no-webkit +// watchOS: no-webkit +// linux: no-webkit + +internal struct UserAgent { + // Duplicate the app names that webkit uses on a given platform. + // Broken out in case they change in the future. + #if os(macOS) + private static let defaultWebKitAppName = "" + #elseif targetEnvironment(macCatalyst) + private static let defaultWebKitAppName = "Mobile/15E148" + #elseif os(iOS) + private static let defaultWebKitAppName = "Mobile/15E148" + #elseif os(visionOS) + private static let defaultWebKitAppName = "Mobile/15E148" + #else + private static let defaultWebKitAppName = "" + #endif + + internal static var _value: String = "" + + public static var value: String { + if _value.isEmpty { + _value = value(applicationName: defaultWebKitAppName) + } + return _value + } + + private static func version() -> String { + let v = ProcessInfo.processInfo.operatingSystemVersion + var result: String + if v.patchVersion > 0 { + result = "\(v.majorVersion)_\(v.minorVersion)_\(v.patchVersion)" + } else { + // webkit leaves the patch version off if it's zero. + result = "\(v.majorVersion)_\(v.minorVersion)" + } + return result + } + + public static func value(applicationName: String) -> String { + let separator: String = applicationName.isEmpty ? "" : " " + #if os(macOS) + // Webkit hard-codes the info if it's on mac desktop + return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)\(separator)\(applicationName)" + #elseif os(iOS) || os(visionOS) || targetEnvironment(macCatalyst) + var model = UIDevice.current.model + + // doing this just in case ... i don't have all these devices to test, only sims. + if model.contains("iPhone") { model = "iPhone" } + else if model.contains("iPad") { model = "iPad" } + // it's not one of the two above .. webkit defaults to iPad (ie: visionOS, catalyst), so use that instead of whatever we got. + else { model = "iPad" } + + let osVersion = Self.version() + #if os(iOS) + // ios likes to tell you it's an iphone twice. + return "Mozilla/5.0 (\(model); CPU \(model) OS \(osVersion) like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)\(separator)\(applicationName)" + #else + return "Mozilla/5.0 (\(model); CPU OS \(osVersion) like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)\(separator)\(applicationName)" + #endif + + #else + return "unknown" + #endif + } +} + diff --git a/Tests/Segment-Tests/UserAgentTests.swift b/Tests/Segment-Tests/UserAgentTests.swift new file mode 100644 index 0000000..072c6d7 --- /dev/null +++ b/Tests/Segment-Tests/UserAgentTests.swift @@ -0,0 +1,34 @@ +// +// UserAgentTests.swift +// +// +// Created by Brandon Sneed on 5/6/24. +// + +import XCTest +#if canImport(WebKit) +import WebKit +#endif +@testable import Segment + +final class UserAgentTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testUserAgent() throws { + #if canImport(WebKit) + let wkUserAgent = WKWebView().value(forKey: "userAgent") as! String + #else + let wkUserAgent = "unknown" + #endif + let userAgent = UserAgent.value + XCTAssertEqual(wkUserAgent, userAgent, "UserAgent's don't match! system: \(wkUserAgent), generated: \(userAgent)") + } + +}