docs: correct push-notification open coverage for iOS, Android and Flutter - #19905
docs: correct push-notification open coverage for iOS, Android and Flutter#19905turnipdabeets wants to merge 4 commits into
Conversation
Deploy preview
|
|
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!
|
| 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 |
Bundle reportTotal JS (gzip)8.85 MiB (no change) Eager graph (modules shipped in each entrypoint's initial chunks)
Largest modules in the
|
| 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).
6054c3e to
9a96cd5
Compare
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
9a96cd5 to
eec947c
Compare
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
left a comment
There was a problem hiding this comment.
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"> |
|
|
||
| <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. |
There was a problem hiding this comment.
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). |
|
|
||
| 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: |
| 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. |
| <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> |
There was a problem hiding this comment.
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. |
| ## 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: |
| | --- | --- | | ||
| | 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. | |

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
UNUserNotificationCenterdelegate. 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_openedis 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 norflutter_local_notificationssets 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 thesingleToprequirement — 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
prewarmPushNotificationOpenCapture(), and the missing-delegate warningPostHogAndroid.capturePushNotificationOpened(intent)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
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
singleToprequirement and the whole Android page are true right now — if you'd rather ship the correction sooner, everything exceptflutter.mdxcould 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
/batchpayload 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
CalloutBoxrendering is unchecked.🤖 Generated with Claude Code
https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd