Merge pull request #5239 from vector-im/feature/adm/missing-pre-consent-identity-values

Fixing missing identify properties
This commit is contained in:
Benoit Marty 2022-02-17 16:35:15 +01:00 committed by GitHub
commit 6784caab9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 753 additions and 55 deletions

1
changelog.d/5234.bugfix Normal file
View File

@ -0,0 +1 @@
Analytics: Fixes missing use case identity values from within the onboarding flow

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.di
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class NamedGlobalScope

View File

@ -29,11 +29,13 @@ import dagger.hilt.components.SingletonComponent
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.EmojiCompatWrapper import im.vector.app.EmojiCompatWrapper
import im.vector.app.EmojiSpanify import im.vector.app.EmojiSpanify
import im.vector.app.config.analyticsConfig
import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.dispatchers.CoroutineDispatchers
import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock import im.vector.app.core.time.DefaultClock
import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
@ -48,6 +50,7 @@ import im.vector.app.features.ui.SharedPreferencesUiStateRepository
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixConfiguration
@ -159,4 +162,16 @@ object VectorStaticModule {
fun providesCoroutineDispatchers(): CoroutineDispatchers { fun providesCoroutineDispatchers(): CoroutineDispatchers {
return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default) return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default)
} }
@Suppress("EXPERIMENTAL_API_USAGE")
@Provides
@NamedGlobalScope
fun providesGlobalScope(): CoroutineScope {
return GlobalScope
}
@Provides
fun providesAnalyticsConfig(): AnalyticsConfig {
return analyticsConfig
}
} }

View File

