From aa01e820167973b042df7b65905b3b82740d7828 Mon Sep 17 00:00:00 2001 From: Tomas Srna Date: Sun, 19 Jul 2026 20:29:11 +0200 Subject: [PATCH 1/6] Use structured operatingSystemVersion for macOS user agent ProcessInfo.operatingSystemVersionString is localized and human-readable ("Version 14.5 (Build 23F79)"), so splitting on spaces and taking index 1 breaks on non-English locales. Build the OS version token from operatingSystemVersion's major/minor/patch components instead. Co-Authored-By: Claude Fable 5 --- Sources/OpenPanel/OpenPanel.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/OpenPanel/OpenPanel.swift b/Sources/OpenPanel/OpenPanel.swift index ecd90d0..9b9ae8d 100644 --- a/Sources/OpenPanel/OpenPanel.swift +++ b/Sources/OpenPanel/OpenPanel.swift @@ -72,12 +72,10 @@ internal class DeviceInfo { #endif private static func getMacOSUserAgent() -> String { - let processInfo = ProcessInfo.processInfo - let osVersion = processInfo.operatingSystemVersionString - let versionParts = osVersion.components(separatedBy: " ") - let version = versionParts.count > 1 ? versionParts[1] : "Unknown" - - let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X \(version.replacingOccurrences(of: ".", with: "_"))) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15" + let osVersion = ProcessInfo.processInfo.operatingSystemVersion + let version = "\(osVersion.majorVersion)_\(osVersion.minorVersion)_\(osVersion.patchVersion)" + + let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X \(version)) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15" return userAgent + " OpenPanel/\(OpenPanel.sdkVersion)" } From 83519cf5cec3e246cf705b15333b9259abe4b6f8 Mon Sep 17 00:00:00 2001 From: Tomas Srna Date: Sun, 19 Jul 2026 20:29:22 +0200 Subject: [PATCH 2/6] Align sdkVersion with release version sdkVersion still reported 0.0.1 while the released tag is 1.0.0. This string is sent in the openpanel-sdk-version header and embedded in every user agent, so bump it to the next release version, 1.0.1. Co-Authored-By: Claude Fable 5 --- Sources/OpenPanel/OpenPanel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenPanel/OpenPanel.swift b/Sources/OpenPanel/OpenPanel.swift index 9b9ae8d..a3b455c 100644 --- a/Sources/OpenPanel/OpenPanel.swift +++ b/Sources/OpenPanel/OpenPanel.swift @@ -332,7 +332,7 @@ public class OpenPanel { private var options: Options? public static var sdkVersion: String { - return "0.0.1" + return "1.0.1" } private init() { From 1934db438955f39fd5dc4c2e5a66458c04d66cbe Mon Sep 17 00:00:00 2001 From: Tomas Srna Date: Sun, 19 Jul 2026 20:29:37 +0200 Subject: [PATCH 3/6] Fix WKWebView user agent fallback never triggering The OpenPanel/ suffix was appended before the isEmpty check, so when the WKWebView JS evaluation failed or timed out the user agent became " OpenPanel/x.y.z" instead of falling back to getBasicUserAgent(). Check for the empty result first, then append the suffix (getBasicUserAgent already appends it itself). Co-Authored-By: Claude Fable 5 --- Sources/OpenPanel/OpenPanel.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/OpenPanel/OpenPanel.swift b/Sources/OpenPanel/OpenPanel.swift index a3b455c..75a6726 100644 --- a/Sources/OpenPanel/OpenPanel.swift +++ b/Sources/OpenPanel/OpenPanel.swift @@ -43,13 +43,11 @@ internal class DeviceInfo { _ = semaphore.wait(timeout: .now() + 1.0) - userAgent += " OpenPanel/\(OpenPanel.sdkVersion)" - if userAgent.isEmpty { - userAgent = getBasicUserAgent() + return getBasicUserAgent() } - - return userAgent + + return userAgent + " OpenPanel/\(OpenPanel.sdkVersion)" } else { return getBasicUserAgent() } From d86d00d3181f1998c6b5a21e414351e930fe4c23 Mon Sep 17 00:00:00 2001 From: Tomas Srna Date: Sun, 19 Jul 2026 20:30:03 +0200 Subject: [PATCH 4/6] Unwrap AnyCodable explicitly when merging identify properties (new as AnyObject).value resolved through AnyObject dynamic lookup, producing an Any?? that was implicitly coerced to Any (compiler warning) and left the merged value double-wrapped. Cast to AnyCodable and unwrap its underlying value instead. Co-Authored-By: Claude Fable 5 --- Sources/OpenPanel/OpenPanel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenPanel/OpenPanel.swift b/Sources/OpenPanel/OpenPanel.swift index 75a6726..e70645a 100644 --- a/Sources/OpenPanel/OpenPanel.swift +++ b/Sources/OpenPanel/OpenPanel.swift @@ -453,7 +453,7 @@ public class OpenPanel { if let global = shared._global { var mergedProperties = global if let payloadProperties = payload.properties { - mergedProperties.merge(payloadProperties) { (_, new) in (new as AnyObject).value } + mergedProperties.merge(payloadProperties) { (_, new) in (new as? AnyCodable)?.value ?? new } } updatedPayload.properties = mergedProperties.mapValues { AnyCodable($0) } } From f3aae89faad2d621cce7169eea5a0d142e13fa59 Mon Sep 17 00:00:00 2001 From: Tomas Srna Date: Sun, 19 Jul 2026 20:30:19 +0200 Subject: [PATCH 5/6] Synchronize access to the pending event queue send() appended to queue and flush() read/cleared it from arbitrary threads with no synchronization. Route both through the existing globalQueue barrier pattern: barrier-append in send(), atomic snapshot-and-clear in flush(). Co-Authored-By: Claude Fable 5 --- Sources/OpenPanel/OpenPanel.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/OpenPanel/OpenPanel.swift b/Sources/OpenPanel/OpenPanel.swift index e70645a..47b3f54 100644 --- a/Sources/OpenPanel/OpenPanel.swift +++ b/Sources/OpenPanel/OpenPanel.swift @@ -383,7 +383,9 @@ public class OpenPanel { } if options.waitForProfile == true, profileId == nil { - queue.append(payload) + globalQueue.async(flags: .barrier) { + self.queue.append(payload) + } return } @@ -482,8 +484,11 @@ public class OpenPanel { } public func flush() { - let currentQueue = queue - queue.removeAll() + let currentQueue = globalQueue.sync(flags: .barrier) { () -> [TrackHandlerPayload] in + let items = queue + queue.removeAll() + return items + } for item in currentQueue { send(item) } From 85ce8dda47739b764de401753c6ed5d65e43f19f Mon Sep 17 00:00:00 2001 From: Tomas Srna Date: Sun, 19 Jul 2026 20:31:21 +0200 Subject: [PATCH 6/6] Declare tvOS support in Package.swift The source already guards automatic tracking with #if os(iOS) || os(tvOS), but the platform was never declared and UIKit was only imported for iOS. Add .tvOS(.v13) and import UIKit on tvOS so the declared platforms match the code (WebKit is unavailable on tvOS, so the generic user agent path is used there). Co-Authored-By: Claude Fable 5 --- Package.swift | 3 ++- Sources/OpenPanel/OpenPanel.swift | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 8c1f4a1..b54da5f 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,8 @@ let package = Package( name: "OpenPanel", platforms: [ .iOS(.v13), - .macOS(.v10_15) + .macOS(.v10_15), + .tvOS(.v13) ], products: [ .library( diff --git a/Sources/OpenPanel/OpenPanel.swift b/Sources/OpenPanel/OpenPanel.swift index 47b3f54..57a0818 100644 --- a/Sources/OpenPanel/OpenPanel.swift +++ b/Sources/OpenPanel/OpenPanel.swift @@ -2,6 +2,8 @@ import Foundation #if os(iOS) import UIKit import WebKit +#elseif os(tvOS) +import UIKit #elseif os(macOS) import AppKit import WebKit