Skip to content

feat(analytics): persist dashboard tab and filters in the URL - #4109

Open
ansmonjol wants to merge 5 commits into
mainfrom
persist-tab-filters-in-url
Open

ansmonjol wants to merge 5 commits into
mainfrom
persist-tab-filters-in-url

Conversation

@ansmonjol

Copy link
Copy Markdown
Contributor

Fixes INT-64

Refreshing the analytics page dropped the user back on the Revenue Streams tab with all 16 filters reset, whatever they were looking at. It stings more than usual because refreshing is also the only workaround for the analytics timeouts in INT-63.

The dashboard now keeps its state in the URL as ?dashboard_state=<key>, so a refresh restores the tab and the filters, and the link is shareable.

How it works

The dashboard runs in a cross-origin iframe, so state can only be injected as query params at mount and read back over the embedded SDK's RPC channel. Superset 6.1.0 restores a saved activeTabs through permalink_key and through nothing else, so we ask the iframe for a permalink whenever the filters or the active tab change, keep the key in our URL, and hand it back as permalink_key on the next load.

dashboardStateSync.ts owns the capture side and is deliberately framework-free: no React, no router, no knowledge of the URL. It takes an EmbeddedDashboard and a callback, and returns a teardown. Filters arrive by push through observeDataMask; tabs have no push event in the SDK, so they are polled once a second, paused while the document is hidden.

Dashboard.tsx owns the URL side: read the param once at mount, feed it to the embed, write new keys back with a replace navigation.

Depends on a Superset-side permission

Creating a permalink needs can_write on DashboardPermalinkRestApi on the LagoViewer guest role, which is not deployed yet: getlago/lago-data#710.

This branch does not wait for it. If the permalink call fails, the sync disables itself and the page behaves exactly as it does today, so the two changes can land in either order.

Also in here

The localStorage filter-persistence path is gone, along with its feature flag, its org-and-dashboard key scoping, and two utility modules that had no other consumer (supersetFilters.ts, risonEncoder.ts). The URL is inherently org-scoped by the slug, so that scoping is no longer needed.

Notes for review

Three things in here are load-bearing and easy to undo by accident:

  • The URL param is read once via a lazy useState, never subscribed to, and navigate is held in a ref rather than listed as an effect dependency. The slug-aware useNavigate wrapper returns a new identity every render, so either mistake remounts the iframe (a full dashboard reload) on every filter change.
  • syncStateKey re-checks its disabled flag after the awaited RPC resolves. debounce.cancel() stops a pending invocation but cannot abort one in flight, so without that check a permalink resolving after teardown would navigate onto whatever route the user has since moved to.
  • The tab poll refuses to take a baseline from an empty reading. embedDashboard resolves on the iframe's load event, well before Superset hydrates, so baselining a pre-hydration reading would make the next poll read the real default tabs as a change and write a dashboard_state onto a page nobody touched.

A permalink failure disables everything (without permalinks there is nothing to sync); a tab-poll failure stops only the poll, leaving the push channel alive.

Cancel the embed continuation on teardown so a race cannot strand the
1 Hz tab poll on a destroyed iframe.

Isolate the tab channel: a getActiveTabs failure now stops only the
poll, leaving filter persistence through observeDataMask alive.

Validate the cross-origin getActiveTabs result and refuse an empty
first reading as the baseline, so a fresh load no longer writes a
dashboard_state the user never asked for.

Also guard against overlapping polls and seed lastEmittedKey from the
url so reverting to the loaded state does not rewrite it.
@ansmonjol ansmonjol self-assigned this Aug 10, 2026
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant