feat: skip push registration when the project has no integration for the app_id - #703
Merged
Conversation
…the app_id A mobile SDK with push capture on registers its device token on every app open, against every project it reports to, whether or not that project has a Firebase or APNs integration. The server accepts the request and discards the token, so the request was never worth making. Remote config now publishes push.appIds, the app_ids a project accepts registrations for. The send is gated on it in attempt(), the single choke point that already covers register, flush-driven retry and identify resend. The record is still persisted when the send is gated, so a project that configures push later has a token to register. The list is cached to disk and preloaded on launch, mirroring errorTracking. Registration runs at startup and remote config resolves asynchronously, so without the cache the gate would only take effect from the second launch. Absent stays permissive. A server older than the key sends nothing, which an SDK cannot tell apart from a project with push disabled, so null means attempt rather than skip. onPushAppIdsChanged clears the delivered marker for an app_id that just became registerable. A device that registered while its project had no integration was answered 200 and recorded a delivery for a token the server never kept; nothing else reaches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bbav6vE7Tm24KkdRhZmvNM
The push config functions were inserted between reevaluateCapturePerformanceFromCachedConfig and the comment describing it, which left the comment attached to the wrong function and failed spotless. Move them below it instead. Also drops a trailing blank line in the test file.
5 tasks
getPushAppIds and consumeNewlyRegisterablePushAppIds on PostHogRemoteConfig, and the push property on PostHogRemoteConfigResponse, which also widens its generated constructors. Signatures verified against the compiled classes with javap rather than hand written, since apiDump cannot run here.
dmarchuk
marked this pull request as ready for review
August 18, 2026 19:54
Contributor
Prompt To Fix All With AI### Issue 1
posthog/src/main/java/com/posthog/internal/PostHogPushSubscriptionManager.kt:439-446
**Stale eligibility permits registration**
If remote configuration excludes the app ID while asynchronous identity-token minting is in progress, `performSend` does not recheck eligibility before posting and persisting `deliveredForDistinctId`, causing a discarded token to be recorded as delivered and suppressing ordinary retries.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "chore: record the new public API in the ..." | Re-trigger Greptile |
Contributor
posthog-android Compliance ReportDate: 2026-08-19 12:22:08 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
|
marandaneto
reviewed
Aug 19, 2026
marandaneto
reviewed
Aug 19, 2026
marandaneto
approved these changes
Aug 19, 2026
…rker is durably cleared
dmarchuk
added a commit
to PostHog/posthog-ios
that referenced
this pull request
Aug 19, 2026
…the app_id (#767) * feat: skip push registration when the project has no integration for the app_id Mirrors PostHog/posthog-android#703, keeping the field names and the three client rules identical between the two SDKs. A device registers its token on every app open whether or not the project has a Firebase or APNs integration. The server accepts the request and discards the token, so the request was never worth making. Remote config now publishes push.appIds, and the send is gated on it in attemptIfAllowed, the choke point every caller already goes through. The record is still persisted when the send is gated, so a project that configures push later has a token to register. Absent stays permissive. A server older than the key sends nothing, which an SDK cannot tell apart from a project with push disabled, so nil means attempt rather than skip. onPushAppIdsChanged clears the delivered marker for an app_id that just became registerable. A device that registered while its project had no integration was answered 200 and recorded a delivery for a token the server never kept; nothing else reaches it. Unlike Android, no per-slice disk cache is needed: iOS already persists the whole remote config blob and preloadPushConfig reads the push slice from it on launch, before the first registration attempt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bbav6vE7Tm24KkdRhZmvNM * fix(push): add explicit self in onPushAppIdsChanged nested closures so it compiles * fix(push): silence file_length lint on PostHogRemoteConfig after the appIds gate * fix(push): recheck app_id eligibility after the identity token mint before sending * chore(push): drop redundant self to satisfy swiftformat * fix(push): atomic marker clear and durable one-time upgrade recovery for the appIds gate * chore(push): multi-line guard to satisfy swiftformat --------- Co-authored-by: Claude <noreply@anthropic.com>
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
A device on a project without push configured makes a registration request on every app open that the server can only throw away, and a project that turns push on afterwards never reaches the devices it already has.
/api/push_subscriptions/answers those with200 {"stored": false, "push_enabled": false}and discards the token.deliveredForDistinctId, the device then records a success for a token the server never kept and stops asking. No response reaches a client that has stopped asking, so a project that configures push later reaches nothing installed before it — until a token rotation, anidentify()to a different distinct id, or a reinstall.Server side is merged: PostHog/posthog#83148 publishes the list, and the contract this implements is
docs/internal/push-subscription-registration.mdthere (plus PostHog/posthog#83941, which adds the caching requirement below).Changes
Remote config now carries
push.appIds— the Firebase project ids and APNs bundle ids a project accepts registrations for.attempt(), the choke point that already coversregister, flush-drivenretryPending, and the identify resend. The record is still persisted when the send is gated, soonPushAppIdsChangedhas a token to register later rather than waiting for the app to hand us one again.errorTracking. Registration runs at startup and remote config resolves asynchronously, so without the cache the gate would only take effect from the second launch onward.nullmeans attempt. Only a published empty list means skip.onPushAppIdsChangedclears the delivered marker for anapp_idthat just became registerable, and re-registers. It fires only on that transition — firing on every config load would put the request back on every launch, which is what the marker exists to prevent.On the first launch that ever sees the key, the previous list is treated as empty rather than unknown, so every configured
app_idcounts as new. That costs one request per device, once, and it is the only way to reach a device whose project was configured before it updated to an SDK that reads this key.💚 How did you test it?
New unit tests in
PostHogPushSubscriptionManagerTest:app_idoutside the list sends nothing but still persists the record. Catches the gate being dropped, and catches the record not being persisted, which would leaveonPushAppIdsChangedwith no token.app_idinside the list registers. Catches the gate being inverted.app_idbecoming registerable clears the delivered marker and re-registers. This is the un-stranding path.app_idbecoming registerable does not re-register. Catches over-firing.Manual E2E on the emulator against a local server
Ran the six-scenario verification brief on an Android emulator (posthog-android-sample as
com.posthog.pushtest, real FCM token, Firebase projectposthog-dev) against a local PostHog serving realpush.appIdsremote config for a team with a togglable Firebase integration.:posthog:test(62 push-manager cases) and the full suite pass.$device_push_subscription_posthog-devset (verified in ClickHouse and the persons UI).push_subscription.pendingwritten without a delivered marker./configresponded, so the disk-cached slice gated it, not the fresh fetch.pushkey removed from the response, the SDK registered.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Authored with Claude Code, as the reference implementation of the three client rules; iOS mirrors it next, matching how #656 and PostHog/posthog-ios#735 were kept in step.
Two design points worth a reviewer's attention. The gate lives in
attempt()rather thanperformRegisterso flush and identify resends are covered by the same check. And the transition set is computed inprocessPushConfigfrom the previously cached list, then drained once byconsumeNewlyRegisterablePushAppIds, so a device re-registers on the transition and not on every subsequent config load.I read the server behavior from the merged endpoint rather than assuming it, and the delivered-marker mechanics from this repo's own
performRegisterandcurrentRecord.Generated by Claude Code