# Analytics: what the numbers measure

> How the BDN analytics screen computes land rate, land latency percentiles, fan-out volume, dedup hit ratio, per-leader attribution, and feed freshness against target.

The Analytics screen reads a single procedure, `analytics.summary`, and
renders it without further aggregation. Everything on the screen is
therefore a direct projection of that response. This page defines what
each field measures, how the series are bucketed, and how to read the
colour bands.

The screen only renders once the BDN is deployed. Until `mod_bdn` /
`mod_solana_tpu` nodes are running and transport metrics are being
collected, you get the pending state instead of charts, because there is
no series to draw.

## The 24-hour window and bucket size

`analytics.summary` takes no input. It serves a **fixed 24-hour
window**. The screen header states this (`last 24h`) and there is no
range selector — an earlier 1h/24h/7d control was removed because it did
not change the procedure's response, and a control that does not move
the data is a lie.

The response carries plain arrays, not timestamped points. Index `0` is
the oldest bucket and the last index is the most recent. The charts label
the axis `−24h · −18h · −12h · −6h · now` (fan-out uses
`−24h · −16h · −8h · now`), so the bucket width is whatever divides the
24-hour window into `array.length` buckets. Read bucket size as
`24h ÷ series length` rather than assuming a fixed interval — different
series in the same response may have different lengths, and the CSV
export explicitly handles that (see below).

Series that FirstByte has no samples for come back empty. The screen
renders `—` for the fan-out peak, and the leader / region / feed lists
render their own empty copy rather than a zero row.

## How land rate and land latency are measured

**Land rate** is the percentage of submitted transactions that landed.
It arrives as two parallel series under `landRate`:

| Field            | Series meaning                                   | Colour       |
| ---------------- | ------------------------------------------------ | ------------ |
| `landRate.bdn`   | Transactions submitted through the BDN edges     | `--chart-1`  |
| `landRate.tpu`   | Transactions submitted direct to the leader TPU  | `--chart-2`  |

Both are plotted on one area chart with a fixed `max` of 100, so the
vertical scale is always the full 0–100% range. The two series are the
comparison the screen exists for: BDN path versus direct TPU path over
the same window.

**Land latency** is reported in milliseconds as three percentile series
under `latency`: `p50`, `p95`, and `p99`. They are drawn on a shared
auto-scaled axis, with `p50` filled and `p95` / `p99` as lines, so the
spread between the median and the tail is the thing you read. The
percentiles are computed per bucket, not across the whole window — each
point is the percentile of the landings in that bucket.

There is no fixed target or threshold attached to land rate or latency
at the aggregate level. The only banded values on this screen are the
per-leader land percentages and the per-track feed freshness comparison.

## Fan-out volume and the dedup hit ratio

`fanout` is a single series sourced from the `bdn.tx_fanout` counter,
expressed as **transactions per second across all edges**. It is drawn as
bars. The value in the card header is the peak of the loaded series,
`Math.max(...an.fanout)` — the highest single bucket in the 24 hours, not
an average.

`dedup` describes duplicate suppression:

| Field             | Meaning                                                         |
| ----------------- | --------------------------------------------------------------- |
| `dedup.hitRatio`  | Percent of observed transactions suppressed as duplicates       |
| `dedup.saved`     | Count of transactions not re-broadcast because of a dedup hit   |

The donut shows `hitRatio` against its complement (`100 - hitRatio`), so
it is a share-of-total reading, not a gauge against a target.

Dedup keys on the **first 64 bytes** of the transaction — the Solana
signature. Two submissions of the same signature, however they arrive
and from whichever edge, collapse to one fan-out. That is why a healthy
multi-submit strategy shows a non-trivial hit ratio rather than zero: it
means the BDN absorbed your retries instead of pushing them onto the
leader.

## Per-leader and per-region attribution

`byLeader` is a list of per-validator rows. Each entry carries:

