Kubesense

iOS SDK Integration

This guide covers every module of the Kubesense iOS SDK (v1.0.0): adding the SDK, initializing the core, and enabling RUM, Logs, distributed tracing (including OpenTelemetry), Session Replay, crash reporting, WebView tracking, Feature Flags and profiling — plus automatic URLSession instrumentation.

FieldValue
PlatformsiOS 15.0+, tvOS 15.0+ (Session Replay and WebView tracking are iOS only)
LanguagesSwift and Objective-C
DistributionSwift Package Manager (https://github.com/kubesense-ai/kubesense-ios-sdk) and CocoaPods trunk
ModulesKubesenseCore (required), KubesenseRUM, KubesenseLogs, KubesenseTrace, KubesenseSessionReplay, KubesenseCrashReporting, KubesenseWebViewTracking, KubesenseFlags, KubesenseProfiling

Prerequisites

Before integrating, obtain the following from your Kubesense workspace:

ValueWhere to find it
applicationIDSettings → RUM Application Management → your application → View Details
clientTokenSettings → RUM Application Management → your application → View Details
kubesenseRumEndpointYour Kubesense collector host, for example us2.kubesense.ai

The client token is a public token meant for client-side SDKs, so it is safe to ship in the app. Never put a server API key in a mobile app.


Core Setup

1. Add the SDK

Swift Package Manager. In Xcode, choose File → Add Package Dependencies… and enter https://github.com/kubesense-ai/kubesense-ios-sdk, or add it to Package.swift:

dependencies: [
    .package(url: "https://github.com/kubesense-ai/kubesense-ios-sdk.git", from: "1.0.0")
],
targets: [
    .target(
        name: "MyApp",
        dependencies: [
            .product(name: "KubesenseCore", package: "kubesense-ios-sdk"),
            .product(name: "KubesenseRUM", package: "kubesense-ios-sdk"),
            // Add only the modules you use:
            .product(name: "KubesenseLogs", package: "kubesense-ios-sdk"),
            .product(name: "KubesenseCrashReporting", package: "kubesense-ios-sdk"),
        ]
    )
]

CocoaPods. Add one pod per module to your Podfile, then run pod install:

platform :ios, '15.0'
use_frameworks!

target 'MyApp' do
  pod 'KubesenseCore', '~> 1.0'
  pod 'KubesenseRUM', '~> 1.0'
  # Add only the modules you use:
  pod 'KubesenseLogs', '~> 1.0'
  pod 'KubesenseCrashReporting', '~> 1.0'
end

2. Initialize the SDK

Initialize the core as early as possible — in application(_:didFinishLaunchingWithOptions:) or your SwiftUI App initializer — before enabling any feature:

import KubesenseCore
import KubesenseRUM

Kubesense.initialize(
    with: Kubesense.Configuration(
        clientToken: "<CLIENT_TOKEN>",
        env: "production",
        service: "my-ios-app",
        // Single intake host for every feature. Host only — no scheme, no path.
        kubesenseRumEndpoint: "<your-collector-host>"
    ),
    trackingConsent: .granted
)

RUM.enable(with: RUM.Configuration(applicationID: "<RUM_APPLICATION_ID>"))

warning: One endpoint for every feature: RUM, Logs, Traces, Session Replay, Profiling and Flags all upload to kubesenseRumEndpoint. The value is a host, with an optional port — intake.example.com or intake.example.com:8443. Any scheme or path you include is ignored; uploads always go over HTTPS to https://<host>/rum/api/v1/.... Leave it out to upload to the site selected with site: — .prod (us2.kubesense.ai) by default.

SettingMeaning
clientTokenRequired. The RUM application's client token.
envRequired. The environment name you filter by in Kubesense, for example production or staging.
kubesenseRumEndpointYour collector host.
siteUsed when kubesenseRumEndpoint is not set: .prod (us2.kubesense.ai, default) or .staging.
service, versionIdentify the app. version defaults to the app's bundle version.
batchSize, uploadFrequency, batchProcessingLevelTune how much data is uploaded, and how often.
backgroundTasksEnabledUse background tasks to finish uploads when the app goes to the background.
remoteConfigurationEnabled, remoteConfigurationRefreshPeriodSee Remote Configuration.

Objective-C uses the same API with the Kubesense prefix: [KubesenseSDK initializeWithConfiguration:…], KubesenseConfiguration, KubesenseRUMConfiguration and so on.

To see SDK messages in the Xcode console while integrating, set Kubesense.verbosityLevel = .debug before Kubesense.initialize.


Remote Configuration

info: Enabled by default. Applications write no remote-configuration code — Kubesense.initialize() does all of it.

Sampling rates, feature switches and privacy masking can be managed centrally in Kubesense and delivered to the SDK, so you can retune or switch off a feature without shipping an app release. The iOS SDK uses the same configuration document as the Android SDK.

  1. Kubesense.initialize() reads the document cached by the previous launch — a local read, never a network call — and applies its settings before uploads start.
  2. Each X.enable() skips the feature if it is switched off remotely, then applies its own settings over what your code passed.
  3. A fresh document is fetched in the background for the next launch, and fetched again periodically.

warning: Configuration is applied at startup, so remote changes take effect at the next launch, not mid-session.

Kubesense.Configuration(
    clientToken: "<CLIENT_TOKEN>",
    env: "production",
    kubesenseRumEndpoint: "<your-collector-host>",
    remoteConfigurationEnabled: false,              // stay on in-code settings only
    remoteConfigurationRefreshPeriod: 6 * 60 * 60   // seconds; the default
)

Values below one minute are raised to one minute; zero or less fetches once after initialization (and when the app returns to the foreground), never on a timer. Missing, malformed or out-of-range values never override your code — see the Android SDK's Remote Configuration section for how the document is applied and which settings it controls.


Features

Real User Monitoring (RUM)

Module

KubesenseRUM (SPM product or pod).

Enable RUM

Automatic tracking of UIKit and SwiftUI views and actions is off until you pass a predicate:

import KubesenseRUM

RUM.enable(
    with: RUM.Configuration(
        applicationID: "<RUM_APPLICATION_ID>",
        sessionSampleRate: 100,
        uiKitViewsPredicate: DefaultUIKitRUMViewsPredicate(),
        uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate(),
        swiftUIViewsPredicate: DefaultSwiftUIRUMViewsPredicate(),
        swiftUIActionsPredicate: DefaultSwiftUIRUMActionsPredicate(isLegacyDetectionEnabled: true),
        urlSessionTracking: .init(
            firstPartyHostsTracing: .trace(hosts: ["api.example.com"], sampleRate: 20)
        ),
        trackBackgroundEvents: false,
        appHangThreshold: 0.25,
        trackWatchdogTerminations: true
    )
)
OptionDefaultMeaning
applicationID—Required. The RUM application ID.
sessionSampleRate100Percentage of sessions recorded (0–100).
uiKitViewsPredicate, uiKitActionsPredicatenilTrack UIViewControllers as views and UIKit taps as actions.
swiftUIViewsPredicate, swiftUIActionsPredicatenilTrack SwiftUI views and taps automatically.
urlSessionTrackingnilTrack URLSession requests as resources; see Network Requests.
trackFrustrationstrueDetect error taps and other frustration signals.
trackBackgroundEventsfalseKeep recording while the app is in the background.
longTaskThreshold0.1 sReport main-thread tasks longer than this.
appHangThresholdnil (off)Report app hangs longer than this many seconds.
trackWatchdogTerminationsfalseReport watchdog terminations.
vitalsUpdateFrequency.averageHow often CPU, memory and frame-rate vitals are sampled.
trackAnonymousUsertrueKeep a stable anonymous ID across sessions.
collectAccessibilityfalseCollect accessibility settings on views.
viewEventMapper, actionEventMapper, resourceEventMapper, errorEventMapper, longTaskEventMappernilEdit or drop events before they are sent.

SwiftUI views and actions

struct CheckoutView: View {
    var body: some View {
        Form { /* ... */ }
            .trackRUMView(name: "checkout")
    }
}

Button("Place order") { placeOrder() }
    .trackRUMTapAction(name: "Place order")

Manual view tracking

let rum = RUMMonitor.shared()
rum.startView(key: "checkout", name: "Checkout")
// ... when the screen goes away:
rum.stopView(key: "checkout")

// Or for a view controller:
rum.startView(viewController: self, name: "Checkout")

Manual action tracking

RUMMonitor.shared().addAction(type: .tap, name: "Place order")

// Continuous actions, such as a scroll:
RUMMonitor.shared().startAction(type: .scroll, name: "Product list")
RUMMonitor.shared().stopAction(type: .scroll, name: "Product list")

Manual error reporting

do {
    try placeOrder()
} catch {
    RUMMonitor.shared().addError(error: error, source: .source)
}

RUMMonitor.shared().addError(message: "Cart is empty", source: .source)

Custom timings, attributes and feature operations

let rum = RUMMonitor.shared()

// Time from the start of the current view:
rum.addTiming(name: "first_item_visible")

// On all following events, or on the current view only:
rum.addAttribute(forKey: "plan", value: "pro")
rum.addViewAttribute(forKey: "cart_items", value: 3)

// Multi-step flows and whether they succeed:
rum.startFeatureOperation(name: "checkout", operationKey: nil, attributes: [:])
rum.succeedFeatureOperation(name: "checkout", operationKey: nil, attributes: [:])

rum.currentSessionID { id in ... } returns the current session ID, and rum.stopSession() ends the session; the next view or action starts a new one.

Network Requests (URLSession)

With urlSessionTracking set on RUM.Configuration (or Trace.Configuration), requests made with URLSession are tracked automatically as RUM resources, and hosts listed in firstPartyHostsTracing (and their subdomains) receive distributed tracing headers. A host may contain one * wildcard, such as api-*.example.com.

To also capture the detailed timing breakdown (DNS, connection, TLS, first byte, download), enable it for your session delegate class:

class APISessionDelegate: NSObject, URLSessionDataDelegate {}

URLSessionInstrumentation.enableDurationBreakdown(
    with: .init(delegateClass: APISessionDelegate.self)
)

let session = URLSession(
    configuration: .default,
    delegate: APISessionDelegate(),
    delegateQueue: nil
)

note: List only your own hosts in firstPartyHostsTracing: third-party APIs may reject unknown tracing headers. Use .traceWithHeaders(hostsWithHeaders:sampleRate:) to choose the header formats per host.

Logs

Module

KubesenseLogs.

Enable Logs and create a logger

import KubesenseLogs

Logs.enable()

let logger = Logger.create(
    with: Logger.Configuration(
        name: "checkout",
        networkInfoEnabled: true,
        remoteLogThreshold: .info,  // lower levels stay on the device
        consoleLogFormat: .short    // also print to the Xcode console
    )
)

logger.info("Checkout started")
logger.warn("Slow payment provider")
logger.error("Payment failed", attributes: ["reason": "card_declined"])

logger.addAttribute(forKey: "cart_id", value: "c-123")  // on every log of this logger
logger.addTag(withKey: "team", value: "payments")

Levels are debug, info, notice, warn, error and critical. bundleWithRumEnabled and bundleWithTraceEnabled (both true by default) link each log to the current RUM session and active span.

Distributed Tracing

Module

KubesenseTrace.

Enable Trace

import KubesenseTrace

Trace.enable(
    with: Trace.Configuration(
        urlSessionTracking: .init(
            firstPartyHostsTracing: .trace(hosts: ["api.example.com"])
        ),
        networkInfoEnabled: true
    )
)

Create spans

let span = Tracer.shared().startSpan(operationName: "checkout")
span.setTag(key: "cart.items", value: 3)
// ... work ...
span.finish()

OpenTelemetry support

KubesenseTrace provides an OpenTelemetry tracer provider, so code instrumented with the OpenTelemetry API reports to Kubesense:

import OpenTelemetryApi

OpenTelemetry.registerTracerProvider(tracerProvider: OTelTracerProvider())

let tracer = OpenTelemetry.instance.tracerProvider.get(
    instrumentationName: "checkout",
    instrumentationVersion: nil
)
let span = tracer.spanBuilder(spanName: "place-order").startSpan()
// ... work ...
span.end()

Session Replay

Session Replay records what the user saw, as a video-like replay attached to the RUM session. It requires RUM and is available on iOS.

Module

KubesenseSessionReplay.

Enable Session Replay

Enable it after RUM:

import KubesenseSessionReplay

SessionReplay.enable(
    with: SessionReplay.Configuration(
        replaySampleRate: 20,                       // % of RUM sessions that get a replay
        textAndInputPrivacyLevel: .maskSensitiveInputs,
        imagePrivacyLevel: .maskNonBundledOnly,
        touchPrivacyLevel: .hide,
        startRecordingImmediately: true,
        featureFlags: [.swiftui: true]              // record SwiftUI views; off by default
    )
)

Privacy levels

SettingValues (default first)
textAndInputPrivacyLevelmaskAll, maskAllInputs, maskSensitiveInputs
imagePrivacyLevelmaskAll, maskNonBundledOnly, maskNone
touchPrivacyLevelhide, show

Override the levels for a single view and its subviews, or hide it entirely:

myView.kubesense.sessionReplayPrivacyOverrides.textAndInputPrivacy = .maskAll
myView.kubesense.sessionReplayPrivacyOverrides.hide = true

warning: SwiftUI views are only recorded with featureFlags: [.swiftui: true]. Without it, SwiftUI screens appear as placeholders in the replay.

Crash Reporting

Module

KubesenseCrashReporting (built on KSCrash).

Enable crash reporting

Enable it together with RUM (and Logs, if you use them), which receive the crash reports:

import KubesenseCrashReporting

CrashReporting.enable()

Crash reports are sent at the next launch. For app hangs and watchdog terminations, set appHangThreshold and trackWatchdogTerminations on RUM.Configuration.

WebView Tracking

If a page shown in a WKWebView already runs the Kubesense browser SDK, its events can join the app's RUM session.

Module

KubesenseWebViewTracking.

Setup

import KubesenseWebViewTracking

WebViewTracking.enable(webView: webView, hosts: ["shop.example.com"])

// When the web view is no longer used:
WebViewTracking.disable(webView: webView)

List only the hosts you trust.

Feature Flags

Module

KubesenseFlags.

Enable Flags and evaluate flags

import KubesenseFlags

Flags.enable()  // after Kubesense.initialize and RUM.enable

let flags = FlagsClient.create()
flags.setEvaluationContext(
    FlagsEvaluationContext(
        targetingKey: "user-123",
        attributes: ["plan": .string("pro")]
    )
) { result in
    // Flags are ready (or kept their defaults if loading failed).
}

let enabled = flags.getBooleanValue(key: "checkout.enabled", defaultValue: false)

getStringValue, getIntegerValue, getDoubleValue and getObjectValue work the same way, and each has a …Details variant that also returns the variant, the reason and any error.

Flags are read from a cache filled by one request when the evaluation context is set, so evaluating a flag never waits on the network. Flags.Configuration controls trackExposures, trackEvaluations and rumIntegrationEnabled (all true by default); with the RUM integration, each evaluation is added to the current RUM view.

note: Feature Flags need flag support on your Kubesense collector. Until it is available there, every flag returns its defaultValue.

Profiling

info: Experimental. Requires RUM.

Module

KubesenseProfiling.

Enable Profiling

import KubesenseProfiling

Profiling.enable(
    with: Profiling.Configuration(
        applicationLaunchSampleRate: 5,  // % of app launches profiled (default 5)
        continuousSampleRate: 5          // % of RUM sessions profiled continuously (default 5)
    )
)

Launch profiles cover the app's startup; continuous profiling records CPU and wall-time samples during sampled sessions and links them to long tasks and app hangs.


Identifying Users

Set the user after sign-in and clear it on sign-out. Use internal IDs rather than emails unless you're allowed to send them:

Kubesense.setUserInfo(id: user.id, name: user.displayName)
Kubesense.setAccountInfo(id: user.accountID)  // optional: the user's company or account

// On sign-out:
Kubesense.clearUserInfo()
Kubesense.clearAccountInfo()

The trackingConsent passed to Kubesense.initialize says whether the user has agreed to be tracked:

ValueEffect
.grantedData is collected and sent.
.pendingData is collected and kept on the device until you decide.
.notGrantedNothing is sent, and data waiting on the device is deleted.

If you ask for consent after startup, start with .pending, then call:

Kubesense.set(trackingConsent: .granted)  // or .notGranted

Verify the Integration

  1. Run the app, open a few screens, tap a button, make a network request, log a message and trigger a handled error.
  2. In Kubesense RUM, find the session under your application, environment and service, with its views, action, resource, log and error. It can take a minute to appear.
  3. If enabled, check for a replay with masked content, a crash report after relaunching a crashed app, and a WebView session linked to the app session.
ProblemWhat to check
No data in KubesensekubesenseRumEndpoint is the host only (no https://, no path); the client token and application ID belong to that collector; consent isn't .pending or .notGranted; the device can reach the collector over HTTPS. Set Kubesense.verbosityLevel = .debug.
No views or actionsPass uiKitViewsPredicate / swiftUIViewsPredicate (and the actions predicates), or track them manually.
CocoaPods: Unable to find a specification for KubesenseCoreYour local specs are out of date: run pod repo update, then pod install.
SwiftUI screens are blank in replaysEnable featureFlags: [.swiftui: true] on SessionReplay.Configuration.

Best Practices

  • Initialize early — call Kubesense.initialize() and the enable() calls at app launch, to capture the first screen and early errors.
  • Enable RUM first — then Logs, Trace, Session Replay, Flags and crash reporting, which all correlate with the RUM session.
  • Pass the view and action predicates — without them, nothing is tracked automatically.
  • List only your own hosts as first-party — tracing headers go to every listed host and its subdomains.
  • Keep Session Replay masked by default — relax privacy only on screens you've checked, with sessionReplayPrivacyOverrides.
  • Provide safe flag defaults — the defaultValue is what users get when flags can't be loaded.
  • Start with .pending consent when you need consent first; data is uploaded only after .granted.