Skip to content

feat(react-native): capture warm-start push opens on Android - #4858

Open
turnipdabeets wants to merge 1 commit into
mainfrom
feat/rn-warm-start-push-open
Open

feat(react-native): capture warm-start push opens on Android#4858
turnipdabeets wants to merge 1 commit into
mainfrom
feat/rn-warm-start-push-open

Conversation

@turnipdabeets

Copy link
Copy Markdown
Contributor

Problem

Closes #4857.

On Android, React Native captures $push_notification_opened for a cold-start tray tap but never for a tap that arrives while the app is already running. That tap is delivered to Activity.onNewIntent, which ActivityLifecycleCallbacks does not expose, so posthog-android's integration cannot see it — and the plugin's own launch-intent read (captureColdStartPushOpenIfNeeded) only ever looks at the intent the Activity was created with. Those opens are silently missing.

Changes

  • The module now implements ActivityEventListener and forwards onNewIntent to PostHogAndroid.capturePushNotificationOpened(intent). ReactActivity already forwards onNewIntent to its listeners, so hosts need no code of their own — unlike a plain Android app, which has to add the call to its own activity.
  • Raises the com.posthog:posthog-android floor from 3.61.0 to 3.62.0, which is where PostHogAndroid.capturePushNotificationOpened(intent) landed (feat(push): capture a launch intent the SDK was installed too late to read posthog-android#753). At 3.61.0 the function does not exist.
  • The listener is registered in initialize() and removed in invalidate().

Deduplication is posthog-android's, keyed on google.message_id, so the new path cannot double-count against the cold-start one.

Worth a reviewer's attention: the RN example app's MainActivity uses android:launchMode="singleTask" (the React Native template default), which delivers onNewIntent just as singleTop does. So the singleTop requirement documented for plain Android hosts does not apply to a stock RN app.

Testing

Verified on a Pixel 9 emulator with examples/example-rn-native-plugin, pointed at a local stand-in for the ingestion host so each assertion is made against the exact /batch body the SDK sent:

Scenario Result
Cold launch carrying google.message_id captured — unchanged, this is the existing path
Tap while the app is running (onNewIntent) captured — 0 before this change
Same message id delivered again not captured

Falsified rather than just confirmed: with only the onNewIntent body reverted and everything else identical, the warm tap captures nothing; restoring it captures once again. The warm event carries the new intent's payload, not a re-read of the cold one.

No unit test. The existing PosthogReactNativePluginModuleTest has junit only — no Robolectric or mocking framework — so covering this would mean pulling one in to fake ReactApplicationContext and Activity. That felt like more surface than the change warrants; happy to add it if you'd rather have it.

Release info Sub-libraries affected

Libraries affected

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react-native-plugin
  • @posthog/react
  • @posthog/ai
  • @posthog/convex
  • @posthog/next
  • @posthog/nextjs-config
  • @posthog/nuxt
  • @posthog/openfeature-node-provider
  • @posthog/openfeature-web-provider
  • @posthog/rollup-plugin
  • @posthog/webpack-plugin
  • @posthog/types
  • @posthog/browser-common

Checklist

  • Tests for new code
  • Accounted for the impact of any changes across different platforms
  • Accounted for backwards compatibility of any changes (no breaking changes!)
  • Took care not to unnecessarily increase the bundle size

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Built with Claude Code (session), driven by @turnipdabeets. Found while correcting the push-notification open-coverage docs across SDKs (PostHog/posthog.com#19905) — the React Native page says "on Android only cold-start taps are captured", and checking whether that was still true turned up the gap rather than a stale sentence.

Two things were measured rather than assumed:

  • Whether RN misses cold starts too, the way Flutter did. It does not — captureColdStartPushOpenIfNeeded already solves it, with a comment naming the same late-install problem. The docs sentence was accurate; only the warm half was missing.
  • Whether hosts would need an onNewIntent line, as plain Android apps do. They do not: ReactActivity forwards to ActivityEventListener, so the plugin can register itself. That is why this closes the gap without a docs-only workaround.

Related, same gap closed elsewhere: PostHog/posthog-android#753 (the entry point), PostHog/posthog-flutter#557 (Flutter's NewIntentListener), PostHog/posthog-ios#792 (the iOS cold-start half).

🤖 Generated with Claude Code

https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd

A tap that arrives while the process is alive is delivered to
Activity.onNewIntent, which ActivityLifecycleCallbacks does not expose —
so posthog-android's integration cannot see it and only the cold-start
tap the plugin already reads by hand was captured.

ReactActivity forwards onNewIntent to its ActivityEventListeners, so the
plugin can observe it directly and hosts need no code of their own. The
entry point it forwards to landed in posthog-android 3.62.0, so the floor
moves with it; it is deduplicated by google.message_id, so it cannot
double-count against the cold-start path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012txiHBCZRkShMdE7V25Jrd
@turnipdabeets
turnipdabeets force-pushed the feat/rn-warm-start-push-open branch from f9e400f to 460dcc2 Compare September 9, 2026 02:43
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

posthog-node Compliance Report

Date: 2026-09-09 02:47:26 UTC
Duration: 146006ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 105ms
Format Validation.Event Has Uuid 15ms
Format Validation.Event Has Lib Properties 10ms
Format Validation.Distinct Id Is String 11ms
Format Validation.Token Is Present 11ms
Format Validation.Custom Properties Preserved 10ms
Format Validation.Event Has Timestamp 11ms
Retry Behavior.Retries On 503 11024ms
Retry Behavior.Does Not Retry On 400 2013ms
Retry Behavior.Does Not Retry On 401 2011ms
Retry Behavior.Respects Retry After Header 8019ms
Retry Behavior.Implements Backoff 24035ms
Retry Behavior.Retries On 500 8019ms
Retry Behavior.Retries On 502 8019ms
Retry Behavior.Retries On 504 8018ms
Retry Behavior.Max Retries Respected 24037ms
Deduplication.Generates Unique Uuids 18ms
Deduplication.Preserves Uuid On Retry 8018ms
Deduplication.Preserves Uuid And Timestamp On Retry 16020ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 8024ms
Deduplication.No Duplicate Events In Batch 14ms
Deduplication.Different Events Have Different Uuids 9ms
Compression.Sends Gzip When Enabled 9ms
Batch Format.Uses Proper Batch Structure 8ms
Batch Format.Flush With No Events Sends Nothing 4ms
Batch Format.Multiple Events Batched Together 12ms
Error Handling.Does Not Retry On 403 2010ms
Error Handling.Does Not Retry On 413 2010ms
Error Handling.Retries On 408 8017ms

Feature_Flags Tests

17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 17ms
Request Payload.Flags Request Uses V2 Query Param 10ms
Request Payload.Flags Request Hits Flags Path Not Decide 10ms
Request Payload.Flags Request Omits Authorization Header 9ms
Request Payload.Token In Flags Body Matches Init 9ms
Request Payload.Groups Round Trip 9ms
Request Payload.Groups Default To Empty Object 9ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 10ms
Request Payload.Disable Geoip Omitted Defaults To False 10ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 9ms
Request Lifecycle.No Flags Request On Init Alone 2ms
Request Lifecycle.No Flags Request On Normal Capture 9ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 13ms
Request Lifecycle.Mock Response Value Is Returned To Caller 9ms
Retry Behavior.Retries Flags On 502 3012ms
Retry Behavior.Retries Flags On 504 3013ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 10ms

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

posthog-js Compliance Report

Date: 2026-09-09 02:52:02 UTC
Duration: 88989ms

✅ All Tests Passed!

26/26 tests passed


Capture Tests

26/26 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields Client 141ms
Format Validation.Event Has Uuid 26ms
Format Validation.Event Has Lib Properties 19ms
Format Validation.Distinct Id Is String Client 20ms
Format Validation.Token Is Present Client 20ms
Format Validation.Custom Properties Preserved 18ms
Format Validation.Event Has Timestamp 17ms
Retry Behavior.Retries On 503 5021ms
Retry Behavior.Does Not Retry On 400 2020ms
Retry Behavior.Does Not Retry On 401 2017ms
Retry Behavior.Respects Retry After Header 5018ms
Retry Behavior.Implements Backoff 15034ms
Retry Behavior.Retries On 500 5016ms
Retry Behavior.Retries On 502 5023ms
Retry Behavior.Retries On 504 5016ms
Retry Behavior.Max Retries Respected 15029ms
Deduplication.Generates Unique Uuids 26ms
Deduplication.Preserves Uuid On Retry 5014ms
Deduplication.Preserves Uuid And Timestamp On Retry 10025ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5030ms
Deduplication.No Duplicate Events In Batch 22ms
Deduplication.Different Events Have Different Uuids 14ms
Batch Format.Flush With No Events Sends Nothing 11ms
Error Handling.Does Not Retry On 403 2029ms
Error Handling.Does Not Retry On 413 2016ms
Error Handling.Retries On 408 5021ms

@turnipdabeets
turnipdabeets marked this pull request as ready for review September 9, 2026 02:52
@turnipdabeets
turnipdabeets requested a review from a team as a code owner September 9, 2026 02:52
@greptile-apps

greptile-apps Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
### Issue 1
packages/react-native-plugin/android/src/main/java/com/posthogreactnativeplugin/PosthogReactNativePluginModule.kt:48-50
**Warm-start path lacks tests**

The new listener registration, cleanup, and `onNewIntent` forwarding path have no automated coverage. The existing Android module test only exercises the unrelated `getBoolean` helper, so a future lifecycle or forwarding regression could silently restore the missing warm-start events. Please add a focused test covering registration, intent forwarding, and removal.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(react-native): capture warm-start p..." | Re-trigger Greptile

Comment on lines +48 to +50
override fun onNewIntent(intent: Intent?) {
PostHogAndroid.capturePushNotificationOpened(intent)
}

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.

P2 Warm-start path lacks tests

The new listener registration, cleanup, and onNewIntent forwarding path have no automated coverage. The existing Android module test only exercises the unrelated getBoolean helper, so a future lifecycle or forwarding regression could silently restore the missing warm-start events. Please add a focused test covering registration, intent forwarding, and removal.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-native-plugin/android/src/main/java/com/posthogreactnativeplugin/PosthogReactNativePluginModule.kt
Line: 48-50

Comment:
**Warm-start path lacks tests**

The new listener registration, cleanup, and `onNewIntent` forwarding path have no automated coverage. The existing Android module test only exercises the unrelated `getBoolean` helper, so a future lifecycle or forwarding regression could silently restore the missing warm-start events. Please add a focused test covering registration, intent forwarding, and removal.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

left a comment otherwise lgtm

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Advisory code review.

}

override fun onActivityResult(
activity: Activity?,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

blocking: Match React Native’s non-null callback signatures — React Native 0.80+ defines ActivityEventListener in Kotlin with non-null Activity and Intent parameters. Consequently, activity: Activity? here and intent: Intent? on line 48 override neither method, breaking Android compilation for consumers on these versions. Both parameters should be non-null; this also remains compatible with RN 0.79.6. Reproduction: reproduced — python3 check-listener-compat.py compiled the reviewed callback signatures against the published RN 0.86.2 artifact and failed with “overrides nothing” for both methods; making both parameters non-null compiled successfully against RN 0.79.6 and 0.86.2.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

React Native: warm-start push notification opens are never captured on Android

2 participants