Docs
grovs.io Start for free

Events & screen tracking

Track custom events and screen views in your iOS app with the Grovs SDK.

Tracking custom events

track records an event with optional properties and tags:

Swift
Grovs.track("purchase", properties: ["item_id": "sku-42", "price": 19.99])
Grovs.track("level_complete", properties: ["level": 5, "score": 1200])
Grovs.track("button_tap")
Grovs.track("signup", tags: ["organic", "campaign-A"])

How events are sent:

  • Written to disk and sent in batches about every 30 seconds. Failed batches retry with increasing delays.
  • Up to 1,000 events are kept on the device. Events older than seven days are dropped.
  • Events tracked before authentication completes are written to disk the same way and sent after it.
  • Events are also held during the device lookup for attribution, so they credit the right link. Lookups run after authentication, on every foreground and when resolving an incoming link. A stalled lookup releases them after 10 seconds.

Every custom event carries a screen_name property with the most recently viewed screen, for segmenting by screen. A screen_name you pass is never overwritten.

Event name rules

Names must be non-empty and can't be a reserved system name:

view, open, install, reinstall, app_open, time_spent, reactivation, user_referred, custom, screen_view

Since 3.1, push, in-app and journey event names are refused too: push_open, push_delivered, push_held, in_app_viewed, in_app_clicked, in_app_dismissed, in_app_answered, in_app_commented, in_app_held, journey_entered, journey_held, journey_path.

Use trackScreenView for screen views, not track("screen_view").

Property values

Values can be strings, numbers, booleans, NSNull, or nested arrays and dictionaries of those. The SDK sanitizes each value:

  • Date, URL and UUID become strings. Dates use yyyy-MM-dd'T'HH:mm:ss.SSSZ, for example 2026-09-14T10:21:33.000+0000.
  • Values JSON can't represent (NaN, Infinity, unsupported objects) are dropped for that key only. Other properties still send.
  • Over 8 KB of encoded properties drops all properties. The event is still recorded.
  • The backend stores at most 64 property keys per event. Keys used for filtering should be 256 bytes or less with no control characters.

Tags

Up to 20 tags per event, each up to 255 characters (backend limit). Global tags attach to every later event:

Swift
Grovs.setGlobalTags(["beta", "experiment-A"])
 
Grovs.setGlobalTags(nil)  // clear

Global and per-event tags are merged. Over 20 combined, per-event tags are kept first and global tags fill the remaining slots.

Tracking screen views

Swift
Grovs.trackScreenView("Checkout", properties: ["section": "payment"])
Grovs.trackScreenView("Product Detail", properties: ["product_id": "abc-123"])

screen_name is added to properties automatically. The same screen name sent twice within one second is dropped, so tab switches and rotations don't flood the dashboard.

Automatic screen tracking

autoTrackScreenViews defaults to true. The SDK hooks UIViewController.viewDidAppear to track screens. It filters out system controllers (navigation, tab bar, alerts and similar), screens the OS supplies (share sheets, Safari, mail) and children embedded in your own containers, including inside a modal. The screen already visible when tracking starts is tracked too, and an alert on top doesn't hide the screen under it.

To turn it off, pass false:

Swift
Grovs.configure(
    APIKey: "your-api-key",
    useTestEnvironment: false,
    autoTrackScreenViews: false,
    delegate: self
)

Grovs.setSDK(enabled: false) stops automatic screen tracking along with all other collection.

How a screen name is chosen

The first rule that applies wins:

  1. Grovs.screenNameProvider, if set. .name(_:) and .suppress are final; .automatic falls through.
  2. A GrovsScreenTracking conformance on the view controller. Final: a name is used, nil skips it.
  3. Children embedded in your own containers are filtered out. Only screen-level controllers continue.
  4. SwiftUI hosting controllers use the view type: CheckoutView becomes "Checkout".
  5. Otherwise the cleaned class name: CheckoutViewController becomes "Checkout".

SwiftUI screens without an unambiguous name (an AnyView root, for example) are dropped, not reported under a meaningless name. Annotate them with .grovsScreen().

SwiftUI

.grovsScreen() names a screen and suppresses automatic tracking for its hosting controller, so it's never counted twice:

Swift
struct CheckoutView: View {
    var body: some View {
        VStack {
            // ...
        }
        .grovsScreen("Checkout")
    }
}

With no name, it uses the file name: CheckoutView.swift becomes "Checkout":

Swift
.grovsScreen()

The name comes from the file, not the view type. If several views share a file, or the file name doesn't match the screen, pass a name.

Automatic tracking can't see a custom router that swaps screens inside one hosting controller. Add .grovsScreen() to each screen.

UIKit

Conform to GrovsScreenTracking to name a controller, or return nil to skip it:

Swift
extension CheckoutViewController: GrovsScreenTracking {
    var grovsScreenName: String? { "Checkout" }
}

Custom resolver

Grovs.screenNameProvider runs before every other rule and covers all view controllers:

Swift
Grovs.screenNameProvider = { viewController in
    switch viewController {
    case is LegalViewController:
        return .suppress
    case let vc as ProductViewController:
        return .name("Product \(vc.categoryName)")
    default:
        return .automatic
    }
}

Return .name(_:) to override, .suppress to skip, or .automatic to fall through to the SDK. The closure runs on the main thread. Keep it fast and don't retain the view controller.

Visibility filtering runs first. Controllers that aren't the frontmost screen, or use .grovsScreen(), never reach the resolver.

Screen name aliases

Map class names to friendlier dashboard names:

Swift
Grovs.setScreenAliases([
    "CheckoutViewController": "Checkout Page",
    "PDPViewController": "Product Detail"
])

Aliases sync after authentication. A failed sync retries at the next foreground.

Next steps

Last updated 2026-10-27
Was this page helpful?