Event log: show only the events with a failed or a pending delivery (closes #390)
check / check (push) Successful in 3m18s
check / check (push) Successful in 3m18s
The event log had no way to list only the events whose delivery failed, and once it showed only the 50 newest, an older failure could not be found at all. It now has All, Failed (N) and Pending (N) links, carried in a `show` query parameter, so they work without the page's script library. Each filtered list keeps the 50-row limit and newest-first order, and lists an event once. It finds matching deliveries through `idx_deliveries_status` and looks their events up by ID, so its cost follows the matches, not the webhook's size. Replay returns to the list it was pressed in. The heading line says what a filter counts. Model: opus-5-5
This commit was merged in pull request #490.
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,
|
||||
@@ -1321,25 +1358,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, 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
|
||||
|
||||
@@ -947,15 +947,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
|
||||
@@ -963,12 +963,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