DEV Community

Savannah Copland ๐Ÿ‘‹
Savannah Copland ๐Ÿ‘‹

Posted on • Originally published at

How the Web Audio API is used for browser fingerprinting

Did you know that you can identify web browsers without using cookies or asking for permissions?

This is known as โ€œbrowser fingerprintingโ€ and it works by reading browser attributes and combining them together into a single identifier. This identifier is stateless and works well in normal and incognito modes.

Graphic of browser fingerprinting

When generating a browser identifier, we can read browser attributes directly or use attribute processing techniques first. One of the creative techniques that weโ€™ll discuss today is audio fingerprinting.

Audio fingerprinting is a valuable technique because it is relatively unique and stable. Its uniqueness comes from the internal complexity and sophistication of the Web Audio API. The stability is achieved because the audio source that weโ€™ll use is a sequence of numbers, generated mathematically. Those numbers will later be combined into a single audio fingerprint value.

Before we dive into the technical implementation, we need to understand a few ideas from the Web Audio API and its building blocks.

A brief overview of the Web Audio API

The Web Audio API is a powerful system for handling audio operations. It is designed to work inside an AudioContext by linking together audio nodes and building an audio graph. A single AudioContext can handle multiple types of audio sources that plug into other nodes and form chains of audio processing.

Graphic of audio context

A source can be an audio element, a stream, or an in-memory source generated mathematically with an Oscillator. Weโ€™ll be using the Oscillator for our purposes and then connecting it to other nodes for additional processing.

Before we dive into the audio fingerprint implementation details, itโ€™s helpful to review all of the building blocks of the API that weโ€™ll be using.


AudioContext represents an entire chain built from audio nodes linked together. It controls the creation of the nodes and execution of the audio processing. You always start by creating an instance of AudioContext before you do anything else. Itโ€™s a good practice to create a single AudioContext instance and reuse it for all future processing.

AudioContext has a destination property that represents the destination of all audio from that context.

There also exists a special type of AudioContext: OfflineAudioContext. The main difference is that it does not render the audio to the device hardware. Instead, it generates the audio as fast as possible and saves it into an AudioBuffer. Thus, the destination of the OfflineAudioContext will be an in-memory data structure, while with a regular AudioContext, the destination will be an audio-rendering device.

When creating an instance of OfflineAudioContext, we pass 3 arguments: the number of channels, the total number of samples and a sample rate in samples per second.

const AudioContext = 
  window.OfflineAudioContext ||
const context = new AudioContext(1, 5000, 44100)
Enter fullscreen mode Exit fullscreen mode


An AudioBuffer represents an audio snippet, stored in memory. Itโ€™s designed to hold small snippets. The data is represented internally in Linear PCM with each sample represented by a 32-bit float between -1.0 and 1.0. It can hold multiple channels, but for our purposes weโ€™ll use only one channel.

Diagram of 32-bit numbers


When working with audio, we always need a source. An oscillator is a good candidate, because it generates samples mathematically, as opposed to playing an audio file. In its simplest form, an oscillator generates a periodic waveform with a specified frequency.

The default shape is a sine wave.

Sine wave
We made a live demo of this! You can play with the real deal on our blog.

Itโ€™s also possible to generate other types of waves, such as square, sawtooth, and triangle.

The default frequency is 440 Hz, which is a standard A4 note.


The Web Audio API provides a DynamicsCompressorNode, which lowers the volume of the loudest parts of the signal and helps prevent distortion or clipping.

DynamicsCompressorNode has many interesting properties that weโ€™ll use. These properties will help create more variability between browsers.

  • Threshold - value in decibels above which the compressor will start taking effect.
  • Knee - value in decibels representing the range above the threshold where the curve smoothly transitions to the compressed portion.
  • Ratio - amount of input change, in dB, needed for a 1 dB change in the output.
  • Reduction - float representing the amount of gain reduction currently applied by the compressor to the signal.
  • Attack - the amount of time, in seconds, required to reduce the gain by 10 dB. This value can be a decimal.
  • Release - the amount of time, in seconds, required to increase the gain by 10 dB.

Compressor attributes
We made a live demo of this! You can play with the real deal on our blog.

How the audio fingerprint is calculated

Now that we have all the concepts we need, we can start working on our audio fingerprinting code.

Safari doesnโ€™t support unprefixed OfflineAudioContext, but does support
webkitOfflineAudioContext, so weโ€™ll use this method to make it work in Chrome and Safari:

