Advanced Usage
These patterns use the current public client API. Keep one application-owned client and make transport-dependent tests use explicit local endpoints.
Custom Filter Evaluators
The SDK's evaluator implementation trait is not exposed for external implementations. Use built-in filters and mapped context. There is no supported FilterEvaluator / Engine::register application recipe.
Filter Parameters in Dashboard
Configure the built-in filter with the parameters expected by that filter. An arbitrary dashboard filter name does not create a Rust evaluator.
Thread-Safe Shared Client
This function reuses an initialized Arc across asynchronous work. Each task receives an owned context, and all tasks finish before the caller closes the client:
use std::sync::Arc;
use toggly::{EvalContext, TogglyClient};
pub async fn check_users(client: Arc<TogglyClient>)
-> Result<Vec<bool>, Box<dyn std::error::Error>>
{
let mut work = tokio::task::JoinSet::new();
for identity in ["alice", "bob"] {
let client = client.clone();
let context = EvalContext::with_identity(identity);
work.spawn(async move {
client.is_enabled("new-dashboard", context).await
});
}
let mut results = Vec::new();
// Drain every task even if one evaluation fails.
let mut failure: Option<Box<dyn std::error::Error>> = None;
while let Some(joined) = work.join_next().await {
match joined {
Ok(Ok(enabled)) => results.push(enabled),
Ok(Err(error)) => { failure = Some(Box::new(error)); }
Err(error) => { failure = Some(Box::new(error)); }
}
}
if let Some(error) = failure { return Err(error); }
Ok(results)
}
Results arrive in completion order. This demonstrates owned operation inputs and shared-client usage; it is not an atomic evaluated snapshot.
Graceful Shutdown
Stop new work and await outstanding tasks before closing. If waiting for Ctrl-C, enable Tokio's signal feature. Use an application error type that can carry both IO and SDK errors:
use toggly::TogglyClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = TogglyClient::builder()
.app_key(std::env::var("TOGGLY_APP_KEY")?)
.environment("Production").build().await?;
// In a server, stop accepting requests and drain them at this point.
let signal = tokio::signal::ctrl_c().await;
client.close().await;
signal?;
Ok(())
}
For actual HTTP draining, use the Actix, Axum or Rocket lifecycle example.
Error Handling
Custom Error Handling
Initialization and explicit refresh return a Result. Missing flags normally return Ok(false). on_error, last_error().await and etag().await supply definition diagnostics. A missing flag is not the same as a failed initial connection.
Fallback on Errors
A fallback is an application decision. This helper logs evaluation errors before choosing classic behavior:
use toggly::{EvalContext, TogglyClient};
pub async fn classic_on_error(client: &TogglyClient, context: EvalContext) -> bool {
match client.is_enabled("new-dashboard", context).await {
Ok(enabled) => enabled,
Err(error) => {
eprintln!("Dashboard evaluation failed: {error}");
false
}
}
}
Logging
The SDK emits tracing events. Install a subscriber in your application if you want to collect those logs; do not initialize a new global subscriber per request.
Metrics Integration
Toggly business telemetry is separate from the optional local metrics/Prometheus integration. Enable the telemetry Cargo feature for native usage and metric export:
[dependencies]
toggly = { version = "0.6", features = ["telemetry"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Enable/configure it at startup, then record usage when the user actually uses a feature and views when it is shown. Evaluation checks are recorded by the client when usage tracking is enabled; a check is not the same action as a view or conversion.
use toggly::{MetricsFeatureOptions, TogglyClient, TogglyConfig};
pub fn telemetry_config(app_key: &str) -> TogglyConfig {
TogglyConfig::builder()
.app_key(app_key).environment("Production")
.enable_usage_tracking(true)
.enable_metrics(true)
.build()
}
pub fn record_conversion(client: &TogglyClient, identity: &str) {
client.record_view("enhanced-submit", Some(identity), "enabled");
client.record_usage("enhanced-submit", Some(identity), "enabled");
let options = MetricsFeatureOptions::new("enhanced-submit", Some("enabled"));
// Measures sum values in a flush window; counters accumulate counts.
client.measure("order-value", 120.0, Some(&options));
client.increment_counter("completed-orders", 1.0, Some(&options));
// Observations preserve individual metric samples.
client.observe("checkout-duration-ms", 350.0, Some(&options));
}
Create the client with TogglyClient::new(telemetry_config(app_key)).await?. In a real handler, call view and usage tracking at their respective events; the function groups them only to show their signatures. Metric options correlate a feature and optional variant label, not identity. A variant label is supplied by your application; Rust does not allocate an experiment variant.
Flush intervals default to 60 seconds and can be set with usage_flush_interval and metrics_flush_interval. metrics_base_url sets the telemetry service address. Await client.flush_telemetry() when an explicit flush is needed, and client.close() at shutdown; transport failures are handled best-effort, not propagated as evaluation failures. Without the telemetry Cargo feature or injected test senders, recording is inactive even when config flags are true.
Local metrics crate macros require an application-installed recorder to export anything; they do not send these business events to Toggly. Do not confuse the metrics Cargo feature with telemetry.
Testing
Add wiremock = "0.5" and serde_json = "1" to dev-dependencies. The following is a complete integration test for tests/features.rs; it never uses a real app key or remote definition URL.
use toggly::{EvalContext, TogglyClient, TogglyConfig};
use wiremock::{matchers::{method, path}, Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn evaluates_local_definitions() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/definitions/test-key/Production"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"new-dashboard": {
"featureKey": "new-dashboard",
"filters": [{"name": "AlwaysOn", "parameters": {}}]
},
"api-v2": {
"featureKey": "api-v2",
"filters": [{"name": "AlwaysOff", "parameters": {}}]
}
})))
.mount(&server).await;
let config = TogglyConfig::builder()
.app_key("test-key").environment("Production")
.definitions_url(server.uri())
.disable_background_refresh(true).enable_live_updates(false)
.disable_entity_context_registration(true)
.enable_usage_tracking(false).enable_metrics(false)
.build();
let context = EvalContext::with_identity("alice");
let client = TogglyClient::new(config).await.unwrap();
assert!(client.is_enabled("new-dashboard", context.clone()).await.unwrap());
assert!(!client.is_enabled("api-v2", context.clone()).await.unwrap());
assert!(!client.is_enabled("missing", context).await.unwrap());
assert_eq!(server.received_requests().await.unwrap().len(), 1);
client.close().await;
}
base_url alone would not redirect the initial definition fetch. The map contains full FeatureDefinition objects, not boolean values. Native adapter tests should register their actual middleware/guards and dispatch requests using the framework test client; a core-only evaluation does not test adapter wiring.
Performance Considerations
Reuse the client, avoid per-request initialization and refresh, and choose cache retention for your application. Measure the actual workload before making performance assumptions.
WebAssembly
Changing a TLS feature is not a WebAssembly integration recipe. These guides describe the native Tokio runtime and server adapters; no browser/WASM adapter is documented here.