Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ public class PostHogFake : PostHogInterface {
override fun resetPersonPropertiesForFlags(reloadFeatureFlags: Boolean) {
}

override fun registerPushToken(token: String): Boolean {
return true
}

override fun setGroupPropertiesForFlags(
type: String,
groupProperties: Map<String, Any>,
Expand Down
3 changes: 3 additions & 0 deletions posthog/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Next

### Added
- Allow collecting FCM device token in SDK core ([#396](https://github.com/PostHog/posthog-android/pull/396))

## 6.3.0 - 2025-01-21

- chore: do not capture $set events if user props have not changed ([#375](https://github.com/PostHog/posthog-android/pull/375))
Expand Down
21 changes: 21 additions & 0 deletions posthog/api/posthog.api
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public final class com/posthog/PostHog : com/posthog/PostHogStateless, com/posth
public fun optIn ()V
public fun optOut ()V
public fun register (Ljava/lang/String;Ljava/lang/Object;)V
public fun registerPushToken (Ljava/lang/String;)Z
public fun reloadFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V
public fun reset ()V
public fun resetGroupPropertiesForFlags (Ljava/lang/String;Z)V
Expand Down Expand Up @@ -67,6 +68,7 @@ public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface
public fun optOut ()V
public final fun overrideSharedInstance (Lcom/posthog/PostHogInterface;)V
public fun register (Ljava/lang/String;Ljava/lang/Object;)V
public fun registerPushToken (Ljava/lang/String;)Z
public fun reloadFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V
public fun reset ()V
public fun resetGroupPropertiesForFlags (Ljava/lang/String;Z)V
Expand Down Expand Up @@ -284,6 +286,7 @@ public abstract interface class com/posthog/PostHogInterface : com/posthog/PostH
public abstract fun isSessionActive ()Z
public abstract fun isSessionReplayActive ()Z
public abstract fun register (Ljava/lang/String;Ljava/lang/Object;)V
public abstract fun registerPushToken (Ljava/lang/String;)Z
public abstract fun reloadFeatureFlags (Lcom/posthog/PostHogOnFeatureFlags;)V
public abstract fun reset ()V
public abstract fun resetGroupPropertiesForFlags (Ljava/lang/String;Z)V
Expand Down Expand Up @@ -585,6 +588,7 @@ public final class com/posthog/internal/PostHogApi {
public static synthetic fun flags$default (Lcom/posthog/internal/PostHogApi;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lcom/posthog/internal/PostHogFlagsResponse;
public final fun localEvaluation (Ljava/lang/String;Ljava/lang/String;)Lcom/posthog/internal/LocalEvaluationApiResponse;
public static synthetic fun localEvaluation$default (Lcom/posthog/internal/PostHogApi;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/posthog/internal/LocalEvaluationApiResponse;
public final fun registerPushSubscription (Ljava/lang/String;Ljava/lang/String;)V
public final fun remoteConfig ()Lcom/posthog/internal/PostHogRemoteConfigResponse;
public final fun snapshot (Ljava/util/List;)V
}
Expand Down Expand Up @@ -737,6 +741,23 @@ public final class com/posthog/internal/PostHogPrintLogger : com/posthog/interna
public fun log (Ljava/lang/String;)V
}

public final class com/posthog/internal/PostHogPushSubscriptionRequest {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/posthog/internal/PostHogPushSubscriptionRequest;
public static synthetic fun copy$default (Lcom/posthog/internal/PostHogPushSubscriptionRequest;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/posthog/internal/PostHogPushSubscriptionRequest;
public fun equals (Ljava/lang/Object;)Z
public final fun getApi_key ()Ljava/lang/String;
public final fun getDistinct_id ()Ljava/lang/String;
public final fun getPlatform ()Ljava/lang/String;
public final fun getToken ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public abstract interface class com/posthog/internal/PostHogQueueInterface {
public abstract fun add (Lcom/posthog/PostHogEvent;)V
public abstract fun clear ()V
Expand Down
78 changes: 78 additions & 0 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.posthog.internal.PostHogPreferences.Companion.ALL_INTERNAL_KEYS
import com.posthog.internal.PostHogPreferences.Companion.ANONYMOUS_ID
import com.posthog.internal.PostHogPreferences.Companion.BUILD
import com.posthog.internal.PostHogPreferences.Companion.DISTINCT_ID
import com.posthog.internal.PostHogPreferences.Companion.FCM_TOKEN
import com.posthog.internal.PostHogPreferences.Companion.FCM_TOKEN_LAST_UPDATED
import com.posthog.internal.PostHogPreferences.Companion.GROUPS
import com.posthog.internal.PostHogPreferences.Companion.IS_IDENTIFIED
import com.posthog.internal.PostHogPreferences.Companion.OPT_OUT
Expand Down Expand Up @@ -1222,6 +1224,78 @@ public class PostHog private constructor(
return PostHogSessionManager.isSessionActive()
}

override fun registerPushToken(token: String): Boolean {
if (!isEnabled()) {
return false
}

if (token.isBlank()) {
config?.logger?.log("registerPushToken called with blank token")
return false
}

val config = this.config ?: return false
val preferences = getPreferences()

// Get stored token and last update timestamp
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

// Check if token has changed or if an hour has passed
val tokenChanged = storedToken != token
val shouldUpdate = tokenChanged || (currentTime - lastUpdated >= oneHourInMillis)

if (!shouldUpdate) {
config.logger.log("FCM token registration skipped: token unchanged and less than hour since last update")
return true
}

// 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)
val distinctId = distinctId()

val executor =
Executors.newSingleThreadExecutor(
PostHogThreadFactory("PostHogFCMTokenRegistration"),
)
val future =
executor.submit<Boolean> {
api.registerPushSubscription(distinctId, token)
true
}
try {
val success = future.get(5, java.util.concurrent.TimeUnit.SECONDS)

if (success) {
preferences.setValue(FCM_TOKEN, token)
preferences.setValue(FCM_TOKEN_LAST_UPDATED, currentTime)
config.logger.log("FCM token registered successfully")
}
success
} 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
} finally {
executor.shutdown()
}
} catch (e: Throwable) {
config.logger.log("Failed to register FCM token: $e")
false
}
}

override fun <T : PostHogConfig> getConfig(): T? {
@Suppress("UNCHECKED_CAST")
return super<PostHogStateless>.config as? T
Expand Down Expand Up @@ -1539,5 +1613,9 @@ public class PostHog private constructor(
override fun getSessionId(): UUID? {
return shared.getSessionId()
}

override fun registerPushToken(token: String): Boolean {
return shared.registerPushToken(token)
}
}
}
13 changes: 13 additions & 0 deletions posthog/src/main/java/com/posthog/PostHogInterface.kt
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,19 @@ public interface PostHogInterface : PostHogCoreInterface {
*/
public fun resetPersonPropertiesForFlags(reloadFeatureFlags: Boolean = true)

/**
* Registers a push notification token (FCM token) with PostHog.
* The SDK will automatically rate-limit registrations to once per hour unless the token has changed.
*
* Users should retrieve the FCM token using:
* - Java: `FirebaseMessaging.getInstance().getToken()`
* - Kotlin: `Firebase.messaging.token`
*
* @param token The FCM registration token
* @return true if registration was successful, false otherwise
*/
public fun registerPushToken(token: String): Boolean

/**
* Sets properties for a specific group type to include when evaluating feature flags.
*
Expand Down
29 changes: 29 additions & 0 deletions posthog/src/main/java/com/posthog/internal/PostHogApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,35 @@ public class PostHogApi(
}
}

@Throws(PostHogApiError::class, IOException::class)
public fun registerPushSubscription(
distinctId: String,
token: String,
) {
val pushSubscriptionRequest =
PostHogPushSubscriptionRequest(
api_key = config.apiKey,
distinct_id = distinctId,
token = token,
platform = "android",
)

val url = "$theHost/api/sdk/push_subscriptions/register"

logRequest(pushSubscriptionRequest, url)

val request =
makeRequest(url) {
config.serializer.serialize(pushSubscriptionRequest, it.bufferedWriter())
}

client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw PostHogApiError(response.code, response.message, response.body)
}
}
}

private fun logResponse(response: Response): Response {
if (config.debug) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public interface PostHogPreferences {
public const val VERSION: String = "version"
public const val BUILD: String = "build"
public const val STRINGIFIED_KEYS: String = "stringifiedKeys"
internal const val FCM_TOKEN = "fcmToken"
internal const val FCM_TOKEN_LAST_UPDATED = "fcmTokenLastUpdated"

public val ALL_INTERNAL_KEYS: Set<String> =
setOf(
Expand All @@ -66,6 +68,8 @@ public interface PostHogPreferences {
FLAGS,
PERSON_PROPERTIES_FOR_FLAGS,
GROUP_PROPERTIES_FOR_FLAGS,
FCM_TOKEN,
FCM_TOKEN_LAST_UPDATED,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.posthog.internal

import com.posthog.PostHogInternal

/**
* Request body for push subscription registration
*/
@PostHogInternal
public data class PostHogPushSubscriptionRequest(
val api_key: String,
val distinct_id: String,
val token: String,
val platform: String,
)
Loading
Loading