I want to start with a small disclaimer that I don’t fully understand what exactly went wrong and why the things we did fixed the problem.

I’m pretty sure I would be able to find out by staring long enough at very complex code, but at some point, you have to stop spending time trying to understand something 100% and be happy you understand it for 90%.

Exploring a hard-to-reproduce bug

The application we’re working on is using cookiebot to manage cookie consent in order to be GDPR compliant. After I arrived at the customer, one of the first things I had to do was to create an account on the platform.

As usual when I first visit a website, I denied all unnecessary cookies and continued doing what I was doing without being tracked. A little while later, I noticed some JavaScript wasn’t working in the app. I told this to my onboarding buddy who promptly asked me whether I had accepted or denied the unnecessary cookies.

This was not the good first impression I was hoping for. Denying cookies made parts of the app unusable. I made a mental note, accepted the cookies, and continued my onboarding.

As it often goes with mental notes, this one started gathering dust quite soon. The app worked for me; there were plenty of other problems to tackle and pretty soon I had moved this particular issue to a small corner far away in my brain.

Until a couple of weeks ago… when we received a bug report from a rather big client. Some JavaScript was not working. After an initial round of works on my machine, this dusty mental note found its way to the front of my brain again and I asked whether the bug reporter had accepted or denied cookies. Turns out they had clicked Deny.

Since this was a pretty important customer and the bug reporter (and many colleagues) insisted on not accepting tracking cookies — which obviously is their right — all of a sudden this small annoyance became a critical bug for the business.

So we put on our fanciest bug hunting helmet and off we went into the jungle that is debugging legacy frontend code.

The hunt for a tricky bug

Now that we could properly reproduce the bug, we quickly figured out that the broken part of the software was a Vue component. A quick check on other pages confirmed that other components were also broken. Other non-Vue JavaScript still mostly worked.

The problem was that we had absolutely no idea what was broken. There were no errors, warnings, or any other clues in the developer console. The broken code didn’t have a lot in common, so there was nothing really obvious jumping out that could clarify the bug.

Apart from the usual legacy code funkiness, there wasn’t anything special about the broken components, so we dismissed the idea that the bug somehow resided in the components themselves.

The next logical step was checking whether cookiebot itself was somehow broken. If accepting or denying cookies would change the code behavior, cookiebot had to be involved somehow, right?

We spent quite some time on the idea that blocking a certain cookie would break some code relying on the presence of said cookie. We checked each non-essential cookie, played with the settings in cookiebot for that cookie, and searched our code for any occurrence of the cookie.

This didn’t change anything.

Next up: did we implement cookiebot correctly? Cookiebot relies on a file that needs to be included in your HTML. The setup guide states, in bold, that this must be the very first script within the HEAD tag of your website. If it’s in bold, it must be important!

fixing cookiebot

Guess what? This is not the way it was implemented. Surely that must’ve been the cause of our problems! That’s an easy fix.

We moved the cookiebot script to the top of the HEAD-tag, reloaded the application, denied cookies, and… the code was still broken. Now what?

We did what every experienced developer would do. We googled “cookiebot breaks javascript.” It turns out that many people had broken websites combined with using Cookiebot, but nothing resembled the specific problem we had.

Isolating the root cause of a bug

We went back to our first idea that maybe we did something in our code that would break things, so we tried to dumb down the Vue component we had. Instead of an (overly) complex tab component, we started testing with a really simple button component. Our simple button component was so simple it would log a message to the console when we clicked it.

<template>
    <div>
        <button @click="handleClick">Click me</button>
    </div>
</template>

<script>
export default {
    name: "ButtonComponent",
    methods: {
        handleClick () {
            console.log("Clicked");
        }
    }
}
</script>

We replaced our original component with this one and hoped for the best. We hoped in vain. Clicking the button would not result in a message being logged to the console. The bug was still around.

This is when the madness set in.

We started playing around a little bit, hoping for a flash of inspiration and found out some interesting behaviors. Vue components were not completely broken.

The mounted hook would still work and by using a direct onclick instead of @click, we could still trigger a reaction. Not that this made any sense to us, but at least it was something.

Mastery of your tools simplifies the process

This is also when I learned about a cool little feature of the Firefox Web Console. When opening the inspector tab, you can see the event listeners attached to a certain element.

fixing cookiebot

This feature helped us easily see that when using onclick the event listener would be attached to the target element. But (!) this would not happen when using the @click method provided by Vue.

I’m not too familiar with the internal workings of Vue, but this would surely prove to be useful at some point to validate any theories we had. Since the Vue component itself was certainly not the problem, we had to find the cause somewhere in the rest of the code.

Create a simple case for a complex bug

