Add a statistics pane to the webhook page (closes #368) #403

Open
clawbot wants to merge 1 commits from issue-368-webhook-stats into next
Collaborator

Adds a statistics pane at the top of the webhook page: entrypoints and targets (and how many are active), deliveries in progress, the last event's time, the retention period; events, deliveries and failures, lifetime and within retention; and events, failures and failure percentage over the last 10 minutes and 24 hours.

Each webhook's event database now keeps running totals: one row for its events, and one row per target for that target's deliveries, how many became delivered and how many failed, each with what retention removed. Storing an event, creating a delivery (fan-out or replay) and a delivery becoming delivered or failed each add to them in the same transaction as the row they count. The webhook's delivery figures are the target rows summed; #372 can read the same rows for the target list.

Deliveries gain a finished_at column, set when a delivery becomes delivered or failed. It and target_id end the existing status index, so each target's deliveries finished in a window come from one index-range query grouped by target. A window's failure percentage is the failed deliveries out of all that finished in it, and a dash when none did.

Retention deletes at most 1000 expired events per transaction, with their delivery results and deliveries and the totals update, committing between batches.

The pane is its own template, webhook_stats.html, included at the top of source_detail.html. Its figures are in tables, so columns stay aligned at any width.

An existing database must be recreated: the schema changes in place and nothing is back-filled.

Model: opus-5-5

Adds a statistics pane at the top of the webhook page: entrypoints and targets (and how many are active), deliveries in progress, the last event's time, the retention period; events, deliveries and failures, lifetime and within retention; and events, failures and failure percentage over the last 10 minutes and 24 hours. Each webhook's event database now keeps running totals: one row for its events, and one row per target for that target's deliveries, how many became delivered and how many failed, each with what retention removed. Storing an event, creating a delivery (fan-out or replay) and a delivery becoming delivered or failed each add to them in the same transaction as the row they count. The webhook's delivery figures are the target rows summed; https://git.eeqj.de/sneak/webhooker/issues/372 can read the same rows for the target list. Deliveries gain a `finished_at` column, set when a delivery becomes delivered or failed. It and `target_id` end the existing status index, so each target's deliveries finished in a window come from one index-range query grouped by target. A window's failure percentage is the failed deliveries out of all that finished in it, and a dash when none did. Retention deletes at most 1000 expired events per transaction, with their delivery results and deliveries and the totals update, committing between batches. The pane is its own template, `webhook_stats.html`, included at the top of `source_detail.html`. Its figures are in tables, so columns stay aligned at any width. An existing database must be recreated: the schema changes in place and nothing is back-filled. Model: opus-5-5
clawbot added the needs-review label 2026-10-01 22:09:39 +02:00
clawbot self-assigned this 2026-10-01 22:09:39 +02:00
Author
Collaborator

Review: changes needed.

  1. The per-target counting required by #368 (comment) is not built. internal/database/model_totals.go:18: Totals is one row per webhook, with no target and no count of delivered deliveries, and the status index (internal/database/model_delivery.go:56) has no target column. As it stands, #372 would need a second mechanism, which that comment rules out. Acceptable: delivery totals kept per target, both delivered and failed (for example one totals row per target, with the webhook's figures summed from those rows), each still written in the same transaction as the rows it counts. Each target's deliveries that finished in a window should come from an index range, in one query grouped by target.

  2. The retention sweep's single transaction stalls the receiver. internal/database/retention.go:274 (reapExpired) deletes every expired row in one transaction. A prune of a few hundred thousand events holds the event database's write lock for longer than the 10-second busy timeout. Inbound webhooks that arrive meanwhile wait, then get a 500. A prune that size happens whenever retention is shortened on a busy webhook. Acceptable: delete in bounded batches, a fixed number of expired events per transaction. Each batch deletes those events' delivery results and deliveries and updates the totals in that same transaction, and the sweep commits between batches. Add a test that a prune larger than one batch removes every expired row and leaves the totals right.

  3. Nothing tests the retention period in the pane, which #368 (comment) requires. internal/handlers/webhook_stats_test.go:275 checks other text only. The existing "Retention: N days" assertions match the line at the foot of the page, so deleting the retention entry at templates/webhook_stats.html:28 leaves every test passing. Acceptable: a test that the rendered statistics pane itself shows the retention period, for a finite webhook and a forever one.

  4. At phone width the figure columns do not line up. templates/webhook_stats.html:42 and the other figure cells: each w-32 cell shrinks by a different amount depending on its row's label. At 375 px the numbers drift off-centre from the "Lifetime", "Within retention", "Last 10 minutes" and "Last 24 hours" headings and from each other. Acceptable: figure cells that keep their width (for example shrink-0), or a table, so each column stays aligned at any width.

  5. README.md:1766 says AutoMigrate creates the listed indexes "on a fresh and on an existing database". That is no longer true of the changed deliveries status index: on an existing database, AutoMigrate keeps the old two-column index of the same name. Acceptable: drop "and on an existing database". No upgrade handling is needed, per #368 (comment).

  • Judgement call: the retention period sits in the pane's top row, one line above the within-retention column. I take that as meeting "next to the within-retention figures".

Model: opus-5-5

Review: changes needed. 1. The per-target counting required by https://git.eeqj.de/sneak/webhooker/issues/368#issuecomment-108111 is not built. `internal/database/model_totals.go:18`: `Totals` is one row per webhook, with no target and no count of delivered deliveries, and the status index (`internal/database/model_delivery.go:56`) has no target column. As it stands, https://git.eeqj.de/sneak/webhooker/issues/372 would need a second mechanism, which that comment rules out. Acceptable: delivery totals kept per target, both delivered and failed (for example one totals row per target, with the webhook's figures summed from those rows), each still written in the same transaction as the rows it counts. Each target's deliveries that finished in a window should come from an index range, in one query grouped by target. 2. The retention sweep's single transaction stalls the receiver. `internal/database/retention.go:274` (`reapExpired`) deletes every expired row in one transaction. A prune of a few hundred thousand events holds the event database's write lock for longer than the 10-second busy timeout. Inbound webhooks that arrive meanwhile wait, then get a 500. A prune that size happens whenever retention is shortened on a busy webhook. Acceptable: delete in bounded batches, a fixed number of expired events per transaction. Each batch deletes those events' delivery results and deliveries and updates the totals in that same transaction, and the sweep commits between batches. Add a test that a prune larger than one batch removes every expired row and leaves the totals right. 3. Nothing tests the retention period in the pane, which https://git.eeqj.de/sneak/webhooker/issues/368#issuecomment-108272 requires. `internal/handlers/webhook_stats_test.go:275` checks other text only. The existing "Retention: N days" assertions match the line at the foot of the page, so deleting the retention entry at `templates/webhook_stats.html:28` leaves every test passing. Acceptable: a test that the rendered statistics pane itself shows the retention period, for a finite webhook and a forever one. 4. At phone width the figure columns do not line up. `templates/webhook_stats.html:42` and the other figure cells: each `w-32` cell shrinks by a different amount depending on its row's label. At 375 px the numbers drift off-centre from the "Lifetime", "Within retention", "Last 10 minutes" and "Last 24 hours" headings and from each other. Acceptable: figure cells that keep their width (for example `shrink-0`), or a table, so each column stays aligned at any width. 5. `README.md:1766` says `AutoMigrate` creates the listed indexes "on a fresh and on an existing database". That is no longer true of the changed `deliveries` status index: on an existing database, `AutoMigrate` keeps the old two-column index of the same name. Acceptable: drop "and on an existing database". No upgrade handling is needed, per https://git.eeqj.de/sneak/webhooker/issues/368#issuecomment-108055. - Judgement call: the retention period sits in the pane's top row, one line above the within-retention column. I take that as meeting "next to the within-retention figures". Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-01 22:45:00 +02:00
clawbot force-pushed issue-368-webhook-stats from 7287e163e7 to ff0018cf43 2026-10-01 23:05:18 +02:00 Compare
clawbot added 1 commit 2026-10-01 23:28:28 +02:00
Each webhook's event database keeps running totals: one row for its
events, and one row per target for that target's deliveries, delivered
and failed, each with what retention removed. Every write to them
shares the transaction of the rows it counts. Deliveries get a
finished_at column; it and target_id end the status index, so each
target's deliveries finished in a window come from one index-range
query grouped by target. Retention deletes 1000 expired events per
transaction. The pane is its own template, its figures in tables.

The schema changes in place with nothing back-filled, so an existing
database must be recreated.

Model: opus-5-5
clawbot force-pushed issue-368-webhook-stats from ff0018cf43 to 840c795c66 2026-10-01 23:28:28 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-01 23:47:35 +02:00
Author
Collaborator

Rework for the review above.

  1. Delivery totals are now one row per target (TargetTotals: deliveries, delivered, failed, and what retention removed of deliveries and failures), plus one row for the events (EventTotals), each written in the transaction of the rows it counts; the webhook's figures are the target rows summed. target_id now ends the status index, and each target's deliveries finished in a window come from one covering index-range query grouped by target, which the pane sums.
  2. Retention deletes at most 1000 expired events per transaction, each batch with their delivery results, deliveries and the totals update, committing between batches. A new test prunes one batch plus one and checks the rows left and every total.
  3. A new test takes the statistics pane out of the rendered page and checks it shows "30 days" for a finite webhook and "forever" for a forever one.
  4. The figures are two tables, so each column stays aligned at any width. shrink-0 is not in the committed stylesheet, and two 128 px columns do not fit at 375 px.
  5. The README now says AutoMigrate creates those indexes on a fresh database.
  • Judgement call: retention selects expired events without the soft-delete condition, through the created_at index, so each batch deletes exactly the events it selected; the README index table is updated to match.
  • Unverified: that a writer waiting on the busy timeout gets the write lock between batches, which run back to back with no pause.

Model: opus-5-5

Rework for the review above. 1. Delivery totals are now one row per target (`TargetTotals`: deliveries, delivered, failed, and what retention removed of deliveries and failures), plus one row for the events (`EventTotals`), each written in the transaction of the rows it counts; the webhook's figures are the target rows summed. `target_id` now ends the status index, and each target's deliveries finished in a window come from one covering index-range query grouped by target, which the pane sums. 2. Retention deletes at most 1000 expired events per transaction, each batch with their delivery results, deliveries and the totals update, committing between batches. A new test prunes one batch plus one and checks the rows left and every total. 3. A new test takes the statistics pane out of the rendered page and checks it shows "30 days" for a finite webhook and "forever" for a forever one. 4. The figures are two tables, so each column stays aligned at any width. `shrink-0` is not in the committed stylesheet, and two 128 px columns do not fit at 375 px. 5. The README now says `AutoMigrate` creates those indexes on a fresh database. - Judgement call: retention selects expired events without the soft-delete condition, through the `created_at` index, so each batch deletes exactly the events it selected; the README index table is updated to match. - Unverified: that a writer waiting on the busy timeout gets the write lock between batches, which run back to back with no pause. Model: opus-5-5
Some checks are pending
check / check (push) Waiting to run
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-368-webhook-stats:issue-368-webhook-stats
git checkout issue-368-webhook-stats
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#403