From d7b1b4535754a33eccc8a0ea8ce87d5289a77168 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Tue, 1 Sep 2026 20:52:50 -0400 Subject: [PATCH 1/5] fix(push): capture $push_notification_opened on Android cold start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugin registration runs inside Activity.onCreate, but the native SDK reads the tray intent in onActivityCreated — which the framework has already dispatched by then. onStart and onResume have also run before Dart reaches setup(), so no lifecycle callback for the launch Activity is ever observed. Hands the Activity's intent to PostHogAndroid.capturePushNotificationOpened from both setup() and onAttachedToActivity: neither hook alone covers both configurations, since onAttachedToEngine precedes onAttachedToActivity. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd --- .changeset/fix-android-cold-start-push-open.md | 5 +++++ posthog_flutter/android/build.gradle | 2 +- .../posthog/flutter/PosthogFlutterPlugin.kt | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-android-cold-start-push-open.md diff --git a/.changeset/fix-android-cold-start-push-open.md b/.changeset/fix-android-cold-start-push-open.md new file mode 100644 index 00000000..6ffe2ad1 --- /dev/null +++ b/.changeset/fix-android-cold-start-push-open.md @@ -0,0 +1,5 @@ +--- +'posthog_flutter': patch +--- + +Fix `$push_notification_opened` not being captured on Android when the app is cold-launched from a notification tap. Requires posthog-android 3.62.0. diff --git a/posthog_flutter/android/build.gradle b/posthog_flutter/android/build.gradle index ca79394b..fc8a638c 100644 --- a/posthog_flutter/android/build.gradle +++ b/posthog_flutter/android/build.gradle @@ -64,7 +64,7 @@ android { dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.mockito:mockito-core:5.0.0' - implementation 'com.posthog:posthog-android:[3.61.0,4.0.0)' + implementation 'com.posthog:posthog-android:[3.62.0,4.0.0)' } testOptions { diff --git a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt index b71f8609..37876cf5 100644 --- a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt +++ b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt @@ -791,11 +791,29 @@ class PosthogFlutterPlugin : PostHogAndroid.setup(applicationContext, config) postHogConfig = config cachedReplayIntegration = null + capturePushNotificationOpenedFromLaunchIntent() + } + + /** + * The SDK reads a notification tap from the launch Activity's intent when that Activity is + * created, which is long before Dart reaches `Posthog().setup()` — by then `onCreate`, `onStart` + * and `onResume` have all run. The intent is still on the Activity, so hand it over once both the + * SDK and the Activity exist. + * + * Called from both ends because neither alone covers both configurations: `onAttachedToEngine` + * (which runs `initPlugin`) always precedes `onAttachedToActivity`, so the AUTO_INIT path has no + * Activity yet, while on the Dart path the Activity is attached long before setup runs. Whichever + * precondition is satisfied last does the work; `PostHogAndroid` dedupes by message id, so a + * double call cannot double-count. + */ + private fun capturePushNotificationOpenedFromLaunchIntent() { + PostHogAndroid.capturePushNotificationOpened(activity?.intent) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity application = binding.activity.application + capturePushNotificationOpenedFromLaunchIntent() // Only if the detector is already running; else the setup path registers // it. Keeps a default-off feature from installing app-wide callbacks. if (occlusionDetectorRunning) { From 3b312ec06512356bcddd589c2be69e0c027b97a7 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 2 Sep 2026 10:18:58 -0400 Subject: [PATCH 2/5] fix(push): capture Android notification taps while the app is running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tap on a live process is delivered to Activity.onNewIntent, which ActivityLifecycleCallbacks does not expose — so posthog-android cannot observe it and this plugin is the only layer that can. Registers a NewIntentListener on attach and re-attach, removed on both detach paths. The native message-id dedupe keeps a redelivered intent from counting twice. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd --- .changeset/android-warm-start-push-open.md | 5 ++++ .../posthog/flutter/PosthogFlutterPlugin.kt | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .changeset/android-warm-start-push-open.md diff --git a/.changeset/android-warm-start-push-open.md b/.changeset/android-warm-start-push-open.md new file mode 100644 index 00000000..31f5a2ba --- /dev/null +++ b/.changeset/android-warm-start-push-open.md @@ -0,0 +1,5 @@ +--- +'posthog_flutter': patch +--- + +Capture `$push_notification_opened` on Android when a notification is tapped while the app is already running. diff --git a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt index 37876cf5..c7c16edd 100644 --- a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt +++ b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt @@ -38,6 +38,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import java.util.Date @@ -67,6 +68,7 @@ class PosthogFlutterPlugin : private lateinit var applicationContext: Context private var activity: Activity? = null + private var activityBinding: ActivityPluginBinding? = null private var application: Application? = null private var postHogConfig: PostHogAndroidConfig? = null @@ -810,9 +812,22 @@ class PosthogFlutterPlugin : PostHogAndroid.capturePushNotificationOpened(activity?.intent) } + /** + * A tap that arrives while the process is alive is delivered to `Activity.onNewIntent`, which + * `ActivityLifecycleCallbacks` does not expose — so the native SDK cannot see it and this plugin + * is the only layer that can. Returning false leaves the intent for other listeners. + */ + private val newIntentListener = + PluginRegistry.NewIntentListener { intent -> + PostHogAndroid.capturePushNotificationOpened(intent) + false + } + override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity application = binding.activity.application + activityBinding = binding + binding.addOnNewIntentListener(newIntentListener) capturePushNotificationOpenedFromLaunchIntent() // Only if the detector is already running; else the setup path registers // it. Keeps a default-off feature from installing app-wide callbacks. @@ -823,12 +838,15 @@ class PosthogFlutterPlugin : override fun onDetachedFromActivityForConfigChanges() { unregisterLifecycleTracking() + removeNewIntentListener() activity = null } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { activity = binding.activity application = binding.activity.application + activityBinding = binding + binding.addOnNewIntentListener(newIntentListener) if (occlusionDetectorRunning) { registerLifecycleTracking() } @@ -836,9 +854,15 @@ class PosthogFlutterPlugin : override fun onDetachedFromActivity() { unregisterLifecycleTracking() + removeNewIntentListener() activity = null } + private fun removeNewIntentListener() { + activityBinding?.removeOnNewIntentListener(newIntentListener) + activityBinding = null + } + // Idempotent: registering the same callbacks twice makes them fire twice. private fun registerLifecycleTracking() { val app = application ?: return From 9026f1b5dc43946115851966f8b96350a584785e Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 2 Sep 2026 10:40:39 -0400 Subject: [PATCH 3/5] fix(push): order the PluginRegistry import, merge the Android changesets ktlint requires lexicographic import order; the new import broke it. The cold- and warm-start fixes ship together, so they read as one changelog entry rather than two adjacent bullets about the same event. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd --- .changeset/android-warm-start-push-open.md | 5 ----- .changeset/fix-android-cold-start-push-open.md | 2 +- .../main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 .changeset/android-warm-start-push-open.md diff --git a/.changeset/android-warm-start-push-open.md b/.changeset/android-warm-start-push-open.md deleted file mode 100644 index 31f5a2ba..00000000 --- a/.changeset/android-warm-start-push-open.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'posthog_flutter': patch ---- - -Capture `$push_notification_opened` on Android when a notification is tapped while the app is already running. diff --git a/.changeset/fix-android-cold-start-push-open.md b/.changeset/fix-android-cold-start-push-open.md index 6ffe2ad1..95f8775f 100644 --- a/.changeset/fix-android-cold-start-push-open.md +++ b/.changeset/fix-android-cold-start-push-open.md @@ -2,4 +2,4 @@ 'posthog_flutter': patch --- -Fix `$push_notification_opened` not being captured on Android when the app is cold-launched from a notification tap. Requires posthog-android 3.62.0. +Fix `$push_notification_opened` not being captured on Android, both on a cold launch from a notification tap and on a tap while the app is already running. Requires posthog-android 3.62.0. diff --git a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt index c7c16edd..862fdcf3 100644 --- a/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt +++ b/posthog_flutter/android/src/main/kotlin/com/posthog/flutter/PosthogFlutterPlugin.kt @@ -38,9 +38,9 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry import java.util.Date import java.util.concurrent.Executors import java.util.concurrent.RejectedExecutionException From 4f8f859061b83b4d839131a1825d3a06ac85bb40 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 2 Sep 2026 11:22:30 -0400 Subject: [PATCH 4/5] docs(push): stop telling users to capture Android warm taps themselves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin now captures them, and the manual API is not deduplicated against it — so an app following the documented onMessageOpenedApp pattern would count every warm tap twice. Bumped to minor: existing integrations must remove that call. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd --- .../fix-android-cold-start-push-open.md | 4 +-- posthog_flutter/lib/src/posthog.dart | 25 ++++++++++--------- posthog_flutter/lib/src/posthog_config.dart | 13 +++++----- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.changeset/fix-android-cold-start-push-open.md b/.changeset/fix-android-cold-start-push-open.md index 95f8775f..d419b264 100644 --- a/.changeset/fix-android-cold-start-push-open.md +++ b/.changeset/fix-android-cold-start-push-open.md @@ -1,5 +1,5 @@ --- -'posthog_flutter': patch +'posthog_flutter': minor --- -Fix `$push_notification_opened` not being captured on Android, both on a cold launch from a notification tap and on a tap while the app is already running. Requires posthog-android 3.62.0. +Capture `$push_notification_opened` on Android for every notification tap, both on a cold launch and while the app is already running. Remove any manual `capturePushNotificationOpened` call wired to `FirebaseMessaging.onMessageOpenedApp` — that tap is now captured automatically and the manual call is not deduplicated against it. Requires posthog-android 3.62.0. diff --git a/posthog_flutter/lib/src/posthog.dart b/posthog_flutter/lib/src/posthog.dart index 305a7815..60a2322b 100644 --- a/posthog_flutter/lib/src/posthog.dart +++ b/posthog_flutter/lib/src/posthog.dart @@ -651,20 +651,21 @@ class Posthog { /// notification. /// /// Call this only for opens [PostHogConfig.capturePushNotificationOpened] - /// cannot see itself — local notifications on either platform, plus - /// warm-start and foreground taps on Android — or the tap is counted twice. - /// That doc has the full coverage matrix. + /// cannot see itself — local notifications on either platform, notifications + /// you display yourself from a foreground message, and push delivered outside + /// FCM on Android. That doc has the full coverage matrix. + /// + /// Do not wire this to `FirebaseMessaging.onMessageOpenedApp`: the SDK already + /// captures that tap, and this call is not deduplicated against it, so the open + /// would be counted twice. /// /// ```dart - /// if (Platform.isAndroid) { - /// FirebaseMessaging.onMessageOpenedApp.listen((m) { - /// Posthog().capturePushNotificationOpened( - /// title: m.notification?.title, - /// body: m.notification?.body, - /// payload: m.data, - /// ); - /// }); - /// } + /// // A notification you built and displayed yourself. + /// Posthog().capturePushNotificationOpened( + /// title: notification.title, + /// body: notification.body, + /// payload: notification.payload, + /// ); /// ``` /// /// The event is built natively, so [PostHogConfig.beforeSend] callbacks do diff --git a/posthog_flutter/lib/src/posthog_config.dart b/posthog_flutter/lib/src/posthog_config.dart index 195de8d4..528ef2cb 100644 --- a/posthog_flutter/lib/src/posthog_config.dart +++ b/posthog_flutter/lib/src/posthog_config.dart @@ -242,13 +242,12 @@ class PostHogConfig { /// Whether to automatically capture `$push_notification_opened` when a user /// taps a PostHog-delivered notification. /// - /// Coverage differs per platform. iOS hooks the notification-response - /// delegate, so every tap on a **remote** notification is captured whatever - /// the app state; locally-scheduled notifications are ignored. Android only - /// reads the launch intent, so it sees cold starts alone. Call - /// [Posthog.capturePushNotificationOpened] for the opens this misses — - /// local notifications on either platform, plus foreground messages and - /// warm-start taps on Android. + /// Every tap on a **remote** notification is captured on both platforms, + /// whether it cold-launched the app or the app was already running. + /// Locally-scheduled notifications are ignored. On Android a tap is + /// recognised by the `google.message_id` extra Firebase puts on the intent, + /// so push delivered outside FCM is not seen. Call + /// [Posthog.capturePushNotificationOpened] for the opens this misses. /// /// **Flutter web:** not supported. Defaults to `true`. bool capturePushNotificationOpened = true; From 7d4a163f24f92781d3f330bcadb92f8e6b7ea9dd Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 2 Sep 2026 11:46:41 -0400 Subject: [PATCH 5/5] docs(push): make the manual-capture example runnable, name getInitialMessage The snippet referenced an undeclared `notification` whose `payload` would not type-check. getInitialMessage is the terminated-state companion to onMessageOpenedApp and double-counts the same way. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd --- .changeset/fix-android-cold-start-push-open.md | 2 +- posthog_flutter/lib/src/posthog.dart | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.changeset/fix-android-cold-start-push-open.md b/.changeset/fix-android-cold-start-push-open.md index d419b264..7f41fc62 100644 --- a/.changeset/fix-android-cold-start-push-open.md +++ b/.changeset/fix-android-cold-start-push-open.md @@ -2,4 +2,4 @@ 'posthog_flutter': minor --- -Capture `$push_notification_opened` on Android for every notification tap, both on a cold launch and while the app is already running. Remove any manual `capturePushNotificationOpened` call wired to `FirebaseMessaging.onMessageOpenedApp` — that tap is now captured automatically and the manual call is not deduplicated against it. Requires posthog-android 3.62.0. +Capture `$push_notification_opened` on Android for every notification tap, both on a cold launch and while the app is already running. Remove any manual `capturePushNotificationOpened` call wired to `FirebaseMessaging.onMessageOpenedApp` or `getInitialMessage()` — that tap is now captured automatically and the manual call is not deduplicated against it. Requires posthog-android 3.62.0. diff --git a/posthog_flutter/lib/src/posthog.dart b/posthog_flutter/lib/src/posthog.dart index 60a2322b..ccb675d1 100644 --- a/posthog_flutter/lib/src/posthog.dart +++ b/posthog_flutter/lib/src/posthog.dart @@ -655,16 +655,16 @@ class Posthog { /// you display yourself from a foreground message, and push delivered outside /// FCM on Android. That doc has the full coverage matrix. /// - /// Do not wire this to `FirebaseMessaging.onMessageOpenedApp`: the SDK already - /// captures that tap, and this call is not deduplicated against it, so the open - /// would be counted twice. + /// Do not wire this to `FirebaseMessaging.onMessageOpenedApp` or + /// `getInitialMessage()`: the SDK already captures those taps, and this call is + /// not deduplicated against them, so the open would be counted twice. /// /// ```dart /// // A notification you built and displayed yourself. /// Posthog().capturePushNotificationOpened( - /// title: notification.title, - /// body: notification.body, - /// payload: notification.payload, + /// title: 'Your order shipped', + /// body: 'Track it in the app', + /// payload: {'order_id': '1234'}, /// ); /// ``` ///