-
Notifications
You must be signed in to change notification settings - Fork 36
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
Changes from all commits
4e28e38
20f5e50
51dcb52
ddb4a2e
ae8a3df
7405f85
9c5fb6b
c59ba16
4c7bcea
4e9a08b
286a45f
e81be16
ff00334
7fb293f
4ef12d3
d2027af
b2398b4
3c67b45
da3b7f2
95cc19f
65d8ff7
8069e27
9ed0b2d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| /build | ||
| /build | ||
| google-services.json |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,7 @@ class MyApp : Application() { | |
| // Demo: | ||
| // val apiKey = "_6SG-F7I1vCuZ-HdJL3VZQqjBlaSb1_20hDPwqMNnGI" | ||
| // ManoelTesting: | ||
| val apiKey = "phc_6lqCaCDCBEWdIGieihq5R2dZpPVbAUFISA75vFZow06" | ||
| val apiKey = "phc_QFbR1y41s5sxnNTZoyKG2NJo2RlsCIWkUfdpawgb40D" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the rotated key, rollback |
||
| // PaulKey | ||
| // val apiKey = "phc_GavhjwMwc75N4HsaLjMTEvH8Kpsz70rZ3N0E9ho89YJ" | ||
| // val config = PostHogAndroidConfig(apiKey, host = "https://3727-86-27-112-156.ngrok-free.app").apply { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,18 +8,23 @@ 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_PROJECT_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 | ||
| import com.posthog.internal.PostHogPreferences.Companion.PERSON_PROCESSING | ||
| import com.posthog.internal.PostHogPreferences.Companion.VERSION | ||
| import com.posthog.internal.PostHogPrintLogger | ||
| import com.posthog.internal.PostHogPushTokenRegistration | ||
| import com.posthog.internal.PostHogQueueInterface | ||
| import com.posthog.internal.PostHogRemoteConfig | ||
| import com.posthog.internal.PostHogSendCachedEventsIntegration | ||
| import com.posthog.internal.PostHogSerializer | ||
| import com.posthog.internal.PostHogSessionManager | ||
| import com.posthog.internal.PostHogThreadFactory | ||
| import com.posthog.internal.executeSafely | ||
| import com.posthog.internal.personPropertiesContext | ||
| import com.posthog.internal.replay.PostHogSessionReplayHandler | ||
| import com.posthog.internal.sortMapRecursively | ||
|
|
@@ -47,6 +52,10 @@ public class PostHog private constructor( | |
| Executors.newSingleThreadScheduledExecutor( | ||
| PostHogThreadFactory("PostHogSendCachedEventsThread"), | ||
| ), | ||
| private val pushTokenExecutor: ExecutorService = | ||
| Executors.newSingleThreadExecutor( | ||
| PostHogThreadFactory("PostHogFCMTokenRegistration"), | ||
| ), | ||
| private val reloadFeatureFlags: Boolean = true, | ||
| ) : PostHogInterface, PostHogStateless() { | ||
| private val anonymousLock = Any() | ||
|
|
@@ -56,9 +65,11 @@ public class PostHog private constructor( | |
|
|
||
| private val featureFlagsCalledLock = Any() | ||
| private val cachedPersonPropertiesLock = Any() | ||
| private var pushTokenRegistration: PostHogPushTokenRegistration? = null | ||
|
|
||
| private var remoteConfig: PostHogRemoteConfig? = null | ||
| private var replayQueue: PostHogQueueInterface? = null | ||
| private lateinit var api: PostHogApi | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd avoid lateinit otherwise you crash if you use it and its not init yet, nullable var is fine |
||
| private val featureFlagsCalled = mutableMapOf<String, MutableList<Any?>>() | ||
|
|
||
| // Used to deduplicate setPersonProperties calls | ||
|
|
@@ -97,25 +108,25 @@ public class PostHog private constructor( | |
|
|
||
| val cachePreferences = config.cachePreferences ?: memoryPreferences | ||
| config.cachePreferences = cachePreferences | ||
| val api = PostHogApi(config) | ||
| this.api = PostHogApi(config) | ||
| val queue = | ||
| config.queueProvider( | ||
| config, | ||
| api, | ||
| this.api, | ||
| PostHogApiEndpoint.BATCH, | ||
| config.storagePrefix, | ||
| queueExecutor, | ||
| ) | ||
| val replayQueue = | ||
| config.queueProvider( | ||
| config, | ||
| api, | ||
| this.api, | ||
| PostHogApiEndpoint.SNAPSHOT, | ||
| config.replayStoragePrefix, | ||
| replayExecutor, | ||
| ) | ||
| val featureFlags = | ||
| config.remoteConfigProvider(config, api, remoteConfigExecutor) { | ||
| config.remoteConfigProvider(config, this.api, remoteConfigExecutor) { | ||
| getDefaultPersonProperties() | ||
| } | ||
|
|
||
|
|
@@ -133,14 +144,20 @@ public class PostHog private constructor( | |
| val sendCachedEventsIntegration = | ||
| PostHogSendCachedEventsIntegration( | ||
| config, | ||
| api, | ||
| this.api, | ||
| startDate, | ||
| cachedEventsExecutor, | ||
| ) | ||
|
|
||
| this.config = config | ||
| this.queue = queue | ||
| this.replayQueue = replayQueue | ||
| this.pushTokenRegistration = | ||
| PostHogPushTokenRegistration( | ||
| config = config, | ||
| api = this.api, | ||
| pushTokenExecutor = pushTokenExecutor, | ||
| ) | ||
|
|
||
| if (featureFlags is PostHogRemoteConfig) { | ||
| this.remoteConfig = featureFlags | ||
|
|
@@ -703,6 +720,9 @@ public class PostHog private constructor( | |
| } | ||
| this.distinctId = distinctId | ||
|
|
||
| // Automatically register stored push token with new distinctId | ||
| registerStoredTokenIfExists() | ||
|
|
||
| // Automatically set person properties for feature flags during identify() call | ||
| setPersonPropertiesForFlagsIfNeeded(userProperties, userPropertiesSetOnce) | ||
|
|
||
|
|
@@ -1148,6 +1168,10 @@ public class PostHog private constructor( | |
| if (config?.reuseAnonymousId == true) { | ||
| except.add(ANONYMOUS_ID) | ||
| } | ||
| // preserve FCM token data so we can re-register it with the new anonymous distinctId | ||
| except.add(FCM_TOKEN) | ||
| except.add(FCM_TOKEN_LAST_UPDATED) | ||
| except.add(FCM_PROJECT_ID) | ||
| getPreferences().clear(except = except.toList()) | ||
| remoteConfig?.clear() | ||
| featureFlagsCalled.clear() | ||
|
|
@@ -1169,6 +1193,9 @@ public class PostHog private constructor( | |
| if (reloadFeatureFlags) { | ||
| reloadFeatureFlags(config?.onFeatureFlags) | ||
| } | ||
|
|
||
| // Automatically register stored push token with new anonymous distinctId after reset | ||
| registerStoredTokenIfExists() | ||
| } | ||
|
|
||
| public override fun register( | ||
|
|
@@ -1223,6 +1250,42 @@ public class PostHog private constructor( | |
| return PostHogSessionManager.isSessionActive() | ||
| } | ||
|
|
||
| override fun registerPushToken( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| token: String, | ||
| fcmProjectId: String, | ||
| callback: PostHogPushTokenCallback?, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| ) { | ||
| if (!isEnabled()) { | ||
| pushTokenExecutor.executeSafely { callback?.onComplete(PostHogPushTokenError.SDK_DISABLED, null) } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You dont need to execute the callback always within the executor, i'd just run the callback on whatever thread it is |
||
| return | ||
| } | ||
|
|
||
| val registration = pushTokenRegistration | ||
| if (registration == null) { | ||
| config?.logger?.log("FCM: registerPushToken failed - config is null") | ||
| pushTokenExecutor.executeSafely { callback?.onComplete(PostHogPushTokenError.CONFIG_NULL, null) } | ||
| return | ||
| } | ||
| val currentDistinctId = distinctId() | ||
| val preferences = getPreferences() | ||
| registration.register(token, fcmProjectId, currentDistinctId, preferences, callback) | ||
| } | ||
|
|
||
| /** | ||
| * Automatically registers stored push token when distinctId changes. | ||
| * This is called internally after identify() and reset() to ensure | ||
| * the push token is associated with the current distinctId. | ||
| */ | ||
| private fun registerStoredTokenIfExists() { | ||
| if (!isEnabled()) { | ||
| return | ||
| } | ||
| pushTokenRegistration?.registerStoredTokenIfExists( | ||
| preferences = getPreferences(), | ||
| distinctId = distinctId(), | ||
| ) | ||
| } | ||
|
|
||
| override fun <T : PostHogConfig> getConfig(): T? { | ||
| @Suppress("UNCHECKED_CAST") | ||
| return super<PostHogStateless>.config as? T | ||
|
|
@@ -1307,6 +1370,8 @@ public class PostHog private constructor( | |
|
|
||
| private val apiKeys = mutableSetOf<String>() | ||
|
|
||
| private const val ONE_HOUR_IN_MILLIS = 60 * 60 * 1000L | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Const usually goes within the companion object in kotlin |
||
|
|
||
| @PostHogVisibleForTesting | ||
| public fun overrideSharedInstance(postHog: PostHogInterface) { | ||
| shared = postHog | ||
|
|
@@ -1336,13 +1401,18 @@ public class PostHog private constructor( | |
| featureFlagsExecutor: ExecutorService, | ||
| cachedEventsExecutor: ExecutorService, | ||
| reloadFeatureFlags: Boolean, | ||
| pushTokenExecutor: ExecutorService = | ||
| Executors.newSingleThreadExecutor( | ||
| PostHogThreadFactory("PostHogFCMTokenRegistration"), | ||
| ), | ||
| ): PostHogInterface { | ||
| val instance = | ||
| PostHog( | ||
| queueExecutor, | ||
| replayExecutor, | ||
| featureFlagsExecutor, | ||
| cachedEventsExecutor, | ||
| pushTokenExecutor, | ||
| reloadFeatureFlags = reloadFeatureFlags, | ||
| ) | ||
| instance.setup(config) | ||
|
|
@@ -1540,5 +1610,13 @@ public class PostHog private constructor( | |
| override fun getSessionId(): UUID? { | ||
| return shared.getSessionId() | ||
| } | ||
|
|
||
| override fun registerPushToken( | ||
| token: String, | ||
| fcmProjectId: String, | ||
| callback: PostHogPushTokenCallback?, | ||
| ) { | ||
| shared.registerPushToken(token, fcmProjectId, callback) | ||
| } | ||
| } | ||
| } | ||
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