# Gateway insight reads

The insight operations, the metrics and units they answer in, the states a value can carry, and every refusal code.

Source: https://tokentriage.com/docs/reference/gateway-insights

---
import Callout from '../../../components/Callout.astro';
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';

This is the read contract behind a hosted gateway's traffic figures: five operations, the
document they take, the measurement catalogue they answer from, the three states a measure
can be in, and the closed refusal table. Reads only; no operation here writes anything.

Field names and values below are taken from `sdk/spec/insight-read-fixtures.json`, the
recording the lane's own handler wrote, and from the measurement catalogue that same
recording carries. [Read your gateway's traffic and spend](/docs/build/gateway-insights)
is the task-shaped version of this page.

## Operations

All five share the customer base path `/portal/api/v1/customer/v1/`. The organization and
workspace come from the path; a body that names different ones is turned away rather than
rewritten. None of them takes an `Idempotency-Key`: a first read mints a read context, so
a repeat is a different answer to the same question.

| Operation | Method and path | TypeScript | Python | CLI |
|---|---|---|---|---|
| `customer.insights.query` | `POST /organizations/{org}/workspaces/{ws}/insights/query` | `insights.query` | `insights.query` | `cloud insights overview`, `cloud insights drilldown` |
| `customer.insights.catalog` | `GET /organizations/{org}/insights/catalog` | `insights.catalog` | `insights.catalog` | `cloud insights catalog` |
| `customer.saved_views.list` | `GET /organizations/{org}/workspaces/{ws}/saved-views` | `savedViews.list` | `saved_views.list` | `cloud view list` |
| `customer.saved_views.get` | `GET /organizations/{org}/saved-views/{sv}` | `savedViews.get` | `saved_views.get` | `cloud view get` |
| `customer.saved_views.open` | `POST /organizations/{org}/saved-views/{sv}/open` | `savedViews.open` | `saved_views.open` | `cloud view open` |

Saved-view creation, editing and deletion are supported by all three clients; see
[Save a gateway view](/docs/build/save-gateway-views). Runtime feedback submission uses
the separately authorized gateway endpoint; see [App feedback](/docs/build/report-app-feedback).
`saved_views.open` sends no body: its current authority comes from the credential.

Every read needs `metadata.read` on the scope it asks about. A metric whose row says so
also needs `cost.read`; without it that cell reads `unavailable{scope_not_authorized}` and
the rest of the answer still arrives.

## The query document

`insight-query/v1`, the body of `customer.insights.query`.

| Field | Type | Notes |
|---|---|---|
| `schema` | integer | `1` |
| `shape` | enum | `overview`, `series`, `facets`, `drilldown`, `markers` |
| `scope` | object | `organization_id`, `workspace_id`, `application_ids`, `gateway_ids`, `environment_ids` |
| `from`, `to` | RFC 3339 | half-open UTC window, `[from, to)` |
| `grain` | enum | `hour`, `day`, `week`, `month`; required for `series`, absent elsewhere |
| `display_tz` | IANA zone | labels buckets; the arithmetic stays UTC |
| `purposes` | list | defaults to `["application"]` server-side |
| `release_ids` | list of release ids | optional; match any listed release within the existing scope; absent or empty means no additional release narrowing |
| `metrics` | list | versioned ids such as `provider_spend.v1`; absent on `drilldown` and `markers` |
| `facets` | list | members of the closed facet set, for the `facets` shape |
| `drilldown` | enum | `request` or `attempt`, for the `drilldown` shape; `feedback` is registered, see [feedback rows](#read-feedback-rows) |
| `feedback_filter` | object | `feedback` grain only: `logical_request_id`, `match` |
| `read_context` | object | absent on the first read of a view, echoed unchanged after it |
| `cursor`, `page_size` | string, integer | paging for row shapes |

A release filter applies to all query shapes and is part of the read-context identity.
Changing it requires a fresh read context. It grants no access to another application or
environment, and a saved view rechecks the opener's current authority.

The scope narrows the ceiling the caller's bindings already resolved and cannot widen it.
An empty list means "every descendant I may read". Naming an application outside the
caller's bindings answers `403 forbidden`; another organization is concealed.

The closed enums, as the catalogue publishes them:

| Set | Members |
|---|---|
| shapes | `overview`, `series`, `facets`, `drilldown`, `markers` |
| grains | `hour`, `day`, `week`, `month` |
| purposes | `application`, `playground`, `comparison`, `judge`, `connection_validation` |
| facets | `final_model`, `route`, `outcome`, `release`, `connection`, `purpose`, `input_size_bucket` |
| units | `count`, `ratio`, `nano_usd`, `tokens`, `milliseconds`, `seconds`, `marker`, `coverage` |

Purposes answer as separate groups. A query naming two of them returns one group per
purpose rather than a blended figure.

## The measurement catalogue

`customer.insights.catalog` answers from the server's build and reaches no warehouse, so
it returns the same document on a deployment whose analytics producer is switched off.
Each row carries the question it answers, its unit, its population, its aggregation, the
rule for a missing value, the actions reading it needs, and whether anything writes it in
this build.

| Metric | Unit | Population | Aggregation | Missing rule | Needs | Producer |
|---|---|---|---|---|---|---|
| `requests.v1` | `count` | requests | count_distinct | `coverage_unknown` | `metadata.read` | produced |
| `outcomes.v1` | `count` | requests | count | `coverage_unknown` | `metadata.read` | produced |
| `completion_rate.v1` | `ratio` | requests | ratio | `denominator_zero` | `metadata.read` | produced |
| `provider_spend.v1` | `nano_usd` | cost_effects | sum | `unpriced_population` | `metadata.read` + `cost.read` | produced |
| `cost_per_1000_requests.v1` | `nano_usd` | cost_effects | ratio | `denominator_zero` | `metadata.read` + `cost.read` | produced |
| `cost_per_completed.v1` | `nano_usd` | cost_effects | ratio | `denominator_zero` | `metadata.read` + `cost.read` | produced |
| `tokens.v1` | `tokens` | attempts | sum | `coverage_unknown` | `metadata.read` | produced |
| `response_latency.v1` | `milliseconds` | requests | quantile | `not_observed` | `metadata.read` | produced |
| `first_output_latency.v1` | `milliseconds` | requests | quantile | `not_observed` | `metadata.read` | produced |
| `route_mix.v1` | `ratio` | requests | share | `denominator_zero` | `metadata.read` | produced |
| `model_mix.v1` | `ratio` | requests | share | `denominator_zero` | `metadata.read` | produced |
| `release_annotations.v1` | `marker` | release_markers | list | `coverage_unknown` | `metadata.read` | produced |
| `app_feedback.v1` | `count` | feedback | count | `not_observed` | `metadata.read` | produced |
| `app_feedback_rated_requests.v1` | `count` | feedback | count_distinct | `not_observed` | `metadata.read` | produced |
| `activity_spend.v1` | `nano_usd` | cost_effects | sum | `unpriced_population` | `metadata.read` + `cost.read` | produced |
| `team_runs.v1` | `count` | requests | count_distinct | `coverage_unknown` | `metadata.read` | produced |
| `freshness_lag.v1` | `seconds` | read_context | latest | `coverage_unknown` | `metadata.read` | produced |
| `coverage.v1` | `coverage` | read_context | latest | `coverage_unknown` | `metadata.read` | produced |
| `protection.v1` | `count` | protection_evaluations | count | `not_observed` | `metadata.read` | produced |
| `test_quality.v1` | `ratio` | none | none | `no_producer` | `metadata.read` | no_producer |
| `paired_reduction.v1` | `ratio` | none | none | `no_producer` | `metadata.read` + `cost.read` | no_producer |
| `cohort_projection.v1` | `nano_usd` | none | none | `unpriced_population` | `metadata.read` + `cost.read` | produced_elsewhere |
| `budget_standing.v1` | `nano_usd` | none | none | `unpriced_population` | `metadata.read` + `cost.read` | produced_elsewhere |
| `attention_rules.v1` | `count` | none | none | `no_producer` | `metadata.read` | no_producer |
| `saved_view.v1` | `count` | none | none | `scope_not_authorized` | `metadata.read` | produced_elsewhere |

A metric whose producer is `no_producer` may be named in a query. Its cell answers
`unavailable{no_producer}`, which is how a metric picker can offer the question while
showing that nothing writes it yet.

`produced_elsewhere` says the opposite: the fact IS recorded, by the control plane rather
than by the analytics projection, and is read through that producer's own operations —
`saved_view.v1` through the saved-view operations — so an insight query answers
`unavailable{no_producer}` for it while a picker must not grey it out.

Model and route shares count each logical request once at its final served model/route,
including an unserved group. Fallback attempts remain separate in attempt usage and spend.
Unit-cost ratios are unavailable when cost, attribution or the request population is incomplete;
the known provider-spend subtotal remains visible with its coverage disclosure.

Ratio metrics carry their numerator and denominator on the catalogue row:
`completion_rate.v1` is requests whose terminal status is succeeded over requests admitted
in the window, and `cost_per_1000_requests.v1` is settled spend in the window times one
thousand over requests admitted in the window. Latency metrics store the figures the
producer measured in milliseconds and are not recomputed from stored instants; a
non-streaming request contributes no `first_output_latency.v1` observation.

<Details summary="The full catalogue as the CLI prints it, with each metric's question">

```text
requests.v1	count	produced	metadata.read	How many requests did this application make?
outcomes.v1	count	produced	metadata.read	How did those requests end?
completion_rate.v1	ratio	produced	metadata.read	What share of requests produced a usable answer?
provider_spend.v1	nano_usd	produced	metadata.read + cost.read	What did this application's upstream usage cost?
cost_per_1000_requests.v1	nano_usd	produced	metadata.read + cost.read	What does a thousand requests cost?
cost_per_completed.v1	nano_usd	produced	metadata.read + cost.read	What does a request that actually worked cost?
tokens.v1	tokens	produced	metadata.read	How many tokens did the upstreams read and write?
response_latency.v1	milliseconds	produced	metadata.read	How long did a request take end to end?
first_output_latency.v1	milliseconds	produced	metadata.read	How long until the first token reached the caller?
route_mix.v1	ratio	produced	metadata.read	Which routes served this traffic, and in what proportion?
model_mix.v1	ratio	produced	metadata.read	Which models answered, and in what proportion?
release_annotations.v1	marker	produced	metadata.read	When did a configuration release take effect here?
app_feedback.v1	count	produced	metadata.read	What did the application's own users say about these answers?
app_feedback_rated_requests.v1	count	produced	metadata.read	How many requests admitted in this window have app-reported feedback?
activity_spend.v1	nano_usd	produced	metadata.read + cost.read	What did each kind of activity cost?
team_runs.v1	count	produced	metadata.read	How much of this traffic was the team's own work rather than the application's?
freshness_lag.v1	seconds	produced	metadata.read	How far behind live is this view?
coverage.v1	coverage	produced	metadata.read	What might be missing from this view, and how much?
protection.v1	count	produced	metadata.read	How many protection component phases were evaluated?
test_quality.v1	ratio	no_producer	metadata.read	How well did this configuration do on the saved tests?
paired_reduction.v1	ratio	no_producer	metadata.read + cost.read	How much cheaper was the new configuration on the same work?
cohort_projection.v1	nano_usd	produced_elsewhere	metadata.read + cost.read	What would this change have cost on last month's traffic?
budget_standing.v1	nano_usd	produced_elsewhere	metadata.read + cost.read	How much of the budget is left?
attention_rules.v1	count	no_producer	metadata.read	What should somebody be told about without asking?
saved_view.v1	count	produced_elsewhere	metadata.read	What did this person choose to keep looking at?
```

That run went through a loopback server replaying the recorded catalogue; the closed enums
follow it on stderr.

</Details>

## A value and its three states

Every measure is one of three things, and only two of them carry a number.

| State | `number` | `unit` | `coverage` | `reason` | Meaning |
|---|---|---|---|---|---|
| `available` | yes | yes | absent | absent | the figure this service stands behind |
| `partial` | yes | yes | one or more entries | absent | a floor: the truth is larger, for the listed reasons |
| `unavailable` | absent | absent | absent | one reason | no figure exists to return |

<Callout type="key">

An `unavailable` value carries no number and no unit, so a renderer has nothing to reach
for and format as `$0.00`. Zero spend and unknown spend are different facts, and the
contract's own validator turns away a value that carries both a reason and a number.

</Callout>

The reason set is closed. Each one implies a different remedy.

| Reason | What it says | What changes it |
|---|---|---|
| `no_producer` | nothing in this build writes the fact | a later release |
| `metric_version_absent` | the metric exists; this version of it does not | ask for a version the catalogue publishes |
| `unpriced_population` | some rows carry no price, so a sum would understate spend | pricing for those rows |
| `not_observed` | the population cannot produce that observation | nothing: a non-streaming request has no first-output sample |
| `scope_not_authorized` | the delegation lacked the action this measure needs | a grant, usually `cost.read` |
| `content_withheld` | the measure would disclose prompt or completion text | `content.read` |
| `coverage_unknown` | the ingest side cannot bound what is missing | retry once the frontier moves |
| `denominator_zero` | a ratio over an empty population | traffic in the window |
| `excluded` | every row of the population sits under an exclusion | nothing: the rows were removed |

Numbers travel as decimal strings, including counts. Money is integer nano-USD: one
US dollar is `1000000000`, and `1234567890123` is $1,234.567890123. Parsing one into a
binary floating-point number loses digits above about $9,007,199 of spend.

## The answer

One answer carries the pin it was computed under and exactly one body: `groups` for
`overview`, `series` and `facets`; `requests` or `attempts` for `drilldown`; `markers` for
`markers`.

| Field | Type | Notes |
|---|---|---|
| `read_context` | object | the pin; echo it on every continuation of the same view |
| `minted` | boolean | true when this read created the context |
| `shape` | string | the shape that was asked for |
| `groups` | list | one entry per purpose, each with `cells` of `{metric, value}` |
| `requests`, `attempts`, `markers` | list | the row bodies; `feedback` on the `feedback` grain |
| `coverage` | list | what the ingest side disclosed for the window |
| `truncated` | boolean | true when a row bound was reached |

A window with no traffic answers with `groups` absent. It is not a group of zeros, and a
client that supplies one is publishing traffic that did not occur.

<Details summary="One recorded overview answer, verbatim">

```json
{
  "read_context": {
    "schema": 1,
    "central_account_id": "acct_K11111111111",
    "workspace_id": "t_K11111111111",
    "scope_digest": "f644d618df3314b70c1de5a5cc3d3da82b31dc31af3917ac8af4ab59279e2800",
    "authority_epoch": "1",
    "catalog_version": 1,
    "filter_version": 1,
    "projection_generation": "4",
    "retention_generation": "1",
    "exclusion_generation": "7",
    "from": "2026-09-20T12:00:00Z",
    "to": "2026-09-21T12:00:00Z",
    "display_tz": "UTC",
    "purposes": [
      "application"
    ],
    "arrival_fence": "918273",
    "watermarks": [
      {
        "producer_class": "gateway_journal",
        "produced_through": "918290",
        "indexed_through": "918273",
        "last_indexed_at": "2026-09-21T11:58:30Z"
      },
      {
        "producer_class": "control_plane_outbox",
        "produced_through": "41902",
        "indexed_through": "41902",
        "last_indexed_at": "2026-09-21T11:59:48Z"
      }
    ],
    "evaluated_at": "2026-09-21T12:00:00Z"
  },
  "minted": true,
  "shape": "overview",
  "groups": [
    {
      "purpose": "application",
      "cells": [
        {
          "metric": "requests.v1",
          "value": {
            "state": "available",
            "number": "1284",
            "unit": "count"
          }
        },
        {
          "metric": "provider_spend.v1",
          "value": {
            "state": "unavailable",
            "reason": "unpriced_population"
          }
        },
        {
          "metric": "cost_per_1000_requests.v1",
          "value": {
            "state": "unavailable",
            "reason": "unpriced_population"
          }
        },
        {
          "metric": "tokens.v1",
          "value": {
            "state": "partial",
            "number": "84210773",
            "unit": "tokens",
            "coverage": [
              "ingest_gap:gateway_journal"
            ]
          }
        },
        {
          "metric": "response_latency.v1",
          "value": {
            "state": "available",
            "number": "412",
            "unit": "milliseconds"
          }
        }
      ]
    }
  ],
  "coverage": [
    "ingest_gap:gateway_journal"
  ]
}
```

Every `unit` in a recorded answer is one of the eight in the closed set above, and it is
the spelling the catalogue publishes for that metric — `milliseconds` for
`response_latency.v1`. A recorded unit outside the set fails the lane's own sweep.

</Details>

### The read context

The pin a read was computed under. Presenting a stale one answers `410
read_context_expired` with the remedy `refresh_view`.

| Field | Notes |
|---|---|
| `schema`, `catalog_version`, `filter_version` | contract versions this answer was computed under |
| `central_account_id`, `workspace_id`, `scope_digest` | who asked, and a digest of the scope that resolved |
| `authority_epoch` | the epoch of the caller's authority |
| `projection_generation`, `retention_generation`, `exclusion_generation` | generations of the projection, the retention policy and the exclusion set |
| `from`, `to`, `grain`, `display_tz`, `purposes` | the question's window and framing |
| `arrival_fence` | the arrival sequence this answer was computed up to |
| `watermarks` | per producer class: `produced_through`, `indexed_through`, `last_indexed_at` |
| `coverage`, `evaluated_at` | disclosed gaps, and when the pin was taken |

A watermark whose `produced_through` is ahead of its `indexed_through` means recent
traffic has been written and not yet indexed. The two producer classes are
`gateway_journal` and `control_plane_outbox`.

### Row shapes

`drilldown` answers with one of two row kinds. Neither carries prompt text, completion
text or money: the tables behind them hold none.

| Grain | Fields |
|---|---|
| `request` | `logical_request_id`, `purpose`, `occurred_at`, `status`, `terminal_kind`, `release_id`, `final_model`, `final_provider`, `attempt_count`, `total_ms`, `first_output_ms`, `first_output_known`, `streaming` |
| `attempt` | `logical_request_id`, `attempt_id`, `ordinal`, `purpose`, `role`, `route_changed`, `output_state`, `provider_id`, `provider_model`, `connection_id`, `usage_source`, `usage_known`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `reasoning_tokens`, `total_tokens`, `dispatched_at`, `terminal_at` |
| `feedback` | `logical_request_id`, `application_id`, `gateway_id`, `environment_id`, `issuer_ref`, `issuer`, `feedback_id`, `value`, `revision`, `occurred_at`, `source`, `match`, `purpose` |

`first_output_ms` is absent whenever `first_output_known` is false, which is every
non-streaming request. The same rule governs `usage_known` on an attempt row: when it is
false, the token fields carry no measurement. Rendering either absence as `0` reports a
figure nobody recorded.

`markers` answers with `marker_kind`, `release_id`, `effective_digest`, `gateway_id`,
`environment_id`, `actor_kind` and `occurred_at`.

## Saved views

A saved view is a stored question, not a grant and not stored SQL. Opening one re-resolves
the reader's own scope, so two members of a team can open the same view and see different
answers.

| Field | Notes |
|---|---|
| `id`, `organization_id`, `workspace_id`, `owner` | identity and author |
| `name` | the label a person chose |
| `audience` | `personal` or `team` |
| `filter_version` | the filter contract the stored query was written against |
| `query` | an `insight-query/v1` document |
| `time_intent` | `fixed`, or `relative` with `relative_window_seconds` |
| `display_tz`, `revision`, `created_at`, `updated_at` | rendering zone and row versioning |

`customer.saved_views.open` answers a document with three members: `view`, `query` and
`answer`. The question and its answer travel together, so a rendered chart and the caption
over it cannot come from two moments of the reader's access.

## Refusals

The catalogue publishes this table, and the clients carry it row for row. A code's
`next_action` is the published remedy.

| Code | Status | Next action |
|---|---|---|
| `forbidden` | 403 | `refresh_access` |
| `budget_exceeded` | 503 | `narrow_window` |
| `coverage_unknown` | 503 | `retry` |
| `snapshot_expired` | 410 | `start_new_query` |
| `read_context_expired` | 410 | `refresh_view` |
| `scope_required` | 400 | `check_selection` |
| `metric_version_unknown` | 400 | `check_selection` |
| `purpose_mixing_refused` | 400 | `check_selection` |
| `window_too_large` | 400 | `check_selection` |
| `invalid_query` | 400 | `check_selection` |
| `dependency_unavailable` | 503 | `retry` |
| `validation_failed` | 422 | `check_selection` |

403 and 503 answer different questions. A 403 is a permission the caller does not hold,
and no retry reaches past it. A 503 is this service failing to answer a question the
caller may ask. Both SDKs give them separate classes, `InsightForbiddenError` and
`InsightUnavailableError`, and recognition takes the status and the `type` together: a
403 body claiming `coverage_unknown` matches neither and arrives as the base problem
error with every field preserved.

The last two rows are the ones a caller meets outside a question of scope or arithmetic:

- `dependency_unavailable`, 503, remedy `retry`. The analytics hop failed, or the
  deployment composes no reader. Both SDKs carry it as an `InsightUnavailableError`.
- `validation_failed`, 422, remedy `check_selection`. A saved view whose stored question
  this build can no longer ask refuses on `open` and still reads through `get`. It is
  neither a permission problem nor an outage, so both SDKs raise the BASE
  `InsightRefusalError` for it rather than either subclass.

Refusal rows arrive from `customer.insights.catalog` as `code`, `status` and
`next_action` — snake_case, like every other member on this lane. The published table and
each SDK's own table are equal row for row, in both directions, and the clients' tests
assert exactly that.

<NextSteps items={[
  { title: "Read your gateway's traffic and spend", href: "/docs/build/gateway-insights", description: "The same contract as a task: ask for an overview, drill into the rows, open a view, and read a cell that has no number." },
  { title: "How to read a cost figure", href: "/docs/build/cost-figures", description: "What licenses a dollar amount, and what an unpriced row does to a total you might forward to finance." },
  { title: "Who can do what in your company", href: "/docs/build/organizations-and-access", description: "Where metadata.read and cost.read come from, which decides which cells you can see." },
  { title: "Report and read app feedback", href: "/docs/build/report-app-feedback", description: "How feedback is written, and how distinct rated requests determine coverage." }
]} />

Partial counts and additive spend/token sums are lower bounds. Ratios (including cost
per request) and percentiles computed from incomplete populations can move higher or
lower as missing rows arrive; they are not lower bounds. A producer with no indexed
observation has **unmeasured freshness**, even if both reported counters are zero.


### Open request evidence from a chart

Selecting a supported model, outcome or release in a gateway chart opens **Requests** with the same UTC window, purpose and environment. Opening the tab starts a fresh read; its arrival fence and coverage are displayed. The current query API returns at most 100 request rows and a truncation flag, without a continuation cursor. Facet and release matching applies to that bounded slice, so its matching row count is not the whole-window count. Narrow the window in Overview when needed. Route and input-size facets are not available in the request projection; the page explicitly refuses to claim those filters matched.

Select a request to read its attempts under the same read context and its separately authorized, current metadata receipt. The receipt shows the observed provider/model, connection, release and route version, protection disclosure and provider cost when available. It does not request prompt or response content. Missing protection evidence does not mean every guard passed. Unpriced, withheld or numerically unsafe costs remain unavailable. An expired read context requires **Refresh requests**; no automatic retry changes the population silently.


### Protection observation counts

`protection.v1` counts observed detector evaluations at the unique logical-request,
component and phase grain. Each answer group also carries `protection` with
`observed_requests`, `unknown_requests`, `eligible`, `evaluated`, `blocked`,
`modified`, `errors`, `not_executed` and `blocked_requests`. All counts use the
same authorized scope, purpose, time, release and read context as that group.
Two evaluated guards on one refused request mean two evaluations and one blocked
request. Eligibility records actual engine selection; configuration alone does
not establish evaluation. A detector that was unavailable or bypassed remains an
explicit error or not-executed observation.

Historical receipts without these observations contribute `unknown_requests`.
A mixed population reports a partial evaluation count with
`protection_observations_missing`; an entirely unknown population is unavailable.
An explicitly observed phase selecting no components can report zero evaluations.
Source ingestion gaps remain additional coverage limitations. The TypeScript
SDK exposes `InsightGroup.protection`; Python and CLI JSON preserve the same
fields. Request drill-down retains logical request identities and its separately
authorized receipt disclosure; counts never grant content access.

Request drill rows may include `protection_evaluations`: the actual component ID,
immutable component version, phase, eligibility, evaluation and outcome for that
request revision. The component version hashes the selected effective binding
and compiled detector generation; no finding text or request content is included.
An absent observation block is unknown, not a passed protection check. Reopening
uses current read authority and the same source arrival fence as the count.


### Completion denominators and spend coverage

Each measured group can include `evidence_coverage`. Its `completion` object
states the completed numerator and observed-request denominator, plus mutually
exclusive completed, refused, failed, partial, cancelled and pending-or-unknown
counts. Cancellation takes precedence over other terminal status labels. Source
gaps remain partial coverage; the observed denominator is not an assertion that
unseen requests do not exist. The completion scalar keeps its ratio meaning.

The `spend` object reports `counts` only with current cost-read authority:
`priced_effects`, `pending_effects`, `unpriced_effects`, `estimated_effects`,
`estimate_status_unknown_effects` and `adjustment_effects`. These count unique
cost effects, not requests. Estimated and unknown-estimate counts are subsets of
priced effects, so do not add them to the priced count. Signed corrections are
listed separately. The known nano-USD subtotal keeps its existing meaning.

Estimate status comes from the pricing authority's explicit flag on the first
terminal settlement. Historical effects and correction companions without that
flag remain unknown; absent does not mean false. Pending and unpriced effects do
not acquire a measured zero price. Scope denial withholds the counts; an empty
or unreadable population is unavailable. The TypeScript SDK exposes
`InsightGroup.evidence_coverage`; Python and CLI JSON preserve these same fields.


### Drill into observed latency

A request-grain `drilldown` query accepts `latency_filter` with `metric` set to
`response_latency.v1` or `first_output_latency.v1`. Optional integer `min_ms`
(inclusive) and `max_ms` (exclusive) narrow the duration range. Optional `outcome`
is `overall`, `succeeded`, `refused`, `failed`, `cancelled`, `partial`, or `unknown`.
These are the same disjoint populations used by the latency summary; cancellation
takes precedence. Missing timing samples and pending requests are excluded,
and first-output timing includes observed streams only. An observed zero remains
zero. Invalid bounds or use on an overview/attempt query are refused.

Keep the same authorized scope, UTC window, purpose, release, and read context
when following a percentile to its requests. Filtering happens before the bounded
request page; truncation remains explicit. The TypeScript and Python clients carry
`latency_filter` on the normal insight query, and CLI JSON accepts the same query.

A request-grain `drilldown` query with `drilldown: "attempt"` accepts
`attempt_filter` with optional `provider_id`, `provider_model`,
`cost_effects_only`, and `logical_request_id` members, each up to 512 bytes
and free of control characters. Setting `logical_request_id` scopes the
attempt page to that one logical request on the server; it is not a
page-local filter applied after the fact. An empty `logical_request_id` or
use on any other grain or shape is refused as `validation_failed`.
`cost_effects_only` requires `cost.read` and selects from the scoped
cost-effect population at cost occurrence time, naming rows without attempt
metadata `unattributed`. The CLI accepts
`tokentriage cloud insights drilldown --grain attempt --request <id>`, and
the TypeScript and Python SDKs carry `logical_request_id` on
`InsightAttemptFilter`.


### Read feedback rows

<Callout type="note" title="feedback is part of the registered insight-query/v1">
The `feedback` drilldown grain and `feedback_filter` are a registered, additive part of
`insight-query/v1`: `schema` stays `1` and the catalogue is unchanged. A deployment older than
this change refuses the grain: it answers `400 invalid_query` or `422 validation_failed`, which
the CLI and both SDKs report as "not served by this deployment". The examples below are
recorded cases from `sdk/spec/insight-read-fixtures.json`, not answers captured from a running
deployment.
</Callout>

A `drilldown` query with `drilldown: "feedback"` answers with `feedback`: one row per
app-reported verdict identity (request, issuer, `feedback_id`) at its newest revision. The
window selects verdicts by their `occurred_at` in `[from, to)`, the same population as
`app_feedback.v1`. Rows are ordered newest `occurred_at` first, then by request, issuer and
`feedback_id`, up to `page_size`; `truncated` is true when the page is full.

The read needs `metadata.read` on the scope. The issuer label also needs `content.read`. A
`ttv_` runtime key, including one holding `feedback.write`, cannot reach this lane and answers
401.

`feedback_filter` narrows the grain:

| Field | Notes |
|---|---|
| `logical_request_id` | scopes the page to one request's verdicts on the server; an empty id is refused rather than read as no filter |
| `match` | `matched`, `pending` or `unmatched` |

`feedback_filter` on any other grain or shape answers `400 invalid_query`, like an off-grain
`attempt_filter`. `release_ids` on the feedback grain is refused the same way: a verdict
carries no release of its own, and a release filter would silently drop every row that has
not joined a request.

Each row carries:

| Field | Notes |
|---|---|
| `logical_request_id` | the request the verdict names |
| `application_id`, `gateway_id`, `environment_id` | where the verdict was filed: the scope of the key that submitted it |
| `issuer_ref` | an opaque stored handle, `fis_` and 32 lowercase hex characters, on every row; `unassigned` for a verdict stored before handles existed |
| `issuer` | the application's label for whoever rated; present only when the reader holds `content.read` |
| `feedback_id` | the application's question id, verbatim |
| `value` | `up` or `down` |
| `revision` | the newest stored revision, numbered by the application, as a decimal string |
| `occurred_at` | that revision's `occurred_at` |
| `source` | always `app_reported` |
| `match` | `matched`, `pending` or `unmatched` |
| `purpose` | the joined request's purpose, on `matched` rows only |

`matched` means the request was found in the reader's scope. `pending` means it may not be
indexed yet. `unmatched` means the gateway journal is indexed past the point the request would
have arrived and no such request was served in the scope the verdict was filed under; while
that progress is unknown, a row stays `pending`. There is no count of earlier revisions: stored
revisions may already have been folded together, so a count would overstate the trail.

Key rows on `issuer_ref`. When any row reads `unassigned`, the answer's `coverage` carries
`issuer_ref_unassigned` once; it qualifies the issuer handles, not a measured population.

The purpose rule: a `matched` row is filtered by the query's `purposes`. A `pending` or
`unmatched` row has joined no request, so its purpose is unknown; it is included whatever
`purposes` the query named, with `purpose` absent. Count only `matched` rows toward a purpose.

The CLI reads the grain with `tokentriage cloud insights drilldown --grain feedback`, taking
`--request <id>` and `--match <state>`, and `tokentriage cloud request get --feedback` reads
one request's verdicts after its receipt. Both SDKs carry `feedback_filter` on the insight
query and `feedback` on the answer, with `revision` kept as a string.

<Details summary="One recorded feedback read, verbatim">

_Not yet executed against a deployment._

The question, from the recorded case `drilldown_feedback_request`:

```json
{
  "feedback_filter": {
    "logical_request_id": "lrq_K11111111111"
  },
  "schema": 1,
  "shape": "drilldown",
  "scope": {
    "organization_id": "",
    "workspace_id": ""
  },
  "from": "2026-09-20T12:00:00Z",
  "to": "2026-09-21T12:00:00Z",
  "display_tz": "UTC",
  "purposes": [
    "application"
  ],
  "drilldown": "feedback",
  "page_size": 50
}
```

The recording's empty `organization_id` and `workspace_id` name nothing, so the lane takes both
from the path. The answer's `data`, as a `metadata.read` reader receives it:

```json
{
  "read_context": {
    "schema": 1,
    "central_account_id": "acct_K11111111111",
    "workspace_id": "t_K11111111111",
    "scope_digest": "f644d618df3314b70c1de5a5cc3d3da82b31dc31af3917ac8af4ab59279e2800",
    "authority_epoch": "1",
    "catalog_version": 1,
    "filter_version": 1,
    "projection_generation": "4",
    "retention_generation": "1",
    "exclusion_generation": "7",
    "from": "2026-09-20T12:00:00Z",
    "to": "2026-09-21T12:00:00Z",
    "display_tz": "UTC",
    "purposes": [
      "application"
    ],
    "arrival_fence": "918273",
    "watermarks": [
      {
        "producer_class": "gateway_journal",
        "produced_through": "918290",
        "indexed_through": "918273",
        "last_indexed_at": "2026-09-21T11:58:30Z"
      },
      {
        "producer_class": "control_plane_outbox",
        "produced_through": "41902",
        "indexed_through": "41902",
        "last_indexed_at": "2026-09-21T11:59:48Z"
      }
    ],
    "evaluated_at": "2026-09-21T12:00:00Z"
  },
  "minted": true,
  "shape": "drilldown",
  "feedback": [
    {
      "logical_request_id": "lrq_K11111111111",
      "application_id": "app_M11111111111",
      "gateway_id": "gw_M11111111111",
      "environment_id": "env_K11111111111",
      "issuer_ref": "fis_fedcba9876543210fedcba9876543210",
      "feedback_id": "fb-7c1e",
      "value": "up",
      "revision": "1",
      "occurred_at": "2026-09-21T11:56:00Z",
      "source": "app_reported",
      "match": "pending"
    },
    {
      "logical_request_id": "lrq_K11111111111",
      "application_id": "app_K11111111111",
      "gateway_id": "gw_K11111111111",
      "environment_id": "env_K11111111111",
      "issuer_ref": "fis_0123456789abcdef0123456789abcdef",
      "feedback_id": "fb-2a90",
      "value": "down",
      "revision": "3",
      "occurred_at": "2026-09-21T11:51:00Z",
      "source": "app_reported",
      "match": "matched",
      "purpose": "application"
    },
    {
      "logical_request_id": "lrq_K11111111111",
      "application_id": "app_K11111111111",
      "gateway_id": "gw_K11111111111",
      "environment_id": "env_K11111111111",
      "issuer_ref": "unassigned",
      "feedback_id": "fb-0001",
      "value": "up",
      "revision": "1",
      "occurred_at": "2026-09-21T11:30:00Z",
      "source": "app_reported",
      "match": "matched",
      "purpose": "application"
    }
  ],
  "coverage": [
    "issuer_ref_unassigned"
  ]
}
```

The `pending` row was filed under another application's scope and has joined no request there,
so it has no `purpose`. The recorded case `drilldown_feedback_content` is the same read by a
reader who also holds `content.read`: the same rows and handles, each with an `issuer` label
added.

</Details>
