From d52eec78d63045d0d538aeabf7166bb853370072 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Sat, 12 Sep 2026 12:06:07 -0400 Subject: [PATCH 1/6] docs(push): correct push open claims that are wrong today - React Native: iOS taps that cold-launch the app are not captured, and automatic capture needs iOS 14 or newer - React Native: Android captures warm-start taps since plugin 2.6.0, except when the process was killed with its task still in the recent apps list - React Native: promote the notification-delegate requirement to a callout, and add the missing $push_notification_opened troubleshooting row - iOS and Flutter: state the iOS 14 floor for the automatic hooks - Flutter: name the plugin version instead of native floors the plugin no longer declares --- .../workflows/push-notifications/flutter.mdx | 4 +-- .../docs/workflows/push-notifications/ios.mdx | 3 +- .../push-notifications/react-native.mdx | 32 +++++++++++-------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/contents/docs/workflows/push-notifications/flutter.mdx b/contents/docs/workflows/push-notifications/flutter.mdx index dc36ee24a793..7cd932554159 100644 --- a/contents/docs/workflows/push-notifications/flutter.mdx +++ b/contents/docs/workflows/push-notifications/flutter.mdx @@ -25,7 +25,7 @@ await Posthog().setup(config); On iOS the native SDK hooks the app delegate's remote-notification registration callback, so it picks up the APNs token once your app registers for remote notifications. On Android it fetches the FCM token at startup when `firebase-messaging` is on the classpath. The token is registered under the current distinct ID, so it follows the user across `identify()`. -Cold-start capture on both platforms, and warm-start capture on Android, arrive with the Flutter SDK release that raises the matching native floor: **posthog-ios 3.72.0** for the iOS half, **posthog-android 3.62.0** for the Android half. The two halves can ship in separate releases, so if only one platform is capturing, check which floors your plugin version declares. Once a platform's floor is in place, every tap on a remote notification is captured there, whether the notification cold-launched the app or it was already running. +Every tap on a remote notification is captured on both platforms, whether the notification cold-launched the app or it was already running. Requires the Flutter SDK 5.40.0 or newer, which is where both halves landed, and iOS 14 or newer on the iOS side – below that the native SDK installs no push hooks at all. Locally-scheduled notifications are ignored. Capture those manually (below). On Android a tap is recognized by the `google.message_id` extra that Firebase puts on the intent, so push delivered outside FCM isn't seen. @@ -85,7 +85,7 @@ Registration and unregistration are durable. If the device is offline or the req ## Capturing opens -Taps on remote notifications are captured for you, once your plugin version carries the native floor for that platform (see above). Automatic capture can't see locally-scheduled notifications, notifications you display yourself from a foreground message, or push delivered outside FCM on Android. Call the manual API only for those: +Taps on remote notifications are captured for you on the Flutter SDK 5.40.0 and newer. Automatic capture can't see locally-scheduled notifications, notifications you display yourself from a foreground message, or push delivered outside FCM on Android. Call the manual API only for those: ```dart Posthog().capturePushNotificationOpened( diff --git a/contents/docs/workflows/push-notifications/ios.mdx b/contents/docs/workflows/push-notifications/ios.mdx index f91f138beeba..c1545e1c3fc8 100644 --- a/contents/docs/workflows/push-notifications/ios.mdx +++ b/contents/docs/workflows/push-notifications/ios.mdx @@ -11,6 +11,7 @@ Available in the iOS SDK version 3.69.0 and newer. - Enable the **Push Notifications** capability for your app and register for remote notifications (`registerForRemoteNotifications()`), which requires requesting notification permission from the user. - A connected **FCM** or **APNs** channel in PostHog whose Firebase project / APNs topic (bundle id) matches your app. +- **iOS 14 or newer** for the automatic hooks. On iOS 13 the SDK installs neither the registration nor the open-capture hook, and captures nothing. ## Automatic registration and open tracking (default) @@ -116,5 +117,5 @@ config.pushIdentityProvider = { distinctId, appId, completion in | --- | --- | | Token never registers | Confirm you call `registerForRemoteNotifications()` and the user granted notification permission. If you set `config.enableSwizzling = false`, automatic registration and open capture are off. Use the manual APIs. | | Push doesn't arrive | Confirm the channel's APNs environment (Production/Sandbox) matches your build, and the bundle id matches. | -| Opens are never captured | Confirm your app sets `UNUserNotificationCenter.current().delegate`, and that `config.enableSwizzling` is `true`. In a cross-platform host that configures PostHog from Dart or JavaScript, call `PostHogSDK.prewarmPushNotificationOpenCapture()` (iOS SDK 3.72.0 and newer) from `application(_:didFinishLaunchingWithOptions:)` so a cold-start tap isn't lost before `setup()` runs. The PostHog Flutter plugin already does this for you. | +| Opens are never captured | Confirm your app sets `UNUserNotificationCenter.current().delegate`, that `config.enableSwizzling` is `true`, and that the device runs iOS 14 or newer. In a cross-platform host that configures PostHog from Dart or JavaScript, call `PostHogSDK.prewarmPushNotificationOpenCapture()` (iOS SDK 3.72.0 and newer) from `application(_:didFinishLaunchingWithOptions:)` so a cold-start tap isn't lost before `setup()` runs. The PostHog Flutter plugin already does this for you. | | Registration rejected on a Required channel | Your `pushIdentityProvider` isn't returning a valid token in time. See [Identity verification](/docs/workflows/push-notifications#identity-verification). | diff --git a/contents/docs/workflows/push-notifications/react-native.mdx b/contents/docs/workflows/push-notifications/react-native.mdx index 0074f6ec31fa..305a32b547b2 100644 --- a/contents/docs/workflows/push-notifications/react-native.mdx +++ b/contents/docs/workflows/push-notifications/react-native.mdx @@ -31,7 +31,17 @@ Both behaviors are on by default. An app that already has push configured and th On iOS the native SDK hooks the app delegate's remote-notification registration callback, so it picks up the APNs token once your app registers for remote notifications. On Android it fetches the FCM token at startup when Firebase Messaging is on the classpath (`@react-native-firebase/messaging` sets this up). The token is registered under the current distinct ID, so it follows the user across `identify()`. -Open coverage differs per platform. On iOS every tap on a remote notification is captured whatever the app state, provided your app sets `UNUserNotificationCenter.current().delegate`. Without one, iOS reports the tap to nobody. Locally-scheduled notifications are ignored. On Android only cold-start taps are captured. Capture the rest manually (below). +Open coverage differs per platform. + +On iOS a tap is captured while the app is running or in the background, and requires iOS 14 or newer – below that the native SDK installs no push hooks at all. A tap that cold-launches the app isn't captured: the native hook goes in when the SDK sets up from JavaScript, which is after iOS has already reported the tap. Locally-scheduled notifications are ignored. + +On Android every tap on a notification from the system tray is captured, whether the notification cold-launched the app or it was already running, with no code in your activity. Warm-start capture requires `@posthog/react-native-plugin` 2.6.0 or newer; older versions only capture cold-start taps. A tap is recognized by the `google.message_id` extra that Firebase puts on the intent, so push delivered outside FCM isn't seen. One state is still missed: when the app's process was killed but its task stayed in the recent apps list, React Native drops the tap's intent before any library in the process can see it. + + + +iOS only reports a notification tap to your app through `UNUserNotificationCenter.current().delegate`. If nothing in your app sets one, the SDK has nothing to observe and `$push_notification_opened` is never captured on iOS, in any app state. + + The Android startup fetch doesn't see later token refreshes, so forward those yourself to keep the registered token current: @@ -70,21 +80,14 @@ Registration and unregistration are durable. If the device is offline or the req ## Capturing opens -For the opens automatic capture misses (locally-scheduled notifications on either platform, plus warm-start taps and foreground messages on Android), call the manual API: +For the opens automatic capture misses (locally-scheduled notifications on either platform, plus foreground messages and push delivered outside FCM on Android), call the manual API: ```react-native -import messaging from '@react-native-firebase/messaging' -import { Platform } from 'react-native' - -if (Platform.OS === 'android') { - messaging().onNotificationOpenedApp((message) => { - posthog.capturePushNotificationOpened({ - title: message.notification?.title, - body: message.notification?.body, - payload: message.data, - }) - }) -} +posthog.capturePushNotificationOpened({ + title: 'Your order shipped', + body: 'Track it in the app', + payload: { order_id: '1234' }, +}) ``` Only call it for opens automatic capture can't see itself, or the tap is counted twice. @@ -122,4 +125,5 @@ If your push channel requires [identity verification](/docs/workflows/push-notif | --- | --- | | Token never registers | Confirm `@posthog/react-native-plugin` is installed, push is set up in your app, and the user granted notification permission. On Android, confirm Firebase Messaging is on the classpath (`@react-native-firebase/messaging` sets this up). | | Push doesn't arrive | Confirm the channel's Firebase project (Android) or APNs environment and bundle id (iOS) match your app. | +| `$push_notification_opened` never fires | On iOS, confirm something in your app sets `UNUserNotificationCenter.current().delegate` and that the device runs iOS 14 or newer. A tap that cold-launches the app isn't captured on iOS. On Android, confirm the notification is sent through FCM (detection keys on the `google.message_id` intent extra), and that you're on `@posthog/react-native-plugin` 2.6.0 or newer for taps that arrive while the app is already running. | | Registration rejected on a Required channel | Your `pushIdentityProvider` isn't returning a valid token in time. See [Identity verification](/docs/workflows/push-notifications#identity-verification). | From bd5e6658fac095d7b028b01fd18d654c3cf17e2f Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Sat, 12 Sep 2026 12:19:38 -0400 Subject: [PATCH 2/6] docs(push): document the push open behavior the SDK releases add - Android, Flutter, iOS: a repeat of a PostHog-sent notification captured in the last 5 minutes is skipped, so one tap counts once - React Native: iOS captures a tap that cold-launches the app, and capturePushNotificationOpened: false no longer stops that launch hook - document the Info.plist key that does - React Native: document the MainActivity onNewIntent override for taps that arrive after the process was killed, via the Expo config plugin or by hand - React Native: replace the manual open guidance with the dedupe rule - Flutter: a tap that lands before setup() is replayed once setup completes - iOS: the React Native plugin now prewarms open capture too --- .../workflows/push-notifications/android.mdx | 2 +- .../workflows/push-notifications/flutter.mdx | 4 +- .../docs/workflows/push-notifications/ios.mdx | 4 +- .../push-notifications/react-native.mdx | 51 +++++++++++++++++-- 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/contents/docs/workflows/push-notifications/android.mdx b/contents/docs/workflows/push-notifications/android.mdx index 06b2a517ee6f..4f88af7d9f86 100644 --- a/contents/docs/workflows/push-notifications/android.mdx +++ b/contents/docs/workflows/push-notifications/android.mdx @@ -62,7 +62,7 @@ override fun onNewIntent(intent: Intent) { } ``` -Your launcher activity needs `android:launchMode="singleTop"`, or the system resumes the task instead of delivering the tap here. Requires Android SDK 3.62.0 or newer. `PostHogAndroid.capturePushNotificationOpened` is deduplicated against the automatic path by message ID, so it can't double-count. `PostHog.capturePushNotificationOpened` below isn't deduplicated. +Your launcher activity needs `android:launchMode="singleTop"`, or the system resumes the task instead of delivering the tap here. Requires Android SDK 3.62.0 or newer. `PostHogAndroid.capturePushNotificationOpened` is deduplicated against the automatic path by message ID, so it can't double-count. From Android SDK 3.65.0, `PostHog.capturePushNotificationOpened` below is deduplicated too: a repeat of a notification PostHog sent, captured in the last 5 minutes, is skipped, so one tap counts once whichever path reports it. Notifications PostHog didn't send are never deduplicated. Notifications you display yourself from a foreground data message, and push delivered outside FCM, aren't detected at all. Capture those with the fully manual API: diff --git a/contents/docs/workflows/push-notifications/flutter.mdx b/contents/docs/workflows/push-notifications/flutter.mdx index 7cd932554159..a69472538b32 100644 --- a/contents/docs/workflows/push-notifications/flutter.mdx +++ b/contents/docs/workflows/push-notifications/flutter.mdx @@ -29,6 +29,8 @@ Every tap on a remote notification is captured on both platforms, whether the no Locally-scheduled notifications are ignored. Capture those manually (below). On Android a tap is recognized by the `google.message_id` extra that Firebase puts on the intent, so push delivered outside FCM isn't seen. +On Android a tap that reaches the app before `Posthog().setup()` runs is held and captured once setup completes, so coverage doesn't depend on your FCM package refreshing the activity's intent. Requires the Flutter SDK 5.41.1 or newer. + iOS only reports a notification tap to your app through `UNUserNotificationCenter.current().delegate`. A stock Flutter app doesn't set one, and neither does `flutter_local_notifications`. Without it, `$push_notification_opened` is never captured on iOS, in any app state. @@ -95,7 +97,7 @@ Posthog().capturePushNotificationOpened( ); ``` -Don't wire this to `FirebaseMessaging.onMessageOpenedApp` or `getInitialMessage()`. The SDK already captures those taps, and the manual call isn't deduplicated against them, so the open is counted twice. +Don't wire this to `FirebaseMessaging.onMessageOpenedApp` or `getInitialMessage()`. The SDK already captures those taps. On Android, from the Flutter SDK 5.41.1, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes, and the same rule applies on iOS once your build resolves posthog-ios 3.75.0 or newer. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. Notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title or body. diff --git a/contents/docs/workflows/push-notifications/ios.mdx b/contents/docs/workflows/push-notifications/ios.mdx index c1545e1c3fc8..24c6561d03b9 100644 --- a/contents/docs/workflows/push-notifications/ios.mdx +++ b/contents/docs/workflows/push-notifications/ios.mdx @@ -98,6 +98,8 @@ There's also a fully manual variant if you don't have a `UNNotificationResponse` PostHogSDK.shared.capturePushNotificationOpened(title: "...", body: "...", payload: [:], action: "...") ``` +From iOS SDK 3.75.0, both calls are deduplicated against automatic capture: a repeat of a notification PostHog sent, captured in the last 5 minutes, is skipped, so one tap counts once whichever path reports it. Notifications PostHog didn't send are never deduplicated. + The `$push_notification_opened` event includes `$notification_title`, `$notification_subtitle`, and `$notification_body`, plus `$notification_action` for action-button taps. Notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title, subtitle, or body. ## Identity verification @@ -117,5 +119,5 @@ config.pushIdentityProvider = { distinctId, appId, completion in | --- | --- | | Token never registers | Confirm you call `registerForRemoteNotifications()` and the user granted notification permission. If you set `config.enableSwizzling = false`, automatic registration and open capture are off. Use the manual APIs. | | Push doesn't arrive | Confirm the channel's APNs environment (Production/Sandbox) matches your build, and the bundle id matches. | -| Opens are never captured | Confirm your app sets `UNUserNotificationCenter.current().delegate`, that `config.enableSwizzling` is `true`, and that the device runs iOS 14 or newer. In a cross-platform host that configures PostHog from Dart or JavaScript, call `PostHogSDK.prewarmPushNotificationOpenCapture()` (iOS SDK 3.72.0 and newer) from `application(_:didFinishLaunchingWithOptions:)` so a cold-start tap isn't lost before `setup()` runs. The PostHog Flutter plugin already does this for you. | +| Opens are never captured | Confirm your app sets `UNUserNotificationCenter.current().delegate`, that `config.enableSwizzling` is `true`, and that the device runs iOS 14 or newer. In a cross-platform host that configures PostHog from Dart or JavaScript, call `PostHogSDK.prewarmPushNotificationOpenCapture()` (iOS SDK 3.72.0 and newer) from `application(_:didFinishLaunchingWithOptions:)` so a cold-start tap isn't lost before `setup()` runs. The PostHog Flutter and React Native plugins already do this for you. | | Registration rejected on a Required channel | Your `pushIdentityProvider` isn't returning a valid token in time. See [Identity verification](/docs/workflows/push-notifications#identity-verification). | diff --git a/contents/docs/workflows/push-notifications/react-native.mdx b/contents/docs/workflows/push-notifications/react-native.mdx index 305a32b547b2..736508da3bfb 100644 --- a/contents/docs/workflows/push-notifications/react-native.mdx +++ b/contents/docs/workflows/push-notifications/react-native.mdx @@ -33,9 +33,9 @@ On iOS the native SDK hooks the app delegate's remote-notification registration Open coverage differs per platform. -On iOS a tap is captured while the app is running or in the background, and requires iOS 14 or newer – below that the native SDK installs no push hooks at all. A tap that cold-launches the app isn't captured: the native hook goes in when the SDK sets up from JavaScript, which is after iOS has already reported the tap. Locally-scheduled notifications are ignored. +On iOS every tap on a remote notification is captured, whether the notification cold-launched the app or it was already running. Cold-launch capture requires `@posthog/react-native-plugin` 2.8.2 or newer, which installs the native hook at launch instead of waiting for the SDK to set up from JavaScript – by then iOS has already reported the tap. Automatic capture requires iOS 14 or newer; below that the native SDK installs no push hooks at all. Locally-scheduled notifications are ignored. -On Android every tap on a notification from the system tray is captured, whether the notification cold-launched the app or it was already running, with no code in your activity. Warm-start capture requires `@posthog/react-native-plugin` 2.6.0 or newer; older versions only capture cold-start taps. A tap is recognized by the `google.message_id` extra that Firebase puts on the intent, so push delivered outside FCM isn't seen. One state is still missed: when the app's process was killed but its task stayed in the recent apps list, React Native drops the tap's intent before any library in the process can see it. +On Android every tap on a notification from the system tray is captured, whether the notification cold-launched the app or it was already running, with no code in your activity. Warm-start capture requires `@posthog/react-native-plugin` 2.6.0 or newer; older versions only capture cold-start taps. A tap is recognized by the `google.message_id` extra that Firebase puts on the intent, so push delivered outside FCM isn't seen. One state needs [an override in your `MainActivity`](#android-taps-after-the-process-is-killed): when the app's process was killed but its task stayed in the recent apps list, React Native drops the tap's intent before any library in the process can see it. @@ -54,6 +54,33 @@ if (Platform.OS === 'android') { } ``` +### Android taps after the process is killed + +When Android kills your app's process but leaves its task in the recent apps list, React Native drops the tap's intent and never updates `getIntent()`, so the tap is invisible to every library in the process – PostHog, `@react-native-firebase/messaging` and your deep links alike. Overriding `onNewIntent` in your `MainActivity` fixes all three. + +If you use the `posthog-react-native/expo` config plugin, `expo prebuild` writes that override for you from `posthog-react-native` 4.72.0. Opt out with `patchMainActivityNewIntent`: + +```json +{ + "expo": { + "plugins": [ + ["posthog-react-native/expo", { "patchMainActivityNewIntent": false }] + ] + } +} +``` + +A bare React Native app that never runs `expo prebuild` adds the override itself, in `MainActivity.kt`. `setIntent(intent)` has to come before the `super` call, or React Native has already swallowed the intent: + +```kotlin +override fun onNewIntent(intent: Intent) { + setIntent(intent) + super.onNewIntent(intent) +} +``` + +If your `MainActivity` already overrides `onNewIntent`, the config plugin leaves it alone and warns instead – add `setIntent(intent)` as the first statement of your own override. + ## Manual registration If you manage push tokens yourself, turn the automatic flags off and call the SDK directly: @@ -80,7 +107,7 @@ Registration and unregistration are durable. If the device is offline or the req ## Capturing opens -For the opens automatic capture misses (locally-scheduled notifications on either platform, plus foreground messages and push delivered outside FCM on Android), call the manual API: +Taps on remote notifications are captured for you (see above). Automatic capture can't see locally-scheduled notifications on either platform, notifications you display yourself from a foreground message, or push delivered outside FCM on Android. Call the manual API only for those: ```react-native posthog.capturePushNotificationOpened({ @@ -90,7 +117,7 @@ posthog.capturePushNotificationOpened({ }) ``` -Only call it for opens automatic capture can't see itself, or the tap is counted twice. +Don't wire this to `messaging().onNotificationOpenedApp` or `getInitialNotification()`. The SDK already captures those taps. On Android, from `@posthog/react-native-plugin` 2.8.2, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes, but on iOS, and for notifications PostHog didn't send, the open is counted twice. If you added an `onNotificationOpenedApp` handler for Android on an earlier plugin version, remove it when you upgrade. The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. Notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title or body. @@ -100,6 +127,20 @@ The event is built and sent by the native SDK, so your JS `before_send` never se Set `capturePushNotificationSubscriptions: false` or `capturePushNotificationOpened: false` in the initialization options. +On iOS from `@posthog/react-native-plugin` 2.8.2, `capturePushNotificationOpened: false` stops the event but not the hook that catches a tap cold-launching the app: the plugin installs that at launch, before any of your JavaScript runs, and only releases it when `setup()` sees the flag. To skip installing it at all, set `com.posthog.posthog.CAPTURE_PUSH_NOTIFICATION_OPENED` to `false` in `Info.plist`, or in `ios.infoPlist` on Expo: + +```json +{ + "expo": { + "ios": { + "infoPlist": { + "com.posthog.posthog.CAPTURE_PUSH_NOTIFICATION_OPENED": false + } + } + } +} +``` + ## Identity verification If your push channel requires [identity verification](/docs/workflows/push-notifications#identity-verification), supply a backend-minted token through `pushIdentityProvider`: @@ -125,5 +166,5 @@ If your push channel requires [identity verification](/docs/workflows/push-notif | --- | --- | | Token never registers | Confirm `@posthog/react-native-plugin` is installed, push is set up in your app, and the user granted notification permission. On Android, confirm Firebase Messaging is on the classpath (`@react-native-firebase/messaging` sets this up). | | Push doesn't arrive | Confirm the channel's Firebase project (Android) or APNs environment and bundle id (iOS) match your app. | -| `$push_notification_opened` never fires | On iOS, confirm something in your app sets `UNUserNotificationCenter.current().delegate` and that the device runs iOS 14 or newer. A tap that cold-launches the app isn't captured on iOS. On Android, confirm the notification is sent through FCM (detection keys on the `google.message_id` intent extra), and that you're on `@posthog/react-native-plugin` 2.6.0 or newer for taps that arrive while the app is already running. | +| `$push_notification_opened` never fires | On iOS, confirm something in your app sets `UNUserNotificationCenter.current().delegate`, that the device runs iOS 14 or newer, and, for a tap that cold-launches the app, that you're on `@posthog/react-native-plugin` 2.8.2 or newer. On Android, confirm the notification is sent through FCM (detection keys on the `google.message_id` intent extra), that you're on `@posthog/react-native-plugin` 2.6.0 or newer for taps that arrive while the app is already running, and that your `MainActivity` [overrides `onNewIntent`](#android-taps-after-the-process-is-killed) for taps after the process was killed. | | Registration rejected on a Required channel | Your `pushIdentityProvider` isn't returning a valid token in time. See [Identity verification](/docs/workflows/push-notifications#identity-verification). | From a7c8426e78fb2d7a1fb12d7bd0d0e8a22422f7e3 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Sat, 12 Sep 2026 12:30:01 -0400 Subject: [PATCH 3/6] docs(push): the dedupe now reaches iOS on both plugins --- contents/docs/workflows/push-notifications/flutter.mdx | 2 +- contents/docs/workflows/push-notifications/react-native.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contents/docs/workflows/push-notifications/flutter.mdx b/contents/docs/workflows/push-notifications/flutter.mdx index a69472538b32..87546f6b99b6 100644 --- a/contents/docs/workflows/push-notifications/flutter.mdx +++ b/contents/docs/workflows/push-notifications/flutter.mdx @@ -97,7 +97,7 @@ Posthog().capturePushNotificationOpened( ); ``` -Don't wire this to `FirebaseMessaging.onMessageOpenedApp` or `getInitialMessage()`. The SDK already captures those taps. On Android, from the Flutter SDK 5.41.1, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes, and the same rule applies on iOS once your build resolves posthog-ios 3.75.0 or newer. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. +Don't wire this to `FirebaseMessaging.onMessageOpenedApp` or `getInitialMessage()`. The SDK already captures those taps. On Android and iOS, from the Flutter SDK 5.41.1, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. Notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title or body. diff --git a/contents/docs/workflows/push-notifications/react-native.mdx b/contents/docs/workflows/push-notifications/react-native.mdx index 736508da3bfb..77033458021c 100644 --- a/contents/docs/workflows/push-notifications/react-native.mdx +++ b/contents/docs/workflows/push-notifications/react-native.mdx @@ -117,7 +117,7 @@ posthog.capturePushNotificationOpened({ }) ``` -Don't wire this to `messaging().onNotificationOpenedApp` or `getInitialNotification()`. The SDK already captures those taps. On Android, from `@posthog/react-native-plugin` 2.8.2, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes, but on iOS, and for notifications PostHog didn't send, the open is counted twice. If you added an `onNotificationOpenedApp` handler for Android on an earlier plugin version, remove it when you upgrade. +Don't wire this to `messaging().onNotificationOpenedApp` or `getInitialNotification()`. The SDK already captures those taps. On Android and iOS, from `@posthog/react-native-plugin` 2.8.2, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. If you added an `onNotificationOpenedApp` handler for Android on an earlier plugin version, remove it when you upgrade. The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. Notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title or body. From f3dc971422b13103b16e24e19b7e7ec75ce0ba6f Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Sat, 12 Sep 2026 15:15:37 -0400 Subject: [PATCH 4/6] docs(push): say per platform what notification content is captured --- contents/docs/workflows/push-notifications/android.mdx | 2 +- contents/docs/workflows/push-notifications/flutter.mdx | 2 +- contents/docs/workflows/push-notifications/ios.mdx | 2 +- contents/docs/workflows/push-notifications/react-native.mdx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contents/docs/workflows/push-notifications/android.mdx b/contents/docs/workflows/push-notifications/android.mdx index 4f88af7d9f86..729562851b3d 100644 --- a/contents/docs/workflows/push-notifications/android.mdx +++ b/contents/docs/workflows/push-notifications/android.mdx @@ -74,7 +74,7 @@ PostHog.capturePushNotificationOpened( ) ``` -The `$push_notification_opened` event includes `$notification_title` and `$notification_body`, plus `$notification_action` for action-button taps. Notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title or body. +The `$push_notification_opened` event includes `$notification_title` and `$notification_body`, plus `$notification_action` for action-button taps. Android taps captured automatically carry no title or body – Firebase strips the notification content from the intent it hands the app, so there is nothing to read. Title and body are only set when you pass them to `PostHog.capturePushNotificationOpened` yourself. ## Identity verification diff --git a/contents/docs/workflows/push-notifications/flutter.mdx b/contents/docs/workflows/push-notifications/flutter.mdx index 87546f6b99b6..c5209641715a 100644 --- a/contents/docs/workflows/push-notifications/flutter.mdx +++ b/contents/docs/workflows/push-notifications/flutter.mdx @@ -99,7 +99,7 @@ Posthog().capturePushNotificationOpened( Don't wire this to `FirebaseMessaging.onMessageOpenedApp` or `getInitialMessage()`. The SDK already captures those taps. On Android and iOS, from the Flutter SDK 5.41.1, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. -The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. Notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title or body. +The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. On iOS, notification content is only captured for notifications sent by PostHog. On Android, taps captured automatically carry no title or body, because Firebase strips the content from the intent; those properties are only set when you pass them to `Posthog().capturePushNotificationOpened` yourself. ## Opting out diff --git a/contents/docs/workflows/push-notifications/ios.mdx b/contents/docs/workflows/push-notifications/ios.mdx index 24c6561d03b9..bb1857bc9189 100644 --- a/contents/docs/workflows/push-notifications/ios.mdx +++ b/contents/docs/workflows/push-notifications/ios.mdx @@ -100,7 +100,7 @@ PostHogSDK.shared.capturePushNotificationOpened(title: "...", body: "...", paylo From iOS SDK 3.75.0, both calls are deduplicated against automatic capture: a repeat of a notification PostHog sent, captured in the last 5 minutes, is skipped, so one tap counts once whichever path reports it. Notifications PostHog didn't send are never deduplicated. -The `$push_notification_opened` event includes `$notification_title`, `$notification_subtitle`, and `$notification_body`, plus `$notification_action` for action-button taps. Notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title, subtitle, or body. +The `$push_notification_opened` event includes `$notification_title`, `$notification_subtitle`, and `$notification_body`, plus `$notification_action` for action-button taps. On iOS, notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title, subtitle, or body. ## Identity verification diff --git a/contents/docs/workflows/push-notifications/react-native.mdx b/contents/docs/workflows/push-notifications/react-native.mdx index 77033458021c..fd97621c31e6 100644 --- a/contents/docs/workflows/push-notifications/react-native.mdx +++ b/contents/docs/workflows/push-notifications/react-native.mdx @@ -119,7 +119,7 @@ posthog.capturePushNotificationOpened({ Don't wire this to `messaging().onNotificationOpenedApp` or `getInitialNotification()`. The SDK already captures those taps. On Android and iOS, from `@posthog/react-native-plugin` 2.8.2, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. If you added an `onNotificationOpenedApp` handler for Android on an earlier plugin version, remove it when you upgrade. -The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. Notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title or body. +The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. On iOS, notification content is only captured for notifications sent by PostHog. On Android, taps captured automatically carry no title or body, because Firebase strips the content from the intent; those properties are only set when you pass them to `posthog.capturePushNotificationOpened` yourself. The event is built and sent by the native SDK, so your JS `before_send` never sees it. Redact anything sensitive before passing it to `capturePushNotificationOpened`. From 64ee021750d06070b387215a18453d44789aa4bf Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Tue, 15 Sep 2026 11:44:32 -0400 Subject: [PATCH 5/6] docs(push): correct the SDK versions to what actually shipped The version numbers were written before the releases and the trains went to other features in between: - React Native cold-launch capture and the dedupe are in @posthog/react-native-plugin 2.9.2 (#4921), not 2.8.2 - the Flutter pre-setup tap fix is in 5.42.1 (#579), not 5.41.1 - the Flutter dedupe is in 5.43.1 (#578), not 5.41.1 posthog-react-native's Expo override is left as TODO_4929_VERSION: PostHog/posthog-js#4929 has not merged, so no release carries it yet. --- contents/docs/workflows/push-notifications/flutter.mdx | 4 ++-- .../docs/workflows/push-notifications/react-native.mdx | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contents/docs/workflows/push-notifications/flutter.mdx b/contents/docs/workflows/push-notifications/flutter.mdx index c5209641715a..aa660dfcef0a 100644 --- a/contents/docs/workflows/push-notifications/flutter.mdx +++ b/contents/docs/workflows/push-notifications/flutter.mdx @@ -29,7 +29,7 @@ Every tap on a remote notification is captured on both platforms, whether the no Locally-scheduled notifications are ignored. Capture those manually (below). On Android a tap is recognized by the `google.message_id` extra that Firebase puts on the intent, so push delivered outside FCM isn't seen. -On Android a tap that reaches the app before `Posthog().setup()` runs is held and captured once setup completes, so coverage doesn't depend on your FCM package refreshing the activity's intent. Requires the Flutter SDK 5.41.1 or newer. +On Android a tap that reaches the app before `Posthog().setup()` runs is held and captured once setup completes, so coverage doesn't depend on your FCM package refreshing the activity's intent. Requires the Flutter SDK 5.42.1 or newer. @@ -97,7 +97,7 @@ Posthog().capturePushNotificationOpened( ); ``` -Don't wire this to `FirebaseMessaging.onMessageOpenedApp` or `getInitialMessage()`. The SDK already captures those taps. On Android and iOS, from the Flutter SDK 5.41.1, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. +Don't wire this to `FirebaseMessaging.onMessageOpenedApp` or `getInitialMessage()`. The SDK already captures those taps. On Android and iOS, from the Flutter SDK 5.43.1, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. On iOS, notification content is only captured for notifications sent by PostHog. On Android, taps captured automatically carry no title or body, because Firebase strips the content from the intent; those properties are only set when you pass them to `Posthog().capturePushNotificationOpened` yourself. diff --git a/contents/docs/workflows/push-notifications/react-native.mdx b/contents/docs/workflows/push-notifications/react-native.mdx index fd97621c31e6..09a079a40916 100644 --- a/contents/docs/workflows/push-notifications/react-native.mdx +++ b/contents/docs/workflows/push-notifications/react-native.mdx @@ -33,7 +33,7 @@ On iOS the native SDK hooks the app delegate's remote-notification registration Open coverage differs per platform. -On iOS every tap on a remote notification is captured, whether the notification cold-launched the app or it was already running. Cold-launch capture requires `@posthog/react-native-plugin` 2.8.2 or newer, which installs the native hook at launch instead of waiting for the SDK to set up from JavaScript – by then iOS has already reported the tap. Automatic capture requires iOS 14 or newer; below that the native SDK installs no push hooks at all. Locally-scheduled notifications are ignored. +On iOS every tap on a remote notification is captured, whether the notification cold-launched the app or it was already running. Cold-launch capture requires `@posthog/react-native-plugin` 2.9.2 or newer, which installs the native hook at launch instead of waiting for the SDK to set up from JavaScript – by then iOS has already reported the tap. Automatic capture requires iOS 14 or newer; below that the native SDK installs no push hooks at all. Locally-scheduled notifications are ignored. On Android every tap on a notification from the system tray is captured, whether the notification cold-launched the app or it was already running, with no code in your activity. Warm-start capture requires `@posthog/react-native-plugin` 2.6.0 or newer; older versions only capture cold-start taps. A tap is recognized by the `google.message_id` extra that Firebase puts on the intent, so push delivered outside FCM isn't seen. One state needs [an override in your `MainActivity`](#android-taps-after-the-process-is-killed): when the app's process was killed but its task stayed in the recent apps list, React Native drops the tap's intent before any library in the process can see it. @@ -58,7 +58,7 @@ if (Platform.OS === 'android') { When Android kills your app's process but leaves its task in the recent apps list, React Native drops the tap's intent and never updates `getIntent()`, so the tap is invisible to every library in the process – PostHog, `@react-native-firebase/messaging` and your deep links alike. Overriding `onNewIntent` in your `MainActivity` fixes all three. -If you use the `posthog-react-native/expo` config plugin, `expo prebuild` writes that override for you from `posthog-react-native` 4.72.0. Opt out with `patchMainActivityNewIntent`: +If you use the `posthog-react-native/expo` config plugin, `expo prebuild` writes that override for you from `posthog-react-native` TODO_4929_VERSION. Opt out with `patchMainActivityNewIntent`: ```json { @@ -117,7 +117,7 @@ posthog.capturePushNotificationOpened({ }) ``` -Don't wire this to `messaging().onNotificationOpenedApp` or `getInitialNotification()`. The SDK already captures those taps. On Android and iOS, from `@posthog/react-native-plugin` 2.8.2, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. If you added an `onNotificationOpenedApp` handler for Android on an earlier plugin version, remove it when you upgrade. +Don't wire this to `messaging().onNotificationOpenedApp` or `getInitialNotification()`. The SDK already captures those taps. On Android and iOS, from `@posthog/react-native-plugin` 2.9.2, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. If you added an `onNotificationOpenedApp` handler for Android on an earlier plugin version, remove it when you upgrade. The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. On iOS, notification content is only captured for notifications sent by PostHog. On Android, taps captured automatically carry no title or body, because Firebase strips the content from the intent; those properties are only set when you pass them to `posthog.capturePushNotificationOpened` yourself. @@ -127,7 +127,7 @@ The event is built and sent by the native SDK, so your JS `before_send` never se Set `capturePushNotificationSubscriptions: false` or `capturePushNotificationOpened: false` in the initialization options. -On iOS from `@posthog/react-native-plugin` 2.8.2, `capturePushNotificationOpened: false` stops the event but not the hook that catches a tap cold-launching the app: the plugin installs that at launch, before any of your JavaScript runs, and only releases it when `setup()` sees the flag. To skip installing it at all, set `com.posthog.posthog.CAPTURE_PUSH_NOTIFICATION_OPENED` to `false` in `Info.plist`, or in `ios.infoPlist` on Expo: +On iOS from `@posthog/react-native-plugin` 2.9.2, `capturePushNotificationOpened: false` stops the event but not the hook that catches a tap cold-launching the app: the plugin installs that at launch, before any of your JavaScript runs, and only releases it when `setup()` sees the flag. To skip installing it at all, set `com.posthog.posthog.CAPTURE_PUSH_NOTIFICATION_OPENED` to `false` in `Info.plist`, or in `ios.infoPlist` on Expo: ```json { @@ -166,5 +166,5 @@ If your push channel requires [identity verification](/docs/workflows/push-notif | --- | --- | | Token never registers | Confirm `@posthog/react-native-plugin` is installed, push is set up in your app, and the user granted notification permission. On Android, confirm Firebase Messaging is on the classpath (`@react-native-firebase/messaging` sets this up). | | Push doesn't arrive | Confirm the channel's Firebase project (Android) or APNs environment and bundle id (iOS) match your app. | -| `$push_notification_opened` never fires | On iOS, confirm something in your app sets `UNUserNotificationCenter.current().delegate`, that the device runs iOS 14 or newer, and, for a tap that cold-launches the app, that you're on `@posthog/react-native-plugin` 2.8.2 or newer. On Android, confirm the notification is sent through FCM (detection keys on the `google.message_id` intent extra), that you're on `@posthog/react-native-plugin` 2.6.0 or newer for taps that arrive while the app is already running, and that your `MainActivity` [overrides `onNewIntent`](#android-taps-after-the-process-is-killed) for taps after the process was killed. | +| `$push_notification_opened` never fires | On iOS, confirm something in your app sets `UNUserNotificationCenter.current().delegate`, that the device runs iOS 14 or newer, and, for a tap that cold-launches the app, that you're on `@posthog/react-native-plugin` 2.9.2 or newer. On Android, confirm the notification is sent through FCM (detection keys on the `google.message_id` intent extra), that you're on `@posthog/react-native-plugin` 2.6.0 or newer for taps that arrive while the app is already running, and that your `MainActivity` [overrides `onNewIntent`](#android-taps-after-the-process-is-killed) for taps after the process was killed. | | Registration rejected on a Required channel | Your `pushIdentityProvider` isn't returning a valid token in time. See [Identity verification](/docs/workflows/push-notifications#identity-verification). | From 6b393a4785ebba574769e68c075ab8f8c30eb5ec Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Tue, 15 Sep 2026 13:54:44 -0400 Subject: [PATCH 6/6] docs(push): correct two dedupe claims and scope the iOS 13 note - React Native: the manual/automatic dedupe shipped in plugin 2.9.1, not 2.9.2 (2.9.2 is the iOS cold-launch hook, which the other references cite correctly) - Android: the two dedupe mechanisms are not the same. The message-ID dedupe on the intent path applies to any FCM notification; only the invocation-id dedupe is PostHog-only. The paragraph claimed the second rule for both. - iOS: on iOS 13 only the push hooks are skipped, not all capture - iOS: note that the field-based call carries no notification identifier, so a genuine re-send inside the window reads as the same tap Names posthog-react-native 4.74.0 for the Expo override, which is the release that will carry PostHog/posthog-js#4929. Do not merge before that release. --- contents/docs/workflows/push-notifications/android.mdx | 2 +- contents/docs/workflows/push-notifications/ios.mdx | 4 ++-- contents/docs/workflows/push-notifications/react-native.mdx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contents/docs/workflows/push-notifications/android.mdx b/contents/docs/workflows/push-notifications/android.mdx index 729562851b3d..0df7a0f6c64c 100644 --- a/contents/docs/workflows/push-notifications/android.mdx +++ b/contents/docs/workflows/push-notifications/android.mdx @@ -62,7 +62,7 @@ override fun onNewIntent(intent: Intent) { } ``` -Your launcher activity needs `android:launchMode="singleTop"`, or the system resumes the task instead of delivering the tap here. Requires Android SDK 3.62.0 or newer. `PostHogAndroid.capturePushNotificationOpened` is deduplicated against the automatic path by message ID, so it can't double-count. From Android SDK 3.65.0, `PostHog.capturePushNotificationOpened` below is deduplicated too: a repeat of a notification PostHog sent, captured in the last 5 minutes, is skipped, so one tap counts once whichever path reports it. Notifications PostHog didn't send are never deduplicated. +Your launcher activity needs `android:launchMode="singleTop"`, or the system resumes the task instead of delivering the tap here. Requires Android SDK 3.62.0 or newer. `PostHogAndroid.capturePushNotificationOpened` is deduplicated against the automatic path by message ID, so it can't double-count. From Android SDK 3.65.0, `PostHog.capturePushNotificationOpened` below is deduplicated too: a repeat of a notification PostHog sent, captured in the last 5 minutes, is skipped, so one tap counts once whichever path reports it. That second dedupe only covers notifications PostHog sent; the message-ID dedupe above applies to any FCM notification. Notifications you display yourself from a foreground data message, and push delivered outside FCM, aren't detected at all. Capture those with the fully manual API: diff --git a/contents/docs/workflows/push-notifications/ios.mdx b/contents/docs/workflows/push-notifications/ios.mdx index bb1857bc9189..f8fe4a0a951c 100644 --- a/contents/docs/workflows/push-notifications/ios.mdx +++ b/contents/docs/workflows/push-notifications/ios.mdx @@ -11,7 +11,7 @@ Available in the iOS SDK version 3.69.0 and newer. - Enable the **Push Notifications** capability for your app and register for remote notifications (`registerForRemoteNotifications()`), which requires requesting notification permission from the user. - A connected **FCM** or **APNs** channel in PostHog whose Firebase project / APNs topic (bundle id) matches your app. -- **iOS 14 or newer** for the automatic hooks. On iOS 13 the SDK installs neither the registration nor the open-capture hook, and captures nothing. +- **iOS 14 or newer** for the automatic hooks. On iOS 13 the SDK installs neither the registration nor the open-capture hook, so no push events are captured. Everything else the SDK captures is unaffected. ## Automatic registration and open tracking (default) @@ -98,7 +98,7 @@ There's also a fully manual variant if you don't have a `UNNotificationResponse` PostHogSDK.shared.capturePushNotificationOpened(title: "...", body: "...", payload: [:], action: "...") ``` -From iOS SDK 3.75.0, both calls are deduplicated against automatic capture: a repeat of a notification PostHog sent, captured in the last 5 minutes, is skipped, so one tap counts once whichever path reports it. Notifications PostHog didn't send are never deduplicated. +From iOS SDK 3.75.0, both calls are deduplicated against automatic capture: a repeat of a notification PostHog sent, captured in the last 5 minutes, is skipped, so one tap counts once whichever path reports it. Notifications PostHog didn't send are never deduplicated. The field-based call carries no notification identifier, so a genuine re-send of the same workflow step inside those 5 minutes reads as the same tap and is skipped – report a re-send through `capturePushNotificationOpened(response:)`, which can tell the deliveries apart. The `$push_notification_opened` event includes `$notification_title`, `$notification_subtitle`, and `$notification_body`, plus `$notification_action` for action-button taps. On iOS, notification content is only captured for notifications sent by PostHog. Opens of other notifications are still captured, but without title, subtitle, or body. diff --git a/contents/docs/workflows/push-notifications/react-native.mdx b/contents/docs/workflows/push-notifications/react-native.mdx index 09a079a40916..6739a93c7b6d 100644 --- a/contents/docs/workflows/push-notifications/react-native.mdx +++ b/contents/docs/workflows/push-notifications/react-native.mdx @@ -58,7 +58,7 @@ if (Platform.OS === 'android') { When Android kills your app's process but leaves its task in the recent apps list, React Native drops the tap's intent and never updates `getIntent()`, so the tap is invisible to every library in the process – PostHog, `@react-native-firebase/messaging` and your deep links alike. Overriding `onNewIntent` in your `MainActivity` fixes all three. -If you use the `posthog-react-native/expo` config plugin, `expo prebuild` writes that override for you from `posthog-react-native` TODO_4929_VERSION. Opt out with `patchMainActivityNewIntent`: +If you use the `posthog-react-native/expo` config plugin, `expo prebuild` writes that override for you from `posthog-react-native` 4.74.0. Opt out with `patchMainActivityNewIntent`: ```json { @@ -117,7 +117,7 @@ posthog.capturePushNotificationOpened({ }) ``` -Don't wire this to `messaging().onNotificationOpenedApp` or `getInitialNotification()`. The SDK already captures those taps. On Android and iOS, from `@posthog/react-native-plugin` 2.9.2, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. If you added an `onNotificationOpenedApp` handler for Android on an earlier plugin version, remove it when you upgrade. +Don't wire this to `messaging().onNotificationOpenedApp` or `getInitialNotification()`. The SDK already captures those taps. On Android and iOS, from `@posthog/react-native-plugin` 2.9.1, a repeat of a notification PostHog sent is skipped when the same notification was captured in the last 5 minutes. Notifications PostHog didn't send are never deduplicated, so a handler you wire up yourself still counts those opens twice. If you added an `onNotificationOpenedApp` handler for Android on an earlier plugin version, remove it when you upgrade. The `$push_notification_opened` event includes `$notification_title` and `$notification_body` (plus `$notification_subtitle` on iOS), and `$notification_action` for action-button taps. On iOS, notification content is only captured for notifications sent by PostHog. On Android, taps captured automatically carry no title or body, because Firebase strips the content from the intent; those properties are only set when you pass them to `posthog.capturePushNotificationOpened` yourself.