Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions contents/docs/workflows/push-notifications/android.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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:

Expand All @@ -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

Expand Down
10 changes: 6 additions & 4 deletions contents/docs/workflows/push-notifications/flutter.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ 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.

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.

<CalloutBox icon="IconWarning" title="iOS needs a notification delegate" type="caution">

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.
Expand Down Expand Up @@ -85,7 +87,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(
Expand All @@ -95,9 +97,9 @@ 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 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. 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

Expand Down
7 changes: 5 additions & 2 deletions contents/docs/workflows/push-notifications/ios.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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, so no push events are captured. Everything else the SDK captures is unaffected.

## Automatic registration and open tracking (default)

Expand Down Expand Up @@ -97,7 +98,9 @@ There's also a fully manual variant if you don't have a `UNNotificationResponse`
PostHogSDK.shared.capturePushNotificationOpened(title: "...", body: "...", payload: [:], action: "...")
```

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.
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.

## Identity verification

Expand All @@ -116,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`, 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 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). |
77 changes: 61 additions & 16 deletions contents/docs/workflows/push-notifications/react-native.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.

<CalloutBox icon="IconWarning" title="iOS needs a notification delegate" type="caution">

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.

</CalloutBox>

The Android startup fetch doesn't see later token refreshes, so forward those yourself to keep the registered token current:

Expand All @@ -44,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.74.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:
Expand All @@ -70,33 +107,40 @@ 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:
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
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.
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. 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`.

## Opting out

Set `capturePushNotificationSubscriptions: false` or `capturePushNotificationOpened: false` in the initialization options.

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
{
"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`:
Expand All @@ -122,4 +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.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). |
Loading