@scalebun-release/react-native
v2.5.38
Published
Production-grade React Native SDK for ScaleBun
Maintainers
Readme
@scalebun-release/react-native
Production-grade React Native SDK for ScaleBun — error reporting, analytics, session replay, and in-app engagement for iOS and Android.
Installation
npm install @scalebun-release/react-native
# or
pnpm add @scalebun-release/react-native
# or
yarn add @scalebun-release/react-nativeReleases from 2.5.0 are published as @scalebun-release/react-native; the
@scalebun/react-native name stops at 2.4.0. An app that already imports from
@scalebun/react-native can keep its imports by installing the new package under
the old name:
npm install @scalebun/react-native@npm:@scalebun-release/react-nativeiOS
cd ios && pod installWire the AppDelegate for OTA bundle loading (required for OTA on iOS):
npx scalebun init ios # wire OTA (ObjC auto-patched; Swift prints the code)
npx scalebun init ios --check # verify the OTA wiring (exit 1 if missing)
npx scalebun init ios --push # also wire the Direct APNs push hooks (optional)Like Android, iOS OTA needs the app's release bundleURL to return
ScaleBunOtaBoot.bundleURL() (Swift) / [ScaleBunOtaBoot bundleURL] (ObjC), or
updates install but never load. It returns the installed update, or the bundle
compiled into the app when there is none yet. init ios applies this to a
standard ObjC AppDelegate and prints the exact code for Swift and custom
bundleURL bodies. Rebuild the native app afterwards.
Push is separate: the --push hooks take over the app's notification-center
delegate, so leave them out if the app already uses Firebase Messaging or notifee
for iOS notifications.
Android
OTA updates need one wiring step in MainApplication so a downloaded bundle
actually loads — without it an update installs, reports success, and the app
silently keeps running the bundle baked into the APK. Run:
npx scalebun init android # wire MainApplication for OTA bundle loading
npx scalebun init android --check # verify (exit 1 if missing)
npx scalebun init android --dry-run # preview changes without writingThis adds ScaleBunOtaModule.getJSBundleFile(...) in the form matching your
React Native version (Kotlin is auto-patched; Java prints manual instructions).
Rebuild the native app afterwards — a JS reload does not load the newly
wired native path.
For push on Android (optional): add android/app/google-services.json, apply
the Google Services Gradle plugin with @react-native-firebase/messaging, and
declare the POST_NOTIFICATIONS permission. Run npx scalebun doctor to see
exactly what's missing.
Run
npx scalebun -hto list every command — SDK setup (doctor,init ios,init android) plus the OTA publishing commands (login,ota publish,rollout, …).
Optional peer dependencies
Some features activate only when their peer package is installed:
| Feature | Peer dependency |
| ------------------ | ----------------------------- |
| Push notifications | @notifee/react-native |
Quick start
Wrap your app in the provider and pass the app's client key from the dashboard
(or run npx scalebun quickstart, which creates one and prints this snippet):
import { ScaleBunProvider } from '@scalebun-release/react-native';
export default function App() {
return (
<ScaleBunProvider
config={{
clientKey: 'YOUR_CLIENT_KEY',
// OTA updates are OFF by default. Leave this out and the app never
// checks for an update. channelOverride is the channel the app asks
// for — without it, `default`. `scalebun ota publish` publishes to it
// unless --channel, SCALEBUN_CHANNEL or scalebun.json says otherwise.
ota: { enabled: true, channelOverride: 'production' },
}}
>
<RootNavigator />
</ScaleBunProvider>
);
}projectId + publishableKey is the legacy configuration: it still sends
analytics, but OTA needs clientKey and never starts without it.
Initialising with ScaleBun.init() instead
ScaleBun.init({ clientKey, appId }) collects the same automatic events as the provider
(automaticEventTracking defaults to true on both paths; pass false to turn it off).
Taps (element_interacted) also need your root wrapped in the provider, because the tapped element
is identified from a React root — with init() already called it takes no config:
ScaleBun.init({ clientKey: 'YOUR_CLIENT_KEY', appId: 'YOUR_APP_ID' }); // module scope, e.g. index.js
export default function App() {
return (
<ScaleBunProvider>
<RootNavigator />
</ScaleBunProvider>
);
}Without the provider, screens are still collected and the SDK logs a one-time warning that taps are not.
The default export exposes the imperative facade for use outside React (identifying users, capturing events, etc.):
import ScaleBun from '@scalebun-release/react-native';Screen names
Screens are named automatically when your navigation container is rendered inside
<ScaleBunProvider> (as <RootNavigator /> above) or the provider is rendered inside it. React
Navigation 6 and 7 and Expo Router are found at runtime with no extra code — the SDK has no dependency
on any navigation library and does not patch one; it locates the container React renders and listens to
it. Calling ScaleBun.setNavigationRef(ref) (or passing navigationRef to the provider) still works and
takes precedence, but is not required. With any other navigation, name each screen with
useScaleBunScreen(name) or <ScaleBunScreen name="…" />.
Automatic screen load timing
Screen loads are timed automatically only when the SDK detects your navigation (React Navigation or
Expo Router): each screen change becomes a screen_load named after the route. An app with neither
records no automatic screen loads — there is no screen to time — so mark them yourself with
ScaleBun.performance.markScreenLoadStart(name) / markScreenLoadEnd(name). Returning from the background is not a screen load (since 2.5.25: earlier versions reported it as a
$app_foreground screen whose duration was the time spent in the background); launch timing
(cold / warm / hot) is reported separately as app start.
Business revenue telemetry
Send purchases with a stable transaction ID and ISO-4217 currency. productId
is the durable plan/SKU key used for grouping; productName is its optional
human-readable label. Tax, fee, and discount are positive deductions from gross
revenue, while refunds and disputes use their own transaction type.
ScaleBun.trackPurchase({
revenue: 29,
currency: 'USD',
transactionId: 'order_123',
productId: 'pro_monthly',
productName: 'Pro monthly',
quantity: 1,
tax: 2.1,
fee: 0.9,
discount: 0,
});
ScaleBun.trackSubscription({
subscriptionId: 'sub_123',
subscriptionEventId: 'invoice:2026-09', // optional; reuse for this same logical change
status: 'started',
plan: {
id: 'pro_monthly',
name: 'Pro monthly',
amount: 29,
currency: 'USD',
interval: 'month',
},
});Subscription lifecycle payloads use schema v2. subscriptionId and optional
subscriptionEventId are trimmed identifiers of 1–160 characters using letters, digits, ., _,
:, or - (the first character must be alphanumeric). Reuse subscriptionEventId only for the
same logical lifecycle change. The backend scopes it to the app and environment and keeps the first
accepted payload. Without it, the SDK-generated envelope ID still makes transport and offline
retries idempotent, but a repeated host call is a new event.
The SDK, not custom properties, stamps the schema and ledger identity. SDK/server provenance is
derived from the authenticated ingestion route and cannot be claimed by the app. React Native does
not invent a Web replay coordinate for subscription events. Invalid/bounded events are dropped;
ScaleBun.events.stats() exposes { pending, dropped } for integration diagnostics.
Recognised revenue, collected cash, currency conversion, and billing-source provenance are server-side measurements; the SDK does not infer them.
Upgrading
To 2.5.25: no $app_foreground screen load
- Screen load no longer reports a return from the background. Earlier versions, in an app
whose navigation the SDK did not detect, recorded every background → foreground switch as a
screen load named
$app_foregroundlasting as long as the app had been in the background (up to 30 s), which inflated screen-load percentiles and Apdex. Automatic screen loads now come only from detected navigation (React Navigation / Expo Router); see Automatic screen load timing. No code change is needed. - Includes 2.5.24's OTA activation fix (architecture-aware restart, no activation restart loop).
To 2.5.22: scalebun.json is optional
- The CLI finds your app by its package name. It reads the Android
applicationId(android/app/build.gradleor.kts), the iOS bundle id (the Xcode project), or Expo'sapp.json/app.config.*, and looks up the app that carries it. Set the package name as the app's Bundle ID in the dashboard (Workspace → Apps) and noscalebun.json,--app-idorSCALEBUN_APP_IDis needed, in CI too.--app-id,SCALEBUN_APP_IDand an existingscalebun.jsonstill take precedence, in that order. - It never guesses between apps. A project that builds several package
names (product flavors, per-configuration bundle ids), or a name that matches
more than one of your apps, gets a picker in a terminal and an error listing
the candidates in CI. Pin one with
scalebun linkor--app-id. - The channel comes from your app's code. With no
--channel,SCALEBUN_CHANNELorchannelinscalebun.json,ota publishandota signuse the literalchannelOverrideyour app sets, anddefaultwhen it sets none. That is the only channel whose releases reach the app. If the app computes the channel at runtime (__DEV__ ? 'staging' : 'production'), the CLI asks in a terminal and requires--channelin CI. ota signno longer defaults toproduction. It used to, whileota publishdefaulted todefault, so the same project could sign for one channel and publish to another. Both now use the order above. Pass--channel productionif you relied on the old default.scalebun linkno longer writeschannel: productionunless you pass--channel. A linkedchanneloutranks the app's ownchannelOverride, so the old default could send releases to a channel the app never checks.scalebun doctornow reports which app and channel commands will use, and warns whenscalebun.jsondisagrees with the app's package name or channel.
To 2.5.17: OTA signing is enforced by default
scalebun ota publishrefuses to publish unsigned. Give it a key with--key-dir <dir>,SCALEBUN_SIGNING_PRIVATE_KEYorSCALEBUN_SIGNER_COMMAND. An app that has never registered a signing key can pass--allow-unsignedexplicitly. In CI, add the flag to the publish step, or better, add the key secrets.--allow-unsignedpublishes unsigned even when a key is available (for example in./.scalebun): the key is not used, and the CLI says so. It cannot be combined with--require-signing.The server refuses an unsigned publish once the app has an active signing key (
UNSIGNED_WHILE_REQUIRED).--allow-unsigneddoes not get past this; sign the release. A device that embeds the key would refuse it anyway.--key-idnow selects the matching key file. Before, a non-default--key-idsigned with the default key and labelled the release with the other id, and devices rejected it asINVALID_SIGNATURE. Now each id maps to its own file:|
--key-id| Private key read from | |---|---| | the default key's id |.scalebun/ota-signing-key.pem| |<default>-standby|.scalebun/ota-standby-key.pem| | anything else |.scalebun/keys/<id>/ota-signing-key.pem|A key kept elsewhere needs
--key-dir. That directory'sota-key-id.txtmust match--key-id.
To 2.5.21
useOtaUpdate()now uses the initialized SDK configuration. It no longer requires an API URL, client key, or app version in normal application code; those are resolved fromScaleBun.init()and the native binary. A channel is only an explicit QA override, so production routing stays server-controlled.
To 2.5.19 (2.5.18 was never published; its change ships here)
ota publish --bundle-pathneeds a bundle built byscalebun ota bundle build(it carries a SHA-bound identity sidecar used to verify the install on the device). A bundle built any other way needs--allow-unverified-prebuilt, and install verification is then unavailable for that release.
Also in 2.5.19
- Trust-anchor failures now fail closed. A key file that is present but invalid refuses every update. Before, iOS silently accepted UNSIGNED releases in that state, and Android errored on every native call. Fix the file (the native log names the problem).
- New "require signed" declaration. Declare it so a build that loses its key
file refuses updates instead of installing them unsigned:
- Android:
<meta-data android:name="com.scalebun.ota.REQUIRE_SIGNED" android:value="true" />in<application>; - iOS:
ScaleBunOtaRequireSigned = YESin Info.plist.
- Android:
- Unsigned refusals are reported properly. An unsigned release refused by a
signing app is no longer retried or reported as a SHA-256 mismatch. It is
reported once, as
UNSIGNED_RELEASE_REFUSED. checkForUpdate()no longer needsinstallationId. Omit it and the SDK's own install id is used, the same onesync()and sessions use. Passing your own id still works but splits the device in the Fleet.ota publishandota promoteaccept--override, the same asota rollout, for the high-risk approval gate (owner/admin only).ota keys generatewrites the key file intoandroid/andios/, or prints exactly what to write. Pass--no-embedto skip.
Requirements
- React Native
>= 0.74 - React
>= 18
License
MIT © ScaleBun
