Skip to content

Commit 3b2093c

Browse files
authored
Start generating UserAgent instead of relying on Webkit (#341)
* Added user-agent sim and tests * Updated vendor files to pull from UserAgent. * Removed webkit dependency
1 parent eded4ac commit 3b2093c

File tree

4 files changed

+124
-36
lines changed

4 files changed

+124
-36
lines changed

Sources/Segment/Plugins/Platforms/Vendors/AppleUtils.swift

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,9 @@ import Foundation
1313

1414
import SystemConfiguration
1515
import UIKit
16-
#if !os(tvOS)
17-
import WebKit
18-
#endif
1916

2017
internal class iOSVendorSystem: VendorSystem {
2118
private let device = UIDevice.current
22-
@Atomic private static var asyncUserAgent: String? = nil
2319

2420
override var manufacturer: String {
2521
return "Apple"
@@ -72,23 +68,7 @@ internal class iOSVendorSystem: VendorSystem {
7268
}
7369

7470
override var userAgent: String? {
75-
#if !os(tvOS)
76-
// BKS: It was discovered that on some platforms there can be a delay in retrieval.
77-
// It has to be fetched on the main thread, so we've spun it off
78-
// async and cache it when it comes back.
79-
// Note that due to how the `@Atomic` wrapper works, this boolean check may pass twice or more
80-
// times before the value is updated, fetching the user agent multiple times as the result.
81-
// This is not a big deal as the `userAgent` value is not expected to change often.
82-
if Self.asyncUserAgent == nil {
83-
DispatchQueue.main.async {
84-
Self.asyncUserAgent = WKWebView().value(forKey: "userAgent") as? String
85-
}
86-
}
87-
return Self.asyncUserAgent
88-
#else
89-
// webkit isn't on tvos
90-
return "unknown"
91-
#endif
71+
return UserAgent.value
9272
}
9373

9474
override var connection: ConnectionStatus {
@@ -156,7 +136,7 @@ internal class watchOSVendorSystem: VendorSystem {
156136
}
157137

158138
override var userAgent: String? {
159-
return nil
139+
return UserAgent.value
160140
}
161141

162142
override var connection: ConnectionStatus {
@@ -207,7 +187,6 @@ import WebKit
207187

208188
internal class MacOSVendorSystem: VendorSystem {
209189
private let device = ProcessInfo.processInfo
210-
@Atomic private static var asyncUserAgent: String? = nil
211190

212191
override var manufacturer: String {
213192
return "Apple"
@@ -248,18 +227,7 @@ internal class MacOSVendorSystem: VendorSystem {
248227
}
249228

250229
override var userAgent: String? {
251-
// BKS: It was discovered that on some platforms there can be a delay in retrieval.
252-
// It has to be fetched on the main thread, so we've spun it off
253-
// async and cache it when it comes back.
254-
// Note that due to how the `@Atomic` wrapper works, this boolean check may pass twice or more
255-
// times before the value is updated, fetching the user agent multiple times as the result.
256-
// This is not a big deal as the `userAgent` value is not expected to change often.
257-
if Self.asyncUserAgent == nil {
258-
DispatchQueue.main.async {
259-
Self.asyncUserAgent = WKWebView().value(forKey: "userAgent") as? String
260-
}
261-
}
262-
return Self.asyncUserAgent
230+
return UserAgent.value
263231
}
264232

265233
override var connection: ConnectionStatus {

Sources/Segment/Plugins/Platforms/Vendors/LinuxUtils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class LinuxVendorSystem: VendorSystem {
4343
}
4444

4545
override var userAgent: String? {
46-
return "unknown"
46+
return UserAgent.value
4747
}
4848

4949
override var connection: ConnectionStatus {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// UserAgent.swift
3+
//
4+
//
5+
// Created by Brandon Sneed on 5/6/24.
6+
//
7+
8+
import Foundation
9+
10+
#if os(iOS) || os(visionOS)
11+
import UIKit
12+
#endif
13+
14+
// macOS: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)"
15+
// iOS: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
16+
// iPad: "Mozilla/5.0 (iPad; CPU OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
17+
// visionOS: "Mozilla/5.0 (iPad; CPU OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
18+
// catalyst: "Mozilla/5.0 (iPad; CPU OS 14_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
19+
// appleTV: no-webkit
20+
// watchOS: no-webkit
21+
// linux: no-webkit
22+
23+
internal struct UserAgent {
24+
// Duplicate the app names that webkit uses on a given platform.
25+
// Broken out in case they change in the future.
26+
#if os(macOS)
27+
private static let defaultWebKitAppName = ""
28+
#elseif targetEnvironment(macCatalyst)
29+
private static let defaultWebKitAppName = "Mobile/15E148"
30+
#elseif os(iOS)
31+
private static let defaultWebKitAppName = "Mobile/15E148"
32+
#elseif os(visionOS)
33+
private static let defaultWebKitAppName = "Mobile/15E148"
34+
#else
35+
private static let defaultWebKitAppName = ""
36+
#endif
37+
38+
internal static var _value: String = ""
39+
40+
public static var value: String {
41+
if _value.isEmpty {
42+
_value = value(applicationName: defaultWebKitAppName)
43+
}
44+
return _value
45+
}
46+
47+
private static func version() -> String {
48+
let v = ProcessInfo.processInfo.operatingSystemVersion
49+
var result: String
50+
if v.patchVersion > 0 {
51+
result = "\(v.majorVersion)_\(v.minorVersion)_\(v.patchVersion)"
52+
} else {
53+
// webkit leaves the patch version off if it's zero.
54+
result = "\(v.majorVersion)_\(v.minorVersion)"
55+
}
56+
return result
57+
}
58+
59+
public static func value(applicationName: String) -> String {
60+
let separator: String = applicationName.isEmpty ? "" : " "
61+
#if os(macOS)
62+
// Webkit hard-codes the info if it's on mac desktop
63+
return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)\(separator)\(applicationName)"
64+
#elseif os(iOS) || os(visionOS) || targetEnvironment(macCatalyst)
65+
var model = UIDevice.current.model
66+
67+
// doing this just in case ... i don't have all these devices to test, only sims.
68+
if model.contains("iPhone") { model = "iPhone" }
69+
else if model.contains("iPad") { model = "iPad" }
70+
// it's not one of the two above .. webkit defaults to iPad (ie: visionOS, catalyst), so use that instead of whatever we got.
71+
else { model = "iPad" }
72+
73+
let osVersion = Self.version()
74+
#if os(iOS)
75+
// ios likes to tell you it's an iphone twice.
76+
return "Mozilla/5.0 (\(model); CPU \(model) OS \(osVersion) like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)\(separator)\(applicationName)"
77+
#else
78+
return "Mozilla/5.0 (\(model); CPU OS \(osVersion) like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)\(separator)\(applicationName)"
79+
#endif
80+
81+
#else
82+
return "unknown"
83+
#endif
84+
}
85+
}
86+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// UserAgentTests.swift
3+
//
4+
//
5+
// Created by Brandon Sneed on 5/6/24.
6+
//
7+
8+
import XCTest
9+
#if canImport(WebKit)
10+
import WebKit
11+
#endif
12+
@testable import Segment
13+
14+
final class UserAgentTests: XCTestCase {
15+
16+
override func setUpWithError() throws {
17+
// Put setup code here. This method is called before the invocation of each test method in the class.
18+
}
19+
20+
override func tearDownWithError() throws {
21+
// Put teardown code here. This method is called after the invocation of each test method in the class.
22+
}
23+
24+
func testUserAgent() throws {
25+
#if canImport(WebKit)
26+
let wkUserAgent = WKWebView().value(forKey: "userAgent") as! String
27+
#else
28+
let wkUserAgent = "unknown"
29+
#endif
30+
let userAgent = UserAgent.value
31+
XCTAssertEqual(wkUserAgent, userAgent, "UserAgent's don't match! system: \(wkUserAgent), generated: \(userAgent)")
32+
}
33+
34+
}

0 commit comments

Comments
 (0)