How Chrome Extension simplified part of my tasks

Oleksandr Reznichenko
YounitedTech
Published in
11 min readJul 11, 2019
How much do you need to type to perform your basic tasks ? Image taken from https://giphy.com/explore/hand-typing

One of the Conversion Team’s responsibilities is a web application that follows the Wizard Design Pattern. The Wizard Design Pattern implies that a user needs to fill in a lot of data in several steps to get to the end of the funnel. What the user does once or twice, our team members perform dozens or hundreds of times. Performing front-end tasks can be quite slow due to the needs of frequent data entry into the funnel.

For the sake of truth, it’s worth pointing out that there is a way to load data into a funnel for one country, but our application is a multi-country one. Such loading works in a pretty straightforward way— you only need to add a /?id=[funnel_id] parameter into the query string and the data will be loaded to the application.

Despite the fact that this approach simplifies the work, there are still disadvantages:

  • only one country supports this feature at the moment
  • each environment requires a different funnel_id to be provided for each country
  • loaded data ought to be modified because some field values have to be changed each time a new funnel is created

Having a positive experience in writing extensions for simplifying daily tasks, I asked myself “how I can avoid or automate data entry into the funnel?”

Statement of the problem

To understand the scope of the task, I listed the most painful points first:

  • filling the funnel data for any country
  • support for various scenarios; there are two possible outcomes at the end of the funnel: positive or negative. For each of them, a different data set is needed
  • quick transition to the end of the funnel to avoid having to go through each step

The implementation of the points above would greatly speed up front-end task development. Besides filling the funnel, another task that can be simplified comes to the mind — A/B tests.

A/B tests are widely used in our application, and as the engine we use Kameleoon. The difficulty with Kameleoon is that it is not clear which tests and their variations are active on a particular page. Of course, this does not mean that it is impossible to find out. You can open the Kameleoon editor (it loads the script on the page) and see all the possible tests, or look at the browser console (Kameleoon has an API that lets you see the tests launched on a page). Both options can be used, but they are far from being convenient or fast ways to view the active tests on a page. Due to this, I decided to add Kameleoon support to my list of tasks.

Search for a solution

For me, it was important not to change anything in the source code of the funnel application. My solution had to be external and independent. Since Google Chrome is our main development tool (of course, after VS Code), I started looking towards Chrome extensions. All kinds of extension types are possible, from adding markup to a page to a custom panel in the developer tools window. Adding html markup to the page sounded good to me.

Having decided on a tool to perform a task, a new problem popped up — how to fill the funnel with data? To store data on the front-end side, we use ngrx store, so I started looking for options to insert data to the store from the outside of the main application.

I went through the ngrx documentation but did not find an answer out there. The Redux DevTools extension came to my mind at that time. From the very beginning, I’ve used that extension while working on this project to track the changes of the store. It became obvious that there was a way to reach the store.

Hi to Redux DevTools

After having downloaded its source code, I started debugging the Redux DevTools extension; after a while, I figured out what I will have to deal with. Here is a diagram describing the main idea behind Redux DevTools:

Redux Devtools flow

Let’s have a closer look at this diagram. At the start,Redux DevTools creates a global variable in the window object called __REDUX_DEVTOOLS_EXTENSION__. Here is what that variable consists of:

export interface ReduxDevtoolsExtension {
connect(
options: ReduxDevtoolsExtensionConfig
): ReduxDevtoolsExtensionConnection;
send(action: any, state: any, options: ReduxDevtoolsExtensionConfig): void;
}

When the connect function is called, an object with the following API is returned:

export interface ReduxDevtoolsExtensionConnection {
subscribe(listener: (change: any) => void): void;
unsubscribe(): void;
send(action: any, state: any): void;
init(state?: any): void;
error(anyErr: any): void;
}

Two functions should be considered in this code example: subscribe and send. The subscribe function is used by ngrx for subscribing to external actions, while the send function is used for notifying about changes happening in the store.

