Skip to content

fix(taxonomic-filter): stop the category label and hidden-event mismatches - #92075

Draft
posthog[bot] wants to merge 4 commits into
masterfrom
posthog-self-driving/fixtaxonomic-filter-stop-the-category-5f8829
Draft

fix(taxonomic-filter): stop the category label and hidden-event mismatches#92075
posthog[bot] wants to merge 4 commits into
masterfrom
posthog-self-driving/fixtaxonomic-filter-stop-the-category-5f8829

Conversation

@posthog

@posthog posthog Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Problem

  • Someone searching the taxonomic filter (the shared event and property picker reached from insights, replay filters, flags, and cohorts) is told there are no matches when matches exist, and in one experiment arm sees the same category name three times instead of results.
  • Two defects, both in the picker:
    • In the treatment arms of the taxonomic-filter-category-dropdown experiment, the category name renders in the search-input pill, again as a list heading, and again inside the open menu. When the Categories column moved into the search input, the heading stayed ungated.
    • The aggregated "All" tab injects recent and pinned rows matched only client-side, with no server filters. The Events tab searches event_definitions with exclude_hidden: true. So the aggregate offers an "Events" row that the Events tab returns nothing for.

Changes

  • The category list heading now renders only in the control variant, matching the Categories column, so the pill variant shows the category name once.
  • Hidden recent and pinned definitions are dropped from the aggregated tab's matches, so it no longer labels a row with a category whose own list excludes it.
  • The pill and control category-dropdown stories set the flag through the featureFlags story parameter instead of an imperative call, so visual regression now captures them. Mechanical: they were skipped before because the imperative call raced the runner.

How did you test this code?

  • Added a render test: the pill variant renders no category list heading. It fails without the heading fix.
  • Added two logic tests: a hidden recent or pinned event is dropped from the aggregate matches while a visible one stays. They fail without the filter.
  • Ran the three TaxonomicFilter jest suites locally. Did not run the visual-regression snapshots; those regenerate in CI. Full repo typecheck reports only pre-existing errors from the unbuilt @posthog/quill workspace, none in the changed files.

Automatic notifications

  • Publish to changelog?

Docs update

None. No user-facing docs cover this picker's internal behavior.

🤖 Agent context

Autonomy: Fully autonomous

  • Authored by an agent (Claude Code) with no human driver.
  • Skills invoked: /writing-tests, /setting-feature-flags-in-storybook, /simplify, /writing-pr-descriptions.
  • The fix targets the surgical option: drop hidden matches from the aggregate rather than reproduce every category's client-side matches inside each category list. exclude_hidden is the only server-side filter the aggregate bypassed.
  • The scratchpad memory tools were not available this session, so no cross-run memory was read or written.

Created with PostHog Desktop from this inbox report.

…tches

Gate the list heading on the control variant so the pill variant shows the
category name once instead of three times.

Drop hidden recent and pinned definitions from the aggregated tab's matches,
so it no longer labels a row with a category whose own list (fetched with
`exclude_hidden: true`) returns nothing for it.

Generated-By: PostHog Desktop
Task-Id: da53cb65-489a-49a7-a6ee-6be6d0389449
@trunk-io

trunk-io Bot commented Sep 1, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@posthog

posthog Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

🦔 PostHog Review reviewed this pull request

Found 1 must fix, 1 should fix, 1 consider.

Published 3 findings (view the review).

Resolved comments: 2 fixed, 1 left for you

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Trunk lane — non-backend lane

This PR is assigned to the non-backend lane. It does not run backend Python tests and may merge in parallel with PRs in other lanes.

⚠️ Complexity (TypeScript) — 4 functions above 10 (max 47)

Cyclomatic complexity above 10 in changed typescript files. Warn only: worth simplifying when you next touch these functions.

