Documentation Index
Fetch the complete documentation index at: https://docs.tell.rs/llms.txt
Use this file to discover all available pages before exploring further.
The Rust SDK sends events and structured logs from your backend services to Tell. It’s a server SDK — you create a client, pass a user ID on every call, and a background worker handles batching and delivery over TCP.
Your thread does about 80 ns of work per call (serialize, encode, enqueue) and never touches the network.
Installation
cargo add tell
cargo add tokio --features rt-multi-thread,macros
The SDK requires Rust 2024 edition and a Tokio runtime.
Quick start
use tell::{Tell, TellConfig, props};
#[tokio::main]
async fn main() {
let client = Tell::new(
TellConfig::production("a1b2c3d4e5f60718293a4b5c6d7e8f90").unwrap()
).unwrap();
client.track("user_123", "Page Viewed", props! {
"url" => "/home",
"referrer" => "google"
});
client.close().await.ok();
}
That’s a working setup. The client connects to collect.tell.rs:50000, batches up to 100 events, and flushes every 10 seconds or when you call close().
Configuration
Use one of the two presets, or build a custom config.
// Production — collect.tell.rs:50000, batch 100, flush 10s
let config = TellConfig::production("a1b2c3d4e5f60718293a4b5c6d7e8f90").unwrap();
// Development — localhost:50000, batch 10, flush 2s, debug logging
let config = TellConfig::development("a1b2c3d4e5f60718293a4b5c6d7e8f90").unwrap();
For custom settings, use the builder:
use std::time::Duration;
let config = TellConfig::builder("a1b2c3d4e5f60718293a4b5c6d7e8f90")
.endpoint("collect.internal:50000")
.batch_size(200)
.flush_interval(Duration::from_secs(5))
.max_retries(5)
.close_timeout(Duration::from_secs(10))
.network_timeout(Duration::from_secs(15))
.on_error(|e| eprintln!("[Tell] {e}"))
.build()
.unwrap();
The API key must be a 32-character hex string. The SDK validates it at config time and returns an error if it’s invalid.
Tracking events
Every tracking method takes user_id as its first parameter. Calls never block or panic — errors go to the optional on_error callback.
// Track a custom event
client.track("user_123", "Feature Used", props! {
"feature" => "dark_mode",
"enabled" => true
});
// Identify a user
client.identify("user_123", props! {
"name" => "Jane",
"plan" => "pro",
"company" => "Acme"
});
// Associate user with a group
client.group("user_123", "company_456", props! {
"plan" => "enterprise",
"seats" => 50
});
// Track revenue
client.revenue("user_123", 49.99, "USD", "order_789", props! {
"product" => "annual_plan"
});
// Link two user identities
client.alias("anon_abc", "user_123");
Standard event names
The SDK provides typed constants for common events so you don’t have to remember exact strings:
use tell::Events;
client.track("user_123", Events::PAGE_VIEWED, props! { "url" => "/pricing" });
client.track("user_123", Events::USER_SIGNED_UP, props! { "source" => "organic" });
client.track("user_123", Events::ORDER_COMPLETED, props! { "total" => 99.00 });
client.track("user_123", Events::FEATURE_USED, props! { "feature" => "export" });
Constants are available for user lifecycle, revenue, subscriptions, trials, shopping, engagement, and communication events. Custom string names are always accepted too.
Super properties
Register properties that get merged into every track, group, and revenue call:
client.register(props! { "app_version" => "2.1.0", "env" => "production" });
// This track call automatically includes app_version and env
client.track("user_123", "Click", props! { "button" => "submit" });
// Remove a super property
client.unregister("env");
Event-specific properties override super properties when keys conflict.
Structured logging
Send logs alongside events through the same pipeline. Each log has an RFC 5424 severity level.
client.log_error("DB connection failed", Some("api"), props! {
"host" => "db.internal",
"retries" => 3
});
client.log_info("Request processed", Some("api"), props! {
"status" => 200,
"duration_ms" => 45
});
client.log_warning("Rate limit approaching", Some("gateway"), None::<serde_json::Value>);
The service parameter is optional — pass None to default to "app". Convenience methods are available for all nine levels: log_emergency, log_alert, log_critical, log_error, log_warning, log_notice, log_info, log_debug, and log_trace.
You can also use the generic log method with an explicit level:
use tell_encoding::LogLevel;
client.log(LogLevel::Error, "Something broke", Some("worker"), props! {
"job_id" => "j_123"
});
Properties
You have three ways to pass properties:
use tell::{props, Props};
// 1. props! macro — fastest, zero intermediate allocation
client.track("user_123", "Click", props! {
"url" => "/home",
"count" => 42,
"active" => true
});
// 2. Props builder — for dynamic values
let p = Props::new()
.add("url", &request.path)
.add("status", response.status);
client.track("user_123", "Request", p);
// 3. serde_json — works with any Serialize type
client.track("user_123", "Click", Some(json!({"url": "/home"})));
// 4. No properties
client.track("user_123", "Click", None::<serde_json::Value>);
Props and props! write JSON bytes directly to a buffer, skipping the intermediate serde_json::Value allocation. Use them on hot paths.
Lifecycle
// Force-send all queued events and logs
client.flush().await?;
// Rotate session ID
client.reset_session();
// Flush + shut down the background worker
client.close().await?;
Always call close() before your process exits to avoid losing buffered events. It blocks up to close_timeout (default 5 seconds).
Sharing across threads
Tell is Clone + Send + Sync. Internally it wraps everything in an Arc, so cloning is cheap:
let client = Tell::new(config).unwrap();
let c = client.clone();
tokio::spawn(async move {
c.track("user_456", "Background Job", props! { "job" => "sync" });
});
Error handling
The constructor and lifecycle methods return Result:
let client = Tell::new(config)?; // TellError::Configuration if invalid
client.flush().await?; // TellError::Network on failure
client.close().await?; // TellError::Closed if already shut down
Tracking and logging calls (track, identify, log_*, etc.) never return errors. Invalid input (empty user ID, event name too long) is reported through the on_error callback:
let config = TellConfig::builder("a1b2c3d4e5f60718293a4b5c6d7e8f90")
.on_error(|e| eprintln!("[Tell] {e}"))
.build()
.unwrap();
Advanced
Configuration reference
| Parameter | Default (production) | Default (development) | Description |
|---|
endpoint | collect.tell.rs:50000 | localhost:50000 | TCP collector address |
batch_size | 100 | 10 | Events per batch before auto-flush |
flush_interval | 10s | 2s | Time between auto-flushes |
max_retries | 3 | 3 | Retry attempts on send failure |
close_timeout | 5s | 5s | Max wait on close() |
network_timeout | 30s | 30s | TCP connect timeout |
on_error | silent | silent | Error callback |
Validation rules
| Field | Rule |
|---|
| API key | 32-character hex string |
| User ID | Non-empty |
| Event name | 1–256 characters |
| Log message | 1–65,536 characters |
| Group ID | Non-empty |
| Revenue amount | Positive number |
Retry behavior
On TCP send failure, the SDK retries with exponential backoff: 1s initial delay, 1.5x multiplier, 20% jitter, capped at 30s. After max_retries attempts, the batch is dropped and the error reported via on_error.