📜 ⬆️ ⬇️

LogRock: Testing through logging

Logock

LogRock: Testing through logging


For over 2 years we have been working on our project Cleverbrush . This is a software for working with vector graphics. Working with a graphic editor involves a huge number of options for using the application. We are trying to save money and time, so we optimize everything, including testing. Covering the test case with each option is too expensive and irrational, especially since it is impossible to cover all the options.

During development, a module was created for React JS applications - LogRock (github) .

This module allows you to organize modern application logging. Based on the logs we are testing. In this article I will tell you about the intricacies of using this module and how to organize testing through logging.

What is the problem?


If you compare the program with a living organism, then the bug in it is a disease. The cause of this “disease” can be a number of factors, including the environment of a particular user. This is especially true if we are considering a web platform. Sometimes a causal relationship is very complex, and the bug that was found during testing is the result of a number of events.
')
As with human illnesses, no one can explain their symptoms better than a patient, no tester can tell what happened, better than the program itself.

What to do?


To understand what is happening, we need a list of actions that the user performed in our application.

In order for our program to inform us itself that it “hurts”, we will take the LogRock module (github) and link it with ElasticSearch, LogStash and Kibana.

image

ElasticSearch is a powerful full-text search engine. You can watch tutorial on ElasticSearch here .
LogStash is a system for collecting logs from various sources that can send logs to ElasticSearch as well.
Kibana - web interface to ElasticSearch with a large number of add-ons.

How it works?


image

In case of an error (or just on demand), the application sends logs to the server where they are saved to a file. Logstash incrementally stores data in ElasticSearch - into a database. A user logs into Kibana and sees the saved logs.

image

This is a well-tuned Kibana. It displays data from ElasticSearch. Kibana can display data in the form of tables, graphs, maps, etc., which is very convenient for analyzing and understanding what is happening with our application.

In this article I will NOT consider setting up ElasticStack!

Creating a logging system


For example, we will integrate the logging system in a one-page JS application written in React. In fact, it does not matter what framework your application will be written on. I will try to describe the system logging approach itself.

1. Customer


1.0 LogRock. Installation


LogRock Link

To install, you must perform:

npm install logrock  yarn add logrock 

1.1 LogRock. Application setup


To begin, wrap our application in the component

 import { LoggerContainer } from "logrock"; <LoggerContainer> <App /> </LoggerContainer> 

LoggerContainer is a component that responds to errors in your application and forms a stack.

A stack is an object with information about the user's operating system, browser, which mouse or keyboard button was pressed, and of course the actions sub-array, where all the user's actions that he performed on our system are recorded.

LoggerContainer has a number of settings, consider some of them

 <LoggerContainer active={true|false} limit={20} onError={stack => { sendToServer(stack); }} > <App /> </LoggerContainer> 

active - enable or disable logger

limit - sets a limit on the number of last saved actions by the user. If the user performs 21 actions, the first one in this array will automatically be deleted. Thus, we will have the last 20 actions that preceded the error.

onError is a callback that is called when an error occurs. The Stack object comes to it, in which all information about the environment, user actions, etc. is stored. It is from this callback that we need to send this data to ElasticSearch or backend, or save to a file for further analysis and monitoring.

1.2 LogRock. Logging


In order to produce high-quality logging of user actions, we will have to cover our code with log calls.

The LogRock module comes with a logger that is associated with the LoggerContainer.

Suppose we have a component

 import React, { useState } from "react"; export default function Toggle(props) { const [toggleState, setToggleState] = useState("off"); function toggle() { setToggleState(toggleState === "off" ? "on" : "off"); } return <div className={`switch ${toggleState}`} onClick={toggle} />; } 

In order to properly cover it with a log, we need to modify the toggle method

 function toggle() { let state = toggleState === "off" ? "on" : "off"; logger.info(`React.Toggle|Toggle component changed state ${state}`); setToggleState(state); } 

We added a logger in which information is divided into 2 parts. React.Toggle shows us that this action occurred at the level of React, the Toggle component, and then we have a verbal explanation of the action and the current state that came into this component. Such a division into levels is not necessary, but with this approach it will be clearer where exactly our code was executed.

We can also use the “componentDidCatch” method, which is introduced in React 16 version, in case of an error.

2. Interaction with the server


Consider the following example.

Suppose we have a method that collects user data from the backend. The method is asynchronous, part of the logic is hidden in the backend. How to properly cover this code with logs?

