Skip to content

[10/10] Warn when sample timeline is truncated#57

Open
oliver-kriska wants to merge 11 commits into
appsignal:mainfrom
oliver-kriska:digest-truncation-note
Open

[10/10] Warn when sample timeline is truncated#57
oliver-kriska wants to merge 11 commits into
appsignal:mainfrom
oliver-kriska:digest-truncation-note

Conversation

@oliver-kriska

Copy link
Copy Markdown

🤖 AI-generated, shared for review. This PR was written by Claude Code (AI). Before opening it I ran the project's gates locally — cargo +1.94.0 fmt --check, clippy -D warnings, and the full test suite — all green — and kept the change strictly additive (existing commands, output, and behaviour are untouched). Anything I couldn't confirm against the live API without a token is called out explicitly below. Feedback very welcome.

What

The samples show performance digest now warns when AppSignal truncated the sample's timeline.

For very large samples the API drops timeline events and reports the dropped count in timelineTruncatedEvents. The digest builds its per-group breakdown and slowest-events list purely from the returned timeline, so a truncated sample silently understated those numbers with no signal to the user.

Change

  • Performance section prints ⚠ N timeline event(s) truncated by the API — the breakdown and slowest-events below are understated. when the timeline was clipped.
  • --output json adds performance.truncated_events (the dropped count).
  • Samples whose timeline was not truncated are completely unaffected (additive).

Notes

  • The API also returns a pre-computed groupDurations rollup (already deserialized but unused). It could serve as an accurate fallback breakdown when the timeline is truncated — deliberately left out here because its value format/units aren't verified against the live API. Good follow-up once that's confirmed.
  • Unit-tested on synthetic samples (truncation surfaced; zero/absent does not flag).

📚 Part 10 of a 10-PR stack — builds on #56. Because cross-fork PRs target main, the diff here currently includes the ancestor commits; it shrinks to just this feature once #56 merges. Easiest to review in stack order (or review only the latest commit).

oliver-kriska and others added 11 commits June 18, 2026 17:27
Bring the AGENTS.md architecture section up to date with the current `src/`
tree (the `commands/logs/` split, `dashboards.rs`, `triggers.rs`, `about.rs`,
`project.rs`, `output.rs`, `error.rs`, `telemetry.rs`, `version_check.rs`,
`client_headers.rs`) and document two conventions that were implicit in the
code: how command output and user-facing errors flow through the `output`
module and `CliError`, and the end-to-end checklist for adding a new command.

Add a minimal CLAUDE.md that points at AGENTS.md as the source of truth, so
Claude Code sessions pick up the same conventions.

[skip changeset]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`incidents show` only exposes incident-level aggregates. Add a `samples`
command that fetches the underlying transaction sample data behind an
incident — action, duration, queue time, params, and the exception backtrace —
which is what most incident triage actually works from.

- `samples show` fetches a single sample: the latest, one by id (--sample-id),
  or the one closest to a timestamp (--at). Targeting a timestamp matters
  because the "latest" sample often hides the one that triggered the incident.
- `samples list` returns an incident's samples, optionally narrowed with
  --start/--end/--limit.
- Both accept a full AppSignal incident or sample URL (or a bare sample id) as
  a positional argument, or the explicit --incident plus the usual
  --app-id/--app/--environment/--org flags, and both support --output json.

A new `appsignal_url` module parses every URL shape, matching
`/samples/timestamp/<ISO>` before a generic sample-id segment and URL-decoding
the timestamp. The fetch uses one GraphQL query that spreads both
`... on PerformanceIncident` and `... on ExceptionIncident`, and the sample
type is taken from the returned `__typename` rather than inferred from the URL
path, so an exception URL can never be miscategorised as performance.

The `timestamp`/`start`/`end` GraphQL variables are declared `DateTime` even
though ISO-8601 strings are sent; declaring them `String` returns an HTTP 400
type mismatch. Regression tests assert the `DateTime` declaration is present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`samples show` printed a flat field dump; raw samples are large and mostly
noise. Render an analysed digest by default instead — the questions a responder
actually asks: what ran, how slow, who hit it, which queries dominated, was
there an N+1, and for errors what blew up and what led to it.

- New `sample_analysis` module distils a `Sample` into a `SampleAnalysis`:
  request overview + acting user (pulled from overview/attributes), a per-group
  performance breakdown (count / total / % / avg), the slowest events, a
  database rollup, slow queries (from timeline payload bodies), N+1 suspects
  (repeated timeline `digest`s, plus the `hasNPlusOne` flag), and for errors the
  exception, backtrace, causes, and breadcrumbs. It is pure and unit-tested on
  synthetic samples.
- `samples show` renders the digest by default, `--output json` adds the
  structured `analysis` object alongside the raw sample, and `--raw` prints the
  unprocessed sample. `samples list` is unchanged.
- The sample GraphQL selection gains the fields the analysis needs (timeline,
  group durations/allocations, params/session/custom data, breadcrumbs); only
  fields with known scalar types are selected to keep deserialization robust.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The forensic question is usually "what happened between T1 and T2" or "what did
this user hit", not "show me one incident". `samples list` previously required
an incident; it can now scan a window across incidents instead.

- Omitting `--incident` and passing `--start`/`--end` scans recent incidents and
  collects the samples in that window. The GraphQL `incidents` query has no
  time-range filter, so `scan_samples_in_window` lists recent incidents (capped
  by `--limit`, default 20) and applies the window at the sample level, where
  `samples(start:, end:)` is supported. Anomaly/log incidents have no samples
  and are skipped.
- `--user` keeps only samples whose user identity matches the given id, email,
  or substring (case-insensitive). Identity is resolved from the sample's
  overview/attributes and, failing that, its session/custom/params JSON, via a
  shared `sample_analysis::sample_user` helper that also feeds the digest.
- `--namespaces` filters which incidents are scanned. `--user` also applies in
  single-incident mode. Existing single-incident `samples list` is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `metrics` command for inspecting an app's metrics from the terminal,
served entirely by the public GraphQL API (no REST access needed):

- `metrics list` discovers metric keys (name, type, fields, tags), with
  `--name` fragment filtering and `--limit`.
- `metrics timeseries --metric <name>` fetches values over time, narrowed by
  `--field` and `--tag key=value`, windowed with `--timeframe` (e.g. R1H) or
  both `--start`/`--end`.
- `metrics history --start --end` reports per-action error and performance
  throughput over a window via the timeDetective*DataPoints fields, scoped by
  `--namespaces`.

The timeseries/history window variables are declared as DateTime / DateTime!
(declaring String returns HTTP 400); the timeframe enum is validated and
inlined as a literal rather than typed; response field names arrive lowercased.
All subcommands take the usual app-selection flags and support `--output json`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `performance` command for finding slow work, built on existing API
surface (no new GraphQL queries):

- `performance actions` lists recent performance incidents and ranks them
  client-side by mean duration (default), total duration, or throughput
  (`--sort`), scoped with `--namespaces`/`--action`/`--state`. The public API
  has no order-by-duration, so ranking happens over the scanned set (`--limit`).
- `performance queries` takes the slowest actions, fetches each one's latest
  sample, and reuses sample_analysis to surface the slow queries and N+1
  suspects behind them. The public performanceIncidents data is action-level,
  not per-SQL, so this view is derived from the latest sampled request per
  action rather than a full aggregate — the human and JSON output both say so.
  A missing sample is reported per-row, not fatal.

Adds Incident accessors namespace()/action_names()/mean()/total_duration() for
the performance-specific fields, and performance.actions/performance.queries
telemetry events. Existing `incidents list-performance` is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cache fetched transaction samples locally so they can be re-inspected offline:

- `samples show`/`samples list` write each fetched sample to a per-sample JSON
  file under the platform cache directory (dirs::cache_dir()/appsignal/samples/).
  Caching is best-effort (never fatal) and additive — it does not change their
  output. Disable per call with `--no-cache` or globally with APPSIGNAL_NO_CACHE.
- New `samples cache` subcommands read that cache without hitting the API:
  `list` (newest first, `--app-id`/`--limit`), `search <query>` (matches the
  query against each entry's serialized JSON, so it covers action, user, query
  bodies, params, and exceptions), `clear`, and `path`.

Adds the sample_cache module (store/load/search/clear, all unit-tested via a
tempdir) and samples.cache.* telemetry events. Samples can hold sensitive data,
so the docs flag the privacy implication and `cache clear` wipes it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Support authenticating with a personal API token, for CI and other
non-interactive environments where the OAuth browser flow is impractical:

- New global `--api-token` flag and `APPSIGNAL_API_TOKEN` environment variable.
  A token from either source takes precedence over stored OAuth credentials and
  is sent as a `?token=` query parameter (the documented personal-token auth for
  the public API). OAuth stays the default when no token is set.
- Adds AuthMethod::Token; the request builders append the token query param.
  The flag is recorded once at startup in a OnceLock so it reaches the single
  authenticated_client choke point without threading through every command, and
  config::api_token() resolves flag-then-env (pure resolve_api_token, unit-tested).
- Token auth is never persisted to disk — purely per-invocation. `auth status`
  and `about` now report which method is active, with the token masked.

Additive throughout: no existing auth behaviour or output changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a global `--verbose` / `-v` flag that dumps each outgoing GraphQL request
(URL, query, and variables) to stderr before it is sent, for debugging what the
CLI actually asks the API for. The dump goes to stderr only, so it never mixes
with `--output json` on stdout.

The flag is recorded once at startup (output::set_verbose, an AtomicBool) so
api::graphql() can trace via output::trace_graphql without threading state
through the client. The rendering core (trace_graphql_to) is unit-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The local sample cache already stores complete samples, but `cache list`/
`search` only showed summary rows — there was no way to re-view the full
investigator digest without re-fetching from the API. Add
`samples cache show <sample-id>`, which loads the cached sample and renders the
same analysed digest as `samples show` (or the unprocessed sample with `--raw`)
entirely offline. Scope with `--app-id`; `--output json` returns the analysis
plus the cached sample and its cached-at timestamp.

Adds sample_cache::find_by_id (unit-tested), samples::cache_show, the
SamplesCacheAction::Show variant, and the samples.cache.show telemetry event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AppSignal drops timeline events for very large samples and reports the
count in `timelineTruncatedEvents`. The performance digest derived its
breakdown and slowest-events lists purely from the returned timeline, so
a truncated sample silently understated those numbers with no indication
to the user.

The performance section now prints a warning when the timeline was
truncated, and the JSON analysis carries a `truncated_events` count on
`performance`. Samples whose timeline was not truncated are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@oliver-kriska oliver-kriska changed the title Warn when sample timeline is truncated [10/10] Warn when sample timeline is truncated Jun 19, 2026
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.

2 participants