Back to Blog Home

Implementing the User Feedback API in Android

Lazar Nikolov image

Lazar Nikolov -

Implementing the User Feedback API in Android

The User Feedback API adds more context to the errors in your app by giving your users the opportunity to directly explain what happened, or what should’ve happened. It represents an additional layer of clarification on top of the Error Reporting feature. With the combination of Issues, Screenshots, and the User Feedback, you’re getting the same amount of information about a specific crash as if the user was in front of you when it happened.

In this article we’ll go through the process of implementing the User Feedback API into our Android app.

The app

To make it simpler, we’ll use a simple app with just two screens: Main screen where the error happens, and the Report a Bug screen which contains a few form fields that lets the user provide a title, what happened, what should’ve happened, and optionally provide their name and email.

Sentry Video

Watch on YouTube

You can find the source code of this tutorial on this link.

The implementation

Let’s say we want to show a Snackbar when an error happens, with a “Report” action. When the user taps the “Report” action we’re redirecting them to the “Report a Bug” screen where they can fill up the form and submit their feedback.

Let’s look at the MainActivity implementation:

class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      // create the navController and snackbarHostState
      val navController = rememberNavController()
      val snackbarHostState = remember { SnackbarHostState() }
      SentryUserFeedbackTheme {
        // add a NavHost as the root component and assign the navController
        NavHost(navController = navController, startDestination = "main") {
          // create a route for the main screen
          composable("main") {
            // wrap the main component with a Scaffold and assign the snackbarHost
            // the Scaffold is required in order to display the Snackbar
            Scaffold(
              snackbarHost = { SnackbarHost(snackbarHostState) },
              content = { innerPadding ->
                Surface(
                  modifier = Modifier
                    .fillMaxSize()
                    .padding(innerPadding),
                  color = MaterialTheme.colors.background
                ) {
                  // render the main screen component
                  // and pass the snackbarHostState and navController
                  TriggerError(snackbarHostState, navController)
                }
              }
            )
          }
          // create a route for the Report a Bug screen
          // the User Feedback API requires the eventId of the last reported exception
          // so we'll add the eventId as part of the route
          composable("reportBug/{eventId}") { backStackEntry ->
            // render the ReportBug component
            // and pass the navController and the eventId from the args
            ReportBug(
              navController,
              backStackEntry.arguments?.getString("eventId")
            )
          }
        }
      }
    }
  }
}

To create the navigation in our app we add a NavHost as the root component and we register the main and reportBug routes. Since we’re going to show a Snackbar, we also need to create a snackbarHostState and assign it to the Scaffold. Within the routes we’re rendering the TriggerError component, which is the main screen component, and the ReportBug component. We’re passing down the navController and snackbarHostState as needed to the components. This is enough to define our navigation system with Snackbar support.

Let’s look at the TriggerError component:

// define the main screen component
@Composable
fun TriggerError(
  snackbarHostState: SnackbarHostState,
  navController: NavController
) {
  // to display the snackbar in a method we need to create a coroutine scope
  val coroutineScope = rememberCoroutineScope()
  val onClick: () -> Unit = {
    try {
      // faulty method
      throw Exception("CRASH")
    } catch (e: Exception) {
      // report the exception to Sentry and obtain the eventId
      val eventId = Sentry.captureException(e)
      coroutineScope.launch {
        // launch the snackbar
        val snackbarResult = snackbarHostState.showSnackbar(
          message = "Oh no \uD83D\uDE27",
          actionLabel = "Report this!"
        )
        when (snackbarResult) {
          SnackbarResult.Dismissed -> {}
          SnackbarResult.ActionPerformed -> {
            // navigate to the Report a Bug screen
            // use the exception's eventId as part of the route
            navController.navigate("reportBug/$eventId")
          }
        }
      }
    }
  }
  Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center,
  ) {
    Button(onClick = onClick) {
      Text("Unleash chaos!")
    }
  }
}

