Flamio On-Live

Record your mobile app

Send sessions from your iOS or Android app to On-Live with the official Datadog SDK — there is no separate Flamio mobile SDK.

Flamio On-Live records native apps as well as websites. You add the official Datadog RUM and Session Replay packages for your stack and point them at Flamio. App sessions land in the same Visits, issues, funnels and analytics as your website.

Supported stacks: iOS (SwiftUI and UIKit), Android (Jetpack Compose and Views), React Native, Flutter and Kotlin Multiplatform.

Before you begin

  • On-Live is enabled for the project and a project key exists — steps 1 and 2 of Install the web SDK. Those steps need the Editor, Admin or Owner role. The app uses the same project key as the website.
  • You need access to your app's code and a way to ship a build. The packages and the config go into your app — Flamio cannot add them for you.

Open the mobile install

In the sidebar, under Flamio On-Live, click Setup.

  • A project that covers a mobile app shows the card Install the mobile SDK, already open. A project that covers only an app shows it as step 3.
  • A project that covers only a website keeps the same card folded under Have a mobile app?. Click it to open.

The card reads Point the official Datadog SDK at Flamio — no separate mobile SDK to install.

Pick your stack and copy the config

Select your stack on the tabs — iOS, Android, React Native, Flutter or Kotlin Multiplatform — and copy the config block. Right after you generate the key, the Setup page fills it into the block. The configs and their placeholders are also listed under Configs by stack.

Fill in the placeholders

The line under the block, Yours to fill in, names the values the config leaves to you — each stack names only the ones its own config carries:

  • your-api-host.com — your own backend host, so your app's requests appear in the replay.
  • com.your.app — your app's bundle id (iOS) or package name (Android).
  • currentUser — your signed-in user. Drop the setUserInfo call if you do not identify users.

Install the packages and paste the config

In your app, install the Datadog RUM and Session Replay packages that the config imports. Paste each block where its comment says, and the rest where your app starts.

Allow your app id

Click Settings > Project settings. Under Allowed apps (Only these app ids can send sessions. Leave empty to allow any.), enter your bundle id or package name, one per line, and click Save.

Run the app

Build and run the app, and use it for a minute.

A mobile session ends only after about 30 minutes without activity, so it appears in Flamio On-Live > Visits about 30 minutes after your last tap.

The key ships inside your app

Anyone who unpacks your app can read the project key. Allowed apps is the only check on who may send sessions with it, so fill it in before you release the build.

Result

App sessions appear in Visits next to your web visits. To see only the app, set the Source filter to iOS or Android. Dead, rage and error taps on your app's controls become issues, the same as clicks on the web.

What the app records

  • Text and images are recorded as they appear. Only sensitive inputs — passwords, emails and phone numbers — are masked. The image-privacy and text-and-input-privacy lines in the config are what change either.
  • The content of a WebView is not recorded. Leave Datadog's WebView tracking off, as the config says.
  • Flutter: the config pins Datadog's Session Replay preview packages. A WebViewWidget or any other platform view records nothing.
  • Kotlin Multiplatform: Android Compose and iOS SwiftUI or UIKit screens are recorded; Compose Multiplatform UI records as one blank shape. Name every screen yourself with the startView and stopView calls in the config — replace catalog with each screen's own name.

Configs by stack

YOUR_KEY_HERE in each config is your project key from step 2 of the Setup page. The line under each config lists the other values to fill in.

iOS

import DatadogCore
import DatadogRUM
import DatadogSessionReplay

// Do not enable Datadog WebView tracking: a WebView's own content is not
// recorded, and bridged WebView data is recognised, counted and dropped.

Datadog.initialize(
  with: Datadog.Configuration(
    clientToken: "YOUR_KEY_HERE",
    env: "prod",
    service: Bundle.main.bundleIdentifier ?? "app"
  ),
  trackingConsent: .granted)

