Documentation
SDKs

React Native

Use the FounderHQ React Native SDK.

Capture screens, app lifecycle, and your own events from a React Native or Expo app.

Before you start

You need a publishable key (fhq_pk_...) for the brand you want to measure. See Getting started.

Install

npm install @founderhq/events-react-native @react-native-async-storage/async-storage

The SDK needs React Native 0.73 or later and AsyncStorage 1.21 or later.

For the Expo build, add the three optional packages it reads context from:

npx expo install expo-application expo-device expo-localization

Initialize

Create one client and share it across the app.

import { FounderHqReactNativeClient } from "@founderhq/events-react-native";

export const events = new FounderHqReactNativeClient("fhq_pk_XXXX");

On Expo, use the Expo client instead. It adds app, device, and locale context for you.

import { createFounderHqExpoClient } from "@founderhq/events-react-native/expo";

export const events = createFounderHqExpoClient("fhq_pk_XXXX");

The client loads stored state, applies your capture settings, starts app lifecycle capture, and records the session start on its own.

Capture events

events.capture("trial.started", { plan: "growth" });

track is an alias for capture. Both queue the work and return at once.

Every writing method has an ...AndWait twin that resolves after the work is applied: captureAndWait, identifyAndWait, screenAndWait, setAccountAndWait, clearAccountAndWait, resetAccountsAndWait, setAccountPropertiesAndWait, setPersonPropertiesAndWait, and resetAndWait. Use them in tests, and before the app leaves a flow. readyForCapture() resolves once startup and the queued calls are done.

Record a deep link when your app opens from one:

events.captureDeepLink(url);

captureDeepLink records the link without its query string, and keeps the campaign parameters it recognizes.

Track screens

Record a screen yourself with events.screen("Pricing"), or wire up your router.

React Navigation:

import { createReactNavigationTracker } from "@founderhq/events-react-native";

const tracker = createReactNavigationTracker(events, navigationRef);

<NavigationContainer
  ref={navigationRef}
  onReady={tracker.onReady}
  onStateChange={tracker.onStateChange}
/>;

Expo Router:

import { createExpoRouterTracker } from "@founderhq/events-react-native";

const trackPath = createExpoRouterTracker(events);
trackPath(pathname, params);

Both trackers skip a repeat of the screen the app is already on.

Identify a contact

events.identify("user_42", { email: "jane@acme.com" });

Pass an account in the third argument to change identity and account together:

await events.identifyAndWait("user_42", { email: "jane@acme.com" }, {
  account: { key: "workspace_123" },
});

Update contact properties on their own with setPersonProperties(set, setOnce). With the default identified_only mode, changes made before identify stay on the device and go out with the identify call.

Call reset() on logout. It clears the identity and the account.

Accounts

events.setAccount("workspace_123");
events.setAccountProperties({ plan: "growth", seats: 12 });
events.clearAccount(); // resetAccounts() does the same

setAccount also takes an object with key, properties, and contextToken. Pass account to the constructor when the first automatic event must already carry it. A queued event keeps the account it was captured with. See Accounts and groups.

You callWhat happens
optIn()Capture is on
optOut()Capture stops, and the queue is cleared
isOptedOut()Returns true while capture is off

Set opt_out_by_default: true to start silent until the person agrees. See Consent and privacy.

Purchases

The client can tie an App Store, Google Play, or RevenueCat purchase to the person who made it.

MethodWhat it does
purchaseAttribution()Returns the identifiers to pass to the store or to RevenueCat
preparePurchase({ source })Records intent before checkout, and returns the purchase context
observePurchase({ prepared, purchase })Reports the purchase that followed
claimSubscription({ purchase, confirmation })Moves future revenue of an existing subscription

The full flow, including what each store needs, is on Mobile purchase claims.

Options

Pass these in the second argument to the constructor.

