Skip to main content
While PageTracker is the recommended approach for most tracking, you can also track custom events directly using the track(event:properties:) method.

When to Use Direct Event Tracking

Use direct event tracking for:
  • Events that don’t fit the page view model
  • Background events (app state changes, push notifications)
  • One-off events without context
  • Events outside of a view lifecycle
For view-based tracking with engagement time, scroll depth, and contextual cohorts, use PageTracker instead.

Tracking Events

Basic Event

do {
    try Permutive.shared.track(event: "AppLaunch")
} catch {
    print("Failed to track event: \(error)")
}

Event with Properties

do {
    let properties = try EventProperties([
        "button_name": "subscribe",
        "position": "header",
        "is_premium": true
    ])
    try Permutive.shared.track(event: "ButtonClick", properties: properties)
} catch {
    print("Failed to track event: \(error)")
}

Event Naming Rules

Event names must follow these rules:
  • Only alphanumeric characters and underscores: [a-zA-Z0-9_]
  • Cannot start with a number
  • Case-sensitive
"AppLaunch"           // ✅
"button_click"        // ✅
"VideoPlay2"          // ✅
"user_subscription"   // ✅

Schema Validation

Event properties must match exactly the schema defined in your Permutive dashboard. Mismatched properties cause event rejection.

Enable Debug Logging

options.logModes = LogMode.all

Schema Error Example

When properties don’t match the schema:
Permutive: [error] Encountered 1 event rejection!
Permutive: [error] Error 1: A validation check carried out by the server did not succeed.(1007)
Permutive: [error] Error 1: Schema validation failed with reason(s): [#: extraneous key [invalid_key] is not permitted]

Checking Events Were Accepted

Successful events show:
Permutive: [info] Accepted: 1 / 1

Common Event Types

App Lifecycle Events

// App launch
func applicationDidBecomeActive() {
    try? Permutive.shared.track(
        event: "AppActive",
        properties: try? EventProperties([
            "launch_type": isFirstLaunch ? "fresh" : "resume"
        ])
    )
}

// App background
func applicationWillResignActive() {
    try? Permutive.shared.track(event: "AppBackground")
}

User Action Events

// Button click
func onSubscribeButtonTapped() {
    try? Permutive.shared.track(
        event: "SubscribeClick",
        properties: try? EventProperties([
            "plan": "premium",
            "source": "paywall"
        ])
    )
}

// Search
func onSearch(query: String, resultCount: Int) {
    try? Permutive.shared.track(
        event: "Search",
        properties: try? EventProperties([
            "query_length": query.count,
            "result_count": resultCount,
            "has_results": resultCount > 0
        ])
    )
}

Push Notification Events

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse
) {
    let userInfo = response.notification.request.content.userInfo

    try? Permutive.shared.track(
        event: "PushNotificationOpen",
        properties: try? EventProperties([
            "campaign_id": userInfo["campaign_id"] as? String ?? "unknown",
            "action": response.actionIdentifier
        ])
    )
}

Purchase Events

func onPurchaseComplete(product: SKProduct, transaction: SKPaymentTransaction) {
    try? Permutive.shared.track(
        event: "Purchase",
        properties: try? EventProperties([
            "product_id": product.productIdentifier,
            "price": product.price.doubleValue,
            "currency": product.priceLocale.currencyCode ?? "USD",
            "transaction_id": transaction.transactionIdentifier ?? ""
        ])
    )
}

Event Enrichment

Add server-side enrichment to events:
let properties = try EventProperties([
    "geo_info": EventProperties.geoInfoValue,
    "isp_info": EventProperties.ispInfoValue,
    "form_id": "contact_form"
])

try Permutive.shared.track(event: "FormSubmit", properties: properties)
See Event Properties for all enrichment options.

Batching and Delivery

Events are:
  • Queued locally when tracked
  • Sent in batches to reduce network overhead
  • Persisted to disk if the app closes before sending
  • Retried automatically on network failure
You don’t need to manage batching - it’s handled automatically.

Error Handling

func trackEvent(name: String, properties: [String: Any]) {
    do {
        let eventProperties = try EventProperties(properties)
        try Permutive.shared.track(event: name, properties: eventProperties)
    } catch let error as NSError {
        // Log but don't crash - tracking failures shouldn't break app
        print("Event tracking failed: \(error.localizedDescription)")

        // Optionally report to error tracking
        // Crashlytics.log("Permutive track error: \(error)")
    }
}

Best Practices

  • Use descriptive, consistent event names
  • Match your dashboard schema exactly
  • Test events with debug logging enabled
  • Handle errors gracefully
  • Use PageTracker for view-based tracking
  • Keep properties minimal and relevant

Troubleshooting

Problem: Tracked events don’t show up.Solutions:
  • Enable debug logging and look for Accepted: X / X
  • Check for schema validation errors in logs
  • Wait up to 5 minutes for processing
  • Verify you’re in the correct workspace
Problem: Creating EventProperties throws an error.Solutions:
  • Verify all values are supported types
  • Check for unsupported custom objects
  • Ensure arrays contain only supported types
Problem: Events sent but rejected by server.Solutions:
  • Check event name matches schema exactly
  • Verify property names and types
  • Remove properties not in the schema
  • Check for typos