feat(push): capture a launch intent the SDK was installed too late to read - #753
Draft
turnipdabeets wants to merge 5 commits into
Draft
feat(push): capture a launch intent the SDK was installed too late to read#753turnipdabeets wants to merge 5 commits into
turnipdabeets wants to merge 5 commits into
Conversation
… read The tray intent is read in onActivityCreated, and install() only registers lifecycle callbacks — it never seeds from an Activity that already exists. A host that configures the SDK from its own runtime (Flutter and React Native reach setup() from Dart/JS) installs after the launch Activity has created, started and resumed, so no callback ever fires for it. Adds PostHogAndroid.capturePushNotificationOpened(intent) for those hosts, sharing the extraction and message-id dedupe with the automatic path. On that path the id is also persisted, because a caller with no savedInstanceState cannot tell a process-kill restore from a real re-tap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd
5 tasks
Contributor
posthog-android Compliance ReportDate: 2026-09-02 16:09:07 UTC ✅ All Tests Passed!46/46 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
Reverting the except-list entry previously failed nothing, so a future trim would have let a process-death restore re-capture the same tap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd
A host that also hands over warm-start intents writes to the same store as the launch intent, and only the launch intent is redelivered after a process death. With a single slot a warm tap displaced the launch id, so a later restore captured the launch tap a second time. Keeps a bounded history instead, and renames the key to match — it is unreleased, so the rename costs nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd
Android gives libraries no way to observe Activity.onNewIntent, so the new API only helps if the host forwards it. The sample now does, and declares singleTop — without it the system resumes the task instead of delivering the intent, and the snippet would never fire. Also corrects two KDoc sentences written for the single-id design. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd
Open
3 tasks
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 Motivation and Context
The Android half of PostHog/posthog-flutter#558 —
$push_notification_openedis never captured in a Flutter app.The tray intent is read in
onActivityCreated, andinstall()only callsregisterActivityLifecycleCallbacks— it never seeds from an Activity that already exists. A host that configures the SDK from its own runtime installs too late to see that callback. Measured in the Flutter example app:Every lifecycle callback for the launch Activity is missed. Native apps that call
setup()fromApplication.onCreateare unaffected — this only bites hosts that initialise late, which today means Flutter and React Native.Unlike iOS, no buffering is needed: the tap is durable state on the Intent, so the fix is to read it late rather than to hold it.
Changes
PostHogAndroid.capturePushNotificationOpened(intent)— one new public entry point for late-installing hosts. Shares the extraction andgoogle.message_iddedupe with the automatic path, so calling both cannot double-count.pushOpenedMessageIds, a bounded history). A caller with nosavedInstanceStatecannot tell a process-kill restore — which hands the Activity back its original intent — from a real second tap. Read and write are both scoped to the new path, so a pure-native app's stored state is byte-identical to today.onActivityCreatedkeepssavedInstanceStateas its restore gate and never touches the persisted id: it is the strictly better signal, because it separates a restore from a genuine re-tap.💚 How did you test it?
./gradlew :posthog-android:testDebugUnitTest :posthog:test— BUILD SUCCESSFUL.spotlessCheck,:posthog:apiCheck,:posthog-android:apiCheckclean.ALL_INTERNAL_KEYSentry,the automatic path still captures a genuine re-tap in a new process, andwith after setup must not redirect the manual entry to the secondary project.google.message_idextra → 1 capture (was 0); same id in a new process → 0; a new id → 1; no push extra → 0.Testing
Beyond the unit tests, verified on a Pixel 9 emulator through two hosts:
posthog-android's own sample (
setup()inApplication.onCreate, plus the newonNewIntentforwarding this PR adds):origin/main, confirming no regressiononNewIntentA Flutter host (PostHog/posthog-flutter#557), where the SDK installs after the launch Activity has already resumed: cold and warm both captured, with the dedupe holding across process death.
Warm delivery needs
android:launchMode="singleTop"— without it the system resumes the task instead of delivering the intent, which is why the sample now declares it.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset fileThe changeset declares
posthogas well asposthog-android— the coreALL_INTERNAL_KEYSentry is load-bearing (see below) and would not otherwise be released.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Built with Claude Code (session), driven by @turnipdabeets. Root cause measured on an emulator before any code was written; a first attempt that hooked
onActivityStarted/onActivityResumedwas implemented, tested, and discarded once the ordering above was measured.Six review rounds ran over this branch. Three findings are worth a reviewer's attention because each was a silent regression caught only by an executed test:
ALL_INTERNAL_KEYS, so it rode on every event as a super property.getAll()feedsbuildProperties(); every other internal key is listed.?: returnon aUnitfunction), and the twosynchronizedblocks it required reopened the race the lock existed to close — two callers both delivered for one id.A warm-start tap arrives through
Activity.onNewIntent, whichActivityLifecycleCallbacksdoes not expose — so a native host forwards it with one line, which the sample now demonstrates. PostHog/posthog-flutter#557 does it automatically for Flutter apps.Consumer: PostHog/posthog-flutter#557 (which closes PostHog/posthog-flutter#558) raises its floor to
[3.62.0,4.0.0)and is held until this releases.🤖 Generated with Claude Code
https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd