Skip to content
Draft
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
15 changes: 14 additions & 1 deletion contents/docs/workflows/push-notifications/android.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,20 @@ Registration and unregistration are durable. If the device is offline or the req

## Capturing opens

Automatic open capture detects cold-start taps on a notification from the system tray. Warm-start taps (handled in `onNewIntent`) and notifications you display yourself from a foreground data message need the manual API:
Automatic open capture detects cold-start taps on a notification from the system tray, recognized by the `google.message_id` extra that Firebase puts on the intent.

Android gives libraries no way to observe `Activity.onNewIntent`, so a tap that arrives while your app is already running needs one line in your activity:

```kotlin
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
PostHogAndroid.capturePushNotificationOpened(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 with it — unlike `PostHog.capturePushNotificationOpened` below, which isn't.

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:

```kotlin
PostHog.capturePushNotificationOpened(
Expand Down
45 changes: 33 additions & 12 deletions contents/docs/workflows/push-notifications/flutter.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,31 @@ 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()`.

Open coverage differs per platform. On iOS every tap on a remote notification is captured whatever the app state; locally-scheduled notifications are ignored. On Android only cold-start taps are captured. Capture the rest manually (below).
Every tap on a remote notification is captured on both platforms, whether the notification cold-launched the app or it was already running. Locally-scheduled notifications are ignored — capture those manually (below).

Cold-start capture needs posthog-ios 3.72.0 on iOS and posthog-android 3.62.0 on Android, which the plugin's version floors bring in. 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.

<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 sets none, and `flutter_local_notifications` doesn't set one either — so without this, iOS reports the tap to nobody and `$push_notification_opened` is never captured, in any app state.

Set it in `ios/Runner/AppDelegate.swift`:

```swift
import UserNotifications

override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UNUserNotificationCenter.current().delegate = self
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
```

The SDK logs a warning when the delegate is still missing shortly after setup, if you set `debug = true` on `PostHogConfig`.

</CalloutBox>

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

Expand Down Expand Up @@ -61,21 +85,17 @@ 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. Call the manual API only for opens automatic capture can't see — locally-scheduled notifications, notifications you display yourself from a foreground message, and push delivered outside FCM on Android:

```dart
if (Platform.isAndroid) {
FirebaseMessaging.onMessageOpenedApp.listen((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 `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.

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.

Expand Down Expand Up @@ -104,4 +124,5 @@ config.pushIdentityProvider = (distinctId, appId) async {
| --- | --- |
| Token never registers | Confirm push is set up in your app and the user granted notification permission. On Android, confirm `firebase-messaging` is on the classpath (`firebase_messaging` sets this up). Any manual `registerPushNotificationToken` call must come after `setup()` completes. |
| 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 your `AppDelegate` sets `UNUserNotificationCenter.current().delegate`. On Android, confirm the notification is sent through FCM — detection keys on the `google.message_id` intent extra — and that your launcher activity still has `android:launchMode="singleTop"`, without which the system resumes the task instead of delivering the tap. |
| 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). |
23 changes: 23 additions & 0 deletions contents/docs/workflows/push-notifications/ios.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ PostHogSDK.shared.setup(config)

With these enabled you only need to register for remote notifications; the SDK picks up the token and open events automatically. The token is registered under the current distinct ID, so it follows the user across `identify()`. Locally-scheduled notifications are ignored. Capture those manually (below).

<CalloutBox icon="IconWarning" title="Your app must set a notification delegate" type="caution">

iOS reports a notification tap through `UNUserNotificationCenter.current().delegate`. If your app never sets one, the system reports the tap to nobody, there's nothing for the SDK to observe, and `$push_notification_opened` is never captured — whatever the app state.

```swift
import UserNotifications

class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
}
```

The SDK logs a warning a few seconds after `setup()` when the delegate is still missing, if you enable `config.debug`.

</CalloutBox>

Automatic registration and open capture require `config.enableSwizzling` to be `true` (the default). If you disable swizzling or manage your own delegates, use the manual APIs below.

## Manual registration
Expand Down Expand Up @@ -92,4 +114,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. |
| 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). |
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ 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; 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 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).

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

Expand Down
Loading