[10/10] Warn when sample timeline is truncated#57
Open
oliver-kriska wants to merge 11 commits into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The
samples showperformance 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
⚠ N timeline event(s) truncated by the API — the breakdown and slowest-events below are understated.when the timeline was clipped.--output jsonaddsperformance.truncated_events(the dropped count).Notes
groupDurationsrollup (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.