From 580ecc9c44c05d16187ad6700c28b24b5a6e7bca Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 15 Feb 2022 12:19:39 +0000 Subject: [PATCH 1/7] adding lateinit user property factory for use when calling the initial identify tracking - this will allow us send the pending onboarding properties once consent is given --- .../impl/LateInitUserPropertiesFactory.kt | 36 +++++++++ .../impl/LateInitUserPropertiesFactoryTest.kt | 73 +++++++++++++++++++ .../test/fakes/FakeActiveSessionDataSource.kt | 30 ++++++++ .../im/vector/app/test/fakes/FakeSession.kt | 16 ++++ .../vector/app/test/fakes/FakeVectorStore.kt | 34 +++++++++ 5 files changed, 189 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactory.kt create mode 100644 vector/src/test/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactoryTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionDataSource.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeVectorStore.kt 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/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/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 + } + } +} From f1f8f518052af3259f2d611cee15945f069287cb Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 15 Feb 2022 12:26:08 +0000 Subject: [PATCH 2/7] lifting the global scope to a provide to allow for unit testing the analytics impl --- .../im/vector/app/core/di/NamedGlobalScope.kt | 23 +++++++++++++++++++ .../im/vector/app/core/di/SingletonModule.kt | 8 +++++++ .../analytics/impl/DefaultVectorAnalytics.kt | 12 +++++----- 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/di/NamedGlobalScope.kt 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 0e19cd4388..94d8df3692 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 @@ -46,6 +46,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.auth.AuthenticationService @@ -147,4 +148,11 @@ object VectorStaticModule { fun providesCoroutineDispatchers(): CoroutineDispatchers { return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default) } + + @Suppress("EXPERIMENTAL_API_USAGE") + @Provides + @NamedGlobalScope + fun providesGlobalScope(): CoroutineScope { + return GlobalScope + } } 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..3694ef7e09 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 @@ -22,13 +22,14 @@ 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.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 @@ -42,7 +43,8 @@ private val IGNORED_OPTIONS: Options? = null @Singleton class DefaultVectorAnalytics @Inject constructor( private val context: Context, - private val analyticsStore: AnalyticsStore + private val analyticsStore: AnalyticsStore, + @NamedGlobalScope private val globalScope: CoroutineScope ) : VectorAnalytics { private var posthog: PostHog? = null @@ -88,7 +90,6 @@ class DefaultVectorAnalytics @Inject constructor( createAnalyticsClient() } - @Suppress("EXPERIMENTAL_API_USAGE") private fun observeAnalyticsId() { getAnalyticsId() .onEach { id -> @@ -96,7 +97,7 @@ class DefaultVectorAnalytics @Inject constructor( analyticsId = id identifyPostHog() } - .launchIn(GlobalScope) + .launchIn(globalScope) } private fun identifyPostHog() { @@ -110,7 +111,6 @@ class DefaultVectorAnalytics @Inject constructor( } } - @Suppress("EXPERIMENTAL_API_USAGE") private fun observeUserConsent() { getUserConsent() .onEach { consent -> @@ -118,7 +118,7 @@ class DefaultVectorAnalytics @Inject constructor( userConsent = consent optOutPostHog() } - .launchIn(GlobalScope) + .launchIn(globalScope) } private fun optOutPostHog() { From 837caabcec07ec31cb2efeed5b1f57da94bb7310 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 15 Feb 2022 12:51:21 +0000 Subject: [PATCH 3/7] providing the posthog creation and analytics config via hilt in order to make the analytics impl testable --- .../im/vector/app/core/di/SingletonModule.kt | 7 +++ .../analytics/impl/DefaultVectorAnalytics.kt | 33 ++---------- .../features/analytics/impl/PostHogFactory.kt | 52 +++++++++++++++++++ 3 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt 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 94d8df3692..56ae63a682 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 @@ -28,11 +28,13 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent 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 @@ -155,4 +157,9 @@ object VectorStaticModule { 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 3694ef7e09..2cbdbf5b5f 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,13 +16,11 @@ 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 @@ -42,8 +40,9 @@ private val IGNORED_OPTIONS: Options? = null @Singleton class DefaultVectorAnalytics @Inject constructor( - private val context: Context, + private val postHogFactory: PostHogFactory, private val analyticsStore: AnalyticsStore, + private val analyticsConfig: AnalyticsConfig, @NamedGlobalScope private val globalScope: CoroutineScope ) : VectorAnalytics { private var posthog: PostHog? = null @@ -85,9 +84,9 @@ class DefaultVectorAnalytics @Inject constructor( } override fun init() { + createAnalyticsClient() observeUserConsent() observeAnalyticsId() - createAnalyticsClient() } private fun observeAnalyticsId() { @@ -133,34 +132,12 @@ class DefaultVectorAnalytics @Inject constructor( 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() + posthog = postHogFactory.createPosthog() 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/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 + } + } +} From e36e67c54cbc82e3cd15c5117a7c91a374176bcc Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 15 Feb 2022 14:23:26 +0000 Subject: [PATCH 4/7] adding unit tests around the analytics impl --- .../analytics/impl/DefaultVectorAnalytics.kt | 4 + .../impl/DefaultVectorAnalyticsTest.kt | 144 ++++++++++++++++++ .../app/test/fakes/FakeAnalyticsStore.kt | 58 +++++++ .../im/vector/app/test/fakes/FakePostHog.kt | 75 +++++++++ .../app/test/fakes/FakePostHogFactory.kt | 28 ++++ .../test/fixtures/AnalyticsConfigFixture.kt | 33 ++++ 6 files changed, 342 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeAnalyticsStore.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt create mode 100644 vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt 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 2cbdbf5b5f..37649d7c39 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 @@ -113,6 +113,7 @@ class DefaultVectorAnalytics @Inject constructor( private fun observeUserConsent() { getUserConsent() .onEach { consent -> + println("!!!, got consent: $consent") Timber.tag(analyticsTag.value).d("User consent updated to $consent") userConsent = consent optOutPostHog() @@ -147,6 +148,9 @@ class DefaultVectorAnalytics @Inject constructor( override fun screen(screen: VectorAnalyticsScreen) { Timber.tag(analyticsTag.value).d("screen($screen)") + + println("userconsnet: $userConsent") + posthog ?.takeIf { userConsent == true } ?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties()) 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..2680979d7e --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt @@ -0,0 +1,144 @@ +/* + * 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.FakePostHog +import im.vector.app.test.fakes.FakePostHogFactory +import im.vector.app.test.fixtures.AnalyticsConfigFixture.anAnalyticsConfig +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 = object : VectorAnalyticsScreen { + override fun getName() = "a-screen-event-name" + override fun getProperties() = mapOf("property-name" to "property-value") +} +private val AN_EVENT = object : VectorAnalyticsEvent { + override fun getName() = "an-event-name" + override fun getProperties() = mapOf("property-name" to "property-value") +} + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultVectorAnalyticsTest { + + private val fakePostHog = FakePostHog() + private val fakeAnalyticsStore = FakeAnalyticsStore() + + private val defaultVectorAnalytics = DefaultVectorAnalytics( + postHogFactory = FakePostHogFactory(fakePostHog.instance).instance, + analyticsStore = fakeAnalyticsStore.instance, + globalScope = CoroutineScope(Dispatchers.Unconfined), + analyticsConfig = anAnalyticsConfig(isEnabled = true) + ) + + @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 `when valid analytics id updates then identify`() = runBlockingTest { + fakeAnalyticsStore.givenAnalyticsId(AN_ANALYTICS_ID) + + fakePostHog.verifyIdentifies(AN_ANALYTICS_ID) + } + + @Test + fun `when signing out analytics id updates then resets`() = 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(), Properties().also { + it.putAll(A_SCREEN_EVENT.getProperties()) + }) + } + + @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(), Properties().also { + it.putAll(AN_EVENT.getProperties()) + }) + } + + @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() + } +} 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/FakePostHog.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt new file mode 100644 index 0000000000..631e09aada --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt @@ -0,0 +1,75 @@ +/* + * 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 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) { + verify { instance.identify(analyticsId) } + } + + 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/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 + } +} From 3236d87323c63b76b46c96e8697a780548c9ea47 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 15 Feb 2022 15:39:05 +0000 Subject: [PATCH 5/7] inlining posthog creation and removing eager optout and identify calls as the backing values are null which means they aren't actually being called --- .../analytics/impl/DefaultVectorAnalytics.kt | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) 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 37649d7c39..1ccdbd8bbc 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 @@ -40,17 +40,29 @@ private val IGNORED_OPTIONS: Options? = null @Singleton class DefaultVectorAnalytics @Inject constructor( - private val postHogFactory: PostHogFactory, + postHogFactory: PostHogFactory, + analyticsConfig: AnalyticsConfig, private val analyticsStore: AnalyticsStore, - private val analyticsConfig: AnalyticsConfig, @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 } @@ -83,12 +95,6 @@ class DefaultVectorAnalytics @Inject constructor( setAnalyticsId("") } - override fun init() { - createAnalyticsClient() - observeUserConsent() - observeAnalyticsId() - } - private fun observeAnalyticsId() { getAnalyticsId() .onEach { id -> @@ -113,7 +119,6 @@ class DefaultVectorAnalytics @Inject constructor( private fun observeUserConsent() { getUserConsent() .onEach { consent -> - println("!!!, got consent: $consent") Timber.tag(analyticsTag.value).d("User consent updated to $consent") userConsent = consent optOutPostHog() @@ -125,20 +130,6 @@ class DefaultVectorAnalytics @Inject constructor( 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 = postHogFactory.createPosthog() - - optOutPostHog() - identifyPostHog() - } - override fun capture(event: VectorAnalyticsEvent) { Timber.tag(analyticsTag.value).d("capture($event)") posthog @@ -148,9 +139,6 @@ class DefaultVectorAnalytics @Inject constructor( override fun screen(screen: VectorAnalyticsScreen) { Timber.tag(analyticsTag.value).d("screen($screen)") - - println("userconsnet: $userConsent") - posthog ?.takeIf { userConsent == true } ?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties()) From d99a2f8d14d4c19d2c53c22b9f019d945744c9df Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 15 Feb 2022 16:03:21 +0000 Subject: [PATCH 6/7] creating and passing stored user properties on post hog initialisation - this allows information captured during the onboarding to be sent once the user has opt'd in --- .../analytics/impl/DefaultVectorAnalytics.kt | 5 +- .../impl/DefaultVectorAnalyticsTest.kt | 47 ++++++++++++------- .../FakeLateInitUserPropertiesFactory.kt | 31 ++++++++++++ .../im/vector/app/test/fakes/FakePostHog.kt | 14 ++++-- .../test/fixtures/UserPropertiesFixture.kt | 26 ++++++++++ .../test/fixtures/VectorAnalyticsFixture.kt | 36 ++++++++++++++ 6 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeLateInitUserPropertiesFactory.kt create mode 100644 vector/src/test/java/im/vector/app/test/fixtures/UserPropertiesFixture.kt create mode 100644 vector/src/test/java/im/vector/app/test/fixtures/VectorAnalyticsFixture.kt 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 1ccdbd8bbc..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 @@ -43,6 +43,7 @@ class DefaultVectorAnalytics @Inject constructor( postHogFactory: PostHogFactory, analyticsConfig: AnalyticsConfig, private val analyticsStore: AnalyticsStore, + private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, @NamedGlobalScope private val globalScope: CoroutineScope ) : VectorAnalytics { @@ -105,14 +106,14 @@ class DefaultVectorAnalytics @Inject constructor( .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) } } 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 index 2680979d7e..b17c1a8bba 100644 --- 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 @@ -20,9 +20,13 @@ 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 @@ -31,26 +35,23 @@ import org.junit.Before import org.junit.Test private const val AN_ANALYTICS_ID = "analytics-id" -private val A_SCREEN_EVENT = object : VectorAnalyticsScreen { - override fun getName() = "a-screen-event-name" - override fun getProperties() = mapOf("property-name" to "property-value") -} -private val AN_EVENT = object : VectorAnalyticsEvent { - override fun getName() = "an-event-name" - override fun getProperties() = mapOf("property-name" to "property-value") -} +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) + analyticsConfig = anAnalyticsConfig(isEnabled = true), + lateInitUserPropertiesFactory = fakeLateInitUserPropertiesFactory.instance ) @Before @@ -87,14 +88,16 @@ class DefaultVectorAnalyticsTest { } @Test - fun `when valid analytics id updates then identify`() = runBlockingTest { + 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) + fakePostHog.verifyIdentifies(AN_ANALYTICS_ID, A_LATE_INIT_USER_PROPERTIES) } @Test - fun `when signing out analytics id updates then resets`() = runBlockingTest { + fun `when signing out then resets posthog`() = runBlockingTest { fakeAnalyticsStore.allowSettingAnalyticsIdToCallBackingFlow() defaultVectorAnalytics.onSignOut() @@ -108,9 +111,7 @@ class DefaultVectorAnalyticsTest { defaultVectorAnalytics.screen(A_SCREEN_EVENT) - fakePostHog.verifyScreenTracked(A_SCREEN_EVENT.getName(), Properties().also { - it.putAll(A_SCREEN_EVENT.getProperties()) - }) + fakePostHog.verifyScreenTracked(A_SCREEN_EVENT.getName(), A_SCREEN_EVENT.toPostHogProperties()) } @Test @@ -128,9 +129,7 @@ class DefaultVectorAnalyticsTest { defaultVectorAnalytics.capture(AN_EVENT) - fakePostHog.verifyEventTracked(AN_EVENT.getName(), Properties().also { - it.putAll(AN_EVENT.getProperties()) - }) + fakePostHog.verifyEventTracked(AN_EVENT.getName(), AN_EVENT.toPostHogProperties()) } @Test @@ -142,3 +141,15 @@ class DefaultVectorAnalyticsTest { 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/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 index 631e09aada..e14f809e66 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt @@ -19,6 +19,7 @@ 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 @@ -41,15 +42,20 @@ class FakePostHog { verify { instance.optOut(optedOut) } } - fun verifyIdentifies(analyticsId: String) { - verify { instance.identify(analyticsId) } + 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) { + fun verifyScreenTracked(name: String, properties: Properties?) { verify { instance.screen(name, properties) } } @@ -61,7 +67,7 @@ class FakePostHog { } } - fun verifyEventTracked(name: String, properties: Properties) { + fun verifyEventTracked(name: String, properties: Properties?) { verify { instance.capture(name, properties) } } 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 +} From 3c226002a03f8c472fd2b955cd614f08f818d070 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 15 Feb 2022 16:24:22 +0000 Subject: [PATCH 7/7] adding changelog entry --- changelog.d/5234.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5234.bugfix 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