noibu-react-native
v2.1.0
Published
Noibu session replay and error monitoring SDK for React Native
Readme
noibu-react-native
Noibu session replay and error monitoring for React Native. The package is a thin JavaScript layer
over the same native Noibu SDKs used by every Noibu mobile app, so replay, touches, keyboard,
network and crash capture all happen natively — you install the package, call Noibu.init, and
tell it which screen the user is on.
Current version: 2.1.0. See the CHANGELOG for what changed in each release.
Requirements
- React Native
0.80or newer, React19.1or newer. - Android:
minSdkVersion26 (Android 8.0) or higher,compileSdkVersion36 or higher, Kotlin 2.1 or newer. React Native's template already sets the last two on 0.81+; details under Android. - iOS: a deployment target of iOS 14.0 or newer, which every supported React Native already sets.
react-native-webview13or newer, only if you useNoibuWebView.
Both the old and the new React Native architecture are supported.
Install
npm install noibu-react-native
cd ios && pod install # iOSAutolinking does the rest on both platforms. Two things to check once:
Android
Open android/build.gradle and make sure of:
minSdkVersionmust be at least 26. React Native's template sets 23 or 24, so a default app fails to build until it is raised (the error mentionsuses-sdk:minSdkVersion … cannot be smaller than version 26).compileSdkVersionmust be at least 36. Templates from React Native 0.81 already set it; on 0.80 raise it from 35 (the error mentionsrequires … to compile against version 36 or later).- Kotlin 2.1 or newer. Every template from React Native 0.80 pins it, so only an app that
overrides
kotlinVersionwith an older one is affected (the error mentionscompiled with an incompatible version of Kotlin).
iOS
The Noibu iOS SDK ships inside the package, so pod install is all it takes: nothing to add to
your Podfile, no extra pod source and no Swift Package to add in Xcode.
- Keep your Podfile's linkage as it is —
use_frameworks! :linkage => :static(the React Native default) or nouse_frameworks!at all both work. Do not switch to plainuse_frameworks!; Hermes does not support it. - Xcode 15 or newer: set
ENABLE_USER_SCRIPT_SANDBOXINGtoNoon your app target, or the build fails withSandbox: rsync … Operation not permittedand the app crashes at launch withLibrary not loaded: @rpath/coreKit.framework/coreKit. From the Podfile:
post_install do |installer|
installer.aggregate_targets.each do |aggregate_target|
aggregate_target.user_project.native_targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_USER_SCRIPT_SANDBOXING'] = 'NO'
end
end
aggregate_target.user_project.save
end
endInitialize
import { Noibu } from 'noibu-react-native';
Noibu.init({
domain: 'shop.example.com', // provided by Noibu
});Call it early, at the top of index.js or App.tsx. Capture starts natively from there. init
resolves true once the SDK is running and false if it could not start (for example an empty
domain); it never rejects. Optional settings, shown with their defaults:
Noibu.init({
domain: 'shop.example.com',
logLevel: undefined, // 'none' | 'error' | 'warning' | 'info' | 'debug' | 'verbose'
trackTouches: true, // taps and scrolls
trackKeyboard: true, // which field was typed in — never the text
trackNetwork: true, // all HTTP capture
trackErrors: true, // JS errors and native crashes
trackWebViews: true, // capture inside NoibuWebView
});Pages (trackNavigation)
How pages work in 2.0
- Pages are reported explicitly. The SDK no longer detects screens: no route or navigation state change opens a page on its own.
- Call
Noibu.trackNavigation(screenName, attributes?, title?)for every page, including tabs, relevant modals and WebView steps (see Web steps as pages).- Track page groups, not individual views:
screenNameis the page group (Product Detail,Checkout), and attributes carry the variations (productId,step).- Report every page entry, including Back.
- Repeating the same name and attributes continues the current visit; changing the attributes starts a new one.
- Navigation helpers (
useNoibuNavigation,registerNavigationContainer) don't report pages; they only manage transitions.
Page groups
A screen name is a page group, not a view. A page is everything the user experiences as one place in your app — not a single screen, route or component. Page analysis, heatmaps and Journeys aggregate by screen name, so every view of that place reports the same one:
- every product detail, its gallery, size picker and reviews sheet →
Product Detail - every category or listing →
Product List - the address, payment and review steps of checkout →
Checkout
| Think of it as… | Not as… |
|---|---|
| Product Detail (all product details) | ProductDetailScreen |
| Product List (any listing) | WomenTopsCategoryScreen |
| Checkout (all steps) | CheckoutAddressScreen, CheckoutPaymentScreen |
Reporting a page
Report each screen with Noibu.trackNavigation(screenName, attributes?, title?) when it gains focus — with
another router, when its route state changes — keyed by the route's key, so a revisited screen is
reported again:
import { Noibu, PageType } from 'noibu-react-native';
Noibu.trackNavigation('Product Detail', { pageType: PageType.Product, category: 'Clothing', productId: '123' }, 'Running Shoes');screenName— the page group the user is in (Product Detail,Cart,Checkout), shared by every view of that place. Trimmed, and the page group is cut at 100 characters; a blank name reports nothing.attributes— what varies inside the page (category,productId,step…). The first 20 keys of 1–50 characters; values are strings (cut at 200 characters), booleans, finite numbers ornull. Anything else is dropped, never thrown.title(optional) — the page's human-readable title, what Noibu shows for it (the product's name on a product page, say). Without one, the screen name is the title. Trimmed and cut at 1024 characters. It does not identify the visit: a repeated call keeps the visit's first title.pageType(optional attribute) — places the page in a standard ecommerce category:PageType.Home,Collection,Search,Product,Cart,Checkout,Login(matched ignoring case). Journeys groups and colours the page under that category; without it, the page is grouped by its screen name. Only the exact keypageTypeis read:pagetypeorpage_typeis an ordinary attribute.
A trackNavigation made before init resolves is held, and the last one is sent once it does.
Repeated calls are safe. Focus effects and listeners can fire again while the user stays on the same page (a modal closed over it, a re-run effect). Reporting the page that is already current, with unchanged attributes, continues the existing visit; only a different name or different attributes starts a new one.
React Navigation
Install useNoibuNavigation(navigationRef) and report each screen from useFocusEffect. The hook
keeps replay transitions clean and lands an event tracked from the new screen's mount effect on that
screen's page (see Tracking events); it does not report pages.
import { createNavigationContainerRef, NavigationContainer, useFocusEffect } from '@react-navigation/native';
import { Noibu, PageType, useNoibuNavigation } from 'noibu-react-native';
const navigationRef = createNavigationContainerRef();
function App() {
useNoibuNavigation(navigationRef);
return <NavigationContainer ref={navigationRef}>…</NavigationContainer>;
}
function ProductDetailScreen({ route }) {
useFocusEffect(
React.useCallback(() => {
Noibu.trackNavigation('Product Detail', { pageType: PageType.Product, category: 'Clothing', productId: route.params.id });
}, [route.key]),
);
…
}Outside a component, registerNavigationContainer(ref) does the same as the hook and returns an
unsubscribe function.
Naming your pages
| Screen | screenName | pageType | Example attributes |
|---|---|---|---|
| Home | Home | Home | — |
| Category listing | Product List | Collection | category, subcategory |
| Search results | Search | Search | query |
| Product detail | Product Detail | Product | category, productType, productId |
| Cart | Cart | Cart | — |
| Checkout | Checkout | Checkout | step |
| Order confirmation | Order Confirmed | — | orderId |
| Sign in | Login | Login | — |
- Template, not content: every product detail is
Product Detail; ids and user data never go in the name. - Hierarchy and ids go in attributes (
Product Detail+category, notProduct Detail - Clothing - Tops). - Attributes identify the visit: the same name with different attributes starts a new visit.
nullis a value:{ size: null }and nosizekey are different visits. Every view of a page reports the same attributes; transient state (selected size, scroll position) stays out. - Small and stable: well under 100 names, unchanged between releases. Reserved names (
None,Page Check,External traffic,Exit,Other pages,No Page Group,All) are sent without a group.
What happens without a call
The SDK opens a page on its own when there is no call to take it:
- On a fresh start (no previous screen), events, errors and replay land on an unnamed
view/<uuid>page with no group until the first call. If the app reports its first screen within 2 seconds, or before any replay frame is captured, that screen takes the page over; otherwise the placeholder is sent as a visit of its own. - On a return from background, a relaunch or a new session, the next page copies the previous screen's name, group, attributes and title.
- An app that never calls
trackNavigationstill records sessions, made ofview/<uuid>pages.
Migrating from 1.x
| 1.x | 2.0 |
|---|---|
| Noibu.trackNavigation() (no name) | Noibu.trackNavigation(screenName, attributes?) |
| autoTrackNavigation: true in init | Removed — call trackNavigation from each screen |
| useNoibuNavigation(ref) / registerNavigationContainer(ref, onNavigate) reporting a page per route | Same install line, plus trackNavigation on focus; onNavigate is removed |
Apps that relied on automatic tracking must now call trackNavigation on every page; the
useNoibuNavigation install line now only keeps transitions clean.
Bottom tab bars in page analysis
Page analysis pins the screen's bottom bar to the bottom of the snapshot while the rest of the page
expands. @react-navigation/bottom-tabs bars are recognised as is. A hand-rolled bar declares itself
with accessibilityRole on the bar View — 'tablist' on Android (read from the view tag) and
'tabbar' on iOS (the only role React Native turns into the .tabBar trait), so
Platform.OS === 'ios' ? 'tabbar' : 'tablist' — and accessibilityRole="tab" on each item.
Errors and attributes
Noibu.addError(new Error('Payment declined'));
Noibu.addError('Payment declined', stackString);
Noibu.addCustomAttribute('customerId', '42');You do not need to wait for init: anything reported before it lands is held and applied once it
does. addCustomAttribute returns SUCCESS or a reason it was not stored (TOO_MANY_IDS_ADDED
above 10 per session, NAME_TOO_LONG / VALUE_TOO_LONG above 50 characters, ID_NAME_ALREADY_ADDED,
NOT_INITIALIZED, …). Uncaught JavaScript errors, unhandled promise rejections and native crashes
are captured automatically; wrap a subtree with ErrorBoundary to also catch React render errors:
import { ErrorBoundary } from 'noibu-react-native';
<ErrorBoundary fallback={<Oops />}>…</ErrorBoundary>fallback is an element or a function receiving { error, componentStack, eventId, resetError };
the boundary also accepts onError, onMount, onReset and onUnmount.
Tracking events
Noibu.track(name, payload) works like window.NOIBUJS.track on web: a standard ecommerce event
with a payload following the ecommerce schema, or any custom event with a JSON object.
Noibu.track('product_added_to_cart', {
cartLine: { quantity: 1, merchandise: { id: 'sku-42', title: 'Running Shoes' } },
});
const { success, errors } = Noibu.track('promo_banner_dismissed', { campaign: 'summer' });It returns { success, errors } and never throws. Names are capped at 500 characters and payloads at
10,000 characters serialized; standard payloads are checked against the ecommerce schema, and a
type mismatch is reported in errors instead of being sent. Events tracked before init lands are
held (up to 100), and an event tracked as a screen mounts is attributed to that screen.
| Event name | Payload key | Payload |
|---|---|---|
| product_viewed | productVariant | { id, sku, title, price: Money, product: { id, title, type, url, vendor } } |
| product_added_to_cart, product_removed_from_cart | cartLine | { quantity, merchandise: productVariant, cost: { totalAmount: Money } } |
| cart_viewed | cart | { id, totalQuantity, lines: [cartLine], cost: { totalAmount: Money } } |
| collection_viewed | collection | { id, title, productVariants: [productVariant] } |
| search_submitted | searchResult | { query, productVariants: [productVariant] } |
| checkout_started, checkout_address_info_submitted, checkout_contact_info_submitted, checkout_shipping_info_submitted, payment_info_submitted, checkout_completed | checkout | { token, currencyCode, subtotalPrice, totalTax, totalPrice, lineItems: [{ id, title, quantity, variant: productVariant, finalLinePrice }], shippingLine: { price }, discountApplications, order: { id, customer: { id, isFirstOrder } }, transactions } |
Money is { amount, currencyCode }. Every field is optional. Any other event name is a custom event.
Network capture
fetch, XHR, remote <Image> loads and native requests are captured on both platforms without any
wiring, on bare React Native and on Expo alike. Bodies and header values are captured up to 64 KB
and scrubbed of personal data (passwords, emails, card numbers, phone numbers, …) before they are
stored. A GraphQL response that returns 2xx with an errors array is reported as an error.
trackNetwork: false turns all of it off; there is no per-URL ignore list.
If your app installs its own HTTP client factory, add Noibu inside it (both calls are safe to repeat):
// Android
OkHttpClientProvider.setOkHttpClientFactory {
OkHttpClientProvider.createClientBuilder(context)
.installNetworkInstrumentation() // com.noibu.mobile.sessionreplay.api
.build()
}// iOS
RCTSetCustomNSURLSessionConfigurationProvider(^NSURLSessionConfiguration *{
NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.defaultSessionConfiguration;
[[NoibuHTTPInterceptor shared] installNetworkInstrumentationOn:configuration]; // @import NoibuSessionReplay
return configuration;
});WebView capture
Use NoibuWebView in place of react-native-webview's WebView where you want the web content to be
part of the session — its replay, clicks, JavaScript errors and requests join the timeline:
import { NoibuWebView } from 'noibu-react-native';
<NoibuWebView source={{ uri: 'https://shop.example.com/checkout' }} />It takes the same props as WebView and fills its parent by default. Webviews rendered with the
plain WebView are not captured, so a sensitive one (payment 3DS, SSO) stays out by not wrapping
it. Inputs inside a tracked webview are masked in replay. trackWebViews: false disables webview
capture everywhere.
The webview opens no page: what it captures lands on the current page, including after a navigation inside the webview.
Web steps as pages
To see each step of a web flow as its own page, report every step with trackNavigation under the
hosting screen's name, with the step's path as an attribute: the hosting screen reports the document
the webview loads first, and onRouteChange reports each later one.
function CheckoutScreen() {
const webPath = React.useRef('/checkout'); // path?query of the document the webview loads first
useFocusEffect(
React.useCallback(() => {
Noibu.trackNavigation('Checkout', { path: webPath.current });
}, []),
);
return (
<NoibuWebView
source={{ uri: 'https://shop.example.com/checkout' }}
onRouteChange={(path) => {
webPath.current = path;
Noibu.trackNavigation('Checkout', { path });
}}
/>
);
}The hosting screen's first path is the key onRouteChange would use for that URL: its path and
query, with the fragment dropped and an empty path as / (https://shop.example.com/checkout?step=1#top
→ /checkout?step=1). For an inline document loaded with a base URL,
use that base URL (https://example.com/ → /).
The SDK calls onRouteChange with the new route's path?query (the fragment is ignored) each time the
page moves to a different one, through the browser history or a new document. It skips the document the
webview loads first, which the hosting screen reports, and opens no page itself.
Returning to the first document later comes through onRouteChange with that same path.
When the hosting screen reports itself again (on focus), it reports the step it is on — the last path,
starting from the first document's — so the current visit continues.
Lifecycle
Noibu.isInitialized is true between a successful init and shutdown. shutdown() stops
capture, flushes what was recorded and lets you call init again later in the same process (after
a logout, for example). A session that stays in the background for 15 minutes ends there; the next
return starts a new one.
Known limitations
- Privacy mode is not selectable. Card, SSN, email and phone numbers in rendered text are always
scrubbed; text typed into a
<TextInput>is never captured, only which field was used. - Animated content replays as a still. A GIF or animated WebP on Android shows its first frame,
an
<ActivityIndicator>its first frame, and a continuously animating SVG,<Switch>, progress bar or slider freezes after its first 30 changes. - iOS truncated text (
numberOfLines={1}) replays in full and may overrun what sits beside it. - iOS borders under the new architecture are not captured: an outlined card, list separator or input underline replays without its border. Android and the old architecture capture them.
- Android gradients on React Native 0.80–0.82: a
backgroundImagelinear gradient replays as an empty box; from 0.83 it is captured. Solid backgrounds, borders and corner radii capture on every supported version.
