Skip to content

Commit ce78cca

Browse files
committed
Refactor lifecycle handling
1 parent f604cef commit ce78cca

File tree

5 files changed

+299
-155
lines changed

5 files changed

+299
-155
lines changed

Sources/Segment/Builtins.swift

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//
2+
// Builtins.swift
3+
// Segment
4+
//
5+
// Created by Brandon Sneed on 10/31/25.
6+
//
7+
8+
import Foundation
9+
10+
extension Analytics {
11+
internal static let versionKey = "SEGVersionKey"
12+
internal static let buildKey = "SEGBuildKeyV2"
13+
14+
internal static var appCurrentVersion: String {
15+
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
16+
}
17+
18+
internal static var appCurrentBuild: String {
19+
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
20+
}
21+
22+
public func checkAndTrackInstallOrUpdate() {
23+
let previousVersion = UserDefaults.standard.string(forKey: Self.versionKey)
24+
let previousBuild = UserDefaults.standard.string(forKey: Self.buildKey)
25+
26+
if previousBuild == nil {
27+
// Fresh install
28+
if configuration.values.trackedApplicationLifecycleEvents.contains(.applicationInstalled) {
29+
trackApplicationInstalled(version: Self.appCurrentVersion, build: Self.appCurrentBuild)
30+
}
31+
} else if let previousBuild, Self.appCurrentBuild != previousBuild {
32+
// App was updated
33+
if configuration.values.trackedApplicationLifecycleEvents.contains(.applicationUpdated) {
34+
trackApplicationUpdated(
35+
previousVersion: previousVersion ?? "",
36+
previousBuild: previousBuild,
37+
version: Self.appCurrentVersion,
38+
build: Self.appCurrentBuild
39+
)
40+
}
41+
}
42+
43+
// Always update UserDefaults
44+
UserDefaults.standard.setValue(Self.appCurrentVersion, forKey: Self.versionKey)
45+
UserDefaults.standard.setValue(Self.appCurrentBuild, forKey: Self.buildKey)
46+
}
47+
48+
/// Tracks an Application Installed event.
49+
/// - Parameters:
50+
/// - version: The app version (e.g., "1.0.0")
51+
/// - build: The app build number (e.g., "42")
52+
public func trackApplicationInstalled(version: String, build: String) {
53+
track(name: "Application Installed", properties: [
54+
"version": version,
55+
"build": build
56+
])
57+
}
58+
59+
/// Tracks an Application Updated event.
60+
/// - Parameters:
61+
/// - previousVersion: The previous app version
62+
/// - previousBuild: The previous build number
63+
/// - version: The current app version
64+
/// - build: The current build number
65+
public func trackApplicationUpdated(previousVersion: String, previousBuild: String, version: String, build: String) {
66+
track(name: "Application Updated", properties: [
67+
"previous_version": previousVersion,
68+
"previous_build": previousBuild,
69+
"version": version,
70+
"build": build
71+
])
72+
}
73+
74+
/// Tracks an Application Opened event.
75+
/// - Parameters:
76+
/// - fromBackground: Whether the app was opened from background (true) or cold start (false)
77+
/// - url: The URL that opened the app, if any
78+
/// - referringApp: The bundle ID of the app that referred this open, if any
79+
public func trackApplicationOpened(fromBackground: Bool, url: String? = nil, referringApp: String? = nil) {
80+
var properties: [String: Any] = [
81+
"from_background": fromBackground,
82+
"version": Self.appCurrentVersion,
83+
"build": Self.appCurrentBuild
84+
]
85+
86+
if let url = url {
87+
properties["url"] = url
88+
}
89+
90+
if let referringApp = referringApp {
91+
properties["referring_application"] = referringApp
92+
}
93+
94+
track(name: "Application Opened", properties: properties)
95+
}
96+
97+
/// Tracks an Application Backgrounded event.
98+
public func trackApplicationBackgrounded() {
99+
track(name: "Application Backgrounded")
100+
}
101+
102+
/// Tracks an Application Foregrounded event.
103+
public func trackApplicationForegrounded() {
104+
track(name: "Application Foregrounded")
105+
}
106+
}
107+
108+
#if os(macOS)
109+
110+
extension Analytics {
111+
/// Tracks an Application Hidden event (macOS only).
112+
public func trackApplicationHidden() {
113+
track(name: "Application Hidden")
114+
}
115+
116+
/// Tracks an Application Unhidden event (macOS only).
117+
/// - Parameters:
118+
/// - version: The app version (defaults to current version)
119+
/// - build: The app build (defaults to current build)
120+
public func trackApplicationUnhidden(version: String? = nil, build: String? = nil) {
121+
track(name: "Application Unhidden", properties: [
122+
"from_background": true,
123+
"version": version ?? Self.appCurrentVersion,
124+
"build": build ?? Self.appCurrentBuild
125+
])
126+
}
127+
128+
/// Tracks an Application Terminated event (macOS only).
129+
public func trackApplicationTerminated() {
130+
track(name: "Application Terminated")
131+
}
132+
}
133+
134+
#endif
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//
2+
// EventDebugger.swift
3+
// Segment
4+
//
5+
// Created by Brandon Sneed on 11/1/25.
6+
//
7+
8+
import Foundation
9+
import OSLog
10+
11+
public class EventDebugger: EventPlugin {
12+
public var type: PluginType = .after
13+
public weak var analytics: Analytics? = nil
14+
15+
/// If true, prints full event JSON. If false, prints compact summary.
16+
public var verbose: Bool = false
17+
18+
private let logger: OSLog
19+
20+
required public init() {
21+
self.logger = OSLog(subsystem: "com.segment.analytics", category: "events")
22+
}
23+
24+
public func identify(event: IdentifyEvent) -> IdentifyEvent? {
25+
log(event: event, dot: "🟣", type: "Analytics.IDENTIFY")
26+
return event
27+
}
28+
29+
public func track(event: TrackEvent) -> TrackEvent? {
30+
log(event: event, dot: "🔵", type: "Analytics.TRACK")
31+
return event
32+
}
33+
34+
public func group(event: GroupEvent) -> GroupEvent? {
35+
log(event: event, dot: "🟡", type: "Analytics.GROUP")
36+
return event
37+
}
38+
39+
public func alias(event: AliasEvent) -> AliasEvent? {
40+
log(event: event, dot: "🟢", type: "Analytics.ALIAS")
41+
return event
42+
}
43+
44+
public func screen(event: ScreenEvent) -> ScreenEvent? {
45+
log(event: event, dot: "🟠", type: "Analytics.SCREEN")
46+
return event
47+
}
48+
49+
public func reset() {
50+
os_log("🔴 [Analytics.RESET]", log: logger, type: .info)
51+
}
52+
53+
public func flush() {
54+
os_log("⚪ [Analytics.FLUSH]", log: logger, type: .info)
55+
}
56+
57+
// MARK: - Private Helpers
58+
59+
private func log(event: RawEvent, dot: String, type: String) {
60+
if verbose {
61+
logVerbose(event: event, dot: dot, type: type)
62+
} else {
63+
logCompact(event: event, dot: dot, type: type)
64+
}
65+
}
66+
67+
private func logCompact(event: RawEvent, dot: String, type: String) {
68+
var summary = "\(dot) [\(type)]"
69+
70+
// Add event-specific details
71+
if let track = event as? TrackEvent {
72+
summary += " \(track.event)"
73+
} else if let screen = event as? ScreenEvent {
74+
summary += " \(screen.name ?? screen.category ?? "Screen")"
75+
} else if let identify = event as? IdentifyEvent {
76+
summary += " userId: \(identify.userId ?? "nil")"
77+
} else if let group = event as? GroupEvent {
78+
summary += " groupId: \(group.groupId ?? "nil")"
79+
} else if let alias = event as? AliasEvent {
80+
summary += " \(alias.previousId ?? "nil")\(alias.userId ?? "nil")"
81+
}
82+
83+
os_log("%{public}@", log: logger, type: .debug, summary)
84+
}
85+
86+
private func logVerbose(event: RawEvent, dot: String, type: String) {
87+
// Pretty-print the JSON
88+
let encoder = JSONEncoder()
89+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
90+
91+
if let data = try? encoder.encode(event),
92+
let jsonString = String(data: data, encoding: .utf8) {
93+
os_log("%{public}@ [%{public}@]\n%{public}@",
94+
log: logger,
95+
type: .debug,
96+
dot,
97+
type,
98+
jsonString)
99+
} else {
100+
os_log("%{public}@ [%{public}@] Failed to encode event",
101+
log: logger,
102+
type: .error,
103+
dot,
104+
type)
105+
}
106+
}
107+
}

Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,75 +5,45 @@
55
// Created by Cody on 4/20/22.
66
//
77

