From b68e9e1f7f344302cc9110916f82268d1ff380a1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 Nov 2021 13:42:46 +0100 Subject: [PATCH 01/49] Analytics: setup the first classes --- .../im/vector/app/core/di/SingletonModule.kt | 5 ++ .../app/features/analytics/VectorAnalytics.kt | 56 ++++++++++++++ .../analytics/impl/DefaultVectorAnalytics.kt | 55 ++++++++++++++ .../analytics/store/AnalyticsStore.kt | 75 +++++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.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 350e1f6b7a..14ed17d0bb 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 @@ -31,6 +31,8 @@ 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.VectorAnalytics +import im.vector.app.features.analytics.impl.DefaultVectorAnalytics import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.CompileTimeAutoAcceptInvites import im.vector.app.features.navigation.DefaultNavigator @@ -57,6 +59,9 @@ abstract class VectorBindModule { @Binds abstract fun bindNavigator(navigator: DefaultNavigator): Navigator + @Binds + abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics + @Binds abstract fun bindErrorFormatter(formatter: DefaultErrorFormatter): ErrorFormatter diff --git a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt new file mode 100644 index 0000000000..55e6147461 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021 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 + +import kotlinx.coroutines.flow.Flow + +interface VectorAnalytics { + /** + * Return a Flow of Boolean, true if the user has given their consent + */ + fun getUserConsent(): Flow + + /** + * Update the user consent value + */ + suspend fun setUserConsent(userConsent: Boolean) + + /** + * Return a Flow of Boolean, true if the user has been asked for their consent + */ + fun didAskUserConsent(): Flow + + /** + * Store the fact that the user has been asked for their consent + */ + suspend fun setDidAskUserConsent(didAskUserConsent: Boolean) + + /** + * Return a Flow of String, used for analytics Id + */ + fun getAnalyticsId(): Flow + + /** + * Update analyticsId from the AccountData + */ + suspend fun setAnalyticsId(analyticsId: String) + + /** + * To be called when a session is destroyed + */ + suspend fun onSignOut() +} 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 new file mode 100644 index 0000000000..7eca77c74f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 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.VectorAnalytics +import im.vector.app.features.analytics.store.AnalyticsStore +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class DefaultVectorAnalytics @Inject constructor( + private val analyticsStore: AnalyticsStore +) : VectorAnalytics { + override fun getUserConsent(): Flow { + return analyticsStore.userConsentFlow + } + + override suspend fun setUserConsent(userConsent: Boolean) { + analyticsStore.setUserConsent(userConsent) + } + + override fun didAskUserConsent(): Flow { + return analyticsStore.didAskUserConsentFlow + } + + override suspend fun setDidAskUserConsent(didAskUserConsent: Boolean) { + analyticsStore.setDidAskUserConsent(didAskUserConsent) + } + + override fun getAnalyticsId(): Flow { + return analyticsStore.analyticsIdFlow + } + + override suspend fun setAnalyticsId(analyticsId: String) { + analyticsStore.setAnalyticsId(analyticsId) + } + + override suspend fun onSignOut() { + // reset the analyticsId + setAnalyticsId("") + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt b/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt new file mode 100644 index 0000000000..b4d93e674f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021 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.store + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.extensions.orFalse +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_analytics") + +/** + * Local storage for: + * - user consent (Boolean) + * - did ask user consent (Boolean) + * - analytics Id (String) + */ +class AnalyticsStore @Inject constructor( + private val context: Context +) { + private val userConsent = booleanPreferencesKey("user_consent") + private val didAskUserConsent = booleanPreferencesKey("did_ask_user_consent") + private val analyticsId = stringPreferencesKey("analytics_id") + + val userConsentFlow: Flow = context.dataStore.data.map { preferences -> + preferences[userConsent].orFalse() + } + + val didAskUserConsentFlow: Flow = context.dataStore.data.map { preferences -> + preferences[didAskUserConsent].orFalse() + } + + val analyticsIdFlow: Flow = context.dataStore.data.map { preferences -> + preferences[analyticsId].orEmpty() + } + + suspend fun setUserConsent(newUserConsent: Boolean) { + context.dataStore.edit { settings -> + settings[userConsent] = newUserConsent + } + } + + suspend fun setDidAskUserConsent(newDidAskUserConsent: Boolean) { + context.dataStore.edit { settings -> + settings[didAskUserConsent] = newDidAskUserConsent + } + } + + suspend fun setAnalyticsId(newAnalyticsId: String) { + context.dataStore.edit { settings -> + settings[analyticsId] = newAnalyticsId + } + } +} From 5606a5bfe7c975f45a26d3d181fe8af9006bb0b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 Nov 2021 14:32:20 +0100 Subject: [PATCH 02/49] Analytics: ask user consent at startup (we may iterate later) --- .../app/core/di/MavericksViewModelModule.kt | 6 ++ .../ui/consent/AnalyticsConsentViewActions.kt | 24 ++++++ .../ui/consent/AnalyticsConsentViewModel.kt | 79 +++++++++++++++++++ .../ui/consent/AnalyticsConsentViewState.kt | 32 ++++++++ .../app/features/login/LoginSplashFragment.kt | 20 +++++ .../main/res/layout/fragment_login_splash.xml | 11 ++- vector/src/main/res/values/strings.xml | 3 + 7 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewActions.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewState.kt diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index cac694e84e..c0a39e2c5a 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -20,6 +20,7 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.multibindings.IntoMap +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel import im.vector.app.features.auth.ReAuthViewModel import im.vector.app.features.call.VectorCallViewModel import im.vector.app.features.call.conference.JitsiCallViewModel @@ -454,6 +455,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(LoginViewModel::class) fun loginViewModelFactory(factory: LoginViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(AnalyticsConsentViewModel::class) + fun analyticsConsentViewModelFactory(factory: AnalyticsConsentViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(HomeServerCapabilitiesViewModel::class) diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewActions.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewActions.kt new file mode 100644 index 0000000000..b43dc4742a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewActions.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 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.ui.consent + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class AnalyticsConsentViewActions : VectorViewModelAction { + data class SetUserConsent(val userConsent: Boolean) : AnalyticsConsentViewActions() + object OnGetStarted : AnalyticsConsentViewActions() +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt new file mode 100644 index 0000000000..00ed86b0ca --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2021 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.ui.consent + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.analytics.VectorAnalytics +import kotlinx.coroutines.launch + +class AnalyticsConsentViewModel @AssistedInject constructor( + @Assisted initialState: AnalyticsConsentViewState, + private val analytics: VectorAnalytics +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: AnalyticsConsentViewState): AnalyticsConsentViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + observeAnalytics() + } + + private fun observeAnalytics() { + analytics.didAskUserConsent().setOnEach { + copy(didAskUserConsent = it) + } + analytics.getUserConsent().setOnEach { + copy(userConsent = it) + } + } + + override fun handle(action: AnalyticsConsentViewActions) { + when (action) { + is AnalyticsConsentViewActions.SetUserConsent -> handleSetUserConsent(action) + AnalyticsConsentViewActions.OnGetStarted -> handleOnScreenLeft() + }.exhaustive + } + + private fun handleSetUserConsent(action: AnalyticsConsentViewActions.SetUserConsent) { + viewModelScope.launch { + analytics.setUserConsent(action.userConsent) + if (!action.userConsent) { + // User explicitly changed the default value, let's avoid reverting to the default value + analytics.setDidAskUserConsent(true) + } + } + } + + private fun handleOnScreenLeft() { + // Whatever the state of the box, consider the user acknowledge it + viewModelScope.launch { + analytics.setDidAskUserConsent(true) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewState.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewState.kt new file mode 100644 index 0000000000..f7496710e2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewState.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 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.ui.consent + +import com.airbnb.mvrx.MavericksState + +data class AnalyticsConsentViewState( + val userConsent: Boolean = false, + val didAskUserConsent: Boolean = false +) : MavericksState { + val shouldCheckTheBox: Boolean = + if (didAskUserConsent) { + userConsent + } else { + // default value + true + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt index 527f9f99b3..d719ab6b31 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt @@ -22,10 +22,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import com.airbnb.mvrx.fragmentViewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.databinding.FragmentLoginSplashBinding +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewActions +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewState import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.failure.Failure import java.net.UnknownHostException @@ -38,6 +42,8 @@ class LoginSplashFragment @Inject constructor( private val vectorPreferences: VectorPreferences ) : AbstractLoginFragment() { + private val analyticsConsentViewModel: AnalyticsConsentViewModel by fragmentViewModel() + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLoginSplashBinding { return FragmentLoginSplashBinding.inflate(inflater, container, false) } @@ -46,10 +52,23 @@ class LoginSplashFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) setupViews() + observeAnalyticsState() + } + + private fun observeAnalyticsState() { + analyticsConsentViewModel.onEach(AnalyticsConsentViewState::shouldCheckTheBox) { + views.loginSplashAnalyticsConsent.isChecked = it + } } private fun setupViews() { views.loginSplashSubmit.debouncedClicks { getStarted() } + // setOnCheckedChangeListener is to annoying since it does not distinguish user changes and code changes + views.loginSplashAnalyticsConsent.setOnClickListener { + analyticsConsentViewModel.handle(AnalyticsConsentViewActions.SetUserConsent( + views.loginSplashAnalyticsConsent.isChecked + )) + } if (BuildConfig.DEBUG || vectorPreferences.developerMode()) { views.loginSplashVersion.isVisible = true @@ -61,6 +80,7 @@ class LoginSplashFragment @Inject constructor( } private fun getStarted() { + analyticsConsentViewModel.handle(AnalyticsConsentViewActions.OnGetStarted) loginViewModel.handle(LoginAction.OnGetStarted(resetLoginConfig = false)) } diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index 1d69cde723..90a33594c3 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -204,9 +204,18 @@ android:layout_height="wrap_content" android:textColor="?vctr_content_secondary" android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/loginSplashAnalyticsConsent" app:layout_constraintStart_toStartOf="parent" tools:text="@string/settings_version" tools:visibility="visible" /> + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ddb731a5e0..85e99edb1a 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1377,6 +1377,9 @@ Please enable analytics to help us improve ${app_name}. Yes, I want to help! + + Send anonymous usage data to element.io + Data save mode Data save mode applies a specific filter so presence updates and typing notifications are filtered out. From 8608230fa00e71dd9a2a3e3e161b92081793d81d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 Nov 2021 15:26:17 +0100 Subject: [PATCH 03/49] Analytics: add config to build.gradle --- .idea/dictionaries/bmarty.xml | 1 + vector/build.gradle | 8 ++++ .../app/features/analytics/AnalyticsConfig.kt | 46 +++++++++++++++++++ .../app/features/login/LoginSplashFragment.kt | 2 + .../main/res/layout/fragment_login_splash.xml | 4 +- 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index e143720aa9..a2e408b50d 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -24,6 +24,7 @@ pbkdf pids pkcs + posthog previewable previewables pstn diff --git a/vector/build.gradle b/vector/build.gradle index ff81c4d721..1b6e81848d 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -233,6 +233,10 @@ android { // Set to true if you want to enable strict mode in debug buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" + // Analytics. Set to empty strings to just disable analytics + buildConfigField "String", "ANALYTICS_POSTHOG_HOST", "\"https://posthog.hss.element.io/\"" + buildConfigField "String", "ANALYTICS_POSTHOG_API_KEY", "\"TODO\"" + signingConfig signingConfigs.debug } @@ -243,6 +247,10 @@ android { buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" + // Analytics. Set to empty strings to just disable analytics + buildConfigField "String", "ANALYTICS_POSTHOG_HOST", "\"https://posthog-poc.lab.element.dev/\"" + buildConfigField "String", "ANALYTICS_POSTHOG_API_KEY", "\"TODO\"" + postprocessing { removeUnusedCode true removeUnusedResources true diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt new file mode 100644 index 0000000000..9a8d2e627d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 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 + +import im.vector.app.BuildConfig +import timber.log.Timber + +data class AnalyticsConfig( + val postHogHost: String, + val postHogApiKey: String +) { + companion object { + /** + * Read the analytics config from the Build config + */ + fun getConfig(): AnalyticsConfig? { + val postHogHost = BuildConfig.ANALYTICS_POSTHOG_HOST.takeIf { it.isNotEmpty() } + ?: return null.also { Timber.w("Analytics is disabled, ANALYTICS_POSTHOG_HOST is empty") } + val postHogApiKey = BuildConfig.ANALYTICS_POSTHOG_API_KEY.takeIf { it.isNotEmpty() } + ?: return null.also { Timber.w("Analytics is disabled, ANALYTICS_POSTHOG_API_KEY is empty") } + + return AnalyticsConfig( + postHogHost = postHogHost, + postHogApiKey = postHogApiKey + ) + } + + fun isAnalyticsEnabled(): Boolean { + return getConfig() != null + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt index d719ab6b31..e045ba11b1 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSplashFragment.kt @@ -27,6 +27,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.databinding.FragmentLoginSplashBinding +import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewActions import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewState @@ -64,6 +65,7 @@ class LoginSplashFragment @Inject constructor( private fun setupViews() { views.loginSplashSubmit.debouncedClicks { getStarted() } // setOnCheckedChangeListener is to annoying since it does not distinguish user changes and code changes + views.loginSplashAnalyticsConsent.isVisible = AnalyticsConfig.isAnalyticsEnabled() views.loginSplashAnalyticsConsent.setOnClickListener { analyticsConsentViewModel.handle(AnalyticsConsentViewActions.SetUserConsent( views.loginSplashAnalyticsConsent.isChecked diff --git a/vector/src/main/res/layout/fragment_login_splash.xml b/vector/src/main/res/layout/fragment_login_splash.xml index 90a33594c3..89a684be01 100644 --- a/vector/src/main/res/layout/fragment_login_splash.xml +++ b/vector/src/main/res/layout/fragment_login_splash.xml @@ -214,8 +214,10 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:text="@string/analytics_consent_splash" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" /> + app:layout_constraintStart_toStartOf="parent" + tools:visibility="visible" /> From b33cddf0e366ae58146871bd9a5449c9fbaabc14 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 Nov 2021 15:34:04 +0100 Subject: [PATCH 04/49] Analytics: add PostHog library --- vector/build.gradle | 3 ++ .../src/main/assets/open_source_licenses.html | 9 +++++ .../java/im/vector/app/VectorApplication.kt | 3 ++ .../app/features/analytics/VectorAnalytics.kt | 5 +++ .../analytics/impl/DefaultVectorAnalytics.kt | 34 +++++++++++++++++++ 5 files changed, 54 insertions(+) diff --git a/vector/build.gradle b/vector/build.gradle index 1b6e81848d..201be09cf6 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -462,6 +462,9 @@ dependencies { implementation libs.dagger.hilt kapt libs.dagger.hiltCompiler + // Analytics + implementation 'com.posthog.android:posthog:1.1.2' + // gplay flavor only gplayImplementation('com.google.firebase:firebase-messaging:23.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 529b7da2f1..29537fc40f 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -262,6 +262,15 @@ SOFTWARE. +
    +
  • + posthog-android +
    + https://github.com/PostHog/posthog-android + PostHog Android integration is licensed under the MIT License +
  • +
+

Apache License
diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 46f155a8d9..5401c23bf1 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -42,6 +42,7 @@ import dagger.hilt.android.HiltAndroidApp import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.startSyncing +import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog @@ -96,6 +97,7 @@ class VectorApplication : @Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var invitesAcceptor: InvitesAcceptor @Inject lateinit var vectorFileLogger: VectorFileLogger + @Inject lateinit var vectorAnalytics: VectorAnalytics // font thread handler private var fontThreadHandler: Handler? = null @@ -113,6 +115,7 @@ class VectorApplication : enableStrictModeIfNeeded() super.onCreate() appContext = this + vectorAnalytics.init() invitesAcceptor.initialize() vectorUncaughtExceptionHandler.activate(this) diff --git a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt index 55e6147461..538508b0de 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt @@ -53,4 +53,9 @@ interface VectorAnalytics { * To be called when a session is destroyed */ suspend fun onSignOut() + + /** + * To be called when application is started + */ + fun init() } 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 7eca77c74f..1d3447efbb 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,14 +16,23 @@ package im.vector.app.features.analytics.impl +import android.content.Context +import com.posthog.android.PostHog +import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.store.AnalyticsStore import kotlinx.coroutines.flow.Flow +import timber.log.Timber import javax.inject.Inject +import javax.inject.Singleton +@Singleton class DefaultVectorAnalytics @Inject constructor( + private val context: Context, private val analyticsStore: AnalyticsStore ) : VectorAnalytics { + private var posthog: PostHog? = null + override fun getUserConsent(): Flow { return analyticsStore.userConsentFlow } @@ -52,4 +61,29 @@ class DefaultVectorAnalytics @Inject constructor( // reset the analyticsId setAnalyticsId("") } + + override fun init() { + val config: AnalyticsConfig = AnalyticsConfig.getConfig() + ?: return Unit.also { Timber.w("Analytics is disabled") } + + posthog = PostHog.Builder(context, config.postHogApiKey, config.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 (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) + .build() + } } From 5c5a547aebed4b4a9cbcd6826d83358e22b6586e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 Nov 2021 16:03:46 +0100 Subject: [PATCH 05/49] Analytics: add capture API --- .../app/features/analytics/VectorAnalytics.kt | 5 +++++ .../analytics/impl/DefaultVectorAnalytics.kt | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt index 538508b0de..b5752942e4 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt @@ -58,4 +58,9 @@ interface VectorAnalytics { * To be called when application is started */ fun init() + + /** + * Capture an Event + */ + fun capture(event: String, properties: Map? = null) } 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 1d3447efbb..09eeb47070 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 @@ -18,6 +18,7 @@ package im.vector.app.features.analytics.impl import android.content.Context import com.posthog.android.PostHog +import com.posthog.android.Properties import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.store.AnalyticsStore @@ -60,6 +61,8 @@ class DefaultVectorAnalytics @Inject constructor( override suspend fun onSignOut() { // reset the analyticsId setAnalyticsId("") + // reset the library + posthog?.reset() } override fun init() { @@ -86,4 +89,15 @@ class DefaultVectorAnalytics @Inject constructor( .collectDeviceId(false) .build() } + + override fun capture(event: String, properties: Map?) { + posthog?.capture( + event, + properties?.let { props -> + Properties().apply { + props.forEach { putValue(it.key, it.value) } + } + } + ) + } } From 995e1e3d49f50fc38ea985a2efd366cd4cafadfd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 Nov 2021 16:08:27 +0100 Subject: [PATCH 06/49] Analytics: add screen API --- .../app/features/analytics/VectorAnalytics.kt | 5 +++++ .../analytics/impl/DefaultVectorAnalytics.kt | 21 ++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt index b5752942e4..4503f4152d 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt @@ -63,4 +63,9 @@ interface VectorAnalytics { * Capture an Event */ fun capture(event: String, properties: Map? = null) + + /** + * Track a displayed screen + */ + fun screen(name: String, properties: Map? = null) } 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 09eeb47070..913f257a32 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 @@ -93,11 +93,22 @@ class DefaultVectorAnalytics @Inject constructor( override fun capture(event: String, properties: Map?) { posthog?.capture( event, - properties?.let { props -> - Properties().apply { - props.forEach { putValue(it.key, it.value) } - } - } + properties.toPostHogProperties() ) } + + override fun screen(name: String, properties: Map?) { + posthog?.screen( + name, + properties.toPostHogProperties() + ) + } + + private fun Map?.toPostHogProperties(): Properties? { + if (this == null) return null + + return Properties().apply { + this@toPostHogProperties.forEach { putValue(it.key, it.value) } + } + } } From 8752fe1e69cdb5f9419284b1838dc88b3df83bd7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 23 Nov 2021 16:18:41 +0100 Subject: [PATCH 07/49] Analytics: observe the store and react --- .../analytics/impl/DefaultVectorAnalytics.kt | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 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 913f257a32..6164e2ded4 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,7 +22,10 @@ import com.posthog.android.Properties import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.store.AnalyticsStore +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -34,6 +37,8 @@ class DefaultVectorAnalytics @Inject constructor( ) : VectorAnalytics { private var posthog: PostHog? = null + private var userConsent: Boolean = false + override fun getUserConsent(): Flow { return analyticsStore.userConsentFlow } @@ -61,8 +66,6 @@ class DefaultVectorAnalytics @Inject constructor( override suspend fun onSignOut() { // reset the analyticsId setAnalyticsId("") - // reset the library - posthog?.reset() } override fun init() { @@ -88,20 +91,43 @@ class DefaultVectorAnalytics @Inject constructor( // Enable or disable collection of ANDROID_ID (true) .collectDeviceId(false) .build() + + observeUserConsent() + observeAnalyticsId() + } + + @Suppress("EXPERIMENTAL_API_USAGE") + private fun observeAnalyticsId() { + GlobalScope.launch { + getAnalyticsId().onEach { + if (it.isEmpty()) { + posthog?.reset() + } else { + posthog?.identify(it) + } + } + } + } + + @Suppress("EXPERIMENTAL_API_USAGE") + private fun observeUserConsent() { + GlobalScope.launch { + getUserConsent().onEach { + userConsent = it + } + } } override fun capture(event: String, properties: Map?) { - posthog?.capture( - event, - properties.toPostHogProperties() - ) + posthog + ?.takeIf { userConsent } + ?.capture(event, properties.toPostHogProperties()) } override fun screen(name: String, properties: Map?) { - posthog?.screen( - name, - properties.toPostHogProperties() - ) + posthog + ?.takeIf { userConsent } + ?.screen(name, properties.toPostHogProperties()) } private fun Map?.toPostHogProperties(): Properties? { From a3173d89e5e0dadfe33f779263a95a069f8efcd8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 09:51:11 +0100 Subject: [PATCH 08/49] Analytics: manage account data --- .../app/core/di/MavericksViewModelModule.kt | 6 + .../AnalyticsAccountDataContent.kt | 35 ++++++ .../AnalyticsAccountDataViewModel.kt | 118 ++++++++++++++++++ .../vector/app/features/home/HomeActivity.kt | 3 + 4 files changed, 162 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataContent.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index c0a39e2c5a..adf1f63110 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -20,6 +20,7 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.multibindings.IntoMap +import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel import im.vector.app.features.auth.ReAuthViewModel import im.vector.app.features.call.VectorCallViewModel @@ -460,6 +461,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(AnalyticsConsentViewModel::class) fun analyticsConsentViewModelFactory(factory: AnalyticsConsentViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(AnalyticsAccountDataViewModel::class) + fun analyticsAccountDataViewModelFactory(factory: AnalyticsAccountDataViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(HomeServerCapabilitiesViewModel::class) diff --git a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataContent.kt b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataContent.kt new file mode 100644 index 0000000000..ecb243dfc8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataContent.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 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.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AnalyticsAccountDataContent( + // A randomly generated analytics token for this user. + // This is suggested to be a 128-bit hex encoded string. + @Json(name = "id") + val id: String? = null, + // Boolean indicating whether the user has opted in. + // If null or not set, the user hasn't yet given consent either way + @Json(name = "pseudonymousAnalyticsOptIn") + val pseudonymousAnalyticsOptIn: Boolean? = null, + // Boolean indicating whether to show the analytics opt-in prompt. + @Json(name = "showPseudonymousAnalyticsPrompt") + val showPseudonymousAnalyticsPrompt: Boolean? = null +) diff --git a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt new file mode 100644 index 0000000000..665a26ce22 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021 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.accountdata + +import androidx.lifecycle.asFlow +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.analytics.VectorAnalytics +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.initsync.SyncStatusService +import org.matrix.android.sdk.flow.flow +import timber.log.Timber +import java.util.UUID + +data class DummyState( + val dummy: Boolean = false +) : MavericksState + +class AnalyticsAccountDataViewModel @AssistedInject constructor( + @Assisted initialState: DummyState, + private val session: Session, + private val analytics: VectorAnalytics +) : VectorViewModel(initialState) { + + private var checkDone: Boolean = false + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: DummyState): AnalyticsAccountDataViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { + private const val ANALYTICS_EVENT_TYPE = "im.vector.analytics"; + } + + init { + observeAccountData() + observeInitSync() + } + + private fun observeInitSync() { + combine( + session.getSyncStatusLive().asFlow(), + analytics.getUserConsent(), + analytics.getAnalyticsId() + ) { status, userConsent, analyticsId -> + if (status is SyncStatusService.Status.IncrementalSyncIdle && + userConsent && + analyticsId.isEmpty() && + !checkDone) { + // Initial sync is over, analytics Id from account data is missing and user has given consent to use analytics + checkDone = true + createAnalyticsAccountData() + } + }.launchIn(viewModelScope) + } + + private fun observeAccountData() { + session.flow() + .liveUserAccountData(setOf(ANALYTICS_EVENT_TYPE)) + .mapNotNull { it.firstOrNull() } + .mapNotNull { it.content.toModel() } + .onEach { analyticsAccountDataContent -> + if (analyticsAccountDataContent.id.isNullOrEmpty()) { + // Probably consent revoked from Element Web + // Ignore here + Timber.d("Consent revoked from Element Web?") + } else { + Timber.d("AnalyticsId has been retrieved") + analytics.setAnalyticsId(analyticsAccountDataContent.id) + } + } + .launchIn(viewModelScope) + } + + override fun handle(action: EmptyAction) { + // No op + } + + private fun createAnalyticsAccountData() { + val content = AnalyticsAccountDataContent( + id = UUID.randomUUID().toString() + ) + + viewModelScope.launch { + session.accountDataService().updateUserAccountData(ANALYTICS_EVENT_TYPE, content.toContent()) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 16c0655d85..b50a3a98a9 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -48,6 +48,7 @@ import im.vector.app.core.pushers.PushersManager import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.navigation.Navigator @@ -103,6 +104,8 @@ class HomeActivity : private lateinit var sharedActionViewModel: HomeSharedActionViewModel private val homeActivityViewModel: HomeActivityViewModel by viewModel() + @Suppress("UNUSED") + private val analyticsAccountDataViewModel: AnalyticsAccountDataViewModel by viewModel() private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() private val promoteRestrictedViewModel: PromoteRestrictedViewModel by viewModel() From 530f4a885145b0052a26724a28ca78643e8840c5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 09:54:26 +0100 Subject: [PATCH 09/49] Analytics: sign out --- vector/src/main/java/im/vector/app/features/MainActivity.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index f4c737c942..acd2a81123 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -32,6 +32,7 @@ import im.vector.app.core.extensions.startSyncing import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.deleteAllFiles import im.vector.app.databinding.ActivityMainBinding +import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.ShortcutsHandler import im.vector.app.features.notifications.NotificationDrawerManager @@ -98,6 +99,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity @Inject lateinit var pinCodeStore: PinCodeStore @Inject lateinit var pinLocker: PinLocker @Inject lateinit var popupAlertManager: PopupAlertManager + @Inject lateinit var vectorAnalytics: VectorAnalytics override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -192,6 +194,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity uiStateRepository.reset() pinLocker.unlock() pinCodeStore.deleteEncodedPin() + vectorAnalytics.onSignOut() } withContext(Dispatchers.IO) { // On BG thread From 4c7ccfb43828ba9f74a6c355cd3e02a09bd6dceb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 11:01:17 +0100 Subject: [PATCH 10/49] Analytics: fix a swap in URL and add API keys --- vector/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index 201be09cf6..f0d79f91f6 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -234,8 +234,8 @@ android { buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" // Analytics. Set to empty strings to just disable analytics - buildConfigField "String", "ANALYTICS_POSTHOG_HOST", "\"https://posthog.hss.element.io/\"" - buildConfigField "String", "ANALYTICS_POSTHOG_API_KEY", "\"TODO\"" + buildConfigField "String", "ANALYTICS_POSTHOG_HOST", "\"https://posthog-poc.lab.element.dev/\"" + buildConfigField "String", "ANALYTICS_POSTHOG_API_KEY", "\"rs-pJjsYJTuAkXJfhaMmPUNBhWliDyTKLOOxike6ck8\"" signingConfig signingConfigs.debug } @@ -248,8 +248,8 @@ android { buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" // Analytics. Set to empty strings to just disable analytics - buildConfigField "String", "ANALYTICS_POSTHOG_HOST", "\"https://posthog-poc.lab.element.dev/\"" - buildConfigField "String", "ANALYTICS_POSTHOG_API_KEY", "\"TODO\"" + buildConfigField "String", "ANALYTICS_POSTHOG_HOST", "\"https://posthog.hss.element.io/\"" + buildConfigField "String", "ANALYTICS_POSTHOG_API_KEY", "\"phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO\"" postprocessing { removeUnusedCode true From 869b5ad55b5a950034715fbe485209d38cfc11ff Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 12:08:07 +0100 Subject: [PATCH 11/49] Analytics: add setting section --- .../analytics/impl/DefaultVectorAnalytics.kt | 3 ++ .../features/settings/VectorPreferences.kt | 23 -------- .../settings/VectorSettingsBaseFragment.kt | 7 ++- .../VectorSettingsSecurityPrivacyFragment.kt | 52 ++++++++++++++----- .../xml/vector_settings_security_privacy.xml | 5 +- 5 files changed, 51 insertions(+), 39 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 6164e2ded4..541c040ed0 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 @@ -24,6 +24,7 @@ import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.store.AnalyticsStore import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber @@ -106,6 +107,7 @@ class DefaultVectorAnalytics @Inject constructor( posthog?.identify(it) } } + .launchIn(GlobalScope) } } @@ -115,6 +117,7 @@ class DefaultVectorAnalytics @Inject constructor( getUserConsent().onEach { userConsent = it } + .launchIn(GlobalScope) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 07cd9d6dac..6887a4f623 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -167,9 +167,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM = "SETTINGS_LABS_SHOW_COMPLETE_HISTORY_IN_ENCRYPTED_ROOM" const val SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB = "SETTINGS_LABS_UNREAD_NOTIFICATIONS_AS_TAB" - // analytics - const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" - // Rageshake const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY" const val SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY" @@ -821,15 +818,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { } } - /** - * Tells if the analytics tracking is authorized (piwik, matomo, etc.). - * - * @return true if the analytics tracking is authorized - */ - fun useAnalytics(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_USE_ANALYTICS_KEY, false) - } - /** * Tells if the user wants to see URL previews in the timeline * @@ -839,17 +827,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_SHOW_URL_PREVIEW_KEY, true) } - /** - * Enable or disable the analytics tracking. - * - * @param useAnalytics true to enable the analytics tracking - */ - fun setUseAnalytics(useAnalytics: Boolean) { - defaultPrefs.edit { - putBoolean(SETTINGS_USE_ANALYTICS_KEY, useAnalytics) - } - } - /** * Tells if media should be previewed before sending * diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt index 0a12a86ff0..c5786b44b0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt @@ -21,6 +21,7 @@ import android.os.Bundle import android.view.View import androidx.annotation.CallSuper import androidx.preference.PreferenceFragmentCompat +import com.airbnb.mvrx.MavericksView import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.error.ErrorFormatter @@ -30,7 +31,7 @@ import im.vector.app.core.utils.toast import org.matrix.android.sdk.api.session.Session import timber.log.Timber -abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() { +abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), MavericksView { val vectorActivity: VectorBaseActivity<*> by lazy { activity as VectorBaseActivity<*> @@ -145,4 +146,8 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat() { .setPositiveButton(R.string.ok, null) .show() } + + override fun invalidate() { + // No op by default + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 438382ab3c..f3a054d46d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -23,6 +23,7 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.view.isVisible @@ -31,6 +32,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.SwitchPreference import androidx.recyclerview.widget.RecyclerView +import com.airbnb.mvrx.fragmentViewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder @@ -43,10 +45,15 @@ import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.platform.SimpleTextWatcher import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreferenceCategory +import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.openFileSelection import im.vector.app.core.utils.toast import im.vector.app.databinding.DialogImportE2eKeysBinding +import im.vector.app.features.analytics.AnalyticsConfig +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewActions +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel +import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewState import im.vector.app.features.crypto.keys.KeysExporter import im.vector.app.features.crypto.keys.KeysImporter import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity @@ -71,7 +78,6 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse import javax.inject.Inject class VectorSettingsSecurityPrivacyFragment @Inject constructor( - private val vectorPreferences: VectorPreferences, private val activeSessionHolder: ActiveSessionHolder, private val pinCodeStore: PinCodeStore, private val keysExporter: KeysExporter, @@ -83,6 +89,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( override var titleRes = R.string.settings_security_and_privacy override val preferenceXmlRes = R.xml.vector_settings_security_privacy + private val analyticsConsentViewModel: AnalyticsConsentViewModel by fragmentViewModel() + // cryptography private val mCryptographyCategory by lazy { findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!! @@ -129,6 +137,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( findPreference("SETTINGS_SECURITY_PIN")!! } + private val analyticsCategory by lazy { + findPreference("SETTINGS_ANALYTICS_PREFERENCE_KEY")!! + } + + private val analyticsConsent by lazy { + findPreference("SETTINGS_USER_ANALYTICS_CONSENT_KEY")!! + } + override fun onCreateRecyclerView(inflater: LayoutInflater?, parent: ViewGroup?, savedInstanceState: Bundle?): RecyclerView { return super.onCreateRecyclerView(inflater, parent, savedInstanceState).also { // Insert animation are really annoying the first time the list is shown @@ -238,18 +254,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( refreshKeysManagementSection() // Analytics + setUpAnalytics() - // Analytics tracking management - findPreference(VectorPreferences.SETTINGS_USE_ANALYTICS_KEY)!!.let { - // On if the analytics tracking is activated - it.isChecked = vectorPreferences.useAnalytics() - - it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - vectorPreferences.setUseAnalytics(newValue as Boolean) - true - } - } - + // Pin code openPinCodeSettingsPref.setOnPreferenceClickListener { openPinCodePreferenceScreen() true @@ -274,6 +281,27 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + observeAnalyticsState() + } + + private fun observeAnalyticsState() { + analyticsConsentViewModel.onEach(AnalyticsConsentViewState::shouldCheckTheBox) { + analyticsConsent.isChecked = it + } + } + + private fun setUpAnalytics() { + analyticsCategory.isVisible = AnalyticsConfig.isAnalyticsEnabled() + + analyticsConsent.setOnPreferenceClickListener { + analyticsConsentViewModel.handle(AnalyticsConsentViewActions.SetUserConsent(analyticsConsent.isChecked)) + true + } + } + // Todo this should be refactored and use same state as 4S section private fun refreshXSigningStatus() { val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys() diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index 5dfde2d1df..97ca51033e 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -104,12 +104,11 @@ + android:title="@string/settings_analytics"> From a8108f2e1734ab848ea075c52bdee55945b61de0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 12:09:15 +0100 Subject: [PATCH 12/49] Analytics: simpler API --- .../java/im/vector/app/features/analytics/VectorAnalytics.kt | 2 +- .../app/features/analytics/impl/DefaultVectorAnalytics.kt | 4 ++-- .../im/vector/app/features/analytics/store/AnalyticsStore.kt | 4 ++-- .../analytics/ui/consent/AnalyticsConsentViewModel.kt | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt index 4503f4152d..ae119561b3 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt @@ -37,7 +37,7 @@ interface VectorAnalytics { /** * Store the fact that the user has been asked for their consent */ - suspend fun setDidAskUserConsent(didAskUserConsent: Boolean) + suspend fun setDidAskUserConsent() /** * Return a Flow of String, used for analytics Id 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 541c040ed0..2b5a7dd544 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 @@ -52,8 +52,8 @@ class DefaultVectorAnalytics @Inject constructor( return analyticsStore.didAskUserConsentFlow } - override suspend fun setDidAskUserConsent(didAskUserConsent: Boolean) { - analyticsStore.setDidAskUserConsent(didAskUserConsent) + override suspend fun setDidAskUserConsent() { + analyticsStore.setDidAskUserConsent() } override fun getAnalyticsId(): Flow { diff --git a/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt b/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt index b4d93e674f..efb824d3db 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt @@ -61,9 +61,9 @@ class AnalyticsStore @Inject constructor( } } - suspend fun setDidAskUserConsent(newDidAskUserConsent: Boolean) { + suspend fun setDidAskUserConsent() { context.dataStore.edit { settings -> - settings[didAskUserConsent] = newDidAskUserConsent + settings[didAskUserConsent] = true } } diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt index 00ed86b0ca..4fdbf08c5f 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsConsentViewModel.kt @@ -65,7 +65,7 @@ class AnalyticsConsentViewModel @AssistedInject constructor( analytics.setUserConsent(action.userConsent) if (!action.userConsent) { // User explicitly changed the default value, let's avoid reverting to the default value - analytics.setDidAskUserConsent(true) + analytics.setDidAskUserConsent() } } } @@ -73,7 +73,7 @@ class AnalyticsConsentViewModel @AssistedInject constructor( private fun handleOnScreenLeft() { // Whatever the state of the box, consider the user acknowledge it viewModelScope.launch { - analytics.setDidAskUserConsent(true) + analytics.setDidAskUserConsent() } } } From 622483cf9f44dcd3dd73e8afc2c633951099e99e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 12:12:11 +0100 Subject: [PATCH 13/49] Analytics: cleanup --- .../analytics/impl/DefaultVectorAnalytics.kt | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 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 2b5a7dd544..0fc0056975 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 @@ -26,7 +26,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -99,26 +98,24 @@ class DefaultVectorAnalytics @Inject constructor( @Suppress("EXPERIMENTAL_API_USAGE") private fun observeAnalyticsId() { - GlobalScope.launch { - getAnalyticsId().onEach { - if (it.isEmpty()) { - posthog?.reset() - } else { - posthog?.identify(it) + getAnalyticsId() + .onEach { id -> + if (id.isEmpty()) { + posthog?.reset() + } else { + posthog?.identify(id) + } } - } - .launchIn(GlobalScope) - } + .launchIn(GlobalScope) } @Suppress("EXPERIMENTAL_API_USAGE") private fun observeUserConsent() { - GlobalScope.launch { - getUserConsent().onEach { - userConsent = it - } - .launchIn(GlobalScope) - } + getUserConsent() + .onEach { consent -> + userConsent = consent + } + .launchIn(GlobalScope) } override fun capture(event: String, properties: Map?) { From 805fcb6bd3082e0b0c8c1cbffbeed12f5e60dc1b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 16:05:06 +0100 Subject: [PATCH 14/49] Analytics: explicitly optOut, maybe useful for stats captured automatically. --- .../vector/app/features/analytics/impl/DefaultVectorAnalytics.kt | 1 + 1 file changed, 1 insertion(+) 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 0fc0056975..31624fc019 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 @@ -114,6 +114,7 @@ class DefaultVectorAnalytics @Inject constructor( getUserConsent() .onEach { consent -> userConsent = consent + posthog?.optOut(!consent) } .launchIn(GlobalScope) } From be2637c4266cd2ef4e3767121900bd8c85a4a8a3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 16:18:58 +0100 Subject: [PATCH 15/49] Analytics: enable some logs --- .../features/analytics/impl/DefaultVectorAnalytics.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 31624fc019..069e2ba8d5 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 @@ -19,6 +19,7 @@ package im.vector.app.features.analytics.impl import android.content.Context import com.posthog.android.PostHog import com.posthog.android.Properties +import im.vector.app.BuildConfig import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.store.AnalyticsStore @@ -90,12 +91,21 @@ class DefaultVectorAnalytics @Inject constructor( // Enable or disable collection of ANDROID_ID (true) .collectDeviceId(false) + .logLevel(getLogLevel()) .build() observeUserConsent() observeAnalyticsId() } + private fun getLogLevel(): PostHog.LogLevel { + return if (BuildConfig.DEBUG) { + PostHog.LogLevel.DEBUG + } else { + PostHog.LogLevel.INFO + } + } + @Suppress("EXPERIMENTAL_API_USAGE") private fun observeAnalyticsId() { getAnalyticsId() From 55c7270ef2af923a4c4b672c3863f1f245ee81d3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 17:05:19 +0100 Subject: [PATCH 16/49] Analytics: Create PostHog client only when user has given their consent --- .../analytics/impl/DefaultVectorAnalytics.kt | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 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 069e2ba8d5..a4f2ce2ca3 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 @@ -70,6 +70,37 @@ class DefaultVectorAnalytics @Inject constructor( } override fun init() { + observeUserConsent() + observeAnalyticsId() + } + + @Suppress("EXPERIMENTAL_API_USAGE") + private fun observeAnalyticsId() { + getAnalyticsId() + .onEach { id -> + if (id.isEmpty()) { + posthog?.reset() + } else { + posthog?.identify(id) + } + } + .launchIn(GlobalScope) + } + + @Suppress("EXPERIMENTAL_API_USAGE") + private fun observeUserConsent() { + getUserConsent() + .onEach { consent -> + userConsent = consent + if (consent) { + createAnalyticsClient() + } + posthog?.optOut(!consent) + } + .launchIn(GlobalScope) + } + + private fun createAnalyticsClient() { val config: AnalyticsConfig = AnalyticsConfig.getConfig() ?: return Unit.also { Timber.w("Analytics is disabled") } @@ -93,9 +124,6 @@ class DefaultVectorAnalytics @Inject constructor( .collectDeviceId(false) .logLevel(getLogLevel()) .build() - - observeUserConsent() - observeAnalyticsId() } private fun getLogLevel(): PostHog.LogLevel { @@ -106,29 +134,6 @@ class DefaultVectorAnalytics @Inject constructor( } } - @Suppress("EXPERIMENTAL_API_USAGE") - private fun observeAnalyticsId() { - getAnalyticsId() - .onEach { id -> - if (id.isEmpty()) { - posthog?.reset() - } else { - posthog?.identify(id) - } - } - .launchIn(GlobalScope) - } - - @Suppress("EXPERIMENTAL_API_USAGE") - private fun observeUserConsent() { - getUserConsent() - .onEach { consent -> - userConsent = consent - posthog?.optOut(!consent) - } - .launchIn(GlobalScope) - } - override fun capture(event: String, properties: Map?) { posthog ?.takeIf { userConsent } From 24a6080090ccb4c6652a70fd1272fe20cf119dc3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 17:16:51 +0100 Subject: [PATCH 17/49] Analytics: Improve logs --- .../app/features/analytics/AnalyticsConfig.kt | 9 ++++++-- .../AnalyticsAccountDataViewModel.kt | 5 +++-- .../analytics/impl/DefaultVectorAnalytics.kt | 14 ++++++++++++- .../analytics/log/AnalyticsLoggerTag.kt | 21 +++++++++++++++++++ 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/log/AnalyticsLoggerTag.kt diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt index 9a8d2e627d..a9045dc325 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsConfig.kt @@ -17,6 +17,7 @@ package im.vector.app.features.analytics import im.vector.app.BuildConfig +import im.vector.app.features.analytics.log.analyticsTag import timber.log.Timber data class AnalyticsConfig( @@ -29,9 +30,13 @@ data class AnalyticsConfig( */ fun getConfig(): AnalyticsConfig? { val postHogHost = BuildConfig.ANALYTICS_POSTHOG_HOST.takeIf { it.isNotEmpty() } - ?: return null.also { Timber.w("Analytics is disabled, ANALYTICS_POSTHOG_HOST is empty") } + ?: return null.also { + Timber.tag(analyticsTag.value).w("Analytics is disabled, ANALYTICS_POSTHOG_HOST is empty") + } val postHogApiKey = BuildConfig.ANALYTICS_POSTHOG_API_KEY.takeIf { it.isNotEmpty() } - ?: return null.also { Timber.w("Analytics is disabled, ANALYTICS_POSTHOG_API_KEY is empty") } + ?: return null.also { + Timber.tag(analyticsTag.value).w("Analytics is disabled, ANALYTICS_POSTHOG_API_KEY is empty") + } return AnalyticsConfig( postHogHost = postHogHost, diff --git a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt index 665a26ce22..89b12cd8c2 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt @@ -28,6 +28,7 @@ import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.features.analytics.log.analyticsTag import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapNotNull @@ -93,9 +94,9 @@ class AnalyticsAccountDataViewModel @AssistedInject constructor( if (analyticsAccountDataContent.id.isNullOrEmpty()) { // Probably consent revoked from Element Web // Ignore here - Timber.d("Consent revoked from Element Web?") + Timber.tag(analyticsTag.value).d("Consent revoked from Element Web?") } else { - Timber.d("AnalyticsId has been retrieved") + Timber.tag(analyticsTag.value).d("AnalyticsId has been retrieved") analytics.setAnalyticsId(analyticsAccountDataContent.id) } } 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 a4f2ce2ca3..3ebe65099c 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,6 +22,7 @@ import com.posthog.android.Properties import im.vector.app.BuildConfig import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.features.analytics.log.analyticsTag import im.vector.app.features.analytics.store.AnalyticsStore import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow @@ -45,6 +46,7 @@ class DefaultVectorAnalytics @Inject constructor( } override suspend fun setUserConsent(userConsent: Boolean) { + Timber.tag(analyticsTag.value).d("setUserConsent($userConsent)") analyticsStore.setUserConsent(userConsent) } @@ -53,6 +55,7 @@ class DefaultVectorAnalytics @Inject constructor( } override suspend fun setDidAskUserConsent() { + Timber.tag(analyticsTag.value).d("setDidAskUserConsent()") analyticsStore.setDidAskUserConsent() } @@ -61,6 +64,7 @@ class DefaultVectorAnalytics @Inject constructor( } override suspend fun setAnalyticsId(analyticsId: String) { + Timber.tag(analyticsTag.value).d("setAnalyticsId($analyticsId)") analyticsStore.setAnalyticsId(analyticsId) } @@ -78,9 +82,12 @@ class DefaultVectorAnalytics @Inject constructor( private fun observeAnalyticsId() { getAnalyticsId() .onEach { id -> + Timber.tag(analyticsTag.value).d("Analytics Id updated to '$id'") if (id.isEmpty()) { + Timber.tag(analyticsTag.value).d("reset") posthog?.reset() } else { + Timber.tag(analyticsTag.value).d("identify") posthog?.identify(id) } } @@ -91,6 +98,7 @@ class DefaultVectorAnalytics @Inject constructor( private fun observeUserConsent() { getUserConsent() .onEach { consent -> + Timber.tag(analyticsTag.value).d("User consent updated to $consent") userConsent = consent if (consent) { createAnalyticsClient() @@ -101,8 +109,10 @@ class DefaultVectorAnalytics @Inject constructor( } private fun createAnalyticsClient() { + Timber.tag(analyticsTag.value).d("createAnalyticsClient()") + val config: AnalyticsConfig = AnalyticsConfig.getConfig() - ?: return Unit.also { Timber.w("Analytics is disabled") } + ?: return Unit.also { Timber.tag(analyticsTag.value).w("Analytics is disabled") } posthog = PostHog.Builder(context, config.postHogApiKey, config.postHogHost) // Record certain application events automatically! (off/false by default) @@ -135,12 +145,14 @@ class DefaultVectorAnalytics @Inject constructor( } override fun capture(event: String, properties: Map?) { + Timber.tag(analyticsTag.value).d("capture($event)") posthog ?.takeIf { userConsent } ?.capture(event, properties.toPostHogProperties()) } override fun screen(name: String, properties: Map?) { + Timber.tag(analyticsTag.value).d("screen($name)") posthog ?.takeIf { userConsent } ?.screen(name, properties.toPostHogProperties()) diff --git a/vector/src/main/java/im/vector/app/features/analytics/log/AnalyticsLoggerTag.kt b/vector/src/main/java/im/vector/app/features/analytics/log/AnalyticsLoggerTag.kt new file mode 100644 index 0000000000..360740b9ce --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/log/AnalyticsLoggerTag.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 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.log + +import org.matrix.android.sdk.api.logger.LoggerTag + +val analyticsTag = LoggerTag("Analytics") From 2968be22334d631fc98ce97cb68901c6a607972e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 17:24:08 +0100 Subject: [PATCH 18/49] Analytics: Fix a race condition --- .../analytics/impl/DefaultVectorAnalytics.kt | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 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 3ebe65099c..4c14432906 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,6 +40,7 @@ class DefaultVectorAnalytics @Inject constructor( private var posthog: PostHog? = null private var userConsent: Boolean = false + private var analyticsId: String? = null override fun getUserConsent(): Flow { return analyticsStore.userConsentFlow @@ -83,17 +84,23 @@ class DefaultVectorAnalytics @Inject constructor( getAnalyticsId() .onEach { id -> Timber.tag(analyticsTag.value).d("Analytics Id updated to '$id'") - if (id.isEmpty()) { - Timber.tag(analyticsTag.value).d("reset") - posthog?.reset() - } else { - Timber.tag(analyticsTag.value).d("identify") - posthog?.identify(id) - } + analyticsId = id + identifyPostHog() } .launchIn(GlobalScope) } + private 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) + } + } + @Suppress("EXPERIMENTAL_API_USAGE") private fun observeUserConsent() { getUserConsent() @@ -134,6 +141,8 @@ class DefaultVectorAnalytics @Inject constructor( .collectDeviceId(false) .logLevel(getLogLevel()) .build() + + identifyPostHog() } private fun getLogLevel(): PostHog.LogLevel { From 42d987f8ef9b1bc49ead319e91a476def8fe6345 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 17:36:03 +0100 Subject: [PATCH 19/49] Analytics: Fix a crash, cannot create several time a PostHog client --- .../analytics/impl/DefaultVectorAnalytics.kt | 18 +++++++++++------- .../features/analytics/store/AnalyticsStore.kt | 18 +++++++++--------- 2 files changed, 20 insertions(+), 16 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 4c14432906..8558c03f9b 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 @@ -39,7 +39,8 @@ class DefaultVectorAnalytics @Inject constructor( ) : VectorAnalytics { private var posthog: PostHog? = null - private var userConsent: Boolean = false + // Cache for the store values + private var userConsent: Boolean? = null private var analyticsId: String? = null override fun getUserConsent(): Flow { @@ -77,6 +78,7 @@ class DefaultVectorAnalytics @Inject constructor( override fun init() { observeUserConsent() observeAnalyticsId() + createAnalyticsClient() } @Suppress("EXPERIMENTAL_API_USAGE") @@ -107,14 +109,15 @@ class DefaultVectorAnalytics @Inject constructor( .onEach { consent -> Timber.tag(analyticsTag.value).d("User consent updated to $consent") userConsent = consent - if (consent) { - createAnalyticsClient() - } - posthog?.optOut(!consent) + optOutPostHog() } .launchIn(GlobalScope) } + private fun optOutPostHog() { + userConsent?.let { posthog?.optOut(!it) } + } + private fun createAnalyticsClient() { Timber.tag(analyticsTag.value).d("createAnalyticsClient()") @@ -142,6 +145,7 @@ class DefaultVectorAnalytics @Inject constructor( .logLevel(getLogLevel()) .build() + optOutPostHog() identifyPostHog() } @@ -156,14 +160,14 @@ class DefaultVectorAnalytics @Inject constructor( override fun capture(event: String, properties: Map?) { Timber.tag(analyticsTag.value).d("capture($event)") posthog - ?.takeIf { userConsent } + ?.takeIf { userConsent == true } ?.capture(event, properties.toPostHogProperties()) } override fun screen(name: String, properties: Map?) { Timber.tag(analyticsTag.value).d("screen($name)") posthog - ?.takeIf { userConsent } + ?.takeIf { userConsent == true } ?.screen(name, properties.toPostHogProperties()) } diff --git a/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt b/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt index efb824d3db..cd5e204d66 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/store/AnalyticsStore.kt @@ -24,6 +24,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject @@ -43,17 +44,16 @@ class AnalyticsStore @Inject constructor( private val didAskUserConsent = booleanPreferencesKey("did_ask_user_consent") private val analyticsId = stringPreferencesKey("analytics_id") - val userConsentFlow: Flow = context.dataStore.data.map { preferences -> - preferences[userConsent].orFalse() - } + val userConsentFlow: Flow = context.dataStore.data + .map { preferences -> preferences[userConsent].orFalse() } + .distinctUntilChanged() - val didAskUserConsentFlow: Flow = context.dataStore.data.map { preferences -> - preferences[didAskUserConsent].orFalse() - } + val didAskUserConsentFlow: Flow = context.dataStore.data + .map { preferences -> preferences[didAskUserConsent].orFalse() } - val analyticsIdFlow: Flow = context.dataStore.data.map { preferences -> - preferences[analyticsId].orEmpty() - } + val analyticsIdFlow: Flow = context.dataStore.data + .map { preferences -> preferences[analyticsId].orEmpty() } + .distinctUntilChanged() suspend fun setUserConsent(newUserConsent: Boolean) { context.dataStore.edit { settings -> From eeeab1dd0ee916e94a59f40fee90fb18f83186ac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 24 Nov 2021 18:43:23 +0100 Subject: [PATCH 20/49] Give analytics to Activities and Fragments --- .../main/java/im/vector/app/core/di/SingletonEntryPoint.kt | 3 +++ .../java/im/vector/app/core/platform/VectorBaseActivity.kt | 3 +++ .../core/platform/VectorBaseBottomSheetDialogFragment.kt | 6 ++++++ .../java/im/vector/app/core/platform/VectorBaseFragment.kt | 3 +++ .../app/features/settings/VectorSettingsBaseFragment.kt | 3 +++ 5 files changed, 18 insertions(+) diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt index 52316751e6..0b9855ef56 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt @@ -21,6 +21,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import im.vector.app.core.dialogs.UnrecognizedCertificateDialog import im.vector.app.core.error.ErrorFormatter +import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.navigation.Navigator @@ -55,6 +56,8 @@ interface SingletonEntryPoint { fun pinLocker(): PinLocker + fun analytics(): VectorAnalytics + fun webRtcCallManager(): WebRtcCallManager fun appCoroutineScope(): CoroutineScope diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 66c5a53cc2..6126bc533d 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -66,6 +66,7 @@ import im.vector.app.core.flow.throttleFirst import im.vector.app.core.utils.toast import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.consent.ConsentNotGivenHelper import im.vector.app.features.navigation.Navigator @@ -134,6 +135,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver private lateinit var sessionListener: SessionListener protected lateinit var bugReporter: BugReporter private lateinit var pinLocker: PinLocker + protected lateinit var analytics: VectorAnalytics @Inject lateinit var rageShake: RageShake @@ -189,6 +191,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java) bugReporter = singletonEntryPoint.bugReporter() pinLocker = singletonEntryPoint.pinLocker() + analytics = singletonEntryPoint.analytics() navigator = singletonEntryPoint.navigator() activeSessionHolder = singletonEntryPoint.activeSessionHolder() vectorPreferences = singletonEntryPoint.vectorPreferences() diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt index 95feb45ad6..cbbcd48310 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -34,9 +34,11 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.EntryPointAccessors import im.vector.app.core.di.ActivityEntryPoint +import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.flow.throttleFirst import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.analytics.VectorAnalytics import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import reactivecircus.flowbinding.android.view.clicks @@ -83,6 +85,8 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe open val showExpanded = false + protected lateinit var analytics: VectorAnalytics + interface ResultListener { fun onBottomSheetResult(resultCode: Int, data: Any?) @@ -120,6 +124,8 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe override fun onAttach(context: Context) { val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) viewModelFactory = activityEntryPoint.viewModelFactory() + val singletonEntryPoint = context.singletonEntryPoint() + analytics = singletonEntryPoint.analytics() super.onAttach(context) } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index f4e1fe84e1..929ea536ca 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -43,6 +43,7 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.flow.throttleFirst +import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.navigation.Navigator import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog import kotlinx.coroutines.flow.launchIn @@ -61,6 +62,7 @@ abstract class VectorBaseFragment : Fragment(), MavericksView * ========================================================================================== */ protected lateinit var navigator: Navigator + protected lateinit var analytics: VectorAnalytics protected lateinit var errorFormatter: ErrorFormatter protected lateinit var unrecognizedCertificateDialog: UnrecognizedCertificateDialog @@ -97,6 +99,7 @@ abstract class VectorBaseFragment : Fragment(), MavericksView val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) navigator = singletonEntryPoint.navigator() errorFormatter = singletonEntryPoint.errorFormatter() + analytics = singletonEntryPoint.analytics() unrecognizedCertificateDialog = singletonEntryPoint.unrecognizedCertificateDialog() viewModelFactory = activityEntryPoint.viewModelFactory() childFragmentManager.fragmentFactory = activityEntryPoint.fragmentFactory() diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt index c5786b44b0..2187765599 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt @@ -28,6 +28,7 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast +import im.vector.app.features.analytics.VectorAnalytics import org.matrix.android.sdk.api.session.Session import timber.log.Timber @@ -42,6 +43,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick // members protected lateinit var session: Session protected lateinit var errorFormatter: ErrorFormatter + protected lateinit var analytics: VectorAnalytics abstract val preferenceXmlRes: Int @@ -56,6 +58,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick super.onAttach(context) session = singletonEntryPoint.activeSessionHolder().getActiveSession() errorFormatter = singletonEntryPoint.errorFormatter() + analytics = singletonEntryPoint.analytics() } override fun onResume() { From eb1a30cc3040cf0b5262e0a318434260b49a9a02 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 Nov 2021 09:25:42 +0100 Subject: [PATCH 21/49] Analytics: code quality --- .../app/features/analytics/impl/DefaultVectorAnalytics.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8558c03f9b..4265f5d747 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 @@ -134,7 +134,7 @@ class DefaultVectorAnalytics @Inject constructor( // Capture deep links as part of the screen call. (off by default) // .captureDeepLinks() - // Maximum number of events to keep in queue before flushing (20) + // Maximum number of events to keep in queue before flushing (default 20) // .flushQueueSize(20) // Max delay before flushing the queue (30 seconds) From 9b7650e5db2a00eb1ce06ece866a96f378e39d9b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 Nov 2021 14:29:00 +0100 Subject: [PATCH 22/49] Analytics: Remove `/` suffix in the URLs. --- vector/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index f0d79f91f6..2fd562fff5 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -234,7 +234,7 @@ android { buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" // Analytics. Set to empty strings to just disable analytics - buildConfigField "String", "ANALYTICS_POSTHOG_HOST", "\"https://posthog-poc.lab.element.dev/\"" + buildConfigField "String", "ANALYTICS_POSTHOG_HOST", "\"https://posthog-poc.lab.element.dev\"" buildConfigField "String", "ANALYTICS_POSTHOG_API_KEY", "\"rs-pJjsYJTuAkXJfhaMmPUNBhWliDyTKLOOxike6ck8\"" signingConfig signingConfigs.debug @@ -248,7 +248,7 @@ android { buildConfigField "boolean", "ENABLE_STRICT_MODE_LOGS", "false" // Analytics. Set to empty strings to just disable analytics - buildConfigField "String", "ANALYTICS_POSTHOG_HOST", "\"https://posthog.hss.element.io/\"" + buildConfigField "String", "ANALYTICS_POSTHOG_HOST", "\"https://posthog.hss.element.io\"" buildConfigField "String", "ANALYTICS_POSTHOG_API_KEY", "\"phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO\"" postprocessing { From f05ed4c6ccc0cf7ad8648e60006fb69261c08165 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 Nov 2021 22:40:59 +0100 Subject: [PATCH 23/49] Cleanup --- .../analytics/accountdata/AnalyticsAccountDataViewModel.kt | 2 +- .../app/features/analytics/impl/DefaultVectorAnalytics.kt | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt index 89b12cd8c2..5d65d7ea42 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/accountdata/AnalyticsAccountDataViewModel.kt @@ -60,7 +60,7 @@ class AnalyticsAccountDataViewModel @AssistedInject constructor( } companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() { - private const val ANALYTICS_EVENT_TYPE = "im.vector.analytics"; + private const val ANALYTICS_EVENT_TYPE = "im.vector.analytics" } init { 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 4265f5d747..b9d85ec30a 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 @@ -127,19 +127,14 @@ class DefaultVectorAnalytics @Inject constructor( posthog = PostHog.Builder(context, config.postHogApiKey, config.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()) @@ -175,7 +170,7 @@ class DefaultVectorAnalytics @Inject constructor( if (this == null) return null return Properties().apply { - this@toPostHogProperties.forEach { putValue(it.key, it.value) } + putAll(this@toPostHogProperties) } } } From 729d9ce815d0eedeede20308c37d34d1e31b6419 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 Nov 2021 22:41:20 +0100 Subject: [PATCH 24/49] Create interface for the coming plan --- .../app/features/analytics/VectorAnalytics.kt | 6 +++-- .../analytics/impl/DefaultVectorAnalytics.kt | 12 +++++----- .../analytics/itf/VectorAnalyticsEvent.kt | 22 +++++++++++++++++++ .../analytics/itf/VectorAnalyticsScreen.kt | 22 +++++++++++++++++++ 4 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsScreen.kt diff --git a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt index ae119561b3..476f5ade56 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/VectorAnalytics.kt @@ -16,6 +16,8 @@ package im.vector.app.features.analytics +import im.vector.app.features.analytics.itf.VectorAnalyticsEvent +import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import kotlinx.coroutines.flow.Flow interface VectorAnalytics { @@ -62,10 +64,10 @@ interface VectorAnalytics { /** * Capture an Event */ - fun capture(event: String, properties: Map? = null) + fun capture(event: VectorAnalyticsEvent) /** * Track a displayed screen */ - fun screen(name: String, properties: Map? = null) + fun screen(screen: VectorAnalyticsScreen) } 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 b9d85ec30a..997bea88a5 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 @@ -23,6 +23,8 @@ import im.vector.app.BuildConfig import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.log.analyticsTag +import im.vector.app.features.analytics.itf.VectorAnalyticsEvent +import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.store.AnalyticsStore import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow @@ -152,18 +154,18 @@ class DefaultVectorAnalytics @Inject constructor( } } - override fun capture(event: String, properties: Map?) { + override fun capture(event: VectorAnalyticsEvent) { Timber.tag(analyticsTag.value).d("capture($event)") posthog ?.takeIf { userConsent == true } - ?.capture(event, properties.toPostHogProperties()) + ?.capture(event.getName(), event.getProperties()?.toPostHogProperties()) } - override fun screen(name: String, properties: Map?) { - Timber.tag(analyticsTag.value).d("screen($name)") + override fun screen(screen: VectorAnalyticsScreen) { + Timber.tag(analyticsTag.value).d("screen($screen)") posthog ?.takeIf { userConsent == true } - ?.screen(name, properties.toPostHogProperties()) + ?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties()) } private fun Map?.toPostHogProperties(): Properties? { diff --git a/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt b/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt new file mode 100644 index 0000000000..c6acb3b87a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsEvent.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2021 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.itf + +interface VectorAnalyticsEvent { + fun getName(): String + fun getProperties(): Map? +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsScreen.kt b/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsScreen.kt new file mode 100644 index 0000000000..7056814aaf --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/itf/VectorAnalyticsScreen.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2021 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.itf + +interface VectorAnalyticsScreen { + fun getName(): String + fun getProperties(): Map? +} From 3917b4c8cf70fbe666929623dc009d1e932eeccb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Nov 2021 15:06:51 +0100 Subject: [PATCH 25/49] tmp expected result --- .../app/features/analytics/plan/Error.kt | 61 +++++++++++++++++++ .../app/features/analytics/plan/Screen.kt | 55 +++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt create mode 100644 vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt new file mode 100644 index 0000000000..d5374f321a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Error.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 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.plan + +import im.vector.app.features.analytics.itf.VectorAnalyticsEvent + +// GENERATED FILE, DO NOT EDIT + +/** + * Triggered when an error occurred + */ +data class Error( + /** + * Context - client defined, can be used for debugging + */ + val context: String? = null, + val domain: Domain, + val name: Name, +) : VectorAnalyticsEvent { + + enum class Domain { + E2EE, + VOIP, + } + + enum class Name { + OlmIndexError, + OlmKeysNotSentError, + OlmUnspecifiedError, + UnknownError, + VoipIceFailed, + VoipIceTimeout, + VoipInviteTimeout, + VoipUserHangup, + VoipUserMediaFailed, + } + + override fun getName() = "Error" + + override fun getProperties(): Map? { + return mutableMapOf().apply { + context?.let { put("context", it) } + put("domain", domain.name) + put("name", name.name) + }.takeIf { it.isNotEmpty() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt new file mode 100644 index 0000000000..3be490fedb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 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.plan + +import im.vector.app.features.analytics.itf.VectorAnalyticsScreen + +// GENERATED FILE, DO NOT EDIT + +/** + * Triggered when the user changed screen + */ +data class Screen( + val durationMs: Double? = null, + val screenName: ScreenName, +) : VectorAnalyticsScreen { + + enum class ScreenName { + Group, + Home, + MyGroups, + Room, + RoomDirectory, + User, + WebCompleteSecurity, + WebE2ESetup, + WebForgotPassword, + WebLoading, + WebLogin, + WebRegister, + WebSoftLogout, + WebWelcome, + } + + override fun getName() = screenName.name + + override fun getProperties(): Map? { + return mutableMapOf().apply { + durationMs?.let { put("durationMs", it) } + }.takeIf { it.isNotEmpty() } + } +} From 055c9be9ce44d6f8d693d905deb798cebe552a06 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Nov 2021 16:47:56 +0100 Subject: [PATCH 26/49] Add a script to import the plan to Element-Android project --- tools/import_analytic_plan.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100755 tools/import_analytic_plan.sh diff --git a/tools/import_analytic_plan.sh b/tools/import_analytic_plan.sh new file mode 100755 index 0000000000..9c020a8e37 --- /dev/null +++ b/tools/import_analytic_plan.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +echo "Deleted existing plan..." +rm vector/src/main/java/im/vector/app/features/analytics/plan/*.* + +echo "Cloning analytics project..." +mkdir analytics_tmp +cd analytics_tmp +git clone https://github.com/matrix-org/matrix-analytics-events.git + +echo "Copy plan..." +cp matrix-analytics-events/types/kotlin2/* ../vector/src/main/java/im/vector/app/features/analytics/plan/ + +echo "Cleanup." +cd .. +rm -rf analytics_tmp + +echo "Done." From 5ab18dfd6dce9337f9568e1d308e4415bc75d329 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Nov 2021 17:27:05 +0100 Subject: [PATCH 27/49] Add automation to import the plan --- .../workflows/sync-from-external-sources.yml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/sync-from-external-sources.yml b/.github/workflows/sync-from-external-sources.yml index 2da8e10542..5a5d8152ff 100644 --- a/.github/workflows/sync-from-external-sources.yml +++ b/.github/workflows/sync-from-external-sources.yml @@ -70,4 +70,27 @@ jobs: body: | - Update SAS Strings from matrix-doc. branch: sync-sas-strings + base: develop + + sync-analytics-plan: + runs-on: ubuntu-latest + # Skip in forks + if: github.repository == 'vector-im/element-android' + steps: + - uses: actions/checkout@v2 + - name: Run analytics import script + run: ./tools/import_analytic_plan.sh + - name: Create Pull Request for analytics plan + uses: peter-evans/create-pull-request@v3 + with: + commit-message: Sync analytics plan + title: Sync analytics plan + body: | + ### Update analytics plan + Reviewers: + - [ ] Please remove usage of Event or Enum which may have been removed or updated + - [ ] please ensure new Events or new Enums are used to send analytics by pushing new commit(s) to this PR. + + *Note*: Change are coming from [this project](https://github.com/matrix-org/matrix-analytics-events) + branch: sync-analytics-plan base: develop \ No newline at end of file From 73f5d77b059934cdac716d8571a08e89b67feed0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Dec 2021 15:41:42 +0100 Subject: [PATCH 28/49] Analytics: Opt-in screen layout - WIP --- .../main/res/drawable/element_logo_stars.xml | 57 +++++++ .../main/res/drawable/ic_list_item_bullet.xml | 20 +++ .../res/layout/fragment_analytics_optin.xml | 146 ++++++++++++++++++ vector/src/main/res/values/strings.xml | 10 ++ 4 files changed, 233 insertions(+) create mode 100644 vector/src/main/res/drawable/element_logo_stars.xml create mode 100644 vector/src/main/res/drawable/ic_list_item_bullet.xml create mode 100644 vector/src/main/res/layout/fragment_analytics_optin.xml diff --git a/vector/src/main/res/drawable/element_logo_stars.xml b/vector/src/main/res/drawable/element_logo_stars.xml new file mode 100644 index 0000000000..d982fbedc4 --- /dev/null +++ b/vector/src/main/res/drawable/element_logo_stars.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_list_item_bullet.xml b/vector/src/main/res/drawable/ic_list_item_bullet.xml new file mode 100644 index 0000000000..b4f13479f7 --- /dev/null +++ b/vector/src/main/res/drawable/ic_list_item_bullet.xml @@ -0,0 +1,20 @@ + + + + diff --git a/vector/src/main/res/layout/fragment_analytics_optin.xml b/vector/src/main/res/layout/fragment_analytics_optin.xml new file mode 100644 index 0000000000..2072529ce6 --- /dev/null +++ b/vector/src/main/res/layout/fragment_analytics_optin.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + +