npm package: @riuve/expo
Description: Behavioral event tracking SDK for React Native with offline support and intelligent batching
Latest version: 2.0.1

# Riuve SDK - React Native/Expo

> **Behavioral event tracking SDK for React Native and Expo applications**

---

## Table of Contents

1. [What is Riuve SDK?](#what-is-riuve-sdk)
2. [Installation](#installation)
3. [Quick Start](#quick-start)
4. [Advanced Features](#advanced-features)
5. [Best Practices](#best-practices)
6. [Troubleshooting](#troubleshooting)
7. [API Reference](#api-reference)

---

## What is Riuve SDK?

**Riuve SDK** is a behavioral event tracking library for React Native and Expo applications.

### Features

- ✅ **Offline Support**: Events are queued when offline and automatically sent when connection is restored
- ✅ **Intelligent Batching**: Events are automatically batched for optimal performance
- ✅ **Automatic Retry**: Failed requests are automatically retried with exponential backoff
- ✅ **Background Flush**: Events are automatically flushed when app goes to background
- ✅ **User Identification**: Manages both anonymous and external user IDs
- ✅ **Push Notification Support**: FCM token management
- ✅ **Zero Configuration**: Backend URL is hardcoded, only API key is required
- ✅ **Pending Events Queue**: Events tracked before SDK initialization are automatically processed

### Use Cases

- Track user behavior and interactions in your app
- Monitor which screens are viewed most
- Track button clicks, form submissions, and user actions
- Segment users based on their behavior
- Send targeted push notifications

---

## Installation

### 1. Install the Package

```bash
npm install @riuve/expo
```

or

```bash
yarn add @riuve/expo
```

### 2. Install a Storage Backend

The SDK needs a storage backend for persisting events and user data. Choose **one** of the following:

**Option A: AsyncStorage (default)**

```bash
npm install @react-native-async-storage/async-storage
```

**Option B: MMKV (recommended for performance)**

```bash
npm install react-native-mmkv
```

> MMKV is a high-performance synchronous storage library. If your project already uses MMKV, you can use it with the SDK — no need to install AsyncStorage. See [MMKV Storage](#6-mmkv-storage) for setup.

### 3. (Optional) Expo Modules

If you're using Expo, install these for device info and network monitoring:

```bash
npx expo install expo-device expo-application expo-localization expo-network
```

### 4. (Optional) Native Modules

For better performance with native modules (Development Build or Bare React Native):

```bash
npm install react-native-device-info @react-native-community/netinfo
```

---

## Quick Start

### Step 1: Initialize the SDK

Initialize the SDK at the start of your app (typically in `App.tsx` or `_layout.tsx`):

```typescript
import { useEffect } from 'react';
import Riuve from '@riuve/expo';

export default function App() {
  useEffect(() => {
    initializeSDK();
  }, []);

  const initializeSDK = async () => {
    try {
      // Get your API key from Riuve dashboard
      await Riuve.initialize('riuve_base_xxxxxxxxxxxxxxxxxxxxx', {
        // Basic configuration
        batchSize: 10,        // Number of events per batch (default: 10)
        batchTimeout: 30000,  // Auto-send after 30 seconds (default: 30000ms)
      });

      console.log('✅ SDK initialized successfully');
    } catch (error) {
      console.error('❌ SDK initialization error:', error);
    }
  };

  return (
    // Your app content
  );
}
```

**Important Notes:**
- The `baseUrl` is hardcoded in the SDK (`https://api.riuve.com`), no need to pass it
- Get your API key from the [Riuve Dashboard](https://dashboard.riuve.com)

### Step 2: Identify Users

When a user logs in or user information is available:

```typescript
import Riuve from '@riuve/expo';

// When user logs in
const handleLogin = async (userId: string, userEmail: string) => {
  try {
    await Riuve.identify(userId, {
      email: userEmail,
      name: 'User Name',
      user_type: 'premium',
      // Add any custom properties you want
    });

    console.log('✅ User identified');
  } catch (error) {
    console.error('❌ Identify error:', error);
  }
};
```

**When to Identify:**
- When user logs in
- When user information is updated
- On app start (if user is already logged in)

### Step 3: Track Events

Track important actions in your app:

```typescript
import Riuve from '@riuve/expo';
import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';

// Screen view tracking (use useFocusEffect for tabs)
useFocusEffect(
  useCallback(() => {
    Riuve.track('screen_viewed', {
      screen_name: 'Home',
      screen_category: 'main',
    }).catch(console.error);
  }, [])
);

// Button click
const handleButtonClick = async () => {
  try {
    await Riuve.track('button_clicked', {
      button_id: 'subscribe_button',
      button_text: 'Subscribe',
      screen: 'Home',
    });
    
    // Button action
    // ...
  } catch (error) {
    console.error('Track error:', error);
  }
};

// Form submission
const handleFormSubmit = async (formData: any) => {
  try {
    await Riuve.track('form_submitted', {
      form_name: 'contact_form',
      form_fields: Object.keys(formData).length,
      success: true,
    });
  } catch (error) {
    console.error('Track error:', error);
  }
};
```

**Event Naming Best Practices:**
- Use `snake_case`: `screen_viewed`, `button_clicked`
- Use descriptive names: `user_subscribed` ✅, `event1` ❌
- Be consistent: `screen_viewed` for all screens, `button_clicked` for all buttons

---

## Advanced Features

### 1. Custom Event Properties

Add detailed information to events:

```typescript
await Riuve.track('purchase_completed', {
  product_id: 'prod_123',
  product_name: 'Premium Subscription',
  price: 99.99,
  currency: 'USD',
  payment_method: 'credit_card',
  discount_code: 'SUMMER2024',
});
```

### 2. Manual Flush

Send events immediately when needed:

```typescript
await Riuve.flush();
```

**When to Use:**
- Before user exits the app for important events
- After critical actions (e.g., payment completed)

### 3. Reset User

When user logs out:

```typescript
await Riuve.reset();
```

This will:
- Clear external user ID
- Clear user properties
- Generate a new anonymous ID
- Clear event queue

### 4. FCM Token Management

Register FCM token for push notifications:

```typescript
import * as Notifications from 'expo-notifications';

// Get FCM token
const token = await Notifications.getExpoPushTokenAsync();

// Register with SDK
await Riuve.setFcmToken(token.data);
```

### 5. MMKV Storage

By default the SDK uses AsyncStorage. If your project uses `react-native-mmkv`, you can pass it as a custom storage adapter for better performance:

```typescript
import { MMKV } from 'react-native-mmkv';
import Riuve, { MMKVStorageAdapter } from '@riuve/expo';

const mmkv = new MMKV({ id: 'riuve-storage' });

await Riuve.initialize('api_key', {
  storage: new MMKVStorageAdapter(mmkv),
});
```

**Why MMKV?**
- Synchronous reads/writes (no async overhead)
- ~30x faster than AsyncStorage
- No need to install AsyncStorage if you already use MMKV

### 6. Locale / Language Override

Override the device locale so the backend session uses your chosen language (e.g. for localized push notifications):

```typescript
// At initialization (e.g. from stored user preference)
await Riuve.initialize('api_key', {
  localeOverride: 'tr',  // 'tr' | 'en' | 'ar' | etc.
});

// Or change later (updates backend session locale)
await Riuve.setLocale('en');
```

- **Persisted**: The override is stored and restored on next app launch.
- **Backend**: Session locale is updated via the initialize endpoint, so campaigns/notifications can use the selected language.
- **Format**: Use short language codes (`tr`, `en`, `ar`) to match backend notification content keys.

---

## Best Practices

### 1. Initialize Early

Initialize the SDK **as early as possible**:

```typescript
// ✅ GOOD: At app start
export default function App() {
  useEffect(() => {
    Riuve.initialize('api_key');
  }, []);
}

// ❌ BAD: After user action
const handleButtonClick = () => {
  Riuve.initialize('api_key'); // Too late!
};
```

### 2. Error Handling

Always use try-catch:

```typescript
// ✅ GOOD
try {
  await Riuve.track('event_name', {});
} catch (error) {
  console.error('Track failed:', error);
  // Don't break app flow, just log it
}

// ❌ BAD
await Riuve.track('event_name', {}); // App may crash on error
```

### 3. Async/Await vs Promise

```typescript
// ✅ GOOD: Async/await (more readable)
const handleAction = async () => {
  try {
    await Riuve.track('action_completed');
  } catch (error) {
    console.error(error);
  }
};

// ✅ GOOD: Promise catch (for quick actions)
Riuve.track('quick_action').catch(console.error);

// ❌ BAD: Unhandled promise
Riuve.track('action'); // Fails silently on error
```

### 4. Event Naming Convention

Use a consistent naming convention:

```typescript
// ✅ GOOD: Consistent and descriptive
Riuve.track('screen_viewed', { screen_name: 'Home' });
Riuve.track('screen_viewed', { screen_name: 'Profile' });
Riuve.track('button_clicked', { button_id: 'subscribe' });
Riuve.track('button_clicked', { button_id: 'cancel' });

// ❌ BAD: Inconsistent
Riuve.track('home_viewed');
Riuve.track('profile_screen');
Riuve.track('click_subscribe');
Riuve.track('cancelButton');
```

### 5. Tab Navigation Events

Use `useFocusEffect` for tab screens to avoid tracking events for unmounted tabs:

```typescript
import { useFocusEffect } from '@react-navigation/native';

// ✅ GOOD: Only tracks when tab is actually focused
useFocusEffect(
  useCallback(() => {
    Riuve.track('screen_viewed', {
      screen_name: 'Home',
    }).catch(console.error);
  }, [])
);

// ❌ BAD: Tracks even if tab is not visible
useEffect(() => {
  Riuve.track('screen_viewed', {
    screen_name: 'Home',
  }).catch(console.error);
}, []);
```

### 6. Critical Events

Use flush for critical events:

```typescript
// Payment completed - send immediately
await Riuve.track('purchase_completed', {
  order_id: 'order_123',
  amount: 99.99,
});
await Riuve.flush(); // Send immediately

// Screen view - send in batch (normal)
Riuve.track('screen_viewed', { screen_name: 'Home' });
```

---

## Configuration Options

### All Configuration Parameters

```typescript
await Riuve.initialize('api_key', {
  // Debugging
  debugMode: false,           // Enable detailed logging (default: false)

  // Locale (session language for backend / push notifications)
  localeOverride: undefined, // e.g. 'tr' | 'en' | 'ar' (optional, persisted)

  // Batch Settings
  batchSize: 10,              // Number of events per batch (default: 10)
  batchTimeout: 30000,        // Auto-send timeout in ms (default: 30000)

  // Network Settings
  requestTimeout: 10000,      // API request timeout in ms (default: 10000)

  // Retry Settings
  maxRetries: 3,              // Maximum retry attempts (default: 3)
  retryDelay: 1000,           // Retry delay in ms (default: 1000)

  // Storage Settings
  persistEvents: true,        // Save events to storage (default: true)
  maxQueueSize: 1000,         // Maximum queue size (default: 1000)
  eventRetentionDays: 7,      // Days to keep failed events (default: 7)

  // Storage
  storage: undefined,           // Custom storage adapter (default: AsyncStorageAdapter)
                                // Use new MMKVStorageAdapter(mmkv) for MMKV

  // Automatic Operations
  autoFlushOnBackground: true, // Auto-flush on background (default: true)
});
```

### Recommended Configurations

**Development:**
```typescript
{
  debugMode: true,     // Enable detailed logging
  batchSize: 5,        // Smaller batches (for testing)
  batchTimeout: 10000, // Faster sending (for testing)
}
```

**Production:**
```typescript
{
  debugMode: false,     // Only WARN and ERROR logs (default)
  batchSize: 10,        // Standard batch size
  batchTimeout: 30000,  // Wait 30 seconds
  autoFlushOnBackground: true, // Send when backgrounded
}
```

**High Volume (Many events):**
```typescript
{
  batchSize: 20,        // Larger batches
  batchTimeout: 60000,  // Wait 1 minute
  maxQueueSize: 5000,   // Larger queue
}
```

---

## Troubleshooting

### Problem 1: SDK Not Initializing

**Symptoms:**
- `SDK not initialized` error
- Events are not being tracked

**Solution:**
```typescript
// Wait for initialization to complete
useEffect(() => {
  const init = async () => {
    await Riuve.initialize('api_key');
    console.log('SDK initialized');
  };
  init();
}, []);

// Check before tracking
if (Riuve.isReady()) {
  await Riuve.track('event');
}
```

**Note:** Events tracked before initialization are automatically queued and processed after initialization.

### Problem 2: Events Not Sending

**Checklist:**
1. ✅ Is SDK initialized?
2. ✅ Is API key correct?
3. ✅ Is there internet connection?

**Debug:**
```typescript
// Enable debug mode to see detailed logs
await Riuve.initialize('api_key', {
  debugMode: true, // Shows DEBUG, INFO, WARN, and ERROR logs
});

// With debugMode: false (default), only WARN and ERROR are visible:
// [Riuve SDK] [WARN] Network offline, events will be stored
// [Riuve SDK] [ERROR] Failed to send batch

// With debugMode: true, you'll see everything:
// [Riuve SDK] [DEBUG] Config: {...}
// [Riuve SDK] [INFO] Making HTTP request: POST /api/sdk/track
// [Riuve SDK] [INFO] Event tracked: button_clicked
// [Riuve SDK] [WARN] Retry attempt 1...
// [Riuve SDK] [ERROR] Failed after 3 retries
```

### Problem 3: Offline Events Not Sending

**Normal Behavior:**
- Offline events are saved to AsyncStorage
- Automatically sent when internet connection is restored
- Sent when app is reopened

**Manual Check:**
```typescript
// Manually flush
await Riuve.flush();

// Or on app start
useEffect(() => {
  Riuve.initialize('api_key').then(() => {
    Riuve.flush(); // Send pending events
  });
}, []);
```

### Problem 4: Too Many Events Accumulating

**Solution:**
```typescript
// Increase batch size
await Riuve.initialize('api_key', {
  batchSize: 20,        // Larger batches
  batchTimeout: 15000,  // Send more frequently
});
```

### Problem 5: TypeScript Errors

**Solution:**
```typescript
// Use type assertion
await Riuve.track('event_name', {
  custom_prop: value,
} as any); // Temporary solution

// Or proper typing
interface MyEventProperties {
  custom_prop: string;
}

await Riuve.track('event_name', {
  custom_prop: 'value',
} as MyEventProperties);
```

---

## API Reference

### Riuve.initialize()

Initializes the SDK.

```typescript
await Riuve.initialize(apiKey: string, config?: RiuveConfig): Promise<void>
```

**Parameters:**
- `apiKey` (string, required): Your API key from Riuve dashboard
- `config` (object, optional): Configuration object

**Example:**
```typescript
await Riuve.initialize('riuve_base_xxx', {
  batchSize: 10,
});
```

### Riuve.identify()

Identifies a user.

```typescript
await Riuve.identify(externalUserId: string, properties?: object): Promise<void>
```

**Parameters:**
- `externalUserId` (string, required): User's unique ID
- `properties` (object, optional): User properties

**Example:**
```typescript
await Riuve.identify('user_123', {
  email: 'user@example.com',
  name: 'John Doe',
});
```

**Note:** If user properties are updated, call `identify` again with the new properties.

### Riuve.track()

Tracks an event.

```typescript
await Riuve.track(eventName: string, properties?: object): Promise<void>
```

**Parameters:**
- `eventName` (string, required): Event name
- `properties` (object, optional): Event properties

**Example:**
```typescript
await Riuve.track('button_clicked', {
  button_id: 'subscribe',
  screen: 'Home',
});
```

**Note:** Can be called before SDK initialization. Events will be queued and processed after initialization.

### Riuve.flush()

Immediately sends all pending events.

```typescript
await Riuve.flush(): Promise<void>
```

**Example:**
```typescript
await Riuve.track('purchase_completed');
await Riuve.flush(); // Send immediately
```

### Riuve.reset()

Clears user information and queue.

```typescript
await Riuve.reset(): Promise<void>
```

**Example:**
```typescript
// When user logs out
await Riuve.reset();
```

### Riuve.setFcmToken()

Registers FCM token (for push notifications).

```typescript
await Riuve.setFcmToken(token: string): Promise<void>
```

**Example:**
```typescript
const token = await Notifications.getExpoPushTokenAsync();
await Riuve.setFcmToken(token.data);
```

### Riuve.setLocale()

Sets the session locale override and syncs it to the backend (e.g. for localized push notifications). Call after SDK is initialized.

```typescript
await Riuve.setLocale(locale: string): Promise<void>
```

**Parameters:**
- `locale` (string, required): Language code (e.g. `'tr'`, `'en'`, `'ar'`)

**Example:**
```typescript
// User changed app language in settings
await Riuve.setLocale('tr');
```

**Note:** The value is persisted and used on next `initialize()`; the backend session is updated immediately so campaign/notification content can use the new locale.

### Riuve.isReady()

Checks if SDK is initialized.

```typescript
const isReady: boolean = Riuve.isReady();
```

**Example:**
```typescript
if (Riuve.isReady()) {
  await Riuve.track('event');
}
```

### Riuve.getQueueSize()

Gets current queue size.

```typescript
const queueSize: number = Riuve.getQueueSize();
```

### Riuve.getFailedBatchCount()

Gets number of failed batches waiting for retry.

```typescript
const failedCount: number = await Riuve.getFailedBatchCount();
```

---

## Example Scenarios

### Scenario 1: E-Commerce App

```typescript
// 1. On app start
useEffect(() => {
  Riuve.initialize('api_key');
}, []);

// 2. When user logs in
const handleLogin = async (user: User) => {
  await Riuve.identify(user.id, {
    email: user.email,
    name: user.name,
    membership_type: user.membershipType,
  });
};

// 3. Product view
const handleProductView = (product: Product) => {
  Riuve.track('product_viewed', {
    product_id: product.id,
    product_name: product.name,
    product_category: product.category,
    price: product.price,
  });
};

// 4. Add to cart
const handleAddToCart = (product: Product) => {
  Riuve.track('add_to_cart', {
    product_id: product.id,
    product_name: product.name,
    price: product.price,
    quantity: 1,
  });
};

// 5. Purchase completed
const handlePurchaseComplete = async (order: Order) => {
  await Riuve.track('purchase_completed', {
    order_id: order.id,
    total_amount: order.total,
    items_count: order.items.length,
    payment_method: order.paymentMethod,
  });
  await Riuve.flush(); // Critical event, send immediately
};
```

### Scenario 2: Social Media App

```typescript
// 1. Post view
const handlePostView = (post: Post) => {
  Riuve.track('post_viewed', {
    post_id: post.id,
    author_id: post.authorId,
    post_type: post.type,
  });
};

// 2. Like/Unlike
const handleLike = (post: Post) => {
  Riuve.track('post_liked', {
    post_id: post.id,
    author_id: post.authorId,
  });
};

// 3. Comment
const handleComment = (post: Post, comment: string) => {
  Riuve.track('comment_added', {
    post_id: post.id,
    comment_length: comment.length,
  });
};

// 4. Profile view
const handleProfileView = (userId: string) => {
  Riuve.track('profile_viewed', {
    profile_user_id: userId,
  });
};
```

### Scenario 3: Game App

```typescript
// 1. Level start
const handleLevelStart = (level: number) => {
  Riuve.track('level_started', {
    level_number: level,
    difficulty: 'normal',
  });
};

// 2. Level completion
const handleLevelComplete = async (level: number, score: number) => {
  await Riuve.track('level_completed', {
    level_number: level,
    score: score,
    time_spent: getTimeSpent(),
  });
  await Riuve.flush(); // Important event
};

// 3. In-app purchase
const handleInAppPurchase = async (item: Item) => {
  await Riuve.track('in_app_purchase', {
    item_id: item.id,
    item_name: item.name,
    price: item.price,
    currency: 'USD',
  });
  await Riuve.flush();
};
```

---

## Security

### API Key Security

- ✅ **Never** commit API keys to public repositories
- ✅ Use environment variables or secure storage
- ✅ Use different API keys for different projects

**Example:**
```typescript
// ✅ GOOD: Environment variable
const API_KEY = process.env.EXPO_PUBLIC_RIUVE_API_KEY || '';

// ❌ BAD: Hardcoded
const API_KEY = 'riuve_base_xxxxxxxxxxxxx';
```

---

## Support

For questions and support:
- 📧 Email: support@riuve.com
- 📚 Documentation: https://docs.riuve.com

---

## License

MIT

---

**Last Updated:** 2026-02-25
**SDK Version:** 2.0.0