All skills
posthog avatar

/modifying-taxonomic-filter

@1cc9560 official
by posthogposthog/posthog40k stars
3,379

Guides safe changes to the TaxonomicFilter, PostHog's picker for events, actions, properties, cohorts, and more. Use when adding features, fixing bugs, improving search or loading performance, or refactoring the classic picker, rebuild menu, or headless filter panel. Covers real selection behavior, two live surfaces, shared telemetry, the conditional Postgres search plan, and result reveal rules.

Use this Skill: https://skilld.dev/gh/posthog/posthog/modifying-taxonomic-filter

This session only. Nothing lands on disk.

SKILL.md

≈107 tokens always: the name and description. ≈2.7k when used: this file. ≈5.6k more on demand in 6 files.

Modifying the TaxonomicFilter

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:

  1. 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.
  2. There are two live surfaces, 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 "Two surfaces" and "Mirroring changes" before assuming one edit is enough.
  3. 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-pill / rebuild-menu) so the surfaces are distinguishable by an explicit property. The classic-picker 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 sends groupType; 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.

Two surfaces

One feature flag selects between two 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-pill TAXONOMIC_FILTER_MENU_REBUILD off classic picker with a category dropdown
rebuild-menu TAXONOMIC_FILTER_MENU_REBUILD on ground-up rewrite in menu/ over headless/
  • legacy-pill is the classic picker (taxonomicFilterLogic.tsx + InfiniteList) with a suffix category dropdown. A person can pin the category rail from that dropdown.
  • rebuild-menu is a separate, opt-in experiment (@adamleith) being tested internally. It is a fresh implementation: the menu/ dropdown and combobox UI on top of headless/ (a hooks-based filter panel). It does not route through taxonomicFilterLogic/infiniteListLogic; it has its own group definitions, fetch/pagination, and ordering. See headless/UX_SPEC.md for 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: both surfaces lead with SuggestedFilters when the picker has more than one substantive group. 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 both surfaces if you touched tabs/groups: 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 internal rebuild-menu opt-in to the human reviewer
hogli test frontend/src/lib/components/TaxonomicFilter/

Source: SKILL.md on GitHub

No alerts16d3 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill consists entirely of markdown documentation and architectural guidelines for modifying PostHog's frontend TaxonomicFilter component. No source code or executable scripts are shipped with this skill, and no security risks were identified.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

Signed by skilld at 1cc9560. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 3 weeks ago

README badge

README badge for posthog/posthog/modifying-taxonomic-filter

Guides safe modification of PostHog's TaxonomicFilter — the multi-tab picker for events, actions, properties, and cohorts. Prioritizes product telemetry (what users actually select and search for) and enforces parallel parity across three live variants (legacy-control, legacy-pill, and rebuild-menu) so changes don't regress real usage or create experimental divergence. Use when adding features, fixing bugs, or refactoring any part of the filter component.

Generated from the current SKILL.md.

What are the three live variants of TaxonomicFilter I need to test?
legacy-control (original tab-pill UI), legacy-pill (with category dropdown), and rebuild-menu (ground-up rewrite behind TAXONOMIC_FILTER_MENU_REBUILD). Changes to tabs or groups must be tested across all three surfaces.
Do I need to make changes in both the legacy code and the rebuild?
Usually yes. The rebuild reimplements the legacy data layer rather than reusing it, so concerns like group definitions, ordering, and fetch logic live in two places. See the mirroring table in the skill for which files need updates.
Can I change the ordering or promotion of items in TaxonomicFilter?
No, not without explicit human sign-off. The top three rows carry ~80% of user selections, and users search for email and url heavily — demoting popular items is a real regression even if tests pass.
What telemetry events do I need to keep in sync?
Both legacy and rebuild emitters must send matching payloads for shared events like `taxonomic filter closed` and `taxonomic filter item selected`, or the experiment arms stop being comparable.
Which call sites see the rebuild variant?
Only TaxonomicPopover and PropertyFilters/components/TaxonomicPropertyFilter check the rebuild flag. Call sites like ActionFilterRow that build their own popover never see the rebuild.

Generated from the current SKILL.md. These answers refresh after source changes.