TL;DR: I went on a journey to figure out how to use Google Analytics from Rust front-end Wasm code. On the way I learned how to call JavaScript code from Rust using Wasm-bindgen and made a crate to wrap Google’s gtag.js framework.

Let’s jump right in. I’ve been experimenting with using Rust for front end web development for the past year or so and I recently realized that I’m going to need analytics for the prototype I’m building. I’d heard of Google Analytics so I decided to start there.

The challenge I faced, was calling Google’s JavaScript code from my Rust front-end Wasm code. Google’s instructions recommend using their global site tag (or gtag.js) framework to integrate with Google Analytics, but I had no idea how call random JavaScript code from Rust and Wasm. Read on to discover how I did it.

First Attempts

The first thing I tried was Rust’s #[link(wasm_import_module)] attribute. The Rust WebAssembly documentation mentions using this to interop with JavaScript code, but it appears to only work with ES6 modules. The gtag.js code is not an ES6 module, it is distributed as a JavaScript file and initialized using a <script> tag. I wasn’t able to figure out a way to make this work. Next, I found the google-tagmanager2 crate that provides access to the Google Tag Manager API via Hyper. In theory, this could work for what I needed, but I wasn’t sure if Hyper works with Wasm and I couldn’t figure out how to use this API with Google Analytics anyway (according to the docs, it should be possible though). Eventually, I stumbled upon a solution in the form of #[wasm_bindgen] .

Wasm-bindgen provides a way to generate bindings to bridge the gap between JavaScript and Rust. The included #[wasm_bindgen] macro is much more flexible than the #[link(wasm_import_module)] directive, as it works by generating JavaScript code to interface with Rust and Wasm. This allows access to globals, classes, and various other things from Rust while taking care of all the details of marshalling and unmarshalling data along the way. That glue adds some overhead, but as Wasm matures, some of that glue code is expected go away. This was just what I needed.

Wasm-bindgen

Binding to gtag.js’ gtag() function is almost trivial using #[wasm_bindgen] . My first attempt looked something like this.

use wasm_bindgen::prelude::*; #[wasm_bindgen]

extern {

pub fn gtag();

}

I compiled the code and called my gtag() wrapper function and it didn’t blow up. A promising start! But the gtag() function in the gtag.js code takes parameters. One variant takes two strings and one variant accepts two strings and an object. Let’s ignore the second variant for now.

use wasm_bindgen::prelude::*; #[wasm_bindgen]

extern {

pub fn gtag(cmd: &str, id: &str);

}

With this, I was able to call the function and see the result in the Google Analytics dashboard. Excellent! Now let’s tackle the second variant that accepts three arguments. Rust doesn’t support function overloading or variadic functions, so how can we bind twice to the same gtag.js function? Lucky for us, the Wasm-bindgen authors thought of this.

use wasm_bindgen::prelude::*; #[wasm_bindgen]

extern {

pub fn gtag(cmd: &str, id: &str); #[wasm_bindgen(js_name = gtag)]

pub fn gtag_with_parameters(cmd: &str, id: &str, params: &JsValue);

}

Using the js_name parameter, we can tell Wasm-bindgen to bind to the gtag() function on the JavaScript side while using a different name on the Rust side. Also notice the JsValue type as the argument for the third parameter. This type can map to JavaScript objects, among other JS types. JsValue provides a from_serde function that can convert things implementing Serde’s Serialize trait into something that JavaScript can understand. This dance is a little clumsy to repeat everywhere, but it works.

A Rustic Detour

Ok, we have some working code, but it’s not very easy to use, so let’s wrap it with something more rustic. First, we always pass the same ID when calling the gtag() function, so let’s store that in a struct so we don’t have to repeat it everywhere.

struct DataLayer {

id: String,

}

Now we need to provide a way to call our gtag() function.

impl DataLayer {

fn push(&self, cmd: &str, params: &JsValue) {

gtag_with_parameters(cmd, &self.id, params);

}

}

Next, let’s make it easier to pass the parameters. The user shouldn’t need to worry about generating their own JsValue all the time.

impl DataLayer {

fn push(&self, cmd: &str, params: &impl Serialize) -> Result<()> {

gtag_with_parameters(cmd, &self.id, &JsValue::from_serde(params)?);

Ok(())

}

}

Great, that’s everything we need. This is how you call it.

fn main() {

let gtag = DataLayer::new("GA_MEASUREMENT_ID");



gtag.push("config", json!({

"page_title": "homepage",

"page_path": "/home",

}).unwrap();

}

(The json!() macro is provided by the serde_json crate and allows you to embed JSON objects directly into your Rust code.)

I’ve published the gtag.js wrapper code I wrote as a low level gtag-js-sys crate and a higher level gtag-js crate. The code snippets above are taken almost verbatim from these crates. Using either of these crates requires importing the gtag.js framework as described in the documentation here.

And that’s it! If you want to use Google Analytics from a Rust Wasm app, I’ve got you covered. If you have some other JavaScript library you want to use, hopefully this information can start you on the path to writing your own wrappers. Good luck!