Making a Logging Plugin with Transpiler
Tags: babel, swc, logging, plugin
Published: 2024-06-24 05:46:43

> * This post is the translation from the original article found here: https://toss.tech/article/27750

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:
```ts
<Button
  onClick={() => {
    log({ content: 'next' });
    handleClick();
  }}
>
  Next
</Button>
```

#### Using logging component
```ts
<LoggingClick>
  <Button onClick={handleClick}>
    Next
  </Button>
</LoggingClick>
```

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`.

```ts
<Button onClick={() => {}} data-click-log>
  Next
</Button>
```

**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.

```json
{
  log_type: 'click',
  content: 'next',
}
```

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.

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

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. 
```ts
<Button onClick={() => {}}>
  next
</Button>
```
2.
```ts
<Button onClick={() => {}} data-click-log>
  next
</Button>
```

"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.

```ts
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);
        }
      },
    },
  };
}
```

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.

```ts
<Button onClick={() => {}}>
  Next
</Button>
```

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.