Skip to content

Conversation

@odin-posthog
Copy link

@odin-posthog odin-posthog commented Jan 20, 2026

💡 Motivation and Context

We want to allow customers to send push notifications to their users in workflows. For that, we need to enable the SDK to collect and update the FCM device token that must be used for pushing notifications.

We're not collecting it automatically when initializing so users that don't need this functionality aren't forced to install firebase dependencies, and they have control over which users have their tokens collected (eg. signed-in only)

💚 How did you test it?

Manually in my emulator through the sample app, and with some automated tests

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.
  • If there are related changes in the core package, I've already released them, or I'll release them before this one.

@odin-posthog odin-posthog force-pushed the odin/feature-add-push-notification-token-upload branch from 95772c0 to f8a4c90 Compare January 26, 2026 14:54
val storedToken = preferences.getValue(FCM_TOKEN) as? String
val lastUpdated = preferences.getValue(FCM_TOKEN_LAST_UPDATED) as? Long ?: 0L
val currentTime = config.dateProvider.currentDate().time
val oneHourInMillis = 60 * 60 * 1000L
Copy link
Member

Choose a reason for hiding this comment

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

this could be a static/global var so we dont have to compute on every method call

// Register with backend on a background thread to avoid StrictMode NetworkViolation
// The Firebase callback runs on the main thread, so we need to move the network call off it
return try {
val api = PostHogApi(config)
Copy link
Member

Choose a reason for hiding this comment

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

setup already creates a PostHogApi, lets make it global or create a class that receives the api instance as we do with many other things eg remoteConfigProvider

val distinctId = distinctId()

val executor =
Executors.newSingleThreadExecutor(
Copy link
Member

Choose a reason for hiding this comment

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

this should be a global instance and reused across calls eg queueExecutor
maybe within the class mentioned above https://github.com/PostHog/posthog-android/pull/376/files#r2728191619

Comment on lines 1279 to 1289
} catch (e: java.util.concurrent.TimeoutException) {
config.logger.log("Failed to register FCM token: Timeout after 5 seconds")
future.cancel(true)
false
} catch (e: java.util.concurrent.ExecutionException) {
val cause = e.cause ?: e
config.logger.log("Failed to register FCM token: ${cause.message ?: "Unknown error"}")
false
} catch (e: Throwable) {
config.logger.log("Failed to register FCM token: ${e.message ?: "Unknown error"}")
false
Copy link
Member

Choose a reason for hiding this comment

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

you probably want this to be done within the PostHogApi class or inside the class mentioned above, also take a look at PostHogApi and how it throws PostHogApiError, you dont need to await the future, maybe this should should receive a callback to be called in case something goes wrong, but the method should be a fire and forget instead of awaiting for up to 5s, if the user calls this from the main thread, you are causing an ANR

@odin-posthog odin-posthog marked this pull request as ready for review January 26, 2026 16:21
@odin-posthog odin-posthog requested a review from a team as a code owner January 26, 2026 16:21
return PostHogSessionManager.isSessionActive()
}

override fun registerPushToken(token: String): Boolean {
Copy link
Member

Choose a reason for hiding this comment

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

Does this method have to be atomic? what if multiple threads call this with multiple tokens? most likely, this will race and cause issues. do we have to protect against that?

@@ -1,4 +1,5 @@
## Next
Copy link
Author

Choose a reason for hiding this comment

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

Is it correct to have the changelog in this PR already or should it be in the version bump PR? (or does it not matter?)

Copy link
Member

Choose a reason for hiding this comment

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

under next is correct, just do not add a version yet

@odin-posthog odin-posthog force-pushed the odin/feature-add-push-notification-token-upload branch from 6f3c3c0 to 09a500b Compare January 26, 2026 17:25
Comment on lines +1271 to +1272
preferences.setValue(FCM_TOKEN, token)
preferences.setValue(FCM_TOKEN_LAST_UPDATED, currentTime)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What happens when the API call fails right below here (L1283)?
Should we clear these preference values we set here?

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, that's a good idea. Updated to clear preferences on error


override fun registerPushToken(
token: String,
callback: PostHogPushTokenCallback?,
Copy link
Collaborator

@ioannisj ioannisj Jan 27, 2026

Choose a reason for hiding this comment

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

Will BE allow integrating only with a single Firebase project? If we support multiple firebase projects, I would expect to register a firebase config identifier along with this token (So that BE would know which Firebase project to use)? Correct me if my rationale is wrong here, but for people using multiple apps within a specific PostHog project, this could be problematic?

Copy link
Author

Choose a reason for hiding this comment

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

That's a good point! Until now I had assumed that users would have one firebase project per posthog project, and that's probably true for most users, but it would be much better to support multiple. I added the firebase app id

*/
@PostHogInternal
public data class PostHogPushSubscriptionRequest(
val api_key: String,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is snake_cased idiomatic to Kotlin @marandaneto? Probably okay since this is a boundary/network object, just felt weird to my eye. iirc, there's a @SerializedName attr we could use?

Copy link
Author

Choose a reason for hiding this comment

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

Turns out we do use that to transform to snake_case, updated the class to do that and keep the kotlin code camelCase

preferences.setValue(FCM_TOKEN_LAST_UPDATED, currentTime)
}

val distinctId = distinctId()
Copy link
Collaborator

Choose a reason for hiding this comment

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

I feel there may be a race condition here where distinctId() may change between the synchronization block above and the api call below? This would mean we may send the token with the wrong distinctId. @marandaneto wdyt?

Copy link
Author

Choose a reason for hiding this comment

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

For our backend, we're okay with a "best effort to send correct distinctId", if we get a token for an outdated one and trigger a push notification of a more recent/correct distinctId, we query the persons db for past distinctIds associated with the user and find the token with the outdated distinctId

}

val distinctId = distinctId()
pushTokenExecutor.executeSafely {
Copy link
Member

Choose a reason for hiding this comment

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

this assumes connectivity, what if the device is offline or if something fails? how would the user know that they should call this again at some point? should we document this behaviour?

* Callback for push token registration results.
* @param success `true` if registration succeeded, `false` if it failed, or `null` if registration was skipped (e.g., token unchanged).
*/
public typealias PostHogPushTokenCallback = (success: Boolean?) -> Unit
Copy link
Member

Choose a reason for hiding this comment

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

i think you should use a SAM interface here which is more friendly to java and kotlin users

fun interface PostHogPushTokenCallback {
    fun onComplete(success: Boolean?)
}

also its common to create new files for classes/interfaces

Comment on lines +2636 to +2637
// Wait for background thread to complete
Thread.sleep(100)
Copy link
Member

Choose a reason for hiding this comment

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

instead of using thread.sleep which slow down tests, you can use another approach
eg CountDownLatch, check the usage in tests and you'd see how to use it

return PostHogSessionManager.isSessionActive()
}

override fun registerPushToken(
Copy link
Member

Choose a reason for hiding this comment

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

i still think that this method code impl should live within another class, and this method just forwards to that class eg remoteConfig/replayQueue etc
this class is huge already and it should be a clean interface where users just call something and dont get distracted by its implementation

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.

4 participants