Browse docs

Explore by section, then jump directly into a page.

Mobile SDK

React Native SDK

Full lifecycle analytics for iOS and Android — screens, deep links, crashes, experiments, and push notifications.

The React Native SDK extends the core SDK with mobile-specific features: app lifecycle, screen views via React Navigation, deep-link capture, native crash reporting, and push notification lifecycle helpers.

Installation

1

Install packages

Terminal
npm install @flash-analytics/react-native@2.1.19 react-native-device-info
Terminal
cd ios && pod install
react-native-device-info is a required peer dependency for device metadata. Run pod install after adding it on iOS.
For persistent batching across force-kills, also install @react-native-async-storage/async-storage. Without it, the SDK still batches in memory for the current app process.
2

Create an analytics module

Initialize the SDK once and export both the analytics instance and the navigation ref.

src/analytics.tsTypeScript
import { createNavigationContainerRef } from '@react-navigation/native';
import { FlashAnalytics } from '@flash-analytics/react-native';

export const navigationRef = createNavigationContainerRef();

export const analytics = new FlashAnalytics({
  appId: process.env.FLASH_APP_ID!, // must be a UUID
  captureAppLifecycle: true,
  captureDeepLinks: true,
  captureScreenViews: true,
  captureErrors: true,
  captureNativeCrashes: true,
  captureVariants: true,
  onSessionUpdated(session) {
    console.log(session.id);
  },
  deliveryStrategy: {
    strategy: 'batching',
    config: {
      size: 10,
      timeout: 5000,
      fallbackToIndividual: true,
    },
  },
});

analytics.setNavigationRef(navigationRef, (routeName, params) => {
  if (routeName === 'Product') return `/product/${(params as any)?.id}`;
  return `/${routeName.toLowerCase()}`;
});
3

Connect React Navigation

App.tsxTSX
import { NavigationContainer } from '@react-navigation/native';
import { navigationRef } from './analytics';

export function App() {
  return (
    <NavigationContainer ref={navigationRef}>
      {/* your navigators */}
    </NavigationContainer>
  );
}
4

Identify users and track events

TypeScript
import { analytics } from './analytics';

// After login
await analytics.identify({
  profileId: 'user_123',
  email: 'user@example.com',
});

// Track product events
await analytics.track('product_added_to_cart', {
  productId: 'sku_123',
  price: 29.99,
});

Auto-capture flags

captureAppLifecycle

app_opened, app_foregrounded, app_backgrounded

captureSessionOnInit

sdk_initialized session warm-up before lifecycle events

captureScreenViews

screen_view on each navigation change

captureDeepLinks

deep_link_opened for URL opens

captureInstallAttribution

Install / reopen attribution

captureErrors

Global JS errors and unhandled rejections

captureNativeCrashes

native_crash on next app launch

capturePushLifecycle

Push notification lifecycle helpers

captureVariants

A/B experiment auto-assignment

Error tracking

TypeScript
try {
  await checkout();
} catch (error) {
  analytics.trackError(error, 'checkout_failed', {
    orderId: cart.orderId,
  });
}

Experiments

TypeScript
// All cached assignments — no API call
const all = analytics.getAllExperiments();

// Cache first, API fallback
const assignment = await analytics.getExperimentById('checkout-cta');
console.log(assignment?.variantName);

// Manual refresh
const assignments = await analytics.autoAssignExperiments();

Push notifications

The JS layer handles what it can. For full coverage you need native host code on both platforms.

EventJS onlyNative required
notification_openedNone — JS handler is enough
notification_delivered (iOS)UNNotificationServiceExtension
notification_expired (iOS)UNNotificationServiceExtension timeout
notification_dismissed (Android)BroadcastReceiver + PendingIntent
notification_action_clicked (Android)BroadcastReceiver + PendingIntent

JS — enable push tracking and handle opens

TypeScript
import { enableFlashPushTracking } from '@flash-analytics/react-native';

enableFlashPushTracking({
  appId: process.env.FLASH_APP_ID!,
  android: {
    defaultChannelId: 'default',
    defaultNotificationTtlMs: 5 * 60 * 1000, // 5 min TTL for expiry inference
  },
});

// Track when the user taps a notification (works from JS)
await analytics.trackNotificationOpened({
  notificationId: payload.notificationId,
  messageId: payload.messageId,
  provider: 'firebase',
});

iOS — Notification Service Extension (delivery + expiry)

In Xcode: File → New → Target → Notification Service Extension. Your push payload must include "mutable-content": 1.

NotificationService.swift (iOS Extension target)Swift
import UserNotifications
import FlashAnalytics

