Docs

Mobile SDKs

React Native and Expo, iOS in Swift, Android in Kotlin. The same calls, the same events, the same store as the web.

A native app sends the same batch payload as the browser with two conventions: screens are pageviews at app://<bundle id>/<Screen> (so the path is /<Screen> and every page report, journey and funnel works), and a device object replaces the User-Agent. Taps are clicks; errors and crashes are error events.

React Native and Expo

npm install @insight/clickiq-react-native @react-native-async-storage/async-storage
cd ios && pod install            # bare React Native only
App.tsx
import { ClickIQProvider, useScreenTracking } from "@insight/clickiq-react-native";
import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native";

export default function App() {
  const navigationRef = useNavigationContainerRef();
  useScreenTracking(navigationRef);      // one screen() per route change
  return (
    <ClickIQProvider options={{ key: "ciq_…", host: "https://clickiq.example.com", appVersion: "2.3.0" }}>
      <NavigationContainer ref={navigationRef}>{/* … */}</NavigationContainer>
    </ClickIQProvider>
  );
}
import { screen, track, tap, identify, reset, flush, optOut, error } from "@insight/clickiq-react-native";

screen("LessonList", { tab: "math" });          // pageview at app://<bundle>/LessonList
tap("Start lesson");                            // click with el.selector "LessonList/start-lesson"
track("Lesson started", { subject: "math" });   // custom event
identify("guid-rn-0001", { role: "student" });  // after login; persisted
reset();                                        // on logout
await flush();                                  // send now
optOut(true);                                   // stop and forget, persisted

React Native 0.72 or later, managed or bare Expo. Optional peers are picked up when installed: AsyncStorage (persistence of ids, session and queue), react-native-device-info or expo-constants/expo-device (model, version, bundle id). Uncaught JS errors are captured through ErrorUtils (the previous handler still runs); unhandled promise rejections on Hermes and JSC where the runtime allows.

iOS (Swift)

iOS 15+ (UIKit and SwiftUI) and tvOS 15+, Swift 6 with strict concurrency, no dependencies. Add the package with Swift Package Manager and configure once at launch.

Swift
import ClickIQ
import SwiftUI

@main
struct LearnApp: App {
    init() { ClickIQ.configure(key: "ciq_…", host: "https://clickiq.example.com") }
    var body: some Scene { WindowGroup { RootView() } }
}

// a screen: pageview on appear, page_leave with time on screen on disappear
List(lessons) { LessonRow($0) }.clickiqScreen("LessonList")

ClickIQ.tap("Start lesson", id: "start")
ClickIQ.track("Lesson started", props: ["subject": "math"])
ClickIQ.identify("guid-0001", traits: ["role": "student", "plan": "pro"])
ClickIQ.reset()

Crashes: the SDK chains an uncaught-exception handler and handles fatal signals, writes a small file, and sends an error with fatal: true on the next launch. It is not a symbolicating crash reporter; if you ship one, configure ClickIQ first or pass captureCrashes: false.

Android (Kotlin)

minSdk 24, no third-party dependencies. Two modules: clickiq-core (pure Kotlin) and clickiq (lifecycle, storage, device info, crash handler, Compose and Fragment helpers).

Kotlin
// app/build.gradle.kts
dependencies { implementation("com.insight.clickiq:clickiq:0.1.0") }

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        ClickIQ.init(this, key = "ciq_…", host = "https://clickiq.example.com",
            options = ClickIQOptions(autoActivityScreens = false, captureCrashes = true))
    }
}

composable("lessons") { ClickIQScreen("LessonList"); LessonListScreen() }   // Compose
ClickIQ.tap("Start lesson")
ClickIQ.track("Lesson started", mapOf("subject" to "math"))
ClickIQ.identify("user-123", mapOf("role" to "student"))

Until the library is published to a Maven repository, include it as a composite build (includeBuild("../insightclickiq-sdk-android")); the published coordinates will be com.insight.clickiq:clickiq.

Sessions, queue and delivery (all three)

BehaviourDetail
Sessions30 minutes of inactivity; time in the background counts. Returning later starts a new session and re-sends the screen on display
Offline queuePersisted on the device, 500 events, oldest dropped first; survives the app being killed
BatchesUp to 50 events every 10 s, when enough are waiting, on background, and on flush()
RetriesNetwork errors, 429 and 5xx back off (10 s, 20 s, 40 s …); other 4xx drop the batch
HeadersX-ClickIQ-Platform: ios|android; no Origin header, which the ingest accepts
Opt-outoptOut(true) clears ids, session and queue and stays off across launches

What the server stores

{"type": "pageview", "name": "screen", "url": "app://com.example.app/LessonList", "title": "LessonList",
 "device": {"platform": "android", "os": "Android", "os_version": "14", "model": "Google Pixel 7",
            "app_version": "2.3.0", "sdk": "clickiq-android", "sdk_version": "0.1.0", "device_type": "mobile"}}

