Events
Report sign ups, purchases and other conversions with Lander.trackEvent, backed by an offline queue.
Installs and opens are recorded for you. Use Lander.trackEvent for the things that happen after: a sign up, a purchase, a completed onboarding. Pass the linkId of the link that brought the user in, and the conversion is attributed to that link.
Track an event
await Lander.trackEvent(
'purchase',
properties: {'productId': '123', 'amount': 49.99, 'currency': 'USD'},
linkId: link?.linkId, // optional: the LanderLink that opened this screen
);static Future<void> trackEvent(
String name, {
Map<String, Object?> properties = const {},
String? linkId,
})Prop
Type
The returned future completes once the event is safely in the on device queue, not when it reaches Lander. You can await it or fire and forget.
What Lander records without you
| Event | When | Sent by |
|---|---|---|
| Install | The first launch report of a new install ID | Lander.initialize (POST /v1/sdk/init) |
| Open | Each time a link opens the app and the SDK resolves it | The resolve call (POST /v1/sdk/resolve) |
| Handoff open | A successful claimHandoff | The claim call |
Do not track these yourself, or they will be counted twice.
Attribute events to a link
linkId is only known while you have the LanderLink. Pass it along to the screen (go_router's extra works well) and use it there:
class ProductScreen extends StatelessWidget {
const ProductScreen({super.key, required this.id, this.link});
final String id;
final LanderLink? link;
Future<void> _buy(BuildContext context) async {
await Lander.trackEvent(
'purchase',
properties: {'productId': id, 'amount': 49.99, 'currency': 'USD'},
// Attribute the purchase to the link that opened this screen, if any.
linkId: link?.linkId,
);
}
// build() ...
}If the conversion happens later (a sign up a few screens or days after the link), keep the linkId yourself, for example in your app state or local storage, and pass it when the event happens. Unresolved links have no linkId; passing null simply records the event without a link.
How the queue works
Events go to a queue on the device first, then to POST /v1/sdk/events in batches:
- Batched. A flush runs about 2 seconds after an event (so bursts go out together), when the app goes to the background, and on the next launch. Each request carries up to 100 events.
- Survives offline and restarts. The queue is stored on the device, outside backups. Up to 1000 events are kept; beyond that the oldest are dropped.
- Waits for the install. Lander rejects events from installs it does not know, so events wait until the first launch report has succeeded, then go out.
- Retries with backoff. A failed batch is retried after 1 second, doubling up to 5 minutes, with jitter. A longer
Retry-Afterfrom the API is honored. - Counted once. Every event carries a client generated UUID, so a batch sent twice (for example after a lost response) is deduplicated by Lander.
- Never stuck. A batch the API rejects as invalid is dropped so it cannot block the queue. An auth error (
401or403) pauses sending until the next launch and keeps the events.
Each event is stored with the time it happened (occurredAt, UTC), so a purchase made offline on Monday and sent on Tuesday is still dated Monday.
Errors
trackEvent throws an ArgumentError for programming mistakes, right away:
nameis empty or longer than 100 characters.linkIdis not a UUID.propertiescontains something that cannot be encoded as JSON (aDateTime, a custom class). Convert those first, for exampledate.toIso8601String().
It never throws for network or server errors; the queue deals with those. It throws a StateError if called before Lander.initialize.
See your events
Open Live events in the dashboard to watch app events arrive as you test. It refreshes every 5 seconds.