# `PhoenixDevToolbar.Report`
[🔗](https://github.com/zoten/phoenix_dev_toolbar/blob/0.0.2/lib/phoenix_dev_toolbar/report.ex#L1)

Reads what the toolbar captured, without a browser: from IEx, a remote shell,
or an agent that evaluates code in the running app (such as Tidewave's
`project_eval`). See [ADR 13](https://github.com/zoten/phoenix_dev_toolbar/blob/main/adr/13-report-read-api.md).

    alias PhoenixDevToolbar.Report

    Report.latest(path: "/posts") |> Report.summary()
    #=> %{queries: %{count: 23, failed: 0, total_us: 4180, slowest: [...]},
    #     n_plus_one: [%{sql: "SELECT ... WHERE post_id = ?", count: 20, ...}], ...}

## Sessions and targets

A session is one page view: a dead render request (`kind: :request`) or a
LiveView process (`kind: :live_view`). `sessions/1` lists them newest first,
`latest/1` returns the newest that matches.

The reading functions take a *target*: a sid, a session from `sessions/1`, or
a list of sids. A request's target includes the LiveView sessions that joined
from it (its connected mount), so the dead render and the connected mount read
as one page; pass `children: false` to read the request alone. A LiveView
reached by live navigation is a session of its own.

## What you get

Plain maps with atom keys, described by the types below. Durations are in
microseconds, `at` is a `DateTime`. Values are already truncated and redacted
when captured: you read what the panel shows. New keys may be added; renaming
or removing one is a breaking change noted in the CHANGELOG.

## Limits

Only what the Store still holds is readable: events are capped per session
and expire after the TTL (10 minutes by default, `limits:` in the config). A
session whose events were dropped or trimmed says so (`dropped`, `trimmed`).
While the toolbar is disabled every function returns `[]` or `nil`.

Every read runs in the caller's process and copies at most a few sessions'
capped events: nothing runs in the host's request or LiveView processes.
EXPLAIN and the process actions are not part of this API.

# `callback`

```elixir
@type callback() :: %{
  sid: sid(),
  seq: pos_integer(),
  span: non_neg_integer() | nil,
  at: DateTime.t(),
  duration_us: non_neg_integer() | nil,
  callback: atom(),
  view: module() | nil,
  component: module() | nil,
  id: String.t() | nil,
  event: String.t() | nil,
  params: String.t() | nil,
  changes: PhoenixDevToolbar.Truncate.changes() | nil,
  queries: non_neg_integer(),
  query_us: non_neg_integer(),
  error: String.t() | nil
}
```

One LiveView or LiveComponent callback (`:mount`, `:handle_params`,
`:handle_event`, `:handle_info`, `:update`, ...). `queries` and `query_us`
count the queries run in it, its tasks included. `changes` is the assigns
diff it produced (see `t:PhoenixDevToolbar.Truncate.changes/0`).

# `error`

```elixir
@type error() :: %{
  sid: sid(),
  seq: pos_integer(),
  at: DateTime.t(),
  source: :request | :live_view | :ecto,
  message: String.t(),
  stacktrace: [String.t()]
}
```

A failure: a request exception, a failed callback or render, a failed query.

# `n_plus_one`

```elixir
@type n_plus_one() :: %{
  sql: String.t() | nil,
  origin: String.t() | nil,
  count: pos_integer(),
  callbacks: pos_integer(),
  sids: [sid()]
}
```

A repeated query: the same SQL from the same origin run `count` times in one
callback, in `callbacks` callbacks so far.

# `query`

```elixir
@type query() :: %{
  sid: sid(),
  seq: pos_integer(),
  span: non_neg_integer() | nil,
  at: DateTime.t(),
  duration_us: non_neg_integer() | nil,
  repo: String.t() | nil,
  sql: String.t() | nil,
  params: String.t() | nil,
  source: String.t() | nil,
  rows: non_neg_integer() | nil,
  error: String.t() | nil,
  times: %{required(atom()) =&gt; non_neg_integer()},
  origin: String.t() | nil,
  stacktrace: [String.t()],
  n_plus_one?: boolean()
}
```

One Ecto query. `rows` is set on success, `error` on failure. `times` holds
Ecto's measurements (`:query_time`, `:queue_time`, `:decode_time`, ...) in
microseconds. `origin` is the first stacktrace frame outside Ecto (nil unless
the repo has `stacktrace: true`). `span` is the callback the query ran in:
see `callbacks/2`. `n_plus_one?` marks the query that reached the N+1
threshold and every repetition after it, as the Ecto tab does.

# `request`

```elixir
@type request() :: %{
  method: String.t() | nil,
  path: String.t() | nil,
  route: String.t() | nil,
  handler: String.t() | nil,
  status: non_neg_integer() | nil,
  duration_us: non_neg_integer() | nil
}
```

The dead render request: its route and response.

# `session`

```elixir
@type session() :: %{
  sid: sid(),
  kind: :request | :live_view,
  parent: sid() | nil,
  view: module() | nil,
  path: String.t() | nil,
  owner: pid() | nil,
  started_at: DateTime.t(),
  events: non_neg_integer(),
  dropped: non_neg_integer(),
  trimmed: non_neg_integer()
}
```

One page view. `owner` is the LiveView process (nil for a request), `parent`
the session it came from (the request for a connected mount, the previous
LiveView after live navigation). `events` is the number stored now.

# `sid`

```elixir
@type sid() :: String.t()
```

A session id.

# `summary`

```elixir
@type summary() :: %{
  sids: [sid()],
  session: session(),
  request: request() | nil,
  queries: %{
    count: non_neg_integer(),
    failed: non_neg_integer(),
    total_us: non_neg_integer(),
    slowest: [query()]
  },
  n_plus_one: [n_plus_one()],
  callbacks: %{
    count: non_neg_integer(),
    total_us: non_neg_integer(),
    slowest: [callback()]
  },
  renders: %{count: non_neg_integer(), total_us: non_neg_integer()},
  errors: [error()],
  dropped: non_neg_integer(),
  trimmed: non_neg_integer()
}
```

A page at a glance. `sids` are the sessions read; `session` is the first.
Each `slowest` list holds at most 5 items, slowest first.
`dropped` and `trimmed` add up the events lost to the caps.

# `target`

```elixir
@type target() :: sid() | session() | [sid()]
```

A sid, a session from `sessions/1`, or a list of sids.

# `callbacks`

```elixir
@spec callbacks(target(), keyword()) :: [callback()]
```

Every LiveView and LiveComponent callback of `target`, oldest first. Option: `children:`.

# `errors`

```elixir
@spec errors(target(), keyword()) :: [error()]
```

Every failure of `target`, oldest first. Option: `children:`.

# `latest`

```elixir
@spec latest(keyword()) :: session() | nil
```

The newest session matching `sessions/1` options, or nil.

# `n_plus_one`

```elixir
@spec n_plus_one(target(), keyword()) :: [n_plus_one()]
```

The repeated queries of `target`, most recent first.

Options: `children:`, `threshold:` (repetitions in one callback, at least 2,
or `false`). The threshold defaults to the Ecto plugin's `n_plus_one:` option.

# `queries`

```elixir
@spec queries(target(), keyword()) :: [query()]
```

Every query of `target`, oldest first.
Options: `children:`, `threshold:` (see `n_plus_one/2`).

# `sessions`

```elixir
@spec sessions(keyword()) :: [session()]
```

Lists the sessions the Store holds, newest first.

Options: `:path` (exact string or a `Regex`), `:view` (module), `:kind`
(`:request` or `:live_view`), `:limit`.

# `summary`

```elixir
@spec summary(target(), keyword()) :: summary() | nil
```

A page at a glance, or nil when no session of `target` is left.
Options: `children:` (see the module docs), `threshold:` (see `n_plus_one/2`).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
