Skip to main content

Overview

The Rust SDK is designed for native applications where you need tracking outside of a browser context:
  • Desktop apps - Tauri, native GUI applications
  • CLI tools - Command-line applications
  • Embedded systems - IoT devices, kiosks
  • Backend services - Rust-based APIs
For anonymous tracking before user sign-in, use fingerprint. See Device Tracking for details.

Installation

cargo add outlit
The SDK is async and requires a tokio runtime. Most async frameworks (Tauri, Axum, Actix) already include tokio.

Quick Start

use outlit::{Outlit, email, fingerprint};

#[tokio::main]
async fn main() -> Result<(), outlit::Error> {
    let client = Outlit::builder("pk_your_public_key")
        .build()?;

    // Track with email (resolves immediately)
    client.track("feature_used", email("user@example.com"))
        .property("feature", "export")
        .send()
        .await?;

    // Flush before exit
    client.shutdown().await?;
    Ok(())
}

Configuration

use std::time::Duration;

let client = Outlit::builder("pk_xxx")
    .api_host("https://app.outlit.ai")  // optional
    .flush_interval(Duration::from_secs(10))
    .max_batch_size(100)
    .timeout(Duration::from_secs(10))
    .build()?;

Options

public_key
&str
required
Your organization’s public key.
api_host
&str
default:"https://app.outlit.ai"
API endpoint for sending events.
flush_interval
Duration
default:"10s"
How often to automatically flush events.
max_batch_size
usize
default:"100"
Maximum events per batch before auto-flush.
timeout
Duration
default:"10s"
HTTP request timeout.

API Reference

client.track(event_name, identity)

Track a custom event.
// With email
client.track("subscription_upgraded", email("user@example.com"))
    .property("plan", "pro")
    .property("mrr", 99)
    .send()
    .await?;

// With user_id
client.track_by_user_id("feature_used", user_id("usr_123"))
    .email("user@example.com")  // optional, for linking
    .property("feature", "export")
    .send()
    .await?;

// With fingerprint (device tracking)
client.track_by_fingerprint("app_opened", fingerprint("device_abc123"))
    .property("version", "1.2.0")
    .send()
    .await?;

Builder Methods

.email()
&str
Add email to the event (if not primary identity).
.user_id()
&str
Add user ID to the event.
.fingerprint()
&str
Add device fingerprint for linking.
.property()
(&str, impl Into<Value>)
Add a property to the event. Call multiple times for multiple properties.
.timestamp()
i64
Set custom timestamp (milliseconds since epoch).

client.identify(identity)

Identify a user and optionally link device events.
client.identify(email("user@example.com"))
    .user_id("usr_123")
    .fingerprint("device_abc123")  // Links this device's events
    .trait_("name", "Jane Doe")
    .trait_("plan", "enterprise")
    .send()
    .await?;
.fingerprint()
&str
Device identifier. All events tracked with this fingerprint are retroactively linked to the user.
.user_id()
&str
Your internal user ID.
.trait_()
(&str, impl Into<Value>)
Add a trait to the user profile.

Contact Stage Methods

Track contact progression through your product lifecycle.
// Mark user as activated
client.user()
    .activate(email("user@example.com"))
    .property("flow", "onboarding")
    .send()
    .await?;

// Mark user as engaged
client.user()
    .engaged(email("user@example.com"))
    .send()
    .await?;

// Mark user as inactive
client.user()
    .inactive(email("user@example.com"))
    .property("reason", "no_login_30_days")
    .send()
    .await?;

// With fingerprint
client.user()
    .activate_by_fingerprint(fingerprint("device_abc123"))
    .send()
    .await?;

Account Billing Methods

Track account billing status by domain.
// Mark account as paid
client.customer()
    .paid("acme.com")
    .property("plan", "pro")
    .property("amount", 99)
    .send()
    .await?;

// Mark account as trialing
client.customer()
    .trialing("acme.com")
    .property("trial_ends", "2024-02-15")
    .send()
    .await?;

// Mark account as churned
client.customer()
    .churned("acme.com")
    .property("reason", "cancelled")
    .send()
    .await?;

client.flush()

Immediately send all queued events.
client.flush().await?;

client.shutdown()

Gracefully shutdown: flushes remaining events and stops the timer.
client.shutdown().await?;
Always call shutdown() or flush() before your application exits to ensure all events are sent.

Tauri Integration

For Tauri desktop apps, initialize the client in your Rust backend.
This example uses the uuid crate for generating device IDs. Add it with cargo add uuid --features v4.
use outlit::{Outlit, email, fingerprint};
use tauri::Manager;
use std::sync::Arc;

// Store device fingerprint persistently
fn get_or_create_device_id(app: &tauri::AppHandle) -> String {
    let config_dir = app.path().app_config_dir().unwrap();
    let id_file = config_dir.join("device_id");

    if let Ok(id) = std::fs::read_to_string(&id_file) {
        return id;
    }

    let id = uuid::Uuid::new_v4().to_string();
    std::fs::create_dir_all(&config_dir).ok();
    std::fs::write(&id_file, &id).ok();
    id
}

#[tauri::command]
async fn track_event(
    app: tauri::AppHandle,
    event_name: String,
    properties: serde_json::Value,
    email: Option<String>,
) -> Result<(), String> {
    let client = app.state::<Arc<Outlit>>();
    let device_id = get_or_create_device_id(&app);

    if let Some(email) = email {
        // User is signed in
        client.track(&event_name, outlit::email(&email))
            .fingerprint(&device_id)
            .property("data", properties)
            .send()
            .await
            .map_err(|e| e.to_string())?;
    } else {
        // Anonymous tracking
        client.track_by_fingerprint(&event_name, fingerprint(&device_id))
            .property("data", properties)
            .send()
            .await
            .map_err(|e| e.to_string())?;
    }

    Ok(())
}

fn main() {
    let client = Arc::new(
        Outlit::builder("pk_xxx")
            .build()
            .expect("Failed to create Outlit client")
    );

    tauri::Builder::default()
        .manage(client.clone())
        .invoke_handler(tauri::generate_handler![track_event])
        .build(tauri::generate_context!())
        .expect("error building tauri app")
        .run(|app, event| {
            if let tauri::RunEvent::Exit = event {
                // Flush on exit
                let client = app.state::<Arc<Outlit>>();
                tauri::async_runtime::block_on(client.shutdown()).ok();
            }
        });
}

Error Handling

The SDK returns Result<(), outlit::Error> for operations that can fail:
match client.track("event", email("user@test.com")).send().await {
    Ok(()) => println!("Event tracked"),
    Err(outlit::Error::Network(e)) => eprintln!("Network error: {}", e),
    Err(outlit::Error::Shutdown) => eprintln!("Client was shutdown"),
    Err(e) => eprintln!("Error: {}", e),
}

Next Steps