Logging client-side errors in the server in a Spring Boot and AngularJS application

When developing a software application which is expected to have a massive amount of users you absolutely need to make sure you have a proper mechanism of tracking down all kinds of errors that could possibly occur. After releasing a product to the public you should be prepared for the incoming wave of people waiting […]

by Lyubomir Petkov

July 11, 2017

4 min read

StockSnap F164KBFZ95 scaled 1 - Logging client-side errors in the server in a Spring Boot and AngularJS application

When developing a software application which is expected to have a massive amount of users you absolutely need to make sure you have a proper mechanism of tracking down all kinds of errors that could possibly occur. After releasing a product to the public you should be prepared for the incoming wave of people waiting to get their hands dirty by abusing your system with tons of scenarios you have not anticipated and corner cases.

Having bugs is practically inevitable. Good news is that you’ll know this will happen and you will also know how to cope with the situation.

In this article you will learn how to eliminate errors in the server in a Spring Boot and AngularJS application – JHipster generated application based on Java with Spring Boot for the backend and AngularJS for the frontend side.

Frontend

So, how do you “catch” browser errors? Just include the following javascript method in one of the root files of the application – you could either do it in index.html in an embedded script or in its controller – app.js. Putting it in the javascript file would be considered the better practice by most and I prefer it as well. Here’s how it looks:

window.onerror = function (msg, url, lineNo, columnNo, error) {
var clientSideError = {
msg: msg,
url: url,
lineNumber: lineNo,
columnNumber: columnNo,
error: error
};

// send the error object to the server
$http.post('/api/clientLog', clientSideError);

return false;
};

The following information about window.onerror and for more insight on how you listen to the onerror event and how exactly this function works you can read here.

onerror is a special browser event that fires whenever an uncaught JavaScript error has been thrown. You listen to the onerror event by assigning a function to window.onerror.

When an error is thrown, the following arguments are passed to the function:

  1. msg – The message associated with the error, e.g. “Uncaught ReferenceError: foo is not defined”
  2. url – The URL of the script or document associated with the error, e.g. “/dist/app.js”
  3. lineNo – The line number (if available)
  4. columnNo – The column number (if available)
  5. error – The Error object associated with this error (if available)

Backend

What happens when we reach the server-side? Here’s our REST endpoint which receives the javascript error object:

import com.codahale.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
* REST controller for logging of client-side errors.
*/
@RestController
@RequestMapping("/api/clientLog")
public class ClientSideErrorLogger {

   private final Logger log = LoggerFactory.getLogger(ClientSideErrorLogger.class);

   @Value("${clientErrorLogSizeLimit}")
   private String clientErrorLogSizeLimit;

   /**
    * POST Post client-side error log to server.
    */
   @RequestMapping(method = RequestMethod.POST,
       produces = MediaType.APPLICATION_JSON_VALUE)
   @Timed
   public ResponseEntity<Boolean> logClientSideError(HttpServletRequest request) {

       long contentLength = request.getContentLengthLong();

       Map<String, String> headersInfo = getHeadersInfo(request);

       if (contentLength > Long.parseLong(clientErrorLogSizeLimit)) {
           log.warn("This request is too big and its content will not be logged. Headers: " + headersInfo);
       } else {
           try {
               log.warn("Client-side error occurred. Request headers: " + headersInfo  + "n" + "Request body: " +  request.getReader().lines().collect(Collectors.joining(System.lineSeparator())));
           } catch (IOException e) {
               log.debug("Client-side error occurred but system cannot process error info.");
           }
       }

       return new ResponseEntity<>(true, HttpStatus.OK);
   }

   private Map<String, String> getHeadersInfo(HttpServletRequest request) {

       Map<String, String> map = new HashMap<>();

       Enumeration headerNames = request.getHeaderNames();
       while (headerNames.hasMoreElements()) {
           String key = (String) headerNames.nextElement();
           String value = request.getHeader(key);
           map.put(key, value);
       }

       return map;
   }

}

Let’s go through the code of the endpoint. First you have this clientErrorLogSizeLimit
variable which we are using to protect the system from error objects that are just too large. It is stored in the application.yml configuration file and that’s why its value is extracted with the @Value annotation. If the size of the request object (in bytes) is bigger than the one we have set we only log the headers of the object. If not, log the whole information – headers + body of the request.

Result

Here’s how an example error message that gets printed in the server logs looks like (after a little formatting):

Client-side error occurred. Request headers: {
// headers information here
}
Request body: {
"msg":"Uncaught Error: Syntax error, unrecognized expression: #/profile",
"url":"https://localhost:8081/bower_components/jquery/dist/jquery.js",
"lineNumber":1458,
"columnNumber":8,
"Error":{}
}

And as the logs also provide a timestamp for each logged message, you can more easily investigate when and what had happened in case of a problem.

Final words

It was an interesting journey for me to find all the necessary information and to make this happen for my project. I’d be happy to see how this code works for you, so looking forward to any kind of feedback or interesting outcomes from your experience. Have fun 🙂

Categories

Senior Software Engineer 1 at Dreamix