const AudioContext =
  window.OfflineAudioContext ||
Enter fullscreen mode Exit fullscreen mode

Now we create an AudioContext instance. Weโ€™ll use one channel, a 44,100 sample rate and 5,000 samples total, which will make it about 113 ms long.

const context = new AudioContext(1, 5000, 44100)
Enter fullscreen mode Exit fullscreen mode

Next letโ€™s create a sound source - an oscillator instance. It will generate a triangular-shaped sound wave that will fluctuate 1,000 times per second (1,000 Hz).

const oscillator = context.createOscillator()
oscillator.type = "triangle"
oscillator.frequency.value = 1000
Enter fullscreen mode Exit fullscreen mode

Now letโ€™s create a compressor to add more variety and transform the original signal. Note that the values for all these parameters are arbitrary and are only meant to change the source signal in interesting ways. We could use other values and it would still work.

const compressor = context.createDynamicsCompressor()
compressor.threshold.value = -50
compressor.knee.value = 40
compressor.ratio.value = 12
compressor.reduction.value = 20
compressor.attack.value = 0
compressor.release.value = 0.2
Enter fullscreen mode Exit fullscreen mode

Letโ€™s connect our nodes together: oscillator to compressor, and compressor to the context destination.

Enter fullscreen mode Exit fullscreen mode

It is time to generate the audio snippet. Weโ€™ll use the oncomplete event to get the result when itโ€™s ready.

