Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start generating UserAgent instead of relying on Webkit #341

Merged
merged 3 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 3 additions & 35 deletions Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -156,7 +136,7 @@ internal class watchOSVendorSystem: VendorSystem {
}

override var userAgent: String? {
return nil
return UserAgent.value
}

override var connection: ConnectionStatus {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Segment/Plugins/Platforms/Vendors/LinuxUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class LinuxVendorSystem: VendorSystem {
}

override var userAgent: String? {
return "unknown"
return UserAgent.value
}

override var connection: ConnectionStatus {
Expand Down
86 changes: 86 additions & 0 deletions Sources/Segment/Utilities/UserAgent.swift
Original file line number Diff line number Diff line change
@@ -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
}
}

34 changes: 34 additions & 0 deletions Tests/Segment-Tests/UserAgentTests.swift
Original file line number Diff line number Diff line change
@@ -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)")
}

}
Loading