OptionTypeDefaultWhat it does
hoststringhttps://i.getfounderhq.comWhere events are sent
flush_atnumber20Sends a batch once this many events are queued
flush_interval_msnumber10000Sends a batch on this timer. 0 turns the timer off
storageadapterAsyncStorageYour own storage adapter
person_profiles"identified_only" | "always" | "never""identified_only"When contact property changes are applied
opt_out_by_defaultbooleanfalseStarts opted out
capture_lifecyclebooleantrueApp opened and backgrounded events
capture_screensbooleantrueScreen events
capture_sessionsbooleantrueSession start events
remote_configbooleantrueReads capture settings you set in FounderHQ
contextobjectnoneYour own values, added to every event
contextProvider() => objectnoneThe same, resolved at capture time
accountstring | object | nullnoneInstalls account context before the first event
revenueCatobjectnoneYour RevenueCat client, kept in step with the identity
purchase_prepare_timeout_msnumber3000How long preparePurchase waits before checkout goes ahead offline
capture_element_interactionsbooleanfalseTaps and rage taps. See Element interactions
capture_element_textbooleantrueRecords the words a control shows
capture_rageclicksbooleantrueRage taps, when element interactions are on
auto_element_capturebooleantrueWatches taps from the app root

Lifecycle and sending

The client sends a batch when the queue reaches flush_at, on the flush timer, and when the app goes to the background.

MethodReturnsWhat it does
flush()Promise<boolean>Sends queued events now
close()Promise<void>Flushes, then stops the timer and the lifecycle listener
startLifecycleCapture()Re-attaches the lifecycle listener after close()
getDistinctId()stringThe current identity
getSessionId()stringThe current session

Element interactions

Set capture_element_interactions to true. The SDK then records $autocapture when someone taps a control, and $rageclick when they tap the same control three times in a second. Both are off by default.

const events = new FounderHqReactNativeClient("fhq_pk_...", {
  capture_element_interactions: true,
});

You add nothing else. The SDK watches taps from the root of every screen, so it needs no code in your components. Your layout does not change, and a root wrapper of your own keeps working.

Create the client in the module your entry file imports. A client created inside a component starts too late to watch the root. Set auto_element_capture to false in that case, and pass elementCaptureProps() to a view yourself.

A Modal is the one place the root cannot see. React Native gives a modal its own window, so taps inside it never reach a view outside it. Pass elementCaptureProps() to the modal's own view:

<Modal visible={open}>
  <View style={{ flex: 1 }} {...events.elementCaptureProps()}>
    <Checkout />
  </View>
</Modal>

Those props only watch. They never take the touch, so every control below behaves as it did before.

Each event carries the control and up to four of its parents.

StoredNot stored
The component name, testID, and accessibilityLabelAnything a person typed
The accessibilityRoleTap coordinates
The words the control showsThe value, placeholder, or contents of a TextInput

A tap on a TextInput, or on any component with value, defaultValue, placeholder, onChangeText, or secureTextEntry, records nothing at all. A field inside a button adds nothing to that button's text, so a Save button around an input reads as Save.

Text is cut to 255 characters. Numbers that look like a card number or a social security number are removed from it. Add fhqNoCapture to a component to skip it and everything inside it. Set capture_element_text to false to record the controls without their words.

Minified builds rename components. Set testID on the controls you want to recognize in your reports.

Remote config

The client reads your capture settings from FounderHQ at startup, caches them on the device, and applies the cached copy on the next launch. It waits up to 1.5 seconds for a fresh answer.

KeyWhat it turns on and off
capture_sessionsSession start events
capture_screensScreen events
capture_lifecycleApp opened and backgrounded events
autocaptureTaps and rage taps
capture_rageclicksRage taps only. Taps are still recorded

Your settings in FounderHQ win. They can turn capture off for every install, and on for an app that shipped with the wrong value. You cannot rebuild an app people have already installed, so the dashboard decides. The options you pass to the constructor apply until your settings arrive.

Events already queued under a key that turns off are dropped before they leave the device.

Call refreshRemoteConfig() to fetch them again while the app runs. Call applyRemoteConfig(config) to apply settings you supply yourself. Set remote_config: false to skip the request.

Next

Read the protocol reference for the wire format, the reserved event names, and the campaign properties a deep link can carry.

AI agent or LLM? Read this page as markdown

On this page