Event log: show only the events with a failed or a pending delivery (closes #390)
check / check (push) Successful in 3m41s
check / check (push) Successful in 3m41s
The event log gains three links above the list: All, Failed (N) and Pending (N), carried in a `show` query parameter; any other value shows every event. Failed lists the events with a failed delivery, Pending those with one pending or retrying, each once, under the full list's 50-row limit and order. A filtered list finds the matching deliveries through `idx_deliveries_status`, looks up their events by ID and sorts them, and reads the rows of only the events shown; its counts read the deliveries alone. The line beside the heading says what a filter counts, and Replay returns to the list it was pressed in. The README and the comments naming the query parameters the service reads include `show`. Model: opus-5-5
This commit is contained in:
@@ -149,6 +149,62 @@ func TestEventTierQueriesUseTheirIndexes(t *testing.T) {
|
||||
Delete(&database.Event{}), "sqlite_autoindex_events_1 (id=?)")
|
||||
}
|
||||
|
||||
// TestEventLogFiltersUseTheStatusIndex does the same for the event log's
|
||||
// Failed and Pending lists, of the newest events with a delivery in
|
||||
// given statuses, and for their counts (eventsWithStatus and
|
||||
// countEventsWithStatus in the handlers). The lists must also reach
|
||||
// the events table only by ID: from the matching deliveries, then from
|
||||
// the newest of those events.
|
||||
func TestEventLogFiltersUseTheStatusIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr, lc := setupTestWebhookDBManager(t)
|
||||
ctx := context.Background()
|
||||
require.NoError(t, lc.Start(ctx))
|
||||
|
||||
defer func() { require.NoError(t, lc.Stop(ctx)) }()
|
||||
|
||||
webhookID := uuid.New().String()
|
||||
|
||||
db, err := mgr.GetDB(webhookID)
|
||||
require.NoError(t, err)
|
||||
|
||||
dry := db.Session(&gorm.Session{DryRun: true})
|
||||
byStatus := "idx_deliveries_status (status=? AND deleted_at=?)"
|
||||
pending := []database.DeliveryStatus{
|
||||
database.DeliveryStatusPending,
|
||||
database.DeliveryStatusRetrying,
|
||||
}
|
||||
|
||||
var (
|
||||
rows []struct{ ID string }
|
||||
count int64
|
||||
)
|
||||
|
||||
matching := dry.Model(&database.Delivery{}).
|
||||
Distinct("event_id").Where("status IN ?", pending)
|
||||
newest := dry.Table("(?) AS matching", matching).
|
||||
Joins("CROSS JOIN events ON events.id = matching.event_id").
|
||||
Where(
|
||||
"events.webhook_id = ? AND events.deleted_at IS NULL",
|
||||
webhookID,
|
||||
).
|
||||
Order("events.created_at DESC").Limit(50).
|
||||
Select("events.id AS event_id")
|
||||
|
||||
// Each step of the plan is printed in braces, so these name the
|
||||
// lookup that follows each scan.
|
||||
byID := "{SEARCH events USING INDEX sqlite_autoindex_events_1 (id=?)}"
|
||||
|
||||
assertPlanUses(t, db, dry.Table("(?) AS newest", newest).
|
||||
Joins("CROSS JOIN events ON events.id = newest.event_id").
|
||||
Select("id").Order("created_at DESC").Limit(50).Find(&rows),
|
||||
byStatus, "{SCAN matching} "+byID, "{SCAN newest} "+byID)
|
||||
assertPlanUses(t, db, dry.Model(&database.Delivery{}).
|
||||
Distinct("event_id").Where("status IN ?", pending).Count(&count),
|
||||
byStatus)
|
||||
}
|
||||
|
||||
// TestStatisticsQueriesUseTheirIndexes does the same for the webhook
|
||||
// page's statistics (readEventStats in the handlers): deliveries in
|
||||
// progress, each target's deliveries finished since a time, which must
|
||||
|
||||
@@ -329,15 +329,25 @@ func replayBody(body string) *string {
|
||||
}
|
||||
|
||||
// redirectToEventLog redirects a replay or resubmit back to the event
|
||||
// log it was triggered from, carrying the outcome as its notice.
|
||||
// log it was triggered from, carrying the outcome as its notice. A
|
||||
// Replay form carries the list it was pressed in as show, so a replay
|
||||
// returns to the Failed or Pending list; a Resubmit form carries none,
|
||||
// so a resubmit returns to the full log, where its new event is the
|
||||
// newest.
|
||||
func redirectToEventLog(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
webhook database.Webhook,
|
||||
code noticeCode,
|
||||
) {
|
||||
http.Redirect(
|
||||
w, r, withNotice("/hook/"+webhook.ID+"/events", code),
|
||||
http.StatusSeeOther,
|
||||
location := withNotice("/hook/"+webhook.ID+"/events", code)
|
||||
|
||||
show := r.PostFormValue(showParam)
|
||||
if eventLogStatuses(show) != nil {
|
||||
location += "&" + showParam + "=" + show
|
||||
}
|
||||
|
||||
http.Redirect( //nolint:gosec // show is checked by eventLogStatuses
|
||||
w, r, location, http.StatusSeeOther,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -471,6 +472,66 @@ func TestHandleDeliveryReplay_RefusesWhileEarlierReplayInFlight(
|
||||
)
|
||||
}
|
||||
|
||||
// TestHandleDeliveryReplay_ReturnsToTheListItWasPressedIn proves a
|
||||
// Replay pressed in the Failed list carries that list in its form and
|
||||
// returns to it, and that a show value the event log does not know
|
||||
// returns to the full log.
|
||||
func TestHandleDeliveryReplay_ReturnsToTheListItWasPressedIn(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
h *handlers.Handlers
|
||||
sess *session.Session
|
||||
db *database.Database
|
||||
dbMgr *database.WebhookDBManager
|
||||
)
|
||||
|
||||
app := newTestApp(t, &h, &sess, &db, &dbMgr)
|
||||
app.RequireStart()
|
||||
|
||||
t.Cleanup(app.RequireStop)
|
||||
|
||||
wh := seedWebhook(t, db)
|
||||
tgt := seedConfiguredTarget(
|
||||
t, db, wh.ID, database.TargetTypeHTTP,
|
||||
`{"url":"`+replayTargetURL+`"}`,
|
||||
)
|
||||
|
||||
_, original := seedFailedDelivery(t, dbMgr, wh.ID, tgt.ID)
|
||||
|
||||
assert.Contains(t, renderSourceLogsPageWithQuery(
|
||||
t, h, sess, wh.ID, "?show=failed",
|
||||
), `name="show" value="failed"`)
|
||||
|
||||
// The second replay is refused, as the first is still queued.
|
||||
for _, tc := range []struct{ show, location string }{
|
||||
{"failed", "/hook/" + wh.ID +
|
||||
"/events?notice=replay-queued&show=failed"},
|
||||
{"made-up", "/hook/" + wh.ID + "/events?notice=replay-in-flight"},
|
||||
} {
|
||||
req := postRequest(
|
||||
"/hook/"+wh.ID+"/deliveries/"+original.ID+"/replay",
|
||||
authenticatedCookies(
|
||||
t, sess, deleteTestUserID, deleteTestUsername,
|
||||
),
|
||||
map[string]string{
|
||||
paramSourceID: wh.ID,
|
||||
paramDeliveryID: original.ID,
|
||||
},
|
||||
)
|
||||
req.Body = io.NopCloser(strings.NewReader("show=" + tc.show))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
h.HandleDeliveryReplay().ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusSeeOther, w.Code, tc.show)
|
||||
assert.Equal(t, tc.location, w.Header().Get("Location"), tc.show)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleSourceLogs_RendersReplayControlAndBanner proves the action
|
||||
// reaches the page it belongs on: a finished delivery renders a POST
|
||||
// form carrying a CSRF token, and the outcome code a refusal redirects
|
||||
|
||||
@@ -75,7 +75,7 @@ func (s *Handlers) LoadEventLogViewsForTest(
|
||||
webhook database.Webhook,
|
||||
) []EventLogView {
|
||||
views, _, _ := s.loadEventsWithDeliveries(
|
||||
w, newRequestForTest(), webhook, nil,
|
||||
w, newRequestForTest(), webhook, nil, nil,
|
||||
)
|
||||
|
||||
return views
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -186,6 +187,152 @@ func TestHandleSourceLogs_ShowsFiftyNewestEvents(t *testing.T) {
|
||||
assert.Contains(t, body, "50 most recent of 51 events")
|
||||
}
|
||||
|
||||
// TestHandleSourceLogs_ShowsEventsByDeliveryStatus proves that the
|
||||
// Failed list holds exactly the events with a failed delivery, the
|
||||
// Pending list exactly those with a delivery pending or retrying, each
|
||||
// once, and any other show value every event; that each link, and the
|
||||
// line beside the heading, counts the events its list holds; and that
|
||||
// the shown link is marked.
|
||||
func TestHandleSourceLogs_ShowsEventsByDeliveryStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newRecentEventsFixture(t)
|
||||
target := seedTarget(t, f.db, f.webhook.ID, database.TargetTypeLog)
|
||||
now := time.Now()
|
||||
|
||||
const (
|
||||
failed = database.DeliveryStatusFailed
|
||||
delivered = database.DeliveryStatusDelivered
|
||||
pending = database.DeliveryStatusPending
|
||||
retrying = database.DeliveryStatusRetrying
|
||||
)
|
||||
|
||||
// Each event is named by its content type. The first failed and
|
||||
// was then replayed and delivered. The second failed, and so did
|
||||
// its replay, and the fifth has one delivery pending and another
|
||||
// retrying: each must still be listed and counted once.
|
||||
events := []struct {
|
||||
contentType string
|
||||
deliveries []database.DeliveryStatus
|
||||
}{
|
||||
{"application/x-failed", []database.DeliveryStatus{failed, delivered}},
|
||||
{"application/x-failed-twice", []database.DeliveryStatus{failed, failed}},
|
||||
{"application/x-pending", []database.DeliveryStatus{pending}},
|
||||
{"application/x-retrying", []database.DeliveryStatus{retrying}},
|
||||
{"application/x-pending-retrying", []database.DeliveryStatus{pending, retrying}},
|
||||
{"application/x-delivered", []database.DeliveryStatus{delivered}},
|
||||
{"application/x-no-delivery", nil},
|
||||
}
|
||||
|
||||
all := make([]string, len(events))
|
||||
|
||||
for i, e := range events {
|
||||
event := f.event(
|
||||
t, e.contentType, "{}", now.Add(time.Duration(i)*time.Second),
|
||||
)
|
||||
|
||||
for _, status := range e.deliveries {
|
||||
f.delivery(t, event, target.ID, status)
|
||||
}
|
||||
|
||||
all[i] = e.contentType
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
query string
|
||||
current string
|
||||
heading string
|
||||
listed []string
|
||||
}{
|
||||
{"", "All", "7 total events", all},
|
||||
{"?show=failed", "Failed (2)", "2 events with a failed delivery",
|
||||
[]string{"application/x-failed", "application/x-failed-twice"}},
|
||||
{"?show=pending", "Pending (3)",
|
||||
"3 events with a delivery pending or retrying", []string{
|
||||
"application/x-pending", "application/x-retrying",
|
||||
"application/x-pending-retrying",
|
||||
}},
|
||||
{"?show=unknown", "All", "7 total events", all},
|
||||
} {
|
||||
body := renderSourceLogsPageWithQuery(
|
||||
t, f.h, f.sess, f.webhook.ID, tc.query,
|
||||
)
|
||||
|
||||
// One row per listed event, so with each listed event shown
|
||||
// no event is listed twice.
|
||||
assert.Equal(t, len(tc.listed),
|
||||
strings.Count(body, `role="button"`), tc.query)
|
||||
|
||||
for _, contentType := range all {
|
||||
assert.Equal(t,
|
||||
slices.Contains(tc.listed, contentType),
|
||||
strings.Contains(body, ">"+contentType+"<"),
|
||||
tc.query+" "+contentType)
|
||||
}
|
||||
|
||||
assert.Contains(t, body, ">"+tc.heading+"<", tc.query)
|
||||
assert.Contains(t, body, "Failed (2)", tc.query)
|
||||
assert.Contains(t, body, "Pending (3)", tc.query)
|
||||
assert.Equal(t, 1, strings.Count(body, "aria-current"), tc.query)
|
||||
assert.Contains(t, body,
|
||||
`aria-current="page">`+tc.current+"</a>", tc.query)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleSourceLogs_FilteredListShowsFiftyNewest proves a filtered
|
||||
// list holds the 50 newest matching events, as the full log does,
|
||||
// while its link and heading count every matching event.
|
||||
func TestHandleSourceLogs_FilteredListShowsFiftyNewest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newRecentEventsFixture(t)
|
||||
target := seedTarget(t, f.db, f.webhook.ID, database.TargetTypeLog)
|
||||
base := time.Now().Add(-time.Hour)
|
||||
|
||||
for i := range 51 {
|
||||
event := f.event(
|
||||
t, fmt.Sprintf("application/x-failed-%02d", i), "{}",
|
||||
base.Add(time.Duration(i)*time.Second),
|
||||
)
|
||||
f.delivery(t, event, target.ID, database.DeliveryStatusFailed)
|
||||
}
|
||||
|
||||
// The newest event has no failed delivery.
|
||||
f.event(t, "application/x-no-delivery", "{}", time.Now())
|
||||
|
||||
body := renderSourceLogsPageWithQuery(
|
||||
t, f.h, f.sess, f.webhook.ID, "?show=failed",
|
||||
)
|
||||
|
||||
assert.Equal(t, 50, strings.Count(body, `role="button"`))
|
||||
assert.NotContains(t, body, "application/x-failed-00")
|
||||
assert.NotContains(t, body, "application/x-no-delivery")
|
||||
assert.Contains(t, body, "Failed (51)")
|
||||
assert.Contains(t, body,
|
||||
"50 most recent of 51 events with a failed delivery")
|
||||
}
|
||||
|
||||
// TestHandleSourceLogs_EmptyFilteredList proves an empty filtered list
|
||||
// says that no event matches rather than that none was recorded.
|
||||
func TestHandleSourceLogs_EmptyFilteredList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newRecentEventsFixture(t)
|
||||
target := seedTarget(t, f.db, f.webhook.ID, database.TargetTypeLog)
|
||||
|
||||
f.delivery(
|
||||
t, f.event(t, contentTypeJSON, "{}", time.Now()),
|
||||
target.ID, database.DeliveryStatusDelivered,
|
||||
)
|
||||
|
||||
assert.Contains(t, renderSourceLogsPageWithQuery(
|
||||
t, f.h, f.sess, f.webhook.ID, "?show=failed",
|
||||
), "No event has a failed delivery.")
|
||||
assert.Contains(t, renderSourceLogsPageWithQuery(
|
||||
t, f.h, f.sess, f.webhook.ID, "?show=pending",
|
||||
), "No event has a delivery pending or retrying.")
|
||||
}
|
||||
|
||||
// TestHandleSourceLogs_OnlyNewestStartsExpanded proves that of the
|
||||
// events in the log only the newest starts expanded.
|
||||
func TestHandleSourceLogs_OnlyNewestStartsExpanded(t *testing.T) {
|
||||
|
||||
@@ -1110,6 +1110,15 @@ func (h *Handlers) ownedWebhook(
|
||||
return webhook, true
|
||||
}
|
||||
|
||||
// The event log's show query parameter and its two values: the events
|
||||
// with a failed delivery, and those with a delivery still pending or
|
||||
// retrying.
|
||||
const (
|
||||
showParam = "show"
|
||||
showFailed = "failed"
|
||||
showPending = "pending"
|
||||
)
|
||||
|
||||
// HandleSourceLogs shows the request/response logs for a
|
||||
// webhook.
|
||||
func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
|
||||
@@ -1129,17 +1138,36 @@ func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Any other value of show lists every event, as no value
|
||||
// does.
|
||||
show := r.URL.Query().Get(showParam)
|
||||
|
||||
statuses := eventLogStatuses(show)
|
||||
if statuses == nil {
|
||||
show = ""
|
||||
}
|
||||
|
||||
evts, total, ok := h.loadEventsWithDeliveries(
|
||||
w, r, webhook, targets,
|
||||
w, r, webhook, targets, statuses,
|
||||
)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
failed, pending, err := h.countFailedAndPendingEvents(webhook.ID)
|
||||
if err != nil {
|
||||
h.serverError(w, r, "failed to count events", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
tmplKeyWebhook: &webhook,
|
||||
"Events": evts,
|
||||
"TotalEvents": total,
|
||||
tmplKeyWebhook: &webhook,
|
||||
"Events": evts,
|
||||
"TotalEvents": total,
|
||||
"Show": show,
|
||||
"FailedEvents": failed,
|
||||
"PendingEvents": pending,
|
||||
}
|
||||
|
||||
h.renderTemplate(w, r, "source_logs.html", data)
|
||||
@@ -1201,9 +1229,10 @@ func (h *Handlers) loadTargetMap(
|
||||
|
||||
// loadEventsWithDeliveries loads the recentEventLimit newest events
|
||||
// and their deliveries from the per-webhook database, and the total
|
||||
// number of events stored. Events come back as capped projections
|
||||
// rather than database.Event rows: see eventLogColumns for why the
|
||||
// cut happens in SQL.
|
||||
// number of events stored. Given delivery statuses, both cover only
|
||||
// the events with a delivery in one of them. Events come back as
|
||||
// capped projections rather than database.Event rows: see
|
||||
// eventLogColumns for why the cut happens in SQL.
|
||||
//
|
||||
// The bool reports whether the load succeeded. It is false
|
||||
// once this has answered the request with an error, and the
|
||||
@@ -1213,6 +1242,7 @@ func (h *Handlers) loadEventsWithDeliveries(
|
||||
r *http.Request,
|
||||
webhook database.Webhook,
|
||||
targetMap map[string]eventLogTarget,
|
||||
statuses []database.DeliveryStatus,
|
||||
) ([]EventLogView, int64, bool) {
|
||||
if !h.dbMgr.DBExists(webhook.ID) {
|
||||
return nil, 0, true
|
||||
@@ -1227,7 +1257,14 @@ func (h *Handlers) loadEventsWithDeliveries(
|
||||
return nil, 0, false
|
||||
}
|
||||
|
||||
rows, totalEvents := loadEventLogRows(webhookDB, webhook.ID)
|
||||
rows, totalEvents, err := loadEventLogRows(
|
||||
webhookDB, webhook.ID, statuses,
|
||||
)
|
||||
if err != nil {
|
||||
h.serverError(w, r, "failed to load events", err)
|
||||
|
||||
return nil, 0, false
|
||||
}
|
||||
|
||||
result, ok := h.eventLogViews(
|
||||
w, r, webhookDB, webhook.ID, rows, targetMap,
|
||||
@@ -1302,25 +1339,142 @@ func (h *Handlers) eventLogViews(
|
||||
|
||||
// loadEventLogRows reads the event log projection of the
|
||||
// recentEventLimit newest events, newest first, and the total number
|
||||
// of events stored.
|
||||
// of events stored, both narrowed by statuses as eventsWithStatus
|
||||
// narrows them.
|
||||
func loadEventLogRows(
|
||||
webhookDB *gorm.DB, webhookID string,
|
||||
) ([]eventLogRow, int64) {
|
||||
var totalEvents int64
|
||||
|
||||
webhookDB.Model(&database.Event{}).Where(
|
||||
"webhook_id = ?", webhookID,
|
||||
).Count(&totalEvents)
|
||||
webhookDB *gorm.DB,
|
||||
webhookID string,
|
||||
statuses []database.DeliveryStatus,
|
||||
) ([]eventLogRow, int64, error) {
|
||||
totalEvents, err := countEventsWithStatus(
|
||||
webhookDB, webhookID, statuses,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var rows []eventLogRow
|
||||
|
||||
webhookDB.Model(&database.Event{}).Select(
|
||||
err = eventsWithStatus(webhookDB, webhookID, statuses).Select(
|
||||
eventLogColumns, maxRenderedBodyBytes,
|
||||
).Where(
|
||||
"webhook_id = ?", webhookID,
|
||||
).Order("created_at DESC").Limit(recentEventLimit).Find(&rows)
|
||||
).Order("created_at DESC").Limit(recentEventLimit).Find(&rows).Error
|
||||
|
||||
return rows, totalEvents
|
||||
return rows, totalEvents, err
|
||||
}
|
||||
|
||||
// eventLogStatuses returns the delivery statuses the event log's show
|
||||
// value lists events by, or nil for one that lists every event.
|
||||
func eventLogStatuses(show string) []database.DeliveryStatus {
|
||||
switch show {
|
||||
case showFailed:
|
||||
return []database.DeliveryStatus{database.DeliveryStatusFailed}
|
||||
case showPending:
|
||||
return []database.DeliveryStatus{
|
||||
database.DeliveryStatusPending,
|
||||
database.DeliveryStatusRetrying,
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// eventsWithStatus selects the webhook's events, or, given statuses,
|
||||
// the recentEventLimit newest of those with at least one delivery in
|
||||
// one of them.
|
||||
//
|
||||
// Given statuses, its cost follows the matching deliveries. SQLite
|
||||
// never reorders a CROSS JOIN, so it reads each join's left side
|
||||
// first: the distinct event IDs of the matching deliveries, through
|
||||
// idx_deliveries_status; then each of those events by ID, sorted to
|
||||
// keep the newest; then the rows of only the events kept, so no other
|
||||
// event's body is read. With a plain "id IN (matching deliveries)"
|
||||
// condition instead, SQLite, which keeps no statistics on these
|
||||
// tables, walks every event newest first.
|
||||
func eventsWithStatus(
|
||||
webhookDB *gorm.DB,
|
||||
webhookID string,
|
||||
statuses []database.DeliveryStatus,
|
||||
) *gorm.DB {
|
||||
if statuses == nil {
|
||||
return webhookDB.Model(&database.Event{}).Where(
|
||||
"webhook_id = ?", webhookID,
|
||||
)
|
||||
}
|
||||
|
||||
matching := webhookDB.Model(&database.Delivery{}).
|
||||
Distinct("event_id").Where("status IN ?", statuses)
|
||||
|
||||
newest := webhookDB.Table("(?) AS matching", matching).
|
||||
Joins("CROSS JOIN events ON events.id = matching.event_id").
|
||||
Where(
|
||||
"events.webhook_id = ? AND events.deleted_at IS NULL",
|
||||
webhookID,
|
||||
).
|
||||
Order("events.created_at DESC").Limit(recentEventLimit).
|
||||
Select("events.id AS event_id")
|
||||
|
||||
return webhookDB.Table("(?) AS newest", newest).
|
||||
Joins("CROSS JOIN events ON events.id = newest.event_id")
|
||||
}
|
||||
|
||||
// countEventsWithStatus counts the webhook's events with at least one
|
||||
// delivery in one of the statuses, or every event when statuses is
|
||||
// nil. Given statuses, it counts the distinct events of the matching
|
||||
// deliveries and reads nothing but those deliveries, through
|
||||
// idx_deliveries_status, where counting the events would read every
|
||||
// event row. That is the same number, because retention deletes an
|
||||
// event's deliveries with it.
|
||||
func countEventsWithStatus(
|
||||
webhookDB *gorm.DB,
|
||||
webhookID string,
|
||||
statuses []database.DeliveryStatus,
|
||||
) (int64, error) {
|
||||
var count int64
|
||||
|
||||
if statuses == nil {
|
||||
err := webhookDB.Model(&database.Event{}).Where(
|
||||
"webhook_id = ?", webhookID,
|
||||
).Count(&count).Error
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
err := webhookDB.Model(&database.Delivery{}).Distinct("event_id").
|
||||
Where("status IN ?", statuses).Count(&count).Error
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// countFailedAndPendingEvents returns how many of the webhook's events
|
||||
// the event log lists when it shows only those with a failed delivery,
|
||||
// and when it shows only those with a delivery pending or retrying.
|
||||
func (h *Handlers) countFailedAndPendingEvents(
|
||||
webhookID string,
|
||||
) (int64, int64, error) {
|
||||
if !h.dbMgr.DBExists(webhookID) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
webhookDB, err := h.dbMgr.GetDB(webhookID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
failed, err := countEventsWithStatus(
|
||||
webhookDB, webhookID, eventLogStatuses(showFailed),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
pending, err := countEventsWithStatus(
|
||||
webhookDB, webhookID, eventLogStatuses(showPending),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return failed, pending, nil
|
||||
}
|
||||
|
||||
// resubmitCounts reports, for each of the page's events, how many
|
||||
|
||||
@@ -91,7 +91,8 @@ func TestPausedTarget_ShownUntilBreakerCloses(t *testing.T) {
|
||||
assert.Contains(t, log, "t-http: delivered")
|
||||
assert.Regexp(t, waiting+cooldownEnds, log)
|
||||
assert.Contains(t, log, waiting+backoffEnds)
|
||||
assert.NotContains(t, log, "retrying")
|
||||
// No delivery shows as retrying; the Pending link's title says it.
|
||||
assert.NotRegexp(t, `t-http: retrying|>retrying<`, log)
|
||||
|
||||
page := eventPage(t, h, sess, wh.ID, retrying.ID)
|
||||
assert.Regexp(t, waiting+cooldownEnds, page)
|
||||
|
||||
@@ -279,8 +279,9 @@ func (lrw *loggingResponseWriter) Unwrap() http.ResponseWriter {
|
||||
// this branch as bounded as the pattern branches below.
|
||||
//
|
||||
// Nothing debuggable is lost. The only query parameters the service
|
||||
// reads are the login page's `next`, the page to return to, and
|
||||
// `notice`, which names the line a page shows after an action. The
|
||||
// reads are the login page's `next`, the page to return to,
|
||||
// `notice`, which names the line a page shows after an action, and
|
||||
// the event log's `show`, which picks the events it lists. The
|
||||
// alternatives that would preserve more (a key count, a key
|
||||
// allowlist) all require parsing an attacker-sized query on every
|
||||
// request, which is work an unauthenticated client would then be
|
||||
|
||||
@@ -943,15 +943,15 @@ func checkEventSelection(
|
||||
}
|
||||
|
||||
// checkEventKeyboard loads the event log and checks that Tab from the
|
||||
// page's Back link reaches the row of the newest event, the first after
|
||||
// it, and that Enter then collapses that event, which starts expanded,
|
||||
// and Space expands it again.
|
||||
// page's Pending link, the last link above the list, reaches the row of
|
||||
// the newest event, the first after it, and that Enter then collapses
|
||||
// that event, which starts expanded, and Space expands it again.
|
||||
func checkEventKeyboard(
|
||||
ctx context.Context, t *testing.T, url, eventID string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
back := `//a[contains(text(), "Back to")]`
|
||||
pending := `//a[starts-with(text(), "Pending")]`
|
||||
expanded := `form[action$="/` + eventID + `/resubmit"]`
|
||||
|
||||
var focused string
|
||||
@@ -959,12 +959,12 @@ func checkEventKeyboard(
|
||||
require.NoError(t, chromedp.Run(
|
||||
ctx,
|
||||
loadPage(url),
|
||||
chromedp.Focus(back, chromedp.BySearch),
|
||||
chromedp.Focus(pending, chromedp.BySearch),
|
||||
chromedp.KeyEvent(kb.Tab),
|
||||
chromedp.Evaluate(`document.activeElement.textContent`, &focused),
|
||||
))
|
||||
require.Contains(t, focused, eventID,
|
||||
"Tab from the Back link does not reach the event's row")
|
||||
"Tab from the Pending link does not reach the event's row")
|
||||
|
||||
require.NoError(t, chromedp.Run(ctx, chromedp.KeyEvent(kb.Enter)))
|
||||
assert.True(t, hidden(ctx, expanded), "Enter does not collapse the event")
|
||||
|
||||
@@ -219,8 +219,8 @@ func keptSentryHeaders(headers map[string]string) map[string]string {
|
||||
// sentryKeepsHeader reports whether a request header is routing or
|
||||
// content metadata rather than client-chosen payload. Referer is kept
|
||||
// on the reasoning that it is browser-set, that the only query
|
||||
// parameters in this service's own URLs are the login page's `next`
|
||||
// and `notice`, and that Referrer-Policy is set to
|
||||
// parameters in this service's own URLs are the login page's `next`,
|
||||
// `notice` and the event log's `show`, and that Referrer-Policy is set to
|
||||
// strict-origin-when-cross-origin. X-Request-Id ties the event to the
|
||||
// local access log line, which holds the rest of the detail.
|
||||
func sentryKeepsHeader(name string) bool {
|
||||
|
||||
Reference in New Issue
Block a user