Event Tracking Patterns for Mobile Analytics

Learn proven event tracking patterns for mobile analytics, including naming conventions, property schemas, and type-safe tracking APIs in Swift.

technical7 min readBy Klivvr Engineering
Share:

Event tracking is the lifeblood of mobile analytics. Every tap, scroll, purchase, and error generates data that drives product decisions. But without disciplined tracking patterns, analytics data quickly becomes an unreliable mess of inconsistent event names, missing properties, and duplicated signals. The difference between useful analytics and noise often comes down to the patterns you establish for defining, naming, and firing events.

This article covers the event tracking patterns we have refined while building KlivvrAnalyticsKit, with a focus on type safety, consistency, and maintainability in Swift codebases.

Type-Safe Event Definitions

The most common analytics bug is a typo in an event name or property key. String-based tracking APIs are a liability at scale. Swift's type system gives us the tools to eliminate entire categories of tracking errors at compile time.

// Define events as structured types, not strings
protocol TrackableEvent {
    var eventName: String { get }
    var properties: [String: Any] { get }
}
 
// Specific event definitions
struct ScreenViewedEvent: TrackableEvent {
    let screenName: String
    let screenClass: String
    let referrer: String?
 
    var eventName: String { "screen_viewed" }
 
    var properties: [String: Any] {
        var props: [String: Any] = [
            "screen_name": screenName,
            "screen_class": screenClass
        ]
        if let referrer {
            props["referrer"] = referrer
        }
        return props
    }
}
 
struct PurchaseCompletedEvent: TrackableEvent {
    let productId: String
    let productName: String
    let price: Decimal
    let currency: String
    let quantity: Int
 
    var eventName: String { "purchase_completed" }
 
    var properties: [String: Any] {
        [
            "product_id": productId,
            "product_name": productName,
            "price": NSDecimalNumber(decimal: price).doubleValue,
            "currency": currency,
            "quantity": quantity
        ]
    }
}
 
// Type-safe tracking API
extension KlivvrAnalytics {
    func track(_ event: TrackableEvent) {
        track(event.eventName, properties: event.properties)
    }
}
 
// Usage - compile-time safety
KlivvrAnalytics.shared.track(PurchaseCompletedEvent(
    productId: "SKU-12345",
    productName: "Premium Subscription",
    price: 9.99,
    currency: "USD",
    quantity: 1
))

This pattern makes it impossible to send a purchase event without a product ID. The compiler enforces your tracking plan.

Event Naming Conventions

Consistent naming is more important than clever naming. We follow a strict convention: object-action format in snake_case, with a controlled vocabulary.

// Event naming taxonomy
enum EventCategory: String {
    case screen = "screen"
    case button = "button"
    case form = "form"
    case purchase = "purchase"
    case notification = "notification"
    case error = "error"
    case session = "session"
    case feature = "feature"
}
 
enum EventAction: String {
    case viewed = "viewed"
    case tapped = "tapped"
    case submitted = "submitted"
    case completed = "completed"
    case failed = "failed"
    case started = "started"
    case ended = "ended"
    case dismissed = "dismissed"
    case received = "received"
    case enabled = "enabled"
    case disabled = "disabled"
}
 
// Generate consistent event names from taxonomy
struct CategorizedEvent: TrackableEvent {
    let category: EventCategory
    let action: EventAction
    let additionalProperties: [String: Any]
 
    var eventName: String {
        "\(category.rawValue)_\(action.rawValue)"
    }
 
    var properties: [String: Any] {
        additionalProperties
    }
}
 
// This creates events like:
// "screen_viewed", "button_tapped", "purchase_completed", "form_submitted"

A controlled vocabulary prevents the proliferation of near-duplicate event names like "btn_click", "button_clicked", "buttonTap", and "tap_button" that plague many analytics implementations.

Event Enrichment Patterns

Raw events lack context. Enrichers automatically attach metadata that makes events useful for analysis without burdening the tracking call site.

// Device context enricher
struct DeviceEnricher: EventEnricher {
    func enrich(_ event: inout AnalyticsEvent) {
        event.properties["device_model"] = UIDevice.current.model
        event.properties["os_version"] = UIDevice.current.systemVersion
        event.properties["app_version"] = Bundle.main.infoDictionary?["CFBundleShortVersionString"]
        event.properties["build_number"] = Bundle.main.infoDictionary?["CFBundleVersion"]
        event.properties["locale"] = Locale.current.identifier
        event.properties["timezone"] = TimeZone.current.identifier
    }
}
 
// Session enricher
final class SessionEnricher: EventEnricher {
    private var sessionId: String = UUID().uuidString
    private var sessionStartTime: Date = Date()
    private var eventIndex: Int = 0
    private let sessionTimeout: TimeInterval = 1800 // 30 minutes
 
    func enrich(_ event: inout AnalyticsEvent) {
        checkSessionExpiry()
 
        event.properties["session_id"] = sessionId
        event.properties["session_duration"] = Date().timeIntervalSince(sessionStartTime)
        event.properties["event_index"] = eventIndex
 
        eventIndex += 1
    }
 
    private func checkSessionExpiry() {
        let timeSinceStart = Date().timeIntervalSince(sessionStartTime)
        if timeSinceStart > sessionTimeout {
            sessionId = UUID().uuidString
            sessionStartTime = Date()
            eventIndex = 0
        }
    }
}
 
// User properties enricher
final class UserEnricher: EventEnricher {
    private var userId: String?
    private var userProperties: [String: Any] = [:]
 
    func identify(userId: String, properties: [String: Any] = [:]) {
        self.userId = userId
        self.userProperties = properties
    }
 