First, since we have a client application, all requests going to the server will take place within the same session of the user, without reloading the page. In order to associate actions on the client with actions on the server, we need to create a global SessionID and add it to the header for each request to the server. On the server, we can use any logger that will cover our logic like an example from the frontend, and in case of an error, send this data with the attached sessionID to Elastic, to the Backend table.

1. We generate SessionID on the client

 window.SESSION_ID = `sessionid-${Math.random().toString(36).substr(3, 9)}`; 

2. We must set the SessionID for all requests to the server. If we use libraries for requests, it is very easy to do this by declaring the SessionID for all requests.

 let fetch = axios.create({...}); fetch.defaults.headers.common.sessionId = window.SESSION_ID; 

3. In LoggerContainer there is a special field for SessionID

 <LoggerContainer active={true|false} sessionID={window.SESSION_ID} limit={20} onError={stack => { sendToServer(stack); }} > <App /> </LoggerContainer> 

4. The request itself (on the client) will look like this:

 logger.info(`store.getData|User is ready for loading... User ID is ${id}`); getData('/api/v1/user', { id }) .then(userData => { logger.info(`store.getData|User have already loaded. User count is ${JSON.stringify(userData)}`); }) .catch(err => { logger.error(`store.getData|User loaded fail ${err.message}`); }); 

How it will all work: we have a log, before the request on the client. According to our code, we see that data loading from the server will now begin. We attached sessionid to the request. If we have a backend logs with the addition of this SessionID and the request ended in error, then we can see what happened on the backend.

Thus, we monitor the entire cycle of our application, not only on the client, but also on the backend.

3. Tester


Working with a tester deserves a separate description of the process.

Since we have a startup, we do not have formal requirements and sometimes not everything is logical in our work.

If the tester does not understand the behavior - this is a case that at least needs to be considered. Also, often, the tester simply cannot repeat one situation twice. Since the steps that led to incorrect behavior can be numerous and nontrivial. In addition, not all errors lead to critical consequences, such as Exception. Some of them can only change the behavior of the application, but not interpreted by the system as an error. For these purposes, you can add a button in the application header to force the sending of logs. The tester sees something is wrong, presses a button and sends a Stack with actions to ElasticSearch.

image


If, after all, a critical error has occurred, we must block the interface so that the tester does not click further and not come to a dead end.

For these purposes, we display a blue screen of death.

We see at the top of the text with a stack of this critical error, and below - the actions that preceded it. We also get the error ID, the tester just select it and attach it to the ticket. Later this error can be easily found in Kibana by this ID.

image

For these purposes, the LoggerContainer has its own properties.

 <LoggerContainer active={true|false} limit={20} bsodActive={true} bsod={BSOD} onError={stack => { sendToServer(stack); }} > <App /> </LoggerContainer> 

bsodActive - enable / disable BSOD (disabling BSOD applies to production code)

bsod is a component. By default, it looks like the above screenshot.

To display the button in the UI LoggerContainer, we can use in context

 context.logger.onError(context.logger.getStackData()); 

4. LogRock. User interaction


You can output logs to the console or show them to the user, for this you need to use the stdout method:

 <LoggerContainer active={true|false} limit={20} bsodActive={true} bsod={BSOD} onError={stack => { sendToServer(stack); }} stdout={(level, message, important) => { console[level](message); if (important) { alert(message); } }} > <App /> </LoggerContainer> 

stdout is a method that is responsible for displaying messages.

In order for the message to become important, it is enough to pass the second parameter true to the logger. Thus, you can display this message for the user in a pop-up window, for example, if data loading failed, we can display an error message.

 logger.log('Something was wrong', true); 

Advanced logging


If you use Redux, or similar solutions with one Store, you can put a logger in the Middleware processing of your Actions, thus, all significant actions will pass through our system.

For effective logging, you can wrap your data in a Proxy object, and put loggers on all actions with an object.

To cover logging of third-party methods (library methods, Legacy code methods), you can use decorators - “@”.

Tips


Log applications, including in production, because better than real users, no tester will find bottlenecks.

Do not forget to specify the collection of logs in the license agreement.

DO NOT log passwords, bank details and other personal information!

The redundancy of the logs is also bad, make the signature as clear as possible.

Alternatives


As an alternative approach, I highlight:


Both services allow you to do almost the same thing and integrate into the backend.

What's next


Logging is not only the search for errors, it is also monitoring user actions, collecting data. Logging can be a good addition to Google Analytics and User Experience testing.

findings


When you release an application, life only begins for it. Be responsible for your offspring, get feedback, follow logs and improve it. Write quality software and flourish :)

PS If you want to help with the development of modules for Angular, Vue, etc. I will be glad pull requests - here .

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


All Articles