Skip to content

Commit

Permalink
feat: include persistent visitor id in context (#130)
Browse files Browse the repository at this point in the history
* feat: include persistent visitor id in context

* fix the fake shared preferences

* fixup! fix the fake shared preferences

---------

Co-authored-by: vahid torkaman <vahidt@spotify.com>
  • Loading branch information
nicklasl and vahidlazio authored Apr 30, 2024
1 parent 2fc83b2 commit 5d4ff21
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 4 deletions.
6 changes: 5 additions & 1 deletion Provider/src/main/java/com/spotify/confidence/Confidence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ class Confidence internal constructor(
}
}

internal const val VISITOR_ID_CONTEXT_KEY = "visitorId"

object ConfidenceFactory {
fun create(
context: Context,
Expand Down Expand Up @@ -152,6 +154,8 @@ object ConfidenceFactory {
flagResolver = flagResolver,
diskStorage = FileDiskStorage.create(context),
flagApplierClient = flagApplierClient
)
).apply {
putContext(VISITOR_ID_CONTEXT_KEY, ConfidenceValue.String(VisitorUtil.getId(context)))
}
}
}
22 changes: 22 additions & 0 deletions Provider/src/main/java/com/spotify/confidence/VisitorUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.spotify.confidence

import android.content.Context
import java.util.UUID

internal const val SHARED_PREFS_NAME = "confidence-visitor"
internal const val VISITOR_ID_SHARED_PREFS_KEY = "visitorId"
internal const val DEFAULT_VALUE = "unable-to-read"

