feat(events): restore header search as its own opt-in facet (#24)#674
Merged
Conversation
Slice 2 removed the `headers::text` branch from the main event search: a trigram GIN on headers measured ~88x ingest p99 write-amp and was refused (packages/db/test/ingest-gin-writeamp.pg.test.ts). The fast `search` now covers only provider_event_id + dedup_key (trigram-indexed, BitmapOr). This restores header search as a SEPARATE, opt-in `headerSearch` facet — an UNINDEXED case-insensitive substring over `headers::text` that AND-composes with `search` and is NEVER OR'd into it, so the fast trigram search stays un-poisoned and a caller who explicitly searches headers accepts a slower (date-bounded, timeout-backstopped) scan. No new index — the 88x refusal stands. Mirrors the existing slice-3b/slice-4 facet pattern (method/eventType) across every surface: - contract: filter.headerSearch = z.string().trim().min(1).max(SEARCH_MAX_LENGTH) (min(1), not the trigram min(3) floor — header search is unindexed) - db: its own `and headers::text ilike ...` residual in browseEvents; threaded through the shared api/mcp read-handler - api: headerSearch query param on both events routes (facet-parity guard) - cli: `wbhk events list --header-search <term>` (honest "slower, unindexed" help) - mcp: honest tool description (separate, unindexed, slower — best date-bounded) - web: a header-search text input that commits on Enter/blur (NOT debounced — each scan is slow), wired through the same applyPatch/URL state, on both the per-endpoint and org-wide pages, with an honest slower-scan hint - plan guard: a date-bounded headerSearch case rides events_org_ordered_idx with no blocking Sort (documents the unindexed-by-design residual) Regenerated the openapi + sdk-ts goldens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9
Adversarial review of PR #674 surfaced four issues, all fixed on this branch. 1. Copy honesty (correctness). Headers are stored as a jsonb ARRAY of [name, value] pairs, so `headers::text` serializes to `[["x-shopify-topic", "orders/create"]]` — NOT wire form. A user pasting a `Name: Value` line matches nothing (the `: ` separator isn't in the text); the residual matches a header NAME substring OR a VALUE substring. Reworded the contract comment, MCP description, CLI --help, OpenAPI param, and the web hint to say "names and values (not a full name: value line)". Regenerated the openapi + sdk goldens. Added a DB assertion pinning that a wire-form line does not match (name/value substrings still do). 2. Per-endpoint timeout advice (correctness). The per-endpoint events page browses ALL-TIME (no 7d default), so the unindexed headerSearch residual can deterministically blow the 5s browse timeout (57014). loadEvents discarded the reason and the page said "Refresh" (which re-runs the identical timeout). Mirrored loadOrgEvents: loadEvents now preserves the 57014→timeout signal and the page renders the actionable "narrow the date range" banner. Test added. 3. Extract the commit-on-blur machinery (cleanup). The headerSearch box was a near-verbatim copy of the eventType box (state + adopt-effect + committedRef + commit + clear reset) — two copies of a hard-won URL-sync discipline that would drift. Extracted ONE shared hook (useCommitOnBlurFilter) used by both. Existing filter-bar tests are the safety net (all green). 4. e2e coverage (jsdom "cannot see the cascade"). Added a Playwright spec that drives the header-search input on the real org events page: type a term, Enter, assert ?headerSearch= in the URL and the row filters, then Clear resets it. Passes in a real browser. The founder must still eyeball the rendered layout + hint copy — the e2e is a floor, not a substitute. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9
choraria
marked this pull request as ready for review
July 18, 2026 17:16
choraria
added a commit
to webhook-co/webhook-go
that referenced
this pull request
Jul 18, 2026
#14) Mirrors webhook-co/webhook#674, which added the `headerSearch` opt-in filter to GET /v1/events and GET /v1/endpoints/{id}/events. The main repo's CI check "webhook-go vendored spec is current" compares packages/openapi/src/openapi.json against this testdata/openapi.json; regenerating this copy keeps them in sync so the route-coverage ratchet sees the new param. Claude-Session: https://claude.ai/code/session_01RybDP88CyucJpg7hCbJVc9 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Internal to-do #24: restore header search as its own opt-in facet — NOT OR'd into the main search.
Slice 2 deliberately removed the
headers::textbranch from the main event search: a trigram GIN on headers measured ~88x ingest p99 write-amp and was refused (packages/db/test/ingest-gin-writeamp.pg.test.ts). The fastsearchnow covers onlyprovider_event_id+dedup_key(trigram-indexed, BitmapOr).This adds a separate, opt-in
headerSearchfacet — an unindexed case-insensitive substring overheaders::textthat AND-composes withsearchand is never OR'd into it, so the fast trigram search stays un-poisoned and a caller who explicitly searches headers accepts a slower (date-bounded, timeout-backstopped) scan. No new index — the 88x refusal stands.Contract field
min(1), not thesearchmin(3)trigram floor — that floor exists only becausepg_trgmextracts no trigrams below 3 chars, and this scan is unindexed, so it doesn't apply. ReusesSEARCH_MAX_LENGTH.Surfaces touched (mirrors the slice-3b/slice-4 method/eventType facet pattern)
packages/contract) —filter.headerSearch, documented as a separate unindexed residualpackages/db/src/reads.ts) — its OWNand headers::text ilike …residual inbrowseEvents, AND-composing;eventSearchFilteruntouched. Threaded through the shared api/mcp read-handlerpackages/openapi) —headerSearchquery param on both events routes (the facet-parity guard)packages/cli) —wbhk events list --header-search <term>(honest "slower, unindexed" help)apps/mcp) — honest tool description: separate, unindexed, slower thansearch, best date-boundedapps/web) — a header-search text input that commits on Enter/blur (NOT debounced — each scan is slow), wired through the sameapplyPatch/URL state as the other facets, on both the per-endpoint and org-wide events pages, with an honest slower-scan hintpackages/db/test/index-usage.test.ts) — a date-boundedheaderSearchcase ridesevents_org_ordered_idxwith no blocking Sort (documents the unindexed-by-design residual; adds NO index)packages/openapi/src/openapi.json+packages/sdk-ts/src/generated/schema.d.tsLoad-bearing invariants held
headerSearchis its OWN residual that AND-composes — never OR'd intoeventSearchFilter/search(a DB test proves an event matchingsearchbut notheaderSearchis excluded).headers::text— the 88x write-amp refusal stands; the plan guard asserts the residual rides the date-ordered path rather than forcing an index.--help, web hint): a slower unindexed scan, best date-bounded.Tests (strict TDD, red→green)
headerSearchvalidates (min 1 — not the min-3 floor, trims, over-max rejected); AND-composes withsearchsearch(separate AND'd filters, not OR'd); threads through the shared handlerheaderSearchrides the ordered index (no blocking Sort)parseEventFilters/effectiveHeaderSearchunit tests; filter-bar component tests (commit-on-blur, own?headerSearch=param, 1-2 char accepted, hint gating, Clear reset)Full affected suites green;
turbo run typecheckclean across all changed packages; prettier + eslint clean.The web filter-bar addition needs founder visual verification — it was NOT visually verified by the agent:
🤖 Generated with Claude Code