Using PostHog with Rust
Tags: rust, programming, webdev, tutorial
Published: 2024-03-14 18:00:47

Hello world! In this article, we’re going to talk about how you can use Posthog with Rust to be able to track user behavior across an application. Analytics are more important than ever to be able to improve your product; Posthog makes it easy to do exactly that.

## How does PostHog work?

Posthog works by allowing you to capture events and then convert those events into “insights” by aggregating the data. PostHog allows you to then query the data using HogQL (a DSL made by PostHog for querying data,, similar to SQL). Alhough there isn’t an officially supported Rust SDK for PostHog, they do have a community-supported one (`posthog-rs`) that can capture events in your application.

We’ll also look at using `reqwest` to set up our own custom client for sending requests to the PostHog API!

## Capturing events

We can get started with the `posthog-rs` client by creating it:

```rust
use posthog::{ client, Event };
use std::env;

let posthog_api_key = env::var("POSTHOG_API_KEY").expect("POSTHOG_API_KEY env var missing");
let client = client(posthog_api_key);
```

The event essentially allows us to add anything we want - for example, we can add user IDs, user cookies or timestamps and whatever else we need.

Next, we’ll want to make a function for sending an event to the PostHog API:

```rust
fn send_posthog_event(client: &posthog::Client) {
    let mut event = Event::new("my_database_event", "1234");
    event.insert_prop("action", "insert").unwrap();
    client.capture(event).unwrap();
}
```

Now whenever we need to track an action, we can do so. Below is an example of using the client in an Axum handler function to be able to send an event:

```rust
use axum::{http::StatusCode, extract::State};

#[derive(Clone)]
struct AppState {
    db: sqlx::PgPool,
    posthog: posthog::Client
}

async fn add_to_database(
    State(state): State<AppState>
) -> StatusCode {
    let res = sqlx::query("SELECT * FROM USERS")
        .fetch_all(&state.db)
        .await
        .unwrap();

    send_posthog_event(&state.client);

    StatusCode::OK
}
```

### Using the posthog-rs client in an async context

It should be noted that the `posthog-rs` client is not async-friendly due to using the `blocking` feature of the `reqwest` crate. This means that if you want to use it with async (for example in a web service), you will need to use `tokio::task::block_in_place()` like so:

```rust
use axum::{Extension, StatusCode, response::IntoResponse};

async fn my_example_axum_handler(
    Extension(client): Extension<posthog::Client>
) -> impl IntoResponse {
    tokio::task::block_in_place(|| move {
        let mut evt = Event::new("my_database_event", "Hello world!");
        evt.insert_prop("action", "insert").unwrap();

        client.capture(evt).unwrap();
    });

    StatusCode::OK
}
```

