diff --git a/changelog.d/5234.bugfix b/changelog.d/5234.bugfix new file mode 100644 index 0000000000..2b5d4dee37 --- /dev/null +++ b/changelog.d/5234.bugfix @@ -0,0 +1 @@ +Analytics: Fixes missing use case identity values from within the onboarding flow \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/di/NamedGlobalScope.kt b/vector/src/main/java/im/vector/app/core/di/NamedGlobalScope.kt new file mode 100644 index 0000000000..cc1ac829a1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/di/NamedGlobalScope.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt index b4bee417b4..a5575ef536 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -29,11 +29,13 @@ import dagger.hilt.components.SingletonComponent import im.vector.app.BuildConfig import im.vector.app.EmojiCompatWrapper import im.vector.app.EmojiSpanify +import im.vector.app.config.analyticsConfig import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.time.Clock 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.VectorAnalytics 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.SupervisorJob import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.MatrixConfiguration @@ -159,4 +162,16 @@ object VectorStaticModule { fun providesCoroutineDispatchers(): CoroutineDispatchers { 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 + } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index 6dbf412d83..7b653ef44b 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -16,19 +16,18 @@ package im.vector.app.features.analytics.impl -import android.content.Context import com.posthog.android.Options import com.posthog.android.PostHog import com.posthog.android.Properties -import im.vector.app.BuildConfig -import im.vector.app.config.analyticsConfig +import im.vector.app.core.di.NamedGlobalScope +import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.log.analyticsTag import im.vector.app.features.analytics.plan.UserProperties 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.launchIn import kotlinx.coroutines.flow.onEach @@ -41,15 +40,30 @@ private val IGNORED_OPTIONS: Options? = null @Singleton class DefaultVectorAnalytics @Inject constructor( - private val context: Context, - private val analyticsStore: AnalyticsStore + postHogFactory: PostHogFactory, + analyticsConfig: AnalyticsConfig, + private val analyticsStore: AnalyticsStore, + private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, + @NamedGlobalScope private val globalScope: CoroutineScope ) : 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 private var userConsent: Boolean? = null private var analyticsId: String? = null + override fun init() { + observeUserConsent() + observeAnalyticsId() + } + override fun getUserConsent(): Flow { return analyticsStore.userConsentFlow } @@ -82,13 +96,6 @@ class DefaultVectorAnalytics @Inject constructor( setAnalyticsId("") } - override fun init() { - observeUserConsent() - observeAnalyticsId() - createAnalyticsClient() - } - - @Suppress("EXPERIMENTAL_API_USAGE") private fun observeAnalyticsId() { getAnalyticsId() .onEach { id -> @@ -96,21 +103,20 @@ class DefaultVectorAnalytics @Inject constructor( analyticsId = id identifyPostHog() } - .launchIn(GlobalScope) + .launchIn(globalScope) } - private fun identifyPostHog() { + private suspend fun identifyPostHog() { val id = analyticsId ?: return if (id.isEmpty()) { Timber.tag(analyticsTag.value).d("reset") posthog?.reset() } else { 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() { getUserConsent() .onEach { consent -> @@ -118,49 +124,13 @@ class DefaultVectorAnalytics @Inject constructor( userConsent = consent optOutPostHog() } - .launchIn(GlobalScope) + .launchIn(globalScope) } private fun optOutPostHog() { 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) { Timber.tag(analyticsTag.value).d("capture($event)") posthog diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactory.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactory.kt new file mode 100644 index 0000000000..d961ceaadc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactory.kt @@ -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()) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt new file mode 100644 index 0000000000..029732f76c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt @@ -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 + } + } +} diff --git a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt new file mode 100644 index 0000000000..b17c1a8bba --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt @@ -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) } + } +} diff --git a/vector/src/test/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactoryTest.kt b/vector/src/test/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactoryTest.kt new file mode 100644 index 0000000000..c2fa50f789 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactoryTest.kt @@ -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 + ) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionDataSource.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionDataSource.kt new file mode 100644 index 0000000000..4dab6daf3b --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionDataSource.kt @@ -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)) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAnalyticsStore.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAnalyticsStore.kt new file mode 100644 index 0000000000..6304da8d37 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAnalyticsStore.kt @@ -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() + private val _idFlow = MutableSharedFlow() + + val instance = mockk(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) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLateInitUserPropertiesFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLateInitUserPropertiesFactory.kt new file mode 100644 index 0000000000..9b442ece73 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLateInitUserPropertiesFactory.kt @@ -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() + + fun givenCreatesProperties(userProperties: UserProperties?) { + coEvery { instance.createUserProperties() } returns userProperties + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt new file mode 100644 index 0000000000..e14f809e66 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt @@ -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 { + every { thread } returns Thread.currentThread() + } + every { Looper.getMainLooper() } returns looper + } + + val instance = mockk(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()) + } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt new file mode 100644 index 0000000000..1d18c97d32 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt @@ -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().also { + every { it.createPosthog() } returns postHog + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt index 91403b3b2c..a23c43b986 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt @@ -16,8 +16,12 @@ 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 io.mockk.coEvery import io.mockk.mockk +import io.mockk.mockkStatic import org.matrix.android.sdk.api.session.Session class FakeSession( @@ -25,7 +29,19 @@ class FakeSession( val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService() ) : Session by mockk(relaxed = true) { + init { + mockkStatic("im.vector.app.core.extensions.SessionKt") + } + override fun cryptoService() = fakeCryptoService override val sharedSecretStorageService = fakeSharedSecretStorageService override val coroutineDispatchers = testCoroutineDispatchers + + fun givenVectorStore(vectorSessionStore: VectorSessionStore) { + coEvery { + this@FakeSession.vectorStore(any()) + } coAnswers { + vectorSessionStore + } + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorStore.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorStore.kt new file mode 100644 index 0000000000..22a4a5f6cf --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorStore.kt @@ -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() + + fun givenUseCase(useCase: FtueUseCase?) { + coEvery { + instance.readUseCase() + } coAnswers { + useCase + } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt new file mode 100644 index 0000000000..5fbcdd98d1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt @@ -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 + } +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/UserPropertiesFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/UserPropertiesFixture.kt new file mode 100644 index 0000000000..5a911e2bc9 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fixtures/UserPropertiesFixture.kt @@ -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 +) diff --git a/vector/src/test/java/im/vector/app/test/fixtures/VectorAnalyticsFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/VectorAnalyticsFixture.kt new file mode 100644 index 0000000000..95590b0a44 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fixtures/VectorAnalyticsFixture.kt @@ -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? = null +) = object : VectorAnalyticsScreen { + override fun getName() = name + override fun getProperties() = properties +} + +fun aVectorAnalyticsEvent( + name: String = "an-event-name", + properties: Map? = null +) = object : VectorAnalyticsEvent { + override fun getName() = name + override fun getProperties() = properties +}