
Research
/Security News
Mini Shai-Hulud Campaign Hits Red Hat Cloud Services npm Packages
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.
@multiplayer-app/session-recorder-react-native
Advanced tools
Multiplayer Fullstack Session Recorder for React Native

The Multiplayer Session Recorder for React Native provides comprehensive session recording capabilities for React Native applications, including gesture tracking, navigation monitoring, screen recording, and full-stack debugging. It includes full support for both bare React Native and Expo applications.
This package does NOT support React Native Web. The session recorder relies on native modules for core functionality:
If you need web support, consider using the browser-specific session recorder package instead.
npm install @multiplayer-app/session-recorder-react-native
# or
yarn add @multiplayer-app/session-recorder-react-native
Recommended: Initialize with SessionRecorder.init(options) before you mount your React Native app to avoid losing any data (similar to Sentry's guidance).
This package requires the following dependencies to be installed in your React Native application:
npm install @react-native-async-storage/async-storage @react-native-community/netinfo react-native-svg react-native-safe-area-context
# or
yarn add @react-native-async-storage/async-storage @react-native-community/netinfo react-native-svg react-native-safe-area-context
Important: Native modules must be installed directly in your app's package.json. React Native autolinking only links native modules that are declared by the app itself, not modules pulled in transitively by libraries. If you don't add them directly, you may see errors like "NativeModule: AsyncStorage is null" or SVGs not rendering.
# Install native dependencies in your app
npm install @react-native-async-storage/async-storage @react-native-community/netinfo react-native-svg react-native-safe-area-context
# iOS: Install pods from your app's ios directory
cd ios && pod install && cd -
# Android: Clean and rebuild
cd android && ./gradlew clean && cd -
Use Expo's version-aware installer so versions match the SDK:
npx expo install @react-native-async-storage/async-storage @react-native-community/netinfo react-native-svg react-native-safe-area-context
If you use Expo Router or a managed workflow, no extra autolinking steps are required beyond installing the packages.
This package includes full Expo autolinking support with the following configuration files:
expo-module.config.json - Defines supported platforms and module namesreact-native.config.js - Configures React Native CLI autolinkingThe module will be automatically detected and linked when you install it in your Expo project.
Important: This package uses native modules that require custom native code. While autolinking is configured, you may encounter issues with Expo Go due to its limitations with custom native modules.
If autolinking doesn't work with Expo Go, try using development builds instead:
# For iOS
npx expo run:ios
# For Android
npx expo run:android
Development builds include your custom native modules and provide the full native functionality needed for session recording.
If you encounter issues with module autolinking in Expo:
npx expo start -c to clear the Expo cachenode_modules/@multiplayer-app/session-recorder-react-native/ directoryFor Expo applications, the package automatically detects the Expo environment:
import SessionRecorder from '@multiplayer-app/session-recorder-react-native';
SessionRecorder.init({
application: 'my-expo-app',
version: '1.0.0',
environment: 'production',
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
recordGestures: true,
recordNavigation: true,
recordScreen: true,
});
The package will automatically:
expo-constantspackage.jsonpeerDependencies and document installationIf you encounter:
[@RNC/AsyncStorage]: NativeModule: AsyncStorage is null
@react-native-async-storage/async-storage is installed in your app (not only in this library)cd ios && pod install, then rebuild the appnpm start -- --reset-cache (or expo start -c)The SessionRecorderProvider is required for the session recorder to work properly. It provides:
import React from 'react';
import {
SessionRecorderProvider,
SessionRecorder,
} from '@multiplayer-app/session-recorder-react-native';
// Initialize with minimal required options
SessionRecorder.init({
application: 'my-react-native-app',
version: '1.0.0',
environment: 'production',
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
});
export default function App() {
return (
<SessionRecorderProvider>{/* Your app content */}</SessionRecorderProvider>
);
}
import React from 'react';
import { Stack } from 'expo-router';
import {
SessionRecorderProvider,
SessionRecorder,
} from '@multiplayer-app/session-recorder-react-native';
// Initialize with minimal required options
SessionRecorder.init({
application: 'my-expo-app',
version: '1.0.0',
environment: 'production',
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
});
export default function RootLayout() {
return (
<SessionRecorderProvider>
<Stack />
</SessionRecorderProvider>
);
}
This minimal setup will:
import React from 'react';
import {
SessionRecorderProvider,
SessionRecorder,
} from '@multiplayer-app/session-recorder-react-native';
SessionRecorder.init({
application: 'my-react-native-app',
version: '1.0.0',
environment: 'production',
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
recordGestures: true, // default is true
recordNavigation: true, // default is true
recordScreen: true, // default is true
});
export default function App() {
return (
<SessionRecorderProvider>{/* Your app content */}</SessionRecorderProvider>
);
}
import React from 'react';
import { Stack } from 'expo-router';
import {
SessionRecorderProvider,
SessionRecorder,
} from '@multiplayer-app/session-recorder-react-native';
SessionRecorder.init({
application: 'my-expo-app',
version: '1.0.0',
environment: 'production',
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
recordGestures: true, // default is true
recordNavigation: true, // default is true
recordScreen: true, // default is true
});
export default function RootLayout() {
return (
<SessionRecorderProvider>
<Stack />
</SessionRecorderProvider>
);
}
Here's a complete example showing how to integrate the session recorder in your React Native app:
import React, { useEffect, useRef } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import {
SessionRecorderProvider,
SessionRecorder,
} from '@multiplayer-app/session-recorder-react-native';
import { AppNavigator } from './navigation/AppNavigator';
// Initialize session recorder
SessionRecorder.init({
application: 'my-react-native-app',
version: '1.0.0',
environment: __DEV__ ? 'development' : 'production',
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
recordGestures: true,
recordNavigation: true,
recordScreen: false, // Enable after adding permissions
});
export default function App() {
const navigationRef = useRef(null);
useEffect(() => {
// Set session attributes for better debugging context
SessionRecorder.setSessionAttributes({
userId: 'user123',
userType: 'premium',
appVersion: '1.0.0',
});
}, []);
return (
<SessionRecorderProvider>
<SafeAreaProvider>
<NavigationContainer
ref={navigationRef}
onReady={() => {
SessionRecorder.setNavigationRef(navigationRef.current);
}}
>
<AppNavigator />
</NavigationContainer>
</SafeAreaProvider>
</SessionRecorderProvider>
);
}
For Expo applications, the package automatically detects the Expo environment:
import SessionRecorder from '@multiplayer-app/session-recorder-react-native';
SessionRecorder.init({
application: 'my-expo-app',
version: '1.0.0',
environment: 'production',
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
recordGestures: true,
recordNavigation: true,
recordScreen: true,
});
The package will automatically:
expo-constantsExpo Router already manages the NavigationContainer. Don't add your own.
import { useEffect } from 'react';
import { Stack, useNavigationContainerRef } from 'expo-router';
import {
SessionRecorderProvider,
SessionRecorder,
} from '@multiplayer-app/session-recorder-react-native';
export default function RootLayout() {
const navigationRef = useNavigationContainerRef();
useEffect(() => {
const unsub = navigationRef.addListener?.('state', () => {
SessionRecorder.setNavigationRef(navigationRef);
unsub?.();
});
return unsub;
}, [navigationRef]);
return (
<SessionRecorderProvider>
<Stack />
</SessionRecorderProvider>
);
}
If you own the NavigationContainer, set the ref in onReady:
import { NavigationContainer } from '@react-navigation/native';
import { useRef } from 'react';
import {
SessionRecorderProvider,
SessionRecorder,
} from '@multiplayer-app/session-recorder-react-native';
export default function App() {
const navigationRef = useRef<any>(null);
return (
<SessionRecorderProvider>
<NavigationContainer
ref={navigationRef}
onReady={() => {
SessionRecorder.setNavigationRef(navigationRef.current);
}}
>
{/* Your navigation stack */}
</NavigationContainer>
</SessionRecorderProvider>
);
}
// Start a new recording session
SessionRecorder.start();
// Pause current recording
SessionRecorder.pause();
// Resume paused recording
SessionRecorder.resume();
// Stop recording with optional reason
SessionRecorder.stop('Session completed');
// Save continuous recording (for continuous mode)
SessionRecorder.save();
// Set session attributes for better context
SessionRecorder.setSessionAttributes({
userId: 'user123',
feature: 'checkout',
version: '2.1.0',
});
The React Native SDK automatically captures uncaught exceptions via the global error handler and converts them into error traces with standard exception attributes (exception.type, exception.message, exception.stacktrace).
You can also report errors manually:
import SessionRecorder from '@multiplayer-app/session-recorder-react-native';
try {
// risky code
} catch (err) {
SessionRecorder.captureException(err); // Error | unknown | string
}
// Arbitrary messages are supported as well
SessionRecorder.captureException('Payment form validation failed');
In Continuous mode, captured exceptions mark the current trace as ERROR and auto‑save the rolling session window so you can replay what led to the failure.
This package exports a simple ErrorBoundary component that reports render errors and shows a fallback UI.
import React from 'react';
import { ErrorBoundary } from '@multiplayer-app/session-recorder-react-native';
export function AppWithBoundary() {
return (
<ErrorBoundary fallback={<></>}>
<App />
</ErrorBoundary>
);
}
To hide the floating widget button but keep the modal functionality:
SessionRecorder.init({
application: 'my-app',
version: '1.0.0',
environment: 'production',
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
// Disable the floating button
widget: {
enabled: true,
button: {
visible: false, // Hide the floating button
},
},
});
Use the useSessionRecorder hook to control the widget modal programmatically:
import React from 'react';
import { View, Button } from 'react-native';
import { useSessionRecorder } from '@multiplayer-app/session-recorder-react-native';
function MyComponent() {
const { openWidgetModal, closeWidgetModal } = useSessionRecorder();
return (
<View>
<Button title="Open Session Recorder" onPress={openWidgetModal} />
<Button title="Close Session Recorder" onPress={closeWidgetModal} />
</View>
);
}
import React from 'react';
import { View, Button } from 'react-native';
import { useSessionRecorder } from '@multiplayer-app/session-recorder-react-native';
function SessionControls() {
const {
startSession,
stopSession,
pauseSession,
resumeSession,
saveSession,
} = useSessionRecorder();
return (
<View>
<Button title="Start Session" onPress={() => startSession()} />
<Button title="Pause Session" onPress={() => pauseSession()} />
<Button title="Resume Session" onPress={() => resumeSession()} />
<Button
title="Stop Session"
onPress={() => stopSession('User completed')}
/>
<Button title="Save Session" onPress={() => saveSession()} />
</View>
);
}
import React from 'react';
import { View, Text } from 'react-native';
import { useSessionRecorderStore } from '@multiplayer-app/session-recorder-react-native';
function SessionStatus() {
const sessionType = useSessionRecorderStore((s) => s.sessionType);
const isWidgetModalVisible = useSessionRecorderStore(
(s) => s.isWidgetModalVisible
);
const sessionState = useSessionRecorderStore((s) => s.sessionState);
const isOnline = useSessionRecorderStore((s) => s.isOnline);
return (
<View>
<Text>Session State: {sessionState}</Text>
<Text>Session Type: {sessionType}</Text>
<Text>Widget Visible: {isWidgetModalVisible ? 'Yes' : 'No'}</Text>
<Text>Online: {isOnline ? 'Yes' : 'No'}</Text>
</View>
);
}
import React, { useEffect } from 'react';
import { View, Button, Text, Alert } from 'react-native';
import {
SessionRecorderProvider,
useSessionRecorder,
useSessionRecorderStore,
} from '@multiplayer-app/session-recorder-react-native';
function SessionRecorderUI() {
const { startSession, stopSession, openWidgetModal } = useSessionRecorder();
const { sessionState, isWidgetModalVisible } = useSessionRecorderStore(
(state) => ({
sessionState: state.sessionState,
isWidgetModalVisible: state.isWidgetModalVisible,
})
);
const handleStartRecording = async () => {
try {
await startSession();
Alert.alert('Success', 'Session recording started');
} catch (error) {
Alert.alert('Error', 'Failed to start recording');
}
};
const handleStopRecording = async () => {
try {
await stopSession('User manually stopped');
Alert.alert('Success', 'Session recording stopped');
} catch (error) {
Alert.alert('Error', 'Failed to stop recording');
}
};
return (
<View style={{ padding: 20 }}>
<Text>Session State: {sessionState}</Text>
<Text>Widget Modal: {isWidgetModalVisible ? 'Open' : 'Closed'}</Text>
<Button title="Start Recording" onPress={handleStartRecording} />
<Button title="Stop Recording" onPress={handleStopRecording} />
<Button title="Open Widget" onPress={openWidgetModal} />
</View>
);
}
export default function App() {
return (
<SessionRecorderProvider>
<SessionRecorderUI />
</SessionRecorderProvider>
);
}
The session recorder automatically captures target element information for all gesture interactions, enriching your OpenTelemetry traces with valuable context about what users are interacting with.
When users interact with elements, the following attributes are automatically added to gesture spans:
| Attribute | Description | Example |
|---|---|---|
gesture.target | Primary identifier for the target element | "Submit Button" |
gesture.target.label | Accessibility label of the element | "Submit form" |
gesture.target.role | Accessibility role of the element | "button" |
gesture.target.test_id | Test ID of the element | "submit-btn" |
gesture.target.text | Text content of the element | "Submit" |
The recorder automatically extracts target information from React Native elements using the following priority:
accessibilityLabel - Explicit accessibility label (highest priority)testID - Test identifier (lowest priority)To get the most useful target information in your traces, follow these practices:
<TouchableOpacity
accessibilityLabel="Submit user registration form"
accessibilityRole="button"
onPress={handleSubmit}
>
<Text>Submit</Text>
</TouchableOpacity>
<TouchableOpacity
testID="registration-submit-btn"
accessibilityLabel="Submit registration"
onPress={handleSubmit}
>
<Text>Submit</Text>
</TouchableOpacity>
<TouchableOpacity onPress={handleSubmit}>
<Text>Submit Registration</Text> {/* Clear, descriptive text */}
</TouchableOpacity>
// ❌ Poor trace information
<TouchableOpacity accessibilityLabel="Button" onPress={handleSubmit}>
<Text>Click</Text>
</TouchableOpacity>
// ✅ Rich trace information
<TouchableOpacity
accessibilityLabel="Submit user registration form"
testID="registration-submit"
onPress={handleSubmit}
>
<Text>Submit Registration</Text>
</TouchableOpacity>
With proper element labeling, your gesture traces will include rich context:
{
"spanName": "Gesture.tap",
"attributes": {
"gesture.type": "tap",
"gesture.platform": "react-native",
"gesture.coordinates.x": 150.5,
"gesture.coordinates.y": 200.3,
"gesture.target": "Submit user registration form",
"gesture.target.label": "Submit user registration form",
"gesture.target.role": "button",
"gesture.target.test_id": "registration-submit",
"gesture.target.text": "Submit Registration"
}
}
This rich context helps you:
The session recorder captures your app's UI using react-native-view-shot, which:
This is different from system-wide screen recording which would require permissions.
| Option | Type | Description |
|---|---|---|
apiKey | string | Your Multiplayer API key |
application | string | Application name |
version | string | Application version |
environment | string | Environment (production, etc.) |
| Option | Type | Default | Description |
|---|---|---|---|
recordGestures | boolean | true | Enable gesture recording |
recordNavigation | boolean | true | Enable navigation tracking |
recordScreen | boolean | true | Enable screen recording |
sampleTraceRatio | number | 0.15 | Trace sampling ratio (0.0-1.0) |
buffering | object | see below | Crash buffer (rolling window when no recording is active) |
captureBody | boolean | true | Capture request/response bodies |
captureHeaders | boolean | true | Capture request/response headers |
masking | object | - | Data masking configuration |
ignoreUrls | array | [] | URLs to exclude from monitoring |
propagateTraceHeaderCorsUrls | array | [] | URLs for CORS trace header propagation |
showContinuousRecording | boolean | true | Show continuous recording option |
widget | object | - | Session widget configuration |
Buffering (buffering.enabled: false, buffering.windowMinutes: 0.5 by default): when enabled, the SDK keeps a rolling window of recent events and traces even when no recording is active. The buffer is flushed into the session when the user starts a session or saves (e.g. on error).
SessionRecorder.init({
// Required options
application: 'my-app',
version: '1.0.0',
environment: 'production',
apiKey: 'YOUR_MULTIPLAYER_API_KEY',
// Recording options
recordGestures: true,
recordNavigation: true,
recordScreen: true, // Captures app UI automatically
// Crash buffer: rolling window when no recording is active (flushed when session starts/saves)
buffering: {
enabled: true,
windowMinutes: 1, // default: 1
},
// Network monitoring
// NOTE: if frontend domain doesn't match to backend one, set backend domain to `propagateTraceHeaderCorsUrls` parameter
propagateTraceHeaderCorsUrls: [
new RegExp('https://your.backend.api.domain', 'i'), // can be regex or string
new RegExp('https://another.backend.api.domain', 'i'),
],
ignoreUrls: [
/https:\/\/analytics\.example\.com/,
/https:\/\/crashlytics\.com/,
],
captureBody: true,
captureHeaders: true,
maxCapturingHttpPayloadSize: 100000,
// Data masking for sensitive information
masking: {
isContentMaskingEnabled: true,
maskHeadersList: ['authorization', 'cookie', 'x-api-key'],
maskBodyFieldsList: ['password', 'token', 'secret', 'creditCard'],
maskTextInputs: false,
},
// Session widget configuration
widget: {
enabled: true,
button: {
visible: true,
placement: 'bottomRight', // or 'bottomLeft'
},
},
// Continuous recording
showContinuousRecording: true,
});
import {
SessionRecorder,
LogLevel,
} from '@multiplayer-app/session-recorder-react-native';
const config = {
application: 'my-app',
version: '1.0.0',
apiKey: process.env.MULTIPLAYER_API_KEY,
// Development-specific options
...(__DEV__ && {
logger: {
enabled: true,
level: LogLevel.DEBUG,
},
}),
};
SessionRecorder.init(config);
[@RNC/AsyncStorage]: NativeModule: AsyncStorage is null
Solution:
@react-native-async-storage/async-storage is installed in your appcd ios && pod installnpm start -- --reset-cacherecordScreen: true in configurationreact-native-view-shot is properly installedSessionRecorder.setNavigationRef(navigationRef)useNavigationContainerRef() hookonReady callbackrecordGestures: true is setexpo-constants is installed: npx expo install expo-constantsIf you see this error in Expo Go:
TurboModuleRegistry.getEnforcing(...): 'SessionRecorderNative' could not be found
Solution: Switch to development builds instead of Expo Go:
# For iOS
npx expo run:ios
# For Android
npx expo run:android
Expo Go has limitations with custom native modules. Development builds include your custom native code and will resolve this issue.
cd ios && pod install after installing dependenciescd android && ./gradlew cleannpm start -- --reset-cacheEnable debug logging to troubleshoot issues:
import {
SessionRecorder,
LogLevel,
} from '@multiplayer-app/session-recorder-react-native';
SessionRecorder.init({
// ... other config
logger: {
enabled: true,
level: LogLevel.DEBUG,
},
});
Check out the complete example applications in the examples/ directory:
examples/example-app/ - Full React Native app with session recordingexamples/example-app-expo/ - Expo app with session recordingBoth examples include:
| Method | Description | Parameters |
|---|---|---|
init(options) | Initialize the session recorder | SessionRecorderOptions |
start() | Start a new recording session | - |
stop(reason?) | Stop current recording | string? |
pause() | Pause current recording | - |
resume() | Resume paused recording | - |
save() | Save continuous recording | - |
captureException(error) | Report exception as error trace | unknown |
setNavigationRef(ref) | Set navigation reference | NavigationContainerRef |
setSessionAttributes(attrs) | Set session metadata | Record<string, any> |
interface SessionRecorderOptions {
// Required
apiKey: string;
application: string;
version: string;
environment: string;
// Optional
exporterEndpoint?: string;
apiBaseUrl?: string;
recordGestures?: boolean;
recordNavigation?: boolean;
recordScreen?: boolean;
sampleTraceRatio?: number;
captureBody?: boolean;
captureHeaders?: boolean;
maxCapturingHttpPayloadSize?: number;
masking?: MaskingOptions;
ignoreUrls?: Array<string | RegExp>;
propagateTraceHeaderCorsUrls?: PropagateTraceHeaderCorsUrls;
showContinuousRecording?: boolean;
schemifyDocSpanPayload?: boolean;
widget?: WidgetConfig;
logger?: {
level?: number;
enabled?: boolean;
};
}
MIT
Made with create-react-native-library
FAQs
Multiplayer Fullstack Session Recorder for React Native
The npm package @multiplayer-app/session-recorder-react-native receives a total of 224 weekly downloads. As such, @multiplayer-app/session-recorder-react-native popularity was classified as not popular.
We found that @multiplayer-app/session-recorder-react-native demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 5 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.

Research
/Security News
The North Korean malware loader hides in a Packagist-listed package and its GitHub branch to fetch and execute remote code in a likely Contagious Interview-style lure.

Security News
The Rust project is moving toward formal rules on LLM use in contributions after months of internal debate over maintainer burden, code quality, and contributor experience.