diff --git a/Sources/Segment/Analytics.swift b/Sources/Segment/Analytics.swift index c04dc9a2..2e646d6b 100644 --- a/Sources/Segment/Analytics.swift +++ b/Sources/Segment/Analytics.swift @@ -111,16 +111,6 @@ public class Analytics { _ = timeline.process(incomingEvent: event) - /*let flushPolicies = configuration.values.flushPolicies - for policy in flushPolicies { - policy.updateState(event: event) - - if (policy.shouldFlush() == true) { - flush() - policy.reset() - } - }*/ - let flushPolicies = configuration.values.flushPolicies var shouldFlush = false diff --git a/Sources/Segment/Builtins.swift b/Sources/Segment/Builtins.swift new file mode 100644 index 00000000..f5ec5023 --- /dev/null +++ b/Sources/Segment/Builtins.swift @@ -0,0 +1,134 @@ +// +// Builtins.swift +// Segment +// +// Created by Brandon Sneed on 10/31/25. +// + +import Foundation + +extension Analytics { + internal static let versionKey = "SEGVersionKey" + internal static let buildKey = "SEGBuildKeyV2" + + internal static var appCurrentVersion: String { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + } + + internal static var appCurrentBuild: String { + Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" + } + + public func checkAndTrackInstallOrUpdate() { + let previousVersion = UserDefaults.standard.string(forKey: Self.versionKey) + let previousBuild = UserDefaults.standard.string(forKey: Self.buildKey) + + if previousBuild == nil { + // Fresh install + if configuration.values.trackedApplicationLifecycleEvents.contains(.applicationInstalled) { + trackApplicationInstalled(version: Self.appCurrentVersion, build: Self.appCurrentBuild) + } + } else if let previousBuild, Self.appCurrentBuild != previousBuild { + // App was updated + if configuration.values.trackedApplicationLifecycleEvents.contains(.applicationUpdated) { + trackApplicationUpdated( + previousVersion: previousVersion ?? "", + previousBuild: previousBuild, + version: Self.appCurrentVersion, + build: Self.appCurrentBuild + ) + } + } + + // Always update UserDefaults + UserDefaults.standard.setValue(Self.appCurrentVersion, forKey: Self.versionKey) + UserDefaults.standard.setValue(Self.appCurrentBuild, forKey: Self.buildKey) + } + + /// Tracks an Application Installed event. + /// - Parameters: + /// - version: The app version (e.g., "1.0.0") + /// - build: The app build number (e.g., "42") + public func trackApplicationInstalled(version: String, build: String) { + track(name: "Application Installed", properties: [ + "version": version, + "build": build + ]) + } + + /// Tracks an Application Updated event. + /// - Parameters: + /// - previousVersion: The previous app version + /// - previousBuild: The previous build number + /// - version: The current app version + /// - build: The current build number + public func trackApplicationUpdated(previousVersion: String, previousBuild: String, version: String, build: String) { + track(name: "Application Updated", properties: [ + "previous_version": previousVersion, + "previous_build": previousBuild, + "version": version, + "build": build + ]) + } + + /// Tracks an Application Opened event. + /// - Parameters: + /// - fromBackground: Whether the app was opened from background (true) or cold start (false) + /// - url: The URL that opened the app, if any + /// - referringApp: The bundle ID of the app that referred this open, if any + public func trackApplicationOpened(fromBackground: Bool, url: String? = nil, referringApp: String? = nil) { + var properties: [String: Any] = [ + "from_background": fromBackground, + "version": Self.appCurrentVersion, + "build": Self.appCurrentBuild + ] + + if let url = url { + properties["url"] = url + } + + if let referringApp = referringApp { + properties["referring_application"] = referringApp + } + + track(name: "Application Opened", properties: properties) + } + + /// Tracks an Application Backgrounded event. + public func trackApplicationBackgrounded() { + track(name: "Application Backgrounded") + } + + /// Tracks an Application Foregrounded event. + public func trackApplicationForegrounded() { + track(name: "Application Foregrounded") + } +} + +#if os(macOS) + +extension Analytics { + /// Tracks an Application Hidden event (macOS only). + public func trackApplicationHidden() { + track(name: "Application Hidden") + } + + /// Tracks an Application Unhidden event (macOS only). + /// - Parameters: + /// - version: The app version (defaults to current version) + /// - build: The app build (defaults to current build) + public func trackApplicationUnhidden(version: String? = nil, build: String? = nil) { + track(name: "Application Unhidden", properties: [ + "from_background": true, + "version": version ?? Self.appCurrentVersion, + "build": build ?? Self.appCurrentBuild + ]) + } + + /// Tracks an Application Terminated event (macOS only). + public func trackApplicationTerminated() { + track(name: "Application Terminated") + } +} + +#endif diff --git a/Sources/Segment/Plugins/EventDebugger.swift b/Sources/Segment/Plugins/EventDebugger.swift new file mode 100644 index 00000000..67b425a9 --- /dev/null +++ b/Sources/Segment/Plugins/EventDebugger.swift @@ -0,0 +1,107 @@ +// +// EventDebugger.swift +// Segment +// +// Created by Brandon Sneed on 11/1/25. +// + +import Foundation +import OSLog + +public class EventDebugger: EventPlugin { + public var type: PluginType = .after + public weak var analytics: Analytics? = nil + + /// If true, prints full event JSON. If false, prints compact summary. + public var verbose: Bool = false + + private let logger: OSLog + + required public init() { + self.logger = OSLog(subsystem: "com.segment.analytics", category: "events") + } + + public func identify(event: IdentifyEvent) -> IdentifyEvent? { + log(event: event, dot: "🟣", type: "Analytics.IDENTIFY") + return event + } + + public func track(event: TrackEvent) -> TrackEvent? { + log(event: event, dot: "🔵", type: "Analytics.TRACK") + return event + } + + public func group(event: GroupEvent) -> GroupEvent? { + log(event: event, dot: "🟡", type: "Analytics.GROUP") + return event + } + + public func alias(event: AliasEvent) -> AliasEvent? { + log(event: event, dot: "🟢", type: "Analytics.ALIAS") + return event + } + + public func screen(event: ScreenEvent) -> ScreenEvent? { + log(event: event, dot: "🟠", type: "Analytics.SCREEN") + return event + } + + public func reset() { + os_log("🔴 [Analytics.RESET]", log: logger, type: .info) + } + + public func flush() { + os_log("⚪ [Analytics.FLUSH]", log: logger, type: .info) + } + + // MARK: - Private Helpers + + private func log(event: RawEvent, dot: String, type: String) { + if verbose { + logVerbose(event: event, dot: dot, type: type) + } else { + logCompact(event: event, dot: dot, type: type) + } + } + + private func logCompact(event: RawEvent, dot: String, type: String) { + var summary = "\(dot) [\(type)]" + + // Add event-specific details + if let track = event as? TrackEvent { + summary += " \(track.event)" + } else if let screen = event as? ScreenEvent { + summary += " \(screen.name ?? screen.category ?? "Screen")" + } else if let identify = event as? IdentifyEvent { + summary += " userId: \(identify.userId ?? "nil")" + } else if let group = event as? GroupEvent { + summary += " groupId: \(group.groupId ?? "nil")" + } else if let alias = event as? AliasEvent { + summary += " \(alias.previousId ?? "nil") → \(alias.userId ?? "nil")" + } + + os_log("%{public}@", log: logger, type: .debug, summary) + } + + private func logVerbose(event: RawEvent, dot: String, type: String) { + // Pretty-print the JSON + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + + if let data = try? encoder.encode(event), + let jsonString = String(data: data, encoding: .utf8) { + os_log("%{public}@ [%{public}@]\n%{public}@", + log: logger, + type: .debug, + dot, + type, + jsonString) + } else { + os_log("%{public}@ [%{public}@] Failed to encode event", + log: logger, + type: .error, + dot, + type) + } + } +} diff --git a/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift b/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift index 0640fe9c..9ba218fd 100644 --- a/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift +++ b/Sources/Segment/Plugins/Platforms/Mac/macOSLifecycleEvents.swift @@ -5,75 +5,45 @@ // Created by Cody on 4/20/22. // -import Foundation - #if os(macOS) +import Foundation import Cocoa class macOSLifecycleEvents: PlatformPlugin, macOSLifecycle { - static var versionKey = "SEGVersionKey" - static var buildKey = "SEGBuildKeyV2" - let type = PluginType.before weak var analytics: Analytics? - /// Since application:didFinishLaunchingWithOptions is not automatically called with Scenes / SwiftUI, - /// this gets around by using a flag in user defaults to check for big events like application updating, - /// being installed or even opening. @Atomic private var didFinishLaunching = false - func application(didFinishLaunchingWithOptions launchOptions: [String : Any]?) { - // Make sure we aren't double calling application:didFinishLaunchingWithOptions - // by resetting the check at the start - _didFinishLaunching.set(true) - - let previousVersion = UserDefaults.standard.string(forKey: Self.versionKey) - let previousBuild = UserDefaults.standard.string(forKey: Self.buildKey) - - let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + @Atomic + private var didCheckInstallOrUpdate = false + + func configure(analytics: Analytics) { + self.analytics = analytics - if previousBuild == nil { - if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationInstalled) == true { - analytics?.track(name: "Application Installed", properties: [ - "version": currentVersion ?? "", - "build": currentBuild ?? "" - ]) - } - } else if currentBuild != previousBuild { - if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationUpdated) == true { - analytics?.track(name: "Application Updated", properties: [ - "previous_version": previousVersion ?? "", - "previous_build": previousBuild ?? "", - "version": currentVersion ?? "", - "build": currentBuild ?? "" - ]) - } + // Check install/update immediately to catch first launch + if !didCheckInstallOrUpdate { + analytics.checkAndTrackInstallOrUpdate() + _didCheckInstallOrUpdate.set(true) } + } + + func application(didFinishLaunchingWithOptions launchOptions: [String : Any]?) { + _didFinishLaunching.set(true) if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationOpened) == true { - analytics?.track(name: "Application Opened", properties: [ - "from_background": false, - "version": currentVersion ?? "", - "build": currentBuild ?? "" - ]) + analytics?.trackApplicationOpened(fromBackground: false) } - - UserDefaults.standard.setValue(currentVersion, forKey: Self.versionKey) - UserDefaults.standard.setValue(currentBuild, forKey: Self.buildKey) } func applicationDidUnhide() { if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationUnhidden) == true { - let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String - analytics?.track(name: "Application Unhidden", properties: [ "from_background": true, - "version": currentVersion ?? "", - "build": currentBuild ?? "" + "version": Analytics.appCurrentVersion, + "build": Analytics.appCurrentBuild ]) } } @@ -83,17 +53,17 @@ class macOSLifecycleEvents: PlatformPlugin, macOSLifecycle { analytics?.track(name: "Application Hidden") } } + func applicationDidResignActive() { if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationBackgrounded) == true { - analytics?.track(name: "Application Backgrounded") + analytics?.trackApplicationBackgrounded() } } func applicationDidBecomeActive() { - if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationForegrounded) == false { - return + if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationForegrounded) == true { + analytics?.trackApplicationForegrounded() } - analytics?.track(name: "Application Foregrounded") // Lets check if we skipped application:didFinishLaunchingWithOptions, // if so, lets call it. diff --git a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift index 478ee0a2..bf20419e 100644 --- a/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift +++ b/Sources/Segment/Plugins/Platforms/iOS/iOSLifecycleEvents.swift @@ -12,85 +12,45 @@ import Foundation import UIKit class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle { - static var versionKey = "SEGVersionKey" - static var buildKey = "SEGBuildKeyV2" - let type = PluginType.before weak var analytics: Analytics? - /// Since application:didFinishLaunchingWithOptions is not automatically called with Scenes / SwiftUI, - /// this gets around by using a flag in user defaults to check for big events like application updating, - /// being installed or even opening. - @Atomic - private var didFinishLaunching = false + @Atomic private var didFinishLaunching = false + @Atomic private var wasBackgrounded = false + @Atomic private var didCheckInstallOrUpdate = false - @Atomic - private var wasBackgrounded = false + func configure(analytics: Analytics) { + self.analytics = analytics + + // Check install/update immediately to catch first launch + if !didCheckInstallOrUpdate { + analytics.checkAndTrackInstallOrUpdate() + _didCheckInstallOrUpdate.set(true) + } + } func application(_ application: UIApplication?, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { - // Make sure we aren't double calling application:didFinishLaunchingWithOptions - // by resetting the check at the start _didFinishLaunching.set(true) - let previousVersion: String? = UserDefaults.standard.string(forKey: Self.versionKey) - let previousBuild: String? = UserDefaults.standard.string(forKey: Self.buildKey) - - let currentVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" - let currentBuild: String = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "" - - if previousBuild == nil { - if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationInstalled) == true { - analytics?.track(name: "Application Installed", properties: [ - "version": currentVersion, - "build": currentBuild - ]) - } - } else if let previousBuild, currentBuild != previousBuild { - if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationUpdated) == true { - analytics?.track(name: "Application Updated", properties: [ - "previous_version": previousVersion ?? "", - "previous_build": previousBuild, - "version": currentVersion, - "build": currentBuild - ]) - } - } - if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationOpened) == true { - let sourceApp: String = launchOptions?[UIApplication.LaunchOptionsKey.sourceApplication] as? String ?? "" + let sourceApp = launchOptions?[.sourceApplication] as? String ?? "" let url = urlFrom(launchOptions) - - analytics?.track(name: "Application Opened", properties: [ - "from_background": false, - "version": currentVersion, - "build": currentBuild, - "referring_application": sourceApp, - "url": url - ]) + + analytics?.trackApplicationOpened(fromBackground: false, url: url.isEmpty ? nil : url, referringApp: sourceApp.isEmpty ? nil : sourceApp) } - - UserDefaults.standard.setValue(currentVersion, forKey: Self.versionKey) - UserDefaults.standard.setValue(currentBuild, forKey: Self.buildKey) } func applicationWillEnterForeground(application: UIApplication?) { if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationOpened) == true { - let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String - if didFinishLaunching == false { - analytics?.track(name: "Application Opened", properties: [ - "from_background": true, - "version": currentVersion ?? "", - "build": currentBuild ?? "" - ]) + analytics?.trackApplicationOpened(fromBackground: true) } } // Only fire if we were actually backgrounded if wasBackgrounded { if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationForegrounded) == true { - analytics?.track(name: "Application Foregrounded") + analytics?.trackApplicationForegrounded() } _wasBackgrounded.set(false) } @@ -98,9 +58,12 @@ class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle { func applicationDidEnterBackground(application: UIApplication?) { _didFinishLaunching.set(false) - _wasBackgrounded.set(true) - if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationBackgrounded) == true { - analytics?.track(name: "Application Backgrounded") + if !wasBackgrounded { + _wasBackgrounded.set(true) + + if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationBackgrounded) == true { + analytics?.trackApplicationBackgrounded() + } } } @@ -109,10 +72,10 @@ class iOSLifecycleEvents: PlatformPlugin, iOSLifecycle { } private func urlFrom(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> String { - if let url = launchOptions?[UIApplication.LaunchOptionsKey.url] as? String { + if let url = launchOptions?[.url] as? String { return url } - if let url = launchOptions?[UIApplication.LaunchOptionsKey.url] as? NSURL, let rawUrl = url.absoluteString { + if let url = launchOptions?[.url] as? NSURL, let rawUrl = url.absoluteString { return rawUrl } return "" diff --git a/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift b/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift index 438369fe..eab1ba6e 100644 --- a/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift +++ b/Sources/Segment/Plugins/Platforms/watchOS/watchOSLifecycleEvents.swift @@ -11,69 +11,41 @@ import Foundation import WatchKit class watchOSLifecycleEvents: PlatformPlugin, watchOSLifecycle { - static var versionKey = "SEGVersionKey" - static var buildKey = "SEGBuildKeyV2" - let type = PluginType.before weak var analytics: Analytics? + @Atomic + private var didCheckInstallOrUpdate = false + + func configure(analytics: Analytics) { + self.analytics = analytics + + // Check install/update immediately to catch first launch + if !didCheckInstallOrUpdate { + analytics.checkAndTrackInstallOrUpdate() + _didCheckInstallOrUpdate.set(true) + } + } + func applicationDidFinishLaunching(watchExtension: WKExtension) { if analytics?.configuration.values.trackedApplicationLifecycleEvents == TrackedLifecycleEvent.none { return } - let previousVersion = UserDefaults.standard.string(forKey: Self.versionKey) - let previousBuild = UserDefaults.standard.string(forKey: Self.buildKey) - - let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String - - if previousBuild == nil { - if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationInstalled) == true { - analytics?.track(name: "Application Installed", properties: [ - "version": currentVersion ?? "", - "build": currentBuild ?? "" - ]) - } - } else if currentBuild != previousBuild { - if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationUpdated) == true { - analytics?.track(name: "Application Updated", properties: [ - "previous_version": previousVersion ?? "", - "previous_build": previousBuild ?? "", - "version": currentVersion ?? "", - "build": currentBuild ?? "" - ]) - } - } - if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationOpened) == true { - analytics?.track(name: "Application Opened", properties: [ - "from_background": false, - "version": currentVersion ?? "", - "build": currentBuild ?? "" - ]) + analytics?.trackApplicationOpened(fromBackground: false) } - - UserDefaults.standard.setValue(currentVersion, forKey: Self.versionKey) - UserDefaults.standard.setValue(currentBuild, forKey: Self.buildKey) } func applicationWillEnterForeground(watchExtension: WKExtension) { if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationOpened) == true { - let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String - - analytics?.track(name: "Application Opened", properties: [ - "from_background": true, - "version": currentVersion ?? "", - "build": currentBuild ?? "" - ]) + analytics?.trackApplicationOpened(fromBackground: true) } } func applicationDidEnterBackground(watchExtension: WKExtension) { if analytics?.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationBackgrounded) == true { - analytics?.track(name: "Application Backgrounded") + analytics?.trackApplicationBackgrounded() } } } diff --git a/Sources/Segment/SwiftUISupport.swift b/Sources/Segment/SwiftUISupport.swift new file mode 100644 index 00000000..5fd303cd --- /dev/null +++ b/Sources/Segment/SwiftUISupport.swift @@ -0,0 +1,122 @@ +// +// SwiftUISupport.swift +// Segment +// +// Created by Brandon Sneed on 11/3/25. +// + +#if canImport(SwiftUI) +import SwiftUI + +@available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) +struct AnalyticsLifecycleModifier: ViewModifier { + let analytics: Analytics + @Environment(\.scenePhase) private var scenePhase + @State private var previousPhase: ScenePhase? + @State private var hasLaunched = false + + func body(content: Content) -> some View { + content + .onAppear { + handleInitialLaunch() + } + .onChange(of: scenePhase) { newPhase in + handlePhaseChange(newPhase) + } + } + + private func handleInitialLaunch() { + guard !hasLaunched else { return } + + // First launch - check install/update and track opened + analytics.checkAndTrackInstallOrUpdate() + + if analytics.configuration.values.trackedApplicationLifecycleEvents.contains(.applicationOpened) { + analytics.trackApplicationOpened(fromBackground: false) + } + + hasLaunched = true + previousPhase = scenePhase + } + + private func handlePhaseChange(_ newPhase: ScenePhase) { + defer { previousPhase = newPhase } + + // Only handle transitions after initial launch + guard hasLaunched else { return } + + let config = analytics.configuration.values.trackedApplicationLifecycleEvents + + switch (previousPhase, newPhase) { + case (.background, .active): + // Coming from background to foreground + if config.contains(.applicationOpened) { + analytics.trackApplicationOpened(fromBackground: true) + } + if config.contains(.applicationForegrounded) { + analytics.trackApplicationForegrounded() + } + + case (.active, .background), (.inactive, .background): + // Going to background + /// NOTE: this isn't needed because the regular lifecycle notifications capture this. + /*if config.contains(.applicationBackgrounded) { + analytics.trackApplicationBackgrounded() + }*/ + break + + case (.background, .inactive): + // Transitioning from background through inactive to active + // Don't track anything here, wait for .active + break + + case (.active, .inactive): + // Brief interruption (like control center pull-down) + // Don't track as background yet + break + + case (.inactive, .active): + // Resuming from brief interruption + // Don't track as opened from background + break + + default: + break + } + } +} + +@available(iOS 14.0, tvOS 14.0, watchOS 7.0, macOS 11.0, *) +public extension View { + /// Automatically tracks Analytics lifecycle events for SwiftUI apps. + /// + /// Attach this modifier to your root view to enable automatic tracking of: + /// - Application Installed (first launch only) + /// - Application Updated (when version/build changes) + /// - Application Opened (cold start and from background) + /// - Application Backgrounded + /// - Application Foregrounded + /// + /// Example: + /// ```swift + /// @main + /// struct MyApp: App { + /// let analytics = Analytics(configuration: config) + /// + /// var body: some Scene { + /// WindowGroup { + /// ContentView() + /// .trackAnalyticsLifecycle(analytics) + /// } + /// } + /// } + /// ``` + /// + /// - Parameter analytics: The Analytics instance to track events with + /// - Returns: A view with lifecycle tracking enabled + func trackAnalyticsLifecycle(_ analytics: Analytics) -> some View { + self.modifier(AnalyticsLifecycleModifier(analytics: analytics)) + } +} + +#endif