GitHub - hidden-spectrum/phoenix: Declarative Swift framework for sending events to various analytics platforms · GitHub
Skip to content
hidden-spectrumPublic

About

Declarative Swift framework for sending events to various analytics platforms

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Phoenix

A Swift analytics abstraction library that provides a unified interface for multiple analytics providers.

Overview

Phoenix abstracts away the complexity of working with different analytics SDKs by providing a single, type-safe API. While originally built around Firebase Analytics, Phoenix currently supports PostHog with additional providers planned for the future.

Features

  • Unified API - Single interface for all analytics operations
  • Type Safety - Strongly-typed events, parameters, and screens
  • SwiftUI Integration - Environment values and view modifiers for seamless tracking
  • Feature Flags - A/B testing and feature flag support with type-safe variants
  • Commerce Tracking - Built-in StoreKit transaction logging
  • Crash Reporting - MetricKit integration for crash diagnostics

Requirements

  • iOS 16.0+ / macOS 13.0+ / tvOS 16.0+
  • Swift 6.0+

Installation

Add Phoenix to your Package.swift:

dependencies: [
    .package(url: "https://github.com/hidden-spectrum/phoenix.git", from: "0.1.0")
]

Then add the targets you need:

.target(
    name: "YourApp",
    dependencies: [
        "Phoenix",      // Core library (required)
        "PhoenixPH",    // PostHog provider
    ]
)

Quick Start

1. Create your analytics instance

import Phoenix
import PhoenixPH
import PostHog

// Configure PostHog
let config = PostHogConfig(apiKey: "your-api-key")
config.host = "https://app.posthog.com"

// Create Phoenix instance
let phoenix = Phoenix(provider: PostHogAnalyticsProvider(with: config))

2. Initialize on app launch

@main
struct MyApp: App {
    init() {
        phoenix.appDidFinishLaunching()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.phoenix, phoenix)
        }
    }
}

3. Track events

struct ContentView: View {
    @Environment(\.phoenix) private var phoenix

    var body: some View {
        Button("Purchase") {
            phoenix.logEvent(.purchaseTapped)
        }
    }
}

Defining Your Analytics

Phoenix uses type-safe structs that you extend with static constants. This provides autocomplete, compile-time safety, and a centralized place to define your analytics taxonomy.

Events

extension AnalyticsEvent {
    // Authentication
    static let loginTapped = Event("login_tapped")
    static let signupCompleted = Event("signup_completed")
    static let logoutTapped = Event("logout_tapped")

    // Navigation
    static let tabSelected = Event("tab_selected")
    static let backTapped = Event("back_tapped")

    // Content
    static let itemViewed = Event("item_viewed")
    static let itemShared = Event("item_shared")

    // Events with default parameters
    static func searchPerformed(query: String) -> Event {
        Event("search_performed", parameters: [
            .searchTerm: query
        ])
    }

    static func itemAddedToCart(itemId: String, price: Double) -> Event {
        Event("item_added_to_cart", parameters: [
            .contentId: itemId,
            .price: price
        ])
    }
}

Screens

extension AnalyticsScreen {
    static let home = Screen("home")
    static let search = Screen("search")
    static let profile = Screen("profile")
    static let settings = Screen("settings")
    static let onboarding = Screen("onboarding")
    static let checkout = Screen("checkout")
    static let productDetail = Screen("product_detail")
}

Parameters

extension AnalyticsParameter {
    static let itemCategory = Parameter("item_category")
    static let subscriptionTier = Parameter("subscription_tier")
    static let referralSource = Parameter("referral_source")
    static let experimentGroup = Parameter("experiment_group")
}

Element Names

extension AnalyticsElementName {
    static let submitButton: ElementName = "submit_button"
    static let cancelButton: ElementName = "cancel_button"
    static let shareButton: ElementName = "share_button"
    static let productCell: ElementName = "product_cell"
    static let settingsRow: ElementName = "settings_row"
}

Feature Flags

extension AnalyticsFeatureFlag where Value == Bool {
    static let newCheckoutFlow = Feature("new_checkout_flow")
    static let darkModeEnabled = Feature("dark_mode_enabled")
    static let premiumFeatures = Feature("premium_features")
}

Feature Flag Payloads

Declare the payload type on the flag so callers cannot choose a different return type:

struct CheckoutConfig: Decodable, Sendable {
    let title: String
    let maximumItems: Int
}

extension AnalyticsFeatureFlag where Value == CheckoutConfig {
    static let checkoutConfig = Feature("checkout_config")
}

let config = phoenix.featureFlagValue(.checkoutConfig) // CheckoutConfig?
let title = config?.title ?? "Checkout"

AnalyticsFeatureFlag<Value> requires only Sendable. The base featureFlagValue overload casts the provider payload directly to Value. An explicit Value: Decodable overload decodes JSON objects, arrays, and scalar values such as String, Int, and Bool. A payload containing only 123 can therefore be read with AnalyticsFeatureFlag<Int>("item_limit"). Missing or mismatched payloads return nil. Providers without payload support also return nil.

