Skip to content

Commit

Permalink
Groups are cached in the disk (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Oct 12, 2023
1 parent 9f906fe commit c8a714b
Show file tree
Hide file tree
Showing 19 changed files with 171 additions and 67 deletions.
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

0 comments on commit c8a714b

Please sign in to comment.