-
Notifications
You must be signed in to change notification settings - Fork 44
feat: add push notification support for PostHog Workflows #667
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
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
fee074b
feat: add push notification support for PostHog Workflows (#642)
ioannisj f3e30bd
feat: attach optional identity token to push subscription requests (#…
ioannisj 41266bb
Merge remote-tracking branch 'origin/feat/push-notifications' into fe…
ioannisj a7534c4
feat(push): durable unregister and passive offline retry (parity with…
ioannisj 33531ab
refactor(push): un-chain reset DELETE and re-register (order-independ…
ioannisj fc5d628
fix(push): treat unclassified send errors as retryable
ioannisj fae8c2b
feat(push): passive failure retry without timer, persistent backoff l…
ioannisj 221bc85
fix(push): drop pending unregister for the current identity when a re…
ioannisj 1396f32
style: apply spotless formatting
ioannisj b310c10
chore: add changeset for push subscription reliability fixes
ioannisj 078f02d
chore: collapse push changesets into the single feature changeset
ioannisj af5c462
fix(push): re-check identity and supersede rules around the identity …
ioannisj 73e1817
fix(push): address platform-specific review findings (host safety, op…
ioannisj 43fdc80
docs(push): document PendingUnregister's single-slot last-write-wins …
ioannisj a60d9e1
chore(push): drop cross-SDK attributions from code comments
ioannisj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "posthog-android": minor | ||
| "posthog": minor | ||
| --- | ||
|
|
||
| Add push notification support for PostHog Workflows. Register device push tokens with `registerPushNotificationToken(...)` (auto-registered from Firebase Cloud Messaging when `firebase-messaging` is on the classpath), and capture opens with `capturePushNotificationOpened(...)` (cold-start tray taps are auto-detected). Both are on by default and can be turned off with the new `capturePushNotificationSubscriptions` and `capturePushNotificationOpened` config flags. On `reset()` the token is unregistered for the logged-out user and re-registered under the new anonymous id, and `unregisterPushNotificationToken()` lets you unregister manually. |
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
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
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
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
Large diffs are not rendered by default.
Oops, something went wrong.
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
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
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
144 changes: 144 additions & 0 deletions
144
...-android/src/main/java/com/posthog/android/internal/PostHogPushSubscriptionIntegration.kt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| package com.posthog.android.internal | ||
|
|
||
| import com.google.firebase.FirebaseApp | ||
| import com.google.firebase.messaging.FirebaseMessaging | ||
| import com.posthog.PostHogIntegration | ||
| import com.posthog.PostHogInterface | ||
| import com.posthog.android.PostHogAndroidConfig | ||
| import com.posthog.internal.PostHogThreadFactory | ||
| import com.posthog.internal.executeSafely | ||
| import java.util.concurrent.ExecutorService | ||
| import java.util.concurrent.Executors | ||
| import java.util.concurrent.atomic.AtomicBoolean | ||
|
|
||
| /** | ||
| * Auto-registers this device's push token at startup so PostHog Workflows can deliver push | ||
| * notifications to it. | ||
| * | ||
| * Firebase Messaging is a `compileOnly` dependency — the SDK never ships its own messaging | ||
| * service and never hard-depends on Firebase. When Firebase is absent, [PushTokenFetcher] no-ops | ||
| * with a debug log. Instant token refresh still requires the host app to forward Firebase's | ||
| * `onNewToken` to [PostHogInterface.registerPushNotificationToken]; the startup fetch here only | ||
| * covers tokens refreshed since the last launch. | ||
| */ | ||
| internal class PostHogPushSubscriptionIntegration( | ||
| private val config: PostHogAndroidConfig, | ||
| private val tokenFetcher: PushTokenFetcher = FirebasePushTokenFetcher(config), | ||
| // Mirrors PostHogReplayIntegration's injectable executor: install() runs on setup()'s caller | ||
| // thread (typically main, inside Application.onCreate()), and FirebaseMessaging's first-touch | ||
| // init is synchronous, so the fetch is hopped off-thread. Tests inject a synchronous executor. | ||
| private val executor: ExecutorService = Executors.newSingleThreadExecutor(PostHogThreadFactory("PostHogPushSub")), | ||
| ) : PostHogIntegration { | ||
| private var postHog: PostHogInterface? = null | ||
| private var ownsInstallation = false | ||
|
|
||
| private companion object { | ||
| private val integrationInstalled = AtomicBoolean(false) | ||
| } | ||
|
|
||
| @Synchronized | ||
| override fun install(postHog: PostHogInterface) { | ||
| if (!integrationInstalled.compareAndSet(false, true)) { | ||
| return | ||
| } | ||
| ownsInstallation = true | ||
| this.postHog = postHog | ||
| executor.executeSafely { | ||
| tokenFetcher.fetchToken { token, appId -> | ||
| this.postHog?.registerPushNotificationToken(token, appId) | ||
| } | ||
| } | ||
| } | ||
|
ioannisj marked this conversation as resolved.
|
||
|
|
||
| @Synchronized | ||
| override fun uninstall() { | ||
| if (!ownsInstallation) { | ||
| return | ||
| } | ||
| try { | ||
| postHog = null | ||
| } finally { | ||
| ownsInstallation = false | ||
| integrationInstalled.set(false) | ||
| } | ||
| } | ||
|
ioannisj marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /** | ||
| * Resolves this device's push token and app identifier. Kept behind an interface so the Firebase | ||
| * dependency stays isolated to [FirebasePushTokenFetcher] and can be faked in tests. | ||
| */ | ||
| internal fun interface PushTokenFetcher { | ||
| fun fetchToken(onToken: (token: String, appId: String) -> Unit) | ||
| } | ||
|
|
||
| private const val FIREBASE_MESSAGING_CLASS = "com.google.firebase.messaging.FirebaseMessaging" | ||
|
|
||
| /** | ||
| * Fetches the FCM token and Firebase `project_id` (used as the push `app_id`). | ||
| * | ||
| * Presence is checked reflectively first, so an app without Firebase on the classpath sees a | ||
| * single debug log and nothing else. All direct Firebase references are confined to | ||
| * [fetchFirebaseToken], which only runs once the class is confirmed present. | ||
| */ | ||
| internal class FirebasePushTokenFetcher( | ||
| private val config: PostHogAndroidConfig, | ||
| // test seam: firebase-messaging is on the unit-test classpath, so the absent-classpath | ||
| // path is exercised by probing for a class name that doesn't resolve | ||
| private val messagingClassName: String = FIREBASE_MESSAGING_CLASS, | ||
| ) : PushTokenFetcher { | ||
| override fun fetchToken(onToken: (token: String, appId: String) -> Unit) { | ||
| try { | ||
| Class.forName(messagingClassName) | ||
| } catch (e: Throwable) { | ||
| config.logger.log("Firebase Messaging not found on the classpath, skipping push token registration.") | ||
| return | ||
| } | ||
| fetchFirebaseToken(onToken) | ||
| } | ||
|
|
||
| // firebase-messaging 25.1.0 deprecates getToken in favor of FID-based registration; | ||
| // PostHog delivers via FCM HTTP v1 registration tokens, so we stay on it until the | ||
| // backend supports FIDs. | ||
| @Suppress("DEPRECATION") | ||
| private fun fetchFirebaseToken(onToken: (token: String, appId: String) -> Unit) { | ||
| val projectId = | ||
| try { | ||
| FirebaseApp.getInstance().options.projectId | ||
|
ioannisj marked this conversation as resolved.
|
||
| } catch (e: IllegalStateException) { | ||
| config.logger.log("Firebase is not initialized, skipping push token registration: $e.") | ||
| return | ||
| } catch (e: Throwable) { | ||
| config.logger.log("Failed to read Firebase project id, skipping push token registration: $e.") | ||
| return | ||
| } | ||
|
|
||
| if (projectId.isNullOrBlank()) { | ||
| config.logger.log("Firebase project id is missing, skipping push token registration.") | ||
| return | ||
| } | ||
|
|
||
| try { | ||
| FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> | ||
|
ioannisj marked this conversation as resolved.
|
||
| // The completion runs later, on Play Services' Tasks executor — outside this | ||
| // try/catch's stack frame, so an uncaught throw here would crash the host app. | ||
| try { | ||
| if (task.isSuccessful) { | ||
| val token = task.result | ||
| if (!token.isNullOrBlank()) { | ||
| onToken(token, projectId) | ||
| } else { | ||
| config.logger.log("Firebase returned a blank push token, skipping registration.") | ||
| } | ||
| } else { | ||
| config.logger.log("Failed to fetch Firebase push token: ${task.exception}.") | ||
| } | ||
| } catch (e: Throwable) { | ||
| config.logger.log("Failed to handle Firebase push token completion: $e.") | ||
| } | ||
| } | ||
| } catch (e: Throwable) { | ||
| config.logger.log("Failed to request Firebase push token: $e.") | ||
| } | ||
|
ioannisj marked this conversation as resolved.
|
||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.