context.oncomplete = event => {
  // We have only one channel, so we get it by index
  const samples = event.renderedBuffer.getChannelData(0)
Enter fullscreen mode Exit fullscreen mode

Samples is an array of floating-point values that represents the uncompressed sound. Now we need to calculate a single value from that array.

Letโ€™s do it by simply summing up a slice of the array values:

function calculateHash(samples) {
  let hash = 0
  for (let i = 0; i < samples.length; ++i) {
    hash += Math.abs(samples[i])
  return hash

Enter fullscreen mode Exit fullscreen mode

Now we are ready to generate the audio fingerprint. When I run it on Chrome on MacOS I get the value:

  • 101.45647543197447

Thatโ€™s all there is to it. Our audio fingerprint is this number!

You can check out a production implementation in our open source browser fingerprinting library.

If I try executing the code in Safari, I get a different number:

  • 79.58850509487092

And get another unique result in Firefox:

  • 80.95458510611206

Every browser we have on our testing laptops generate a different value. This value is very stable and remains the same in incognito mode.

This value depends on the underlying hardware and OS, and in your case may be different.

Why the audio fingerprint varies by browser

Letโ€™s take a closer look at why the values are different in different browsers. Weโ€™ll examine a single oscillation wave in both Chrome and Firefox.

First, letโ€™s reduce the duration of our audio snippet to 1/2000th of a second, which corresponds to a single wave and examine the values that make up that wave.

We need to change our context duration to 23 samples, which roughly corresponds to a 1/2000th of a second. Weโ€™ll also skip the compressor for now and only examine the differences of the unmodified oscillator signal.

const context = new AudioContext(1, 23, 44100)
Enter fullscreen mode Exit fullscreen mode

Here is how a single triangular oscillation looks in both Chrome and Firefox now:

One wave

However the underlying values are different between the two browsers (Iโ€™m showing only the first 3 values for simplicity):

Chrome: Firefox:
0.08988945186138153 0.09155717492103577
0.18264609575271606 0.18603470921516418
0.2712443470954895 0.2762767672538757

Letโ€™s take a look at this demo to visually see those differences.

Audio wave by browser
We made a live demo of this! You can play with the real deal on our blog.

Historically, all major browser engines (Blink, WebKit, and Gecko) based their Web Audio API implementations on code that was originally developed by Google in 2011 and 2012 for the WebKit project.

Examples of Google contributions to the Webkit project include:
creation of OfflineAudioContext,
creation of OscillatorNode, creation of DynamicsCompressorNode.

Since then browser developers have made a lot of small changes. These changes, compounded by the large number of mathematical operations involved, lead to fingerprinting differences. Audio signal processing uses floating point arithmetic, which also contributes to discrepancies in calculations.

You can see how these things are implemented now in the three major browser engines:

Additionally, browsers use different implementations for different CPU architectures and OSes to leverage features like SIMD. For example, Chrome uses a separate fast Fourier transform implementation on macOS (producing a different oscillator signal) and different vector operation implementations on different CPU architectures (which are used in the DynamicsCompressor implementation). These platform-specific changes also contribute to differences in the final audio fingerprint.

Fingerprint results also depend on the Android version (itโ€™s different in Android 9 and 10 on the same devices for example).

According to browser source code, audio processing doesnโ€™t use dedicated audio hardware or OS featuresโ€”all calculations are done by the CPU.


When we started to use audio fingerprinting in production, we aimed to achieve good browser compatibility, stability and performance. For high browser compatibility, we also looked at privacy-focused browsers, such as Tor and Brave.


As you can see on, OfflineAudioContext works almost everywhere. But there are some cases that need special handling.

The first case is iOS 11 or older. It does support OfflineAudioContext, but the rendering only starts if triggered by a user action, for example by a button click. If context.startRendering is not triggered by a user action, the context.state will be suspended and rendering will hang indefinitely unless you add a timeout. There are not many users who still use this iOS version, so we decided to disable audio fingerprinting for them.

The second case are browsers on iOS 12 or newer. They can reject starting audio processing if the page is in the background. Luckily, browsers allow you to resume the processing when the page returns to the foreground.
When the page is activated, we attempt calling context.startRendering() several times until the context.state becomes running. If the processing doesnโ€™t start after several attempts, the code stops. We also use a regular setTimeout on top of our retry strategy in case of an unexpected error or freeze. You can see a code example here.


In the case of the Tor browser, everything is simple. Web Audio API is disabled there, so audio fingerprinting is not possible.


With Brave, the situation is more nuanced. Brave is a privacy-focused browser based on Blink. It is known to slightly randomize the audio sample values, which it calls โ€œfarblingโ€.

Farbling is Braveโ€™s term for slightly randomizing the output of semi-identifying browser features, in a way thatโ€™s difficult for websites to detect, but doesnโ€™t break benign, user-serving websites. These โ€œfarbledโ€ values are deterministically generated using a per-session, per-eTLD+1 seed so that a site will get the exact same value each time it tries to fingerprint within the same session, but that different sites will get different values, and the same site will get different values on the next session. This technique has its roots in prior privacy research, including the PriVaricator (Nikiforakis et al, WWW 2015) and FPRandom (Laperdrix et al, ESSoS 2017) projects.

Brave offers three levels of farbling (users can choose the level they want in settings):

  • Disabled โ€” no farbling is applied. The fingerprint is the same as in other Blink browsers such as Chrome.
  • Standard โ€” This is the default value. The audio signal values are multiplied by a fixed number, called the โ€œfudgeโ€ factor, that is stable for a given domain within a user session. In practice it means that the audio wave sounds and looks the same, but has tiny variations that make it difficult to use in fingerprinting.
  • Strict โ€” the sound wave is replaced with a pseudo-random sequence.

The farbling modifies the original Blink AudioBuffer by transforming the original audio values.

Reverting Brave standard farbling

To revert the farbling, we need to obtain the fudge factor first. Then we can get back the original buffer by dividing the farbled values by the fudge factor:

async function getFudgeFactor() {
  const context = new AudioContext(1, 1, 44100)
  const inputBuffer = context.createBuffer(1, 1, 44100)
  inputBuffer.getChannelData(0)[0] = 1

  const inputNode = context.createBufferSource()
  inputNode.buffer = inputBuffer

  // See the renderAudio implementation 
  // at
  const outputBuffer = await renderAudio(context)
  return outputBuffer.getChannelData(0)[0]

const [fingerprint, fudgeFactor] = await Promise.all([
  // This function is the fingerprint algorithm described
  // in the โ€œHow audio fingerprint is calculatedโ€ section
const restoredFingerprint = fingerprint / fudgeFactor
Enter fullscreen mode Exit fullscreen mode

Unfortunately, floating point operations lack the required precision to get the original samples exactly. The table below shows restored audio fingerprint in different cases and shows how close they are to the original values:

OS, browser Fingerprint Absolute difference between the target fingerprint
macOS 11, Chrome 89 (the target fingerprint) 124.0434806260746 n/a
macOS 11, Brave 1.21 (same device and OS) Various fingerprints after browser restarts:
0.00000014% โ€“ 0.00000214%
Windows 10, Chrome 89 124.04347527516074 0.00000431%
Windows 10, Brave 1.21 Various fingerprints after browser restarts:
0.00000364% โ€“ 0.00000679%
Android 11, Chrome 89 124.08075528279005 0.03%
Android 9, Chrome 89 124.08074500028306 0.03%
ChromeOS 89 124.04347721464 0.00000275%
macOS 11, Safari 14 35.10893232002854 71.7%
macOS 11, Firefox 86 35.7383295930922 71.2%

As you can see, the restored Brave fingerprints are closer to the original fingerprints than to other browsersโ€™ fingerprints. This means that you can use a fuzzy algorithm to match them. For example, if the difference between a pair of audio fingerprint numbers is more than 0.0000022%, you can assume that these are different devices or browsers.


Web Audio API rendering

Letโ€™s take a look at what happens under the hood in Chrome during audio fingerprint generation. In the screenshot below, the horizontal axis is time, the rows are execution threads, and the bars are time slices when the browser is busy. You can learn more about the performance panel in this Chrome article. The audio processing starts at 809.6 ms and completes at 814.1 ms:

The main thread, labeled as โ€œMainโ€ on the image, handles user input (mouse movements, clicks, taps, etc) and animation. When the main thread is busy, the page freezes. Itโ€™s a good practice to avoid running blocking operations on the main thread for more than several milliseconds.

As you can see on the image above, the browser delegates some work to the OfflineAudioRender thread, freeing the main thread.
Therefore the page stays responsive during most of the audio fingerprint calculation.

Web Audio API is not available in web workers, so we cannot calculate audio fingerprints there.

Performance summary in different browsers

The table below shows the time it takes to get a fingerprint on different browsers and devices. The time is measured immediately after the cold page load.

Device, OS, browser Time to fingerprint
MacBook Pro 2015 (Core i7), macOS 11, Safari 14 5 ms
MacBook Pro 2015 (Core i7), macOS 11, Chrome 89 7 ms
Acer Chromebook 314, Chrome OS 89 7 ms
Pixel 5, Android 11, Chrome 89 7 ms
iPhone SE1, iOS 13, Safari 13 12 ms
Pixel 1, Android 7.1, Chrome 88 17 ms
Galaxy S4, Android 4.4, Chrome 80 40 ms
MacBook Pro 2015 (Core i7), macOS 11, Firefox 86 50 ms

Audio fingerprinting is only a small part of the larger identification process.

Audio fingerprinting is one of the many signals our open source library uses to generate a browser fingerprint. However, we do not blindly incorporate every signal available in the browser. Instead we analyze the stability and uniqueness of each signal separately to determine their impact on fingerprint accuracy.

For audio fingerprinting, we found that the signal contributes only slightly to uniqueness but is highly stable, resulting in a small net increase to fingerprint accuracy.

You can learn more about stability, uniqueness and accuracy in our beginnerโ€™s guide to browser fingerprinting.

Try Browser Fingerprinting for Yourself

Browser fingerprinting is a useful method of visitor identification for a variety of anti-fraud applications. It is particularly useful to identify malicious visitors attempting to circumvent tracking by clearing cookies, browsing in incognito mode or using a VPN.

You can try implementing browser fingerprinting yourself with our open source library. FingerprintJS is the most popular browser fingerprinting library available, with over 12K GitHub stars.

For higher identification accuracy, we also developed the FingerprintJS Pro API, which uses machine learning to combine browser fingerprinting with additional identification techniques. You can try FingerprintJS Pro free for 10 days with no usage limits.

Get in touch

Top comments (27)

shadowfaxrodeo profile image

That's super interesting. But also deeply troubling.

What measures does FingerprintJS put in place to stop it's users from abusing it?

What's to stop someone using FingerprintJS for extortion? Or a oppressive regime using it to identify disidents?

savannahjs profile image
Savannah Copland ๐Ÿ‘‹

Hi Nathaniel - I answered some of this in my response to Pankaj -

More specifically to your question though: browser fingerprinting aims to uniquely identify identifies browsers, but it is not able to identify individual people. In that way, this technology behaves very similarly to cookies, though is a little more difficult to spoof.

We do try to ensure our customers use the technology for anti-fraud, and we never do cross-domain tracking.

shadowfaxrodeo profile image

Could you clarify that statement. In what sense does it identify the browser but not the individual using the browser?

Thread Thread
savannahjs profile image
Savannah Copland ๐Ÿ‘‹

A browser fingerprinting script generates a hash using signals collected via the browser. This hash serves as a "fingerprint" of that a specific site visitor's browser that remains stable between browsing sessions. If you were generating and storing browser fingerprints for your website, you would be able to tell if a visitor returned and associate multiple browsing sessions with the same browser.

It's tricky to ever know exactly who is visiting on a specific browser. You could associate the fingerprint with account information if the visitor has ever logged in, but that's probably as close as you can get. As we don't do cross-domain tracking, a website would only be able to associate browsing information for users of their site only.

Hopefully that answers your question - forgive me if I'm on the wrong track!

Thread Thread
shadowfaxrodeo profile image

So the distinction is that you can identify the device but not the person using the device?

Either I'm fundamentally misunderstanding, or that's misleading thing to say.

I'm sure the vast majority of devices are used by a single individual โ€”ย and with the exception of libraries and internet cafes, are used by a close knit group.

If it has the same capability of indentifying users as cookies then it definitely can 100% identify an individual person.

So is this statement true or false?

it is not able to identify individual people.

Just because it can't identify everybody 100% of the time it doesn't mean it can't identify an individual.

I hope you can appreciate why people find this disturbing.

Thread Thread
savannahjs profile image
Savannah Copland ๐Ÿ‘‹

The distinction I'm trying to make is that even if you assume a device is used by a single individual, you still need to associate that device with additional data sources (like user data) to know that person's name, email, or phone number (to tie back to your dissident example).

I totally understand your concern though. To your argument, while there's clearly a difference between a hashed ID and a user's name or address, GDPR considers cookies and fingerprints 'personal' data, which allows it to extend protections around how this information is stored, when consent is required, and the conditions under which personal data must be deleted. We are 100% on board with this type of governance as it ensures a healthy balance between privacy and security.

Thread Thread
shadowfaxrodeo profile image

Okay, I think I understand.

In a sense it's the same as cookies, but it's for people who have explicitely taken steps to avoid being tracked online.

If one of fingerprintJS's users breaks the law and invades my privacy with it, who is held responsible?

Is there a list of organizations that use FingerprintJS?
I couldn't find any on the site.

Thread Thread
savannahjs profile image
Savannah Copland ๐Ÿ‘‹

To the cookies comment - yes that's right.

For breaking laws (as it pertains to GDPR and the EU), there are different rules for 'data processors' and 'data controllers'. We have responsibilities as a data processor that include data encryption, ensuring proper authorization access and confidentiality of data, and security incident reports and auditing. The data controller also has its own set of requirements, including asking for consent to track for marketing purposes. The Information Commissionerโ€™s Office (who enforces GDPR) can levy significant fines against either the processor, the controller, or both, depending on who is breaking the rules. So in short, it depends, but we take our end of upholding privacy laws very seriously.

For organizations using us - we have some logos on our homepage but other than that we don't provide a full list!

Thread Thread
shadowfaxrodeo profile image

I'm sorry to belabour the point.

The privacy and security implications of this go beyond legal questions into ethical ones. Tools like this are always abused โ€”ย and it's often the most vulnerable people who pay the price.

I'm sure you take all kinds of strict security and legal measures, but in my opinion this is going to hurt people. I hope I'm wrong.

Thread Thread
bezpowell profile image

I'm in complete agreement here. It may also be subject to the same restrictions as cookies, but we all know cookies are regularly abused when different websites pool their data on individuals.

I suppose a big advantage of having this as a open-source project is it might make it easier to develop tools to circumvent it. We know that many unscrupulous websites are already fingerprinting users (try running some websites to use through Blacklight, the results can be scary), so knowing how they are doing so is better than having to guess.

The prevalence of tracking scripts and finger-printers is the reason I do most of my day to day browsing using Tor browser. Just because I'm not doing anything illegal doesn't mean I want websites tracking everything I do.

Thread Thread
shadowfaxrodeo profile image

Thanks for the link to Blacklight โ€” that's a really interesting website.

Looking at the Github for FingerprintJS there's an thread about the ethical implications of the project: #430.

The authors defend themselves by saying the library helps defend users from privacy violations by being open source, and therefore bringing to light these issues.

They build weapons so world can better defend itself against people who use their weapons.

@savannahjs Does fingerprintJS also maintain an opensource library for protecting users?

pankajpatel profile image
Pankaj Patel

Good explanation on how Audio can be used for Fingerprinting

Though now I am wondering why do we need this extensive Fingerprinting, can you please elaborate?

savannahjs profile image
Savannah Copland ๐Ÿ‘‹

Hi Pankaj!

Our company (FingerprintJS) focuses on using browser fingerprinting as one tool of many to fight online fraud. Generally, a very small percentage of a website's traffic is responsible for the lion's share of fraudulent activity - cracking account logins, testing stolen credit cards, etc. By identifying fraudulent visitors via first-party tracking, websites can require additional authentication or other security workflows without gumming up the user experience for everyone else.

As far as ensuring that our paid product is used for anti-fraud reasons, we do work to ensure our customers are GDPR compliant, as sites using browser fingerprinting need to follow the same rules as cookies. Our pricing model also makes our solution not particularly viable for advertising use cases, which requires a very high volume of tracking.

For our open-source project, we can't control how our solution is used (and browser fingerprinting is already endemic on the web), but we feel that being transparent about the technology is better for the developer community at large.

bezpowell profile image

I fully appreciate that controlling how your product is used is basically impossible on the web. Saying that your product focuses on fighting online fraud (and that what you do is already endemic on the web), however, does feel like a bit of a cop-out; as it totally ignores the implications of any other possible uses people might make of it.

For me personally (and I'm sure for many others), spying on everyone who visits a website in-case they are one of the small percentage of bad actors mentioned does not feel like a reasonable trade-off to reduce additional authentication. Especially as that is a trade-off that is almost certainly being made without a visitors consent or knowledge.

I appreciate you being open and transparent with the developer community, but how open and transparent is your company being with visitors?

fetis profile image
Sergey Fetiskin

can you see different browser versions with that approach?

with your charts, I don't see the difference between Firefox Windows and my Firefox MacOs. Is it because of visual representation or it's intended

savannahjs profile image
Savannah Copland ๐Ÿ‘‹

Yes, it should be able to distinguish between Firefox instances (and Chrome and Safari too) if the underlying OS or hardware is different. It might be just that the visualization doesn't show enough detail to see it!

layzee profile image
Lars Gyrup Brink Nielsen

Darn, this is scary! As Pankaj Patel said, please explain what you do at FingerprintJS.

savannahjs profile image
Savannah Copland ๐Ÿ‘‹

Hey Lars - I wrote a more in-depth response to Pankaj's comment. Hopefully answers your question!

layzee profile image
Lars Gyrup Brink Nielsen

Thanks for your explanation, Savannah.

jankapunkt profile image
Jan Kรผster

So we all know the ethical implications of this but this tool is not more/less ethical than wireshark/nmap.

It is a dual use software and we should treat it like such:

  • it can be used for tracking und de-anonymization
  • companies, governments, militaries and intelligence agencies can use it for unethical reasons
  • however, especially military and intelligence agencies have much better options to track you down on a lower level or via metada
  • company fingerprinting is still covered by gdpr
  • there is definitely white use as well
  • for example: in our public research project we provide a tool for functional illiterates to assess and improve their fundamental literacy skills. Their only identifier is a code for login. If they forget the code they need to generate a new one. We use the fingerprinting to connect the sessions in such case and by thus ensure their anonymity
  • this contradicts the view we have on fingerprinting as de-anonymization tool
  • you see the world is not as dualistic as we might perceive it
archonic profile image

It's hard to see this as anything but "tracking for users that don't want to be tracked". The privacy implications of being able to uniquely identify browsers anonymously is precisely why GDPR was created. Adherence to GDPR is a good thing but this library is an exploit of it. Given others' responses it seems I'm not alone. I think it's a matter of time before privacy conscious browsers thwart this. Being transparent about this technology is better than not being transparent about it, so thanks for that I guess, but I just can't put FingerprintJS in my good books.

ziizium profile image
Habdul Hazeez

I'll suggest you change the tags #webdev and #typescript to #privacy and #security.

rahoulb profile image
Rahoul Baruah

This is unsurprising but terrifying.

andrewbaisden profile image
Andrew Baisden

Wow this was very insightful good read.

theroka profile image

Hi Savannah,
thank you for this absolute interesting article!
Kind regards

savannahjs profile image
Savannah Copland ๐Ÿ‘‹

Thanks for the kind words theroka! Glad it was interesting :)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.