Docs

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

EventWhenSent by
InstallThe first launch report of a new install IDLander.initialize (POST /v1/sdk/init)
OpenEach time a link opens the app and the SDK resolves itThe resolve call (POST /v1/sdk/resolve)
Handoff openA successful claimHandoffThe claim call

Do not track these yourself, or they will be counted twice.

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:

lib/product_screen.dart
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-After from 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 (401 or 403) 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:

  • name is empty or longer than 100 characters.
  • linkId is not a UUID.
  • properties contains something that cannot be encoded as JSON (a DateTime, a custom class). Convert those first, for example date.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.

Next steps

On this page