The server stores platform, app_version and device_model as columns (filter and break down by them), sets os to “Android 14” and browser to the SDK name. Browser events carry platform = "web".

Taps for heatmaps

Taps carry their position (x as a fraction of the width, y in points from the top of the screen, or of the content inside a vertical scroll view), so the console draws tap, rage and dead layers per screen with a Web / iOS / Android switch. Three taps within 600 ms inside 24 points also send a rage_click; a tap on a view nothing interactive claimed sends a dead_click.

Turn it onElement names come from
iOSClickIQ.installTapCapture(window: window) (a passive recognizer; never cancels or delays touches)accessibilityIdentifier, accessibilityLabel, a button title
AndroidOn by default (captureTaps): every Activity’s touch dispatch is observed, never consumedcontentDescription, the TextView text, the resource id name; Compose taps keep coordinates only
React NativeWrap the app in <ClickIQTouchCapture> (it only listens)<ClickIQView label id>, testID, nativeID; otherwise coordinates only

A view marked private (below), or with an id starting with ciq-private, is counted without its label.

Mobile vitals

VitalWhatGood / poor
APP_STARTCold launch to the first frame after the first screen(), once per launch. iOS and Android from the process start; React Native from the JS bundle start (pass init({appStartTime}) to measure from the process)≤ 2 s / > 5 s
SCREEN_LOADscreen() to the next frame≤ 700 ms / > 2 s
FROZEN_FRAMESFrames over 700 ms in one screen view, sent with its page_leave2% / 10% of screen views
ANRMain thread blocked 2 s (iOS) or 5 s (Android), sent when it recovers; not on React Native0.47% / 1% of sessions

captureVitals is on by default; vital(name, value, props) sends your own measurement. Performance → Mobile shows them by platform, app version and screen.

Session replay and privacy

Native replay records masked screenshots: on a screen change, 0.3 s after a tap, and while the screen changes at most once a second (an unchanged screen sends nothing). JPEG at about half quality, at most 720 px wide, in a separate 5 MB queue sent after the events; replayOnCellular: false holds uploads for Wi-Fi. It records only when the project’s replay setting is on and the session is sampled in. The server refuses any frame that is neither masked on the device nor carries the regions it must paint.

iOSAndroidReact Native
Masked automaticallyUITextField, UITextView, secure entry, WKWebViewEditText, password inputs, WebViewClickIQTextInput, the focused TextInput
Mark your ownview.clickiqPrivate = true; SwiftUI .clickiqPrivate()view.clickiqPrivate(); Compose: ClickIQ.privateRegion(id, rect)<ClickIQPrivate>, useClickIQPrivate(ref)
Painted blackIn the renderer, before the image is encodedOn the bitmap, before compressionBy the server, before the frame is stored
Project setting mask_all_textEvery UILabel and SwiftUI textEvery TextViewThe whole frame is blurred
NeedsNothingACCESS_NETWORK_STATE only for Wi-Fi-only uploadsreact-native-view-shot (without it replay stays off)
// iOS
passwordField.clickiqPrivate = true            // text fields are already masked
Text(account.iban).clickiqPrivate()            // SwiftUI

// Android
pinView.clickiqPrivate()
Modifier.onGloballyPositioned { ClickIQ.privateRegion("pin", it.boundsInWindow().toAndroidRect()) }   // Compose

// React Native: plain TextInputs are not found automatically, so use the drop-in
<ClickIQTextInput placeholder="Email" value={email} onChangeText={setEmail} />
<ClickIQPrivate><Text>{user.cardNumber}</Text></ClickIQPrivate>

Measured on busy scripted sessions with a screen change every 3 seconds: about 24 frames a minute and 1.3–1.5 MB a minute uploaded; a calmer app (a change every 10 s) is about 300–500 KB a minute. Turn replay off with replay: false (React Native), .enabled = false (iOS) or ReplayOptions(enabled = false) (Android).

Feature flags and surveys

// React Native
if (isEnabled("new-onboarding")) { /* … */ }
const v = variant("checkout-redesign");            // "control" | "new" | true | false | undefined

// iOS
if ClickIQ.variant("checkout-redesign") == "new" { … }

// Android
if (ClickIQ.isEnabled("new-onboarding")) { … }

Flags are evaluated by the server on start (with the device object, so platform, app version and device type targeting works), after identify and after reset, and cached on the device so reads are synchronous from the next launch. The first read of each flag per session sends $flag_exposure. For surveys, the SDKs return the live list for this app (surveys()) and send the answers with surveyShown, surveyResponded and surveyDismissed; the app draws the questions itself. A project’s session sample rate applies here too (sampleRate overrides it).

Identify by account id

Identify people by a stable account id, never an email or a name. Only what you pass to the SDK, the taps and vitals it measures, and (if replay is on) masked frames leave the device.

Need a hand with the integration?

Send us your event list or your app's screen map and we will review it before you ship.

Talk to an engineer