Repository navigation
A breaking release that makes every generated string match what the real browser sends, validates all input, and removes the DeviceKit dependency. See Migrating from 1.x in the README.
Breaking changes
- New value types.
APWebBrowserAgentBuilder→BrowserUserAgentandAPAppUserAgentBuilder→AppUserAgent. Both areSendable,Hashablevalue types conforming to the newUserAgentprotocol (value,description). Builders areBrowserUserAgent.Builder(device(_:),browser(_:),build()) andAppUserAgent.Builder. The old names remain asunavailablealiases that point at the replacements. - Building throws.
BrowserUserAgentthrowsUserAgentErrorfor unsupported browser/platform combinations, malformed versions or device models, and inconsistent versions;AppUserAgentthrows only for an empty app name.BrowserUserAgent.defaultis a non-throwing shortcut for the defaults. UAPlatformenum.UADeviceis nowplatform+osVersion+model; all formatting lives in one place and switches exhaustively overUAPlatform×BrowserType.UAPlatform.supportedBrowsers/supports(_:)expose the support matrix.- Slimmer protocols.
UABrowseriskind+version;version(for:),userAgentPlatformInfo(for:),usesSystemWebKitUserAgent,browserType,UADevice.deviceModelanduserAgentSystemInfo(for:)are gone.UAVersions.defaultVersion(for:on:)andBrowserUserAgent.browserVersionreplaceversion(for:). - Deterministic
IOSDevice(). It always reportsUAVersions.iOS; useIOSDevice.current(iOS only) for the running device.IOSDevice(osVersion:)now takes a non-optionalString. - Devices without knobs.
MacDevice,WindowsDeviceandLinuxDeviceno longer takeosVersion:— no browser reports it.AndroidDevice(deviceModel:osVersion:usesReducedPlatformToken:)→AndroidDevice(model:osVersion:); the reduced token is used exactly when no model is given. - App User-Agent format.
MyApp 1.0 (iOS; arm64; 27.0; B123)→ RFC 9110 product formMyApp/1.0 (iOS 27.0; iPhone17,1; build:B123).withPlatform/withPlatformArchitecture/withPlatformVersion/addPart→systemName/deviceModel/systemVersion/addingComment.platformArchitecturewas really the model identifier and is now nameddeviceModel. Values are percent-encoded instead of having characters stripped. - No
@MainActor.AppUserAgentandAppEnvironment.currentwork from any isolation domain. - No DeviceKit. The package has no dependencies (Dependabot PR #12 for DeviceKit is obsolete).
UAVersionsreorganised. Versions are stored unreduced. Renamed/removed:safari/safariMajor→safariMacOS(iOS Safari follows the device);chromeDesktop→chrome;edgeDesktop→edge;operaDesktop→opera;operaDesktopChrome/operaAndroidChrome→operaChromiumOffset/operaAndroidChromiumOffset;windowsNT→Frozen.windowsNT;Frozen.iOSFreezeIntroducedInSafari→Frozen.iOSFreezeMajor. New:firefoxIOS,lastVerified.- Platforms. Linux is now supported (no UIKit/DeviceKit), alongside iOS 17, macOS 14, tvOS 17, watchOS 10, visionOS 1.
Fixed
- The iOS OS-version freeze was decided by the Safari version (default 27), so an iOS 17 device was reported as
18_7_8by every WebKit browser. It is now decided by the device: iOS 26+ only. - The frozen iOS token is
18_7, not18_7_8. - Chrome on Android sent its full version; it is reduced to
Chrome/<major>.0.0.0like every Chromium platform since Chrome 110. - iOS versions were truncated to two components. WebKit now sends the full version (
17_4_1) before the freeze, and Chrome for iOS always sends exactly three (17_4_0,27_0_1). - Every default version was stale. Bumped to the current stable releases verified today (Chrome 156 / Android 156 / iOS 155, Firefox 157 / iOS 157.1, Edge 155 / Android 154 / iOS 153, Opera 137 / Android 102 / iOS 6.7, iOS 27.0.1), and a scheduled Version freshness workflow now fails when a default falls behind. Edge desktop and Android now send the reduced
Edg/<major>.0.0.0/EdgA/<major>.0.0.0(per Microsoft's documentation), and Firefox sendsFirefox/<major>.0even for point-release overrides. - Edge's embedded
Chrome/token fell back to the Safari major (Chrome/27.0.0.0) when the Edge version did not parse. Versions are validated now, so there is no fallback. - Embedded tokens did not follow overrides: Edge on Android always embedded the default Chrome version, Opera always embedded its default Chromium, and Edge/Opera on iOS embedded the default Safari
Version/regardless of the device. They now follow the Edge/Opera override and the device's iOS version. - Browser input was not validated: a version or Android model containing CR/LF,
;or)went straight into the header. All versions and models are validated and rejected withUserAgentError. - Impossible combinations (Safari on Android/Windows/Linux) silently returned an iOS Safari string. They throw
.unsupportedCombination. - The app User-Agent was not a valid RFC 9110 product token (
MyApp 1.0). It is nowMyApp/1.0, with CFNetwork-style percent-encoding. - Auto-detected app fields could not be omitted — passing
nilmeant "auto-detect".AppUserAgent.Builder.omitting(_:)removes any of them. - Main-actor use was inconsistent:
APAppUserAgentBuilder's initializer andBuilder.build()were@MainActorwhile the rest of the API was not, andIOSDevice()read DeviceKit'sUIDevicefrom any thread. Nothing is main-actor-bound any more.
Added
UserAgentError,UAPlatform,UserAgent,AppEnvironment(injectable auto-detection),OperaBrowser(chromiumVersion:),BrowserUserAgent.default,BrowserUserAgent.browserVersion.- Golden tests against vendor-published User-Agents (Chromium source, Microsoft Learn, MDN, WebKit), iOS freeze-boundary tests, combination matrix tests and injection tests; the suite runs on macOS, the iOS simulator and Linux.
- CI: pinned Xcode, iOS 17.4 minimum lane, tvOS/watchOS/visionOS build lanes, Linux (Swift 6.0 and 6.4) lane, SwiftLint (
--strict) lane, Codecovfail_ci_if_error, builds on tags, and the scheduled Version freshness workflow (scripts/check-versions.py).
Removed
- DeviceKit,
.ruby-version, the committedPackage.resolved, and dead SwiftLint rules (anyobject_protocol,inert_defer,unused_private_declaration, theidentifier_nameconfig for a disabled rule).