Using the Beacon API with Spring Boot
While I was travelling this summer, I saw an article about the Beacon API and put it on my list of things to check out in more detail. The Beacon API is an asynchronous, non-blocking way to send a bit of data from the browser to the server. How is it different from AJAX and the Fetch API? Mainly in that when we send a Beacon, we’re not going to get anything back. With AJAX and Fetch, the request is sent off and then gets a promise back and then is likely to do something with it. Given it’s simplicity, beacons seemed like something handy to add to the possible toolkit.
In this post, we’re going to go through an example of using the Beacon API with a Spring Boot app.
Setup
The example is going to display a dummy user profile entry page. To support that page, the application has a UserProfileDto and a UserProfileController.
public class UserProfileDto { private Long id; private String username; private String firstName; private String lastName; private String title; private String emailAddress; private String phoneNumber; private String aboutMe; //getters and setters omitted for brevity }
The UserProfileController is bare-bones and has just a method for showing the empty profile page and a dummy save method that just goes back to the profile page. There’s also a method for exposing a list of titles to the page.
@Controller public class UserProfileController { /** * Shows a blank profile page. * * @param dto * @param model * @return path to the entry template */ @RequestMapping("/") public String showEmptyProfile(UserProfileDto dto, Model model) { return "userProfile/entry"; } /** * Logs the "saved" profile and goes back to the profile page. * * @param dto * @param model * @return path to the entry template */ @RequestMapping(value="/save", method=RequestMethod.POST) public String save(UserProfileDto dto, Model model) { model.addAttribute("userProfileDto", dto); System.out.println("submitted: " + dto.toString()); return "userProfile/entry"; } /** * Exposes a collection of titles to populate a dropdown. * @return */ @ModelAttribute("titles") public List getTitles() { List titles = new ArrayList(); titles.add("Mrs."); titles.add("Mr."); titles.add("Ms."); titles.add("Miss"); titles.add("Dr."); return titles; } }
There’s also an object into which we’ll be mapping the Beacon’s JSON string.
public class BeaconDto { private String browser; private String os; private String lang; private String time; private String action; //getters and setters omitted for brevity }
The BeaconController
Before we can send any beacons, we need somewhere for them to land. We’re going to create a REST controller for that. The controller sets the base path to “/beacon” and has one at the route (e.g. “/beacon”) and also one at “/beacon/info”. The first method gets the data in a request parameter of the URL as the variable data
and just prints it out. The second method receiveInfoBeacon
gets the data in a JSON string in the body of the request and uses Jackson to parse the JSON string into our BeaconDto object. Since a beacon doesn’t expect to get anything back, the controller methods just return an empty string.
@RestController @RequestMapping("/beacon") public class BeaconController { /** * Receives a simple beacon sent with a data query string parameter. * @param data a line of text * @return an empty string, since a beacon is never expecting to get anything back. */ @RequestMapping(value = "/", method=RequestMethod.POST) public String receiveSimpleBeacon(@RequestParam("data") String data) { System.out.println("-- Incoming Simple Beacon --"); System.out.println(" Data: " + data); return ""; } /** * Receives a more elaborate beacon, with a JSON string in the body. * Parses the string into a dto and displays it. * * @param request the incoming request * @return an empty string, since a beacon is never expecting to get anything back. */ @RequestMapping(value= "/info", method=RequestMethod.POST) public String receiveInfoBeacon(HttpServletRequest request) { System.out.println(request.getContentType()); String line = null; ObjectMapper objectMapper = new ObjectMapper(); { try { line = request.getReader().readLine(); if (line != null) { //Once we have a line, use Jackson to convert the JSON string to an object BeaconDto beacon = objectMapper.readValue(line, BeaconDto.class); System.out.println("-- Incoming Beacon --"); System.out.println(" Browser: " + beacon.getBrowser()); System.out.println(" OS: " + beacon.getOs()); System.out.println(" Language: " + beacon.getLang()); System.out.println(" Time: " + beacon.getTime()); System.out.println(" Action: " + beacon.getAction()); } } catch (IOException e) { e.printStackTrace(); } } while (line !=null); return ""; } }
Sending Beacons
Now that we’ve got everything set up for receiving our beacons, we can go into our Thymeleaf template and actually send some. The <script
block of our template contains two javascript functions that correspond to our two BeaconController methods. Each method starts with the line if (!navigator.sendBeacon) return;
which is a test to see if this method would actually work and if not, there’s no point in continuing in the method. The sendBeacon
method is part of the navigator object which is a javascript object that contains information about the browser.
The sendSimpleBeacon
method takes a piece of data as a parameter and just adds it to the query string of the URL. It calls navigator.sendBeacon
and only sends a URL.
function sendSimpleBeacon(data) { if (!navigator.sendBeacon) return; var url = "http://localhost:8080/beacon/"; var data = "data=" + data; var status = navigator.sendBeacon(url + "?" + data); console.log("Status of sendBeacon: " + status); }
The sendInfoBeacon
method creates a JSON string with some random information about the browser and the time and sends the data as part of the request body.
function sendInfoBeacon(action) { if (!navigator.sendBeacon) return; var url = "http://localhost:8080/beacon/info/"; var data = JSON.stringify({ browser: navigator.appName + " " + navigator.browserCodeName + " " + navigator.appVersion, os: navigator.platform, lang: navigator.language, time: Date(), action: action }); var status = navigator.sendBeacon(url, data); console.log("Status of sendBeacon: " + status); }
Once the methods are set up, we can call them at various points in the on the page. We call sendSimpleBeacon
in the body onload event with the text ‘loading the form.’
<body onload="sendSimpleBeacon('loading the form')">
We’ll call the sendInfoBeacon
when we submit the form with the action value of ‘submit.’
<form action="save" id="saveProfileForm" th:object="${userProfileDto}" method="post" onsubmit="sendInfoBeacon('submit')">
And just for a little variety, we’ll call it when we leave the About Me text area with an action value of ‘leaving the about me box.’
<div> <label for="aboutMe">About Me:</label> <textarea rows="4" cols="80" th:field="*{aboutMe}" id="aboutMe" onblur="sendInfoBeacon('leaving about me box')"><textarea> </div>
This is what our Profile Entry form looks like after being filled in and submitted.
Here’s the output after loading the form, filling it in and clicking “Save.” You can see that it first calls the simple beacon method when the page loads. More detailed beacon information is sent when we exit the “About Me” box and when we submit the form. Lastly, the page reloads and the simple beacon is called again.
-- Incoming Simple Beacon -- Data: loading the form -- Incoming Beacon -- Browser: Netscape undefined 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 OS: Win32 Language: en-US Time: Thu Nov 01 2018 09:42:18 GMT-0400 (Eastern Daylight Time) Action: leaving about me box -- Incoming Beacon -- Browser: Netscape undefined 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 OS: Win32 Language: en-US Time: Thu Nov 01 2018 12:30:46 GMT-0400 (Eastern Daylight Time) Action: submit submitted: UserProfileDto [id=null, username=amdegregorio, firstName=Amy, lastName=DeGregorio, title=, emailAddress=123@fake.com, phoneNumber=555-555-5555, aboutMe=This is some informational text.] -- Incoming Simple Beacon -- Data: loading the form
Conclusion
The Beacon API makes it quite easy to quickly send bits of data to the server that you can then use as you need. The Beacon API has been around a few years, but the documentation (last updated in 2017), indicates that it’s an experimental technology. I wouldn’t use it for anything critical. Since you don’t get anything back from the beacon confirming if it works, it’s probably best not for anything critical anyhow. I’d be remiss if I didn’t point out that if you’re going to use the API for tracking and analytics purposes you should be mindful of the user’s Do Not Track settings and also the EU General Data Protection Regulation (GDPR).
The example code is available on GitHub.