    func enrich(_ event: inout AnalyticsEvent) {
        if let userId {
            event.properties["user_id"] = userId
        }
        for (key, value) in userProperties {
            event.properties["user_\(key)"] = value
        }
    }
}

By stacking enrichers, each event automatically carries device info, session context, and user identity without any extra code at the call site.

Funnel and Flow Tracking

Individual events tell you what happened. Funnels tell you what did not happen. Tracking user flows through multi-step processes requires a pattern that links related events together.

// Funnel tracking with step correlation
final class FunnelTracker {
    private let analytics: KlivvrAnalytics
    private var activeFunnels: [String: FunnelState] = [:]
 
    struct FunnelState {
        let funnelId: String
        let funnelName: String
        var currentStep: Int
        let startTime: Date
    }
 
    func startFunnel(name: String) -> String {
        let funnelId = UUID().uuidString
        activeFunnels[funnelId] = FunnelState(
            funnelId: funnelId,
            funnelName: name,
            currentStep: 0,
            startTime: Date()
        )
 
        analytics.track("funnel_started", properties: [
            "funnel_id": funnelId,
            "funnel_name": name
        ])
 
        return funnelId
    }
 
    func trackStep(funnelId: String, stepName: String, properties: [String: Any] = [:]) {
        guard var state = activeFunnels[funnelId] else { return }
 
        state.currentStep += 1
        activeFunnels[funnelId] = state
 
        var eventProperties: [String: Any] = [
            "funnel_id": funnelId,
            "funnel_name": state.funnelName,
            "step_number": state.currentStep,
            "step_name": stepName,
            "time_in_funnel": Date().timeIntervalSince(state.startTime)
        ]
        eventProperties.merge(properties) { _, new in new }
 
        analytics.track("funnel_step_completed", properties: eventProperties)
    }
 
    func completeFunnel(funnelId: String) {
        guard let state = activeFunnels[funnelId] else { return }
 
        analytics.track("funnel_completed", properties: [
            "funnel_id": funnelId,
            "funnel_name": state.funnelName,
            "total_steps": state.currentStep,
            "total_duration": Date().timeIntervalSince(state.startTime)
        ])
 
        activeFunnels.removeValue(forKey: funnelId)
    }
 
    func abandonFunnel(funnelId: String, reason: String? = nil) {
        guard let state = activeFunnels[funnelId] else { return }
 
        var properties: [String: Any] = [
            "funnel_id": funnelId,
            "funnel_name": state.funnelName,
            "abandoned_at_step": state.currentStep,
            "total_duration": Date().timeIntervalSince(state.startTime)
        ]
        if let reason {
            properties["abandonment_reason"] = reason
        }
 
        analytics.track("funnel_abandoned", properties: properties)
        activeFunnels.removeValue(forKey: funnelId)
    }
}
 
// Usage for an onboarding funnel
let funnelId = funnelTracker.startFunnel(name: "onboarding")
funnelTracker.trackStep(funnelId: funnelId, stepName: "welcome_screen")
funnelTracker.trackStep(funnelId: funnelId, stepName: "profile_setup")
funnelTracker.trackStep(funnelId: funnelId, stepName: "preferences")
funnelTracker.completeFunnel(funnelId: funnelId)

Automatic Screen Tracking

Manual screen tracking is error-prone because developers forget to add tracking calls. Swizzling or base class approaches can automate this, but they come with trade-offs. A protocol-based opt-in approach strikes the right balance.

// Protocol for automatic screen tracking
protocol AnalyticsScreenTracking: UIViewController {
    var analyticsScreenName: String { get }
    var analyticsScreenProperties: [String: Any] { get }
}
 
extension AnalyticsScreenTracking {
    var analyticsScreenProperties: [String: Any] { [:] }
}
 
// Base tracking implementation via viewDidAppear hook
extension UIViewController {
    @objc func analytics_viewDidAppear(_ animated: Bool) {
        analytics_viewDidAppear(animated) // Calls original (swizzled)
 
        if let trackable = self as? AnalyticsScreenTracking {
            var properties: [String: Any] = [
                "screen_name": trackable.analyticsScreenName,
                "screen_class": String(describing: type(of: self))
            ]
            properties.merge(trackable.analyticsScreenProperties) { _, new in new }
            KlivvrAnalytics.shared.track("screen_viewed", properties: properties)
        }
    }
}
 
// Opt in to tracking per screen
class ProductDetailViewController: UIViewController, AnalyticsScreenTracking {
    let product: Product
 
    var analyticsScreenName: String { "product_detail" }
 
    var analyticsScreenProperties: [String: Any] {
        [
            "product_id": product.id,
            "product_category": product.category
        ]
    }
}

Practical Tips

When implementing event tracking patterns, start with a tracking plan document that lists every event, its properties, and its expected values. Share this document across engineering, product, and data teams. Use enums and structs for event definitions so refactoring tools can find every usage. Run automated tests that verify your critical events fire with the correct properties. Avoid tracking everything -- be intentional about what you measure, because excessive tracking creates noise that obscures signal.

Conclusion

Strong event tracking patterns transform analytics from a chore into a reliable data foundation. Type-safe event definitions catch errors at compile time. Consistent naming conventions make data queryable. Enrichers add context without cluttering call sites. Funnel tracking reveals where users drop off. These patterns, applied consistently with KlivvrAnalyticsKit, ensure that the data flowing from your app to your dashboard is accurate, complete, and actionable.

Related Articles

business

Ensuring Data Quality in Mobile Analytics

Establish data quality practices for mobile analytics, including validation, monitoring, testing, and governance to maintain trustworthy analytics data.

7 min read