Skip to Content
Course content

168: Practice Exercise: Building a Type-Safe Analytics Event Logger

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent way too many hours in my career staring at a Mixpanel or Firebase dashboard and wondering why our "Sign Up" conversion rate dropped by 40% overnight, only to realize some developer had typed "user_signed_up" in one place and "UserSignedUp" in another. When you rely on strings for analytics, you're essentially praying that every dev on your team has a perfect memory and a steady hand.

The "Stringly-Typed" Disaster

If we were doing this the quick-and-dirty way, we'd probably just write a function like this:

func logEvent(_ name: String, parameters: [String: Any]?) {
    print("Logging event: \(name) with params: \(parameters ?? [:])")
}

// Usage
logEvent("button_clicked", parameters: ["screen": "home"])
logEvent("buton_cliked", parameters: ["screen": "settings"]) // Typo! The compiler doesn't care.

This is what I call "stringly-typed" code. It's flexible, sure, but it's a nightmare for maintenance. I can't search for every single place an event is fired easily, and there's no way to enforce that a "Purchase" event must include a price and a currency.

Trying to Tighten the Screws with Enums

My first instinct to fix this is usually to wrap everything in an enum. Enums are great because they force you to pick from a predefined list. Let's try that:

enum AnalyticsEvent {
    case appLaunched
    case buttonClicked(screen: String)
    case itemPurchased(itemId: String, price: Double)
}

func log(_ event: AnalyticsEvent) {
    switch event {
    case .appLaunched:
        print("Event: app_launched")
    case .buttonClicked(let screen):
        print("Event: button_clicked, screen: \(screen)")
    case .itemPurchased(let id, let price):
        print("Event: item_purchased, id: \(id), price: \(price)")
    }
}

This is a massive improvement. Now, if I typo the event name, the code won't even compile. But here's where I hit a wall: scalability. In a real production app, you might have 200 different events. If I put them all in one enum, that switch statement becomes a thousand-line monster. It violates the Single Responsibility Principle; the logger now has to know the internal details of every single event in the entire app.

Designing a Protocol for Event Data

I want the type-safety of the enum, but I want the events to be decoupled from the logger itself. The way to do this in Swift is to define what it means to be an analytics event using a protocol. I'll define a protocol that requires a name and a way to export parameters.

protocol AnalyticsEvent {
    var name: String { get }
    var parameters: [String: Any] { get }
}

// Now, each event is its own little object.
struct PurchaseEvent: AnalyticsEvent {
    let itemId: String
    let price: Double
    
    var name: String { "item_purchased" }
    var parameters: [String: Any] {
        ["item_id": itemId, "price": price]
    }
}

struct ScreenViewEvent: AnalyticsEvent {
    let screenName: String
    
    var name: String { "screen_view" }
    var parameters: [String: Any] {
        ["screen": screenName]
    }
}

Now, look at how clean the logger becomes. It doesn't need to know about PurchaseEvent or ScreenViewEvent specifically; it just needs something that conforms to AnalyticsEvent.

class AnalyticsLogger {
    static let shared = AnalyticsLogger()
    private init() {}

    func log(_ event: AnalyticsEvent) {
        print("Sending to server -> \(event.name) | Params: \(event.parameters)")
    }
}

// Usage:
AnalyticsLogger.shared.log(PurchaseEvent(itemId: "swift_book_01", price: 29.99))
AnalyticsLogger.shared.log(ScreenViewEvent(screenName: "Settings"))

This is the "Pro" way to do it. If we need to add a new event, we just create a new struct. We don't touch the AnalyticsLogger class, and we don't touch any existing events. The compiler ensures that we provide the required data for each event, and the logger stays blissfully ignorant of the specifics.




📋 Practical Task

Exercise: Implementing a Type-Safe User-Action Tracker

You are tasked with extending the analytics system to track user engagement. Your goal is to build a type-safe system that prevents developers from logging "User Action" events without the necessary context.

Requirements:

  • Create an AnalyticsEvent protocol with a name: String and parameters: [String: Any].
  • Implement a UserActionEvent struct that conforms to the protocol. It must require a username: String and an actionType: String (e.g., "like", "share", "follow") in its initializer.
  • Implement a SystemErrorEvent struct that conforms to the protocol. It must require an errorCode: Int and a message: String.
  • Create an EventDispatcher class with a method dispatch(_ event: AnalyticsEvent) that prints the event name and its parameters to the console.
  • In your main execution code, instantiate the EventDispatcher and log one UserActionEvent and one SystemErrorEvent.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.