It follows from the diagram that there is no way to connect to ngrx store directly. What you can do is provide an API which the store will use to connect to your extension; that approach is used in Redux DevTools.

A limitation of this approach is that the store can connect only to a single API. This prevents me from using the same approach since it would lead to an incompatibility between my extension and Redux DevTools (they wouldn’t work simultaneously). This is not what I wanted.

I found the solution inside the Redux DevTools extension, namely in the message exchanging approach between the page and the extension contexts. To understand what it means, let’s take a look at the following diagram:

Page and Extension contexts

As shown, an extension code can be executed in two different contexts:

  • page context: there is access to window object and DOM elements, while there is no access to the Chrome API;
  • extension context: has independent window object which has nothing in common with the window object from the running page. It doesn’t have access to the page javascript, but it is allowed to manipulate the DOM and call the Chrome API.

For data exchange between contexts, messaging is being used. It can be achieved by using the window.sendMessage function. I think this approach is used for the sake of security, since the Chrome engine can check message contents for infected code.

And so, we know that there are two contexts and you, as a developer, decide which scripts should be executed in which context. Some of the Redux DevTools scripts run in the page context, which allows the store to connect to the provided API; other scripts run in the extension context, this includes the developer tools panel. Between themselves, these scripts communicate through messages. Since there are a lot of messages and not all of them are yours, it is necessary to filter out the extra ones. We will dive deeper into this later in the article.

My idea was to pretend to be a Redux DevTools extension and send messages to the store on its behalf. An advantage of this approach is the ability to use Redux DevTools with my own extension; on the other hand, it leads to dependency with Redux DevTools. I decided that it is an acceptable trade-off.

Writing the extension

Before starting to write the code, it was necessary to solve one more problem: how to display the available options (scenarios) for filling the funnel on the page? I wanted the page to have a small widget that could be moved around the page and which would stay on top of all other elements. I found the solution in our own project: in solving one of the problems, Bilel Msekni used angular-elements, which can be leverage as an independent angular application. This approach is very convenient, since I will need to add only one custom element to the page, and Angular will do the rest for me.

The Chrome Extension development started from defining the skeleton in which the manifest.json file plays an important role:

manifest.json file

In the above code, content_scripts contains scripts that will be uploaded to the page. To determine at what point in time a particular script needs to be loaded, the run_at flag is used with the document_start or document_end(used by default) options. It is also worth paying attention to the matches parameter, it has information on which sites or domains the extension will be loaded. If your extension should be run only on certain sites, it would be better to list them in the matches option.

Once the extension skeleton is defined, it’s time to create script files and configure them to load on the page. For instance, decide which files to load in the page context and which ones in the extension context. I defined the following files for future extension:

script definitions in manifest.json

pageScriptWrap.js loads the pageScript.js file in the page context by adding the <script /> tag to the page’s head (this is how the code will be executed in the page context and will have access to the window object):

load script in page context

pageScript.js contains the code for sending messages on behalf of Redux DevTools to the store.

webPanelScriptWrap.js loads the webPanel.js script in the page context the same way pageScriptWrap.js does. The webPanel.js script is a built angular-elements application, it must also be run in the page context in order to have access to the store.

contentScript.js file contains the logic for working with the Chrome API and therefore runs in the extension context.

After having defined the main scripts and ways to load them, I started writing the code.

Angular-elements

Angular-elements is an Angular application that contains all the attributes of a common application, including change detection. It is independent, i.e. it can be used as a separate component in any application. The difference from the usual Angular application is the way for bootstrapping:

bootstrap Angular-Elements

ngDoBootstrap() is called to initialize the Angular-Elements, where createCustomElement creates the application wrapper and puts it to the customElements.define() function, which defines the new custom tag to the browser.

customElements.define() is part of the ECMAScript standard and is supported by all major browsers.

After the custom element has been declared, the<rettoua-web-panel /> tag can be added to the page. Browsers will detect the custom element and will initialize an Angular application in it. This is main feature of Angular-elements —the ability to run a full-fledged application in a single tag.

