DEV Community

Sol Lee
Sol Lee

Posted on

Making a Logging Plugin with Transpiler

If you're a frontend developer, you've probably heard of or used a transpiler. With the fast development of the frontend ecosystem, the transpiler has become an integral part of the process of creating and distributing applications.

At Toss Bank, we are using transpilers in various ways to improve the development experience. Today, I will introduce an example of improving the logging process with the transpiler.

What is transpiler?

A transpiler means a tool for converting code. It is a tool for converting the ES6 grammar of JavaScript into ES5 grammar, or for converting the JSX and Typescript codes of React into JavaScript that the browser can understand. Transpiler allows you to utilize a variety of grammar while maintaining multiple browser compatibility.

Representative transpilers include Babel and SWC. At Toss Bank, we have micro-frontend structures that use Babel and SWC to suit the taste of various services.

The convenience that transpilers have brought to developers is incredible just by what I mentioned. It provides convenience in writing code so that you can focus only on business logic, and handles additional tasks on your own.

What is logging?

Toss Bank makes decisions based on data. For the right decision, we collect data on various user activities such as clicks and page views, excluding sensitive information such as personal information. This process is called logging. (The user's data is collected and used based on consent to process personal information.)

Logging is required in most service codes. So it needs to be properly abstracted and distinguished from business logic. To build user data without compromising the overall development experience.

In addition, in order to collect data efficiently, you should not log all click events on the screen, but only meaningful information. For example, if you actually click a clickable button, you should log it, and if you click a non-clickable letter or an empty screen, you should ignore it.

Let's see two different ways to proceed to logging:

Manually calling a loggin function:

<Button
  onClick={() => {
    log({ content: 'next' });
    handleClick();
  }}
>
  Next
</Button>
Enter fullscreen mode Exit fullscreen mode

Using logging component

<LoggingClick>
  <Button onClick={handleClick}>
    Next
  </Button>
</LoggingClick>
Enter fullscreen mode Exit fullscreen mode

I think there are many other different and creative ways. But what if logging takes care of itself even if you don't do anything? I'll tell you how Toss Bank originally used to log, and how to automate logging with the transpiler.

Solving the root problem

The click logging methods previously selected by the Toss Bank frontend team are as follows. Logging was handled using two types of event capturing (window listen) and data attributes.

<Button onClick={() => {}} data-click-log>
  Next
</Button>
Enter fullscreen mode Exit fullscreen mode

When the user clicks the button, it recognizes the click event through event capturing and finds the DOM with the closest data-click-log attribute to the click target. Identify the text node of the found DOM and log the information of the component clicked by the user as follows.

{
  log_type: 'click',
  content: 'next',
}
Enter fullscreen mode Exit fullscreen mode

However, it was cumbersome to add data-click-log every time. There was also a risk that if there was a typo, the log could be missing. Also, more props made it harder to find the problem.

<Button
  type="primary"
  variant="weak"
  size="large"
  data-cilck-log // typo
  onClick={() => {}}
  disabled={false}
  loading={false}
  css={{ minWidth: 100, minHeight: 80 }}
>
  Next
</Button>
Enter fullscreen mode Exit fullscreen mode

To reduce simple mistakes, we also considered adding a separate lint rule or providing autocomplete, but we decided to solve the root cause of the problem.

At Toss Bank, the logging system when an event occurs was already fully automated. So, if the data-click-log attribute could be automatically injected into the clickable element, we could fundamentally solve the problem.

If code number 1 below was converted to code number 2, the problem could be solved.

1.

<Button onClick={() => {}}>
  next
</Button>
Enter fullscreen mode Exit fullscreen mode

2.

<Button onClick={() => {}} data-click-log>
  next
</Button>
Enter fullscreen mode Exit fullscreen mode

"Converting the code appropriately under certain conditions." For this, I thought the transpiler was the right tool. It's converting the code so that the data-click-log attribute goes in, based on the condition that it's a clickable element.

Making a logging plugin with the transpiler

If the definition of "clickable" stipulates that there is an event handler that responds to a user's action, such as onClick, onChange, and onTouchStart, we could also judge the clickable condition.

Based on that, we made a plugin for SWC and Babel.

Let me introduce the plugin for Babel as an example.

const CLICK_EVENTS = ['onClick', 'onTouchStart', 'onChange', 'onMouseDown'];
const CLICK_LOG_ATTR = 'data-click-log';

function plugin({ types: t }: typeof babel): PluginObj {
  return {
    name: 'babel-plugin-tossbank-logger',
    visitor: {
      JSXOpeningElement(path) {
        const { node } = path;

        const hasOnClickAttribute = node.attributes.some(attr => {
          return CLICK_EVENTS.includes(attr.name.name);
        });

        if (hasOnClickAttribute) {
          const dataClickLogAttribute = t.jSXAttribute(t.jSXIdentifier(CLICK_LOG_ATTR), null);

          node.attributes.push(dataClickLogAttribute);
        }
      },
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Babel creates an Abstract Syntax Tree(AST) and provides it to the plugin to provide an interface for travelling and processing each node. visitor's JSXOpeningElement defines the callback to be executed while traversing the starting elements of the JSX tag.

If you look at the code above, you will tour each element and check whether there is an event handler whose node is clickable (hasClickAttribute), such as onClick, onTouchStart, onChange, and onMouseDown. *If there is a clickable event, we are injecting a data attribute called data-click-log. *

The plugin for SWC was created in Rust following the similar logic.

Developers using this logging plugin can only write business logic without clicking logging as shown in the code below.

<Button onClick={() => {}}>
  Next
</Button>
Enter fullscreen mode Exit fullscreen mode

In addition, you can automatically log what design system components you click on, and under certain conditions, you can use more if you want, such as preventing click logging.

Now, no matter who's developing it, we can always handle the same logging and ensure that the same logging results come out.

Top comments (0)