diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 7730a2b..0000000 --- a/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "Alamofire", - "repositoryURL": "https://github.com/Alamofire/Alamofire.git", - "state": { - "branch": null, - "revision": "513364f870f6bfc468f9d2ff0a95caccc10044c5", - "version": "5.10.2" - } - } - ] - }, - "version": 1 -} diff --git a/Package.swift b/Package.swift index a09288a..1a43fbe 100644 --- a/Package.swift +++ b/Package.swift @@ -9,29 +9,10 @@ let package = Package( .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macOS(.v10_15) ], products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .library( - name: "SimpleAnalytics", - targets: ["SimpleAnalytics"]), + .library(name: "SimpleAnalytics", targets: ["SimpleAnalytics"]) ], - dependencies: [ - .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.8.1")), - ], - targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - .target( - name: "SimpleAnalytics", - dependencies: [ - .product( - name: "Alamofire", - package: "Alamofire" - ), - ], - path: "Sources"), - .testTarget( - name: "SimpleAnalyticsTests", - dependencies: ["SimpleAnalytics"]), + .target(name: "SimpleAnalytics", path: "Sources"), + .testTarget(name: "SimpleAnalyticsTests", dependencies: ["SimpleAnalytics"]) ] ) diff --git a/Sources/SimpleAnalytics/RequestDispatcher.swift b/Sources/SimpleAnalytics/RequestDispatcher.swift index 19d8e77..df71759 100644 --- a/Sources/SimpleAnalytics/RequestDispatcher.swift +++ b/Sources/SimpleAnalytics/RequestDispatcher.swift @@ -6,50 +6,19 @@ // import Foundation -import Alamofire internal struct RequestDispatcher { /// Sends the event to Simple Analytics /// - Parameter event: the event to dispatch static internal func sendEventRequest(event: Event) async throws { - return try await withCheckedThrowingContinuation { continuation in - AF.request("https://queue.simpleanalyticscdn.com/events", - method: .post, - parameters: event, - encoder: JSONParameterEncoder.default).responseData { response in - - switch(response.result) { - case .success(_): - continuation.resume() - case let .failure(error): - continuation.resume(throwing: self.handleError(error: error)) - } - } - } + guard let url = URL(string: "https://queue.simpleanalyticscdn.com/events") else { throw URLError(.badURL) } + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + let jsonData = try JSONEncoder().encode(event) + request.httpBody = jsonData + let (_, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } + guard (200...299).contains(httpResponse.statusCode) else { throw URLError(URLError.Code(rawValue: httpResponse.statusCode)) } } - - static private func handleError(error: AFError) -> Error { - if let underlyingError = error.underlyingError { - let nserror = underlyingError as NSError - let code = nserror.code - if code == NSURLErrorNotConnectedToInternet || - code == NSURLErrorTimedOut || - code == NSURLErrorInternationalRoamingOff || - code == NSURLErrorDataNotAllowed || - code == NSURLErrorCannotFindHost || - code == NSURLErrorCannotConnectToHost || - code == NSURLErrorNetworkConnectionLost - { - var userInfo = nserror.userInfo - userInfo[NSLocalizedDescriptionKey] = "Unable to connect to the server" - let currentError = NSError( - domain: nserror.domain, - code: code, - userInfo: userInfo - ) - return currentError - } - } - return error - } } diff --git a/Sources/SimpleAnalytics/SimpleAnalytics.swift b/Sources/SimpleAnalytics/SimpleAnalytics.swift index 36db11d..d8ab7f3 100644 --- a/Sources/SimpleAnalytics/SimpleAnalytics.swift +++ b/Sources/SimpleAnalytics/SimpleAnalytics.swift @@ -25,12 +25,11 @@ import Foundation final public class SimpleAnalytics: NSObject { /// The hostname of the website in Simple Analytics the tracking should be send to. Without `https://` let hostname: String - private let userAgent: String + private var userAgent: String? private let userLanguage: String private let userTimezone: String /// The last date a unique visit was tracked. private var visitDate: Date? - private let userAgentProvider = UserAgentProvider() private var sharedDefaultsSuiteName: String? /// Defines if the user is opted out. When set to `true`, all tracking will be skipped. This is persisted between sessions. @@ -47,7 +46,6 @@ final public class SimpleAnalytics: NSObject { /// - Parameter hostname: The hostname as found in SimpleAnalytics, without `https://` public init(hostname: String) { self.hostname = hostname - self.userAgent = userAgentProvider.userAgent self.userLanguage = Locale.current.identifier self.userTimezone = TimeZone.current.identifier self.visitDate = UserDefaults.standard.object(forKey: Keys.visitDateKey) as? Date @@ -58,7 +56,6 @@ final public class SimpleAnalytics: NSObject { /// - Parameter: sharedDefaultsSuiteName: When extensions (such as a main app and widget) have a set of sharedDefaults (using an App Group) that unique user can be counted once using this (instead of two or more times when using app and widget, etc.) public init(hostname: String, sharedDefaultsSuiteName: String) { self.hostname = hostname - self.userAgent = userAgentProvider.userAgent self.userLanguage = Locale.current.identifier self.userTimezone = TimeZone.current.identifier self.sharedDefaultsSuiteName = sharedDefaultsSuiteName @@ -96,6 +93,7 @@ final public class SimpleAnalytics: NSObject { guard !isOptedOut else { return } + let userAgent = try await getUserAgent() let event = Event( type: .pageview, hostname: hostname, @@ -115,6 +113,7 @@ final public class SimpleAnalytics: NSObject { guard !isOptedOut else { return } + let userAgent = try await getUserAgent() let event = Event( type: .event, hostname: hostname, @@ -188,6 +187,14 @@ final public class SimpleAnalytics: NSObject { } } + /// Get the cached userAgent or fetch a new one + internal func getUserAgent() async throws -> String { + if let userAgent { return userAgent } + let newUserAgent = try await UserAgentFetcher.fetch() + userAgent = newUserAgent + return newUserAgent + } + /// Keys used to store things in UserDefaults internal struct Keys { static let visitDateKey = "simpleanalytics.visitdate" diff --git a/Sources/SimpleAnalytics/UserAgentFetcher.swift b/Sources/SimpleAnalytics/UserAgentFetcher.swift new file mode 100644 index 0000000..93d34ea --- /dev/null +++ b/Sources/SimpleAnalytics/UserAgentFetcher.swift @@ -0,0 +1,21 @@ +// +// UserAgent.swift +// +// +// Created by Max Humber on 2025-01-02. + +import WebKit + +enum UserAgentFetcher { + @MainActor + static func fetch() async throws -> String { + let webView = WKWebView(frame: .zero) + let result = try await webView.evaluateJavaScript("navigator.userAgent") + guard let userAgent = result as? String else { throw UserAgentError.unableToFetchUserAgent } + return userAgent + } +} + +enum UserAgentError: Error { + case unableToFetchUserAgent +} diff --git a/Sources/SimpleAnalytics/UserAgentProvider.swift b/Sources/SimpleAnalytics/UserAgentProvider.swift deleted file mode 100644 index 38c5a77..0000000 --- a/Sources/SimpleAnalytics/UserAgentProvider.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// UserAgent.swift -// -// -// Created by Roel van der Kraan on 27/10/2023. - -import Foundation -import WebKit - -internal class UserAgentProvider { - /// Generates a useragent for the app that SimpleAnalytics is included in. Simple Analytics uses this user agent to determine - /// the device type. - /// - Returns: A useragent for the app. - let userAgent: String = WKWebView().value(forKey: "userAgent") as! String -} -