internal object VisitorUtil {
fun getId(context: Context): String {
return with(context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)) {
if (contains(VISITOR_ID_SHARED_PREFS_KEY)) {
getString(VISITOR_ID_SHARED_PREFS_KEY, DEFAULT_VALUE) ?: DEFAULT_VALUE
} else {
val visitorId = UUID.randomUUID().toString()
edit().putString(VISITOR_ID_SHARED_PREFS_KEY, visitorId).apply()
visitorId
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class ConfidenceIntegrationTests {
fun setup() {
whenever(mockContext.filesDir).thenReturn(Files.createTempDirectory("tmpTests").toFile())
whenever(mockContext.getDir(any(), any())).thenReturn(Files.createTempDirectory("events").toFile())
whenever(mockContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)).thenReturn(InMemorySharedPreferences())
}

@Test
Expand All @@ -49,7 +50,7 @@ class ConfidenceIntegrationTests {

val storedValue = 10

val context = ImmutableContext(
val evalMap = ImmutableContext(
targetingKey = UUID.randomUUID().toString(),
attributes = mutableMapOf(
"user" to Value.Structure(
Expand All @@ -60,6 +61,11 @@ class ConfidenceIntegrationTests {
)
)

// we do create a confidence object to have the visitor id injected into the context
val oldConfidence = ConfidenceFactory.create(mockContext, clientSecret)
oldConfidence.putContext(evalMap.toConfidenceContext().map)
val context = oldConfidence.getContext()

val storage = FileDiskStorage.create(mockContext).apply {
val flags = listOf(
ResolvedFlag(
Expand All @@ -70,13 +76,14 @@ class ConfidenceIntegrationTests {
)
)

store(FlagResolution(context.toConfidenceContext().map, flags, resolveToken))
store(FlagResolution(context, flags, resolveToken))
}

val eventsHandler = EventHandler(Dispatchers.IO).apply {
publish(OpenFeatureEvents.ProviderStale)
}
val mockConfidence = ConfidenceFactory.create(mockContext, clientSecret)
mockConfidence.getContext()
OpenFeatureAPI.setProvider(
ConfidenceFeatureProvider.create(
context = mockContext,
Expand All @@ -85,7 +92,7 @@ class ConfidenceIntegrationTests {
initialisationStrategy = InitialisationStrategy.ActivateAndFetchAsync,
eventHandler = eventsHandler
),
context
evalMap
)
runBlocking {
awaitProviderReady(eventsHandler = eventsHandler)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.spotify.confidence

import android.content.Context
import android.content.SharedPreferences
import com.spotify.confidence.client.SdkMetadata
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
Expand All @@ -10,13 +11,17 @@ import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import java.io.File
import java.nio.file.Files

private const val clientSecret = "WciJVLIEiNnRxV8gaYPZNCFF8vbAXOu6"
private val mockContext: Context = mock()
private val mockSharedPrefs: SharedPreferences = mock()
private val mockSharedPrefsEdit: SharedPreferences.Editor = mock()

@OptIn(ExperimentalCoroutinesApi::class)
class EventSenderIntegrationTest {
Expand All @@ -27,12 +32,29 @@ class EventSenderIntegrationTest {
@Before
fun setup() {
whenever(mockContext.getDir("events", Context.MODE_PRIVATE)).thenReturn(directory)
whenever(mockContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)).thenReturn(mockSharedPrefs)
whenever(mockSharedPrefs.edit()).thenReturn(mockSharedPrefsEdit)
whenever(mockSharedPrefsEdit.putString(any(), any())).thenReturn(mockSharedPrefsEdit)
doNothing().whenever(mockSharedPrefsEdit).apply()
eventSender = null
for (file in directory.walkFiles()) {
file.delete()
}
}

@Test
fun created_event_sender_has_visitor_id_context() = runTest {
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
eventSender = ConfidenceFactory.create(
mockContext,
clientSecret,
dispatcher = testDispatcher
)
val context = eventSender?.getContext()
Assert.assertNotNull(context)
Assert.assertTrue(context!!.containsKey(VISITOR_ID_CONTEXT_KEY))
}

@Test
fun emitting_an_event_writes_to_file() = runTest {
val eventStorage = EventStorageImpl(mockContext)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.spotify.confidence

import android.content.SharedPreferences

internal class InMemorySharedPreferences : SharedPreferences {
private var visitorId: String = ""
override fun getAll(): MutableMap<String, *> {
TODO("Not yet implemented")
}

override fun getString(key: String?, default: String?): String? =
if (key == VISITOR_ID_SHARED_PREFS_KEY) {
visitorId
} else {
default
}

override fun getStringSet(p0: String?, p1: MutableSet<String>?): MutableSet<String>? {
TODO("Not yet implemented")
}

override fun getInt(p0: String?, p1: Int): Int {
TODO("Not yet implemented")
}

override fun getLong(p0: String?, p1: Long): Long {
TODO("Not yet implemented")
}

override fun getFloat(p0: String?, p1: Float): Float {
TODO("Not yet implemented")
}

override fun getBoolean(p0: String?, p1: Boolean): Boolean {
TODO("Not yet implemented")
}

override fun contains(key: String?) = if (key == VISITOR_ID_SHARED_PREFS_KEY) {
visitorId.isNotEmpty()
} else {
false
}

override fun edit(): SharedPreferences.Editor = object : SharedPreferences.Editor {
private var visitorId: String = ""
override fun putString(key: String?, value: String?): SharedPreferences.Editor {
if (key == VISITOR_ID_SHARED_PREFS_KEY) {
visitorId = value ?: ""
}
return this
}

override fun putStringSet(p0: String?, p1: MutableSet<String>?): SharedPreferences.Editor {
TODO("Not yet implemented")
}

override fun putInt(p0: String?, p1: Int): SharedPreferences.Editor {
TODO("Not yet implemented")
}

override fun putLong(p0: String?, p1: Long): SharedPreferences.Editor {
TODO("Not yet implemented")
}

override fun putFloat(p0: String?, p1: Float): SharedPreferences.Editor {
TODO("Not yet implemented")
}

override fun putBoolean(p0: String?, p1: Boolean): SharedPreferences.Editor {
TODO("Not yet implemented")
}

override fun remove(p0: String?): SharedPreferences.Editor {
TODO("Not yet implemented")
}

override fun clear(): SharedPreferences.Editor {
TODO("Not yet implemented")
}

override fun commit(): Boolean {
TODO("Not yet implemented")
}

override fun apply() {
this@InMemorySharedPreferences.visitorId = this.visitorId
}
}

override fun registerOnSharedPreferenceChangeListener(p0: SharedPreferences.OnSharedPreferenceChangeListener?) {
TODO("Not yet implemented")
}

override fun unregisterOnSharedPreferenceChangeListener(p0: SharedPreferences.OnSharedPreferenceChangeListener?) {
TODO("Not yet implemented")
}
}

0 comments on commit 5d4ff21

Please sign in to comment.