class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?
    var extensionAnalytics: FlashAnalytics?

    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        self.contentHandler = contentHandler
        bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent

        extensionAnalytics = FlashAnalytics.configureShared(
            options: FlashAnalyticsOptions(
                appId: "YOUR_APP_ID",
                endpoint: "https://api.flashanalytics.app",
                capturePushLifecycle: true
            )
        )

        // Tracks notification_delivered
        extensionAnalytics?.trackNotificationDelivered(request: request)
        contentHandler(bestAttemptContent ?? request.content)
    }

    override func serviceExtensionTimeWillExpire() {
        // Tracks notification_expired
        if let content = bestAttemptContent {
            extensionAnalytics?.trackNotificationExpired(content: content)
        }
        contentHandler?(bestAttemptContent ?? UNNotificationContent())
    }
}

Also set UNUserNotificationCenterDelegate in your AppDelegate and call FlashAnalytics.shared.trackNotificationResponse(response) inside didReceive(_:withCompletionHandler:) to capture open, dismiss, and action events. See the Swift SDK push section for the full delegate code.

Android — BroadcastReceiver (dismiss + action)

Android has no OS-level delivery callback. The SDK infers notification_expired from the payload's expiresAt field or defaultNotificationTtlMs.
FlashNotificationReceiver.ktKotlin
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import app.flashanalytics.sdk.FlashNotificationEvent
import app.flashanalytics.sdk.android.FlashAnalyticsAndroid

class FlashNotificationReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val analytics = FlashAnalyticsAndroid.getInstance()

        val event = when (intent.action) {
            "FLASH_NOTIFICATION_DISMISSED"     -> FlashNotificationEvent.DISMISSED
            "FLASH_NOTIFICATION_ACTION"        -> FlashNotificationEvent.ACTION_CLICKED
            else -> return
        }

        analytics.trackNotificationEvent(
            event = event,
            intent = intent,
            source = "android_broadcast_receiver",
            appState = "background",
        )
    }
}
AndroidManifest.xmlXML
<receiver
    android:name=".FlashNotificationReceiver"
    android:exported="false">
    <intent-filter>
        <action android:name="FLASH_NOTIFICATION_DISMISSED" />
        <action android:name="FLASH_NOTIFICATION_ACTION" />
    </intent-filter>
</receiver>

When building your notification with NotificationCompat.Builder, attach a PendingIntent pointing at FlashNotificationReceiver for the delete action (dismiss) and each action button.

Building the notificationKotlin
val dismissIntent = Intent(context, FlashNotificationReceiver::class.java).apply {
    action = "FLASH_NOTIFICATION_DISMISSED"
    putExtra("notificationId", notifId)
    putExtra("provider", "firebase")
}
val dismissPi = PendingIntent.getBroadcast(
    context, 0, dismissIntent,
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)

NotificationCompat.Builder(context, CHANNEL_ID)
    .setContentTitle(title)
    .setContentText(body)
    .setDeleteIntent(dismissPi) // fires DISMISSED when swiped away
    .build()

Session access

By default the SDK sends a lightweight sdk_initialized event when it is constructed. That lets the backend create and return a session before app lifecycle, install, update, and foreground events are sent. Use onSessionUpdated if your app needs that session as soon as the startup response returns. Set captureSessionOnInit: false only if you do not want that startup event.

TypeScript
const analytics = new FlashAnalytics({
  appId: process.env.FLASH_APP_ID!,
  captureSessionOnInit: true,
  onSessionUpdated(session) {
    console.log(session.id);
    console.log(session.estimatedExpiresAt);
    console.log(session.estimatedTtlMs);
  },
});

const session = analytics.getSession();
console.log(session?.id);
console.log(session?.estimatedExpiresAt);
console.log(session?.estimatedTtlMs);
Disable startup session warm-upTypeScript
const analytics = new FlashAnalytics({
  appId: process.env.FLASH_APP_ID!,
  captureSessionOnInit: false,
});

Batching

React Native supports the same deliveryStrategy batching API as the core SDK. The SDK flushes by queue size, by timeout, and when the app moves to background or inactive state. Batch responses update getSession() and trigger onSessionUpdated.

TypeScript
const analytics = new FlashAnalytics({
  appId: process.env.FLASH_APP_ID!,
  deliveryStrategy: {
    strategy: 'batching',
    config: {
      size: 10,                    // flush after 10 queued events
      timeout: 5000,               // or every 5 seconds
      fallbackToIndividual: true,  // retry individually if batch fails
    },
  },
});

// Flush manually before a critical transition if needed
await analytics.flushBatch();
If @react-native-async-storage/async-storage is installed, queued batch events are persisted so they can survive app force-kills. If it is not installed, batching still works in memory for the active process.

Related guides