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

    Not built yet

    • Play install referrer on React Native
    • deep_link_url on app opens
    • Showing in-app messages
    • Deferred deep link calls
    See the code
  • 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

    Not built yet

    • Consent per purpose (optOut / optIn only)
    • Showing in-app messages
    • Deferred deep link calls
    See the code
  • 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

    Not built yet

    • Consent per purpose (optOut / optIn only)
    • SKAdNetwork / AdAttributionKit conversion values
    • Showing in-app messages
    • Deferred deep link calls
    See the code
  • 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

    Not built yet

    • Consent per purpose (optOut / optIn only)
    • Install referrer without a plugin
    • Showing in-app messages
    • Deferred deep link calls
    See the code

Quick start

Five steps, from an empty project to your first event.

  1. 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.

  2. 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_….

  3. 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.

  4. 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.

  5. 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 page
  • checkout_startedA user began checkout
  • order_completedAn order was paid, with revenue and currency
  • subscription_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.

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.

  1. 01Build with the development key (la_pk_dev_…) and set debug: true to see what the SDK does.
  2. 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.
  3. 03Fix what the debugger reports. The Implementation score shows which planned events are still missing.
  4. 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.

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.