Skip to content

Server-paged aggregation, cohort queries, and projection (#1) - #2

Merged
abhinavsquadstack merged 4 commits into
mainfrom
fix-lens-mcp-issue-1
Aug 10, 2026
Merged

Server-paged aggregation, cohort queries, and projection (#1)#2
abhinavsquadstack merged 4 commits into
mainfrom
fix-lens-mcp-issue-1

Conversation

@tarungarg546

@tarungarg546 tarungarg546 commented Aug 10, 2026

Copy link
Copy Markdown
Member

Why

Closes #1. A full latency root-cause investigation through the lens MCP was mostly working around the tool surface, not analysing: to answer "is latency growing across this call?" you pulled 60+ ttfb spans and computed percentiles in Python, two pulls overflowed the tool token limit and had to be spilled to disk, and several behaviours returned empty instead of erroring — indistinguishable from "no data", which led to a wrong conclusion caught in review. Every change here is grounded in a specific thing that cost time or produced a wrong number.

What changed

Area Change
aggregate_spans (new) Server-paged percentiles/mean/trend over a call's spans — pages past the 500-row cap so a p90 is over every span, not the first page. group_by=turn_bucket:N for the growth curve; metric=metadata:<key> for per-turn tokens.
aggregate_calls (new) The same statistic across a cohort, per-call and pooled — "is it this call or the campaign?"
get_call_details sections= and transcript_range= projection — a 258KB payload that overflowed the limit becomes 1,139 chars.
search_spans Rejects an unscoped metadata_filter (returned 0 hits silently against spans that carry the key); re-exposes event_name/phase; prefixes FILTER_IGNORED when the backend is older than #55 and drops them.
_get Retries 502/503/504 with backoff (the list_calls 504 seen in testing).
get_schema Appends the semantics that silently produce wrong numbers: turn_number is bot_stopped_count (not a user-turn counter), row caps, per-table retention, where token telemetry lives.
README / version Documents install/update (per-machine git pull, no auto-update) and reports the running version, so a stale install is diagnosable.

How I observed this working

Percentiles cross-checked against the raw rows for the same call — identical both ways:

raw p50 313.0 / p90 410.4   ==   aggregate_spans p50 313.0 / p90 410.4

Payload reduction (real call): raw spans 16,960 chars → aggregate_spans 836; get_call_details 18,1491,139 for sections="metadata".

The metadata_filter root cause, reproduced live on spans whose preview is literally {"model":"gpt-5.6-luna","transport":"sse"}:

metadata_filter=transport:sse            → 0 hits      (the bug)
node=llm.openai + metadata_filter=…sse   → hits        (scoped works)

Now the unscoped form raises with the fix, instead of returning a misleading empty page.

tests/test_stats.py passes (the only code here that produces a number rather than relaying one) — percentile/grouping/range/metric helpers.

Rollout & gating

No flag — new tools + additive params, existing tools unchanged. Install is a per-machine git pull ~/.cache/lens-mcp/repo (documented in the README); nothing auto-updates, so after merge, users must pull. FILTER_IGNORED means the backend predates squadrun/lens#55 — deploy that first.

Risks & deferred scope

  • Two tools depend on the backend: event_name/phase need lens#55; FILTER_IGNORED detects and reports when they're missing rather than returning wrong rows.
  • metric=metadata: per-turn tokens work only where the provider emits llm.usage (simplismart yes, OpenAI path no) — documented; backend fix is lens#54.
  • Backend-only items (a read-only query endpoint, retention) are out of scope, cross-filed as lens#54.

Tests

tests/test_stats.py — helper coverage, no new deps (uv run python tests/test_stats.py). Percentiles also cross-checked against raw rows live (above).

Related

Followed by #3 (fleet aggregation + discovery tools). Backend counterpart: squadrun/lens#56. Filed from #1.


  • No lead PII — this relays lens telemetry; adds no new data path. Prompts/transcripts are fetched by pre-existing tools, unchanged here.
  • I observed it — percentile cross-check, payload deltas, and the live metadata_filter reproduction above.
  • Shared surface? This is the MCP client; the shared HTTP contract is the lens ext API, whose consumers were traced in lens#56.

🤖 Generated with Claude Code

tarungarg546 and others added 2 commits August 10, 2026 13:20
Investigations were pulling hundreds of raw spans to compute percentiles
client-side, overflowing the tool output limit and — because a single spans
response is capped at 500 rows — producing wrong numbers with no warning.

- aggregate_spans: pages /call/{id}/spans to exhaustion and returns
  count/min/mean/max/percentiles, a first-third-vs-last-third trend, and
  optional grouping by node/phase/event_name/turn/turn_bucket:N. Reports
  complete:false rather than quietly measuring a prefix, and refuses to
  compute a trend across mixed node types.
- aggregate_calls: same statistic across a cohort selected with list_calls
  filters, per call and pooled, so "is this one call or the campaign?" is
  one tool call.
- get_call_spans: limit/offset paging, and a leading TRUNCATED notice when
  the page is a prefix of the match set.
- get_call_details: sections= and transcript_range= projection, so a 100KB+
  payload no longer has to be spilled to disk to read the metadata.
- search_spans: rejects metadata_filter without a node/event_name scope —
  unscoped it silently returns 0 hits against spans that carry the key — and
  exposes the event_name and phase filters that provide that scope.
- _get retries 502/503/504 and transport errors twice with backoff.
- get_schema appends the semantics that produce wrong numbers when assumed:
  turn_number is bot_stopped_count (not a user-turn counter), metadata_filter
  scoping, row caps, per-table retention, and where token telemetry does and
  does not live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up probing of the live ext API corrected two things in the previous
commit.

/search accepts event_name and phase and filters on neither: phase="ttfb" and
phase="complete" return byte-identical rows, and node=llm.openai&event_name=
llm.usage returns 20 llm.request spans. Exposing them invited exactly the
silent-wrong-answer this PR exists to remove, so search_spans no longer takes
them, and its metadata_filter guard now requires node alone. count_spans does
honour both and is unchanged. Verified as genuinely filtering on /search: node,
level, campaign_id, value_ms_min/max, node-scoped metadata_filter.

Per-turn token counts turned out to already be in spans — the llm.usage event
carries prompt_tokens, cache_read_input_tokens, completion_tokens and
total_tokens keyed by turn_number — but only on providers that emit it
(simplismart does, the OpenAI path does not). aggregate_spans and
aggregate_calls take metric="metadata:<key>" to measure a numeric metadata
field instead of value_ms, so prompt-context growth comes from spans (30-day
retention) rather than trace logs (4 days):

    aggregate_spans(sid, node="llm", event_name="llm.usage",
                    metric="metadata:prompt_tokens", group_by="turn")

Stat keys lose the _ms suffix when the metric is not a duration, since tokens
are not milliseconds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… skew

Reverses the workaround from the previous commit. squadrun/lens#55 makes /search
apply both filters, so removing them from the client was fixing the wrong layer —
a client workaround for a bug being fixed upstream in the same breath.

They deploy separately though, and installs here are per-machine git pulls, so a
client can outrun the backend. search_spans now inspects the rows it gets back and
prefixes FILTER_IGNORED when no row matches a filter it asked for, which is proof
the backend dropped it. The check is one-sided: absence of matches proves a drop,
presence proves nothing, since an ignored filter looks correct on a uniform page.
Verified against the current (un-deployed) backend, which trips it.

metadata_filter keeps its local guard, now worded as what the server enforces
rather than as a workaround for an empty result.

Also addresses the invisibility that let four merged commits sit unshipped:
version is reported in the get_schema client notes, bumped to 0.2.0, and the README
documents the update command, the bump-on-tool-change rule, and the backend-first
deploy order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Concrete vendor/model references (openai, deepgram, gemma4, simplismart) in tool
docstrings and the get_schema client notes replaced with neutral placeholders
(llm.<provider>, "the full model id") and vendor-free phrasing. Real node/model
values come from get_schema (the backend), so examples lose nothing and the docs
stop rotting when a provider/model name changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@abhinavsquadstack
abhinavsquadstack merged commit ff2c339 into main Aug 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make lens MCP exhaustive enough to root-cause a call without client-side reconstruction

2 participants