Let’s imagine that tapping the button triggers an exception. Inside of our catch block we’re going to capture the original exception and send it to Sentry. That’ll give us the eventId of the event. Since the User Feedback API requires the eventId, we’ll keep the value in a variable and use it to construct the Report a Bug route. To show a Snackbar in a method, we need to create a coroutineScope and launch it. When the Snackbar’s action gets performed, we’re navigating the user to the Report a Bug screen.

Now let’s look at the ReportBug component:

@Composable
fun ReportBug(
  navController: NavController = rememberNavController(),
  eventId: String? = ""
) {
  // create local variables for context and form fields
  val context = LocalContext.current
  var titleState by remember { mutableStateOf("") }
  var whatHappenedState by remember { mutableStateOf("") }
  var whatShouldveHappenedState by remember { mutableStateOf("") }
  var nameState by remember { mutableStateOf("") }
  var emailState by remember { mutableStateOf("") }

  // if the eventId is null, pop back to the previous screen
  if (eventId == null) {
    navController.popBackStack()
    return
  }

  val sendFeedback: () -> Unit = {
    // create a new UserFeedback instance using the eventId and form data
    val userFeedback = UserFeedback(SentryId(eventId)).apply {
      name = nameState
      email = emailState
	    // concatenate some of the fields in the comments property
      comments = """Title: $titleState
==============================================
What Happened: $whatHappenedState
==============================================
What Should've Happened: $whatShouldveHappenedState"""
    }
    // send the feedback to Sentry
    Sentry.captureUserFeedback(userFeedback)

    // show a confirmational toast
    Toast.makeText(
      context,
      "Feedback sent. Thank you \uD83D\uDC96",
      Toast.LENGTH_LONG
    ).show()

    // pop back to the previous screen
    navController.popBackStack()
  }

  Scaffold(
    topBar = {
      // ...
      // define the top bar
      // ...
    },
    content = { innerPadding ->
      Column(
        modifier = Modifier
          .padding(innerPadding)
          .padding(top = 16.dp)
          .fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
      ) {
        // ...
        // define the form fields
        // ...
        Button(
          onClick = sendFeedback,
          modifier = Modifier.padding(top = 16.dp)
        ) {
          Text("Send")
        }
      }
    }
  )
}

The sendFeedback method uses the eventId and the local form field state variables to create a new UserFeedback instance. Since the UserFeedback accepts just a comments property, we’re concatenating some of the values, like the title, what happened, and what should’ve happened form field values. Then, using the captureUserFeedback method we’re sending the user’s feedback to Sentry, which is going to be linked with the previous exception that we reported. We can run the app now, fill up the form and open our Sentry dashboard. In the User Feedback screen we will see our feedback showing up: User Feedback Entry It’s also going to show up in the Issues screen if we open the exception: Issue Details with Feedback and Screenshot attached There’s also a “User Feedback” tab at the top that lists all submitted feedbacks for that exception. Each user will most likely provide a different level of detail in their feedback, so having all of the feedbacks in one place helps you get a clearer picture of how the error happened.

You might’ve also noticed the Screenshot there. Sentry automatically snaps a screenshot when an error happens, if we provide the attach-screenshot value in the AndroidManifest file, so we can see what the users saw at the time the crash happened. How cool is that?!

Conclusion

So there you go! By using Sentry’s User Feedback feature, you can give your users an opportunity to provide more feedback when a crash happens. Since every feedback is linked to a certain exception, Sentry groups the feedbacks that belong to the same exception so you can get a clearer picture of how the exception happened. Couple that with the Screenshots feature, and you get so much info as if you’re right there with the user when it happened.

Share

Share on Twitter
Share on Facebook
Share on HackerNews
Share on LinkedIn

Published

Sentry Sign Up CTA

Code breaks, fix it faster

Sign up for Sentry and monitor your application in minutes.

Try Sentry Free

Topics

Mobile

The best way to debug slow web pages

Listen to the Syntax Podcast

Of course we sponsor a developer podcast. Check it out on your favorite listening platform.

Listen To Syntax
    TwitterGitHubDribbbleLinkedinDiscord
© 2024 • Sentry is a registered Trademark
of Functional Software, Inc.