npm package: @insightech/react-native
Description: Insightech analytics SDK for React Native
Latest version: 1.0.0

# @insightech/react-native

Insightech analytics SDK for React Native. Captures user interactions, screen views, and component tree snapshots for session replay, heatmaps, click maps, scroll depth, and form analytics on the Insightech dashboard.

The SDK sends data in the exact same format as the Insightech web JavaScript SDK, so no backend changes are needed.

## Table of Contents

- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start (2 Steps)](#quick-start)
- [What Gets Tracked Automatically](#what-gets-tracked-automatically)
- [Custom Event Tracking](#custom-event-tracking)
- [Error Tracking](#error-tracking)
- [Form Tracking](#form-tracking)
- [Element Identification with testID](#element-identification-with-testid)
- [Privacy & Data Masking](#privacy--data-masking)
- [Configuration Reference](#configuration-reference)
- [Advanced Usage](#advanced-usage)
- [How It Works](#how-it-works)
- [Troubleshooting](#troubleshooting)
- [API Reference](#api-reference)

---

## Requirements

- React >= 17
- React Native >= 0.68
- `@react-native-async-storage/async-storage` >= 1.17
- Optional: `@react-navigation/native` for automatic screen tracking

**Compatible with:** Expo managed workflow, Expo bare workflow, React Native CLI, Expo Go, and the New Architecture (Fabric). No native modules required.

**Tested with:** React Native 0.68 through 0.76, React 17 and 18, both Paper and Fabric renderers.

---

## Installation

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

If using `@react-navigation/native` (recommended for automatic screen tracking):

```bash
npm install @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context
```

---

## Quick Start

Integration requires just **two steps**: add the Babel plugin and wrap your app with `InsightechProvider`. No component swapping needed.

### Step 1: Add the Babel Plugin

```js
// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'], // or 'module:metro-react-native-babel-preset'
    plugins: ['@insightech/babel-plugin-react-native'],
  };
};
```

The plugin automatically instruments interactive components at build time. Your app code stays 100% standard React Native.

### Step 2: Wrap Your App

```tsx
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
import { InsightechProvider } from '@insightech/react-native';

export default function App() {
  const navigationRef = useNavigationContainerRef();

  return (
    <InsightechProvider
      config={{
        account: 'YOUR_PROFILE_ID:YOUR_SERVER_ID',
        appName: 'MyApp',
      }}
      navigationRef={navigationRef}
    >
      <NavigationContainer ref={navigationRef}>
        {/* Your app navigator */}
      </NavigationContainer>
    </InsightechProvider>
  );
}
```

> **Where to find your account string:** Log in to the Insightech dashboard. Your profile ID and server ID are in the tracking code snippet, formatted as `profileId:serverId`.

That's it. The SDK starts tracking immediately.

---

## What Gets Tracked Automatically

Once set up, the following are captured without any additional code:

| What | How | Requires |
|------|-----|----------|
| Screen views | React Navigation listener | `navigationRef` prop |
| Tap / click events | Babel plugin instruments `Pressable`, `TouchableOpacity`, `TouchableHighlight` | Babel plugin |
| Scroll events | Babel plugin instruments `ScrollView`, `FlatList`, `SectionList` | Babel plugin |
| Text input & changes | Babel plugin instruments `TextInput` | Babel plugin |
| Field focus / blur | Tracked via `TextInput` wrapper | Babel plugin |
| App background / foreground | `AppState` listener | Nothing (built-in) |
| Viewport resize | `Dimensions` listener | Nothing (built-in) |
| Component tree snapshots | React fiber tree serialization | Nothing (built-in) |
| DOM mutations | Tree diff after interactions | Nothing (built-in) |
| Rage taps | Rapid repeated taps on same element | Nothing (built-in) |
| JS crashes | Global error handler | Nothing (built-in) |
| API errors | Fetch/XHR interception | Server config |

### Excluding Components from Auto-Tracking

If you don't want certain components to be tracked, exclude them in the Babel plugin config:

```js
plugins: [
  ['@insightech/babel-plugin-react-native', {
    exclude: ['ScrollView', 'FlatList'],
  }],
],
```

---

## Custom Event Tracking

Use the `useInsightech` hook to track custom business events:

```tsx
import { useInsightech } from '@insightech/react-native';

function ProductDetailScreen({ product }) {
  const { trackCustomEvent } = useInsightech();

  const handleAddToCart = () => {
    addToCart(product);

    trackCustomEvent({
      event: 'add_to_cart',
      product_id: product.id,
      product_name: product.name,
      price: product.price,
      currency: 'USD',
    });
  };

  return (
    <Pressable testID="add-to-cart-btn" onPress={handleAddToCart}>
      <Text>Add to Cart</Text>
    </Pressable>
  );
}
```

Custom events appear as type 99 (DATA_LAYER) in the session timeline and can be used for conversion tracking, funnel analysis, and segmentation.

---

## Error Tracking

### Automatic JS Crash Tracking

The SDK automatically captures unhandled JavaScript exceptions via React Native's `ErrorUtils`. These are sent as `js_error` events and appear in the session timeline. The handler chains with existing error handlers (Sentry, Crashlytics, etc.) so it won't interfere with your crash reporting setup.

### Manual Error Tracking

Track error messages shown to users — validation errors, API failures, or any error state:

```tsx
const { trackError } = useInsightech();

// Form validation error
trackError({
  message: 'Please fill in all required fields',
  type: 'validation',
  context: { screen: 'Checkout', empty_fields: ['email', 'phone'] },
});

// API error
trackError({
  message: 'Payment failed: card declined',
  type: 'api',
  context: { endpoint: '/api/payment', status: 402 },
});
```

Each `trackError` call sends a custom event with `event: "error"`, `error_message`, `error_type` (defaults to `"validation"`), and any additional context fields.

---

## Form Tracking

Track form submissions with field-level detail:

```tsx
const { trackFormSubmit, flush } = useInsightech();

const handleCheckout = async () => {
  trackFormSubmit({
    nodeIndex: 0,
    cssPath: '#checkout-form',
    name: 'checkout',
    method: 'POST',
    fields: [
      { nodeIndex: 1, cssPath: '#email', name: 'email', value: email },
      { nodeIndex: 2, cssPath: '#name', name: 'name', value: name },
      // Mask sensitive fields before tracking
      { nodeIndex: 3, cssPath: '#card', name: 'card', value: '****' },
      { nodeIndex: 4, cssPath: '#cvv', name: 'cvv', value: '***' },
    ],
  });

  // Force send events before navigating away
  await flush();

  navigation.navigate('OrderConfirmation', { orderId });
};
```

> **Important:** Always call `flush()` before navigating away from a screen with tracked form data to ensure events are sent before the component unmounts.

---

## Element Identification with testID

Add `testID` to your components for stable, readable element identification in analytics:

```tsx
<Pressable testID="add-to-cart" onPress={handleAdd}>
  <Text>Add to Cart</Text>
</Pressable>

<TextInput testID="email-input" placeholder="Email" onChangeText={setEmail} />

<ScrollView testID="product-list">
  {/* ... */}
</ScrollView>
```

**Why this matters:**
- `testID` maps to the HTML `id` attribute in the synthetic DOM, producing CSS selectors like `#add-to-cart`
- Without `testID`, elements are identified by position (e.g., `div:nth-child(2)`), which breaks when the layout changes
- `testID` is also used for content masking rules (see Privacy section)
- `testID` is already standard practice for testing — use the same IDs for both

---

## Privacy & Data Masking

All personal data is **masked on-device before transmission**. Raw values never leave the device.

### Automatic Input Masking

All `TextInput` values are masked by default:

| User types | Sent as | Rule |
|------------|---------|------|
| `John Doe` | `**** ***` | Letters/symbols → `*` |
| `4242424242424242` | `0000000000000000` | Digits → `0` |
| `john@example.com` | `****@*******.**` | Email pattern detected, structure preserved |
| `123 Main St` | `000 **** **` | Mixed: digits → `0`, letters → `*` |

### Automatic Email Detection

Email addresses are detected and masked in **all text content** (not just inputs). Any text matching an email pattern is masked before the component tree is sent.

### Content Masking with testID

Mask specific on-screen text using `testID` combined with the server's `contentBlockList`:

```tsx
{/* Order ID — visible (not in block list) */}
<Text testID="order-id">{orderId}</Text>

{/* Personal details — masked via contentBlockList */}
<Text testID="personal-details">{userName}</Text>
<Text testID="personal-details">{userEmail}</Text>
<Text testID="personal-details">{userAddress}</Text>
```

In the replay:
- Order ID: `ORD-12345` (readable)
- Name: `**** ***` (masked)
- Email: `****@*******.**` (masked)
- Address: `*** **** **` (masked)

### Server-Controlled Masking Rules

The server returns two lists on the SDK's first request — **no app update needed** to change masking rules:

- **`fieldAllowList`** — Input fields to leave **unmasked** (e.g., `["#search-input", "#quantity"]`)
- **`contentBlockList`** — Elements to **fully mask** (e.g., `["#personal-details", "#payment-info"]`)

---

## Configuration Reference

All configuration options for `InsightechProvider`:

```tsx
<InsightechProvider
  config={{
    // ── Required ──
    account: 'profileId:serverId',  // From your Insightech dashboard
    appName: 'MyApp',               // Used in screen URLs

    // ── Optional ──
    protocol: 'https:',             // Server protocol (default: 'https:')
    trackingLevel: 'full',          // 'full' or 'lite' (default: 'full')
    devMode: false,                 // true = new visitor ID on each app start,
                                    //        enables console logging (default: false)

    // ── Network ──
    maxConcurrentRequests: 1,       // Max parallel HTTP requests (default: 1)
    sendRequestSize: 200,           // KB threshold to trigger send (default: 200)
    requestTimeout: 10000,          // Request timeout in ms (default: 10000)
    maxRetries: 3,                  // Max retries per failed batch (default: 3)
    retryBaseDelay: 1000,           // Base delay for exponential backoff in ms (default: 1000)

    // ── Event Throttling ──
    scrollInterval: 150,            // Scroll event throttle in ms (default: 150)
    resizeInterval: 150,            // Resize event throttle in ms (default: 150)
    mutationBatchInterval: 200,     // Mutation batch window in ms (default: 200)

    // ── Storage ──
    visitorIdStorageKey: 'insightech_vid', // AsyncStorage key for visitor ID
    maxQueueSize: 1000,             // Max queued events before dropping oldest (default: 1000)
  }}
  navigationRef={navigationRef}     // Optional: React Navigation ref for auto screen tracking
/>
```

### Development Mode

Set `devMode: true` during development to:
- Generate a new visitor ID on each app start (each reload is a new session)
- Enable `console.log` output for debugging SDK behavior
- See SDK initialization, event queuing, and transport logs in Metro

```tsx
<InsightechProvider
  config={{
    account: 'YOUR_PROFILE_ID:YOUR_SERVER_ID',
    appName: 'MyApp',
    devMode: __DEV__,  // Auto-enable in development
  }}
  navigationRef={navigationRef}
/>
```

---

## Advanced Usage

### Manual Screen Tracking (Without React Navigation)

If you don't use React Navigation, track screens manually:

```tsx
const { trackScreen } = useInsightech();

// Call when a screen mounts or becomes visible
useEffect(() => {
  trackScreen('ProductDetail', { productId: product.id });
}, []);
```

### Force Sending Events

The SDK batches events and sends them automatically. To force-send immediately (e.g., before the app navigates to an external URL or before a critical action):

```tsx
const { flush } = useInsightech();

const handleExternalLink = async () => {
  await flush();
  Linking.openURL(url);
};
```

### Accessing the SDK Instance Directly

For advanced use cases, access the raw SDK instance:

```tsx
const { sdk } = useInsightech();

// Track a tap manually
sdk.trackTap({
  nodeIndex: 0,
  cssPath: '#custom-element',
  tag: 'button',
  text: 'Custom Button',
  elementX: 100,
  elementY: 200,
  screenX: 100,
  screenY: 300,
  scrollTop: 0,
  scrollLeft: 0,
  pageTop: 0,
  pageLeft: 0,
  name: '',
  href: '',
});
```

### Offline Support

Events are persisted to AsyncStorage when the app goes to background. If the app is killed before events can be sent, they are restored and sent on the next app launch. This ensures no data loss during:
- OS killing the app in the background
- App crashes
- Brief network outages (events are retried with exponential backoff)

### Using Tracked Components Directly

If you prefer not to use the Babel plugin, import tracked components directly:

```tsx
import {
  TrackedPressable,
  TrackedScrollView,
  TrackedFlatList,
  TrackedSectionList,
  TrackedTextInput,
} from '@insightech/react-native';

// Use them as drop-in replacements
<TrackedPressable testID="my-button" onPress={handlePress}>
  <Text>Press Me</Text>
</TrackedPressable>

<TrackedFlatList
  testID="product-list"
  data={products}
  renderItem={renderProduct}
/>
```

---

## How It Works

### Architecture Overview

```
Your App
  │
  ├── InsightechProvider (wraps app, captures fiber tree)
  │     │
  │     ├── Babel plugin rewrites imports at build time
  │     │     Pressable → TrackedPressable
  │     │     ScrollView → TrackedScrollView
  │     │     FlatList → TrackedFlatList
  │     │     SectionList → TrackedSectionList
  │     │     TextInput → TrackedTextInput
  │     │
  │     ├── Tracked wrappers intercept events (taps, scrolls, inputs)
  │     │
  │     ├── Component tree serialized to synthetic DOM
  │     │     View → <div>, Text → <span>, Pressable → <button>, etc.
  │     │
  │     └── Privacy masking applied on-device
  │
  └── Events batched → compressed → sent to Insightech backend
        ↓
      Insightech Dashboard (replay, heatmaps, funnels, analytics)
```

### Synthetic DOM Tree

React Native has no HTML DOM. The SDK constructs a synthetic DOM by walking the React fiber tree and mapping components to HTML equivalents:

| React Native Component | HTML Element |
|------------------------|-------------|
| `View`, `SafeAreaView`, `KeyboardAvoidingView` | `<div>` |
| `Text` | `<span>` |
| `Image`, `ImageBackground` | `<img>` |
| `TextInput` | `<input>` |
| `ScrollView` | `<div>` (with overflow styles) |
| `FlatList`, `SectionList` | `<ul>` |
| `Pressable`, `TouchableOpacity`, `TouchableHighlight` | `<button>` |
| `Switch` | `<input type="checkbox">` |
| `Modal` | `<div>` |

### Screen URLs

Screens are represented as URLs for backend compatibility:

```
https://app.insightech.com/rn/{appName}/{screenName}?param=value
```

For example, navigating to a ProductDetail screen with `{ productId: '42' }` generates:
```
https://app.insightech.com/rn/MyApp/ProductDetail?productId=42
```

### Event Batching & Delivery

Events are queued and sent based on type:
- **Immediate:** Pageview, DOM tree, navigation, form submit, app background
- **Force send:** Clicks, rage taps (flushes entire queue)
- **Batched:** Scroll, resize, input, mutations (merged into arrays)
- **Size-based:** Queue flushed when payload exceeds 200KB

Failed requests are retried with exponential backoff (1s, 2s, 4s) up to 3 times. Events are persisted to AsyncStorage on app background and restored on next launch.

### Fiber Access & Compatibility

The SDK accesses React's internal fiber properties to serialize the component tree. These are undocumented internals that may change across versions. The following properties are tried in order:

1. `__internalInstanceHandle` (Fabric / New Architecture)
2. `_internalFiberInstanceHandleDEV` (Paper / development builds)
3. `_reactInternals` (React 18+)
4. `_reactInternalInstance` (React 17 / legacy)

If fiber access fails, the SDK degrades gracefully — events are still tracked but session replay won't have DOM data. **Test replay after major React/RN version upgrades.**

---

## Troubleshooting

### No data appearing in the dashboard

1. **Check devMode logs:** Set `devMode: true` and look for `[Insightech:Transport]` logs in Metro. You should see `Response: 200` for successful sends.
2. **Verify account string:** Ensure `account` is formatted as `profileId:serverId` (e.g., `'080360e96:us-0-api'`).
3. **Check date filter:** The dashboard defaults to "Yesterday". Switch to "Today" to see current sessions.

### Session replay shows empty/broken layout

1. **Check fiber access:** In devMode, look for `[Insightech:Provider] Root fiber captured`. If you see "No fiber found", the SDK can't access the component tree.
2. **Verify RN version:** Ensure you're on a supported React Native version (0.68+).
3. **Add testIDs:** Elements without `testID` use positional selectors which are less stable.

### Events not being tracked for specific components

1. **Check Babel plugin:** Verify `@insightech/babel-plugin-react-native` is in your `babel.config.js`. Clear the Metro cache after changes: `npx expo start --clear` or `npx react-native start --reset-cache`.
2. **Check exclusions:** Make sure the component isn't in the `exclude` list.
3. **Custom components:** The Babel plugin only instruments direct imports from `react-native`. If you wrap `Pressable` in a custom component, add tracking manually using `useInsightech`.

### High memory usage or battery drain

Adjust throttling and batching settings:

```tsx
config={{
  scrollInterval: 300,        // Less frequent scroll events
  mutationBatchInterval: 500, // Longer mutation batch window
  sendRequestSize: 500,       // Larger batches, fewer requests
  maxQueueSize: 500,          // Smaller max queue
}}
```

### Events lost when app is killed

Events are automatically persisted to AsyncStorage when the app goes to background and restored on next launch. If you're still losing events, ensure `@react-native-async-storage/async-storage` is properly installed and linked.

---

## API Reference

### InsightechProvider

```tsx
<InsightechProvider
  config={InsightechConfig}        // Required: SDK configuration
  navigationRef={NavigationRef}    // Optional: React Navigation container ref
>
  {children}
</InsightechProvider>
```

### useInsightech()

```ts
const {
  trackScreen,       // (screenName: string, params?: Record<string, unknown>) => void
  trackCustomEvent,  // (data: Record<string, unknown>) => void
  trackFormSubmit,   // (info: FormInfo) => void
  trackError,        // (info: { message: string, type?: string, context?: Record }) => void
  flush,             // () => Promise<void>
  sdk,               // InsightechSDK instance (for advanced usage)
} = useInsightech();
```

### Tracked Components

All tracked components are drop-in replacements that forward all props and refs:

| Component | Wraps | Tracks |
|-----------|-------|--------|
| `TrackedPressable` | `Pressable`, `TouchableOpacity`, `TouchableHighlight` | Tap events with position, element text, CSS path |
| `TrackedScrollView` | `ScrollView` | Scroll position, content size, throttled |
| `TrackedFlatList` | `FlatList` | Scroll position, content size, throttled |
| `TrackedSectionList` | `SectionList` | Scroll position, content size, throttled |
| `TrackedTextInput` | `TextInput` | Input keystrokes (masked), focus, blur, change |

### Event Types

| Type | Name | Trigger |
|------|------|---------|
| 1 | Pageview | Screen mount / navigation |
| 2 | DOM Tree | Component tree snapshot |
| 3 | App Ready | SDK initialization |
| 4 | App Unload | App backgrounding |
| 7 | Click | `onPress` on tracked pressables |
| 8 | Input | `onChangeText` on tracked text inputs |
| 9 | Input Change | `onEndEditing` / blur with final value |
| 12 | Scroll | `onScroll` on tracked scroll views |
| 13 | Tab Hidden | App goes to background |
| 14 | Tab Visible | App returns to foreground |
| 15 | Resize | Device orientation / dimensions change |
| 16 | DOM Mutation | Component tree diff after interactions |
| 17 | Field Focus | `onFocus` on tracked text inputs |
| 18 | Field Blur | `onBlur` on tracked text inputs |
| 19 | URL Change | Screen navigation |
| 20 | Form Submit | `trackFormSubmit()` call |
| 97 | API Error | HTTP errors on monitored endpoints |
| 98 | Rage Tap | Rapid repeated taps on same element |
| 99 | Custom / Error | `trackCustomEvent()` or `trackError()` |

---

## Development

```bash
npm install          # Install dependencies
npm test             # Run tests (124 tests)
npm test -- --coverage  # Run with coverage report
npm run build        # Build CJS, ESM, and TypeScript definitions
cd example && npx expo start --ios  # Run example app
```

## Version

SDK version: `rn-1.0.0`