Modifying the TaxonomicFilter
SkillSearchGuides safe modification of the TaxonomicFilter — PostHog's multi-tab picker for events, actions, properties, cohorts, and more. Front-loads the empirical product reality (what users actually pick and search for) plus the three live variants (legacy-control, legacy-pill behind TAXONOMIC_FILTER_CATEG
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Modifying the TaxonomicFilter skill
What this skill tells your AI
The instructions your AI receives, as published by posthog/posthog in .agents/skills/modifying-taxonomic-filter/SKILL.md and read by ahel’s review.
The TaxonomicFilter is the picker users hit to choose any "thing PostHog
knows about" — events, properties, actions, cohorts, groups. It's the
on-ramp into almost every analytics and replay configuration. Code lives
in frontend/src/lib/components/TaxonomicFilter/.
Three guardrails:
- Changes that demote items users actually pick are regressions, even with all tests passing. Read "Product reality" before deciding any change is safe. Ordering, promotion, or position-0 changes need explicit human sign-off — don't let an agent decide alone.
- There are three live variants behind two feature flags, and the rebuild is a parallel reimplementation of the legacy data + group layer — not a skin over it. A behaviour change usually has to land in both the legacy code and the rebuild, or the two arms of the experiment diverge. Read "Three variants" and "Mirroring changes" before assuming one edit is enough.
- Search is the main selection path. The definition endpoints select a Postgres plan from the project size. The picker also shows results before optional counts finish. Read references/performance.md before you change search, loading, pagination, or result reveal behavior.
Product reality (last refreshed 2026-05-02, 90-day window)
Ratios from production telemetry. Re-run via references/refreshing-product-reality.md when older than ~3 months.
How users pick
- Top three rows carry ~80% of selections (position 0: ~56%, position 1: ~15%, position 2: ~8%). Demoting a popular item out of the top three is a real-user regression.
- ~34% selection rate. Two of every three opens close without a pick. p50 dwell ~7s, p90 ~53s — most opens are quick glances.
- Selection paths: ~65% via search, ~19% browsed-no-search, ~16% from recents, <1% from pinned items.
What users select
| Source group type | Share |
|---|---|
events | ~40% |
event_properties | ~30% |
person_properties | ~14% |
cohorts | ~2% |
email_addresses | ~2% |
actions | ~2% |
pageview_urls | ~1% |
| everything else | <1% |
What users search for (share of top-8 terms)
| Term | Share |
|---|---|
email | ~29% |
url | ~22% |
user | ~12% |
utm | ~10% |
page | ~9% |
path | ~8% |
current | ~6% |
country | ~5% |
email and url are over half the top-8. They're the entire reason
PROMOTED_PROPERTIES_BY_SEARCH_TERM (in infiniteListLogic.ts) maps
them to $email and $current_url at position 0. Touching
promotion or ordering needs explicit human sign-off.
Empty searches
email, url, utm, path against cohorts, event_feature_flags,
session_properties produce most empty-result events — users type the
same canonical terms across every tab. Tab order, suggested-filters
aggregation, and shortcut routing are how they get to the right answer.
Input mode
~93% typed, ~7% pasted. Both feed inputMode on taxonomic_filter_search_query.
Telemetry is a contract
Treat property shapes as a public API. Every taxonomic filter * event now
carries a surface property (legacy-control / legacy-pill /
rebuild-menu) so the experiment arms are distinguishable by an explicit
property, not a feature-flag join. The legacy stamp comes from
legacyTaxonomicSurface() in taxonomicFilterSurface.ts; the rebuild stamps
rebuild-menu from menu/TaxonomicFilterMenu.tsx.
Shared events both surfaces emit (keep these comparable across arms):
taxonomic filter closed—surface,dwellMs,hadSelection(legacy also sendsgroupType; the rebuild omits it — there's no single active tab at close)taxonomic filter item selected—surface,groupType,sourceGroupType,wasFromRecents,wasFromPinnedList,wasQuickFilter,hadSearchInput,position,query,wasStale
Legacy-only: taxonomic_filter_search_query
(searchQuery, groupType, inputMode, pastedFraction),
taxonomic filter empty result (groupType, searchQuery),
taxonomic filter include stale toggled,
taxonomic filter category dropdown opened (pill only).
Rebuild-only menu events: taxonomic filter menu opened / drilled /
closed / option clicked / item selected.
When you add a property to a shared event, add it to both emitters or the arms stop being comparable. Adding properties: fine. Removing dead ones: fine. Renaming or repurposing silently is the worst case — dashboards keep working and start lying.
Three variants
Two feature flags, three surfaces. A bug report that doesn't reproduce locally is almost always a variant mismatch — confirm which surface the reporter is on first.
| Surface | Flag | Value | What renders |
|---|---|---|---|
legacy-control | TAXONOMIC_FILTER_CATEGORY_DROPDOWN | 'control' | original tab-pill UI |
legacy-pill | TAXONOMIC_FILTER_CATEGORY_DROPDOWN | 'pill' | suffix category dropdown (CategoryDropdown.tsx) |
rebuild-menu | TAXONOMIC_FILTER_MENU_REBUILD | on | ground-up rewrite in menu/ over headless/ |
- legacy-control vs legacy-pill is the same A/B we've always had —
one codebase (
taxonomicFilterLogic.tsx+InfiniteList), two render paths. Owner@pauldambra, multivariatecontrol,pill. The direction of travel is to move everyone from control onto pill. - rebuild-menu is a separate, opt-in experiment (
@adamleith) being tested internally. It is a fresh implementation: themenu/dropdown and combobox UI on top ofheadless/(a hooks-based filter panel). It does not route throughtaxonomicFilterLogic/infiniteListLogic; it has its own group definitions, fetch/pagination, and ordering. Seeheadless/UX_SPEC.mdfor its design source of truth.
The rebuild is opt-in in exactly two consumer wrappers:
TaxonomicPopover.tsx and
PropertyFilters/components/TaxonomicPropertyFilter.tsx. Both check
TAXONOMIC_FILTER_MENU_REBUILD and render <TaxonomicFilterMenu> or the
legacy <TaxonomicFilter>. A call site reaching one of those wrappers can
still land on the legacy path: TaxonomicPopover renders the rebuild only
when newMenuSupportsCallSite holds (!allowClear && closeOnChange && ref == null), because the rebuilt menu cannot honour those three capabilities.
So "does this reach the rebuild?" depends on the wrapper and the props
that call site passes, not a single global switch — ActionFilterRow goes
through TaxonomicPopover and passes none of the three, so it does reach
the rebuild. Only a call site that hand-rolls its own popover around
<TaxonomicFilter> bypasses the wrappers entirely.
Touching tab/group rendering means testing all three surfaces.
Mirroring changes across variants
The rebuild reimplements the legacy data layer rather than reusing it, so the same concern lives in two files. There is no lint rule or test enforcing parity — the only guard is "Mirrors the legacy…" comments. When you change one, change the other (or flag to the human that you can't).
| Concern | Legacy | Rebuild |
|---|---|---|
| Group definitions (endpoint, excluded props, group meta) | taxonomicFilterLogic.tsx taxonomicGroups selector | utils/buildTaxonomicGroups.tsx |
| Group ordering + SuggestedFilters injection | taxonomicFilterLogic.tsx taxonomicGroupTypes selector | hooks/useTaxonomicFilter.ts resolveTaxonomicGroupTypes |
| Per-tab fetch / pagination / min-query-length | infiniteListLogic.ts | hooks/useGroupList.ts + useTaxonomicResource.ts + fetchTaxonomicListPage.ts |
| Data-warehouse config flow | inline in InfiniteList.tsx | menu/DwhFlow.tsx |
taxonomic filter item selected / closed telemetry | taxonomicFilterLogic.tsx | menu/TaxonomicFilterMenu.tsx |
New TaxonomicFilterGroupType enum value | types.ts (shared) — then add group config in both tables above | |
| Logic-backed group data (Actions, Dashboards, …) | already in kea | also register in hooks/useTaxonomicLocalOverrides.ts |
Genuinely shared — change once: types.ts (the enum),
utils/promoteProperties.ts (PROMOTED_PROPERTIES_BY_SEARCH_TERM),
recentTaxonomicFiltersLogic.ts and
taxonomicFilterPinnedPropertiesLogic.ts (the rebuild reads recents/pinned
through these via a bridge, it doesn't fork them).
redistributeTopMatches is legacy-only, not shared: it lives in
taxonomicFilterLogic.tsx and the rebuild never calls it.
One intentional divergence is already documented in
useTaxonomicFilter.ts: the rebuild always leads with SuggestedFilters,
whereas legacy gates that on the pill variant. Preserve documented
divergences; don't "fix" them into parity.
Pre-change checklist
- Read references when relevant: architecture, common-pitfalls (X/Y matrix), call-sites (smoke tests), testing-patterns, performance (search plan, reveal barrier)
- Decide whether the change must mirror across legacy and rebuild (see "Mirroring changes") — if you can only do one, say so explicitly
- Test all three surfaces if you touched tabs/groups:
legacy-control,legacy-pill,rebuild-menu - Confirm shared telemetry payloads still match across both emitters
- Ordering / promotion / position-0 -> human sign-off, not agent judgement
- If you changed search or loading, verify the plan boundary and confirm that optional requests do not delay results
- Flag the ongoing experiments to the human reviewer: the
control->pill rollout and the internal
rebuild-menuopt-in
hogli test frontend/src/lib/components/TaxonomicFilter/
Signals
- GitHub stars
- 40k
- Forks
- 3k
- Last commit
- Sep 2026
Others that do the same job
Advanced
- Catalog kind
- skill
- Gateway key
modifying-taxonomic-filter- Source
- github.com/posthog/posthog