Android
Use the FounderHQ Android SDK.
Capture screens, app lifecycle, and your own events from a Kotlin app.
Before you start
You need a publishable key (fhq_pk_...) for the brand you want to
measure. See Getting started.
Install
dependencies {
implementation("com.getfounderhq:events:0.8.0")
// Navigation Compose screens
implementation("com.getfounderhq:events-compose:0.8.0")
}The SDK needs minSdk 24 and Java 17.
Initialize
Create one client in your Application, and share it.
class MyApplication : Application() {
lateinit var events: FounderHQEvents
override fun onCreate() {
super.onCreate()
events = FounderHQEvents(this, "fhq_pk_XXXX")
}
}Pass a config when you want to change the defaults:
events = FounderHQEvents(
this,
"fhq_pk_XXXX",
FounderHQEventsConfig(captureScreens = true),
)The client registers activity lifecycle callbacks, captures activity screens and application lifecycle, and records the session start.
Capture events
events.capture("trial.started", mapOf("plan" to "growth"))capture waits for startup to finish, then queues the event. Call
readyForCapture() when you want to wait for startup on its own.
Record a deep link when your app opens from one:
events.captureDeepLink(url)captureDeepLink drops the query and fragment, and keeps the campaign
parameters it recognizes. After you read Play Install Referrer data, pass
it on:
events.captureInstallReferrer(mapOf("utm_source" to "google_play"))Track screens
Activity screens are captured for you. Call events.screen("Pricing")
anywhere else.
With Navigation Compose, add the observer inside your NavHost scope:
import com.founderhq.events.compose.FounderHQNavigationObserver
FounderHQNavigationObserver(navController = navController, events = events)It records the current route, and skips a repeat of the route the app is already on.
Identify a contact
events.identify("user_42", mapOf("email" to "jane@acme.com"))Change identity and account in one call:
events.identify(
"user_42",
mapOf("email" to "jane@acme.com"),
FounderHQAccountContext("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", mapOf("plan" to "growth"))
events.setAccountProperties(mapOf("seats" to 12))
events.clearAccount() // resetAccounts() does the samesetAccount also takes a FounderHQAccountContext, which carries key,
properties, and contextToken. Set account in the config when the first
automatic event must already carry it. An account change rotates the
account span only. The person session and queued events are untouched. See
Accounts and groups.
Consent
| You call | What happens |
|---|---|
optIn() | Capture is on |
optOut() | Capture stops, and the queue is cleared |
isOptedOut() | Returns true while capture is off |
Set optOutByDefault = true in the config to start silent until the person
agrees. See Consent and privacy.
The SDK never collects advertising identifiers.
Purchases
The client can tie a Play Billing or RevenueCat purchase to the person who made it.
| Method | What it does |
|---|---|
purchaseAttribution() | Returns the identifiers to pass to the store or to RevenueCat |
preparePurchase(source, completion) | Records intent before checkout, and returns the purchase context |
applyPurchaseContext(builder, prepared) | Puts the context on your Play Billing flow |
observePurchase(purchase, prepared) | 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.
Config
FounderHQEventsConfig takes these values.
| Value | Type | Default | What it does |
|---|---|---|---|
host | String | https://i.getfounderhq.com | Where events are sent |
flushAt | Int | 20 | Sends a batch once this many events are queued |
flushIntervalSeconds | Long | 10 | Sends a batch on this timer. 0 turns the timer off |
personProfiles | PersonProfiles | IDENTIFIED_ONLY | When contact property changes are applied |
optOutByDefault | Boolean | false | Starts opted out |
captureLifecycle | Boolean | true | App opened and backgrounded events |
captureScreens | Boolean | true | Activity screen events |
captureSessions | Boolean | true | Session start events |
captureInstallUpdates | Boolean | true | App installed and updated events |
remoteConfig | Boolean | true | Reads capture settings you set in FounderHQ |
account | FounderHQAccountContext? | null | Installs account context before the first event |
purchasePrepareTimeoutMillis | Long | 3000 | How long preparePurchase waits before checkout goes ahead offline |
captureElementInteractions | Boolean | false | Taps and rage taps. See Element interactions |
capturePushNotificationOpened | Boolean | true | Records the notification taps your app reports |
tracingHeaders | List<String>? | null | Hostnames whose requests carry the session id |
maxQueueSize | Int | 1000 | Events kept offline before the oldest are dropped |
eventTtlMillis | Long | 86400000 | How long an unsent event may wait |
maxRetries | Int | 5 | Times a failed event is retried |
debug | Boolean | false | Logs what the SDK drops or refuses |
FounderHQEventsDependencies replaces the clock, UUID source, storage,
transport, and platform facts. Use it in tests.
Sending
| Method | Returns | What it does |
|---|---|---|
flush() | Boolean | Sends queued events now, and tells you whether the queue drained |
close() | — | Flushes, stops the executors, and unregisters the lifecycle callbacks |
getDistinctId() | String | The current identity |
getSessionId() | String | The current session |
FounderHQEvents implements AutoCloseable.
Element interactions
Set captureElementInteractions 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.
Each event carries the control and up to four of its parents.
| Stored | Not stored |
|---|---|
| The view class, resource entry name, and content description | Anything a person typed |
| The path through the view hierarchy | Tap coordinates |
The label a Button shows, or the selected tab of a TabLayout | The contents or hint of an EditText |
A tap on an EditText records nothing at all, and a plain TextView never
contributes its words. Give a view the tag android:tag="fhq-no-capture" to
skip it and everything inside it.
Labels are cut to 255 characters. Numbers that look like a card number or a social security number are removed from them.
Only a tap counts. A finger that moves further than the platform's touch slop, a second finger, or a cancelled touch means the gesture was a scroll. Lifting a finger over a row at the end of a scroll records nothing.
Remote config
The client reads your capture settings from FounderHQ at startup, caches them in its storage, and applies the cached copy on the next launch.
| Key | What it turns on and off |
|---|---|
capture_sessions | Session start events |
capture_screens | Screen events |
capture_lifecycle | App opened and backgrounded events |
autocapture | Taps and rage taps |
capture_rageclicks | Rage 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 values in your config 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(remote) to apply settings you supply yourself. Set
remoteConfig = false in the config 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.