Skip to content

Instantly share code, notes, and snippets.

@julianfbeck
Created August 18, 2025 17:49
Show Gist options
  • Select an option

  • Save julianfbeck/babe0b27cdacd52dc2d8dc5fd8dd57fb to your computer and use it in GitHub Desktop.

Select an option

Save julianfbeck/babe0b27cdacd52dc2d8dc5fd8dd57fb to your computer and use it in GitHub Desktop.
import Foundation
import UIKit
public enum PlausibleError: Error {
case domainNotSet
case invalidDomain
case eventIsPageview
}
public class Plausible {
public private(set) var endpoint = ""
public private(set) var domain = ""
public static let shared = Plausible()
private let queue = DispatchQueue(label: "com.plausibleswift.queue", qos: .utility)
private let userDefaults = UserDefaults.standard
private let appOpenCountKey = "com.plausibleswift.appOpenCount"
private init() {}
public func configure(domain: String, endpoint: String) {
self.endpoint = endpoint
self.domain = domain
let appOpenCount = incrementAppOpenCount()
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
var deviceInfo = self.gatherDeviceInfo()
deviceInfo["app_open_count"] = String(appOpenCount)
self.trackEvent(event: "open", path: "/open", properties: deviceInfo)
if appOpenCount == 1 {
self.trackEvent(event: "install", path: "/install")
}
}
}
public func trackPageview(path: String, properties: [String: String] = [:]) {
queue.async { [weak self] in
guard let self = self, self.domain != "" else { return }
Task {
do {
try await self.plausibleRequest(name: "pageview", path: path, properties: properties)
} catch {
print("Plausible error: \(error)")
}
}
}
}
public func trackEvent(event: String, path: String, properties: [String: String] = [:]) {
queue.async { [weak self] in
guard let self = self, event != "pageview" else { return }
Task {
do {
try await self.plausibleRequest(name: event, path: path, properties: properties)
} catch {
print("Plausible error: \(error)")
}
}
}
}
private func plausibleRequest(name: String, path: String, properties: [String: String]) async throws {
guard let plausibleEventURL = URL(string: self.endpoint) else {
throw PlausibleError.invalidDomain
}
var req = URLRequest(url: plausibleEventURL)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
var jsonObject: [String: Any] = ["name": name, "url": constructPageviewURL(path: path), "domain": domain]
if !properties.isEmpty {
jsonObject["props"] = properties
}
let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject)
req.httpBody = jsonData
do {
let (_, _) = try await URLSession.shared.data(for: req)
} catch {
print("Plausible network error: \(error)")
}
}
private func constructPageviewURL(path: String) -> String {
let url = URL(string: "https://\(domain)")!
return url.appendingPathComponent(path).absoluteString
}
private func gatherDeviceInfo() -> [String: String] {
let device = UIDevice.current
let screenSize = UIScreen.main.bounds.size
let locale = Locale.current
var info: [String: String] = [
"os_version": device.systemVersion,
"device_model": device.model,
"device_name": device.name,
"screen_width": String(format: "%.0f", screenSize.width),
"screen_height": String(format: "%.0f", screenSize.height),
"locale": locale.identifier,
"language": locale.languageCode ?? "unknown"
]
if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
info["app_version"] = appVersion
}
if let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
info["build_number"] = buildNumber
}
return info
}
private func incrementAppOpenCount() -> Int {
let currentCount = userDefaults.integer(forKey: appOpenCountKey)
let newCount = currentCount + 1
userDefaults.set(newCount, forKey: appOpenCountKey)
return newCount
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment