Skip to content

Cache visitor graph results locally#6119

Merged
apata merged 16 commits intomasterfrom
internal-api-refactor--typescript
Mar 11, 2026
Merged

Cache visitor graph results locally#6119
apata merged 16 commits intomasterfrom
internal-api-refactor--typescript

Conversation

@apata
Copy link
Copy Markdown
Contributor

@apata apata commented Mar 3, 2026

Changes

This PR refactors top stats and the main graph to cache results locally. This makes the stats and the main graph load instantly for views that the user has already visited in the session.
The results are cached until the next time the page is refreshed (normal, not hard refresh), or the page is closed and reopened.
Realtime results are updated any time that period is selected and then at regular intervals.

Other changes

  • Pulls selected interval and selected metric to component state and passes them to subcomponents as props. This makes React aware of these values, which in turn makes the component updates more predictable.
  • Fixes the reserved area jump from a stale height guess if the user rotates their screen to landscape mode and refreshes the page
  • Removes unused sampling notice

Tests

  • Manually tested

Changelog

  • This PR does not make a user-facing change

Documentation

  • This change does not need a documentation update

Dark mode

  • The UI has been tested both in dark and light mode

@apata apata force-pushed the internal-api-refactor--typescript branch from 1423b3c to f62199e Compare March 3, 2026 16:42
@apata apata changed the title Internal api refactor Cache visitor graph results locally Mar 3, 2026
@apata apata added the preview label Mar 3, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 3, 2026

Preview environment👷🏼‍♀️🏗️
PR-6119

@apata apata marked this pull request as ready for review March 4, 2026 06:21
@apata apata requested a review from a team March 4, 2026 06:48
Copy link
Copy Markdown
Contributor

@RobertJoonas RobertJoonas left a comment

Choose a reason for hiding this comment

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

visitor-graph looks a lot cleaner, and caching is definitely a good step for us. Nice work! 🙌

Everything looks good to me codewise. Noticed a couple of UI things that should be fixed before merging though. There's one comment inline about bottom margin, but there's also this:

Screen.Recording.2026-03-04.at.09.56.11.mov

Making two requests is expected in realtime -- one is for current visitors. But what's not expected is the flicker when the stats update. Hope you can see it in the recording.

import GraphTooltip from './graph-tooltip'
import { buildDataSet, METRIC_LABELS, hasMultipleYears } from './graph-util'
import dateFormatter from './date-formatter'
import FadeIn from '../../fade-in'
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.

Could we keep this? Comparing with master, the graph appearing with a tiny fade-in makes it noticeably smoother.

Or does it not play nice with the instant "cached stats render" transition?

Copy link
Copy Markdown
Contributor Author

@apata apata Mar 5, 2026

Choose a reason for hiding this comment

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

Thanks! The show prop was always truthy for it, so I removed it. Will try to add something like it back in.

</div>
</FadeIn>
)
return <canvas id="main-graph-canvas" className={canvasClass} />
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.

Not sure if I'm commenting on the correct line, but just noticed on staging that the padding below the time labels (x-axis of the graph) was removed. Let’s add back that empty space.

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.

Oh, nice catch! I did change around which components have margins and made sure there was no jumpiness during load, but missed this one. In this PR, the chart is 4 tailwind units taller. Will fix

.filter((stat) => stat.graphable)
.map((stat) => stat.metric)

setSelectedMetric((currentlySelectedMetric) => {
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.

Just curious: what's the reason for passing this kind of function? Why not just this:

if (currentlySelectedMetric && availableMetrics.includes(currentlySelectedMetric)) {
  setSelectedMetric(currentlySelectedMetric)
} else {
  setSelectedMetric(availableMetrics[0])
}

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.

Good question. selectedMetric state is a reactive value. It should then become a dependency of the useEffect hook, because the hook makes use of that reactive value.

Because the effect changes it sometimes to a new value, the effect's dependencies changed, and the effect would run again immediately. On the second run, it would probably not change the value again (because the effect is idempotent: given the same dependencies, yields the same value), so there wouldn't be an infinite loop, but still inefficient.

If we'd leave it out of the dependency array (and ignore the linter), sometimes the effect might not run when it should: a subtle bug in our code. https://react.dev/learn/removing-effect-dependencies#why-is-suppressing-the-dependency-linter-so-dangerous

Copy link
Copy Markdown
Contributor

@ukutaht ukutaht left a comment

Choose a reason for hiding this comment

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

Navigating around with cached frontend data feels amazing 👏 I'm also curious to see if it makes Clickhouse any happier.

Besides the comments, there are two general issues I'm seeing that I can't comment on the diff:

Caching doesn't work for relative date ranges

Date ranges like 28d don't work properly with the caching implementation. This is because the nowForSite(site) function called in DashboardState will return a timestamp with millisecond precision which is used for caching. It will never match the cache key.

Proposed fix: in DashboardContext call nowForSite(site).startOf('day') instead to clamp the timestamp properly for caching.

Dashboard rendering freeze

When clicking on top stats metrics or changing intervals, the UI freezes momentarily. This is because the line-graph's componentDidUpdate is synchronously destroying the chart and rebuilding it within the same rendering cycle. This creates a noticeable delay in the the UI giving user feedback for the click they just made (the top stat highlighting is delayed).

Proposed fix: wrap the chart rebuilding in line-graph in requestAnimationFrame so chart is rebuild after the react rendering cycle which provides feedback to user that the click was registered.

const isRealtime = dashboardState.period === 'realtime'
const refetchTopStats = topStatsQuery.refetch

useEffect(() => {
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.

Seems like only top stats are re-fetched in realtime view. I tested by staying on the realtime view for a few minutes and the main graph does not change after multiple ticks.

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.

Thanks, on it! Also, I noticed that the main-graph is sometimes requested with disallowed interval. Working on that too.

return await fetchTopStats(site, dashboardState)
},
placeholderData: (previousData) => previousData,
staleTime: Infinity
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.

I think this might be too aggressive for when the tab is left open for a long time and the user returns. Something like 30 minutes would get most of the benefit but also make sure the data isn't too stale, what do you think?

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.

Thanks for pointing it out! Because refreshing throws out all stored data at once, we want the user to refresh as little as possible. That means we need to set really unaggressive cache times, as appropriate by period.

Of the periods that include the present moment, I think probably the shorter the range, the shorter the staleTime should be. For year to date, it can be hours. For month to date, an hour. Last 7 days, 15min, Today, 5min. Realtime data should be no more stale than the last updated ticker shows.

Historical ranges can have much longer cache times, let's say 10h, enough to last one working day. By historical I mean anything that isn't supposed to include the present moment.

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.

Something like this eab7baa#diff-6ebff7af37ed5b145ec3ca60c9db1593545938d3d71b5ed7be93a7c8d64dc7fd

No doubt it could be further improved (take into account native stats begin and comparison date range), but I hope it's good enough for starting out.

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.

Agree with the idea to make it dynamic based on the time range - nice touch!

@apata apata added this pull request to the merge queue Mar 11, 2026
Merged via the queue into master with commit 6950812 Mar 11, 2026
24 checks passed
@apata apata deleted the internal-api-refactor--typescript branch March 11, 2026 06:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants