Store and show an event's query string, and pass it on to an HTTP target when set (closes #312)
check / check (push) Successful in 6m32s

The receiver keeps the query string of the request it received on the event, in a new `raw_query` column of the per-webhook `events` table; a resubmitted copy carries its original's. The event log and the event's page show it with the request, the event log leaving out one over 32 KiB as it does request headers. The archive and log targets carry it. An HTTP target gets a `forwardQuery` setting, off by default, on both target forms and in the target list: on, each delivery, replays and resubmits included, appends the query string to the target URL, joined with `&` to one it already has.

Model: opus-5-5
This commit is contained in:
2026-10-04 00:18:34 +00:00
parent a8fc0c5d32
commit ba19430b87
32 changed files with 537 additions and 69 deletions
+34 -18
View File
@@ -1638,11 +1638,19 @@ URL, custom headers, timeout settings).
**`http` target configuration:**
| Key | Type | Description |
| --------- | ------------- | -------------------------------------------------------------------------------------- |
| `url` | string | Destination the event is POSTed to |
| `headers` | object | Extra request headers, applied last so they win over the event's own forwarded headers |
| `timeout` | integer (sec) | Per-target request timeout; unset (or 0) uses the shared 30-second client timeout |
| Key | Type | Description |
| -------------- | ------------- | ----------------------------------------------------------------------------------------- |
| `url` | string | Destination the event is POSTed to |
| `headers` | object | Extra request headers, applied last so they win over the event's own forwarded headers |
| `timeout` | integer (sec) | Per-target request timeout; unset (or 0) uses the shared 30-second client timeout |
| `forwardQuery` | boolean | Pass the query string each event arrived with on to the target; unset (or false) does not |
`forwardQuery` is off by default, and the target URL is then sent exactly as
configured. On, each delivery appends the event's query string to the target
URL, joined with `&` when the URL already has a query string of its own; a
replayed delivery and a resubmitted event's deliveries do the same. Both target
forms offer it as "Pass the query string on to this target", and the target list
shows it when it is on.
`timeout` is capped at **300 seconds**, and the form rejects anything above it
rather than substituting the cap. A delivery attempt holds one of the bounded
@@ -1704,6 +1712,7 @@ auditing, for replay, and for resubmission.
| `webhook_id` | UUID | Foreign key → Webhook |
| `entrypoint_id` | UUID | Foreign key → Entrypoint |
| `method` | string | HTTP method of the captured request. Always `POST`: the receiver answers every other method with 405 before an Event is created |
| `raw_query` | text | The query string of the captured request, as sent, without the leading `?`; empty when there was none. A resubmitted copy carries its original's |
| `headers` | JSON | Complete request headers |
| `body` | text | Raw request body |
| `content_type` | string | Content-Type header value |
@@ -1712,9 +1721,15 @@ auditing, for replay, and for resubmission.
**Relations:** Belongs to Webhook. Belongs to Entrypoint. Has many Deliveries.
When a request arrives at an entrypoint, the full request (method, headers,
body) is captured as an Event. The event is then queued for delivery to every
active target configured on the parent webhook.
When a request arrives at an entrypoint, the full request (method, query string,
headers, body) is captured as an Event. The event is then queued for delivery to
every active target configured on the parent webhook.
The event log and the event's own page show the query string with the rest of
the request. The event log leaves out one larger than 32 KiB, as it does request
headers, and links to the event's page, which shows it whole. The `database` and
`log` targets carry it with the rest of the event. An `http` target receives it
only when its `forwardQuery` setting is on.
#### Delivery
@@ -1760,14 +1775,14 @@ the webhook's currently active targets.
**Resubmit.** Replay recovers one delivery; **resubmit** re-injects one EVENT.
The event log offers a per-event **Resubmit** action that stores a NEW event
copying the stored one's `method`, `headers`, `body` and `content_type`
verbatim, then fans it out to the webhook's currently **active** targets —
resolved fresh by the same query the receiver uses, so a target created long
after the original event arrived receives it. That is the difference that
matters: a target added to test a backend under development has no prior
delivery, so there is nothing to replay to it, while a resubmit reaches it like
any other active target. Inactive targets are skipped, exactly as the receiver
skips them.
copying the stored one's `method`, `raw_query`, `headers`, `body` and
`content_type` verbatim, then fans it out to the webhook's currently **active**
targets — resolved fresh by the same query the receiver uses, so a target
created long after the original event arrived receives it. That is the
difference that matters: a target added to test a backend under development has
no prior delivery, so there is nothing to replay to it, while a resubmit reaches
it like any other active target. Inactive targets are skipped, exactly as the
receiver skips them.
The new event is a first-class event in the log with its own deliveries, not a
marker on the one it came from, and the original's deliveries are left
@@ -2438,7 +2453,8 @@ in front of them, so a query on a fixed 200 URL would otherwise buy the same
amplification as an invented path. Nothing debuggable is lost: the only query
parameters this service reads are the sign-in 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.
log's `show`, which picks the events it lists. A query string sent to an
entrypoint is not lost either: the event stores it, and the event log shows it.
Client-supplied request content does not leave the host by the other route
either. The Sentry SDK attaches the request to every event it captures,
@@ -2901,7 +2917,7 @@ page that was asked for.
| `POST` | `/hook/{id}/edit` | Edit webhook submission |
| `POST` | `/hook/{id}/delete` | Delete webhook |
| `GET` | `/hook/{id}/events` | Full Event Log. `?show=failed` lists only the events with a failed delivery, and `?show=pending` only those with a delivery pending or retrying |
| `GET` | `/hook/{id}/events/{eventID}` | One event's own page: its details, the entrypoint it arrived at (for a resubmitted copy, the one the request it copies arrived at), its request headers, its whole body and every delivery of it |
| `GET` | `/hook/{id}/events/{eventID}` | One event's own page: its details, the entrypoint it arrived at (for a resubmitted copy, the one the request it copies arrived at), its query string, its request headers, its whole body and every delivery of it |
| `GET` | `/hook/{id}/events/{eventID}/body` | Download an event's stored body. The pages show a body as text, cut at 32 KiB in the recent events and the event log, and leave a binary one out, so this is the only route that serves the stored bytes; it is offered wherever a body is cut or binary |
| `POST` | `/hook/{id}/deliveries/{deliveryID}/replay` | Replay a finished delivery: creates a new delivery for the same event against the target's current configuration (30 per minute per bucket, then `429`) |
| `POST` | `/hook/{id}/events/{eventID}/resubmit` | Resubmit a stored event: creates a new event copying it and fans that out to every currently active target (30 per minute per bucket, then `429`) |