Skip to content
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

Groups are cached in the disk #48

Merged
merged 6 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Next

- SDK only sends the `$feature_flag_called` event once per flag ([#47](https://github.com/PostHog/posthog-android/pull/47))
- Groups are cached in the disk ([#48](https://github.com/PostHog/posthog-android/pull/48))

## 3.0.0-alpha.7 - 2023-10-10

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.content.Context
import com.posthog.PostHog
import com.posthog.PostHogIntegration
import com.posthog.android.PostHogAndroidConfig
import com.posthog.internal.PostHogPreferences.Companion.BUILD
import com.posthog.internal.PostHogPreferences.Companion.VERSION

/**
* Captures app installed and updated events
Expand All @@ -20,8 +22,8 @@ internal class PostHogAppInstallIntegration(
val versionName = packageInfo.versionName
val versionCode = packageInfo.versionCodeCompat()

val previousVersion = preferences.getValue("version") as? String
var previousBuild = preferences.getValue("build")
val previousVersion = preferences.getValue(VERSION) as? String
var previousBuild = preferences.getValue(BUILD)

val event: String
val props = mutableMapOf<String, Any>()
Expand All @@ -47,8 +49,8 @@ internal class PostHogAppInstallIntegration(
props["version"] = versionName
props["build"] = versionCode

preferences.setValue("version", versionName)
preferences.setValue("build", versionCode)
preferences.setValue(VERSION, versionName)
preferences.setValue(BUILD, versionCode)

PostHog.capture(event, properties = props)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import com.posthog.android.PostHogAndroidConfig
import com.posthog.internal.PostHogPreferences
import com.posthog.internal.PostHogPreferences.Companion.GROUPS

/**
* Reads and writes to the SDKs shared preferences
Expand All @@ -23,11 +24,24 @@ internal class PostHogSharedPreferences(
private val lock = Any()

override fun getValue(key: String, defaultValue: Any?): Any? {
val defValue: Any?
val value: Any?
synchronized(lock) {
defValue = sharedPreferences.all[key] ?: defaultValue
value = sharedPreferences.all[key] ?: defaultValue
}

return when (value) {
is String -> {
// we only want to deserialize special keys
if (SPECIAL_KEYS.contains(key)) {
deserializeObject(value)
} else {
value
}
}
else -> {
value
}
}
return defValue
}

override fun setValue(key: String, value: Any) {
Expand Down Expand Up @@ -59,18 +73,18 @@ internal class PostHogSharedPreferences(
(value.toSet() as? Set<String>)?.let {
edit.putStringSet(key, it)
} ?: run {
config.logger.log("Value type: ${value.javaClass.name} and value: $value isn't valid.")
serializeObject(key, value, edit)
}
}
is Array<*> -> {
@Suppress("UNCHECKED_CAST")
(value.toSet() as? Set<String>)?.let {
edit.putStringSet(key, it)
} ?: run {
config.logger.log("Value type: ${value.javaClass.name} and value: $value isn't valid.")
serializeObject(key, value, edit)
}
} else -> {
config.logger.log("Value type: ${value.javaClass.name} and value: $value isn't valid.")
serializeObject(key, value, edit)
}
}

Expand All @@ -94,6 +108,27 @@ internal class PostHogSharedPreferences(
}
}

private fun serializeObject(key: String, value: Any, editor: SharedPreferences.Editor) {
try {
config.serializer.serializeObject(value)?.let {
editor.putString(key, it)
}
} catch (e: Throwable) {
config.logger.log("Value type: ${value.javaClass.name} and value: $value isn't valid.")
}
}

private fun deserializeObject(value: String): Any {
try {
config.serializer.deserializeString(value)?.let {
// only return the deserialized object if it's not null otherwise fallback
// to the original (and stringified) value
return it
}
} catch (ignored: Throwable) { }
return value
}

override fun remove(key: String) {
val edit = sharedPreferences.edit()
synchronized(lock) {
Expand All @@ -103,11 +138,15 @@ internal class PostHogSharedPreferences(
}

override fun getAll(): Map<String, Any> {
val props: Map<String, Any>
val preferences: Map<String, Any>
synchronized(lock) {
@Suppress("UNCHECKED_CAST")
props = sharedPreferences.all.toMap() as? Map<String, Any> ?: emptyMap()
preferences = sharedPreferences.all.toMap() as? Map<String, Any> ?: emptyMap()
}
return props
return preferences
}

companion object {
private val SPECIAL_KEYS = listOf(GROUPS)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.posthog.android.FakeSharedPreferences
import com.posthog.android.PostHogAndroidConfig
import com.posthog.android.apiKey
import com.posthog.internal.PostHogPreferences.Companion.GROUPS
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import kotlin.test.Test
Expand Down Expand Up @@ -94,12 +95,33 @@ internal class PostHogSharedPreferencesTests {
}

@Test
fun `preferences does not set a non valid type`() {
fun `preferences stringify a non valid type`() {
val sut = getSut()

sut.setValue("key", Any())

assertNull(sut.getValue("key"))
assertEquals("{}", sut.getValue("key"))
}

@Test
fun `preferences deserialize groups`() {
val sut = getSut()

val props = mapOf("key" to "value")
sut.setValue(GROUPS, props)

assertEquals(props, sut.getValue(GROUPS))
}

@Test
fun `preferences fallback to stringified version if not special key`() {
val sut = getSut()

val props = mapOf("key" to "value")
sut.setValue("key", props)

val json = """{"key":"value"}"""
assertEquals(json, sut.getValue("key"))
}

@Test
Expand Down
18 changes: 18 additions & 0 deletions posthog/api/posthog.api
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public class com/posthog/PostHogConfig {
public final fun getSdkName ()Ljava/lang/String;
public final fun getSdkVersion ()Ljava/lang/String;
public final fun getSendFeatureFlagEvent ()Z
public final fun getSerializer ()Lcom/posthog/internal/PostHogSerializer;
public final fun getStoragePrefix ()Ljava/lang/String;
public final fun removeIntegration (Lcom/posthog/PostHogIntegration;)V
public final fun setCachePreferences (Lcom/posthog/internal/PostHogPreferences;)V
Expand Down Expand Up @@ -206,13 +207,23 @@ public abstract interface class com/posthog/internal/PostHogNetworkStatus {
}

public abstract interface class com/posthog/internal/PostHogPreferences {
public static final field BUILD Ljava/lang/String;
public static final field Companion Lcom/posthog/internal/PostHogPreferences$Companion;
public static final field GROUPS Ljava/lang/String;
public static final field VERSION Ljava/lang/String;
public abstract fun clear (Ljava/util/List;)V
public abstract fun getAll ()Ljava/util/Map;
public abstract fun getValue (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
public abstract fun remove (Ljava/lang/String;)V
public abstract fun setValue (Ljava/lang/String;Ljava/lang/Object;)V
}

public final class com/posthog/internal/PostHogPreferences$Companion {
public static final field BUILD Ljava/lang/String;
public static final field GROUPS Ljava/lang/String;
public static final field VERSION Ljava/lang/String;
}

public final class com/posthog/internal/PostHogPreferences$DefaultImpls {
public static synthetic fun clear$default (Lcom/posthog/internal/PostHogPreferences;Ljava/util/List;ILjava/lang/Object;)V
public static synthetic fun getValue$default (Lcom/posthog/internal/PostHogPreferences;Ljava/lang/String;Ljava/lang/Object;ILjava/lang/Object;)Ljava/lang/Object;
Expand All @@ -224,3 +235,10 @@ public final class com/posthog/internal/PostHogPrintLogger : com/posthog/interna
public fun log (Ljava/lang/String;)V
}

public final class com/posthog/internal/PostHogSerializer {
public fun <init> (Lcom/posthog/PostHogConfig;)V
public final fun deserializeString (Ljava/lang/String;)Ljava/lang/Object;
public final fun getGson ()Lcom/google/gson/Gson;
public final fun serializeObject (Ljava/lang/Object;)Ljava/lang/String;
}

47 changes: 29 additions & 18 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import com.posthog.internal.PostHogApi
import com.posthog.internal.PostHogCalendarDateProvider
import com.posthog.internal.PostHogFeatureFlags
import com.posthog.internal.PostHogMemoryPreferences
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.GROUPS
import com.posthog.internal.PostHogPreferences.Companion.OPT_OUT
import com.posthog.internal.PostHogPreferences.Companion.VERSION
import com.posthog.internal.PostHogQueue
import com.posthog.internal.PostHogSendCachedEventsIntegration
import com.posthog.internal.PostHogSerializer
Expand Down Expand Up @@ -53,15 +59,14 @@ public class PostHog private constructor(

val cachePreferences = config.cachePreferences ?: PostHogMemoryPreferences()
config.cachePreferences = cachePreferences
val serializer = PostHogSerializer(config)
val dateProvider = PostHogCalendarDateProvider()
val api = PostHogApi(config, serializer, dateProvider)
val queue = PostHogQueue(config, api, serializer, dateProvider, queueExecutor)
val featureFlags = PostHogFeatureFlags(config, serializer, api, featureFlagsExecutor)
val api = PostHogApi(config, dateProvider)
val queue = PostHogQueue(config, api, dateProvider, queueExecutor)
val featureFlags = PostHogFeatureFlags(config, api, featureFlagsExecutor)

// no need to lock optOut here since the setup is locked already
val optOut = config.cachePreferences?.getValue(
"opt-out",
OPT_OUT,
defaultValue = config.optOut,
) as? Boolean
optOut?.let {
Expand All @@ -72,7 +77,6 @@ public class PostHog private constructor(
val sendCachedEventsIntegration = PostHogSendCachedEventsIntegration(
config,
api,
serializer,
startDate,
cachedEventsExecutor,
)
Expand All @@ -83,7 +87,7 @@ public class PostHog private constructor(

config.addIntegration(sendCachedEventsIntegration)

legacyPreferences(config, serializer)
legacyPreferences(config, config.serializer)

enabled = true

Expand Down Expand Up @@ -160,7 +164,7 @@ public class PostHog private constructor(
get() {
var anonymousId: String?
synchronized(anonymousLock) {
anonymousId = config?.cachePreferences?.getValue("anonymousId") as? String
anonymousId = config?.cachePreferences?.getValue(ANONYMOUS_ID) as? String
if (anonymousId == null) {
anonymousId = UUID.randomUUID().toString()
this.anonymousId = anonymousId ?: ""
Expand All @@ -169,18 +173,18 @@ public class PostHog private constructor(
return anonymousId ?: ""
}
set(value) {
config?.cachePreferences?.setValue("anonymousId", value)
config?.cachePreferences?.setValue(ANONYMOUS_ID, value)
}

private var distinctId: String
get() {
return config?.cachePreferences?.getValue(
"distinctId",
DISTINCT_ID,
defaultValue = anonymousId,
) as? String ?: ""
}
set(value) {
config?.cachePreferences?.setValue("distinctId", value)
config?.cachePreferences?.setValue(DISTINCT_ID, value)
}

private fun buildProperties(
Expand Down Expand Up @@ -290,7 +294,7 @@ public class PostHog private constructor(

synchronized(lockOptOut) {
config?.optOut = false
config?.cachePreferences?.setValue("opt-out", false)
config?.cachePreferences?.setValue(OPT_OUT, false)
}
}

Expand All @@ -301,7 +305,7 @@ public class PostHog private constructor(

synchronized(lockOptOut) {
config?.optOut = true
config?.cachePreferences?.setValue("opt-out", true)
config?.cachePreferences?.setValue(OPT_OUT, true)
}
}

Expand Down Expand Up @@ -397,8 +401,11 @@ public class PostHog private constructor(
props["\$group_set"] = it
}

// just defensive, if there's no cachePreferences, we fallback to in memory
val preferences = config?.cachePreferences ?: memoryPreferences

@Suppress("UNCHECKED_CAST")
val groups = memoryPreferences.getValue("\$groups") as? Map<String, Any>
val groups = preferences.getValue(GROUPS) as? Map<String, Any>
val newGroups = mutableMapOf<String, Any>()
var reloadFeatureFlags = false

Expand All @@ -415,7 +422,7 @@ public class PostHog private constructor(

capture("\$groupidentify", properties = props)

memoryPreferences.setValue("\$groups", newGroups)
preferences.setValue(GROUPS, newGroups)

if (reloadFeatureFlags) {
loadFeatureFlagsRequest(null)
Expand All @@ -434,8 +441,11 @@ public class PostHog private constructor(
props["\$anon_distinct_id"] = anonymousId
props["distinct_id"] = distinctId

// just defensive, if there's no config.cachePreferences, we fallback to in memory
val preferences = config?.cachePreferences ?: memoryPreferences

@Suppress("UNCHECKED_CAST")
val groups = memoryPreferences.getValue("\$groups") as? Map<String, Any>
val groups = preferences.getValue(GROUPS) as? Map<String, Any>

featureFlags?.loadFeatureFlags(distinctId, anonymousId, groups, onFeatureFlags)
}
Expand Down Expand Up @@ -495,10 +505,11 @@ public class PostHog private constructor(
return
}

memoryPreferences.clear()
// only remove properties, preserve BUILD and VERSION keys in order to to fix over-sending
// of 'Application Installed' events and under-sending of 'Application Updated' events
config?.cachePreferences?.clear(except = listOf("build", "version"))
val except = listOf(VERSION, BUILD)
memoryPreferences.clear(except = except)
config?.cachePreferences?.clear(except = except)
featureFlags?.clear()
queue?.clear()
featureFlagsCalled.clear()
Expand Down
6 changes: 6 additions & 0 deletions posthog/src/main/java/com/posthog/PostHogConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.posthog.internal.PostHogLogger
import com.posthog.internal.PostHogNetworkStatus
import com.posthog.internal.PostHogPreferences
import com.posthog.internal.PostHogPrintLogger
import com.posthog.internal.PostHogSerializer

/**
* The SDK Config
Expand Down Expand Up @@ -93,6 +94,11 @@ public open class PostHogConfig(
@PostHogInternal
public var logger: PostHogLogger = PostHogPrintLogger(this)

@PostHogInternal
public val serializer: PostHogSerializer by lazy {
PostHogSerializer(this)
}

@PostHogInternal
public var context: PostHogContext? = null

Expand Down
Loading