November 7, 2018 Facebook, JavaScript, History API

Roughly two weeks ago, Facebook started adding a tracking parameter, fbclid (Facebook click id?), to all external links users share. And I didn't like it so I'm hiding it.

The new parameter was probably added to somehow work around third-party tracking restrictions, and it works. Mozilla had to fix their Facebook Container which prevents Facebook from tracking users around the web. It's also busting Cloudflare's caching for example.

Modifying history entries

Luckily, I don't need to deal with such issues but I still didn't like the parameter in my URLs people see when coming from Facebook. I wanted them to see, and copy & paste, nice and clean addresses. I could strip the parameter server-side, and issue a redirect for the browser to load the same page. That would require an extra round trip and people would be “penalized” with slightly longer load times when coming to my site from Facebook.

Instead, I took a different approach: I'll just hide the URL part I don't like. No reloads, no redirects, no extra round trips. You can use history.replaceState() for that. Heavily simplified description: it will just change some parts (no, it won't change the domain) of the URL you see in the address bar of your browser but it won't (re-)load the page. The replaceState() method has a friend, pushState(), which will also insert new record in the page history, and both of them can also assign a state that will be available in popstate event. Go check out MDN article on adding and modifying history entries if you want to know more.

Removing fbclid 🚮

With this knowledge, I've created a piece of JavaScript code that will run as soon as possible and just change the URL in the browser without issuing any network requests or redirects:

(function() {
        var param = 'fbclid';
        if (location.search.indexOf(param + '=') !== -1) {
                var replace = '';
                try {
                        var url = new URL(location);
                        url.searchParams.delete(param);
                        replace = url.href;
                } catch (ex) {
                        var regExp = new RegExp('[?&]' + param + '=.*$');
                        replace = location.search.replace(regExp, '');
                        replace = location.pathname + replace + location.hash;
                }
                history.replaceState(null, '', replace);
        }
})();

This is how it works: when there's a fbclid parameter in the URL it will remove it using the nice and shiny URL API, and if that fails, primarily because the API is not supported by Internet Explorer, it will just remove the parameter and its value from the end of the URL using regular expression. It's not guaranteed that the fbclid parameter will always be at the end of the URL so the regular expression might match other parameters as well but since the code will only be used for Internet Explorer, and I didn't want to bring in a full-fledged query string parser, it's an acceptable risk for me. Eventually, the old URL will be replaced with the new one using history.replaceState().

You could use just the regular expression variant but I like the URL API so I'd like to use it for browsers that support it. Another option is to ignore Internet Explorer and don't hide the the parameter in it, which seems like the price-performance ratio. If that's what you want to do then replace the code in the if () {} block with this snippet:

try {
        var url = new URL(location);
        url.searchParams.delete(param);
        history.replaceState(null, '', url.href);
} catch (ex) {
        // pass
}

Save the code to a file called e.g. remove-fbclid.js and load it with async and defer attributes so it doesn't block page rendering:

<script src="remove-fbclid.js" async defer></script>

You can try it on this very article actually, just append ?fbclid=1337 to the URL, and load the page. To see modifying history entries in action on any page, just open developer tools, go to console, and run for example:

history.replaceState(null, '', '/admin');

The address bar will now display a new URL but the content will still be the same. The new address has essentially replaced the old one. If you hit the back button in your browser, you'll go to a page where you were before you've come to the page where history.replaceState() was executed. Unlike history.pushState(), which adds a new entry to browser history. We're using this one on Report URI, when updating URL to reflect the reports filter for bookmarking etc.

Trust no one

Oh, and there's another lesson you should have learned today: the URL displayed in the address bar might not be the URL that was actually requested and loaded which brings a lot of fun for attacks like Cross-Site Scripting. Just use developer tools for debugging sites, and don't trust the URL the browser shows you.

Analytics

If your site is built as a single-page application and URL changes are driven by History API then running the code snippet above could result in a duplicated page views. Regular analytics.js won't do that but other tools could. But I'll leave web analytics issues to pros.

Try at least adding a self-referential <link> tag with rel=canonical attribute that will point to the same page without all the unnecessary parameters including fbclid. They call it self-referencing canonical tag and it helps search engines to get the address for the page right. Using my favorite framework, Nette, I've added the following line to the template to the <head> section. It uses the n:href macro to do the job:

<link rel="canonical" n:href="//this">

When needed, I'll replace //this with a different value. For example when I publish slides, I also add URLs pointing to the individual slides which are all just a part of the same page.

Updates

15.11. Added a paragraph about analytics and canonical tags

8.11. Anonymous function to not pollute the global scope

Michal Špaček

I build web applications and I'm into web application security. I like to speak about secure development. My mission is to teach web developers how to build secure and fast web applications and why.

Public trainings

Come to my public trainings, everybody's welcome:

PHP application security
(December 11–12, 2018 Praha)

HTTPS for developers and admins
(December 13, 2018 Praha)