Skip to content

docs: correct push-notification open coverage for iOS, Android and Flutter - #19905

Open
turnipdabeets wants to merge 4 commits into
masterfrom
docs/push-open-coverage
Open

docs: correct push-notification open coverage for iOS, Android and Flutter#19905
turnipdabeets wants to merge 4 commits into
masterfrom
docs/push-open-coverage

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Changes

Corrects the push-notification open-tracking docs for iOS, Android and Flutter. Some of this is wrong today, independent of any SDK change.

iOS never captures a tap unless the app sets a UNUserNotificationCenter delegate. The page currently says the SDK "picks up the token and open events automatically". The token half is true; the open half is not — PostHog swizzles the app's delegate rather than installing its own, so with no delegate iOS reports the tap to nobody and $push_notification_opened is never captured, in any app state. This is the most likely reason a reader sees zero events, and it's what PostHog/posthog-flutter#555 turned out to be. Neither a stock Flutter app nor flutter_local_notifications sets one.

The Flutter page tells users to capture Android warm taps themselves. Once PostHog/posthog-flutter#557 ships, the plugin captures that tap too, and the manual API isn't deduplicated against it — so anyone following the current snippet double-counts every warm tap. Replaced with an example that can't collide, plus an explicit "don't wire this to onMessageOpenedApp / getInitialMessage()".

The Android page points at the manual API for warm taps. PostHog/posthog-android#753 adds PostHogAndroid.capturePushNotificationOpened(intent), which dedupes against the automatic path. Also documents the singleTop requirement — without it the system resumes the task instead of delivering the tap, so the snippet would never fire — and that detection is FCM-specific (google.message_id).

React Native carries the same iOS overclaim, so that one sentence is corrected. The RN cold-start gap itself is not fixed and is out of scope here.

Related

PostHog/posthog-ios#792 prewarmPushNotificationOpenCapture(), and the missing-delegate warning
PostHog/posthog-android#753 PostHogAndroid.capturePushNotificationOpened(intent)
PostHog/posthog-flutter#556 iOS cold-start capture — PostHog/posthog-flutter#555
PostHog/posthog-flutter#557 Android cold and warm start — PostHog/posthog-flutter#558
#19905 docs for all of the above — ← this PR

Order: the two native PRs merge and release first → the two Flutter PRs leave draft and go green on their own once the floors publish → this one last, since posthog.com deploys on merge.

Timing

⚠️ Do not merge before the remaining SDK releases land. posthog.com deploys on merge. Status as of 2026-09-09:

Needed by Status
posthog-android 3.62.0 ✅ released — the Android page is accurate today
posthog-ios 3.72.0 PostHog/posthog-ios#792 merged, release pending
Flutter release carrying both floors PostHog/posthog-flutter#556 and PostHog/posthog-flutter#557

Only the Flutter page still contains statements that are false today, and they are now scoped to a sentence that names the release they need rather than asserted up front. The iOS delegate callout, the FCM-detection note, the singleTop requirement and the whole Android page are true right now — if you'd rather ship the correction sooner, everything except flutter.mdx could merge today.

The Flutter page no longer assumes a single release carries both halves. An earlier draft said cold-start and Android warm-start capture arrive together in "the" release raising both floors, which would be wrong if PostHog/posthog-flutter#556 and PostHog/posthog-flutter#557 ship in different releases — or if only one of them lands. Each half now names the native floor it needs (posthog-ios 3.72.0 for iOS, posthog-android 3.62.0 for Android) and says the two can ship separately, so the page reads correctly under any merge order, including neither.

Draft. The two behavioural statements — Android warm-start capture being automatic, and cold start working at all — only become true when the SDK releases land. The delegate and FCM-detection corrections are true today and could merge sooner if you'd rather split them.

The Flutter SDK version numbers are deliberately not stated: #556 and #557 ship separately with different native gates, so which release carries which half depends on merge order. Native floors (posthog-ios 3.72.0, posthog-android 3.62.0) are certain and are stated.

Testing

The behaviour described here was verified on device — iPhone 17 Pro simulator and a Pixel 9 emulator — for cold and warm starts on both platforms, in native and Flutter hosts, with each event read out of the /batch payload the SDK sent (the example apps were pointed at a local stand-in for the ingestion host). Re-run 2026-09-08; all eight combinations captured, one event each. Details in the linked SDK PRs.

Not verified: the site hasn't been built locally, so the CalloutBox rendering is unchecked.

🤖 Generated with Claude Code

https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd

@turnipdabeets turnipdabeets self-assigned this Sep 2, 2026
@github-actions github-actions Bot added docs Improvements or additions to product documentation, "Docs" content PR only touches files under contents/ labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Deploy preview

Status Details Updated (UTC)
🟢 Ready View preview Sep 09, 2026 02:40AM

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Vale prose linter → found 8 errors, 37 warnings, 11 suggestions in your markdown

Full report → Copy the linter results into an LLM to batch-fix issues.

Linter being weird? Update the rules!

contents/docs/workflows/push-notifications/android.mdx — 2 errors, 6 warnings, 2 suggestions
Line Severity Message Rule
12:94 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
16:72 warning 'classpath' is a possible misspelling. PostHogBase.Spelling
48:37 warning 'unregisters' is a possible misspelling. PostHogBase.Spelling
48:127 warning Use 'ID' instead of 'id'. Vale.Terms
50:18 warning 'unregistration' is a possible misspelling. PostHogBase.Spelling
65:270 warning Use 'ID' instead of 'id'. Vale.Terms
65:307 error Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. PostHogBase.EnDash
67:123 error Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. PostHogBase.EnDash
94:66 warning 'classpath' is a possible misspelling. PostHogBase.Spelling
95:102 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
contents/docs/workflows/push-notifications/flutter.mdx — 4 errors, 12 warnings, 3 suggestions
Line Severity Message Rule
12:118 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
26:110 warning 'APNs' is a possible misspelling. PostHogBase.Spelling
26:254 warning 'classpath' is a possible misspelling. PostHogBase.Spelling
26:334 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
28:151 warning Use 'PostHog' instead of 'posthog'. Vale.Terms
28:159 warning Use 'iOS' instead of 'ios'. Vale.Terms
28:192 warning Use 'PostHog' instead of 'posthog'. Vale.Terms
28:200 warning Use 'Android' instead of 'android'. Vale.Terms
30:45 error Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. PostHogBase.EnDash
34:192 error Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. PostHogBase.EnDash
50:9 warning Capitalize 'Logs' for PostHog's product. Use 'logs' for the general industry concept. PostHogBase.ProductNames
76:1 warning 'Unregister' is a possible misspelling. PostHogBase.Spelling
84:18 warning 'unregistration' is a possible misspelling. PostHogBase.Spelling
88:196 error Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. PostHogBase.EnDash
125:66 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
125:159 warning 'classpath' is a possible misspelling. PostHogBase.Spelling
126:77 warning 'APNs' is a possible misspelling. PostHogBase.Spelling
126:105 warning Use 'ID' instead of 'id'. Vale.Terms
127:189 error Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. PostHogBase.EnDash
contents/docs/workflows/push-notifications/ios.mdx — 1 errors, 10 warnings, 3 suggestions
Line Severity Message Rule
12:192 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
13:28 warning 'APNs' is a possible misspelling. PostHogBase.Spelling
13:79 warning 'APNs' is a possible misspelling. PostHogBase.Spelling
13:98 warning Use 'ID' instead of 'id'. Vale.Terms
27:199 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
31:243 error Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. PostHogBase.EnDash
47:9 warning Capitalize 'Logs' for PostHog's product. Use 'logs' for the general industry concept. PostHogBase.ProductNames
69:1 warning 'Unregister' is a possible misspelling. PostHogBase.Spelling
75:52 warning 'unregisters' is a possible misspelling. PostHogBase.Spelling
75:142 warning Use 'ID' instead of 'id'. Vale.Terms
77:18 warning 'unregistration' is a possible misspelling. PostHogBase.Spelling
115:83 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
116:47 warning 'APNs' is a possible misspelling. PostHogBase.Spelling
116:120 warning Use 'ID' instead of 'id'. Vale.Terms
contents/docs/workflows/push-notifications/react-native.mdx — 1 errors, 9 warnings, 3 suggestions
Line Severity Message Rule
12:132 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
32:110 warning 'APNs' is a possible misspelling. PostHogBase.Spelling
32:252 warning 'classpath' is a possible misspelling. PostHogBase.Spelling
32:382 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
34:184 error Hi, Andy here... use an en dash ( – ) with spaces. On Mac, holding down the Option and hyphen key will give you an en dash. PostHogBase.EnDash
59:54 warning 'APNs' is a possible misspelling. PostHogBase.Spelling
59:83 warning Use 'ID' instead of 'id'. Vale.Terms
61:1 warning 'Unregister' is a possible misspelling. PostHogBase.Spelling
69:18 warning 'unregistration' is a possible misspelling. PostHogBase.Spelling
123:112 suggestion Address the reader directly. Use 'you' instead of 'the user'. PostHogDocs.DirectAddress
123:203 warning 'classpath' is a possible misspelling. PostHogBase.Spelling
124:77 warning 'APNs' is a possible misspelling. PostHogBase.Spelling
124:105 warning Use 'ID' instead of 'id'. Vale.Terms

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Bundle report

Total JS (gzip)

8.85 MiB (no change)

Eager graph (modules shipped in each entrypoint's initial chunks)

Entrypoint Eager size Budget Modules
app 18.44 MiB (no change) report-only 2053
Largest modules in the app closure
Module Size
./src/data/mcp-tools.json 1100.8 KiB
css ./node_modules/.pnpm/css-loader@5.2.7_webpack@5.101.3/node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[8].oneOf[1].use[1]!./node_modules/.pnpm/postcss-loader@4.3.0_postcss@8.5.6_webpack@5.101.3/node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[8].oneOf[1].use[2]!./src/styles/global.css 757.9 KiB
./src/components/Stickers/Stickers.tsx 696.4 KiB
./node_modules/.pnpm/@radix-ui+react-icons@1.3.2_react@18.3.1/node_modules/@radix-ui/react-icons/dist/react-icons.esm.js 481.4 KiB
./node_modules/.pnpm/@posthog+brand@0.8.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/x-ray.mjs 480.8 KiB
./node_modules/.pnpm/rehype-raw@7.0.0/node_modules/rehype-raw/lib/index.js + 29 modules 395.1 KiB
./node_modules/.pnpm/@posthog+brand@0.8.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/im-the-driver.mjs 385.7 KiB
./src/hooks/useCustomers.tsx + 55 modules 370.0 KiB
./node_modules/.pnpm/@posthog+icons@0.36.6_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js 354.8 KiB
./node_modules/.pnpm/react-markdown@8.0.7_@types+react@16.14.66_react@18.3.1/node_modules/react-markdown/lib/react-markdown.js + 88 modules 351.4 KiB
./src/components/ProductComparisonTable/index.tsx + 126 modules 302.5 KiB
./node_modules/.pnpm/cloudinary-core@2.14.0_lodash@4.17.21/node_modules/cloudinary-core/cloudinary-core.js 281.9 KiB
./node_modules/.pnpm/@posthog+brand@0.8.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/doll-house.mjs 281.7 KiB
./node_modules/.pnpm/@posthog+brand@0.8.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/director.mjs 275.6 KiB
./src/components/SearchUI/index.tsx + 87 modules 273.0 KiB

Eager-graph budgets are report-only until a baseline is established. Sizes are gzip of public/**/*.js; eager size is webpack module source bytes for the modules actually shipped in the entrypoint's initial chunks (post-tree-shake).

turnipdabeets and others added 3 commits September 8, 2026 22:04
iOS never captures a tap unless the app sets a UNUserNotificationCenter
delegate — true today, and the most likely reason a reader sees nothing.
The Flutter page still told users to capture Android warm taps themselves,
which now double-counts, and the Android page pointed at the manual API
rather than the intent overload that dedupes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd
The paragraph claimed both platforms capture every tap, then walked it
back in the next one. A reader who stops at the first sentence gets a
statement that is false until the Flutter release carrying both native
floors ships.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd
The page assumed one Flutter release would carry both floors, so if the
iOS and Android halves ship separately it would be wrong for whichever
release lands first. Each half now names the floor it needs, and the
"captured for you" claim points back at that gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd

@dustinbyrne dustinbyrne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approving ahead of release, some minor comments below. feel free to ping me back on this for another look!


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quite a large callout, maybe just the header?

Image


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think em-dash is against style/writing guide

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

em-dash here too


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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

em-dash

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

em-dash

Comment on lines +32 to +52
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is actually the large callout i was looking at originally (screenshot attached to another comment). i'd consider splitting this up, but maybe there's precedence for this?


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

em-dash

## 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, once your plugin version carries the native floor for that platform (see above). 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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

em-dash

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

em-dashes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

content PR only touches files under contents/ docs Improvements or additions to product documentation, "Docs"

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants