Swipes and a reader that will not scroll are reported (#384)

A swipe that only bounces sends no request and throws nothing, so
#383's stop at the 50th item left nothing in the log or the traces, and
the reader that would not scroll in the same report was never found.

The page now reports each swipe in the reader (next, prev, list, end,
short; a tap is not one) with where the reader stood, under the tap's
trace, and scroll_stuck when a drag up or down with room to move the
reader left it where it was. That one is watched by where the finger
was, not by what it landed on, so a layer lying over the reader is
caught, and its state names what is under the finger, what is inert,
the dialog, the reader's transform, a #dpeek left behind, and how long
since the app came back. ipx logs swipe as info and scroll_stuck as a
warning, in a new free-text state field, and the dashboard counts both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 20:46:31 +00:00
parent 385589ff77
commit 5c1970e8ee
7 changed files with 94 additions and 7 deletions

View File

@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- The log has a line for each swipe in the page's reader, saying what came of it and where the reader stood, and a warning when a drag would not scroll the reader, with what lay over it (#384).
- `/metrics` says how long statements take and, for hand-written SQL, how long they waited for a connection (#367); counts feed failures by kind, new items, downloads saved and failed, and the silence DeadAir found (#368); and holds the page's own timings, a click to its list, play to sound, a stall (#369).
- Traces for what had none: each command the worker runs, from the CLI, the socket or the page, with its job inside (#373); the daemon's start, from opening the database to serving (#374); a list's sync and an OPML upload, step by step (#372); and the tap or key in the page that set a trace's requests off, as its root span, from the tap to its last answer (#376).

View File

@@ -1413,8 +1413,11 @@ mod tests {
trace: None,
span: None,
start: None,
state: Some("3/50 of 120".into()),
};
for ev in ["page_error", "rejection", "player_error", "play_start", "stall", "list_shown"] {
for ev in
["page_error", "rejection", "player_error", "play_start", "stall", "list_shown", "swipe", "scroll_stuck"]
{
assert_eq!(client_event(u.clone(), Json(e(ev))).await, StatusCode::NO_CONTENT, "{ev}");
}
// A tap's span takes a trace's ids and a time the server can believe (#376).
@@ -2783,12 +2786,14 @@ struct ClientEvent {
trace: Option<String>,
span: Option<String>,
start: Option<u64>,
/// For `swipe` and `scroll_stuck` (#384): where the reader stood, and what could hold it.
state: Option<String>,
}
async fn client_event(user: crate::db::User, Json(e): Json<ClientEvent>) -> StatusCode {
// From the browser: only the kinds the page sends, and nothing long enough to fill the log.
let cut = |s: &Option<String>| s.as_deref().map(|s| s.chars().take(500).collect::<String>());
let (msg, src, feed, filter) = (cut(&e.msg), cut(&e.src), cut(&e.feed), cut(&e.filter));
let (msg, src, feed, filter, state) = (cut(&e.msg), cut(&e.src), cut(&e.feed), cut(&e.filter), cut(&e.state));
let (ev, line, enclosure, deadair, ms, user) =
(e.ev.as_str(), e.line, e.enclosure, e.deadair, e.ms, user.name.as_str());
match ev {
@@ -2815,6 +2820,17 @@ async fn client_event(user: crate::db::User, Json(e): Json<ClientEvent>) -> Stat
target: "ipx::client", ev, ms, feed, filter, user,
"{user}'s page: list_shown {} ms", ms.unwrap_or(0)
),
// A swipe in the reader and what came of it, so one that did nothing is seen (#384): a
// swipe sends no request when it only bounces, and #383's stopped at the 50th unseen.
"swipe" => tracing::info!(
target: "ipx::client", ev, msg, ms, feed, filter, state, user,
"{user}'s page: swipe {} at {}", msg.as_deref().unwrap_or(""), state.as_deref().unwrap_or("")
),
// A drag up or down on an item with room to scroll that did not move it (#384).
"scroll_stuck" => tracing::warn!(
target: "ipx::client", ev, feed, state, user,
"{user}'s page: scroll_stuck: {}", state.as_deref().unwrap_or("")
),
_ => return StatusCode::BAD_REQUEST,
}
// The timings in /metrics as well, so weeks of them are percentiles without reading each line