# Devtools

Inspect live query and mutation state in gpui-query: ClientDiagnostic snapshots and patterns for building a debug panel.

gpui-query exposes runtime diagnostics through the [QueryClient](/docs/api/query-client). You get a serializable snapshot of every active query and mutation, which you can log, render in a debug panel, or serialize to JSON for later analysis.

## ClientDiagnostic

Calling `client.diagnostics(cx)` returns a `ClientDiagnostic` struct. It is a point-in-time snapshot of the client internals.

```rust
pub struct ClientDiagnostic {
    pub total_resources: usize,
    pub bucket_count: usize,
    pub mutation_count: usize,
    pub queries: Vec<QueryDiagnostic>,
}
```

- **total_resources**: number of active query resources across all type buckets.
- **bucket_count**: number of type-partitioned buckets. Each unique `(T, E)` pair gets its own bucket.
- **mutation_count**: number of active mutation resources.
- **queries**: per-query diagnostics, one entry per resource.

The struct derives `Clone`, `Debug`, and `Serialize`. You can serialize it to JSON and ship it to a log aggregator or render it in a GPUI debug panel.

## QueryDiagnostic

Each entry in `queries` is a `QueryDiagnostic`:

```rust
pub struct QueryDiagnostic {
    pub key: String,
    pub status: String,
    pub has_data: bool,
    pub has_error: bool,
    pub cache_policy: String,
    pub request_policy: String,
    pub cache_hits: u64,
    pub cancelled_count: u64,
    pub ignored_results: u64,
    pub last_updated_at_ms: Option<u128>,
    pub started_at_ms: Option<u128>,
}
```

The fields:

| Field | What it tells you |
|---|---|
| `key` | The stringified [QueryKey](/docs/api/queries) for this resource. |
| `status` | Human-readable label from `QueryStatus`: `"Idle"`, `"Loading empty"`, `"Loading with data"`, `"Success"`, `"Failure"`, or `"Cancelled"`. |
| `has_data` | Whether the resource currently holds cached data. |
| `has_error` | Whether the resource currently holds an error. |
| `cache_policy` | Label from the configured `CachePolicy`: `"No cache"`, `"Cache TTL 60s"`, or `"Stale-while-revalidate 60s"`. |
| `request_policy` | Label from the configured `RequestPolicy`: `"Latest wins"` or `"Ignore while loading"`. |
| `cache_hits` | How many times this resource returned cached data instead of fetching. |
| `cancelled_count` | How many in-flight requests were cancelled (for example, by a `LatestWins` policy replacing them). |
| `ignored_results` | How many stale or out-of-order responses were discarded. |
| `last_updated_at_ms` | UNIX timestamp in milliseconds of the last successful data update, if any. |
| `started_at_ms` | UNIX timestamp in milliseconds of the current or most recent request start, if any. |

## Basic usage

```rust
let client = cx.global::<QueryClient>();
let diag = client.diagnostics(cx);

println!("Active queries: {}", diag.total_resources);
println!("Buckets: {}", diag.bucket_count);
println!("Mutations: {}", diag.mutation_count);

for q in &diag.queries {
    println!(
        "  {} - {} (data: {}, error: {}, hits: {})",
        q.key, q.status, q.has_data, q.has_error, q.cache_hits
    );
}
```

## Filtering by type

If you only care about one type, use `query_diagnostics::<T, E>()`. It returns diagnostics for a single `(T, E)` bucket, or an empty vec if no bucket exists for that type pair.

```rust
let user_diags = client.query_diagnostics::<User, QueryError>(cx);
for q in &user_diags {
    println!("user query: {} - {}", q.key, q.status);
}
```

This avoids iterating over unrelated type buckets when you are debugging a specific domain type.

## Practical debugging patterns

### Check whether your TTL is right

Use `last_updated_at_ms` to figure out how old cached data is. Compare it to the current time. If data is consistently stale, your `CachePolicy::Ttl { ttl_ms }` might be too long. If `cache_hits` is zero across the board, your TTL might be too short and every read triggers a refetch.

```rust
let now = std::time::SystemTime::now()
    .duration_since(std::time::UNIX_EPOCH)
    .unwrap()
    .as_millis();

for q in &diag.queries {
    if let Some(updated) = q.last_updated_at_ms {
        let age_ms = now - updated;
        println!("{} is {}ms old", q.key, age_ms);
    }
}
```

### Verify deduplication is working

A high `cache_hits` count means the cache is doing its job. A count of zero on a resource that should be shared means you might be creating duplicate resources with slightly different keys. Check that your key construction is consistent.

```rust
for q in &diag.queries {
    if q.cache_hits == 0 && q.status == "Success" {
        println!("warning: {} has data but zero cache hits", q.key);
    }
}
```

### Spot flaky endpoints

Look at `cancelled_count` and `ignored_results`. A high cancelled count on a resource suggests rapid successive fetches are replacing each other (normal under `RequestPolicy::LatestWins`, but worth investigating if unexpected). High ignored results mean responses are arriving out of order relative to newer requests.

```rust
for q in &diag.queries {
    if q.cancelled_count > 5 {
        println!("{}: {} cancelled requests", q.key, q.cancelled_count);
    }
}
```

### Verify invalidation hit the right keys

After calling `invalidate_queries` on the [QueryClient](/docs/api/query-client), run diagnostics again to confirm the target resources moved to the expected status. If a key is still `"Success"` with stale data, your invalidation filter did not match it.

```rust
client.invalidate_queries(&filter, cx);

let diag = client.diagnostics(cx);
for q in &diag.queries {
    println!("{} - {}", q.key, q.status);
}
```

## Serialization

Because `ClientDiagnostic` and `QueryDiagnostic` both derive `Serialize`, you can dump the full client state to JSON in one call. This is useful for logging, snapshot tests, or building a debug panel that communicates over IPC.

```rust
let json = serde_json::to_string_pretty(&client.diagnostics(cx))
    .expect("diagnostics are serializable");
println!("{}", json);
```

> If you are building a devtools panel in GPUI, consider polling `diagnostics()` on a timer. The cost is low: it reads entity state without cloning the actual cached data.

## Observers

For real-time state tracking rather than point-in-time snapshots, see [Observers](/docs/advanced/observers). Observers subscribe to individual resource state changes, while diagnostics give you the full client picture in a single call. Use both together: diagnostics for the overview, observers for reactive updates on specific keys.
