For developers
Add LeanApp to your app
Four SDKs and a REST API send your app's events to LeanApp. This guide covers which SDK to use, how to set it up, what to send and how to check it before you ship.
The SDKs are built and tested in our CI, but not yet on public package registries. We give them to your team during onboarding, and this page will show the install command when each one is published.
SDKs
One SDK per platform, with the same method names and rules for queueing, retries and sessions.
JavaScript / React Native
Not published yet- Package
@leanapp/analytics- Runs on
- Web browsers, React Native apps and Node.js 18+ servers. No dependencies.
- Status
- Not public yet: available from us during onboarding.
What it does
- Events, screens, users and sessions
- Consent per purpose (analytics, marketing, push, attribution)
- Campaign data from URLs: utm_* and ad click ids
- Offline queue: localStorage in browsers, AsyncStorage on React Native
See the codeNot built yet
- Play install referrer on React Native
- deep_link_url on app opens
- Showing in-app messages
- Deferred deep link calls
Android (Kotlin)
Not published yet- Package
io.leanapp:leanapp-android- Runs on
- Android 5.0+ (API 21). Needs Play Install Referrer.
- Status
- Not public yet: available from us during onboarding.
What it does
- Events, screens, users and sessions
- app_installed, app_updated and app_opened sent for you
- Google Play install referrer, read once per install
- Campaign data from the link that opened the app
See the codeNot built yet
- Consent per purpose (optOut / optIn only)
- Showing in-app messages
- Deferred deep link calls
iOS (Swift)
Not published yet- Package
LeanApp (Swift Package)- Runs on
- iOS 13 and later, plus macOS 10.15+ and tvOS 13+. It has no dependencies, so adding it brings no other libraries into your app's build.
- Status
- Not public yet: available from us during onboarding.
What it does
- Events, screens, users and sessions
- app_installed, app_updated and app_opened sent for you
- APNs push tokens
- Campaign data from deep links and universal links you pass in
See the codeNot built yet
- Consent per purpose (optOut / optIn only)
- SKAdNetwork / AdAttributionKit conversion values
- Showing in-app messages
- Deferred deep link calls
Flutter (Dart)
Not published yet- Package
leanapp_analytics- Runs on
- Flutter 3.13+ (Dart 3.1+) on Android and iOS, with http and shared_preferences as dependencies.
- Status
- Not public yet: available from us during onboarding.
What it does
- Events, screens, users and sessions
- app_installed, app_updated and app_opened sent for you
- Install referrer passed in from a plugin such as play_install_referrer
- Campaign data from links you pass in
See the codeNot built yet
- Consent per purpose (optOut / optIn only)
- Install referrer without a plugin
- Showing in-app messages
- Deferred deep link calls
Quick start
Five steps, from an empty project to your first event.
- 01
Create your project
Sign up and create your app. Every project gets three environments: development, staging and production, and each keeps its data apart.
- 02
Get your keys
Open Settings → Dev Ops → SDK & API keys. Each environment has its own public SDK key: la_pk_dev_…, la_pk_stg_… and la_pk_live_….
- 03
Add the SDK and initialize it
Initialize the SDK once, as early as you can: in Application.onCreate, didFinishLaunching or main(). Use the development key while you build.
- 04
Send the events in your tracking plan
Settings → Dev Ops → Implementation lists every event to send, with its properties and a code snippet you can copy for each one.
- 05
Watch them arrive
Settings → Dev Ops → Debugger shows each event live and whether it matches the plan. Switch to the production key when it is clean.
Code by platform
The same calls work on every platform. Open a platform to see its code.
JavaScript / React Native
Add and initialize
// Not on npm yet: we share the package with you during onboarding.
import { Analytics } from "@leanapp/analytics";
Analytics.initialize({
apiKey: "la_pk_dev_…", // development key while you build
appVersion: "2.4.0",
// React Native: keep the queue across restarts and send it in the background
// storage: asyncStorageAdapter(AsyncStorage), appState: AppState,
});Screens, events and users
Analytics.screen("Home");
Analytics.track("product_viewed", { product_id: "123", price: 299, currency: "SAR" });
// After login
Analytics.identify("user_123", { plan: "plus", language: "ar" });
// Your own id as eventId, so a retry is never counted twice
Analytics.track("order_completed", { order_id: "o1", revenue: 45, currency: "SAR" }, { eventId: "order-o1" });
// On logout
Analytics.reset();Links, privacy and sending
// Campaign data from the opening URL (utm_* and click ids)
Analytics.captureAttribution(window.location.href);
// Consent: hold events until the user answers, then record the answer
Analytics.initialize({ apiKey: "la_pk_dev_…", consentDefault: "pending" });
Analytics.setConsent({ analytics: true, marketing: false, push: true, attribution: true });
// Optional: send the queue now
await Analytics.flush();Android (Kotlin)
Add and initialize
// Not on Maven Central yet: we share the module with you during onboarding.
import io.leanapp.analytics.Analytics
import io.leanapp.analytics.AnalyticsOptions
class App : Application() {
override fun onCreate() {
super.onCreate()
Analytics.initialize(this, "la_pk_dev_…", AnalyticsOptions(debug = true))
}
}Screens, events and users
Analytics.screen("Home")
Analytics.track("product_viewed", mapOf("product_id" to "123", "price" to 299, "currency" to "SAR"))
// After login
Analytics.identify("user_123", mapOf("plan" to "plus", "language" to "ar"))
// Your own id as eventId, so a retry is never counted twice
Analytics.track("order_completed", mapOf("order_id" to "o1", "revenue" to 45.0, "currency" to "SAR"), eventId = "order-o1")
// On logout
Analytics.reset()Links, privacy and sending
// The link that launched the app is captured automatically.
// Links that arrive while the app is running:
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intent.dataString?.let { Analytics.captureAttribution(it) }
}
// The user turned analytics off (optIn() turns it back on)
Analytics.optOut()
// Optional: send the queue now
Analytics.flush()iOS (Swift)
Add and initialize
// Not tagged as a public Swift package yet: we share it with you during onboarding.
import LeanApp
// application(_:didFinishLaunchingWithOptions:), on the main thread
var options = AnalyticsOptions()
options.debug = true
Analytics.initialize(apiKey: "la_pk_dev_…", options: options)Screens, events and users
Analytics.screen("Home")
Analytics.track("product_viewed", properties: ["product_id": "123", "price": 299, "currency": "SAR"])
// After login
Analytics.identify("user_123", traits: ["plan": "plus", "language": "ar"])
// Your own id as eventId, so a retry is never counted twice
Analytics.track("order_completed", properties: ["order_id": "o1", "revenue": 45.0, "currency": "SAR"], eventId: "order-o1")
// On logout
Analytics.reset()Links, privacy and sending
// Deep links and universal links
func scene(_ scene: UIScene, openURLContexts contexts: Set<UIOpenURLContext>) {
contexts.forEach { Analytics.captureAttribution($0.url) }
}
// The user turned analytics off (optIn() turns it back on)
Analytics.optOut()
// Optional: send the queue now
Analytics.flush()Flutter (Dart)
Add and initialize
// Not on pub.dev yet: we share the package with you during onboarding.
import 'package:leanapp_analytics/leanapp_analytics.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Analytics.initialize(apiKey: 'la_pk_dev_…', appVersion: '2.4.0', appBuild: '240', debug: true);
runApp(const MyApp());
}Screens, events and users
Analytics.screen('Home');
Analytics.track('product_viewed', {'product_id': '123', 'price': 299, 'currency': 'SAR'});
// After login
Analytics.identify('user_123', {'plan': 'plus', 'language': 'ar'});
// Your own id as eventId, so a retry is never counted twice
Analytics.track('order_completed', {'order_id': 'o1', 'revenue': 45, 'currency': 'SAR'}, 'order-o1');
// On logout
Analytics.reset();Links, privacy and sending
// Links from app_links (or your own link handling)
Analytics.captureAttribution(uri.toString());
// The user turned analytics off (optIn() turns it back on)
Analytics.optOut();
// Optional: send the queue now
await Analytics.flush();Event names and tracking plan
Clean names make every report easier to read. The tracking plan we build with you follows these rules.
- Use snake_case, object first, then the action in the past tense: product_viewed, order_completed.
- An event name must start with a letter and can be at most 100 characters long.
- Send one event per business fact, with variations in properties (payment_method, source).
- Money: send a number in revenue, value or price, plus an ISO 4217 currency code such as SAR or AED. A refund is its own event, never a negative revenue on the order event.
- User properties describe the person (city, plan, language); event properties describe each action.
- Unplanned events are kept and shown, with suggestions to map each one to a planned event.
Examples
product_viewedA user opened a product pagecheckout_startedA user began checkoutorder_completedAn order was paid, with revenue and currencysubscription_renewedA subscription renewed (send it from your server)refund_completedA refund was issued (send it from your server)
Users: identify and logout
Events before login carry an anonymous id. Once you identify the user, LeanApp links both histories.
- Right after login or sign-up, call identify with your own stable user id, not an email or phone number.
- Pass traits that describe the person (plan, city, language). setUserProperties updates them later.
- Call reset on logout. The next user on the same device starts with a new anonymous id.
- Use alias to merge a guest id into a real account when your app has guest checkout.
Consent and privacy
Ask for consent where the law requires it, then pass the answer to LeanApp so the platform enforces it.
Purposes in the JavaScript / React Native SDK
analyticstrack, screen, identify and alias. Denied: events are dropped on the device, never stored.pushregisterPushToken. Denied: the user is suppressed from every push message.attributionCampaign data on events. Denied: it is removed on the device and again at ingestion.marketingNothing on the device. Denied: the user is suppressed from every marketing message.
- JavaScript / React Native: initialize with consentDefault: "pending" to hold events in memory until the user answers, then call setConsent. The answer is stored on the device and follows the user after login.
- Android, iOS and Flutter: optOut() stops sending, optIn() resumes. No consent per purpose yet.
- None of the SDKs collect advertising ids (IDFA, GAID), Android ID or IDFV.
- Privacy exports and deletions are available from the dashboard and the API.
How events are sent
After setup, calls return at once and never throw: events are queued and sent in the background.
- Batching
- Sent every 10 seconds or when 20 events are queued, up to 100 events per request.
- Offline
- The queue is saved on the device, up to 1,000 events with the oldest dropped first, and it survives restarts. Events older than 7 days are dropped.
- Background
- The queue is sent when the app goes to the background: on page hide in browsers, with appState on React Native, when the last activity stops on Android, in a background task on iOS, and on pause in Flutter apps, so nothing waits for the next launch.
- Retries
- Network errors and server errors retry with backoff from 1 second to 5 minutes. Every event carries its own id, so a retry is never counted twice.
- Wrong or revoked key
- The SDK stops sending but keeps events on the device, so none are lost while you fix it.
- Sessions
- A new session starts after 30 minutes of inactivity.
Test before production
Check every event in the development environment before you release your app to real users.
- 01Build with the development key (la_pk_dev_…) and set debug: true to see what the SDK does.
- 02Open Settings → Dev Ops → Debugger in the development environment, then use the app. Each event appears live as it arrives, together with any validation errors it has.
- 03Fix what the debugger reports. The Implementation score shows which planned events are still missing.
- 04Repeat on staging if you have it, then ship with the production key (la_pk_live_…). Development data never appears in production reports, so your real numbers stay clean.
Deep links and attribution
Tracking links and deep links show which campaign brought each user. Here is what the SDKs do today.
- Android reads the link that opened the app by itself. On iOS and Flutter, and for links that arrive while an Android app is already running, pass the URL to captureAttribution.
- captureAttribution keeps utm_* parameters and click ids (click_id, gclid, fbclid, ttclid, ScCid and others): the first touch is kept, and the latest touch is sent with every event.
- Android reads the Google Play install referrer once per install, so installs from LeanApp links match exactly. Flutter needs a plugin for this, and React Native doesn't read it yet.
- Deferred deep links (opening the right screen after a fresh install) are not called by the SDKs yet. The server API exists in beta; until the SDKs use it, routing after install is up to your app.
- Opening the right screen when the app is already installed is up to your app's link handling.
From your servers: REST API
Send events your server knows best, and read data from your own tools, with the REST API.
- Use secret keys (la_sk_…) on servers only. The JavaScript SDK refuses one outside a server.
- POST /v1/events sends one event and POST /v1/events/batch up to 500.
- Send payments, renewals and refunds from your backend, with the transaction id as event_id.
- Secret keys have scopes for privacy requests, the tracking plan and reports. Ask us for the API spec.
curl -X POST https://api.leanapp.io/v1/events \
-H "Authorization: Bearer la_sk_live_…" \
-H "Content-Type: application/json" \
-d '{"type":"track","event_name":"order_completed","event_id":"o-1029",
"user_id":"u-42","properties":{"order_id":"o-1029","revenue":120,"currency":"SAR"}}'Notes and recommendations
Build with the development key
Use la_pk_dev_… while you build and test, and the production key only in release builds, so test events never land in the reports your team reads.
Use snake_case event names
Follow your tracking plan exactly. A typo in an event name creates a new, unplanned event that your reports count apart from the planned one.
No personal data in properties
Don't send names, emails, phone numbers or national ids in event properties. Identify users with your own internal id instead of anything personal.
Identify after login
Call identify as soon as the user logs in or signs up, and call reset on logout, so a shared device never mixes the events of two different users.
Let the queue work
Events are batched and sent in the background, so you rarely need to step in. Call flush only when you must, for example before a forced exit.
Send revenue from the server
Payment confirmations belong in your backend, sent with a secret key and the transaction id as event_id, so a retried payment is never counted twice.
Keep the SDK updated
Take new SDK versions when we release them: they carry fixes and new capabilities, such as deferred deep links once the SDKs call the server API.
Never ship a secret key
Apps get the public key (la_pk_…) only. Secret keys stay on your servers, because anyone who downloads your app can extract a key from it.
Ready to connect your app?
Create your account to get your keys, or write to us and we'll help your developers.