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:
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,URLandUUIDbecome strings. Dates useyyyy-MM-dd'T'HH:mm:ss.SSSZ, for example2026-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:
Grovs.setGlobalTags(["beta", "experiment-A"])
Grovs.setGlobalTags(nil) // clearGlobal 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
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:
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:
Grovs.screenNameProvider, if set..name(_:)and.suppressare final;.automaticfalls through.- A
GrovsScreenTrackingconformance on the view controller. Final: a name is used,nilskips it. - Children embedded in your own containers are filtered out. Only screen-level controllers continue.
- SwiftUI hosting controllers use the view type:
CheckoutViewbecomes"Checkout". - Otherwise the cleaned class name:
CheckoutViewControllerbecomes"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:
struct CheckoutView: View {
var body: some View {
VStack {
// ...
}
.grovsScreen("Checkout")
}
}With no name, it uses the file name: CheckoutView.swift becomes "Checkout":
.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:
extension CheckoutViewController: GrovsScreenTracking {
var grovsScreenName: String? { "Checkout" }
}Custom resolver
Grovs.screenNameProvider runs before every other rule and covers all view controllers:
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:
Grovs.setScreenAliases([
"CheckoutViewController": "Checkout Page",
"PDPViewController": "Product Detail"
])Aliases sync after authentication. A failed sync retries at the next foreground.