📜 ⬆️ ⬇️

Error on the site ... What to do?

When the code gets into production, the programmer releases to the outside world, along with useful functionality, also errors. It is possible that, for example, on a certain website, they will sometimes lead to minor failures, which will be written off for a variety of reasons, and without getting to the bottom of the essence. It would be good for a developer who knows his business to foresee some kind of mechanism by which he can meet his mistakes, listen to their story about the adventures that they had to go through, and, as a result, correct them.



Today we want to share with you a translation of the article by programmer David Gilbertson, in which he talks about an experimental system developed by him that allows him to track and reproduce errors in web projects written in React. We believe that a similar approach can be transferred to other environments, but first things first.

Approaches to collecting error information


Perhaps you are using such a simple system for collecting information about errors in web projects (please do not throw stones at me for the following example):
')
window.onerror = err => fetch(`/errors/${err}`); 

In order to look at the error reports, it is enough to ask a friendly IT specialist to give you a file with all 404 page entries starting with /errors , and here it is - happiness.

However, the “code” that you get with this approach will not help you find out exactly where the error occurred. It is likely that this will require some improvements and error messages, which contain information about the file and the line number:

 window.addEventListener('error', e => { fetch('/errors', {   method: 'POST',   body: `${e.message} (in ${e.filename} ${e.lineno}:${e.colno})`, }); }); 

This code balances somewhere on the verge of decency, however, it is still just a skeleton of something more serious. If the error is related to specific data, then information about line numbers will not bring you any particular benefit.

It would be nice if you had a complete report on the activities of the user at the time of the error, which will give an opportunity to recreate the situation in which the failure occurred. For example, something like this:


User Activity Report

The most interesting thing is that the user went to the page with detailed information about the product (step 4) and clicked on the buy button (at the fifth, last step).

I can immediately assume that there is probably something suspicious going on with the data for a particular product, so I can follow the same link and click on the purchase button that says “Buy this for $”.

Having done this, I, of course, will see the same mistake. This particular product does not have a price, so calling toLocaleString fails. Before us is a typical oversight of a not very experienced developer.

But what if the order of user interaction with the site is much more complicated? Maybe the user was on one of the many tabs, the work with which is not reflected in the URL, or an error occurred during the validation of the data from the form. Clicking on the link and clicking on the button will not reveal such an error.

In this situation, I would like to be able to reproduce all the actions of the user before the error occurred. Ideally, just clicking during playback on a certain button that says “Next Step”.

Here's how, if I have not lost my imagination, I imagine all this for myself:


Reproducing user actions by monitoring the DOM

The error information displayed on the screen and the file opening in my editor is a merit of the Create React App.

I want to note that I really built, in the form of an experiment, a system that allows you to conduct something like "unmoderated usability testing." I wrote code that tracks user actions, and then plays them, and then asked a knowledgeable person, John, about what he thinks about all this. He said it was a stupid idea, but added that my code could be useful for reproducing errors.

Actually, this is what I want to tell here. Thanks, John.

Core system


The code in question can be found here . You might be more interested in reading it than my story. Below, I show simplified versions of functions and give links to their full texts.

I have a module, record.js , which contains several functions for intercepting various user actions. All this enters the object of the journey , which can be transferred to the server when an error occurs.

At the entry point of the application, I start gathering information by calling the startRecording() function, which looks like this:

 const journey = { meta: {}, steps: [], }; export const startRecording = () => { journey.meta.startTime = Date.now(); journey.meta.startUrl = document.location.href; journey.meta.screenWidth = window.innerWidth; journey.meta.screenHeight = window.innerHeight; journey.meta.userAgent = navigator.userAgent; }; 

If an error occurs, the journey object, for example, can be sent to the server for analysis. For this, the corresponding event handler is connected:

 window.addEventListener('error', sendErrorReport); 

At the same time, the sendErrorReport function sendErrorReport declared in the same module as the journey object:

 export const sendErrorReport = (err) => { journey.meta.endTime = Date.now(); journey.meta.error = `${err.message} (in ${err.filename} ${err.lineno}:${err.colno})`; fetch('/error', {   method: 'POST',   body: JSON.stringify(journey) }) .catch(console.error); }; 

By the way, if someone can explain why the JSON.stringify(err) command does not give me the body of an error, it will be very cool.

While all this does not bring much benefit. However, now we have a framework on which to build everything else.

If your application is state-based (that is, DOM is derived only based on some main state), then it will be easier for you to live (and I would venture to assume that the probability that you will encounter errors will be less). When you try to reproduce the error, you can simply recreate the state, which will probably give you the opportunity to cause this error.

