Skip to content

Commit

Permalink
Merge pull request #20979 from wordpress-mobile/issue/v2c-error-loggi…
Browse files Browse the repository at this point in the history
…ng-tracking

[Voice to Content] Error logging, tracking, and error state
  • Loading branch information
zwarm committed Jun 13, 2024
2 parents d2199d4 + 028cb1e commit 10b3bb3
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,11 @@ class ActivityNavigator @Inject constructor() {
.addNextIntent(intent)
.startActivities()
}

fun openIneligibleForVoiceToContent(
context: Context,
url: String
) {
WPWebViewActivity.openUrlByUsingGlobalWPCOMCredentials(context, url)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,38 @@ import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.model.jetpackai.JetpackAIAssistantFeature
import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIAssistantFeatureResponse
import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore
import org.wordpress.android.ui.voicetocontent.PrepareVoiceToContentResult.Success
import org.wordpress.android.ui.voicetocontent.PrepareVoiceToContentResult.Failure.NetworkUnavailable
import org.wordpress.android.ui.voicetocontent.PrepareVoiceToContentResult.Failure.RemoteRequestFailure
import org.wordpress.android.util.NetworkUtilsWrapper
import javax.inject.Inject

class PrepareVoiceToContentUseCase @Inject constructor(
private val jetpackAIStore: JetpackAIStore
private val jetpackAIStore: JetpackAIStore,
private val networkUtilsWrapper: NetworkUtilsWrapper,
private val logger: VoiceToContentTelemetry
) {
suspend fun execute(site: SiteModel): PrepareVoiceToContentResult =
withContext(Dispatchers.IO) {
if (!networkUtilsWrapper.isNetworkAvailable()) {
return@withContext NetworkUnavailable
}
when (val response = jetpackAIStore.fetchJetpackAIAssistantFeature(site)) {
is JetpackAIAssistantFeatureResponse.Success -> {
PrepareVoiceToContentResult.Success(model = response.model)
Success(model = response.model)
}
is JetpackAIAssistantFeatureResponse.Error -> {
PrepareVoiceToContentResult.Error
logger.logError("${response.type.name} - ${response.message}")
RemoteRequestFailure
}
}
}
}

sealed class PrepareVoiceToContentResult {
data class Success(val model: JetpackAIAssistantFeature) : PrepareVoiceToContentResult()
data object Error : PrepareVoiceToContentResult()
sealed class Failure: PrepareVoiceToContentResult() {
data object NetworkUnavailable: Failure()
data object RemoteRequestFailure: Failure()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ import org.wordpress.android.R
import org.wordpress.android.util.audio.IAudioRecorder.Companion.REQUIRED_RECORDING_PERMISSIONS
import android.provider.Settings
import androidx.compose.material.ExperimentalMaterialApi
import org.wordpress.android.ui.ActivityNavigator
import javax.inject.Inject

@AndroidEntryPoint
class VoiceToContentDialogFragment : BottomSheetDialogFragment() {
@Inject
lateinit var activityNavigator: ActivityNavigator

private val viewModel: VoiceToContentViewModel by viewModels()

@ExperimentalMaterialApi
Expand Down Expand Up @@ -49,6 +54,10 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() {
viewModel.dismiss.observe(viewLifecycleOwner) {
dismiss()
}

viewModel.onIneligibleForVoiceToContent.observe(viewLifecycleOwner) { url ->
launchIneligibleForVoiceToContent(url)
}
}

private val requestMultiplePermissionsLauncher = registerForActivityResult(
Expand Down Expand Up @@ -84,6 +93,12 @@ class VoiceToContentDialogFragment : BottomSheetDialogFragment() {
.show()
}

private fun launchIneligibleForVoiceToContent(url: String) {
context?.let {
activityNavigator.openIneligibleForVoiceToContent(it, url)
}
}

companion object {
const val TAG = "voice_to_content_fragment_tag"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
Expand Down Expand Up @@ -117,7 +118,12 @@ fun ErrorView(model: VoiceToContentUiState) {
) {
Header(model.header)
Spacer(modifier = Modifier.height(16.dp))
Text("Unable to use Voice to Content at the moment, please try again later")
Text(stringResource(id = model.errorPanel?.errorMessage?:R.string.voice_to_content_generic_error))
if (model.errorPanel?.allowRetry == true) {
IconButton(onClick = model.errorPanel.onRetryTap?:{}) {
Icon(imageVector = Icons.Default.Refresh, contentDescription = null)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.wordpress.android.ui.voicetocontent

import org.wordpress.android.analytics.AnalyticsTracker.Stat
import org.wordpress.android.fluxc.utils.AppLogWrapper
import org.wordpress.android.util.AppLog.T.POSTS
import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
import javax.inject.Inject

class VoiceToContentTelemetry @Inject constructor(
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper,
private val appLogWrapper: AppLogWrapper
) {
fun track(stat: Stat) {
analyticsTrackerWrapper.track(stat)
}

fun track(stat: Stat, properties: Map<String, Any?>) {
analyticsTrackerWrapper.track(stat, properties)
}

fun logError(message: String) {
appLogWrapper.e(POSTS, "Voice to content $message")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ data class RecordingPanelUIModel(
@StringRes val actionLabel: Int
)

data class ErrorUiModel(
@StringRes val errorMessage: Int? = null,
val allowRetry: Boolean = false,
val onRetryTap: (() -> Unit)? = null
)

enum class VoiceToContentUIStateType(val trackingName: String) {
INITIALIZING("initializing"),
READY_TO_RECORD("ready_to_record"),
Expand All @@ -45,5 +51,6 @@ data class VoiceToContentUiState(
val uiStateType: VoiceToContentUIStateType,
val header: HeaderUIModel,
val secondaryHeader: SecondaryHeaderUIModel? = null,
val recordingPanel: RecordingPanelUIModel? = null
val recordingPanel: RecordingPanelUIModel? = null,
val errorPanel: ErrorUiModel? = null
)
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package org.wordpress.android.ui.voicetocontent

import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAIQueryResponse
import org.wordpress.android.fluxc.network.rest.wpcom.jetpackai.JetpackAITranscriptionResponse
import org.wordpress.android.fluxc.store.jetpackai.JetpackAIStore
import org.wordpress.android.util.NetworkUtilsWrapper
import org.wordpress.android.ui.voicetocontent.VoiceToContentResult.Failure.NetworkUnavailable
import org.wordpress.android.ui.voicetocontent.VoiceToContentResult.Failure.RemoteRequestFailure
import org.wordpress.android.ui.voicetocontent.VoiceToContentResult.Success
import java.io.File
import javax.inject.Inject

class VoiceToContentUseCase @Inject constructor(
private val jetpackAIStore: JetpackAIStore
private val jetpackAIStore: JetpackAIStore,
private val networkUtilsWrapper: NetworkUtilsWrapper,
private val logger: VoiceToContentTelemetry
) {
companion object {
const val FEATURE = "voice_to_content"
Expand All @@ -25,6 +30,10 @@ class VoiceToContentUseCase @Inject constructor(
file: File
): VoiceToContentResult =
withContext(Dispatchers.IO) {
if (!networkUtilsWrapper.isNetworkAvailable()) {
return@withContext NetworkUnavailable
}

val transcriptionResponse = jetpackAIStore.fetchJetpackAITranscription(
siteModel,
FEATURE,
Expand All @@ -36,21 +45,17 @@ class VoiceToContentUseCase @Inject constructor(
transcriptionResponse.model
}
is JetpackAITranscriptionResponse.Error -> {
val message = "${transcriptionResponse.type} ${transcriptionResponse.message}"
Log.i(
javaClass.simpleName,
"Error transcribing audio file: $message"
)
logger.logError("${transcriptionResponse.type} ${transcriptionResponse.message}")
null
}
}

transcribedText?.let {
transcribedText?.let { transcribed ->
val response = jetpackAIStore.fetchJetpackAIQuery(
site = siteModel,
feature = FEATURE,
role = ROLE,
message = it,
message = transcribed,
stream = false,
type = TYPE
)
Expand All @@ -61,22 +66,33 @@ class VoiceToContentUseCase @Inject constructor(
// __JETPACK_AI_ERROR__ is a special marker we ask GPT to add to the request when it can’t
// understand the request for any reason, so maybe something confused GPT on some requests.
if (finalContent == JETPACK_AI_ERROR) {
return@withContext VoiceToContentResult(isError = true)
// Send back the transcribed text here
logger.logError(JETPACK_AI_ERROR)
return@withContext Success(content = transcribed)
} else {
return@withContext VoiceToContentResult(content = response.choices[0].message.content)
return@withContext Success(content = response.choices[0].message.content)
}
}

is JetpackAIQueryResponse.Error -> {
return@withContext VoiceToContentResult(isError = true)
logger.logError("${response.type.name} - ${response.message}")
return@withContext Success(content = transcribed)
}
}
} ?:return@withContext VoiceToContentResult(isError = true)
} ?: run {
logger.logError("Unable to transcribe audio content")
return@withContext RemoteRequestFailure
}
}
}

// todo: build out the result object
data class VoiceToContentResult(
val content: String? = null,
val isError: Boolean = false
)
sealed class VoiceToContentResult {
data class Success(
val content: String
): VoiceToContentResult()

sealed class Failure: VoiceToContentResult() {
data object NetworkUnavailable: Failure()
data object RemoteRequestFailure: Failure()
}
}
Loading

0 comments on commit 10b3bb3

Please sign in to comment.