Middleware React Native Real User Monitoring SDK
- AutoInstrumentation HTTP Monitoring
- AutoInstrumentaion JS Errors
- AutoInstrumenation navigation tracking for
react-navigation - AutoInstrumenation native crash errors
- Custom Instrumenation using OpenTelemetry
- Custom logging
- RUM Session Tracking
- Session Recording
Middleware React Native for Mobile supports React Native 0.68 and higher.
Since v2.0.0 the SDK wraps the stable Middleware native SDKs
(io.github.middleware-labs:android-sdk 3.1.4+ and the MiddlewareRum
CocoaPod 2.2.2+), which brings v3 session recording (rrweb replay), native
crash/ANR reporting, and screen-name linked replays. Native toolchain
requirements:
- Android:
compileSdkVersion35+, Kotlin Gradle Plugin 2.0.21+ (pinclasspath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21")on React Native < 0.77),minSdkVersion21. If your app still enables Jetifier, addandroid.jetifier.ignorelist=jackson-coretogradle.properties. - iOS: deployment target 13.0+, CocoaPods (the
MiddlewareRumpod is pulled automatically). When building pods as static libraries (the default), addpod 'Reachability', :modular_headers => trueto your Podfile.
The library is also compatible with the following frameworks and libraries:
- Expo (SDK 52+, via a development build — see below)
- React Navigation 5, 6 and 7
yarn add @middleware.io/middleware-react-nativeThis SDK contains custom native code, so it cannot run in Expo Go — Expo Go ships a fixed native binary and has no way to load the Middleware native SDKs. You need a development build.
Add the config plugin to app.json / app.config.js:
{
"expo": {
"plugins": ["@middleware.io/middleware-react-native"]
}
}Then create a development build:
npx expo prebuild --clean # regenerate ios/ and android/ with the plugin applied
npx expo run:android # or: npx expo run:iosFor EAS, eas build --profile development picks the plugin up automatically.
The plugin applies the native requirements that a managed project can't set on its own:
| Change | Why |
|---|---|
android.jetifier.ignorelist=jackson-core |
Jetifier fails on the jackson-core inside android-sdk 3.x |
| Kotlin Gradle Plugin >= 2.0.21 | android-sdk 3.x is compiled with Kotlin 2.0.21 metadata |
compileSdkVersion >= 35 |
required by android-sdk 3.x (an already-higher value is left alone) |
| iOS deployment target >= 13.0 | minimum for the MiddlewareRum pod |
pod 'Reachability', :modular_headers => true |
Reachability ships no modulemap, so Swift can't import it under static libraries (skipped when the app uses use_frameworks!) |
Options, if you need to override a default:
{
"expo": {
"plugins": [
["@middleware.io/middleware-react-native", {
"kotlinVersion": "2.0.21",
"compileSdkVersion": 35,
"iosDeploymentTarget": "13.0",
"jetifierIgnorelist": true,
"reachabilityModularHeaders": true
}]
]
}
}If spans stop flowing, the first thing to check is whether the native module made it into the binary:
import { MiddlewareRum } from '@middleware.io/middleware-react-native';
console.log('Middleware native linked:', MiddlewareRum.isNativeAvailable());false means you are on Expo Go, or running a binary built before the package
was added — rebuild with npx expo run:android / run:ios.
The SDK falls back to sending traces from JS over OTLP/HTTP in two cases: the native module isn't linked, or native initialization failed. Either way traces still reach Middleware, while crash/ANR reporting and session recording stay off until the native side is healthy. Native failures are logged with their cause, for example:
ERROR [MiddlewareRum] native initialize failed: Method addObserver must be called on the main thread
Set debug: true in your configuration to also see per-export detail.
import { MiddlewareWrapper, type ReactNativeConfiguration } from '@middleware.io/middleware-react-native';
const MiddlewareConfig: ReactNativeConfiguration = {
serviceName: 'Mobile-SDK-ReactNative',
projectName: '$Mobile-SDK-ReactNative',
accountKey: '<middleware-account-key>',
target: '<target-url>',
deploymentEnvironment: 'PROD',
globalAttributes: {
name: '<your-name>',
},
};
export default function App() {
return (
<MiddlewareWrapper configuration={MiddlewareConfig}>
// Application Components
</MiddlewareWrapper>
);
}You can add custom logs such as debug, error, warn, info these logs will be shown on Middleware Logs Dashboard
MiddlewareRum.debug("I am debug");
MiddlewareRum.error("I am error");
MiddlewareRum.info("I am info");
MiddlewareRum.warn("I am warn");You can set global attributes by calling setGlobalAttributes function.
MiddlewareRum.setGlobalAttributes({
"name": "Middleware",
"app.version": "1.0.0",
"custom_key": "some value"
});To ignore capturing urls pass Array<String | RegExp> in ignoreUrls key in ReactNativeConfiguration
Example:
ignoreUrls: [/^\/api\/facts/, /^\/api\/v1\/users\/.*/],Note: By default SDK captures following
Content-type
application/jsonapplication/texttext/x-component
To redact network headers Set<String> in ignoreHeaders key in ReactNativeConfiguration
Example:
ignoreHeaders: new Set(['x-ignored-header']),Note: By default x-access-token will be readacted.
To disable network instrumentation set networkInstrumentation: false
const MiddlewareConfig: ReactNativeConfiguration = {
...
networkInstrumentation: false
};End-to-end tracing links a RUM session to the backend traces it caused, so you can open a slow screen in the session explorer and see the server spans behind it.
It works by trace-context propagation: the SDK creates a client span for each outgoing request and
injects the W3C traceparent header. Your instrumented backend continues that same trace, and
Middleware correlates the two by trace ID.
This is on by default and requires no code. Every request made through fetch or
XMLHttpRequest is traced and carries trace headers.
To keep your trace IDs off third-party APIs, narrow propagation to your own domains with
tracePropagationTargets, which takes Array<string | RegExp>:
const MiddlewareConfig: ReactNativeConfiguration = {
...
tracePropagationTargets: [/api.example.com/, /anotherapi.example.com/]
};Requests to other hosts are still timed and still appear in the session — they just travel without trace headers. An explicit empty array disables propagation entirely.
Prefer regexes: a RegExp entry is matched against the URL, but a plain string entry has to
equal the whole URL exactly, so 'api.example.com' matches nothing.
By default both W3C (traceparent) and B3 headers are sent. Use tracePropagationFormat: 'w3c'
or 'b3' to send only one.
You can report handled errors, exceptions, and messages using the reportError function
try{
throw new Error("I am error")
} catch (err) {
MiddlewareRum.reportError(err);
}You can set latitude & longitde as global attributes.
MiddlewareRum.updateLocation(latitude: number, longitude: number)By default session recording is enabled, to disable session recording pass sessionRecording: false configuration as follows -
const MiddlewareConfig: ReactNativeConfiguration = {
serviceName: 'Mobile-SDK-ReactNative',
projectName: '$Mobile-SDK-ReactNative',
accountKey: '<middleware-account-key>',
target: '<target-url>',
sessionRecording: false,
deploymentEnvironment: 'PROD',
globalAttributes: {
name: '<your-name>',
},
};Tune how the recording is captured with recordingOptions:
const MiddlewareConfig: ReactNativeConfiguration = {
// ...
sessionRecording: true,
recordingOptions: {
frequency: 'standard', // 'low' (~1 fps, default) | 'standard' | 'high'
quality: 'standard', // 'low' | 'standard' (default) | 'high'
maskAllTextInputs: true, // default true
maskAllImages: true, // default true
},
// Fraction of sessions that get recorded (0.0 - 1.0). Defaults to 1.0.
sessionSamplingRatio: 1.0,
};Recording can be controlled after initialization — useful when you only want to record a specific flow:
import { MiddlewareRum } from '@middleware.io/middleware-react-native';
await MiddlewareRum.startRecording(); // -> boolean: recording is running
await MiddlewareRum.stopRecording(); // -> boolean: recording was stopped
await MiddlewareRum.isRecording(); // -> booleanBoth calls are sticky: they survive session rotation and override the session sampler, so recording stays in the state you asked for until you change it again.
startRecording() also overrides sessionRecording: false, which lets you keep
recording off by default and turn it on only where you need it:
MiddlewareRum.init({ ...config, sessionRecording: false });
// later, e.g. when the user enters the checkout flow
await MiddlewareRum.startRecording();
// ...
await MiddlewareRum.stopRecording();Views will get blurred hiding sensitive information in session recording.
<MiddlewareSanitizedView>
<Component/>
</MiddlewareSanitizedView>See the contributing guide to learn how to contribute to the repository and the development workflow.
Apache 2.0