AppLogger: A React Native Debugging Framework

AppLogger is a React Native mobile application framework that helps in minimizing QA (Quality Assurance) Team issue-reporting time and developer debugging time by providing a web interface for the logs generated and steps taken by the user on a device using the device's IP address. The user can keep track of their sessions and logs, as well as upload, download, and delete them.

Concept around AppLogger

When an issue is reported, developers sometimes have to spend a significant amount of time figuring out the steps to reproduce it. This time-consuming task lengthens the development process and may potentially cause software release delays.

On the developer side, QA data isn't always accurate for replicating the problem, and incomplete data can complicate troubleshooting. As a result, debugging time increases, and the issue may remain unresolved if it is not reproducible.

To solve these problems, we need to track events like user actions, application state, and navigation state, and allow them to be shared easily.

AppLogger provides React Native components with customized tracking to minimize the integration effort in any existing or new application. It also provides a generic wrapper that can be used with any React Native component. A simple track function can be used by developers to provide tracking logic to custom methods.

Developers can access the app sessions including the steps and associated logs via the web interface using the device's IP address. The web interface includes a session dashboard with a list of sessions and a session details page that includes device information as well as user action and developer logs.

Features of AppLogger

  • Session management - from the helper menu, the user can stop the current session, start a new one, and can temporarily disable tracking in the current session.
  • Download Logs - session logs can be downloaded in HTML or JSON format from the web interface.
  • Upload Logs - helper menu allows tracked session logs to be uploaded on any third-party services. To use this feature, you must implement the upload function.
  • Delete Logs - from the helper menu user can delete session logs.

Important Note: The Web Server would work only in a LAN connection i.e. both devices (one on which application is running, the other on which web interface is being accessed) should be on the same network. Also, the application must be running in the foreground.


Implementation overview

HTTP bridge (webserver):

For displaying the logs, we needed a local server. We researched how we could achieve it in react native and found a library called react-native-http-bridge that met our needs. It's simple to set up and can handle GET, POST, PUT, and DELETE requests in the application/json format.

We created a hook so that it can be easily exposed for starting and stopping the server. The server can easily be accessed through the IP address.

function useWebServer(port?: number) {

  useEffect(() => {
    deviceInfoModule.getIpAddress().then(ip => {
      console.log('-------------> ip address: ', ip);
    });
 

    WebServerHelper.startWebServer(port);
    return () => {
      WebServerHelper.stopWebServer();
    
    };
  }, [port]);
}

How did we override React Native functions with our custom functions?

We have created a hook called useLoggingFunction which takes in the props provided to a component as input and replaces the provided functions with modified versions of those functions, which contains the logic of logging. These updated props are then returned as filteredProps which are used by the component wrappers. The component wrappers provided in this framework use this hook to add logging in the components. Creating the hook ensured that the logic of logging is not repeated in every component wrapper.

The code snippet below describes how the useLoggingFunction hook is used in our component wrappers.

import {RefreshControl as RNRefreshControl} from 'react-native';

/**
 * @function RefreshControl - Component for rendering the React Native Refresh Control component with tracking.
 * @param {RefreshControlProps} props Properties for refresh control.
 * @returns {JSX.Element} Refresh Control Component.
 */
export function RefreshControl(props: RefreshControlProps): JSX.Element {
  const {filteredProps} = useLoggingFunctions(
    props,
    ComponentTypes.RefreshControl,
  );

  return (
    <RNRefreshControl {...filteredProps}>{props.children}</RNRefreshControl>
  );
}

All the functions mentioned in the Supported functions list (mentioned at the end of this article) are modified for logging.

How do generic wrapper and generic tracker functions help developers?

Along with the supported components and methods, there could be a few other components for which the developer would require to add tracking. For such components, they should be able to enable tracking with minimal effort. So we came up with a wrapper that, when wrapped around the component and provided the testID in the component's params, would start tracking the actions performed on the component.

The components provided as children components of ComponentWrapper are tracked if they contain testID in the prop. As of now, ComponentWrapper works only for direct children, which means it does not track the activity on nested components of wrapped components.

Example:

import {ComponentWrapper} from '@plgworks/applogger';

<ComponentWrapper>
    <Component testID="component_test_id" {...props}/>
</ComponentWrapper>

At the same time, for unsupported or custom user-defined methods we should be able to track actions. So we exposed a track function that tracks the custom user action on the method with log data, which contains log/step description, action type, params, and so on.

Its implementation could be as follows:

import {useCallback} from 'react';
import {Image} from 'react-native';
import {getLogTracker} from '@plgworks/applogger';

function CustomTrackingDemo() {
    const imageTestID = 'custom_tracking_demo_image';

    const fnOnError = useCallback((error) => {
        const logTracker = getLogTracker();
        logTracker.track({
            description: `Encountered error on image load ${error}`,
            type: 'ImageLoadError',
            params: {
                testId: imageTestID,
              },
            },
        });
    }, []);

    return <Image src="image_url" testID={imageTestID} onError={fnOnError} />
}