RUM.enable(with: RUM.Configuration(
  applicationID: Bundle.main.bundleIdentifier ?? "app",
  // Name your screens yourself — .trackRUMView(name:) on a SwiftUI view,
  // startView/stopView in a UIKit controller. Both *views* predicates stay nil:
  // a default one names views by type on top of your names, so every screen
  // gets a second identity. Both *actions* predicates stay on — a tap is
  // hit-tested by exactly one framework, so they never compete for it.
  uiKitViewsPredicate: nil,
  uiKitActionsPredicate: DefaultUIKitRUMActionsPredicate(),
  swiftUIViewsPredicate: nil,
  swiftUIActionsPredicate: DefaultSwiftUIRUMActionsPredicate(isLegacyDetectionEnabled: false),
  urlSessionTracking: .init(firstPartyHostsTracing: .trace(hosts: ["your-api-host.com"])),
  trackBackgroundEvents: true,
  customEndpoint: URL(string: "https://api.flamio.org/api/v1/mobile/rum")))

URLSessionInstrumentation.enable(with: .init(delegateClass: DatadogURLSessionDelegate.self))

SessionReplay.enable(with: SessionReplay.Configuration(
  replaySampleRate: 100,
  textAndInputPrivacyLevel: .maskSensitiveInputs,
  imagePrivacyLevel: .maskNone,
  touchPrivacyLevel: .show,
  customEndpoint: URL(string: "https://api.flamio.org/api/v1/mobile/replay"),
  // Without this every SwiftUI screen records as one grey placeholder.
  featureFlags: [.swiftui: true]))

Datadog.setUserInfo(id: currentUser.id, name: currentUser.name, email: currentUser.email)

Yours to fill in: your-api-host.com is your own backend host; currentUser is your signed-in user — drop the setUserInfo call if you don’t identify users.

The bundle id is read from your app.

Android

// Do not enable Datadog WebView tracking: a WebView's own content is not
// recorded, and bridged WebView data is recognised, counted and dropped.
Datadog.initialize(
  this,
  Configuration.Builder(clientToken = "YOUR_KEY_HERE", env = "prod", service = packageName).build(),
  TrackingConsent.GRANTED
)

Rum.enable(RumConfiguration.Builder(applicationId = packageName)
  .useCustomEndpoint("https://api.flamio.org/api/v1/mobile/rum")
  .trackBackgroundEvents(true)
  .trackUserInteractions()
  // Without this every Compose tap is unnamed, so a Compose screen reports no
  // dead, rage or error clicks — trackUserInteractions() alone does not cover it.
  .enableComposeActionTracking()
  // Keep this even in a Compose app: it is the only thing that names an XML
  // Activity. Compose destinations name themselves through NavigationEffect
  // in the nav graph, and Flamio folds the host Activity into them.
  .useViewTrackingStrategy(ActivityViewTrackingStrategy(true))
  .build())

val client = OkHttpClient.Builder()
  .addInterceptor(DatadogInterceptor.Builder(listOf("your-api-host.com")).build())
  .eventListenerFactory(DatadogEventListener.Factory())
  .build()

SessionReplay.enable(SessionReplayConfiguration.Builder(sampleRate = 100f)
  .useCustomEndpoint("https://api.flamio.org/api/v1/mobile/replay")
  .setTextAndInputPrivacy(TextAndInputPrivacy.MASK_SENSITIVE_INPUTS)
  .setImagePrivacy(ImagePrivacy.MASK_NONE)
  .setTouchPrivacy(TouchPrivacy.SHOW)
  .addExtensionSupport(ComposeExtensionSupport())
  .build())

Datadog.setUserInfo(id = currentUser.id, name = currentUser.name, email = currentUser.email)

Yours to fill in: your-api-host.com is your own backend host; currentUser is your signed-in user — drop the setUserInfo call if you don’t identify users.

The package name is read from your app.

React Native