We dumbed everything down as much as possible:

  • We removed as much backend code as possible (without removing it entirely),
  • any authentication was cut out,
  • and we removed as much layout and other surrounding html as possible.

It’s a lot easier to think about some functionality when there is no surrounding code trying to invade your mental space.

Our bug still persisted, but this time it was expected. I cannot start to imagine the dread I would have felt if removing back end code would have solved the issue.

We ended up with a very simple page containing:

  • our ButtonComponent,
  • all of the app’s JavaScript,
  • and, of course, the Cookiebot snippet.

From here on it became a game of elimination. We removed all JavaScript files, one by one, hoping this would lead us to a specific code snippet that once removed would turn out to be the baddy.

After some time filled with trying and retrying a couple of things, we found that removing the import of 2 specific files solved the issue. Removing just one didn’t solve the problem; removing the other didn’t solve it either. They both had to be removed.

Functionally, they were not related, so we looked for code that looked similar in both files. Luckily, these weren’t complex files and we quickly found that both files contained an event listener on the load event of the window.

window.addEventListener('load', someFunction)

Commenting these out confirmed that these event listeners were indeed the culprit.

I set up a repository that contains code that reproduces the bug and resembles the code we ended up with. The repo can be found at https://github.com/peterpacket/cookiebot-bug, but the relevant files are here.

resources/js/ButtonComponent.vue

<template>
    <div>
        <button onclick="console.log('Onclicked me')">Onclick me</button>
        <button @click="handleClick">@Click me</button>
    </div>
</template>

<script>
export default {
    name: "ButtonComponent",
    methods: {
        handleClick () {
            console.log("@Clicked me");
        }
    }
}
</script>

Setting up Vue (and Bootstrap) in resources/js/app.js

import Vue from 'vue';
import './bootstrap';

window.Vue = Vue;

Vue.component('ButtonComponent', require('./ButtonComponent').default);

window.onload = function () {
    const app = new Vue({
        el: '#app',
    });
};

Some functionality usable all over the app defined in resources/js/global.js

window.addEventListener('load', () => {
    console.log('loaded')
});

And we tie it all together in resources/views/welcome.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="{{ config('app.cookiebot_key') }}" data-blockingmode="auto" type="text/javascript"></script>    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Cookiebot demo</title>

    <!-- Fonts -->
    <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
    <script type="text/javascript" src="{{ asset('js/global.js') }}"></script>
    <script type="text/javascript" src="{{ asset('js/app.js') }}"></script>
    <!-- Styles -->
</head>
<body class="antialiased">
<div id="app">
    <ButtonComponent is="button-component"></ButtonComponent>
</div>
</body>
</html>

Fixing a complex bug after isolating the cause

Of course, after finding the problem, we also needed to fix it. A first idea was to attach the event listener to the document instead of the window.

document.addEventListener('load', someFunction);

But apparently that’s not something that works in Google Chrome. We tried some other events and in the end we settled on the following

document.addEventListener('readystatechange', () => {
	if (document.readyState === 'complete') {
		someFunction();
	}
});

This replicated the behavior of the load event listener sufficiently and didn’t interfere with any cookiebot code as far as we could tell. All that was left to do was to restore all the code we removed along the way, commit, merge, and deploy. Customers happy, our client happy, me happy😀

But, but, but…

What happened, exactly? Why did this break Cookiebot and why did some part of the Vue component still work and another part didn’t?

Remember that disclaimer I wrote before I started this compelling story? The honest answer is that I don’t know.

I checked the Cookiebot code that we included in our app and I didn’t understand a lot of it. I managed to figure out that it does some wild stuff with event listeners. But if you have a function called overrideEventListeners, I’m not sure I really want to dive in further than that.

I know that Cookiebot works by blocking the loading of all your own scripts (hence why it should be first in the HEAD tag). Next, it will load a configuration file that contains a list of all your cookies, which consent category they belong in, and which script sets them.

Based on that configuration file and the consent given by the user, it will load the scripts that don’t set cookies and the ones that set allowed cookies. This can be seen in the network tab of your developer tools.

cookiebot fix it

I can see how this could interfere with our own code that relies on the load event, but I don’t see how this would prevent Vue from doing its thing. The stories don’t add up. Surely others must have encountered this before, but even some googling after the fact didn’t bring any new insights.

Another train of thought we had was that our own event listeners would prevent Cookiebot from functioning properly. If cookiebot would block our scripts and crash before it loads and executes them again, it could explain why things are broken. But this reasoning is also flawed in many different ways.

I hope that at some point someone — maybe you? —can tell me what happened, or perhaps I’ll encounter another issue that puts things into perspective.

But maybe this particular bug will just end up as a dusty mental note, somewhere in the back of my brain, next to all the other almost-understood bugs.

Happy bug hunting.