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

feat: include persistent visitor id in context #130

Merged
merged 4 commits into from
Apr 30, 2024
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
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)) {
fabriziodemaria marked this conversation as resolved.
Show resolved Hide resolved
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")
}
}
Loading