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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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.

Note:

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:

copy
icon/buttons/copy
RUM.enable(
    with: RUM.Configuration(
        swiftUIViewsPredicate: DefaultSwiftUIRUMViewsPredicate()
    )
)
Warning:

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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
// 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.

Tip:

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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
Atatus.clearUserInfo()
Warning:

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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
RUMMonitor.shared().addAction(type: .tap, name: "Apply coupon")

addAction is for an instantaneous action. For one with a duration, bracket it:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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
        }
    )
)
Note:

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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
RUMMonitor.shared().stopSession()

To react to each new session, pass onSessionStart:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
// 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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
#if DEBUG
Atatus.verbosityLevel = .debug
#endif

To see view and action boundaries on the device while you work, turn on the RUM debug overlay:

copy
icon/buttons/copy
RUMMonitor.shared().debug = true
Warning:

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