-
Notifications
You must be signed in to change notification settings - Fork 33
feat(odin): Allow collecting FCM device token in SDK & demonstrate it in sample app #376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(odin): Allow collecting FCM device token in SDK & demonstrate it in sample app #376
Conversation
95772c0 to
f8a4c90
Compare
| 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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
| } 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 |
There was a problem hiding this comment.
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
| return PostHogSessionManager.isSessionActive() | ||
| } | ||
|
|
||
| override fun registerPushToken(token: String): Boolean { |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
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
6f3c3c0 to
09a500b
Compare
| preferences.setValue(FCM_TOKEN, token) | ||
| preferences.setValue(FCM_TOKEN_LAST_UPDATED, currentTime) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
| // Wait for background thread to complete | ||
| Thread.sleep(100) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
💡 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