If your application is not based on the latest technologies, it uses bindings and display of something, based directly on how the user interacts with the page, then it becomes a bit more complicated. To reproduce the error, you will need to recreate mouse clicks, events related to the loss and gain of focus elements, and, I believe, keystrokes. True, I find it difficult to say how to be if the user inserts something into the fields from the clipboard. Here I can only wish good luck in experiments.

I want to admit that I am a lazy and selfish person, so what I’m going to talk about will be aimed at the technologies I work with, namely, projects built on React and Redux.

That's what exactly I want to intercept:


Redux interception


Here is the code that is used to intercept and save Redux actions in the journey object:

 export const captureActionMiddleware = () => next => action => { journey.steps.push({   time: Date.now(),   type: INTERACTION_TYPES.REDUX_ACTION,   data: action, }); return next(action); }; 

At the beginning you can see the construction = () => next => action => { , which is simply impossible not to understand at a glance. If you still don't understand her, read this . True, I, instead of delving into it, would rather spend time on something more important, for example, I would practice painting a happy smile that would be useful to me when I was congratulated on my birthday.

The most important thing to understand in this code is the role it plays in the project. Namely, he is busy putting Redux “actions”, as they are executed, into a journey object.

Then I applied the above function when creating the Redux repository, passing the reference to it to the functions of the applyMiddleware() framework:

 const store = createStore( reducers, applyMiddleware(captureActionMiddleware), ); ReactDOM.render( <Provider store={store}>   <App /> </Provider>, document.getElementById('root') ); 

Record URL changes


The location where URL changes are intercepted depends on how the application performs the routing.

The React router doesn’t help very well in determining URL changes, so you’ll have to resort to this approach or maybe this one . I would like, with the help of the React router, just to set the handler for onRouteChange . It is worth noting here that this is necessary not only for me. For example, many are faced with the need to send information about the views of virtual pages in Google Analytics.

Anyway, I prefer to write my own routing system for most sites, as it takes only seventeen minutes, and in the end, what happens is very fast .

To intercept URL changes, I prepared the following function, which is called each time the URL changes:

 export const captureCurrentUrl = () => { journey.steps.push({   time: Date.now(),   type: INTERACTION_TYPES.URL_CHANGE,   data: document.location.href, }); }; 

I call her in two places. In the same place where I execute the history.push() command to update the URL, and also in the popstate event, which is called if the user clicks the button in the browser:

 window.addEventListener('popstate', () => { //  - ,     captureCurrentUrl(); }); 

Record user actions


This is probably the most "intrusive" mechanism for intercepting information about working with the site, since it has to be built literally everywhere. I would, if it depended only on my desires, would not bother with it. However, I encountered errors that I thought could not be reproduced without knowing exactly where the user clicked.

In any case, the task was interesting, so here I will tell you about its solution. When developing on React, I always use the <Link> and <Button> components, as a result, the development of a centralized system for intercepting clicks is quite simple. Take a look at <Link> :

 const Link = props => ( <a   href={props.to}   data-interaction-id={props.interactionId} //     onClick={(e) => {     e.preventDefault();         captureInteraction(e); //           historyManager.push(props.to);   }} >   {props.children} </a> ); 

What we are talking about here is the data-interaction-id={props.interactionId} and captureInteraction(e); .

When it comes time to play a session, I would like to highlight what the user clicked on. For this, I need some sort of selector. I can state with confidence that the items I click on have ids, but for some reason, which I don’t remember, I decided that something specifically designed for my surveillance system would be better. for user activity.

Here is the captureInteraction() function:

 export const captureInteraction = (e) => { journey.steps.push({   time: Date.now(),   type: INTERACTION_TYPES.ELEMENT_INTERACTION,   data: {     interactionId: e.target.dataset.interactionId,     textContent: e.target.textContent,   }, }); }; 

Here you can find its full code, which checks that the element, after playing the session, can be found again.

As with other information, I collect what I need, and then I execute the journey.steps.push command.

Scrolling


It only remains for me to tell you about how I record scrolling data in order to know exactly which parts of the pages the user is viewing. If, for example, the page was rewound to the very bottom and began to fill in a form, reproducing this without scrolling would not be of particular use.

I collect all consecutive scrolling events into one event in order not to waste system resources to record many small events and use Lodash , as I don’t like setting and clearing timeouts in cycles.

 const startScrollCapturing = () => { function handleScroll() {   journey.steps.push({     type: INTERACTION_TYPES.SCROLL,     data: window.scrollY,   }); } window.addEventListener('scroll', debounce(handleScroll, 200)); }; 

In the working version of this code, events associated with continuous scrolling are excluded.

The startScrollCapturing() function is called when the application is first started.

Additional ideas


Here is a small list of ideas not used in my project. Perhaps you seem worthy of implementation.


Here I made an addition after the publication of the original version of the article. In contrast to what has been voiced in several comments, I can note that the methods described in this material do not give rise to additional concerns about security or about the protection of personal data. If you are already working with confidential user data, then any requirements that apply to the collection and storage of such data should also be applied when preparing and sending error reports. If you, for example, do not automatically save form data, without asking the corresponding question to the user, then you should not automatically send error reports without asking the user about it. If you are obliged, before sending the user's personal data, to receive consent from him in the form of a check mark in a special field, the same must be done before sending an error report. In sending user data to the address /signup , when it is registered in the system, or to the address /error , when an error occurs, there is not much difference. The most important thing, and there, and there, to work with the data correctly and legally.

Perhaps you believe that we are already ending the conversation, but at this point we only recorded what the user is doing on the site. Now let's do the most interesting thing - playing the recording.

Replaying user actions


Speaking about the reproduction of actions performed by the user when working with the site, I would like to discuss two issues:


Interface for reproducing user actions


On the page, iFrame used to repeat user actions, where the website opens, where the steps previously recorded in the journey object are played.

This page downloads information about the work session during which an error occurred, and then sends each recorded step to the site, which changes its state, eventually leading to the same error.

When I open this page, I see a simple, unsightly interface, after which the site is loaded as if it is being viewed on an iPad (the usual picture of the tablet is used here, I like it more).
Here is the same animated picture that I showed at the beginning of the article. Here you can find its code.


User session replay process

When I click on the Next step button, iFrame sends a message using the iFrame.contentWindow.postMessage(nextStep, '*') construct. There is one exception related to URL changes. Namely, in this situation, the iFrame src property simply changes. For an application, this is, in fact, a complete page refresh, so whether it will work depends on how you transfer the state of the application between pages.

If you do not know, then postMessage — a Window object method created to allow interaction between different windows (in this case, this is the main window of the page and the window opened in iFrame ).

As a matter of fact, this is all that can be said about the page for reproducing user actions.

Mechanisms to manage the site from the outside


The mechanism for reproducing user actions while working with the site is implemented in the file playback.js .

When the application starts, I call a function that expects messages that go to the repository and can be called later. This is done only in development mode.

 const store = createStore( //     ); if (process.env.NODE_ENV === 'development') { startListeningForPlayback(store); } 

This is where this code is used.

The function we are interested in looks like this:

 export const startListeningForPlayback = (store) => { window.addEventListener('message', (message) => {   switch (message.data.type) {     case INTERACTION_TYPES.REDUX_ACTION:       store.dispatch(message.data.data);       break;     case INTERACTION_TYPES.SCROLL:       window.scrollTo(0, message.data.data);       break;     case INTERACTION_TYPES.ELEMENT_INTERACTION:       highlightElement(message.data.data.interactionId);       break;     default:       //  -   ,          return;   } }); }; 

Here you can find its full version.

When working with Redux actions, they are dispatched to the repository and nothing else.

When playing scrolling, exactly what can be expected is performed. In this situation, it is important that the page had the correct width. You can see, looking at the project repository, that everything will work incorrectly if the user resizes the window or, for example, rotates the mobile device on which the site is watching, but I think the scrollIntoView() — call is, in any case, a reasonable solution .

The highlightElement() function simply adds a frame around the element. Its code looks like this:

 function highlightElement(interactionId) { const el = document.querySelector(`[data-interaction-id="${interactionId}"]`); el.style.outline = '5px solid rgba(255, 0, 0, 0.67)'; setTimeout(() => {   el.style.outline = ''; }, 2000); } 

As usual, here is the complete code for this function.

Results


We looked at a simple system for collecting information about errors in React / Redux applications. Is it useful in practice? I suppose it depends on how many errors are manifested in your project, and how difficult their search turns out to be.

It may be quite enough, when an error occurs, to record the URL and save information about it, which will allow you to identify the source of the problem. Or, perhaps, the system of recording user actions will seem successful to you, but the page for replaying a session with the site is not. If, for example, you encounter errors that, say, occur only in Safari 9 on iOS, the session playback page will be useless, because with its help it will not be possible to repeat the error.

If we talk about various kinds of research, about one of which I have just told, then for me the moment of truth comes when I ask myself whether I am ready to embed what was created as a result of the experiment into one of my real projects . In this case, the answer to this question is negative.

In any case, working on a system for intercepting and reproducing user actions is an interesting experience that allowed me to learn something new. In addition, I believe that one day it all may come in handy if I need to quickly implement a monitoring system on any site.

Dear readers! How do you handle bugs? We offer to participate in the survey and share your ideas about this.

Source: https://habr.com/ru/post/344682/


All Articles