From 75dec40335528e895a25df85730fc5b2fb5e56aa Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 21 Feb 2026 04:30:52 -0800 Subject: [PATCH] Add app version/build properties to PostHog DAU events (#256) --- Sources/PostHogAnalytics.swift | 47 +++++++++++++++++++++++++---- cmuxTests/GhosttyConfigTests.swift | 48 ++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/Sources/PostHogAnalytics.swift b/Sources/PostHogAnalytics.swift index 2d181bcf..091870d8 100644 --- a/Sources/PostHogAnalytics.swift +++ b/Sources/PostHogAnalytics.swift @@ -39,8 +39,9 @@ final class PostHogAnalytics { PostHogSDK.shared.setup(config) - // Tag every event so PostHog can distinguish desktop from web. - PostHogSDK.shared.register(["platform": "cmuxterm"]) + // Tag every event so PostHog can distinguish desktop from web and + // break events down by released app version/build. + PostHogSDK.shared.register(Self.superProperties(infoDictionary: Bundle.main.infoDictionary ?? [:])) // The SDK automatically generates and persists an anonymous distinct ID. @@ -68,10 +69,14 @@ final class PostHogAnalytics { defaults.set(today, forKey: lastActiveDayUTCKey) - PostHogSDK.shared.capture("cmux_daily_active", properties: [ - "day_utc": today, - "reason": reason, - ]) + PostHogSDK.shared.capture( + "cmux_daily_active", + properties: Self.dailyActiveProperties( + dayUTC: today, + reason: reason, + infoDictionary: Bundle.main.infoDictionary ?? [:] + ) + ) // For DAU we care more about delivery than batching. PostHogSDK.shared.flush() @@ -90,4 +95,34 @@ final class PostHogAnalytics { formatter.dateFormat = "yyyy-MM-dd" return formatter.string(from: date) } + + nonisolated static func superProperties(infoDictionary: [String: Any]) -> [String: Any] { + var properties: [String: Any] = ["platform": "cmuxterm"] + properties.merge(versionProperties(infoDictionary: infoDictionary)) { _, new in new } + return properties + } + + nonisolated static func dailyActiveProperties( + dayUTC: String, + reason: String, + infoDictionary: [String: Any] + ) -> [String: Any] { + var properties: [String: Any] = [ + "day_utc": dayUTC, + "reason": reason, + ] + properties.merge(versionProperties(infoDictionary: infoDictionary)) { _, new in new } + return properties + } + + nonisolated private static func versionProperties(infoDictionary: [String: Any]) -> [String: Any] { + var properties: [String: Any] = [:] + if let value = infoDictionary["CFBundleShortVersionString"] as? String, !value.isEmpty { + properties["app_version"] = value + } + if let value = infoDictionary["CFBundleVersion"] as? String, !value.isEmpty { + properties["app_build"] = value + } + return properties + } } diff --git a/cmuxTests/GhosttyConfigTests.swift b/cmuxTests/GhosttyConfigTests.swift index cd81e89e..386c7131 100644 --- a/cmuxTests/GhosttyConfigTests.swift +++ b/cmuxTests/GhosttyConfigTests.swift @@ -436,3 +436,51 @@ final class SocketControlSettingsTests: XCTestCase { ) } } + +final class PostHogAnalyticsPropertiesTests: XCTestCase { + func testDailyActivePropertiesIncludeVersionAndBuild() { + let properties = PostHogAnalytics.dailyActiveProperties( + dayUTC: "2026-02-21", + reason: "didBecomeActive", + infoDictionary: [ + "CFBundleShortVersionString": "0.31.0", + "CFBundleVersion": "230", + ] + ) + + XCTAssertEqual(properties["day_utc"] as? String, "2026-02-21") + XCTAssertEqual(properties["reason"] as? String, "didBecomeActive") + XCTAssertEqual(properties["app_version"] as? String, "0.31.0") + XCTAssertEqual(properties["app_build"] as? String, "230") + } + + func testSuperPropertiesIncludePlatformVersionAndBuild() { + let properties = PostHogAnalytics.superProperties( + infoDictionary: [ + "CFBundleShortVersionString": "0.31.0", + "CFBundleVersion": "230", + ] + ) + + XCTAssertEqual(properties["platform"] as? String, "cmuxterm") + XCTAssertEqual(properties["app_version"] as? String, "0.31.0") + XCTAssertEqual(properties["app_build"] as? String, "230") + } + + func testPropertiesOmitVersionFieldsWhenUnavailable() { + let superProperties = PostHogAnalytics.superProperties(infoDictionary: [:]) + XCTAssertEqual(superProperties["platform"] as? String, "cmuxterm") + XCTAssertNil(superProperties["app_version"]) + XCTAssertNil(superProperties["app_build"]) + + let dailyProperties = PostHogAnalytics.dailyActiveProperties( + dayUTC: "2026-02-21", + reason: "activeTimer", + infoDictionary: [:] + ) + XCTAssertEqual(dailyProperties["day_utc"] as? String, "2026-02-21") + XCTAssertEqual(dailyProperties["reason"] as? String, "activeTimer") + XCTAssertNil(dailyProperties["app_version"]) + XCTAssertNil(dailyProperties["app_build"]) + } +}