Generic wrappers must retain the Decodable constraint to select the decoding overload; overload selection uses the constraints known at the call site.

featureFlagValue reads the payload independently of the enabled state. Use featureEnabled(.checkoutConfig) when the configuration should only apply to enabled flags. featureFlagValue on a Bool flag reads its boolean payload, not its enabled state. Call reloadFeatureFlags() before reading when you need to wait for a refresh.

When migrating existing boolean flags, add where Value == Bool to their AnalyticsFeatureFlag extensions and use AnalyticsFeatureFlag<Bool> for explicit type annotations and standalone initializers. Custom providers must make their featureEnabled method generic: func featureEnabled<Value>(_ feature: AnalyticsFeatureFlag<Value>) -> Bool. Providers that support payloads should implement both featureFlagValue overloads, including the one constrained to Value: Decodable.

Tests can supply payloads using TestingAnalyticsProvider.setFeatureFlagValue(_:for:).

Experiments

enum OnboardingVariant: String, AnalyticsExperimentVariant {
    case control
    case shortFlow = "short_flow"
    case videoIntro = "video_intro"
}

extension AnalyticsExperiment where Variant == OnboardingVariant {
    static let onboarding = AnalyticsExperiment(
        "onboarding_experiment",
        variantType: OnboardingVariant.self
    )
}

Usage

Logging Events

// Simple event
phoenix.logEvent(.loginTapped)

// Event with screen context
phoenix.logEvent(.itemViewed, on: .productDetail)

// Event with additional parameters
phoenix.logEvent(
    .searchPerformed(query: "swift"),
    additionalParameters: [
        .success: true,
        .itemCategory: "books"
    ]
)

Screen Views

phoenix.logScreenView(.productDetail, parameters: [
    .contentId: "product-123"
])

User Properties

// Set user ID
phoenix.setUserId("user-12345")

// Set individual property
phoenix.setUserProperty(.subscriptionTier, to: "premium")

// Set multiple properties
phoenix.setUserProperties([
    .subscriptionTier: "premium",
    .referralSource: "friend"
])

Global Properties

Properties attached to all subsequent events:

// Register global properties
phoenix.registerGlobalProperties([
    .experimentGroup: "A",
    .subscriptionTier: "free"
])

// Remove a global property
phoenix.unregisterGlobalProperty(.experimentGroup)

Element Tracking

Track UI element interactions:

// Track button click
phoenix.logElementClick(
    name: .submitButton,
    type: .button,
    on: .checkout
)

// Track with value
phoenix.logElementClick(
    name: "quantity_stepper",
    type: .number,
    value: 5,
    on: .productDetail
)

Flushing Events

Force pending events to be sent immediately:

// Flush before app goes to background
phoenix.flush()

SwiftUI Integration

Environment Values

Phoenix provides SwiftUI environment integration:

struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.phoenix, phoenix)
                .environment(\.analyticsScreen, .home)
        }
    }
}

View Modifiers

Track clicks with view modifiers:

Button("Add to Cart") {
    addToCart()
}
.analyticsClick(
    name: "add_to_cart",
    type: .button,
    screen: .productDetail
)

Screen Tracking (PostHog)

Automatically track screen views:

NavigationStack {
    ProductListView()
        .analyticsScreen(.productDetail)
}

AnalyticsTrackingButton

A drop-in Button replacement with built-in analytics. Prefer this over .analyticsClick() which can sometimes override tap behavior on certain views.

// Simple usage
AnalyticsTrackingButton(name: "submit") {
    submitForm()
} label: {
    Text("Submit")
}

// With custom type and parameters
AnalyticsTrackingButton(
    name: "add_to_cart",
    type: .button,
    parameters: [
        .contentId: product.id,
        .price: product.price
    ]
) {
    addToCart(product)
} label: {
    Label("Add to Cart", systemImage: "cart.badge.plus")
}

Presentation Context

Track modal presentations with context:

.sheet(isPresented: $showDetail) {
    DetailView()
        .analyticsPresentationContext(
            elementName: .productCell,
            elementType: .cell,
            screen: .search
        )
}

Feature Flags & A/B Testing

Feature Flags

if phoenix.featureEnabled(.newCheckoutFlow) {
    showNewCheckout()
} else {
    showLegacyCheckout()
}

A/B Testing with Variants

switch phoenix.enabledVariant(for: .onboarding) {
case .control:
    showStandardOnboarding()
case .shortFlow:
    showShortOnboarding()
case .videoIntro:
    showVideoOnboarding()
case nil:
    showStandardOnboarding() // Default fallback
}

Reload Feature Flags

Task {
    await phoenix.reloadFeatureFlags()
    updateUI()
}

Commerce & StoreKit

Transaction Logging

// Log StoreKit transaction
phoenix.logTransaction(transaction)

Error & Crash Reporting

Error Logging

do {
    try riskyOperation()
} catch {
    phoenix.logError(
        error,
        on: .settings,
        additionalParameters: [.init("operation"): "data_sync"]
    )
}

License

MIT License. See LICENSE for details.

About

Declarative Swift framework for sending events to various analytics platforms

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages