@userpilot/babel-plugin-react-native-autocapture
v1.0.0
Published
Babel plugin that instruments React Native JSX for Userpilot autocapture. Wraps Pressable, Touchable*, TextInput, Switch, Modal at build time so autocapture works without runtime monkey-patching.
Readme
Userpilot React Native Autocapture Babel Plugin
Part of the Userpilot React Native monorepo. The runtime SDK that consumes the events emitted by this plugin lives at
@userpilot/react-native. For the cross-cutting autocapture guide (screens, interactions, privacy, hierarchy format), seedocs/Autocapture.md.
The Userpilot React Native Autocapture Babel Plugin is the build-time companion to @userpilot/react-native. It instruments your app's JSX at compile time so the SDK can emit unified InteractionEvent payloads — bit-for-bit identical to the iOS and Android Userpilot SDKs — without any runtime monkey-patching of React Native components.
This document provides a step-by-step walkthrough of the installation and configuration process, plus reference material for the plugin's options, supported targets, and emitted payloads.
🚀 Getting Started
Prerequisites
React Native — your application should use React Native version 0.73 or above. Applications using Expo should use version 50 or above. The New Architecture is fully supported.
Userpilot React Native SDK — this plugin emits JSX that imports a runtime helper from @userpilot/react-native. The runtime SDK must be installed and initialized via Userpilot.setup(token, { enableInteractionAutoCapture: true }) for events to be dispatched.
npm install @userpilot/react-nativeNode.js — the plugin runs in your build pipeline (Babel/Metro). Node 18+ is required.
Installation
Add the plugin to your application's dev dependencies.
- In your app's root directory, install the plugin:
npm install --save-dev @userpilot/babel-plugin-react-native-autocapture - Reset the Metro cache once after the first install so the new transform applies to every source file:
npx react-native start --reset-cache
Configuring babel.config.js
Add the plugin to your application's babel.config.js. The plugin must run inside React Native's Babel pipeline; it does not apply to files that bypass Babel (e.g. precompiled native libraries).
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['@userpilot/babel-plugin-react-native-autocapture'],
};That's it. As long as your app calls Userpilot.setup(token, { enableInteractionAutoCapture: true }) at startup, every supported JSX tag in your app emits autocapture events on each interaction.
What the Plugin Does
For each matching JSX node, the plugin rewrites the call site to render through a tiny React.forwardRef runtime helper exported from @userpilot/react-native:
// before
<Pressable onPress={handlePress}>Login</Pressable>
// after (conceptually — actual binding is uniqued per file)
<__userpilotWrap
__upTag={Pressable}
__upInteractionType="tap"
__upElementType="Pressable"
onPress={handlePress}>
Login
</__userpilotWrap>The helper dispatches the unified InteractionEvent and renders the original component with all original props and refs intact. The transform is idempotent — re-running Babel on already-instrumented JSX is a no-op.
Plugin Options
Pass options to the plugin via the babel.config.js array form:
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
[
'@userpilot/babel-plugin-react-native-autocapture',
{
enabled: true,
presets: ['react-native'],
targets: [],
exclude: [],
logging: false,
},
],
],
};Options:
| Option | Type | Description | Default |
| ---------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ |
| enabled | boolean | Master switch. Set to false to disable instrumentation entirely (e.g. in a build profile that ships without analytics). | true |
| presets | ('react-native')[] | Built-in component groups to wrap. The react-native preset covers Pressable, Touchable*, Button, TextInput, Switch, and Modal. | ['react-native'] |
| targets | TargetGroup[] | Additional component groups to wrap. Layered on top of presets — useful for opting third-party UI libraries in. See Adding Third-Party Targets. | [] |
| exclude | string[] | Literal path substrings to skip. Glob and regex syntax is not supported; node_modules is always implicitly excluded. | [] |
| logging | boolean | Log per-file/per-match diagnostics to stderr. Use only when investigating why a node was not wrapped. | false |
Built-in Presets
The react-native preset covers the standard interactive primitives shipped with React Native:
| Preset name | Components covered | Default interactionType |
| -------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| react-native | Pressable, TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, TouchableNativeFeedback, Button | tap |
| react-native | TextInput | text_field_changed |
| react-native | Switch | switch_changed |
| react-native | Modal | view_presented |
These mirror the Android InteractionType enum exactly so the wire payload stays consistent across iOS, Android, and React Native.
Adding Third-Party Targets
To instrument components from third-party UI libraries (e.g. react-native-paper), declare them in targets. The source field matches the import source string — both literal strings and RegExp are supported. The components array names the imports to wrap and the interactionType they emit.
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: [
[
'@userpilot/babel-plugin-react-native-autocapture',
{
targets: [
{
source: 'react-native-paper',
components: [
{ name: 'Button', interactionType: 'tap' },
{ name: 'IconButton', interactionType: 'tap' },
{ name: 'TextInput', interactionType: 'text_field_changed' },
],
},
{
source: '@react-native-community/checkbox',
components: [
{ name: 'CheckBox', interactionType: 'checkbox_selected' },
],
},
],
},
],
],
};Each TargetComponent accepts:
| Field | Type | Description |
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
| name | string | The local binding used in JSX (e.g. "Button"). |
| interactionType | string | The Userpilot InteractionType string emitted for this component. Must match a value the runtime understands. |
| elementType | string | Optional. Override the elementType recorded on the payload (defaults to name). |
Excluding Files
To skip specific paths from instrumentation, pass literal path substrings via
exclude. To target one file, use its full project-relative path. Each entry is
matched literally against the filename Babel provides, so glob and regular-expression
syntax is not supported. node_modules is always implicitly excluded.
plugins: [
[
'@userpilot/babel-plugin-react-native-autocapture',
{
exclude: ['src/screens/internal/Debug.tsx', 'src/storybook/'],
},
],
];What Gets Emitted
Each instrumented interaction produces an InteractionEvent whose shape mirrors the iOS and Android Userpilot SDKs. The runtime SDK forwards it through autoCaptureEvent(eventType, payload) to the same analytics pipeline iOS and Android use — no schema branching downstream.
{
raw_interaction_type: 'tap' | 'switch_changed' | 'text_field_changed' | 'view_presented' | …,
target_class: 'react-native/Pressable',
target_view_name: 'Pressable',
target_text: 'Login',
accessibility_label: 'Login button',
accessibility_identifier: 'login-button',
hierarchy: 'Pressable:attr__id="login-button",attr__index="0",attr__desc="Login button";View:attr__index="0";LoginScreen',
// and more, depending on InteractionType — see the InteractionEvent reference.
}⚙️ Coexistence with the SDK
@userpilot/react-native does not monkey-patch React Native components. Interaction autocapture depends on this Babel plugin wrapping source files that pass through Babel.
- Source files that go through Metro/Babel are instrumented automatically once the plugin is configured.
- Precompiled third-party libraries that do not go through the app's Babel pipeline are not instrumented unless you explicitly compile them through Metro/Babel and configure matching
targets. - Calling
Userpilot.stopAutocapture()at runtime suppresses event dispatch from already-wrapped components, so you don't need to disable the plugin to pause capture during sensitive flows.
⚠️ Limitations
- Compile-time only. Code that doesn't go through Babel (precompiled native libraries, third-party bundles) is invisible to the instrumenter.
React.createElement(Pressable, …)direct calls are not wrapped. The plugin only sees JSX.- HOCs that capture a component reference (
styled(Pressable)) are not wrapped at the call site. Add the wrapped HOC totargetsinstead. WebViewinteractions are out of scope — content inside a WebView is web territory.
📝 Documentation
For the high-level autocapture story, including screen tracking adapters, privacy controls, and runtime APIs, see the React Native Autocapture guide.
For the runtime SDK reference, see @userpilot/react-native.
Full product documentation is available at Userpilot.
🎬 Examples
The example directory in this repository contains a full example iOS/Android app that uses this plugin alongside @userpilot/react-native for end-to-end autocapture, including third-party targets, privacy components, and screen tracking via React Navigation.
📄 License
This project is licensed under the MIT License. See LICENSE for more information.