8-
import Foundation
9-
108
#if os(macOS)
119

10+
import Foundation
1211
import Cocoa
1312

1413
class macOSLifecycleEvents: PlatformPlugin, macOSLifecycle {
15-
static var versionKey = "SEGVersionKey"
16-
static var buildKey = "SEGBuildKeyV2"
17-
1814
let type = PluginType.before
1915
weak var analytics: Analytics?
2016

21-
/// Since application:didFinishLaunchingWithOptions is not automatically called with Scenes / SwiftUI,
22-
/// this gets around by using a flag in user defaults to check for big events like application updating,
23-
/// being installed or even opening.
2417
@Atomic
2518
private var didFinishLaunching = false
2619

27-
func application(didFinishLaunchingWithOptions launchOptions: [String : Any]?) {
28-
// Make sure we aren't double calling application:didFinishLaunchingWithOptions
29-
// by resetting the check at the start
30-
_didFinishLaunching.set(true)
31-
32-
let previousVersion = UserDefaults.standard.string(forKey: Self.versionKey)
33-
let previousBuild = UserDefaults.standard.string(forKey: Self.buildKey)
34-
35-
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
36-
let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
20+
@Atomic
21+
private var didCheckInstallOrUpdate = false
22+
23+
func configure(analytics: Analytics) {
24+
self.analytics = analytics
3725

38-
if previousBuild == nil {
39-
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationInstalled) == true {
40-
analytics?.track(name: "Application Installed", properties: [
41-
"version": currentVersion ?? "",
42-
"build": currentBuild ?? ""
43-
])
44-
}
45-
} else if currentBuild != previousBuild {
46-
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationUpdated) == true {
47-
analytics?.track(name: "Application Updated", properties: [
48-
"previous_version": previousVersion ?? "",
49-
"previous_build": previousBuild ?? "",
50-
"version": currentVersion ?? "",
51-
"build": currentBuild ?? ""
52-
])
53-
}
26+
// Check install/update immediately to catch first launch
27+
if !didCheckInstallOrUpdate {
28+
analytics.checkAndTrackInstallOrUpdate()
29+
_didCheckInstallOrUpdate.set(true)
5430
}
31+
}
32+
33+
func application(didFinishLaunchingWithOptions launchOptions: [String : Any]?) {
34+
_didFinishLaunching.set(true)
5535

5636
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationOpened) == true {
57-
analytics?.track(name: "Application Opened", properties: [
58-
"from_background": false,
59-
"version": currentVersion ?? "",
60-
"build": currentBuild ?? ""
61-
])
37+
analytics?.trackApplicationOpened(fromBackground: false)
6238
}
63-
64-
UserDefaults.standard.setValue(currentVersion, forKey: Self.versionKey)
65-
UserDefaults.standard.setValue(currentBuild, forKey: Self.buildKey)
6639
}
6740

