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.

Revisions

  1. julianfbeck created this gist Aug 18, 2025.
    130 changes: 130 additions & 0 deletions Plausible.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,130 @@
    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
    }
    }