This page covers what you can change after iOS Monitoring Setup is working. Configuration is split in two: Atatus.Configuration labels your data and controls the transport, and RUM.Configuration controls what RUM collects.
Nothing here is required. Each option has a default, so add only what you need.
Identity and transport
These options are passed to Atatus.initialize() and apply to every product:
| Option | Default | Purpose |
|---|---|---|
licenseKey |
Required | Authenticates the SDK against your Atatus account |
env |
Required | Environment tag, such as production or staging |
service |
Bundle identifier | Your application's identity in Atatus |
version |
CFBundleShortVersionString |
Application version, used to compare releases |
site |
.us |
Region your data is ingested in: .us, .in, or .eu |
serverUrl |
nil |
Base URL of a self-hosted intake. Overrides site |
batchSize |
.medium |
Preferred size of an upload batch: .small, .medium, or .large |
uploadFrequency |
.average |
How often batches are uploaded: .frequent, .average, or .rare |
batchProcessingLevel |
.medium |
Batches processed per upload cycle: .low (5), .medium (20), or .high (100) |
backgroundTasksEnabled |
false |
Keeps uploading for about 30 seconds after the app is backgrounded |
proxyConfiguration |
nil |
URLSessionConfiguration proxy attributes for uploads |
encryption |
nil |
Encrypts batch files on disk through your own DataEncryption implementation |
bundle |
.main |
The bundle the SDK reads version and identifier information from |
Tune batching
Batch size and upload frequency trade data latency against radio use. Larger, less frequent batches amortize each radio wake-up across more events:
Atatus.initialize(
with: Atatus.Configuration(
licenseKey: "<LICENSE_KEY>",
env: "<ENV_NAME>",
service: "<APP_NAME>",
batchSize: .large,
uploadFrequency: .rare
),
trackingConsent: .granted
)
Use .large with .rare when battery consumption is the constraint, and .small with .frequent during development, where fast feedback matters more than efficiency.
Encrypt data at rest
Events waiting to be uploaded are stored in the application's cache directory. To encrypt them, pass an object that implements DataEncryption:
struct Encryptor: DataEncryption {
func encrypt(data: Data) throws -> Data { /* your implementation */ }
func decrypt(data: Data) throws -> Data { /* your implementation */ }
}
Atatus.initialize(
with: Atatus.Configuration(
licenseKey: "<LICENSE_KEY>",
env: "<ENV_NAME>",
encryption: Encryptor()
),
trackingConsent: .granted
)
RUM options
Every option on RUM.Configuration, with the value it takes when you leave it out:
| Option | Default | Purpose |
|---|---|---|
sessionSampleRate |
100 |
Percentage of sessions collected |
uiKitViewsPredicate |
nil |
Enables automatic UIKit view tracking |
uiKitActionsPredicate |
nil |
Enables automatic UIKit action tracking |
swiftUIViewsPredicate |
nil |
Enables automatic SwiftUI view tracking |
swiftUIActionsPredicate |
nil |
Enables automatic SwiftUI action tracking |
urlSessionTracking |
nil |
Reports URLSession requests as resources |
trackFrustrations |
true |
Detects frustration signals, such as repeated taps on an unresponsive control |
trackBackgroundEvents |
false |
Keeps events recorded while no view is active |
trackWatchdogTerminations |
false |
Reports terminations by the iOS watchdog |
trackMemoryWarnings |
true |
Reports memory warnings the app receives |
trackSlowFrames |
true |
Reports slow and frozen frames |
longTaskThreshold |
0.1 seconds |
Main-thread blocks longer than this are reported as long tasks. nil turns it off |
appHangThreshold |
nil |
Main-thread hangs longer than this are reported. nil turns it off |
vitalsUpdateFrequency |
.average |
How often CPU and memory vitals are sampled. nil turns it off |
trackAnonymousUser |
true |
Keeps a per-install identifier so sessions from the same device can be counted as one user |
collectAccessibility |
false |
Adds the device's accessibility settings to view events |
telemetrySampleRate |
20 |
Percentage of SDK self-monitoring events sent to Atatus |
customEndpoint |
nil |
Sends RUM events to a different intake URL than the other products |
onSessionStart |
nil |
Callback invoked with the session ID when a session starts |
viewEventMapper |
nil |
Modifies view events before they are sent |
resourceEventMapper |
nil |
Modifies or drops resource events |
actionEventMapper |
nil |
Modifies or drops action events |
errorEventMapper |
nil |
Modifies or drops error events |
longTaskEventMapper |
nil |
Modifies or drops long task events |
networkSettledResourcePredicate |
TimeBasedTNSResourcePredicate() |
Decides which resources count toward a view's time to network settled |
nextViewActionPredicate |
TimeBasedINVActionPredicate() |
Decides which action counts toward interaction to next view |
The performance options are covered in Mobile Vitals, and the network options in Network Tracking.
Track views automatically
A views predicate decides which screens become RUM views and what they are called. Without one, no views are tracked at all.
UIKit
DefaultUIKitRUMViewsPredicate tracks each view controller as a screen, named after its class, such as MyApp.CheckoutViewController. It skips UIKit's own container controllers, so UINavigationController and UITabBarController do not appear as screens, and it skips SwiftUI hosting controllers, which the SwiftUI predicate handles instead. A subclass of your own is always tracked, including a subclass of a container controller.
Class names make poor screen names in a dashboard. Write a predicate to name them yourself:
import UIKit
import AtatusRUM
struct AppViewsPredicate: UIKitRUMViewsPredicate {
func rumView(for viewController: UIViewController) -> RUMView? {
switch viewController {
case is HomeViewController:
var view = RUMView(name: "Home", attributes: ["screen.tier": "free"])
view.path = "app/home"
return view
case is CheckoutViewController:
return RUMView(name: "Checkout")
case is UIAlertController:
return RUMView(name: "Alert", isUntrackedModal: true)
default:
return nil
}
}
}
RUM.enable(with: RUM.Configuration(uiKitViewsPredicate: AppViewsPredicate()))
Returning nil leaves a screen untracked. isUntrackedModal: true is for an overlay you do not want recorded as a screen: the view underneath stops while it is up, and restarts when it is dismissed, so its time is not counted as time on the screen behind it.
path is not an initializer parameter. Create the RUMView first, then set view.path as the example does. When you leave it unset, the path falls back to the class name.
SwiftUI
DefaultSwiftUIRUMViewsPredicate detects SwiftUI screens by inspecting the hosting controller, and names them after the view type it finds:
RUM.enable(
with: RUM.Configuration(
swiftUIViewsPredicate: DefaultSwiftUIRUMViewsPredicate()
)
)
SwiftUI view detection is experimental. It reads SwiftUI's internal structures, so names vary with the iOS version, and a screen it cannot resolve is reported under a generic fallback name. For screens that matter, name them yourself with .trackRUMView(name:) as shown in iOS Monitoring Setup, and do not apply the modifier to a screen the predicate already detects, because the view is then counted twice.
To rename what the predicate finds, implement it yourself:
struct AppSwiftUIViewsPredicate: SwiftUIRUMViewsPredicate {
func rumView(for extractedViewName: String) -> RUMView? {
switch extractedViewName {
case "ProductDetailView": return RUMView(name: "Product")
case "CartView": return RUMView(name: "Cart")
default: return nil
}
}
}
Track a view manually
Manual tracking is the most reliable option when the predicates do not fit your navigation:
// From a view controller
RUMMonitor.shared().startView(viewController: self, name: "Checkout")
RUMMonitor.shared().stopView(viewController: self)
// Or with a key of your own
RUMMonitor.shared().startView(key: "checkout", name: "Checkout")
RUMMonitor.shared().stopView(key: "checkout")
Track user actions automatically
DefaultUIKitRUMActionsPredicate reports taps. It reports a tap only when it lands on a control, a table view cell, a collection view cell, or an alert action, so taps on plain views and labels are not reported. Taps on the system keyboard are dropped for privacy.
Action names come from the view: if it has an accessibilityIdentifier, the name is the class plus that identifier, such as UIButton(checkout-button). Otherwise it is the class name alone.
Set accessibilityIdentifier on the controls you care about. It is the only way to get readable action names from the default predicate, and it costs nothing because it does not affect the interface.
A custom predicate can rename an action or return nil to drop it. It cannot widen what is captured, because the filtering above happens before the predicate is asked:
struct AppActionsPredicate: UITouchRUMActionsPredicate {
func rumAction(targetView: UIView) -> RUMAction? {
guard let identifier = targetView.accessibilityIdentifier else {
return nil
}
return RUMAction(name: identifier)
}
}
On tvOS the predicate is press based rather than touch based, and reports select, menu, and play or pause presses as click actions.
Scroll and swipe
Scroll and swipe actions are reported for UIScrollView, UITableView, and UICollectionView whenever a UIKit actions predicate is set. The SDK decides which of the two it was from how fast the finger was moving when it left the screen, and records the direction. Turn it off with the feature flag:
RUM.enable(
with: RUM.Configuration(
uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate(),
featureFlags: [.trackScrollAndSwipeActions: false]
)
)
This is available on iOS and iPadOS.
Frustration signals
With trackFrustrations on, which is the default, a tap followed by an error is reported as an error tap. That is the one frustration signal the iOS SDK detects, and it rolls up into a frustration count per view.
Identify your users
Attaching a user to the session lets you find every session, error, and request belonging to one person:
Atatus.setUserInfo(
id: "1234",
name: "Priya R",
email: "priya@example.com",
extraInfo: ["plan": "enterprise"]
)
Only id is required. Add fields you learn later without repeating the rest:
Atatus.addUserExtraInfo(["trial_days_left": 7])
Clear the user on sign-out, so the next session is not attributed to the person who signed out:
Atatus.clearUserInfo()
User information is attached to every event from the moment you set it, so set it as soon as you know who the user is, and treat it as data leaving the device. Send an identifier you can resolve internally rather than personal data you do not need in a dashboard.
For business-to-business applications, the account APIs record the organization a user belongs to, alongside the user:
Atatus.setAccountInfo(id: "acme", name: "Acme Inc", extraInfo: ["tier": "gold"])
Atatus.addAccountExtraInfo(["seats": 250])
Atatus.clearAccountInfo()
trackAnonymousUser is on by default and keeps a random per-install identifier, so sessions from the same device are counted as one user even before sign-in. Turn it off if you do not want that identifier stored:
RUM.enable(with: RUM.Configuration(trackAnonymousUser: false))
Attach your own attributes
Global attributes are attached to every RUM event from the point you add them:
import AtatusRUM
RUMMonitor.shared().addAttribute(forKey: "tenant", value: "acme")
RUMMonitor.shared().addAttributes(["feature.new_checkout": true, "cart.items": 3])
RUMMonitor.shared().removeAttribute(forKey: "tenant")
RUMMonitor.shared().removeAttributes(forKeys: ["cart.items"])
View attributes apply to the active view only, which is the right scope for something that describes one screen:
RUMMonitor.shared().addViewAttribute(forKey: "product.id", value: "SKU-42")
RUMMonitor.shared().removeViewAttribute(forKey: "product.id")
Send custom events
Views
Track a screen yourself when the automatic predicates cannot name it correctly, for example a screen rendered by a single container controller:
RUMMonitor.shared().startView(key: "checkout", name: "Checkout")
RUMMonitor.shared().stopView(key: "checkout")
A view started with a key must be stopped with the same key. Starting a new view stops the previous one, so you do not need to pair them when you navigate forward.
Actions
Report an action the SDK cannot see, such as a gesture handled by your own recognizer:
RUMMonitor.shared().addAction(type: .tap, name: "Apply coupon")
addAction is for an instantaneous action. For one with a duration, bracket it:
RUMMonitor.shared().startAction(type: .custom, name: "Import photos")
// work
RUMMonitor.shared().stopAction(type: .custom, name: "Import photos")
The action types are .tap, .click, .scroll, .swipe, and .custom.
Errors
Report a handled error, so a failure your code recovers from is still visible:
do {
try importPhotos()
} catch {
RUMMonitor.shared().addError(error: error, source: .source)
}
See Error Tracking for error sources, fingerprints, and crash reporting.
Resources
Report a request made by a client the SDK cannot instrument. See Network Tracking.
Feature flag evaluations
Record which variant a user received, so you can compare errors and performance between variants:
RUMMonitor.shared().addFeatureFlagEvaluation(name: "new-checkout", value: true)
Report the evaluation wherever your flag library resolves it, whichever library that is. Evaluations are attached to the active view and to the errors recorded in it, and are cleared when the view stops, so report them on each screen that depends on the flag rather than once at launch.
Modify or drop events
An event mapper runs on every event of its type before it is uploaded. Use it to redact a URL, rename a view, or drop an event you do not want:
RUM.enable(
with: RUM.Configuration(
viewEventMapper: { event in
var event = event
event.view.url = redact(event.view.url)
return event
},
resourceEventMapper: { event in
if event.resource.url.contains("/health") {
return nil
}
return event
}
)
)
The view mapper must return an event, so a view cannot be dropped. The resource, action, error, and long task mappers can return nil to drop the event. Mappers run on a background thread, once per event, so keep them fast and free of side effects.
Control the session
Read the current session ID to attach it to a support ticket or your own logs:
RUMMonitor.shared().currentSessionID { sessionID in
guard let sessionID = sessionID else { return }
print("Atatus session: \(sessionID)")
}
End the session yourself, for example on sign-out, so the next user's activity starts a new one:
RUMMonitor.shared().stopSession()
To react to each new session, pass onSessionStart:
RUM.enable(
with: RUM.Configuration(
onSessionStart: { sessionID, isDiscarded in
print("session \(sessionID), discarded by sampling: \(isDiscarded)")
}
)
)
Manage stored data
These apply to data the SDK has collected but not yet uploaded:
// Delete everything waiting on disk, for example after the user withdraws consent
Atatus.clearAllData()
// Stop the SDK instance; nothing is collected afterwards
Atatus.stopInstance()
Atatus.isInitialized() reports whether an instance is running, which is useful in a shared framework that must not initialize twice.
Run more than one instance
A single instance covers almost every application. If you need to send data to two accounts or two regions, name each instance and pass it to each product:
let core = Atatus.initialize(
with: Atatus.Configuration(licenseKey: "<LICENSE_KEY>", env: "<ENV_NAME>"),
trackingConsent: .granted,
instanceName: "secondary"
)
RUM.enable(with: RUM.Configuration(), in: core)
RUMMonitor.shared(in: core).startView(key: "checkout", name: "Checkout")
Every API that touches the SDK takes an in core: parameter, which defaults to the unnamed instance.
Debugging
Print what the SDK is doing, in debug builds only:
#if DEBUG
Atatus.verbosityLevel = .debug
#endif
To see view and action boundaries on the device while you work, turn on the RUM debug overlay:
RUMMonitor.shared().debug = true
Both are debugging aids. Leave them out of release builds, because the overlay draws over your interface and verbose logging prints SDK internals to the device console.
Next steps
- Report API calls as resources with Network Tracking.
- Measure launch time, frame rate, and hangs with Mobile Vitals.
- Report crashes and custom errors with Error Tracking.
+1-415-800-4104