PostHog / posthog-flutter

PostHog Flutter SDK
https://posthog.com/docs/libraries/flutter
MIT License
48 stars 34 forks source link

Missing Events Immediately After Startup #85

Closed jakobhec closed 4 months ago

jakobhec commented 4 months ago

Version

4.0.1

Steps to Reproduce

Capture an event very early after the app starts up. For example, here we do it right after WidgetsFlutterBinding.ensureInitialized().

Future<void> main() async {
  WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
  Posthog().capture(eventName: 'dummy_event');
  ...

Expected Result

I would expect all events to show up on my dashboard, regardless of when I capture them in the app's lifecycle.

Actual Result

Events within the first second after the app starts up are not always captured. A rough estimate would be that 90% of the dummy_event in the example above are missed, only 10% appear on my dashboard.

I would expect 100% of the events to appear on my dashboard.

This works for subsequent events (e.g., when I log a button tap on the second screen).

marandaneto commented 4 months ago

@jakobhec can you enable the debug option and check the logs? I'd be interested to know if there's anything strange.

jakobhec commented 4 months ago

@marandaneto It now looks like this:

Future<void> main() async {
  WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
  await Posthog().debug(true);
  Posthog().capture(eventName: 'dummy_event');
  ...

But I cannot see any additional output in my terminal or debug console.

marandaneto commented 4 months ago

@jakobhec your test is Android or iOS?

jakobhec commented 4 months ago

@marandaneto iOS

marandaneto commented 4 months ago

Events are sent in batches, it only flushes when the queue is 20 (by default), so if you capture a few more events after, once it is 20, all events are flushed, including the dummy_event, maybe that's the issue you are seeing? like a delay. I just tested it and it's all working well on my sample. If that's not the case, can you provide an MRE?

marandaneto commented 4 months ago

@jakobhec did you check it?

jakobhec commented 4 months ago

@marandaneto Thanks for getting back! I would assume that there is another reason.

I use a single Dart function for logging to Firebase and PostHog, which seems reliable.

For instance, on March 5, 2024, PostHog recorded 26,585 for a specific event (button press later in the app), while Firebase had 26,712—only a very small difference which could also be because one of the dashboards takes my timezone into account and the other doesn't. Not sure, anyways, it works.

Yet, for the initial app open event "onboarding screen shown," there's a significant difference: On March 5, 2024, Firebase shows 174, but PostHog only 5 (!) events.

Function looks like this:

Future<void> logEvent({
    required String name,
    Map<String, dynamic>? parameters,
}) async {
    // Posthog().capture(...
    // FirebaseAnalytics.instance.logEvent(...
marandaneto commented 4 months ago

@jakobhec can you reproduce this issue? can you provide a MRE? I cannot reproduce myself sadly, everything works as expected.

jakobhec commented 4 months ago

@marandaneto If the app is opened, 15 events are triggered by a user, and then the app is closed, will PostHog send those events? Currently thinking about the MRE

marandaneto commented 4 months ago

@marandaneto If the app is opened, 15 events are triggered by a user, and then the app is closed, will PostHog send those events? Currently thinking about the MRE

Yes, by default the SDK will only send after it's accumulated at least 20 events or the app is idle for at least 30 seconds.

jakobhec commented 4 months ago

@marandaneto just to clarify: in the example there were less than 20 events and the app was not idle for at least 30 secondes. Shouldn't that mean that nothing will be sent?

Or does posthog also trigger on app close?

marandaneto commented 4 months ago

@marandaneto just to clarify: in the example there were less than 20 events and the app was not idle for at least 30 secondes. Shouldn't that mean that nothing will be sent?

Or does posthog also trigger on app close?

It's either at least 20 events or after 30 seconds the app is opened, not idle, sorry wrong wording.

jakobhec commented 4 months ago

@marandaneto thanks for clarifying.

So if a user opens an app for the first time, sees the onboarding flow, slides through the first 3 onboarding screens (triggering < 20 events in total) and then leaves the app after 15 seconds. Will Posthog send events?

marandaneto commented 4 months ago

@marandaneto thanks for clarifying.

So if a user opens an app for the first time, sees the onboarding flow, slides through the first 3 onboarding screens (triggering < 20 events in total) and then leaves the app after 15 seconds. Will Posthog send events?

yes, after 30 seconds if the app is open (it can be in the background), but not closed (forcefully killed from recent apps), events should have been sent.

jakobhec commented 4 months ago

@marandaneto As far as I understand this makes it impossible to use Posthog for onboarding A/B testing and onboarding event tracking. Because if users don't like the onboarding in the first 15 seconds, then they most likely kill the app and delete it.

I would really like to use Posthog for this.

Can I override this behavior? Or manually send off events before the user forcefully closes the app? For example by using this: https://api.flutter.dev/flutter/widgets/AppLifecycleListener-class.html.

marandaneto commented 4 months ago

Usually, people don't kill the app, either they uninstall it or just leave it in the background (in this case is fine). But you can call flush which forcefully sends all pending events, the flush is being added here https://github.com/PostHog/posthog-flutter/pull/92

jakobhec commented 4 months ago

@marandaneto flush is great! But even if only 10% kill the app, it would make a big difference for a/b testing.

I think that might explain a small part of the difference between firebase analytics (174 events) and Posthog (5 events). But I would assume that 100-165 events are still missing. I'll test some more and keep you informed

jakobhec commented 4 months ago

I am testing it like this now:

class _MyAppState extends State<MyApp> {
  late final AppLifecycleListener _listener;

  @override
  void initState() {
    super.initState();
    _listener = AppLifecycleListener(
        onPause: () => Posthog().flush(),
    );

To be clear: I think that the main issue that leads to missing events after startup is not resolved.

marandaneto commented 4 months ago

@jakobhec then I need an MRE, I cannot reproduce this.

jakobhec commented 3 months ago

If anyone else runs into this issue: delaying to capture initial events and manually calling flush() seems to work:

This is how I manually call flush():

// in the root StatefulWidget initState() method
_listener = AppLifecycleListener(
    onPause: () => Posthog().flush()
);

And I've created a function that delays the initial events while keeping correct timestamps:

final ts = DateTime.now().toIso8601String();

// I store a timestamp of the first app open in shared preferences.
// I use that to calculate the necessary delay to ensure that it is captures events at least 3 seconds after first app open.
// final msDelayToEnsureDelivery = ... 

Future.delayed(Duration(milliseconds: msDelayToEnsureDelivery)).then((_) {
  Posthog().capture(eventName: name, properties: {
    ...properties,
    '\$timestamp': ts,  // manually set the correct timestamp
  });
});

Would be very happy to hear alternate ideas :)