The Angular application is quite simple and is presented as a widget on the page:

widget built on Angular-Elements

The Widget is responsible for displaying a prepared scenario that can be sent to the store. It can also be used to go to the last funnel step (insurance link). The widget is very simple and does not require an additional description. What is really interesting is the way for interacting with the store.

Mimicking Redux DevTools

As I already mentioned, I decided to use the Redux DevTools extension, namely sending messages on its behalf, to send data to the store. The first thing to do is create a message in the correct format, the second is to send a message on behalf of Redux DevTools. For that purpose, I’ve created small class:

ReduxProxy for sending messages to the store

In the sample code, the source field contains the Redux DevTools identifier with which this extension determines that the message belongs to it and processes it by sending a message to the store.

The sendMessage function creates a message and extends it with a source property which acts as an identifier.

The sendAction function creates an ACTION message that corresponds to an Action in ngrx.

The sendGo function creates a [Router] Go type message to invoke the action to change the location specified in the url.

To determine the required message format, it was necessary to debug the Redux DevTools extension. At this stage, I would like to express my gratitude to the Redux DevTools extension, since it saved me from having to write a large amount of code.

Leveraging the Chrome API

The widget contains prepared scenarios that can be sent to the store by the user. In order to expand the widget capabilities, the ability to edit scenario has been added, which is very convenient if you need to cover a specific case. The Chrome API is used for saving scenarios to local storage:

Save scenario state to local storage

The persist function saves data to the storage by key, and the get function is used for retrieving data from the storage.

Since the Chrome API is available only in the extension context, the widget cannot use it as it is part of the page and executes in its context. To solve this problem I used messages and rxjs streams:

Exchange messages between page and extension contexts

In the sendMessage function, the source parameter in the payload object is important — thanks to it, messages can be identified as belonging to my extension. To receive messages, the window.addEventListener is used with the first parameter indicated as message, which allows only messages to be received; then it checks the source field, if it matches, then the message should be handled. All messages are sent to the subject stream for further processing.

The next step is to send messages when the scenario is changed; an effect is suitable for that:

Save scenario in the effect

When the scenario is changed, a new state is sent for saving to local storage where SAVE_SCENARIO_STATE is used as a key. By the same key, saved state can be retrieved from local storage. It remains only to receive the desired message and save actual value to the storage:

Save scenario state to the storage

In addition to saving the state, in the above code there is also fetching the state from the storage and sending it to the page context. Thanks to this, the widget can save and request data from local storage, which greatly expands the extension functionality.

Having written everything that was originally planned for working with scenarios, I moved on to the last but no less important part — Kameleoon.

Handle Kameleoon A/B tests

Using A/B Tests often means changing the UI or page behavior depending on the active variation. While developing and testing, it is often necessary to switch between A/B Test active variation, which is rather inconvenient due to the lack of a simple and quick way to do this.

A/B Tests are loaded on the page by a dedicated script and stored in the window.Kameleoon.API.experiments object. That object contains a list of all loaded tests and their variations. Each experiment contains important information like:

  • a test active or not
  • list with possible variations
  • active variation

Available A/B Tests are fairly easy to display on the widget by using the experiments object:

Showing Kameleoon A/B Tests on the widget

After displaying available A/B Tests, it possible to add a way to change the active variation by clicking on it. The following code shows how to do it:

Change active variation in A/B Test

Adding the ability to change the active variation of the experiment was the last feature added to the widget.

All in all, the widget provides the following features:

  • sending selected scenario to the store
  • supporting scenario editing
  • ability to go directly to last step
  • displaying loaded A/B Tests
  • changing active A/B Test variation

As a result, I got a convenient and useful extension, which greatly simplified the work with the front-end part. I published the extension in the Chrome WebStore, but made it available only with a link because the extension was created for the Conversion team only.

The sources can be found here.

--

--