@convivainc/conviva-react-native-appanalytics
Version:
Conviva React Native Application Analytics Library
844 lines (688 loc) • 23.6 kB
Markdown
# Conviva React Native App Analytics - JS/TS Snippets
Only read this file when directed by AGENTS.md. All imports use `@convivainc/conviva-react-native-appanalytics`.
---
## Initialization
Always create a dedicated `src/conviva.ts` (TypeScript) or `src/conviva.js` (JavaScript) module. Call `createTracker` once at module level in this file and export the tracker instance. Then side-effect import it from the root component so the tracker is initialized before any screen renders.
**`src/conviva.ts` (TypeScript):**
```ts
import { createTracker, ReactNativeTracker } from '@convivainc/conviva-react-native-appanalytics';
let tracker: ReactNativeTracker | undefined;
try {
tracker = createTracker('YOUR_CUSTOMER_KEY', 'YOUR_APP_NAME');
if (!tracker) {
console.error('Tracker initialization returned null');
}
} catch (error) {
console.error(error);
}
export { tracker };
```
**`src/conviva.js` (JavaScript):**
```js
import { createTracker } from '@convivainc/conviva-react-native-appanalytics';
let tracker;
try {
tracker = createTracker('YOUR_CUSTOMER_KEY', 'YOUR_APP_NAME');
if (!tracker) {
console.error('Tracker initialization returned null');
}
} catch (error) {
console.error(error);
}
export { tracker };
```
**Root component (`App.tsx` / `App.js` / `index.js`) — side-effect import only:**
```ts
import './conviva';
```
> **YOUR_CUSTOMER_KEY** - A string to identify this specific Conviva account. Use separate keys for development/debug and production.
> **YOUR_APP_NAME** - A string value for the app name that uniquely identifies your app across platforms.
**Do NOT** call `createTracker` inside `App.tsx` or any other file. The `conviva.ts` module is the single source of truth for tracker initialization.
---
## Accessing the Tracker Across Components
Call `createTracker` exactly once — inside `src/conviva.ts` (or `src/conviva.js`). Import the exported tracker instance wherever it is needed. Never re-initialize.
**Importing via path alias (preferred when a `@src` or similar alias is configured in `babel.config.js` / `tsconfig.json`):**
```ts
import { tracker } from '@src/conviva';
try {
if (tracker != null) {
tracker.trackCustomEvent('my_event', { key: 'value' });
}
} catch (error) {
console.error(error);
}
```
**Importing via relative path (when no alias is configured):**
```ts
import { tracker } from '../conviva';
try {
if (tracker != null) {
tracker.trackCustomEvent('my_event', { key: 'value' });
}
} catch (error) {
console.error(error);
}
```
Before writing any import, check `babel.config.js` (or `.babelrc`) for a `module-resolver` plugin with an `alias` entry pointing to `src/`. Use the alias form if present; use a relative path otherwise.
**Pattern 2 - React Context (for larger apps that need dependency injection):**
```ts
// ConvivaContext.tsx
import React, { createContext, useContext } from 'react';
import { createTracker, ReactNativeTracker } from '@convivainc/conviva-react-native-appanalytics';
let tracker: ReactNativeTracker | undefined;
try {
tracker = createTracker('YOUR_CUSTOMER_KEY', 'YOUR_APP_NAME');
if (!tracker) {
console.error('Tracker initialization returned null');
}
} catch (error) {
console.error(error);
}
const ConvivaContext = createContext<ReactNativeTracker | undefined>(tracker);
export const ConvivaProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<ConvivaContext.Provider value={tracker}>{children}</ConvivaContext.Provider>
);
export const useConviva = (): ReactNativeTracker => useContext(ConvivaContext);
```
Wrap the root component:
```tsx
// App.tsx
import { ConvivaProvider } from './ConvivaContext';
export default function App() {
return (
<ConvivaProvider>
<NavigationContainer>
{/* ... */}
</NavigationContainer>
</ConvivaProvider>
);
}
```
Use in any component:
```tsx
const tracker = useConviva();
try {
if (tracker != null) {
tracker.trackCustomEvent('my_event', { key: 'value' });
}
} catch (error) {
console.error(error);
}
```
**Pattern 3 - Root component with prop drilling (only for very small apps with no Redux/Context layer):**
Initialize in `src/conviva.ts` and pass `tracker` as a prop to child screens that need it directly.
---
## iOS Native Tracker Initialization (Hybrid Apps Only)
For hybrid iOS apps with native ViewControllers, initialize the tracker natively in `AppDelegate` before the RN bridge loads. This ensures native screens are tracked from app launch.
**Objective-C (`AppDelegate.m` or `AppDelegate.mm`):**
```objc
@import Foundation; // Required for NSException and NSLog
@import ConvivaAppAnalytics;
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
@try {
id<CATTrackerController> tracker = [CATAppAnalytics createTrackerWithCustomerKey:@"YOUR_CUSTOMER_KEY" appName:@"YOUR_APP_NAME"];
if (tracker == nil) {
NSLog(@"Conviva tracker init returned nil");
}
} @catch (NSException *exception) {
NSLog(@"Conviva tracker init failed: %@", exception);
}
// ... existing RCTBridge / RCTRootView setup ...
return YES;
}
@end
```
**Swift (`AppDelegate.swift`):**
```swift
import ConvivaAppAnalytics
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Note: CATAppAnalytics.createTracker is an Objective-C method and does not throw Swift errors.
// Use guard to safely handle a nil result.
guard let tracker = CATAppAnalytics.createTracker(customerKey: "YOUR_CUSTOMER_KEY", appName: "YOUR_APP_NAME") else {
return false
}
// ... existing RCTBridge / RCTRootView setup ...
return true
}
}
```
> **YOUR_CUSTOMER_KEY** and **YOUR_APP_NAME** must match the values used in the JavaScript `createTracker(...)` call in `src/conviva.ts`. The SDK deduplicates internally -- the second `createTracker` call from the JS bridge reuses the tracker already initialized natively.
>
> Use only the two-argument form. Do not pass a configuration dictionary or builder as a third argument.
---
## User ID
Place immediately after successful login or registration at the convergence point. Use the tracker returned from `createTracker`, or the same module-level instance.
**Set user ID (preferred - using setSubjectData):**
```js
try {
if (tracker != null) {
tracker.setSubjectData({ userId: userId });
}
} catch (error) {
console.error(error);
}
```
**Set user ID (alternative):**
```js
try {
if (tracker != null) {
tracker.setUserId(userId);
}
} catch (error) {
console.error(error);
}
```
**Clear on logout:**
```js
try {
if (tracker != null) {
tracker.setSubjectData({ userId: null });
// or
tracker.setUserId(null);
}
} catch (error) {
console.error(error);
}
```
> `userId` must be a non-PII identifier: opaque provider UID, stored UUID, or backend account ID. Never pass email, phone, full name, or any PII.
---
## Custom Events
```js
const eventName = 'your_event_name';
const eventData = {
identifier1: intValue,
identifier2: boolValue,
identifier3: 'stringValue',
};
try {
if (tracker != null) {
tracker.trackCustomEvent(eventName, eventData);
}
} catch (error) {
console.error(error);
}
```
**TypeScript:**
```ts
try {
if (tracker != null) {
tracker.trackCustomEvent('your_event_name', {
identifier1: 42,
identifier2: true,
identifier3: 'stringValue',
});
}
} catch (error) {
console.error(error);
}
```
---
## Custom Tags
Custom tags are global key-value pairs applied to all subsequent events until cleared.
**Set tags:**
```js
const tagsToSet = {
tagKey1: 'tagValue1',
tagKey2: 100,
tagKey3: true,
};
try {
if (tracker != null) {
tracker.setCustomTags(tagsToSet);
}
} catch (error) {
console.error(error);
}
```
**Set tags with category:**
```js
try {
if (tracker != null) {
tracker.setCustomTagsWithCategory('categoryName', {
tagKey1: 'tagValue1',
tagKey2: 100,
});
}
} catch (error) {
console.error(error);
}
```
**Clear specific tags:**
```js
try {
if (tracker != null) {
tracker.clearCustomTags(['tagKey1', 'tagKey2']);
}
} catch (error) {
console.error(error);
}
```
**Clear all tags:**
```js
try {
if (tracker != null) {
tracker.clearAllCustomTags();
}
} catch (error) {
console.error(error);
}
```
---
## Revenue Event Tracking (>= 0.2.8)
Use `trackRevenueEvent()` to track purchase and revenue events. Required fields: `totalOrderAmount`, `transactionId`, `currency`.
```js
try {
if (tracker != null) {
tracker.trackRevenueEvent({
totalOrderAmount: 49.99,
transactionId: 'txn-001',
currency: 'USD',
taxAmount: 4.50,
shippingCost: 5.99,
paymentMethod: 'credit_card',
items: [
{
productId: 'prod-123',
name: 'Widget',
unitPrice: 19.99,
quantity: 2,
},
],
});
}
} catch (error) {
console.error(error);
}
```
**TypeScript:**
```ts
import { RevenueEventProps } from '@convivainc/conviva-react-native-appanalytics';
const revenueEvent: RevenueEventProps = {
totalOrderAmount: 49.99,
transactionId: 'txn-001',
currency: 'USD',
};
try {
if (tracker != null) {
tracker.trackRevenueEvent(revenueEvent);
}
} catch (error) {
console.error(error);
}
```
---
## PageView Tracking
Use to track in-app page navigations explicitly (distinct from automatic screen view tracking).
```js
try {
if (tracker != null) {
tracker.trackPageView({
pageUrl: 'https://example.com/page',
pageTitle: 'Page Title', // optional
referrer: 'https://example.com', // optional
});
}
} catch (error) {
console.error(error);
}
```
---
## Error Tracking (>= 0.5.0)
JS error tracking is **on by default** — uncaught errors and unhandled promise rejections are captured automatically once `createTracker(...)` runs.
Everything below is **optional**; add it only when the developer requests it (see AGENTS.md § 16a Step 1). `trackError` and `errorTracker` are **standalone exports** — import them directly; they do not require the `tracker` instance and need no `if (tracker != null)` guard.
### Disable or configure via `errorTracking`
Pass `errorTracking` as the third argument to `createTracker` in `src/conviva.ts` (or `src/conviva.js`). Pass `false` to disable, or an object to tune capture. Omit it entirely to keep the defaults.
#### Allowed `errorTracking` config keys
When passing the object form, use **only** these keys — do not invent others. All are optional.
| Key | Type | Default | Effect |
|---|---|---|---|
| `enabled` | `boolean` | `true` | Master switch for the error tracking module. |
| `captureGlobalErrors` | `boolean` | `true` | Capture uncaught errors via the global handler. |
| `captureUnhandledRejections` | `boolean` | `true` | Capture unhandled promise rejections. |
| `suppressInDev` | `boolean` | `false` | Skip capture while running in `__DEV__`. |
| `enableRateLimiting` | `boolean` | `true` | Drop events once the per-window limit is exceeded. |
| `maxEventsPerWindow` | `number` | `20` | Max events captured per rate-limit window. |
| `rateLimitWindowMs` | `number` | `1000` | Rate-limit window length, in milliseconds. |
| `disconnectDurationMs` | `number` | `2000` | Cool-down after the limit is hit, in milliseconds. |
| `promiseRejectionsAsHandled` | `boolean` | `false` | Report unhandled rejections as handled (`warning`) instead of unhandled (`error`). |
| `bundleId` | `string` | — | JS bundle hash for symbolication of OTA-updated apps. |
| `beforeCapture` | `(payload) => boolean \| void` | — | Enrich or filter each event; return `false` to drop it. |
> `errorTracking: false` (the boolean form) fully disables the module. The object form keeps tracking on and overrides only the keys you set.
**`src/conviva.ts` (TypeScript) — disable:**
```ts
import { createTracker, ReactNativeTracker } from '@convivainc/conviva-react-native-appanalytics';
let tracker: ReactNativeTracker | undefined;
try {
tracker = createTracker('YOUR_CUSTOMER_KEY', 'YOUR_APP_NAME', {
errorTracking: false, // fully disable JS error capture
});
if (!tracker) {
console.error('Tracker initialization returned null');
}
} catch (error) {
console.error(error);
}
export { tracker };
```
**`src/conviva.js` (JavaScript) — tune capture:**
```js
import { createTracker } from '@convivainc/conviva-react-native-appanalytics';
let tracker;
try {
tracker = createTracker('YOUR_CUSTOMER_KEY', 'YOUR_APP_NAME', {
errorTracking: {
suppressInDev: true, // skip capture in __DEV__
maxEventsPerWindow: 10, // default 20
beforeCapture: (payload) => {
// Return false to drop an event; mutate payload to enrich it.
if (payload.message.includes('Network request failed')) {
return false;
}
},
},
});
if (!tracker) {
console.error('Tracker initialization returned null');
}
} catch (error) {
console.error(error);
}
export { tracker };
```
**Merging with an existing third argument** — `createTracker` takes a single config object. If one is already passed (e.g. `clidSyncConfig` from WebView CLID sync), add `errorTracking` to the **same** object; do not add a second argument or replace existing keys:
```js
tracker = createTracker('YOUR_CUSTOMER_KEY', 'YOUR_APP_NAME', {
clidSyncConfig: {
webViewCookie: { domains: ['.example.com'] },
},
errorTracking: {
suppressInDev: true,
},
});
```
### Capture render-phase errors with `ConvivaErrorBoundary`
Wrap any subtree to capture errors thrown during render and show a fallback UI.
```jsx
import { ConvivaErrorBoundary } from '@convivainc/conviva-react-native-appanalytics';
import { View, Text, Button } from 'react-native';
<ConvivaErrorBoundary
name="CheckoutScreen"
fallback={({ error, reset }) => (
<View>
<Text>Something went wrong: {String(error && error.message)}</Text>
<Button title="Try again" onPress={reset} />
</View>
)}
>
<CheckoutScreen />
</ConvivaErrorBoundary>
```
### Report handled errors with `trackError`
```js
import { trackError } from '@convivainc/conviva-react-native-appanalytics';
try {
await riskyOperation();
} catch (error) {
try {
await trackError({
message: error.message,
errorType: error.name, // optional
stackTrace: error.stack, // optional
isFatal: false, // optional
isHandled: true, // optional
attributes: { feature: 'checkout', step: 'payment' }, // optional
});
} catch (e) {
console.error(e);
}
}
```
> `trackError` is also available on the tracker instance: `tracker.trackError({ message: '...' })`. Only `message` is required.
### Attach custom attributes to every error
```js
import { errorTracker } from '@convivainc/conviva-react-native-appanalytics';
try {
errorTracker.addAttribute('appBuild', '2026.6.1');
errorTracker.addAttribute('environment', 'production');
// Remove when no longer relevant:
errorTracker.removeAttribute('environment');
} catch (error) {
console.error(error);
}
```
> `errorTracker` also exposes `setEnabled(boolean)` to toggle capture and `setRateLimitingEnabled(boolean)` to toggle rate limiting at runtime.
---
## React Navigation Autotracking
### React Navigation >= 5 (NavigationContainer)
Import and wrap `NavigationContainer` in your root component. Pass all existing props unchanged.
**JavaScript:**
```js
import { NavigationContainer } from '@react-navigation/native';
import {
withReactNavigationAutotrack,
autocaptureNavigationTrack,
} from '@convivainc/conviva-react-native-appanalytics';
let ConvivaNavigationContainer;
try {
ConvivaNavigationContainer =
withReactNavigationAutotrack(autocaptureNavigationTrack)(NavigationContainer);
if (!ConvivaNavigationContainer) {
console.error('Navigation container setup returned null');
}
} catch (error) {
console.error(error);
}
```
**TypeScript:**
```ts
import { NavigationContainer } from '@react-navigation/native';
import {
withReactNavigationAutotrack,
autocaptureNavigationTrack,
} from '@convivainc/conviva-react-native-appanalytics';
let ConvivaNavigationContainer: typeof NavigationContainer | undefined;
try {
ConvivaNavigationContainer =
withReactNavigationAutotrack(autocaptureNavigationTrack)(NavigationContainer) as typeof NavigationContainer;
if (!ConvivaNavigationContainer) {
console.error('Navigation container setup returned null');
}
} catch (error) {
console.error(error);
}
// In your render / return:
export default function App() {
return (
<ConvivaNavigationContainer>
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
</ConvivaNavigationContainer>
);
}
```
### React Navigation < 5 (createAppContainer)
Wrap the result of `createAppContainer()` with `withReactNavigationAutotrack(autocaptureNavigationTrack)`.
```js
import { createStackNavigator, createAppContainer } from 'react-navigation';
import {
withReactNavigationAutotrack,
autocaptureNavigationTrack,
} from '@convivainc/conviva-react-native-appanalytics';
const AppNavigator = createAppContainer(
createStackNavigator(
{
Home: { screen: HomeScreen },
Settings: { screen: SettingsScreen },
},
{ initialRouteName: 'Home' }
)
);
let App;
try {
App = withReactNavigationAutotrack(autocaptureNavigationTrack)(AppNavigator);
if (!App) {
console.error('Navigation app setup returned null');
}
} catch (error) {
console.error(error);
}
export default App;
```
---
## Babel Plugin Configuration
The required Babel configuration depends on the installed `CONVIVA_RN_VERSION`. Check `package.json` before applying.
### For CONVIVA_RN_VERSION <= 0.2.8
Add two separate plugins for button click auto-detection and `displayName` injection.
#### babel.config.js
```js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
// ... existing plugins ...
'./node_modules/@convivainc/conviva-react-native-appanalytics/instrumentation/index.js',
'add-react-displayname',
],
};
```
#### .babelrc
```json
{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
"./node_modules/@convivainc/conviva-react-native-appanalytics/instrumentation/index.js",
"add-react-displayname"
]
}
```
Add the following to `devDependencies` in `package.json`:
```json
"babel-plugin-add-react-displayname": "0.0.5",
"babel-types": "^6.26.0",
"babel-template": "^6.26.0"
```
- `babel-plugin-add-react-displayname` `0.0.5` is the final published version.
- `babel-types` and `babel-template` at `^6.26.0` are legacy Babel 6 packages required by `instrumentation/index.js`; they are not installed automatically in Babel 7 projects.
Then run `npm install` (or `yarn` / `pnpm install`) from the project root.
---
### For CONVIVA_RN_VERSION >= 0.3.0
A single unified Conviva plugin handles both button click and screen view auto-detection.
#### babel.config.js
Add the Conviva instrumentation plugin to the `plugins` array. Append only - do not modify or remove existing entries.
```js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
// ... existing plugins ...
'@convivainc/conviva-react-native-appanalytics/plugin',
],
};
```
#### .babelrc
```json
{
"presets": ["module:metro-react-native-babel-preset"],
"plugins": [
"@convivainc/conviva-react-native-appanalytics/plugin"
]
}
```
> The Conviva instrumentation plugin bundles `@babel/plugin-transform-react-display-name` internally via Babel's `inherits` -- do NOT add it separately to `plugins`. `displayName` injection (used for button click and screen_view component identification) is handled automatically.
> `@babel/plugin-transform-react-display-name` is declared as a direct `dependency` of the Conviva package and installs transitively into the host project's `node_modules` -- no explicit `devDependencies` entry is required.
---
After modifying `babel.config.js` (either version), restart the Metro bundler with `--reset-cache`:
```bash
npx react-native start --reset-cache
```
---
## WebView Event Bridge (Optional)
To track events from web content rendered in `react-native-webview`, set the `onMessage` callback:
```js
import { getWebViewCallback } from '@convivainc/conviva-react-native-appanalytics';
import { WebView } from 'react-native-webview';
let webViewCallback;
try {
webViewCallback = getWebViewCallback();
if (!webViewCallback) {
console.error('WebView callback setup returned null');
}
} catch (error) {
console.error(error);
}
// In your component render:
<WebView
source={{ uri: 'https://your-web-app.com' }}
onMessage={webViewCallback}
/>
```
---
## Client ID (Optional)
> **Prefer automatic sync for `react-native-webview`.** If the app uses `react-native-webview` v11+, use `clidSyncConfig` in `createTracker` (see **§ WebView Client ID Sync** below) — it handles cookie seeding and JS bridge automatically with no manual code.
>
> Use `getClientId()` / `setClientId()` only for surfaces automatic sync cannot reach, or when managing the client ID for your own backend purposes.
```js
import { getClientId, setClientId } from '@convivainc/conviva-react-native-appanalytics';
try {
// Get current client ID
const clientId = await getClientId();
if (clientId == null) {
console.error('getClientId returned null');
}
// Set a specific client ID
await setClientId('your-client-id');
} catch (error) {
console.error(error);
}
```
---
## WebView Client ID Sync (Optional)
Pass `clidSyncConfig` to `createTracker` only when the developer explicitly requests linking native and in-app WebView sessions. Omit this config entirely if not requested.
**`src/conviva.ts` (TypeScript):**
```ts
import { createTracker, ReactNativeTracker } from '@convivainc/conviva-react-native-appanalytics';
let tracker: ReactNativeTracker | undefined;
try {
tracker = createTracker('YOUR_CUSTOMER_KEY', 'YOUR_APP_NAME', {
clidSyncConfig: {
webViewCookie: {
domains: ['.example.com', '.partner.com'], // leading-dot covers all subdomains
},
},
});
if (!tracker) {
console.error('Tracker initialization returned null');
}
} catch (error) {
console.error(error);
}
export { tracker };
```
**`src/conviva.js` (JavaScript):**
```js
import { createTracker } from '@convivainc/conviva-react-native-appanalytics';
let tracker;
try {
tracker = createTracker('YOUR_CUSTOMER_KEY', 'YOUR_APP_NAME', {
clidSyncConfig: {
webViewCookie: {
domains: ['.example.com', '.partner.com'], // leading-dot covers all subdomains
},
},
});
if (!tracker) {
console.error('Tracker initialization returned null');
}
} catch (error) {
console.error(error);
}
export { tracker };
```
> Replace `.example.com` and `.partner.com` with the actual domains hosting the in-app WebView content. Use the same domain list in Conviva remote config to ensure uninterrupted client ID sharing from the very first WebView load.