import {
  DatadogProvider,
  DatadogProviderConfiguration,
  DdSdkReactNative,
  RumConfiguration,
  TrackingConsent,
} from '@datadog/mobile-react-native';
import { DdRumReactNavigationTracking } from '@datadog/mobile-react-navigation';
import {
  ImagePrivacyLevel,
  SessionReplay,
  TextAndInputPrivacyLevel,
  TouchPrivacyLevel,
} from '@datadog/mobile-react-native-session-replay';
import type { SessionReplayConfiguration } from '@datadog/mobile-react-native-session-replay';

// Paper and Fabric, iOS and Android. A minified Android release also needs
// -keep class com.facebook.react.fabric.** { *; } and
// -keep class com.facebook.jni.** { *; } in proguard-rules.pro.
// Do not enable Datadog WebView tracking: a WebView's own content is not
// recorded, and bridged WebView data is recognised, counted and dropped.
const datadogConfiguration = new DatadogProviderConfiguration(
  'YOUR_KEY_HERE',
  'prod',
  TrackingConsent.GRANTED,
  {
    service: 'com.your.app',
    rumConfiguration: new RumConfiguration('com.your.app', true, true, true, {
      customEndpoint: 'https://api.flamio.org/api/v1/mobile/rum',
      // Required: without it every tap is named "RCTView", the React element
      // type, so no dead, rage or error click lands on any one control.
      actionNameAttribute: 'testID',
      trackBackgroundEvents: true,
      sessionSampleRate: 100,
    }),
  },
);

const sessionReplayConfiguration: SessionReplayConfiguration = {
  replaySampleRate: 100,
  textAndInputPrivacyLevel: TextAndInputPrivacyLevel.MASK_SENSITIVE_INPUTS,
  imagePrivacyLevel: ImagePrivacyLevel.MASK_NONE,
  touchPrivacyLevel: TouchPrivacyLevel.SHOW,
  customEndpoint: 'https://api.flamio.org/api/v1/mobile/replay',
};

// Your app's root; navigationRef is your NavigationContainer's ref.
<DatadogProvider
  configuration={datadogConfiguration}
  onInitialization={() => {
    SessionReplay.enable(sessionReplayConfiguration);
  }}>
  <NavigationContainer
    ref={navigationRef}
    onReady={() => {
      DdRumReactNavigationTracking.startTrackingViews(
        navigationRef.current,
      );
    }}>
  </NavigationContainer>
</DatadogProvider>

// Once the user signs in (after DatadogProvider has initialized):
DdSdkReactNative.setUserInfo({
  id: currentUser.id,
  name: currentUser.name,
  email: currentUser.email,
});

Yours to fill in: com.your.app is your app’s bundle id; currentUser is your signed-in user — drop the setUserInfo call if you don’t identify users.

Flutter

import 'package:datadog_flutter_plugin/datadog_flutter_plugin.dart';
import 'package:datadog_session_replay/datadog_session_replay.dart';
import 'package:datadog_tracking_http_client/datadog_tracking_http_client.dart';