This way we can track the activity on any custom or react-native components and methods.

The Helper menu:

The helper menu was added for quick and easy access to perform actions on sessions and logs. We used the react-native-shake library to detect shake events and launch the helper menu. The helper menu contains the following options:

  • Share server URL: Using this option you can share the Web Interface URL.
  • Upload Logs:  This option helps users upload logs on any third-party service based on the configurations provided.
  • Delete current session logs:  As the name indicates, it deletes the current session logs and creates a new session.
  • Delete all logs:  This option deletes all the log sessions stored on the device.
  • Disable/Enable tracking - Disables/Enables the log tracking for the current session.
  • Stop Session/Start Session - Stop session stops the current session and the log tracking is disabled. Start session creates a new session and resumes the log tracking.

Download and upload functionalities :

Taking into account the existing issues in current workflows, we implemented a feature where anyone may download logs in JSON or HTML format to deliver to the developer for effective debugging or might be used as an attachment while logging the bugs.

For downloading the logs, an option is available on the web interface which downloads a zip file containing the JSON, text, and HTML files.

To download the files, we hit XHR request to the http-bridge web server which returns data containing logs. To create and zip the log files, we used the JSZip library.

const zip = new JSZip();
     zip.file("Log.txt", content);
     zip.file("Log.json", content);
const htmlData=encodeURIComponent(document.documentElement.outerHTML)
     zip.file("Log.html", htmlData);

This zip file can be downloaded using the generateAsync function provided by JSZip.

zip.generateAsync({
             type: "blob"
         })
         .then(function(content) {
             const element = document.createElement("a");
             element.setAttribute("href", window.URL.createObjectURL(content));
             element.setAttribute("download", filename);
             element.style.display = "none";
             document.body.appendChild(element);
             element.click();
             document.body.removeChild(element);
         });
     }

These logs could also be uploaded to AWS S3, Firebase, or any other storage services which can be triggered from the helper menu. For this, we accept an uploader callback function while configuring. This uploader function will be called upon the creation of JSON log files in the document directory of the application.

The createAndUploadJSONLogs function in AppLogger looks something like this:

function createAndUploadJSONLogs() {
     var data; // get log data from storage;
     var filename = `/session_logs.json`;
     getJsonLogFile(data, filename)
     .then((logFilePath) => {
       var logFiles: []string = [];
       logFiles.push(logFilePath);
       // Config is initialized and stored when app starts
       config?.uploadLogs?.(logFiles, () => {
         deleteSessionLogFiles(sessionLogFilePaths); // Function to delete log files
       }).then(() => {
         console.log('Log upload successful');
       }).catch(() => {
         console.log('Could not upload logs');
       })
     })
     .catch(() => {});
   }


In this function, first, we get the JSON data stored in the async store. getJsonLogFile is an async function that uses react-native-fs to create a JSON file and write data to it. Upon writing the data, it returns the absolute path of the log file which is then stored in an array. The uploadLogs function, an uploader function provided by the developer while configuring, is then called with the file paths array to upload all the files to external storage. It also accepts a callback function as an argument which could be called upon successful upload to delete the log files from app storage.


Installation & Usage:

For Installation and usage, you can refer to our npm package @plgworks/applogger.

Components provided by AppLogger

Following are some of the react-native components that AppLogger provides with tracking:

  • Button
  • Pressable
  • RefreshControl
  • Switch
  • TextInput
  • TouchableHighlight
  • TouchableOpacity
  • TouchableWithoutFeedback

Supported functions list

Following are a few functions that have log tracking enabled in them:

  • onChange
  • onPress
  • onLongPress
  • onPressIn
  • onPressOut
  • onChangeText
  • onContentSizeChange
  • onEndEditing
  • onFocus
  • onKeyPress
  • onLayout
  • onScroll
  • onSelectionChange
  • onSubmitEditing
  • onRefresh
  • onValueChange

Screenshots

  • Web Interface




Mohit Charkha

Mohit Charkha

Mobile Application Developer at PLG works
Pune, India
Dhruv Tailor

Dhruv Tailor

Software Engineer developing mobile apps at PLG Works
Surat, Gujarat
Daksh
Hey, I Am Daksh Bhardwaj, A Tech Enthusiast , My Core Interests Lies In Hybrid App Development And Web Development Using Latest Tools And Techniques. Improving every day!
Harsh Siriah

Harsh Siriah

Hi, I'm Harsh! A geek with a love for dogs & mobiles.
Pune, India
Vinay Harwani

Vinay Harwani

Hi, I am Vinay. I am a software engineer particularly interested in mobile application and backend development.
Pune, India