| Field    | Rendered as                                                     |
| -------- | --------------------------------------------------------------- |
| `name`   | Leader identity, monospaced                                     |
| `region` | Region code, with a swatch coloured by `regionColor(region)`    |
| `land`   | Land success percent for that leader                            |
| `p95`    | 95th-percentile land latency for that leader, in milliseconds   |
| `share`  | That leader's share of the total, drawn as a meter bar           |

The **land percent colour bands** are applied in the table and are the
only per-row health signal on the screen:

| Condition       | Colour           | Read it as |
| --------------- | ---------------- | ---------- |
| `land > 94`     | `--success`      | Good       |
| `89 < land ≤ 94`| `--warning`      | Watch      |
| `land ≤ 89`     | `--destructive`  | Bad        |

Leader rows only exist after something has landed. With no landings the
table renders a single explanatory row — leader stats appear after the
first landed transaction — rather than an empty grid.

`byRegion` is the same data rolled up by region. Each entry has
`region`, `landed` (absolute count of landings) and `share` (percent of
total landings). The meters are drawn against a `max` of 26, so a region
at 26% or above fills the bar; the numeric `share` next to it is the
authoritative value. Region swatch colours match the per-leader table
because both call `regionColor`, so you can trace a hot region down to
the leaders inside it.

## Feed freshness targets and breach handling

`feedSlo` is a per-track list comparing observed feed latency against
that track's target. Each entry has:

| Field     | Meaning                                          |
| --------- | ------------------------------------------------ |
| `track`   | Feed track name                                  |
| `actual`  | Observed latency, milliseconds                   |
| `target`  | Target latency for that track, milliseconds      |
| `ok`      | Boolean — whether the track is within target      |

The row renders `actual / target` in milliseconds followed by a pill:
`in SLO` when `ok` is true, `breach` when it is false. The targets are
per-track and come from the procedure — the screen does not hardcode a
number, and neither should your alerting. Read `target` from the response
if you mirror this comparison elsewhere.

`ok` is the authoritative breach signal. Do not recompute it by
comparing `actual` to `target` yourself; the procedure owns the
comparison, and the pill follows `ok` exactly.

With no samples, the card explains that the SLO fills in once the
private feed is subscribed. An unsubscribed feed produces no rows, which
is distinct from a feed that is subscribed and breaching.

## The CSV export format

**Export CSV** in the page header serialises the series that are already
loaded in the browser — it does not re-query. The file is named
`bdn-analytics-24h.csv`.

The row count is the **longest** of the exported series:

```
n = max(
  landRate.bdn.length, landRate.tpu.length,
  latency.p50.length, latency.p95.length, latency.p99.length,
  fanout.length,
)
```

One row per bucket, with these columns in order:

| Column        | Source            |
| ------------- | ----------------- |
| `bucket`      | Zero-based index; `0` is the oldest bucket |
| `land_bdn`    | `landRate.bdn[i]` |
| `land_tpu`    | `landRate.tpu[i]` |
| `latency_p50` | `latency.p50[i]`  |
| `latency_p95` | `latency.p95[i]`  |
| `latency_p99` | `latency.p99[i]`  |
| `fanout`      | `fanout[i]`       |

Where a series is shorter than `n`, the cell is written **empty** rather
than `0`. An empty cell means "no sample in this bucket for this
series", and treating it as a zero will skew any average you compute
downstream. If every series is empty you get a header-only file — the
export reports the absence of data honestly instead of fabricating rows.

Note what is *not* in the export: the dedup ratio, the per-leader table,
the per-region breakdown and the feed SLO rows are screen-only. Only the
three time-bucketed families (land rate, latency percentiles, fan-out)
are exported, because only they share the bucket index that the CSV is
keyed on.

## Related

- [Telemetry](/platform/telemetry) — the metric and trace streams the gateway emits
- [Telephony Metrics](/glossary/metrics) — percentile and rate definitions in general