6841
func applicationDidUnhide() {
6942
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationUnhidden) == true {
70-
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
71-
let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
72-
7343
analytics?.track(name: "Application Unhidden", properties: [
7444
"from_background": true,
75-
"version": currentVersion ?? "",
76-
"build": currentBuild ?? ""
45+
"version": Analytics.appCurrentVersion,
46+
"build": Analytics.appCurrentBuild
7747
])
7848
}
7949
}
@@ -83,17 +53,17 @@ class macOSLifecycleEvents: PlatformPlugin, macOSLifecycle {
8353
analytics?.track(name: "Application Hidden")
8454
}
8555
}
56+
8657
func applicationDidResignActive() {
8758
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationBackgrounded) == true {
88-
analytics?.track(name: "Application Backgrounded")
59+
analytics?.trackApplicationBackgrounded()
8960
}
9061
}
9162

9263
func applicationDidBecomeActive() {
93-
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationForegrounded) == false {
94-
return
64+
if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationForegrounded) == true {
65+
analytics?.trackApplicationForegrounded()
9566
}
96-
analytics?.track(name: "Application Foregrounded")
9767

9868
// Lets check if we skipped application:didFinishLaunchingWithOptions,
9969
// if so, lets call it.

0 commit comments

Comments
 (0)