Skip to content

feat(inbox): share filters via URL params - #70699

Merged
andrewm4894 merged 4 commits into
masterfrom
posthog-code/inbox-filter-url-params
Jul 14, 2026
Merged

feat(inbox): share filters via URL params#70699
andrewm4894 merged 4 commits into
masterfrom
posthog-code/inbox-filter-url-params

Conversation

@andrewm4894

Copy link
Copy Markdown
Member

Problem

Inbox users had no way to share the exact view they were looking at. The author (reviewer scope), source, priority and sort filters lived only in local storage, so a link to /inbox always dropped the recipient into their own persisted filters. If you spotted something worth handing off ("look at the error-tracking reports assigned to me, priority first") you had to describe the filters in words.

Changes

Filter state now round-trips through the URL query string:

  • scope — the author/reviewer scope (for-you, entire-project, teammate:<uuid>)
  • source — comma-separated source products
  • priority — comma-separated priorities
  • sortfield:direction

The logic (inboxFiltersLogic) gained actionToUrl/urlToAction. Any filter change rewrites the current URL (with replace: true, so toggles don't pile up in history). Defaults are omitted, keeping shared links clean. On load a link is authoritative for the filters it carries and resets the rest to defaults, so it reproduces the sender's view faithfully; a bare /inbox keeps the persisted state and reflects it back into the URL so the current view is immediately shareable. Unknown or malformed param values are dropped rather than trusted.

Local storage persistence is unchanged. The logic is now connected into inboxSceneLogic so its URL sync applies on a cold deep-link before the filter bar renders.

How did you test this code?

Added unit tests on the pure encode/decode functions in inboxFiltersLogic.test.ts: a round-trip through filterSearchParamsparseFilterSearchParams, that default filters produce an empty param set (clean links), and that unknown source/priority/scope/sort values fall back to defaults. These guard the two ways shared links break silently — a default leaking into the URL, or an invalid param leaking into the filter state. No existing test covered URL syncing (it didn't exist before).

I (Claude, the PostHog Slack app) could not run the frontend typecheck or jest locally — dependencies aren't installed in this environment — so CI is the first place these run.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Andy Maguire asked for the inbox author and source filters to become shareable URL params. I chose to sync the full filter/sort set (scope, source, priority, sort) rather than just the two named ones, so a link reproduces the whole view. Key design decision: make the link authoritative on load but fall back to persisted state on a bare URL, which keeps existing local-storage persistence intact while enabling sharing. Invoked the /writing-tests skill before adding tests and kept them at the cheapest rung (pure functions) rather than a full kea-router harness test.


Created with PostHog from a Slack thread

Mirror the inbox filter state (reviewer scope, source product, priority, sort) to the URL query string so a specific view can be shared via a link. On load a link is authoritative for the filters it carries; a bare /inbox falls back to the persisted state and reflects it back into the URL so the current view stays shareable.

Generated-By: PostHog Code
Task-Id: f4904064-96b4-4bcb-b45e-6abf5d5f38f2
@github-actions

Copy link
Copy Markdown
Contributor

Hey @andrewm4894! 👋

It looks like your git author email on this PR isn't your @posthog.com address (andrewm4894@gmail.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit 4b6dd04 · box box-405f5a288517 · ready in 1192s (push → usable) · build log · rebuilds on every push, torn down on close

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Bundle size — 🔺 +12.2 KiB (+0.0%)

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

Total: 64.44 MiB · 🔺 +12.2 KiB (+0.0%)

File Size Δ vs base
posthog-app/src/scenes/bootApp.js 5.2 KiB 🔺 +5.2 KiB (new)
posthog-app/src/scenes/App.js 22.4 KiB 🟢 -4.5 KiB (-16.6%)
posthog-app/src/scenes/inbox/InboxScene.js 653.8 KiB 🔺 +2.5 KiB (+0.4%)
posthog-app/src/scenes/AuthenticatedShell.js 220.9 KiB 🔺 +1.3 KiB (+0.6%)

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.21 MiB · 22 files 🔺 +94 B (+0.0%) ███░░░░░░░ 28.2% of 4.29 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.12 MiB · 2,974 files 🔺 +2.4 KiB (+0.0%) █████████░ 87.7% of 9.25 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
278.6 KiB ../node_modules/.pnpm/posthog-js@1.399.5/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
234.9 KiB src/taxonomy/core-filter-definitions-by-group.json
222.1 KiB ../node_modules/.pnpm/posthog-js@1.399.5/node_modules/posthog-js/dist/module.js
164.0 KiB src/queries/validators.js
154.3 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
105.9 KiB src/lib/api.ts
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

⚠️ Dist folder size — 🔺 +1.44 MiB (+0.1%)

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

Total: 1285.87 MiB · 🔺 +1.44 MiB (+0.1%)

Playwright — all passed

All tests passed.

View test results →

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 14, 2026 12:08
@andrewm4894
andrewm4894 removed the request for review from a team July 14, 2026 12:10
@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Security Review

The new URL scope parser accepts arbitrary teammate: values. Non-member UUIDs appear to fail closed, but malformed UUIDs can still reach the report list API and produce user-visible validation errors.

Reviews (1): Last reviewed commit: "feat(inbox): sync author, source and fil..." | Re-trigger Greptile

import api from 'lib/api'
import { urls } from 'scenes/urls'

import { INBOX_PRIORITY_OPTIONS, INBOX_SOURCE_OPTIONS } from '../filterOptions'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Filter Options Import Cycle

Importing ../filterOptions here creates a runtime cycle because filterOptions.tsx already imports the inbox sort types from this logic file. When the inbox scene loads the filter logic, the module can re-enter itself through filterOptions, leaving one side with partially initialized exports and breaking inbox initialization depending on module evaluation order.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 71c8f69. Made filterOptions.tsx'''s import of the sort types type-only (import type { InboxSortDirection, InboxSortField }), so it'''s erased at build and the runtime cycle is gone. The logic keeps importing the value constants from filterOptions — only the reverse edge was the runtime one.

Comment thread frontend/src/scenes/inbox/logics/inboxFiltersLogic.ts Outdated
@andrewm4894 andrewm4894 added the stamphog Request AI approval (no full review) label Jul 14, 2026
@trunk-io

trunk-io Bot commented Jul 14, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

chatgpt-codex-connector[bot]

This comment was marked as outdated.

@stamphog

stamphog Bot commented Jul 14, 2026

Copy link
Copy Markdown

Note

🤖 stamphog reviewed 760dbf645d5d19600b004e018d2ab17b38617047 — verdict: REFUSED

Two substantive Codex review comments on the current head remain unresolved and unaddressed (unlike the other three, which the author fixed and replied to): sort validation checks field and direction independently, so a shared link with an unsupported combination (e.g. priority:desc) is accepted even though no UI sort option produces it, causing the displayed sort label to mismatch the actual list order; and hydrating from a URL doesn't clear a stale searchQuery, so a shared link can silently show a narrower list than the sender intended. Both directly undercut the PR's stated goal of faithfully reproducing the sender's view.

  • Author wrote 0% of the modified lines and has 42 merged PRs in these paths (familiarity MODERATE).
  • 👍 on the PR from greptile-apps[bot], hex-security-app[bot].
  • Unresolved: chatgpt-codex-connector's 'Reject unsupported sort combinations' comment on inboxFiltersLogic.ts — field/direction are validated independently instead of against the actual INBOX_SORT_OPTIONS combos, so e.g. sort=priority:desc or updated_at:asc is accepted though no UI option matches it.
  • Unresolved: chatgpt-codex-connector's 'Clear stale search when hydrating shared filters' comment on inboxFiltersLogic.ts — searchQuery isn't reset or included when a shared URL hydrates filters, so an existing session with a typed search can show a narrower list than the shared link represents.
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list no deny categories matched
size 184L, 4F substantive, 276L/5F incl. docs/generated/snapshots — within ceiling
tier T1-agent / T1c-medium (276L, 5F, single-area, feat)
stamphog 2.0.0b3 .stamphog/policy.yml @ 04a79ad · reviewed head 760dbf6

Updated in place — this replaces 1 earlier stamphog review(s) on this PR.

@stamphog stamphog Bot removed the stamphog Request AI approval (no full review) label Jul 14, 2026
- Hydrate a shared link's filters via a single atomic setFilters action wired to one reportListLogic refresh, instead of fanning out separate field actions. Fixes source/priority-only links not refetching in an already-mounted session, and the multi-request race between partial filter states.
- Validate the teammate scope id (isUUIDLike) so a malformed shared link falls back to the default scope rather than forwarding a bad reviewer UUID to the list API.
- Make filterOptions' sort-type import type-only to break the runtime import cycle with inboxFiltersLogic.

Generated-By: PostHog Code
Task-Id: f4904064-96b4-4bcb-b45e-6abf5d5f38f2

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 71c8f69c47

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let sortDirection = DEFAULT_SORT_DIRECTION
if (typeof searchParams.sort === 'string') {
const [field, direction] = searchParams.sort.split(':')
if (VALID_SORT_FIELDS.has(field) && (direction === 'asc' || direction === 'desc')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject unsupported sort combinations

When a shared URL contains a syntactically valid but unsupported combination like sort=priority:desc or sort=updated_at:asc, this accepts it because the field and direction are validated independently. The report list then requests that ordering, while the Sort control has no matching option and falls back to displaying Priority first, so recipients can see results sorted differently from the UI label. Validate against the actual emitted sort option keys instead of the full field/direction Cartesian product.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 4b6dd04. parseFilterSearchParams now validates the whole field:direction key against the actual INBOX_SORT_OPTIONS (VALID_SORT_KEYS) rather than validating field and direction independently, so an unsupported combo like priority:desc or updated_at:asc falls back to the default sort instead of driving an ordering the Sort control can't display. Added a test case for it.

values.sortField !== parsed.sortField ||
values.sortDirection !== parsed.sortDirection
if (changed) {
actions.setFilters(parsed)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear stale search when hydrating shared filters

When the inbox is already mounted with a non-empty search box, following a shared URL like /inbox?source=github only dispatches setFilters(parsed) for the URL-backed fields and leaves searchQuery intact; if the URL-backed fields already match, the changed guard skips dispatch entirely. reportListLogic still sends search: searchQuery.trim() with the list request, so the recipient sees a further narrowed list that is not represented in the shared URL. Clear search on URL hydration or include it in the URL state.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 4b6dd04. Rather than clearing search on hydration (which would clobber an in-app search on ordinary navigation, since urlToAction also fires when opening a report), I made searchQuery part of the URL state: it's now included in the search param, hydrated via setFilters, and covered by the changed guard. So a shared link reproduces the search too, and following /inbox?source=github resets a recipient's stale search. It's still not persisted to localStorage, matching desktop.

stamphog[bot]
stamphog Bot previously approved these changes Jul 14, 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.

Contained frontend feature (URL-shareable inbox filters) by an owning-team author; the P1-level import-cycle and malformed-UUID security concerns were fixed with tests, and no in-flight (👀) review or maintainer hold exists.

  • Author wrote 0% of the modified lines and has 42 merged PRs in these paths (familiarity MODERATE).
  • chatgpt-codex-connector[bot] reviewed the current head.
  • Two unresolved Codex P2 comments remain: unsupported sort field/direction combos (e.g. priority:desc) are accepted into state without matching a UI sort option, and a stale in-session search query isn't cleared when hydrating from a shared-filter URL, so results can silently differ from what the URL/link implies. Neither is a crash/data-loss/security issue, but worth a follow-up fix or reply to Codex.
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list no deny categories matched
size 184L, 4F substantive, 271L/5F incl. docs/generated/snapshots — within ceiling
tier T1-agent / T1c-medium (271L, 5F, single-area, feat)
stamphog 2.0.0b3 .stamphog/policy.yml @ 1231bb9 · reviewed head 71c8f69

@andrewm4894
andrewm4894 enabled auto-merge (squash) July 14, 2026 12:52
Fix import ordering in inboxSceneLogic and wrap a long object literal in the filters test so oxfmt --check passes.

Generated-By: PostHog Code
Task-Id: f4904064-96b4-4bcb-b45e-6abf5d5f38f2
@stamphog
stamphog Bot dismissed their stale review July 14, 2026 12:55

New commits pushed (delta classified non_trivial_delta) — stamphog approval dismissed; re-review running automatically.

@stamphog stamphog Bot removed the stamphog Request AI approval (no full review) label Jul 14, 2026
Comment on lines +226 to 240
DEFAULT_SORT_FIELD as InboxSortField,
{ persist: true },
{
setSort: (_, { field }) => field,
setFilters: (_, { filters }) => filters.sortField,
},
],
sortDirection: [
'asc' as InboxSortDirection,
DEFAULT_SORT_DIRECTION as InboxSortDirection,
{ persist: true },
{
setSort: (_, { direction }) => direction,
setFilters: (_, { filters }) => filters.sortDirection,
},
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The clearFilters action does not reset sortField and sortDirection to their defaults, only sourceProductFilter and priorityFilter. This creates inconsistent behavior where clearing filters leaves the sort in a non-default state.

When a user clicks "Clear Filters", they expect all filters to reset, but the sort will remain unchanged. This is especially problematic because:

  1. The URL will still contain sort params after "clearing" (line 284 includes clearFilters in actionToUrl)
  2. The visual state won't match user expectations
  3. Shared URLs after "clearing" will still have custom sort applied

Fix by adding clearFilters handlers to both reducers:

sortField: [
    DEFAULT_SORT_FIELD as InboxSortField,
    { persist: true },
    {
        setSort: (_, { field }) => field,
        setFilters: (_, { filters }) => filters.sortField,
        clearFilters: () => DEFAULT_SORT_FIELD,
    },
],
sortDirection: [
    DEFAULT_SORT_DIRECTION as InboxSortDirection,
    { persist: true },
    {
        setSort: (_, { direction }) => direction,
        setFilters: (_, { filters }) => filters.sortDirection,
        clearFilters: () => DEFAULT_SORT_DIRECTION,
    },
],
Suggested change
DEFAULT_SORT_FIELD as InboxSortField,
{ persist: true },
{
setSort: (_, { field }) => field,
setFilters: (_, { filters }) => filters.sortField,
},
],
sortDirection: [
'asc' as InboxSortDirection,
DEFAULT_SORT_DIRECTION as InboxSortDirection,
{ persist: true },
{
setSort: (_, { direction }) => direction,
setFilters: (_, { filters }) => filters.sortDirection,
},
],
DEFAULT_SORT_FIELD as InboxSortField,
{ persist: true },
{
setSort: (_, { field }) => field,
setFilters: (_, { filters }) => filters.sortField,
clearFilters: () => DEFAULT_SORT_FIELD,
},
],
sortDirection: [
DEFAULT_SORT_DIRECTION as InboxSortDirection,
{ persist: true },
{
setSort: (_, { direction }) => direction,
setFilters: (_, { filters }) => filters.sortDirection,
clearFilters: () => DEFAULT_SORT_DIRECTION,
},
],

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks — I'm going to leave this one as-is. clearFilters intentionally resets only source/priority/search and not scope/sort; that's a pre-existing product decision documented on the hasActiveFilters selector ("Scope and sort are excluded … clearFilters leaves them untouched"). The URL after clearing still accurately mirrors the real state (sort preserved), so there's no new inconsistency introduced by this PR, and changing the Clear-filters semantics is outside this PR's scope. Happy to do it as a separate change if the team wants that behavior.

- Validate the sort param against the actual offered sort options instead of the field/direction Cartesian product, so an unsupported combo like priority:desc no longer sends an ordering the Sort control can't display.
- Mirror the search query to the URL as well, so a shared link reproduces (and hydration resets) the search box instead of leaving a recipient's existing search silently narrowing the shared view.

Generated-By: PostHog Code
Task-Id: f4904064-96b4-4bcb-b45e-6abf5d5f38f2
@andrewm4894
andrewm4894 merged commit 86858fa into master Jul 14, 2026
210 checks passed
@andrewm4894
andrewm4894 deleted the posthog-code/inbox-filter-url-params branch July 14, 2026 13:44
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 14, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-14 14:18 UTC Run
prod-us ✅ Deployed 2026-07-14 14:30 UTC Run
prod-eu ✅ Deployed 2026-07-14 14:34 UTC Run

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.

2 participants