Function Location Complexity
<anonymous> frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts:1728 47
loadRemoteItems frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts:890 36
CategoryPillContent frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx:50 19
InfiniteSelectResults frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx:155 15
Bundle size — 🟢 -257 B (-0.0%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 68.04 MiB · 🟢 -257 B (-0.0%)

File Size Δ vs base
posthog-app/src/scenes/surveys/Surveys.js 26.6 KiB 🟢 -2.4 KiB (-8.4%)

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.41 MiB · 22 files 🔺 +5.7 KiB (+0.4%) ███░░░░░░░ 31.4% of 4.51 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.83 MiB · 3,245 files 🔺 +173 B (+0.0%) █████████░ 90.9% of 9.71 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
307.0 KiB ../node_modules/.pnpm/posthog-js@1.422.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
263.5 KiB ../node_modules/.pnpm/posthog-js@1.422.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.js
252.9 KiB src/taxonomy/core-filter-definitions-by-group.json
154.2 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
104.7 KiB src/lib/api.ts
95.2 KiB ../packages/quill/packages/quill/dist/index.js
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Toolbar bundle — eager 2.26 MiB within budget

What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.

Metric Size Δ vs base Budget
Eager (shipped)
entry + static imports
2.26 MiB · 18 files 🔺 +74 B (+0.0%) ████░░░░░░ 39.5% of 5.72 MiB
Deferred (lazy) 2.11 MiB · 45 files no change n/a — loads on demand
Loader dist/toolbar.js 1.1 KiB no change █░░░░░░░░░ 5.8% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
750.8 KiB dist/toolbar/toolbar-app-X3YWLWXD.css
588.3 KiB dist/toolbar/chunk-chunk-IA6FHEMF.js
484.7 KiB dist/toolbar/chunk-chunk-OQ4CRAT2.js
134.1 KiB dist/toolbar/chunk-chunk-CCNNNLCV.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
71.3 KiB dist/toolbar/toolbar-app-SRVV5DDX.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-WNQQAHFE.js
21.0 KiB dist/toolbar/chunk-chunk-CQNTSFCC.js
6.8 KiB dist/toolbar/chunk-chunk-DV7IWQNF.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

Dist folder size — 🔺 +222.1 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1443.30 MiB · 🔺 +222.1 KiB (+0.0%)

stamphog[bot]
stamphog Bot previously approved these changes Sep 1, 2026

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved.

Contained frontend bug fix in the taxonomic filter picker, backed by new unit/render tests that fail without the fix; no risky territory (no auth, billing, API contract, schema, dependency, or CI changes) and no unresolved review concerns.

Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list no deny categories matched
size 43L, 3F substantive, 129L/5F incl. docs/generated/snapshots — within ceiling
tier T1-agent / T1c-medium (129L, 5F, single-area, fix)
stamphog 2.0.0b4 .stamphog/policy.yml @ d0d62e2 · reviewed head d0d62e2

…n stories

The smoke test waited on the first `.taxonomic-infinite-list`, which is an
empty, hidden Recent/Pinned list that never becomes visible. Skip the empty
lists so the runner waits on the visible, populated active list.

Generated-By: PostHog Desktop
Task-Id: da53cb65-489a-49a7-a6ee-6be6d0389449
@stamphog
stamphog Bot dismissed their stale review September 1, 2026 00:29

A new stamphog review started for this PR — the fresh verdict replaces this approval.

@posthog

posthog Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review

Found 1 must fix, 1 should fix, 1 consider.

Comment thread frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts
Comment thread frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts Outdated
Comment on lines +80 to +81
function isHiddenDefinition(item: TaxonomicDefinitionTypes): boolean {
return 'hidden' in item && !!item.hidden

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persisted shortcuts cannot report a later hidden state

must_fix bug

Why we think it's a valid issue
  • Checked: every production dispatcher of recordRecentFilter, the pinned write and refresh paths, and the two tests the PR adds.
  • Found: all four production recent writers strip the field. taxonomicFilterLogic.tsx:2497 builds const cleanItem = { name: stripped.name, ...(stripped.id ? { id: stripped.id } : {}) }; hooks/useTaxonomicFilter.ts:485-488 builds the same shape; PropertyFilters/propertyFilterLogic.ts:206 passes item: { name: displayName }; UniversalFilters/universalFiltersLogic.ts:64 passes item: { name: key }. A repo-wide grep finds no other non-test dispatcher. hidden therefore never reaches recentFilters, so isHiddenDefinition cannot return true for any recent row in production.
  • Found: the new test manufactures a shape production cannot write — infiniteListLogic.test.ts:1485-1490 dispatches recordRecentFilter with item: { name: 'hidden event', hidden: true }. The test passes while the guard it covers is unreachable, so it cannot catch a regression.
  • Found: pinned snapshots do keep hidden (PINNED_ITEM_DENYLIST at taxonomicFilterPinnedPropertiesLogic.ts:49 does not list it, and DefinitionPopover.tsx:82 passes the whole definition), but nothing refreshes them. togglePin writes the snapshot once (taxonomicFilterPinnedPropertiesLogic.ts:206-224), and the only other writer, setPinnedFilters, runs solely for the one-time localStorage migration and default seeding (taxonomicFilterPinnedPropertiesLogic.ts:295, :328).
  • Found: the PR's own example cannot produce a hidden pin either. The Events group fetches with exclude_hidden: true (utils/buildTaxonomicGroups.tsx:216), so a hidden event is not in that list to pin from.
  • Impact: the realistic route to the reported mismatch is "use or pin a visible definition, hide it later". In that case the persisted snapshot carries no hidden, the guard passes, and the aggregated tab still offers a row the category endpoint rejects — the exact behavior the change claims to remove. The recent half of the change cannot fire at all, and the pinned half fires only when the pin was taken while the item already reported hidden: true, which needs a group whose endpoint returns hidden definitions.
Issue description

isHiddenDefinition reads hidden from the locally persisted item. Production recent writes keep only name and id, so recent items never contain this field. Pinned items keep a snapshot and never refresh it. If a user hides a definition later, both shortcuts still pass this check. The source endpoint rejects the definition, so the category mismatch remains.

Suggested fix

Use the current source-group response as the authority before adding a recent or pinned match. Do not use the persisted hidden snapshot. Add a regression test that records a visible event through production actions. Hide that event, and verify that All does not return it.

Prompt to fix with AI (copy-paste)
## Context
@frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts#L80-81
@frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts#L1577-1578
@frontend/src/lib/components/TaxonomicFilter/infiniteListLogic.ts#L1594-1595

<issue_description>
`isHiddenDefinition` reads `hidden` from the locally persisted item. Production recent writes keep only `name` and `id`, so recent items never contain this field. Pinned items keep a snapshot and never refresh it. If a user hides a definition later, both shortcuts still pass this check. The source endpoint rejects the definition, so the category mismatch remains.
</issue_description>

<issue_validation>
- **Checked:** every production dispatcher of `recordRecentFilter`, the pinned write and refresh paths, and the two tests the PR adds.
- **Found:** all four production recent writers strip the field. `taxonomicFilterLogic.tsx:2497` builds `const cleanItem = { name: stripped.name, ...(stripped.id ? { id: stripped.id } : {}) }`; `hooks/useTaxonomicFilter.ts:485-488` builds the same shape; `PropertyFilters/propertyFilterLogic.ts:206` passes `item: { name: displayName }`; `UniversalFilters/universalFiltersLogic.ts:64` passes `item: { name: key }`. A repo-wide grep finds no other non-test dispatcher. `hidden` therefore never reaches `recentFilters`, so `isHiddenDefinition` cannot return true for any recent row in production.
- **Found:** the new test manufactures a shape production cannot write — `infiniteListLogic.test.ts:1485-1490` dispatches `recordRecentFilter` with `item: { name: 'hidden event', hidden: true }`. The test passes while the guard it covers is unreachable, so it cannot catch a regression.
- **Found:** pinned snapshots do keep `hidden` (`PINNED_ITEM_DENYLIST` at `taxonomicFilterPinnedPropertiesLogic.ts:49` does not list it, and `DefinitionPopover.tsx:82` passes the whole definition), but nothing refreshes them. `togglePin` writes the snapshot once (`taxonomicFilterPinnedPropertiesLogic.ts:206-224`), and the only other writer, `setPinnedFilters`, runs solely for the one-time localStorage migration and default seeding (`taxonomicFilterPinnedPropertiesLogic.ts:295`, `:328`).
- **Found:** the PR's own example cannot produce a hidden pin either. The Events group fetches with `exclude_hidden: true` (`utils/buildTaxonomicGroups.tsx:216`), so a hidden event is not in that list to pin from.
- **Impact:** the realistic route to the reported mismatch is "use or pin a visible definition, hide it later". In that case the persisted snapshot carries no `hidden`, the guard passes, and the aggregated tab still offers a row the category endpoint rejects — the exact behavior the change claims to remove. The recent half of the change cannot fire at all, and the pinned half fires only when the pin was taken while the item already reported `hidden: true`, which needs a group whose endpoint returns hidden definitions.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Use the current source-group response as the authority before adding a recent or pinned match. Do not use the persisted `hidden` snapshot. Add a regression test that records a visible event through production actions. Hide that event, and verify that All does not return it.
</potential_solution>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and I've confirmed it in the current code. The recent half of this filter is effectively dead in production: both recent writers persist only { name, id } (taxonomicFilterLogic.tsx and hooks/useTaxonomicFilter.ts), and recent items are rebuilt from that stored snapshot with no re-hydration (recentTaxonomicFiltersLogic.ts), so a recent row never carries hidden and the guard never fires for it. The added recent test records hidden: true directly, a shape production can't write, so it passes without covering a real path. Pinned items do keep hidden, but only when the pin was taken while the definition was already hidden (which needs a group whose endpoint returns hidden items); the common 'hide it later' case leaves the snapshot with no hidden, so the mismatch you describe remains.

I'm escalating rather than fixing this unattended, because the correct fix — judging hidden against the current source-group response instead of the persisted snapshot — is a design change, not a contained edit. It needs the aggregate to read each source group's live results, handle groups that haven't loaded yet, and define what 'not in the current response' means (hidden, not-yet-loaded, or just filtered out by the search). Those are decisions with real trade-offs and behavior that only shows up against live async data, so a human should pick the direction. The main options: (1) resolve hidden from live source-group data; (2) keep the snapshot approach as best-effort and make the test and the code comment honest that it only catches pinned-while-hidden; or (3) drop the aggregate hidden-filter entirely, since the item stays selectable through its group's own matches anyway. One connected note for whoever takes this: I extended the same snapshot-based filter to the aggregate's idle (no-query) prefix in an earlier commit on this PR for internal consistency, so it carries the same limitation and should be revisited alongside the search-path filter when you choose a direction.

stamphog[bot]
stamphog Bot previously approved these changes Sep 1, 2026

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved.

Contained frontend bug fix in the taxonomic filter picker with new tests covering both fixed behaviors; no risky-territory surface (auth, billing, data models, APIs, CI/deploy) and no unresolved concerns.

Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list no deny categories matched
size 47L, 3F substantive, 133L/5F incl. docs/generated/snapshots — within ceiling
tier T1-agent / T1c-medium (133L, 5F, single-area, fix)
stamphog 2.0.0b4 .stamphog/policy.yml @ c816fe0 · reviewed head c816fe0

@trunk-io

trunk-io Bot commented Sep 1, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic Badge

Failed Test Failure Summary Logs
Filters/Taxonomic Filter CategoryDropdownPill smoke-test The test timed out while waiting for a specific list element to become visible. Logs ↗︎

View Full Report ↗︎Docs

The aggregated "All" tab filtered hidden recents and pinned only while a
search query was typed. With no query it leads with recent/pinned prefixes
built from the raw arrays, so a hidden event returned under its source
category on open and disappeared once a character was typed.

Apply the same `isHiddenDefinition` filter to the idle prefixes in the
`items` selector and the `dedupedTopMatches` dedupe keys, so the aggregate
no longer offers an "Events" row the Events tab returns nothing for.

Generated-By: PostHog Desktop
Task-Id: aeabee01-b2e8-4cc0-865d-1e5a6f4757be
@stamphog
stamphog Bot dismissed their stale review September 1, 2026 00:42

New commits were pushed — dismissing the stamphog approval from an earlier head. This PR no longer qualifies for automatic review.

The comment claimed every category endpoint sends `exclude_hidden: true`.
Feature flags and Numerical event properties omit the param, so their own
tabs still list hidden items while the aggregate suppresses their hidden
recents and pinned. Describe that exception and why the suppression is a
deliberate minor inconsistency rather than a full mismatch.

Generated-By: PostHog Desktop
Task-Id: aeabee01-b2e8-4cc0-865d-1e5a6f4757be
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants