Repository: wordpress-mobile/WordPress-Android (language: kotlin)

Issue:
Site Monitor: Prevent webview reload
Prevent the webview from reloading when navigating between tabs


Discussion hints:
nan

Pull request: Fixes: site monitor prevent webview reload
## Fixes 
#20067 

## Description
This PR fixes the issue of reloading of the webviews in site monitoring activity when the tabs are selected again.

[Screen_recording_20240131_151636.webm](https://github.com/wordpress-mobile/WordPress-Android/assets/17463767/7b014247-4a46-41c7-b5de-d856f718e494)  


## Pending issues 
- [Fixed] On Loading of the Webview first time - there is a glitch happening (not sure if its because of the emulator or something else)

## To Test:


#### Prerequisite
<details><summary>◐ Toggle the <code>site_monitoring</code> flag</summary>

1. Go to `Me` → `App Settings` → `Debug settings` → `Remote Feature Flags`
2. Tap on the item corresponding to the flag - site_monitoring
3. Tap `RESTART THE APP` button

</details>

###  Site monitor webviews are not re-loaded when selected again 
1. Go to More → Site Monitoring 
2. ✅ Verify that the logs include tracking` 🔵 Tracked: site_monitoring_screen_shown`
3. ✅  Verify that for each tab - Metrics, PHP logs and Web Server logs, the webview loads from the network first time
4. ✅ Verify that on changing tab, the logs include tracking - `🔵 Tracked: site_monitoring_tab_shown, Properties: {"tab":"php_logs"}
5. ✅ Verify that when the tabs are selected again, the webviews are not reloaded
`

## Regression Notes

1. Potential unintended areas of impact
- Site monitor webview screens are loaded on selecting tabs after the first time
- Site monitor screens are not loaded in the first time 

2. What I did to test those areas of impact (or what existing automated tests I relied on)
Manual testing 

3. What automated tests I added (or what prevented me from doing so)
N/A


PR submission checklist:
- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

UI Changes testing checklist:
- [x] Portrait and landscape orientations.
- [x] Light and dark modes.
- [x] Fonts: Larger, smaller and bold text.
- [x] High contrast.
- [x] Talkback.
- [x] Languages with large words or with letters/accents not frequently used in English.
- [x] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [x] Large and small screen sizes. (Tablet and smaller phones)
- [x] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)


Test command: ./gradlew :WordPress:testWordpressVanillaDebugUnitTest --tests "org.wordpress.android.ui.sitemonitor.SiteMonitorUtilsTest" --tests "org.wordpress.android.ui.sitemonitor.SiteMonitorWebViewClientTest" --tests "org.wordpress.android.ui.sitemonitor.SiteMonitorParentViewModelTest" --tests "org.wordpress.android.ui.sitemonitor.SiteMonitorTabViewModelSliceTest" --tests "org.wordpress.android.ui.sitemonitor.SiteMonitorMapperTest"

Fix patch:
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt
index 94e60930694d..ac9ab5de738d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/personalization/PersonalizationActivity.kt
@@ -107,7 +107,7 @@ class PersonalizationActivity : AppCompatActivity() {
                 contentColor = MaterialTheme.colors.onSurface,
             ) {
                 tabs.forEachIndexed { index, title ->
-                    Tab(text = { Text(stringResource(id = title)) },
+                    Tab(text = { Text(stringResource(id = title).uppercase()) },
                         selected = tabIndex == index,
                         onClick = { tabIndex = index }
                     )
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorFragmentContainer.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorFragmentContainer.kt
deleted file mode 100644
index eb5eea65c8a1..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorFragmentContainer.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-package org.wordpress.android.ui.sitemonitor
-
-import android.content.Context
-import android.view.View
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentActivity
-import androidx.fragment.app.FragmentContainerView
-import androidx.fragment.app.FragmentTransaction
-import androidx.fragment.app.commit
-import androidx.fragment.app.findFragment
-
-@Suppress("SwallowedException")
-@Composable
-fun SiteMonitorFragmentContainer(
-    modifier: Modifier = Modifier,
-    commit: FragmentTransaction.(containerId: Int) -> Unit
-) {
-    val currentLocalView = LocalView.current
-    // Using the current view, check if a parent fragment exists.
-    // This will help ensure that the fragment are nested correctly.
-    // This assists in saving/restoring the fragments to their proper state
-    val parentFragment = remember(currentLocalView) {
-        try {
-            currentLocalView.findFragment<Fragment>()
-        } catch (e: IllegalStateException) {
-            null
-        }
-    }
-    val viewId by rememberSaveable { mutableIntStateOf(View.generateViewId()) }
-    val container = remember { mutableStateOf<FragmentContainerView?>(null) }
-    val viewSection: (Context) -> View = remember(currentLocalView) {
-        { context ->
-            FragmentContainerView(context)
-                .apply { id = viewId }
-                .also {
-                    val fragmentManager = parentFragment?.childFragmentManager
-                        ?: (context as? FragmentActivity)?.supportFragmentManager
-                    fragmentManager?.commit { commit(it.id) }
-                    container.value = it
-                }
-        }
-    }
-    AndroidView(
-        modifier = modifier,
-        factory = viewSection,
-        update = {}
-    )
-
-    // Be sure to clean up the fragments when the FragmentContainer is disposed
-    val localContext = LocalContext.current
-    DisposableEffect(currentLocalView, localContext, container) {
-        onDispose {
-            val fragmentManager = parentFragment?.childFragmentManager
-                ?: (localContext as? FragmentActivity)?.supportFragmentManager
-            // Use the FragmentContainerView to find the inflated fragment
-            val existingFragment = fragmentManager?.findFragmentById(container.value?.id ?: 0)
-            if (existingFragment != null && !fragmentManager.isStateSaved) {
-                // A composable has been removed from the hierarchy if the state isn't saved
-                fragmentManager.commit {
-                    remove(existingFragment)
-                }
-            }
-        }
-    }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt
index 9a9c52b4920b..c1773d48c83e 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentActivity.kt
@@ -1,62 +1,121 @@
 package org.wordpress.android.ui.sitemonitor
 
 import android.annotation.SuppressLint
+import android.os.Build
 import android.os.Bundle
 import android.util.SparseArray
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.WebView
 import androidx.activity.compose.setContent
+import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.Surface
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRow
+import androidx.compose.material3.TabRowDefaults
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
 import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentManager
-import androidx.fragment.app.FragmentTransaction
 import dagger.hilt.android.AndroidEntryPoint
 import org.wordpress.android.R
 import org.wordpress.android.WordPress
 import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.ui.WPWebViewActivity
 import org.wordpress.android.ui.compose.components.MainTopAppBar
 import org.wordpress.android.ui.compose.components.NavigationIcons
 import org.wordpress.android.ui.compose.theme.AppTheme
+import org.wordpress.android.ui.compose.utils.uiStringText
 import org.wordpress.android.util.extensions.getSerializableExtraCompat
 import javax.inject.Inject
 
+@SuppressLint("SetJavaScriptEnabled")
 @AndroidEntryPoint
-class SiteMonitorParentActivity: AppCompatActivity() {
+class SiteMonitorParentActivity : AppCompatActivity(), SiteMonitorWebViewClient.SiteMonitorWebViewClientListener {
     @Inject
     lateinit var siteMonitorUtils: SiteMonitorUtils
 
     private var savedStateSparseArray = SparseArray<Fragment.SavedState>()
     private var currentSelectItemId = 0
 
+    private val siteMonitorParentViewModel: SiteMonitorParentViewModel by viewModels()
+
+    private val metricsWebView by lazy {
+        commonWebView(SiteMonitorType.METRICS)
+    }
+
+    private val phpLogsWebView by lazy {
+        commonWebView(SiteMonitorType.PHP_LOGS)
+    }
+
+    private val webServerLogsWebView by lazy {
+        commonWebView(SiteMonitorType.WEB_SERVER_LOGS)
+    }
+
+    private fun commonWebView(
+        siteMonitorType: SiteMonitorType
+    ) = WebView(this@SiteMonitorParentActivity).apply {
+        layoutParams = ViewGroup.LayoutParams(
+            ViewGroup.LayoutParams.MATCH_PARENT,
+            ViewGroup.LayoutParams.MATCH_PARENT
+        )
+        scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
+        settings.userAgentString = siteMonitorUtils.getUserAgent()
+        settings.javaScriptEnabled = true
+        settings.domStorageEnabled = true
+        webViewClient = SiteMonitorWebViewClient(this@SiteMonitorParentActivity, siteMonitorType)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            // not sure about this one, double check if this works as expected
+            settings.isAlgorithmicDarkeningAllowed = true
+        }
+    }
+
     @Suppress("DEPRECATION")
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
-        siteMonitorUtils.trackActivityLaunched()
-
         if (savedInstanceState != null) {
             savedStateSparseArray = savedInstanceState.getSparseParcelableArray(
                 SAVED_STATE_CONTAINER_KEY
             )
                 ?: savedStateSparseArray
             currentSelectItemId = savedInstanceState.getInt(SAVED_STATE_CURRENT_TAB_KEY)
+            siteMonitorParentViewModel.loadData()
+        } else {
+            siteMonitorParentViewModel.start(getSite())
+            currentSelectItemId = getInitialTab()
         }
         setContent {
             AppTheme {
                 Surface(
                     modifier = Modifier.fillMaxSize(),
                 ) {
-                    SiteMonitorScreen()
+                    SiteMonitorScreen(initialTab = currentSelectItemId)
                 }
             }
         }
@@ -72,9 +131,14 @@ class SiteMonitorParentActivity: AppCompatActivity() {
         return requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) as SiteModel
     }
 
-    private fun getInitialTab(): SiteMonitorType {
-        return intent?.getSerializableExtraCompat(ARG_SITE_MONITOR_TYPE_KEY) as SiteMonitorType?
+    private fun getInitialTab(): Int {
+        val tab = intent?.getSerializableExtraCompat(ARG_SITE_MONITOR_TYPE_KEY) as SiteMonitorType?
             ?: SiteMonitorType.METRICS
+        return when (tab) {
+            SiteMonitorType.METRICS -> 0
+            SiteMonitorType.PHP_LOGS -> 1
+            SiteMonitorType.WEB_SERVER_LOGS -> 2
+        }
     }
 
     companion object {
@@ -85,8 +149,7 @@ class SiteMonitorParentActivity: AppCompatActivity() {
 
     @Composable
     @SuppressLint("UnusedMaterialScaffoldPaddingParameter")
-    fun SiteMonitorScreen() {
-        var selectedTab by rememberSaveable { mutableStateOf(SiteMonitorTabItem.Metrics.route) }
+    fun SiteMonitorScreen(initialTab: Int, modifier: Modifier = Modifier) {
         Scaffold(
             topBar = {
                 MainTopAppBar(
@@ -94,59 +157,171 @@ class SiteMonitorParentActivity: AppCompatActivity() {
                     navigationIcon = NavigationIcons.BackIcon,
                     onNavigationIconClick = onBackPressedDispatcher::onBackPressed,
                 )
+            },
+            content = {
+                SiteMonitorHeader(initialTab, modifier = modifier)
             }
-        ) { padding ->
-            Column(modifier = Modifier.padding(padding)) {
-                SiteMonitorTabHeader { clickTab ->
-                    selectedTab = clickTab
+        )
+    }
+
+    @Composable
+    @SuppressLint("UnusedMaterialScaffoldPaddingParameter")
+    fun SiteMonitorHeader(initialTab: Int, modifier: Modifier = Modifier) {
+        var tabIndex by remember { mutableStateOf(initialTab) }
+
+        val tabs = SiteMonitorTabItem.entries
+
+        LaunchedEffect(true) {
+            siteMonitorUtils.trackTabLoaded(tabs[initialTab].siteMonitorType)
+        }
+
+        Column(modifier = modifier.fillMaxWidth()) {
+            TabRow(
+                selectedTabIndex = tabIndex,
+                containerColor = MaterialTheme.colors.surface,
+                contentColor = MaterialTheme.colors.onSurface,
+                indicator = { tabPositions ->
+                    // Customizing the indicator color and style
+                    TabRowDefaults.Indicator(
+                        Modifier.tabIndicatorOffset(tabPositions[tabIndex]),
+                        color = MaterialTheme.colors.onSurface,
+                        height = 2.0.dp
+                    )
                 }
-                SiteMonitorTabNavigation(selectedTab) { selectedTab ->
-                    val item = enumValues<SiteMonitorTabItem>().find {
-                        it.route == selectedTab
-                    } ?: initialItem(getInitialTab())
-
-                    siteMonitorUtils.trackTabLoaded(item.siteMonitorType)
-
-                    SiteMonitorFragmentContainer(
-                        modifier = Modifier.fillMaxSize(),
-                        commit = getCommitFunction(
-                            SiteMonitorTabFragment.newInstance(item.urlTemplate, item.siteMonitorType, getSite()),
-                            item.route
-                        )
+            ) {
+                tabs.forEachIndexed { index, item ->
+                    Tab(
+                        text = {
+                            Column(horizontalAlignment = Alignment.CenterHorizontally) {
+                                Text(
+                                    text = stringResource(item.title).uppercase(),
+                                    maxLines = 1,
+                                    overflow = TextOverflow.Ellipsis
+                                )
+                            }
+                        },
+                        selected = tabIndex == index,
+                        onClick = {
+                            siteMonitorUtils.trackTabLoaded(tabs[index].siteMonitorType)
+                            tabIndex = index
+                        },
                     )
                 }
             }
+            when (tabIndex) {
+                0 -> SiteMonitorTabContent(SiteMonitorType.METRICS)
+                1 -> SiteMonitorTabContent(SiteMonitorType.PHP_LOGS)
+                2 -> SiteMonitorTabContent(SiteMonitorType.WEB_SERVER_LOGS)
+            }
         }
     }
 
-    private fun initialItem(type: SiteMonitorType): SiteMonitorTabItem {
-        return enumValues<SiteMonitorTabItem>().find {
-            it.siteMonitorType == type
-        } ?: SiteMonitorTabItem.Metrics
+    @Composable
+    private fun SiteMonitorTabContent(tabType: SiteMonitorType, modifier: Modifier = Modifier) {
+        val uiState by remember(key1 = tabType) {
+            siteMonitorParentViewModel.getUiState(tabType)
+        }
+        LazyColumn  {
+            item {
+                when (uiState) {
+                    is SiteMonitorUiState.Preparing -> LoadingState(modifier)
+                    is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded ->
+                        SiteMonitorWebView(uiState, tabType, modifier)
+                    is SiteMonitorUiState.Error -> SiteMonitorError(uiState as SiteMonitorUiState.Error, modifier)
+                }
+            }
+        }
+    }
+    @Composable
+    fun LoadingState(modifier: Modifier = Modifier) {
+        Box(
+            contentAlignment = Alignment.Center,
+            modifier = modifier.fillMaxSize()
+        ) {
+            CircularProgressIndicator()
+        }
     }
 
-    private fun getCommitFunction(
-        fragment : Fragment,
-        tag: String
-    ): FragmentTransaction.(containerId: Int) -> Unit =
-        {
-            saveAndRetrieveFragment(supportFragmentManager, it, fragment)
-            replace(it, fragment, tag)
+    @Composable
+    fun SiteMonitorError(error: SiteMonitorUiState.Error, modifier: Modifier = Modifier) {
+        Column(
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.Center,
+            modifier = modifier
+                .padding(20.dp)
+                .fillMaxWidth()
+                .fillMaxHeight(),
+        ) {
+            androidx.compose.material.Text(
+                text = uiStringText(uiString = error.title),
+                style = androidx.compose.material.MaterialTheme.typography.h5,
+                textAlign = TextAlign.Center
+            )
+            androidx.compose.material.Text(
+                text = uiStringText(uiString = error.description),
+                style = androidx.compose.material.MaterialTheme.typography.body1,
+                textAlign = TextAlign.Center,
+                modifier = Modifier.padding(top = 8.dp)
+            )
+            if (error.button != null) {
+                Button(
+                    modifier = Modifier.padding(top = 8.dp),
+                    onClick = error.button.click
+                ) {
+                    Text(text = uiStringText(uiString = error.button.text))
+                }
+            }
         }
+    }
 
-    private fun saveAndRetrieveFragment(
-        supportFragmentManager: FragmentManager,
-        tabId: Int,
-        fragment: Fragment
+    @SuppressLint("SetJavaScriptEnabled")
+    @Composable
+    private fun SiteMonitorWebView(
+        uiState: SiteMonitorUiState,
+        tabType: SiteMonitorType,
+        modifier: Modifier = Modifier
     ) {
-        val currentFragment = supportFragmentManager.findFragmentById(currentSelectItemId)
-        if (currentFragment != null) {
-            savedStateSparseArray.put(
-                currentSelectItemId,
-                supportFragmentManager.saveFragmentInstanceState(currentFragment)
-            )
+        // retrieve the webview from the actvity
+        var webView = when (tabType) {
+            SiteMonitorType.METRICS -> metricsWebView
+            SiteMonitorType.PHP_LOGS -> phpLogsWebView
+            SiteMonitorType.WEB_SERVER_LOGS -> webServerLogsWebView
         }
-        currentSelectItemId = tabId
-        fragment.setInitialSavedState(savedStateSparseArray[currentSelectItemId])
+
+        if (uiState is SiteMonitorUiState.Prepared) {
+            webView.postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, uiState.model.addressToLoad.toByteArray())
+        }
+
+        Box(
+            modifier = modifier.fillMaxSize(),
+            contentAlignment = Alignment.Center
+        ) {
+            if (uiState is SiteMonitorUiState.Prepared) {
+                LoadingState()
+            } else {
+                webView.let { theWebView ->
+                    AndroidView(
+                        factory = { theWebView },
+                        update = { webView = it },
+                        modifier = Modifier.fillMaxWidth()
+                    )
+                }
+            }
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        metricsWebView.destroy()
+        phpLogsWebView.destroy()
+        webServerLogsWebView.destroy()
+    }
+
+    override fun onWebViewPageLoaded(url: String, tabType: SiteMonitorType) =
+        siteMonitorParentViewModel.onUrlLoaded(tabType)
+
+    override fun onWebViewReceivedError(url: String, tabType: SiteMonitorType) {
+        siteMonitorParentViewModel.onWebViewError(tabType)
+        siteMonitorUtils.trackTabLoadingError(tabType)
     }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt
new file mode 100644
index 000000000000..2a18f07e1fb2
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModel.kt
@@ -0,0 +1,95 @@
+package org.wordpress.android.ui.sitemonitor
+
+import androidx.compose.runtime.State
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.CoroutineDispatcher
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.modules.BG_THREAD
+import org.wordpress.android.viewmodel.ScopedViewModel
+import javax.inject.Inject
+import javax.inject.Named
+
+@HiltViewModel
+class SiteMonitorParentViewModel @Inject constructor(
+    @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
+    private val siteMonitorUtils: SiteMonitorUtils,
+    private val metricsViewModel: SiteMonitorTabViewModelSlice,
+    private val phpLogViewModel: SiteMonitorTabViewModelSlice,
+    private val webServerViewModel: SiteMonitorTabViewModelSlice
+) : ScopedViewModel(bgDispatcher) {
+    private lateinit var site: SiteModel
+
+    init {
+        metricsViewModel.initialize(viewModelScope)
+        phpLogViewModel.initialize(viewModelScope)
+        webServerViewModel.initialize(viewModelScope)
+    }
+
+    fun start(site: SiteModel) {
+        this.site = site
+        siteMonitorUtils.trackActivityLaunched()
+        loadData()
+    }
+
+    fun loadData() {
+        metricsViewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site)
+        phpLogViewModel.start(SiteMonitorType.PHP_LOGS, SiteMonitorTabItem.PHPLogs.urlTemplate, site)
+        webServerViewModel.start(SiteMonitorType.WEB_SERVER_LOGS, SiteMonitorTabItem.WebServerLogs.urlTemplate, site)
+    }
+
+    fun getUiState(siteMonitorType: SiteMonitorType): State<SiteMonitorUiState> {
+        return when (siteMonitorType) {
+            SiteMonitorType.METRICS -> {
+                metricsViewModel.uiState
+            }
+
+            SiteMonitorType.PHP_LOGS -> {
+                phpLogViewModel.uiState
+            }
+
+            SiteMonitorType.WEB_SERVER_LOGS -> {
+                webServerViewModel.uiState
+            }
+        }
+    }
+
+    fun onUrlLoaded(siteMonitorType: SiteMonitorType) {
+        when (siteMonitorType) {
+            SiteMonitorType.METRICS -> {
+                metricsViewModel.onUrlLoaded()
+            }
+
+            SiteMonitorType.PHP_LOGS -> {
+                phpLogViewModel.onUrlLoaded()
+            }
+
+            SiteMonitorType.WEB_SERVER_LOGS -> {
+                webServerViewModel.onUrlLoaded()
+            }
+        }
+    }
+
+    fun onWebViewError(siteMonitorType: SiteMonitorType) {
+        when (siteMonitorType) {
+            SiteMonitorType.METRICS -> {
+                metricsViewModel.onWebViewError()
+            }
+
+            SiteMonitorType.PHP_LOGS -> {
+                phpLogViewModel.onWebViewError()
+            }
+
+            SiteMonitorType.WEB_SERVER_LOGS -> {
+                webServerViewModel.onWebViewError()
+            }
+        }
+    }
+
+    override fun onCleared() {
+        super.onCleared()
+        metricsViewModel.onCleared()
+        phpLogViewModel.onCleared()
+        webServerViewModel.onCleared()
+    }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt
deleted file mode 100644
index 8fb61cc8ed03..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabFragment.kt
+++ /dev/null
@@ -1,184 +0,0 @@
-package org.wordpress.android.ui.sitemonitor
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.webkit.WebView
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.Button
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
-import dagger.hilt.android.AndroidEntryPoint
-import org.wordpress.android.WordPress
-import org.wordpress.android.fluxc.model.SiteModel
-import org.wordpress.android.ui.WPWebViewActivity
-import org.wordpress.android.ui.compose.utils.uiStringText
-
-@AndroidEntryPoint
-class SiteMonitorTabFragment : Fragment(), SiteMonitorWebViewClient.SiteMonitorWebViewClientListener {
-    override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View = ComposeView(requireContext()).apply {
-        setContent {
-            SiteMonitorTabContent()
-        }
-    }
-
-    private val viewModel: SiteMonitorTabViewModel by viewModels()
-
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        super.onViewCreated(view, savedInstanceState)
-        initViewModel(getSiteMonitorType(), getUrlTemplate(), getSite())
-    }
-
-    @Suppress("DEPRECATION")
-    private fun getSite(): SiteModel {
-        return requireNotNull(arguments?.getSerializable(WordPress.SITE)) as SiteModel
-    }
-
-    private fun getUrlTemplate(): String {
-        return requireNotNull(arguments?.getString(KEY_URL_TEMPLATE))
-    }
-
-    @Suppress("DEPRECATION")
-    private fun getSiteMonitorType(): SiteMonitorType {
-        return requireNotNull(arguments?.getSerializable(KEY_SITE_MONITOR_TYPE)) as SiteMonitorType
-    }
-
-    private fun initViewModel(type: SiteMonitorType, urlTemplate: String, site: SiteModel) {
-        viewModel.start(type, urlTemplate, site)
-    }
-
-    override fun onWebViewPageLoaded(url: String)  = viewModel.onUrlLoaded()
-
-    override fun onWebViewReceivedError(url: String) = viewModel.onWebViewError()
-
-    companion object {
-        const val KEY_URL_TEMPLATE = "KEY_URL"
-        const val KEY_SITE_MONITOR_TYPE = "KEY_SITE_MONITOR_TYPE"
-        fun newInstance(url: String, type: SiteMonitorType, site: SiteModel): Fragment {
-            val fragment = SiteMonitorTabFragment()
-            val argument = Bundle()
-            argument.putString(KEY_URL_TEMPLATE, url)
-            argument.putSerializable(KEY_SITE_MONITOR_TYPE, type)
-            argument.putSerializable(WordPress.SITE, site)
-            fragment.arguments = argument
-            return fragment
-        }
-    }
-
-    @Composable
-    private fun SiteMonitorTabContent() {
-        val uiState by viewModel.uiState.collectAsState()
-        when (uiState) {
-            is SiteMonitorUiState.Preparing -> LoadingState()
-            is SiteMonitorUiState.Prepared, is SiteMonitorUiState.Loaded -> SiteMonitorWebView(uiState)
-            is SiteMonitorUiState.Error -> SiteMonitorError(uiState as SiteMonitorUiState.Error)
-        }
-    }
-
-    @Composable
-    fun SiteMonitorError(error: SiteMonitorUiState.Error) {
-        Column(
-            horizontalAlignment = Alignment.CenterHorizontally,
-            verticalArrangement = Arrangement.Center,
-            modifier = Modifier
-                .padding(20.dp)
-                .fillMaxWidth()
-                .fillMaxHeight(),
-        ) {
-            androidx.compose.material.Text(
-                text = uiStringText(uiString = error.title),
-                style = androidx.compose.material.MaterialTheme.typography.h5,
-                textAlign = TextAlign.Center
-            )
-            androidx.compose.material.Text(
-                text = uiStringText(uiString = error.description),
-                style = androidx.compose.material.MaterialTheme.typography.body1,
-                textAlign = TextAlign.Center,
-                modifier = Modifier.padding(top = 8.dp)
-            )
-            if (error.button != null) {
-                Button(
-                    modifier = Modifier.padding(top = 8.dp),
-                    onClick = error.button.click
-                ) {
-                    androidx.compose.material.Text(text = uiStringText(uiString = error.button.text))
-                }
-            }
-        }
-    }
-
-    @SuppressLint("SetJavaScriptEnabled")
-    @Composable
-    private fun SiteMonitorWebView(uiState: SiteMonitorUiState) {
-        var webView: WebView? by remember { mutableStateOf(null) }
-
-        if (uiState is SiteMonitorUiState.Prepared) {
-            val model = uiState.model
-            LaunchedEffect(true) {
-                webView = WebView(requireContext()).apply {
-                    layoutParams = ViewGroup.LayoutParams(
-                        ViewGroup.LayoutParams.MATCH_PARENT,
-                        ViewGroup.LayoutParams.MATCH_PARENT
-                    )
-                    scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
-                    settings.userAgentString = model.userAgent
-                    settings.javaScriptEnabled = true
-                    settings.domStorageEnabled = true
-                    webViewClient = SiteMonitorWebViewClient(this@SiteMonitorTabFragment)
-                    postUrl(WPWebViewActivity.WPCOM_LOGIN_URL, model.addressToLoad.toByteArray())
-                }
-            }
-        }
-
-        Box(
-            modifier = Modifier.fillMaxSize(),
-            contentAlignment = Alignment.Center
-        ) {
-            if (uiState is SiteMonitorUiState.Prepared) {
-                LoadingState()
-            } else {
-                webView?.let { theWebView ->
-                    AndroidView(
-                        factory = { theWebView },
-                        modifier = Modifier.fillMaxSize()
-                    )
-                }
-            }
-        }
-    }
-}
-
-@Composable
-fun LoadingState(modifier: Modifier = Modifier) {
-    Box(
-        contentAlignment = Alignment.Center,
-        modifier = modifier.fillMaxSize()
-    ) {
-        CircularProgressIndicator()
-    }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabHeader.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabHeader.kt
deleted file mode 100644
index 7a15059a31fc..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabHeader.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.wordpress.android.ui.sitemonitor
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material3.Tab
-import androidx.compose.material3.TabRow
-import androidx.compose.material3.TabRowDefaults
-import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-
-@Composable
-fun SiteMonitorTabHeader(navController: (String) -> Unit) {
-    var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) }
-    val tabs = listOf(
-        SiteMonitorTabItem.Metrics,
-        SiteMonitorTabItem.PHPLogs,
-        SiteMonitorTabItem.WebServerLogs
-    )
-    TabRow(
-        selectedTabIndex = selectedTabIndex,
-        containerColor = MaterialTheme.colors.surface,
-        contentColor = MaterialTheme.colors.onSurface,
-        indicator = { tabPositions ->
-            // Customizing the indicator color and style
-            TabRowDefaults.Indicator(
-                Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
-                color = MaterialTheme.colors.onSurface,
-                height = 2.0.dp
-            )
-        }
-    ) {
-        tabs.forEachIndexed { index, item ->
-            Tab(
-                text = {
-                    Column (horizontalAlignment = Alignment.CenterHorizontally) {
-                        Text(
-                            text = stringResource(item.title),
-                            maxLines = 1,
-                            overflow = TextOverflow.Ellipsis
-                        )
-                    }
-                },
-                selected = selectedTabIndex == index,
-                onClick = {
-                    selectedTabIndex = index
-                    navController(item.route)
-                },
-            )
-        }
-    }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabNavigation.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabNavigation.kt
deleted file mode 100644
index 023e651a66d5..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabNavigation.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.wordpress.android.ui.sitemonitor
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.saveable.rememberSaveableStateHolder
-import androidx.compose.ui.Modifier
-
-@Composable
-fun <T : Any> SiteMonitorTabNavigation (
-    currentScreen: T,
-    modifier: Modifier = Modifier,
-    content: @Composable (T) -> Unit
-) {
-    val saveableStateHolder = rememberSaveableStateHolder()
-    Box(modifier) {
-        saveableStateHolder.SaveableStateProvider(currentScreen) {
-            content(currentScreen)
-        }
-    }
-}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt
similarity index 77%
rename from WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt
rename to WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt
index 275c65087676..45ec9a56d76f 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSlice.kt
@@ -1,34 +1,36 @@
 package org.wordpress.android.ui.sitemonitor
 
 import android.text.TextUtils
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
 import org.wordpress.android.fluxc.model.SiteModel
 import org.wordpress.android.fluxc.store.AccountStore
 import org.wordpress.android.fluxc.store.SiteStore
-import org.wordpress.android.modules.BG_THREAD
 import org.wordpress.android.util.NetworkUtilsWrapper
-import org.wordpress.android.viewmodel.ScopedViewModel
 import javax.inject.Inject
-import javax.inject.Named
 
-@HiltViewModel
-class SiteMonitorTabViewModel @Inject constructor(
-    @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
+class SiteMonitorTabViewModelSlice @Inject constructor(
     private val networkUtilsWrapper: NetworkUtilsWrapper,
     private val accountStore: AccountStore,
     private val mapper: SiteMonitorMapper,
     private val siteMonitorUtils: SiteMonitorUtils,
     private val siteStore: SiteStore,
-) : ScopedViewModel(bgDispatcher) {
+){
+    private lateinit var scope: CoroutineScope
+
     private lateinit var site: SiteModel
     private lateinit var siteMonitorType: SiteMonitorType
     private lateinit var urlTemplate: String
 
-    private val _uiState = MutableStateFlow<SiteMonitorUiState>(SiteMonitorUiState.Preparing)
-    val uiState: StateFlow<SiteMonitorUiState> = _uiState
+    private val _uiState = mutableStateOf<SiteMonitorUiState>(SiteMonitorUiState.Preparing)
+    val uiState: State<SiteMonitorUiState> = _uiState
+
+    fun initialize(scope: CoroutineScope) {
+        this.scope = scope
+    }
 
     fun start(type: SiteMonitorType, urlTemplate: String, site: SiteModel) {
         this.siteMonitorType = type
@@ -50,13 +52,13 @@ class SiteMonitorTabViewModel @Inject constructor(
 
     private fun checkForInternetConnectivityAndPostErrorIfNeeded() : Boolean {
         if (networkUtilsWrapper.isNetworkAvailable()) return true
-        postUiState(mapper.toNoNetworkError(this@SiteMonitorTabViewModel::loadView))
+        postUiState(mapper.toNoNetworkError(::loadView))
         return false
     }
 
     private fun validateAndPostErrorIfNeeded(): Boolean {
         if (accountStore.account.userName.isNullOrEmpty() || accountStore.accessToken.isNullOrEmpty()) {
-            postUiState(mapper.toGenericError(this@SiteMonitorTabViewModel::loadView))
+            postUiState(mapper.toGenericError(this::loadView))
             return false
         }
         return true
@@ -98,17 +100,23 @@ class SiteMonitorTabViewModel @Inject constructor(
     }
 
     private fun postUiState(state: SiteMonitorUiState) {
-        launch {
+        scope.launch {
             _uiState.value = state
         }
     }
 
     fun onUrlLoaded() {
-        postUiState(SiteMonitorUiState.Loaded)
+        if (uiState.value is SiteMonitorUiState.Prepared){
+            postUiState(SiteMonitorUiState.Loaded)
+        }
     }
 
     fun onWebViewError() {
-        postUiState(mapper.toGenericError(this@SiteMonitorTabViewModel::loadView))
+        postUiState(mapper.toGenericError(::loadView))
+    }
+
+    fun onCleared() {
+        scope.cancel()
     }
 
     companion object {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt
index a4b25ed2a1c7..a64e92029030 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtils.kt
@@ -33,6 +33,14 @@ class SiteMonitorUtils @Inject constructor(
             ))
     }
 
+    fun trackTabLoadingError(siteMonitorType: SiteMonitorType) {
+        analyticsTrackerWrapper.track(
+            AnalyticsTracker.Stat.SITE_MONITORING_TAB_LOADING_ERROR,
+            mapOf(
+                TAB_TRACK_KEY to siteMonitorType.analyticsDescription
+            ))
+    }
+
     companion object {
         const val HTTP_PATTERN = "(https?://)"
         const val PHP_LOGS_PATTERN = "/php"
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt
index 29305e0d2ea0..af0717e8481b 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClient.kt
@@ -7,13 +7,15 @@ import android.webkit.WebView
 import android.webkit.WebViewClient
 
 class SiteMonitorWebViewClient(
-    private val listener: SiteMonitorWebViewClientListener
+    private val listener: SiteMonitorWebViewClientListener,
+    private val tabType: SiteMonitorType
 ) : WebViewClient() {
     private var errorReceived = false
     private var requestedUrl: String? = null
+
     interface SiteMonitorWebViewClientListener {
-        fun onWebViewPageLoaded(url: String)
-        fun onWebViewReceivedError(url: String)
+        fun onWebViewPageLoaded(url: String, tabType: SiteMonitorType)
+        fun onWebViewReceivedError(url: String, tabType: SiteMonitorType)
     }
     override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
         return false
@@ -28,7 +30,7 @@ class SiteMonitorWebViewClient(
     override fun onPageFinished(view: WebView, url: String?) {
         super.onPageFinished(view, url)
         if (!errorReceived) {
-            url?.let { listener.onWebViewPageLoaded(it) }
+            url?.let { listener.onWebViewPageLoaded(it, tabType) }
         }
     }
 
@@ -40,7 +42,7 @@ class SiteMonitorWebViewClient(
         // > Thus, it is recommended to perform minimum required work in this callback.
         if (request?.isForMainFrame == true && requestedUrl == request.url.toString()) {
             errorReceived = true
-            listener.onWebViewReceivedError(request.url.toString())
+            listener.onWebViewReceivedError(request.url.toString(), tabType)
         }
     }
 }
diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java
index 4ecc68844c66..9c93b7293a73 100644
--- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java
+++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java
@@ -1104,6 +1104,7 @@ public enum Stat {
         SITE_MONITORING_SCREEN_SHOWN,
         OPENED_SITE_MONITORING,
         SITE_MONITORING_TAB_SHOWN,
+        SITE_MONITORING_TAB_LOADING_ERROR
     }
 
     private static final List<Tracker> TRACKERS = new ArrayList<>();
diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java
index 6bc51ec5bdbf..5e5fa351b389 100644
--- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java
+++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java
@@ -2701,6 +2701,8 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) {
                 return "opened_site_monitoring";
             case SITE_MONITORING_TAB_SHOWN:
                 return "site_monitoring_tab_shown";
+            case SITE_MONITORING_TAB_LOADING_ERROR:
+                return "site_monitoring_tab_loading_error";
         }
         return null;
     }


Test patch:
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapperTest.kt
new file mode 100644
index 000000000000..d9b349bc2873
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorMapperTest.kt
@@ -0,0 +1,58 @@
+package org.wordpress.android.ui.sitemonitor
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+
+@ExperimentalCoroutinesApi
+@RunWith(MockitoJUnitRunner::class)
+class SiteMonitorMapperTest : BaseUnitTest() {
+    @Mock
+    lateinit var siteMonitorUtils: SiteMonitorUtils
+
+    private lateinit var siteMonitorMapper: SiteMonitorMapper
+
+    @Before
+    fun setup() {
+        siteMonitorMapper = SiteMonitorMapper(siteMonitorUtils)
+    }
+
+    @Test
+    fun `given prepared request, when mapper is called, then site monitor model is created`() {
+        whenever(siteMonitorUtils.getUserAgent()).thenReturn(USER_AGENT)
+
+        val state = siteMonitorMapper.toPrepared(URL, ADDRESS_TO_LOAD, SiteMonitorType.METRICS)
+
+        assertThat(state.model.siteMonitorType).isEqualTo(SiteMonitorType.METRICS)
+        assertThat(state.model.url).isEqualTo(URL)
+        assertThat(state.model.addressToLoad).isEqualTo(ADDRESS_TO_LOAD)
+        assertThat(state.model.userAgent).isEqualTo(USER_AGENT)
+    }
+
+    @Test
+    fun `given network error, when mapper is called, then NoNetwork error is created`() {
+        val state = siteMonitorMapper.toNoNetworkError(mock())
+
+        assertThat(state).isInstanceOf(SiteMonitorUiState.NoNetworkError::class.java)
+    }
+
+    @Test
+    fun `given generic error error, when mapper is called, then Generic error is created`() {
+        val state = siteMonitorMapper.toGenericError(mock())
+
+        assertThat(state).isInstanceOf(SiteMonitorUiState.GenericError::class.java)
+    }
+
+    companion object {
+        const val USER_AGENT = "user_agent"
+        const val URL = "url"
+        const val ADDRESS_TO_LOAD = "address_to_load"
+    }
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt
new file mode 100644
index 000000000000..411f72a9a442
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorParentViewModelTest.kt
@@ -0,0 +1,170 @@
+package org.wordpress.android.ui.sitemonitor
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.any
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+import org.wordpress.android.fluxc.model.SiteModel
+
+@ExperimentalCoroutinesApi
+@RunWith(MockitoJUnitRunner::class)
+class SiteMonitorParentViewModelTest: BaseUnitTest(){
+    @Mock
+    private lateinit var siteMonitorUtils: SiteMonitorUtils
+    @Mock
+    private lateinit var metricsViewModel: SiteMonitorTabViewModelSlice
+    @Mock
+    private lateinit var phpLogViewModel: SiteMonitorTabViewModelSlice
+    @Mock
+    private lateinit var webServerViewModel: SiteMonitorTabViewModelSlice
+
+    private lateinit var viewModel: SiteMonitorParentViewModel
+
+    @Before
+    fun setUp() {
+        viewModel = SiteMonitorParentViewModel(
+            testDispatcher(),
+            siteMonitorUtils,
+            metricsViewModel,
+            phpLogViewModel,
+            webServerViewModel
+        )
+    }
+
+    @Test
+    fun `when viewmodel is started, then track screen shown`() {
+        val site = mock<SiteModel>()
+        viewModel.start(site)
+
+        verify(siteMonitorUtils).trackActivityLaunched()
+    }
+
+    @Test
+    fun `when viewmodel is created, then view model slices are initialized`() {
+        verify(metricsViewModel).initialize(any())
+        verify(phpLogViewModel).initialize(any())
+        verify(webServerViewModel).initialize(any())
+    }
+
+    @Test
+    fun `when start is invoked, then view models are started with the correct tab item`() {
+        val site = mock<SiteModel>()
+        viewModel.start(site)
+
+        verify(metricsViewModel).start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site)
+        verify(phpLogViewModel).start(SiteMonitorType.PHP_LOGS, SiteMonitorTabItem.PHPLogs.urlTemplate, site)
+        verify(webServerViewModel).start(
+            SiteMonitorType.WEB_SERVER_LOGS,
+            SiteMonitorTabItem.WebServerLogs.urlTemplate,
+            site
+        )
+    }
+
+    @Test
+    fun `when loadData is invoked, then view models are started with the correct tab item`() {
+        val site = mock<SiteModel>()
+        viewModel.start(site)
+
+        clearInvocations(metricsViewModel, phpLogViewModel, webServerViewModel)
+
+        viewModel.loadData()
+
+        verify(metricsViewModel).start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site)
+        verify(phpLogViewModel).start(SiteMonitorType.PHP_LOGS, SiteMonitorTabItem.PHPLogs.urlTemplate, site)
+        verify(webServerViewModel).start(
+            SiteMonitorType.WEB_SERVER_LOGS,
+            SiteMonitorTabItem.WebServerLogs.urlTemplate,
+            site
+        )
+    }
+
+    @Test
+    fun `given metrics, when getUiState is invoked, then ui state is returned`() {
+        whenever(metricsViewModel.uiState).thenReturn(mock())
+        val site = mock<SiteModel>()
+        viewModel.start(site)
+
+        advanceUntilIdle()
+
+        val state = viewModel.getUiState(SiteMonitorType.METRICS)
+
+        assertThat(state).isNotNull
+    }
+
+    @Test
+    fun `given phplogs, when getUiState is invoked, then ui state is returned`() {
+        whenever(phpLogViewModel.uiState).thenReturn(mock())
+        val site = mock<SiteModel>()
+        viewModel.start(site)
+
+        advanceUntilIdle()
+
+        val state = viewModel.getUiState(SiteMonitorType.PHP_LOGS)
+
+        assertThat(state).isNotNull
+    }
+
+    @Test
+    fun `given webserver logs, when getUiState is invoked, then ui state is returned`() {
+        whenever(webServerViewModel.uiState).thenReturn(mock())
+        val site = mock<SiteModel>()
+        viewModel.start(site)
+
+        advanceUntilIdle()
+
+        val state = viewModel.getUiState(SiteMonitorType.WEB_SERVER_LOGS)
+
+        assertThat(state).isNotNull
+    }
+
+    @Test
+    fun `given metrics, when onUrlLoaded is invoked, then metric vm slice onUrlLoaded is invoked`() {
+        viewModel.onUrlLoaded(SiteMonitorType.METRICS)
+
+        verify(metricsViewModel).onUrlLoaded()
+    }
+
+    @Test
+    fun `given php logs, when onUrlLoaded is invoked, then php logs vm slice onUrlLoaded is invoked`() {
+        viewModel.onUrlLoaded(SiteMonitorType.PHP_LOGS)
+
+        verify(phpLogViewModel).onUrlLoaded()
+    }
+
+    @Test
+    fun `given webserver logs, when onUrlLoaded is invoked, then webserver logs vm slice onUrlLoaded is invoked`() {
+        viewModel.onUrlLoaded(SiteMonitorType.WEB_SERVER_LOGS)
+
+        verify(webServerViewModel).onUrlLoaded()
+    }
+
+    @Test
+    fun `given metrics, when onWebViewError is invoked, then metric vm slice onWebViewError is invoked`() {
+        viewModel.onWebViewError(SiteMonitorType.METRICS)
+
+        verify(metricsViewModel).onWebViewError()
+    }
+
+    @Test
+    fun `given php logs, when onWebViewError is invoked, then php logs vm slice onWebViewError is invoked`() {
+        viewModel.onWebViewError(SiteMonitorType.PHP_LOGS)
+
+        verify(phpLogViewModel).onWebViewError()
+    }
+
+    @Test
+    fun `given webserver logs, when onWebViewError is invoked, then webserver vm slice onWebViewError is invoked`() {
+        viewModel.onWebViewError(SiteMonitorType.WEB_SERVER_LOGS)
+
+        verify(webServerViewModel).onWebViewError()
+    }
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt
new file mode 100644
index 000000000000..3fe113c67fb7
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorTabViewModelSliceTest.kt
@@ -0,0 +1,127 @@
+package org.wordpress.android.ui.sitemonitor
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.fluxc.store.AccountStore
+import org.wordpress.android.fluxc.store.SiteStore
+import org.wordpress.android.util.NetworkUtilsWrapper
+
+@ExperimentalCoroutinesApi
+class SiteMonitorTabViewModelSliceTest : BaseUnitTest() {
+    @Mock
+    private lateinit var networkUtilsWrapper: NetworkUtilsWrapper
+    @Mock
+    private lateinit var accountStore: AccountStore
+    @Mock
+    private lateinit var mapper: SiteMonitorMapper
+    @Mock
+    private lateinit var siteMonitorUtils: SiteMonitorUtils
+    @Mock
+    private lateinit var siteStore: SiteStore
+
+    private lateinit var viewModel: SiteMonitorTabViewModelSlice
+
+    val site = mock<SiteModel>()
+
+    @Before
+    fun setUp() = test {
+        viewModel = SiteMonitorTabViewModelSlice(
+            networkUtilsWrapper,
+            accountStore,
+            mapper,
+            siteMonitorUtils,
+            siteStore
+        )
+
+        whenever(accountStore.account).thenReturn(mock())
+        whenever(accountStore.account.userName).thenReturn(USER_NAME)
+        whenever(accountStore.accessToken).thenReturn(ACCESS_TOKEN)
+
+        whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true)
+        whenever(mapper.toGenericError(any())).thenReturn(mock())
+        whenever(mapper.toNoNetworkError(any())).thenReturn(mock())
+        whenever(mapper.toPrepared(any(), any(), any())).thenReturn(mock())
+
+        whenever(site.url).thenReturn(URL)
+        whenever(siteMonitorUtils.sanitizeSiteUrl(any())).thenReturn(URL)
+        whenever(siteMonitorUtils.getAuthenticationPostData(any(), any(), any(), any(), any())).thenReturn(URL)
+
+        viewModel.initialize(testScope())
+    }
+
+    @Test
+    fun `when slice is instantiated, then uiState is in preparing`() {
+        assertThat(viewModel.uiState.value).isEqualTo(SiteMonitorUiState.Preparing)
+    }
+
+    @Test
+    fun `given loadView(), when slice is started, then uiState is in prepared`() = test {
+        viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site)
+
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.Prepared::class.java)
+    }
+
+    @Test
+    fun `given null username, when slice is started, then uiState is in toGenericError`() {
+        whenever(accountStore.account.userName).thenReturn(null)
+
+        viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site)
+
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.GenericError::class.java)
+    }
+
+    @Test
+    fun `given null accessToken, when slice is started, then uiState is in toGenericError`() {
+        whenever(accountStore.accessToken).thenReturn(null)
+
+        viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site)
+
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.GenericError::class.java)
+    }
+
+    @Test
+    fun `given no network, when slice is started, then uiState is in error`() {
+        whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false)
+
+        viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site)
+
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.NoNetworkError::class.java)
+    }
+
+    @Test
+    fun `given prepared state, when url is loaded, then uiState loaded is posted`() = test {
+        viewModel.start(SiteMonitorType.METRICS, SiteMonitorTabItem.Metrics.urlTemplate, site)
+        advanceUntilIdle()
+        viewModel.onUrlLoaded()
+
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.Loaded::class.java)
+    }
+
+    @Test
+    fun `given preparing state, when url is loaded, then uiState loaded is not posted`() = test {
+        viewModel.onUrlLoaded()
+
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.Preparing::class.java)
+    }
+
+    @Test
+    fun `when web view error, then error state is posted`() = test {
+        viewModel.onWebViewError()
+
+        assertThat(viewModel.uiState.value).isInstanceOf(SiteMonitorUiState.GenericError::class.java)
+    }
+
+    companion object {
+        const val USER_NAME = "user_name"
+        const val ACCESS_TOKEN = "access_token"
+        const val URL = "test.wordpress.com"
+    }
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtilsTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtilsTest.kt
new file mode 100644
index 000000000000..0bf5076d32d5
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorUtilsTest.kt
@@ -0,0 +1,92 @@
+package org.wordpress.android.ui.sitemonitor
+
+import junit.framework.TestCase.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.verify
+import org.wordpress.android.analytics.AnalyticsTracker
+import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
+
+@RunWith(MockitoJUnitRunner::class)
+class SiteMonitorUtilsTest {
+    @Mock
+    lateinit var analyticsTrackerWrapper: AnalyticsTrackerWrapper
+
+    private lateinit var siteMonitorUtils: SiteMonitorUtils
+
+    @Before
+    fun setup() {
+        siteMonitorUtils = SiteMonitorUtils(analyticsTrackerWrapper)
+    }
+
+    @Test
+    fun `when activity is launched, then event is tracked`() {
+        siteMonitorUtils.trackActivityLaunched()
+
+        verify(analyticsTrackerWrapper).track(AnalyticsTracker.Stat.SITE_MONITORING_SCREEN_SHOWN)
+    }
+
+    @Test
+    fun `given url matches pattern, when sanitize is requested, then url is sanitized`() {
+        val result = siteMonitorUtils.sanitizeSiteUrl("http://example.com")
+
+        assertEquals("example.com", result)
+    }
+
+    @Test
+    fun `given url is null, when sanitize is requested, then url is empty`() {
+        val result = siteMonitorUtils.sanitizeSiteUrl(null)
+
+        assertEquals("", result)
+    }
+
+    @Test
+    fun `given url does not match pattern, when sanitize is requested, then url is not sanitized`() {
+        val url = "gibberish"
+        val result = siteMonitorUtils.sanitizeSiteUrl(url)
+
+        assertEquals(url, result)
+    }
+
+    @Test
+    fun `when metrics tab is launched, then event is tracked`() {
+        siteMonitorUtils.trackTabLoaded(SiteMonitorType.METRICS)
+
+        // Verify that the correct method was called on the analyticsTrackerWrapper
+        verify(analyticsTrackerWrapper).track(
+            AnalyticsTracker.Stat.SITE_MONITORING_TAB_SHOWN,
+            mapOf(
+                SiteMonitorUtils.TAB_TRACK_KEY to SiteMonitorType.METRICS.analyticsDescription
+            )
+        )
+    }
+
+    @Test
+    fun `when php logs tab is launched, then event is tracked`() {
+        siteMonitorUtils.trackTabLoaded(SiteMonitorType.PHP_LOGS)
+
+        // Verify that the correct method was called on the analyticsTrackerWrapper
+        verify(analyticsTrackerWrapper).track(
+            AnalyticsTracker.Stat.SITE_MONITORING_TAB_SHOWN,
+            mapOf(
+                SiteMonitorUtils.TAB_TRACK_KEY to SiteMonitorType.PHP_LOGS.analyticsDescription
+            )
+        )
+    }
+
+    @Test
+    fun `when web server logs tab is launched, then event is tracked`() {
+        siteMonitorUtils.trackTabLoaded(SiteMonitorType.WEB_SERVER_LOGS)
+
+        // Verify that the correct method was called on the analyticsTrackerWrapper
+        verify(analyticsTrackerWrapper).track(
+            AnalyticsTracker.Stat.SITE_MONITORING_TAB_SHOWN,
+            mapOf(
+                SiteMonitorUtils.TAB_TRACK_KEY to SiteMonitorType.WEB_SERVER_LOGS.analyticsDescription
+            )
+        )
+    }
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClientTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClientTest.kt
new file mode 100644
index 000000000000..38b11dbd998d
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/sitemonitor/SiteMonitorWebViewClientTest.kt
@@ -0,0 +1,70 @@
+package org.wordpress.android.ui.sitemonitor
+
+import android.net.Uri
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+import android.webkit.WebResourceError
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.never
+
+@ExperimentalCoroutinesApi
+class SiteMonitorWebViewClientTest : BaseUnitTest() {
+    @Mock
+    private lateinit var mockListener: SiteMonitorWebViewClient.SiteMonitorWebViewClientListener
+
+    @Mock
+    private lateinit var uri: Uri
+
+    private lateinit var webViewClient: SiteMonitorWebViewClient
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.openMocks(this)
+        webViewClient = SiteMonitorWebViewClient(mockListener, SiteMonitorType.METRICS)
+    }
+
+    @Test
+    fun `when onPageFinished, then should invoke on web view page loaded`() {
+        webViewClient.onPageFinished(mock(WebView::class.java), "https://example.com")
+
+        verify(mockListener).onWebViewPageLoaded("https://example.com", SiteMonitorType.METRICS)
+    }
+
+    @Test
+    fun `when onReceivedError, then should invoke on web view error received`() {
+        val mockRequest = mock(WebResourceRequest::class.java)
+        whenever(mockRequest.isForMainFrame).thenReturn(true)
+        val url = "https://some.domain"
+        whenever(uri.toString()).thenReturn(url)
+        whenever(mockRequest.url).thenReturn(uri)
+
+        webViewClient.onPageStarted(mock(WebView::class.java), url, null)
+        webViewClient.onReceivedError(
+            mock(WebView::class.java),
+            mockRequest,
+            mock(WebResourceError::class.java)
+        )
+
+        verify(mockListener).onWebViewReceivedError(url, SiteMonitorType.METRICS)
+    }
+
+    @Test
+    fun `when onPageFinished, then should not invoke OnReceivedError`() {
+        val url = "https://some.domain"
+
+        webViewClient.onPageFinished(mock(WebView::class.java), url)
+
+        verify(mockListener, never()).onWebViewReceivedError(anyString(), any())
+    }
+}
+