// datadog_flutter_plugin 3.5.1 with datadog_session_replay 1.0.0-preview.14 and
// datadog_tracking_http_client 3.2.0. Pin all three exactly: Session Replay is
// a preview at this version and may change its API without a major bump.
// A WebViewWidget — and any platform view — records nothing at all: the screen
// arrives as its AppBar and nothing else, because the preview has no
// platform-view recorder.
// Do not enable Datadog WebView tracking: a WebView's own content is not
// recorded, and bridged WebView data is recognised, counted and dropped.
DatadogConfiguration buildDatadogConfiguration() {
  final configuration = DatadogConfiguration(
    clientToken: 'YOUR_KEY_HERE',
    env: 'prod',
    // Required by the constructor; the two custom endpoints decide where data goes.
    site: DatadogSite.us1,
    service: 'com.your.app',
    rumConfiguration: DatadogRumConfiguration(
      applicationId: 'com.your.app',
      customEndpoint: 'https://api.flamio.org/api/v1/mobile/rum',
      sessionSamplingRate: 100,
      detectLongTasks: true,
      trackBackgroundEvents: true,
    ),
  )..enableSessionReplay(
      DatadogSessionReplayConfiguration(
        replaySampleRate: 100,
        textAndInputPrivacyLevel: TextAndInputPrivacyLevel.maskSensitiveInputs,
        imagePrivacyLevel: ImagePrivacyLevel.maskNone,
        touchPrivacyLevel: TouchPrivacyLevel.show,
        customEndpoint: 'https://api.flamio.org/api/v1/mobile/replay',
      ),
    )
    // A dart:io HttpClient call is a RUM resource only through this override, so
    // without it your requests are not in the network channel at all. It tracks
    // every URL the app opens; firstPartyHosts would only add tracing headers.
    ..enableHttpTracking();
  return configuration;
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await DatadogSdk.runApp(
    buildDatadogConfiguration(),
    TrackingConsent.granted,
    () async => runApp(const MyApp()),
  );
}

// In your root widget's State: only what SessionReplayCapture wraps is
// recorded, and it needs a stable GlobalKey.
final _captureKey = GlobalKey();
late final DatadogNavigationObserver _navigationObserver =
    DatadogNavigationObserver(datadogSdk: DatadogSdk.instance);

@override
Widget build(BuildContext context) {
  return SessionReplayCapture(
    key: _captureKey,
    rum: DatadogSdk.instance.rum!,
    sessionReplay: DatadogSessionReplay.instance!,
    child: MaterialApp(
      navigatorObservers: [_navigationObserver],
    ),
  );
}

// Around every tappable control. Without it a tap is named after its visible
// label, so two controls that share a caption share one identity — and
// Semantics(identifier:) is never read by the SDK.
RumUserActionAnnotation(
  description: 'add_to_cart',
  child: ElevatedButton(onPressed: addToCart, child: const Text('Add to cart')),
)

// Once the user signs in:
DatadogSdk.instance.setUserInfo(id: currentUser.id, name: currentUser.name, email: currentUser.email);

Yours to fill in: com.your.app is your app’s bundle id; currentUser is your signed-in user — drop the setUserInfo call if you don’t identify users.

Kotlin Multiplatform

// dd-sdk-kotlin-multiplatform 1.8.0, plus dd-sdk-kotlin-multiplatform-ktor3 for the
// Ktor client and dd-sdk-android-session-replay-compose in androidMain.
// Android Compose and iOS SwiftUI/UIKit screens only —
// Compose Multiplatform UI is not supported: it records as one blank shape.
// The endpoint must be https: a Kotlin Multiplatform Android app cannot upload
// over clear-text http at all.
// Do not enable Datadog WebView tracking: a WebView's own content is not
// recorded, and bridged WebView data is recognised, counted and dropped.

// commonMain
import com.datadog.kmp.Datadog
import com.datadog.kmp.core.configuration.Configuration
import com.datadog.kmp.ktor.datadogKtorPlugin
import com.datadog.kmp.privacy.TrackingConsent
import com.datadog.kmp.rum.Rum
import com.datadog.kmp.rum.RumMonitor
import com.datadog.kmp.rum.configuration.RumConfiguration
import com.datadog.kmp.sessionreplay.SessionReplay
import com.datadog.kmp.sessionreplay.configuration.ImagePrivacy
import com.datadog.kmp.sessionreplay.configuration.SessionReplayConfiguration
import com.datadog.kmp.sessionreplay.configuration.TextAndInputPrivacy
import com.datadog.kmp.sessionreplay.configuration.TouchPrivacy
import io.ktor.client.HttpClient

internal expect fun applyPlatformRumTracking(builder: RumConfiguration.Builder): RumConfiguration.Builder

internal expect fun applyPlatformSessionReplay(
    builder: SessionReplayConfiguration.Builder,
): SessionReplayConfiguration.Builder

Datadog.initialize(
    platformContext,
    Configuration.Builder(
        clientToken = "YOUR_KEY_HERE",
        env = "prod",
        service = "com.your.app",
    ).build(),
    TrackingConsent.GRANTED,
)
Rum.enable(
    applyPlatformRumTracking(
        RumConfiguration.Builder("com.your.app")
            .useCustomEndpoint("https://api.flamio.org/api/v1/mobile/rum")
            .trackBackgroundEvents(true),
    ).build(),
)
SessionReplay.enable(
    applyPlatformSessionReplay(
        SessionReplayConfiguration.Builder(100f)
            .useCustomEndpoint("https://api.flamio.org/api/v1/mobile/replay")
            .setTextAndInputPrivacy(TextAndInputPrivacy.MASK_SENSITIVE_INPUTS)
            .setImagePrivacy(ImagePrivacy.MASK_NONE)
            .setTouchPrivacy(TouchPrivacy.SHOW),
    ).build(),
)

// Name every screen yourself: the multiplatform SDK bridges no view-tracking
// strategy on either OS, so without these two calls a session arrives with no
// pages at all. On Android call them as each screen appears and goes away — a
// Compose destination's LifecycleResumeEffect, a plain Activity's
// onResume/onPause. On iOS name SwiftUI screens with dd-sdk-ios's own
// .trackRUMView(name:) modifier on each screen's root view instead.
RumMonitor.get().startView("catalog", "catalog")
RumMonitor.get().stopView("catalog")

// Your API calls go through one Ktor client with the Datadog plugin. Without it
// no request is a RUM resource, so no error click fires on either OS.
val client = HttpClient { install(datadogKtorPlugin(tracedHosts = listOf("your-api-host.com"))) }

Datadog.setUserInfo(id = currentUser.id, name = currentUser.name, email = currentUser.email)

// androidMain
// The two platform calls are extension functions — and ComposeExtensionSupport
// comes from the Android SDK, not the multiplatform one.
import com.datadog.android.sessionreplay.compose.ComposeExtensionSupport
import com.datadog.kmp.rum.configuration.trackUserInteractions
import com.datadog.kmp.sessionreplay.configuration.addExtensionSupport

internal actual fun applyPlatformRumTracking(
    builder: RumConfiguration.Builder,
): RumConfiguration.Builder = builder.trackUserInteractions()

// Required: without it a Compose screen is not recorded at all.
internal actual fun applyPlatformSessionReplay(
    builder: SessionReplayConfiguration.Builder,
): SessionReplayConfiguration.Builder = builder.addExtensionSupport(ComposeExtensionSupport())

// A Compose tap on Android exists only where your app calls
// RumMonitor.get().addAction(...): Kotlin Multiplatform has no bridge to
// Datadog's Compose action tracking, and a tap on a disabled control yields none.

// iosMain
import com.datadog.kmp.rum.configuration.trackUiKitActions
import com.datadog.kmp.sessionreplay.configuration.enableSwiftUISupport

// Required: a UIKit control cannot name its own tap otherwise. A SwiftUI tap is
// named by .trackRUMTapAction(name:) on the control — leave trackSwiftUIActions
// off, or the tap reports the class (SwiftUI_Button) instead of your name.
internal actual fun applyPlatformRumTracking(
    builder: RumConfiguration.Builder,
): RumConfiguration.Builder = builder.trackUiKitActions()

// Required: without it every SwiftUI screen records as one placeholder.
internal actual fun applyPlatformSessionReplay(
    builder: SessionReplayConfiguration.Builder,
): SessionReplayConfiguration.Builder = builder.enableSwiftUISupport(true)

Yours to fill in: your-api-host.com is your own backend host; com.your.app is your app’s bundle id; currentUser is your signed-in user — drop the setUserInfo call if you don’t identify users.

Also replace catalog with each screen's own name.