Alternatively, Shuttle has a fork of a fork of `posthog-rs` that contains an async client. You can find out more [here.](https://github.com/shuttle-hq/posthog-rs) Due to being a fork it’s not an officially published crate, but you can add it to your Rust project with this shell snippet:

```bash
cargo add posthog-async --git https://github.com/shuttle-hq/posthog-rs.git --branch main
```

Now when you refer to it in your Rust files, you can use `posthog_async` as the dependency name.

## Using a custom client

In this section, we’re going to create a custom client for working with Posthog. We’ll get started by installing `reqwest`, which is a library for writing HTTP requests and is async by default (requiring a feature to be blocking):

```bash
cargo add reqwest
```

Next, we will want to write a function that we can use to make our request client easily. We’ll need to add bearer authorization on the header, which we can do like so:

```bash
let posthog_api_key = std::env::var("POSTHOG_API_KEY")
    .expect("Could not find POSTHOG_API_KEY environment variable");

let client = reqwest::Client::builder()
    .header("Authorization", format!("Bearer {}"))
    .build().unwrap();
```

Now whenever we use `client`, it will always have the appropriate header attached.

## Using PostHog insights

### Creating insights

Insights in Posthog are the main building blocks of dashboards and allow you to visualise how users use your product. To create insights, we can use our `reqwest::Client` to send an API key manually. There’s quite a few endpoints - we’ll be making POST requests to `/api/projects/:project_id/insights/` for this example.

To be able to set the JSON body, we will need to make use of the `serde_json` crate which we can install with this shell snippet:

```bash
cargo add serde_json
```

Next, we’ll want to make a JSON map - which we can do below with the `serde_json::json!()` macro:

```rust
use serde_json::json;

fn my_json_body() -> serde_json::Value {
    json!({
        "name": "my_insight",
    })
}
```

You can find a more extensive list of request body parameters [here.](https://posthog.com/docs/api/insights#post-api-projects-project_id-insights) Once you’re finished creating the request body you want to send to Posthog, you can then create the request using the client we created.

```rust
async fn create_insight(
    client: request::Client,
    project_id: String
) -> Result<(), Box<dyn std::error::Error>> {
    let url = format!(
        "https://app.posthog.com/api/projects/{project_id}/insights/"
    );

    let json_body = my_json_body();
    let response = client.post(&url).json(json_body).send.await?;
}
```

However, this is just the start of insight creation. To make our insight do anything, we need to add a funnel to it. Similarly to the last API endpoint where we attached some basic values, we’ll create a new JSON body using the `json!()` macro and then send it:

```rust
async fn create_insight_funnel(
    client: request::Client,
    project_id: String
) -> Result<(), Box<dyn std::error::Error>> {
    let url = format!(
        "https://app.posthog.com/api/projects/{project_id}/insights/"
    );

    let json_body = json!({
        events: [{"id":"my_database_event"}],
        date_from: "-1m"
    })
    let response = client.post(&url).json(json_body).send.await?;
}
```

Here, we’ve specified in the JSON body we want to get events from up to a month ago and the name of the event we want to get is `my_database_event`, which we created earlier. Note that the `$pageview` event signifies any time a user views a page that PostHog is added to.

### Retrieving an insight

Once you have some events and insights built up, it’s time to retrieve them! You can do this by making a GET request to the insights URL. See below:

```rust
async fn get_insights(
    client: request::Client,
    project_id: String
) -> Result<(), Box<dyn std::error::Error>> {
    let url = format!(
        "https://app.posthog.com/api/projects/{project_id}/insights/"
    );

    let response = client.get(&url).send.await?;
}
```

When you get the response, you will need to turn it into a struct that implements `Deserialize`. Thankfully, `reqwest` provides a method for turning the response into JSON-compatible structs. Let’s make a struct that represents some of the properties of the JSON response from PostHog.  `serde` completely ignores undeclared fields by default, allowing us to only take what we need.

```rust
#[derive(Deserialize, Debug)]
struct PosthogResponse {
    count: i32,
    next: String,
    previous: String,
    results: Vec<PosthogResult>
}

#[derive(Deserialize, Debug)]
struct PosthogResult {
    id: String,
    name: String,
    deleted: bool
}
```

Now we can expand our previous function to include the struct conversion:

```rust
async fn get_insights(
    client: request::Client,
    project_id: String
) -> Result<(), Box<dyn std::error::Error>> {
    let url = format!(
        "https://app.posthog.com/api/projects/{project_id}/insights/"
    );

    let response = client.get(&url).send.await?;
    let json: PosthogResponse = response.json().await?;

    println!("{json:?}");
    Ok(())
}
```

## Finishing Up

Posthog makes it easy to track how users are using your application and find easy wins. While the Rust SDK is not complete, hopefully you’ve found some clarity in communicating with the Posthog API with Rust!

Read more:
- Get started with using Rust's most popular framework [here](https://www.shuttle.rs/blog/2023/12/06/using-axum-rust)
- Learn about logging for your Rust application [here](https://www.shuttle.rs/blog/2023/09/20/logging-in-rust)
- Check out our guide for using Stripe with Rust [here](https://www.shuttle.rs/blog/2024/03/07/stripe-payments-rust)