@ -16,19 +16,18 @@
package im.vector.app.features.analytics.impl package im.vector.app.features.analytics.impl
import android.content.Context
import com.posthog.android.Options import com.posthog.android.Options
import com.posthog.android.PostHog import com.posthog.android.PostHog
import com.posthog.android.Properties import com.posthog.android.Properties
import im.vector.app.BuildConfig import im.vector.app.core.di.NamedGlobalScope
import im.vector.app.config.analyticsConfig import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import im.vector.app.features.analytics.log.analyticsTag import im.vector.app.features.analytics.log.analyticsTag
import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.analytics.store.AnalyticsStore import im.vector.app.features.analytics.store.AnalyticsStore
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -41,15 +40,30 @@ private val IGNORED_OPTIONS: Options? = null
@Singleton @Singleton
class DefaultVectorAnalytics @Inject constructor( class DefaultVectorAnalytics @Inject constructor(
private val context: Context, postHogFactory: PostHogFactory,
private val analyticsStore: AnalyticsStore analyticsConfig: AnalyticsConfig,
private val analyticsStore: AnalyticsStore,
private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory,
@NamedGlobalScope private val globalScope: CoroutineScope
) : VectorAnalytics { ) : VectorAnalytics {
private var posthog: PostHog? = null
private val posthog: PostHog? = when {
analyticsConfig.isEnabled -> postHogFactory.createPosthog()
else -> {
Timber.tag(analyticsTag.value).w("Analytics is disabled")
null
}
}
// Cache for the store values // Cache for the store values
private var userConsent: Boolean? = null private var userConsent: Boolean? = null
private var analyticsId: String? = null private var analyticsId: String? = null
override fun init() {
observeUserConsent()
observeAnalyticsId()
}
override fun getUserConsent(): Flow<Boolean> { override fun getUserConsent(): Flow<Boolean> {
return analyticsStore.userConsentFlow return analyticsStore.userConsentFlow
} }
@ -82,13 +96,6 @@ class DefaultVectorAnalytics @Inject constructor(
setAnalyticsId("") setAnalyticsId("")
} }
override fun init() {
observeUserConsent()
observeAnalyticsId()
createAnalyticsClient()
}
@Suppress("EXPERIMENTAL_API_USAGE")
private fun observeAnalyticsId() { private fun observeAnalyticsId() {
getAnalyticsId() getAnalyticsId()
.onEach { id -> .onEach { id ->
@ -96,21 +103,20 @@ class DefaultVectorAnalytics @Inject constructor(
analyticsId = id analyticsId = id
identifyPostHog() identifyPostHog()
} }
.launchIn(GlobalScope) .launchIn(globalScope)
} }
private fun identifyPostHog() { private suspend fun identifyPostHog() {
val id = analyticsId ?: return val id = analyticsId ?: return
if (id.isEmpty()) { if (id.isEmpty()) {
Timber.tag(analyticsTag.value).d("reset") Timber.tag(analyticsTag.value).d("reset")
posthog?.reset() posthog?.reset()
} else { } else {
Timber.tag(analyticsTag.value).d("identify") Timber.tag(analyticsTag.value).d("identify")
posthog?.identify(id) posthog?.identify(id, lateInitUserPropertiesFactory.createUserProperties()?.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS)
} }
} }
@Suppress("EXPERIMENTAL_API_USAGE")
private fun observeUserConsent() { private fun observeUserConsent() {
getUserConsent() getUserConsent()
.onEach { consent -> .onEach { consent ->
@ -118,49 +124,13 @@ class DefaultVectorAnalytics @Inject constructor(
userConsent = consent userConsent = consent
optOutPostHog() optOutPostHog()
} }
.launchIn(GlobalScope) .launchIn(globalScope)
} }
private fun optOutPostHog() { private fun optOutPostHog() {
userConsent?.let { posthog?.optOut(!it) } userConsent?.let { posthog?.optOut(!it) }
} }
private fun createAnalyticsClient() {
Timber.tag(analyticsTag.value).d("createAnalyticsClient()")
if (analyticsConfig.isEnabled.not()) {
Timber.tag(analyticsTag.value).w("Analytics is disabled")
return
}
posthog = PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost)
// Record certain application events automatically! (off/false by default)
// .captureApplicationLifecycleEvents()
// Record screen views automatically! (off/false by default)
// .recordScreenViews()
// Capture deep links as part of the screen call. (off by default)
// .captureDeepLinks()
// Maximum number of events to keep in queue before flushing (default 20)
// .flushQueueSize(20)
// Max delay before flushing the queue (30 seconds)
// .flushInterval(30, TimeUnit.SECONDS)
// Enable or disable collection of ANDROID_ID (true)
.collectDeviceId(false)
.logLevel(getLogLevel())
.build()
optOutPostHog()
identifyPostHog()
}
private fun getLogLevel(): PostHog.LogLevel {
return if (BuildConfig.DEBUG) {
PostHog.LogLevel.DEBUG
} else {
PostHog.LogLevel.INFO
}
}
override fun capture(event: VectorAnalyticsEvent) { override fun capture(event: VectorAnalyticsEvent) {
Timber.tag(analyticsTag.value).d("capture($event)") Timber.tag(analyticsTag.value).d("capture($event)")
posthog posthog

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.analytics.impl
import android.content.Context
import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.extensions.vectorStore
import im.vector.app.features.analytics.extensions.toTrackingValue
import im.vector.app.features.analytics.plan.UserProperties
import javax.inject.Inject
class LateInitUserPropertiesFactory @Inject constructor(
private val activeSessionDataSource: ActiveSessionDataSource,
private val context: Context,
) {
suspend fun createUserProperties(): UserProperties? {
val useCase = activeSessionDataSource.currentValue?.orNull()?.vectorStore(context)?.readUseCase()
return useCase?.let {
UserProperties(ftueUseCaseSelection = it.toTrackingValue())
}
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.analytics.impl
import android.content.Context
import com.posthog.android.PostHog
import im.vector.app.BuildConfig
import im.vector.app.config.analyticsConfig
import javax.inject.Inject
class PostHogFactory @Inject constructor(private val context: Context) {
fun createPosthog(): PostHog {
return PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost)
// Record certain application events automatically! (off/false by default)
// .captureApplicationLifecycleEvents()
// Record screen views automatically! (off/false by default)
// .recordScreenViews()
// Capture deep links as part of the screen call. (off by default)
// .captureDeepLinks()
// Maximum number of events to keep in queue before flushing (default 20)
// .flushQueueSize(20)
// Max delay before flushing the queue (30 seconds)
// .flushInterval(30, TimeUnit.SECONDS)
// Enable or disable collection of ANDROID_ID (true)
.collectDeviceId(false)
.logLevel(getLogLevel())
.build()
}
private fun getLogLevel(): PostHog.LogLevel {
return if (BuildConfig.DEBUG) {
PostHog.LogLevel.DEBUG
} else {
PostHog.LogLevel.INFO
}
}
}

View File

@ -0,0 +1,155 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.analytics.impl
import com.posthog.android.Properties
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import im.vector.app.test.fakes.FakeAnalyticsStore
import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory
import im.vector.app.test.fakes.FakePostHog
import im.vector.app.test.fakes.FakePostHogFactory
import im.vector.app.test.fixtures.AnalyticsConfigFixture.anAnalyticsConfig
import im.vector.app.test.fixtures.aUserProperties
import im.vector.app.test.fixtures.aVectorAnalyticsEvent
import im.vector.app.test.fixtures.aVectorAnalyticsScreen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Test
private const val AN_ANALYTICS_ID = "analytics-id"
private val A_SCREEN_EVENT = aVectorAnalyticsScreen()
private val AN_EVENT = aVectorAnalyticsEvent()
private val A_LATE_INIT_USER_PROPERTIES = aUserProperties()
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultVectorAnalyticsTest {
private val fakePostHog = FakePostHog()
private val fakeAnalyticsStore = FakeAnalyticsStore()
private val fakeLateInitUserPropertiesFactory = FakeLateInitUserPropertiesFactory()
private val defaultVectorAnalytics = DefaultVectorAnalytics(
postHogFactory = FakePostHogFactory(fakePostHog.instance).instance,
analyticsStore = fakeAnalyticsStore.instance,
globalScope = CoroutineScope(Dispatchers.Unconfined),
analyticsConfig = anAnalyticsConfig(isEnabled = true),
lateInitUserPropertiesFactory = fakeLateInitUserPropertiesFactory.instance
)
@Before
fun setUp() {
defaultVectorAnalytics.init()
}
@Test
fun `when setting user consent then updates analytics store`() = runBlockingTest {
defaultVectorAnalytics.setUserConsent(true)
fakeAnalyticsStore.verifyConsentUpdated(updatedValue = true)
}
@Test
fun `when consenting to analytics then updates posthog opt out to false`() = runBlockingTest {
fakeAnalyticsStore.givenUserContent(consent = true)
fakePostHog.verifyOptOutStatus(optedOut = false)
}
@Test
fun `when revoking consent to analytics then updates posthog opt out to true`() = runBlockingTest {
fakeAnalyticsStore.givenUserContent(consent = false)
fakePostHog.verifyOptOutStatus(optedOut = true)
}
@Test
fun `when setting the analytics id then updates analytics store`() = runBlockingTest {
defaultVectorAnalytics.setAnalyticsId(AN_ANALYTICS_ID)
fakeAnalyticsStore.verifyAnalyticsIdUpdated(updatedValue = AN_ANALYTICS_ID)
}
@Test
fun `given lateinit user properties when valid analytics id updates then identify with lateinit properties`() = runBlockingTest {
fakeLateInitUserPropertiesFactory.givenCreatesProperties(A_LATE_INIT_USER_PROPERTIES)
fakeAnalyticsStore.givenAnalyticsId(AN_ANALYTICS_ID)
fakePostHog.verifyIdentifies(AN_ANALYTICS_ID, A_LATE_INIT_USER_PROPERTIES)
}
@Test
fun `when signing out then resets posthog`() = runBlockingTest {
fakeAnalyticsStore.allowSettingAnalyticsIdToCallBackingFlow()
defaultVectorAnalytics.onSignOut()
fakePostHog.verifyReset()
}
@Test
fun `given user consent when tracking screen events then submits to posthog`() = runBlockingTest {
fakeAnalyticsStore.givenUserContent(consent = true)
defaultVectorAnalytics.screen(A_SCREEN_EVENT)
fakePostHog.verifyScreenTracked(A_SCREEN_EVENT.getName(), A_SCREEN_EVENT.toPostHogProperties())
}
@Test
fun `given user has not consented when tracking screen events then does not track`() = runBlockingTest {
fakeAnalyticsStore.givenUserContent(consent = false)
defaultVectorAnalytics.screen(A_SCREEN_EVENT)
fakePostHog.verifyNoScreenTracking()
}
@Test
fun `given user consent when tracking events then submits to posthog`() = runBlockingTest {
fakeAnalyticsStore.givenUserContent(consent = true)
defaultVectorAnalytics.capture(AN_EVENT)
fakePostHog.verifyEventTracked(AN_EVENT.getName(), AN_EVENT.toPostHogProperties())
}
@Test
fun `given user has not consented when tracking events then does not track`() = runBlockingTest {
fakeAnalyticsStore.givenUserContent(consent = false)
defaultVectorAnalytics.capture(AN_EVENT)
fakePostHog.verifyNoEventTracking()
}
}
private fun VectorAnalyticsScreen.toPostHogProperties(): Properties? {
return getProperties()?.let { properties ->
Properties().also { it.putAll(properties) }
}
}
private fun VectorAnalyticsEvent.toPostHogProperties(): Properties? {
return getProperties()?.let { properties ->
Properties().also { it.putAll(properties) }
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.analytics.impl
import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.onboarding.FtueUseCase
import im.vector.app.test.fakes.FakeActiveSessionDataSource
import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.FakeVectorStore
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
@ExperimentalCoroutinesApi
class LateInitUserPropertiesFactoryTest {
private val fakeActiveSessionDataSource = FakeActiveSessionDataSource()
private val fakeVectorStore = FakeVectorStore()
private val fakeContext = FakeContext()
private val fakeSession = FakeSession().also {
it.givenVectorStore(fakeVectorStore.instance)
}
private val lateInitUserProperties = LateInitUserPropertiesFactory(
fakeActiveSessionDataSource.instance,
fakeContext.instance
)
@Test
fun `given no active session when creating properties then returns null`() = runBlockingTest {
val result = lateInitUserProperties.createUserProperties()
result shouldBeEqualTo null
}
@Test
fun `given no use case set on an active session when creating properties then returns null`() = runBlockingTest {
fakeVectorStore.givenUseCase(null)
fakeSession.givenVectorStore(fakeVectorStore.instance)
fakeActiveSessionDataSource.setActiveSession(fakeSession)
val result = lateInitUserProperties.createUserProperties()
result shouldBeEqualTo null
}
@Test
fun `given use case set on an active session when creating properties then includes the use case`() = runBlockingTest {
fakeVectorStore.givenUseCase(FtueUseCase.TEAMS)
fakeActiveSessionDataSource.setActiveSession(fakeSession)
val result = lateInitUserProperties.createUserProperties()
result shouldBeEqualTo UserProperties(
ftueUseCaseSelection = UserProperties.FtueUseCaseSelection.WorkMessaging
)
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import arrow.core.Option
import im.vector.app.ActiveSessionDataSource
import org.matrix.android.sdk.api.session.Session
class FakeActiveSessionDataSource {
val instance = ActiveSessionDataSource()
fun setActiveSession(session: Session) {
instance.post(Option.just(session))
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import im.vector.app.features.analytics.store.AnalyticsStore
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
class FakeAnalyticsStore {
private val _consentFlow = MutableSharedFlow<Boolean>()
private val _idFlow = MutableSharedFlow<String>()
val instance = mockk<AnalyticsStore>(relaxed = true) {
every { userConsentFlow } returns _consentFlow
every { analyticsIdFlow } returns _idFlow
}
fun allowSettingAnalyticsIdToCallBackingFlow() {
coEvery { instance.setAnalyticsId(any()) } answers {
runBlocking { _idFlow.emit(firstArg()) }
}
}
fun verifyConsentUpdated(updatedValue: Boolean) {
coVerify { instance.setUserConsent(updatedValue) }
}
suspend fun givenUserContent(consent: Boolean) {
_consentFlow.emit(consent)
}
fun verifyAnalyticsIdUpdated(updatedValue: String) {
coVerify { instance.setAnalyticsId(updatedValue) }
}
suspend fun givenAnalyticsId(id: String) {
_idFlow.emit(id)
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import im.vector.app.features.analytics.impl.LateInitUserPropertiesFactory
import im.vector.app.features.analytics.plan.UserProperties
import io.mockk.coEvery
import io.mockk.mockk
class FakeLateInitUserPropertiesFactory {
val instance = mockk<LateInitUserPropertiesFactory>()
fun givenCreatesProperties(userProperties: UserProperties?) {
coEvery { instance.createUserProperties() } returns userProperties
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import android.os.Looper
import com.posthog.android.PostHog
import com.posthog.android.Properties
import im.vector.app.features.analytics.plan.UserProperties
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
class FakePostHog {
init {
// workaround to avoid PostHog.HANDLER failing
mockkStatic(Looper::class)
val looper = mockk<Looper> {
every { thread } returns Thread.currentThread()
}
every { Looper.getMainLooper() } returns looper
}
val instance = mockk<PostHog>(relaxed = true)
fun verifyOptOutStatus(optedOut: Boolean) {
verify { instance.optOut(optedOut) }
}
fun verifyIdentifies(analyticsId: String, userProperties: UserProperties?) {
verify {
val postHogProperties = userProperties?.getProperties()
?.let { rawProperties -> Properties().also { it.putAll(rawProperties) } }
?.takeIf { it.isNotEmpty() }
instance.identify(analyticsId, postHogProperties, null)
}
}
fun verifyReset() {
verify { instance.reset() }
}
fun verifyScreenTracked(name: String, properties: Properties?) {
verify { instance.screen(name, properties) }
}
fun verifyNoScreenTracking() {
verify(exactly = 0) {
instance.screen(any())
instance.screen(any(), any())
instance.screen(any(), any(), any())
}
}
fun verifyEventTracked(name: String, properties: Properties?) {
verify { instance.capture(name, properties) }
}
fun verifyNoEventTracking() {
verify(exactly = 0) {
instance.capture(any())
instance.capture(any(), any())
instance.capture(any(), any(), any())
}
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import com.posthog.android.PostHog
import im.vector.app.features.analytics.impl.PostHogFactory
import io.mockk.every
import io.mockk.mockk
class FakePostHogFactory(postHog: PostHog) {
val instance = mockk<PostHogFactory>().also {
every { it.createPosthog() } returns postHog
}
}

View File

@ -16,8 +16,12 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import im.vector.app.core.extensions.vectorStore
import im.vector.app.features.session.VectorSessionStore
import im.vector.app.test.testCoroutineDispatchers import im.vector.app.test.testCoroutineDispatchers
import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
class FakeSession( class FakeSession(
@ -25,7 +29,19 @@ class FakeSession(
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService() val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService()
) : Session by mockk(relaxed = true) { ) : Session by mockk(relaxed = true) {
init {
mockkStatic("im.vector.app.core.extensions.SessionKt")
}
override fun cryptoService() = fakeCryptoService override fun cryptoService() = fakeCryptoService
override val sharedSecretStorageService = fakeSharedSecretStorageService override val sharedSecretStorageService = fakeSharedSecretStorageService
override val coroutineDispatchers = testCoroutineDispatchers override val coroutineDispatchers = testCoroutineDispatchers
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
coEvery {
this@FakeSession.vectorStore(any())
} coAnswers {
vectorSessionStore
}
}
} }

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import im.vector.app.features.onboarding.FtueUseCase
import im.vector.app.features.session.VectorSessionStore
import io.mockk.coEvery
import io.mockk.mockk
class FakeVectorStore {
val instance = mockk<VectorSessionStore>()
fun givenUseCase(useCase: FtueUseCase?) {
coEvery {
instance.readUseCase()
} coAnswers {
useCase
}
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fixtures
import im.vector.app.features.analytics.AnalyticsConfig
object AnalyticsConfigFixture {
fun anAnalyticsConfig(
isEnabled: Boolean = false,
postHogHost: String = "http://posthog.url",
postHogApiKey: String = "api-key",
policyLink: String = "http://policy.link"
) = object : AnalyticsConfig {
override val isEnabled: Boolean = isEnabled
override val postHogHost = postHogHost
override val postHogApiKey = postHogApiKey
override val policyLink = policyLink
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fixtures
import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.analytics.plan.UserProperties.FtueUseCaseSelection
fun aUserProperties(
ftueUseCase: FtueUseCaseSelection? = FtueUseCaseSelection.Skip
) = UserProperties(
ftueUseCaseSelection = ftueUseCase
)

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fixtures
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
fun aVectorAnalyticsScreen(
name: String = "a-screen-name",
properties: Map<String, Any>? = null
) = object : VectorAnalyticsScreen {
override fun getName() = name
override fun getProperties() = properties
}
fun aVectorAnalyticsEvent(
name: String = "an-event-name",
properties: Map<String, Any>? = null
) = object : VectorAnalyticsEvent {
override fun getName() = name
override fun getProperties() = properties
}