-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Cache breakdown responses in details modals #6274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0810b9b
ca87cf7
6c98f8f
c5e1b60
b1e207b
6102dfb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import { DEFAULT_SITE } from '../../../test-utils/app-context-providers' | ||
| import { ComparisonMode, DashboardPeriod } from '../dashboard-time-periods' | ||
| import { formatISO, nowForSite } from '../util/date' | ||
| import { | ||
| CACHE_TTL_HISTORICAL, | ||
| CACHE_TTL_LONG_ONGOING, | ||
| CACHE_TTL_REALTIME, | ||
| CACHE_TTL_SHORT_ONGOING, | ||
| getStaleTime | ||
| } from './api-client' | ||
|
|
||
| const site = DEFAULT_SITE | ||
| const today = nowForSite(site) | ||
| const yesterday = nowForSite(site).subtract(1, 'day') | ||
|
|
||
| const noComparison = { comparison: null, compare_from: null, compare_to: null } | ||
| const base = { site, date: null, from: null, to: null, ...noComparison } | ||
|
|
||
| describe(`${getStaleTime.name}`, () => { | ||
| describe('realtime periods', () => { | ||
| it('for realtime', () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. meganitpick, non-blocking: Can we refactor |
||
| expect(getStaleTime({ ...base, period: DashboardPeriod.realtime })).toBe( | ||
| CACHE_TTL_REALTIME | ||
| ) | ||
| }) | ||
|
|
||
| it('for realtime_30m', () => { | ||
| expect( | ||
| getStaleTime({ ...base, period: DashboardPeriod.realtime_30m }) | ||
| ).toBe(CACHE_TTL_REALTIME) | ||
| }) | ||
| }) | ||
|
|
||
| describe('historical periods (does not include today)', () => { | ||
| it('for 28d', () => { | ||
| expect(getStaleTime({ ...base, period: DashboardPeriod['28d'] })).toBe( | ||
| CACHE_TTL_HISTORICAL | ||
| ) | ||
| }) | ||
|
|
||
| it('for 6mo', () => { | ||
| expect(getStaleTime({ ...base, period: DashboardPeriod['6mo'] })).toBe( | ||
| CACHE_TTL_HISTORICAL | ||
| ) | ||
| }) | ||
|
|
||
| it('for period=day and date=yesterday', () => { | ||
| expect( | ||
| getStaleTime({ ...base, period: DashboardPeriod.day, date: yesterday }) | ||
| ).toBe(CACHE_TTL_HISTORICAL) | ||
| }) | ||
|
|
||
| it('for custom period ending yesterday', () => { | ||
| expect( | ||
| getStaleTime({ | ||
| ...base, | ||
| period: DashboardPeriod.custom, | ||
| from: yesterday.subtract(7, 'day'), | ||
| to: yesterday, | ||
| ...noComparison | ||
| }) | ||
| ).toBe(CACHE_TTL_HISTORICAL) | ||
| }) | ||
| }) | ||
|
|
||
| describe('ongoing periods with short TTL (supports day or shorter interval)', () => { | ||
| it('for today period', () => { | ||
| expect(getStaleTime({ ...base, period: DashboardPeriod.day })).toBe( | ||
| CACHE_TTL_SHORT_ONGOING | ||
| ) | ||
| }) | ||
|
|
||
| it('for 24h period', () => { | ||
| expect(getStaleTime({ ...base, period: DashboardPeriod['24h'] })).toBe( | ||
| CACHE_TTL_SHORT_ONGOING | ||
| ) | ||
| }) | ||
|
|
||
| it('for period=month and date=today', () => { | ||
| expect( | ||
| getStaleTime({ ...base, period: DashboardPeriod.month, date: today }) | ||
| ).toBe(CACHE_TTL_SHORT_ONGOING) | ||
| }) | ||
|
|
||
| it('for period=year and date=today', () => { | ||
| expect( | ||
| getStaleTime({ ...base, period: DashboardPeriod.year, date: today }) | ||
| ).toBe(CACHE_TTL_SHORT_ONGOING) | ||
| }) | ||
|
|
||
| it('for custom period under 12 months ending today', () => { | ||
| expect( | ||
| getStaleTime({ | ||
| ...base, | ||
| period: DashboardPeriod.custom, | ||
| from: today.subtract(6, 'month'), | ||
| to: today | ||
| }) | ||
| ).toBe(CACHE_TTL_SHORT_ONGOING) | ||
| }) | ||
|
|
||
| it('for all time period when stats begin recently', () => { | ||
| const siteWithRecentStats = { | ||
| ...DEFAULT_SITE, | ||
| statsBegin: formatISO(yesterday) | ||
| } | ||
| expect( | ||
| getStaleTime({ | ||
| ...base, | ||
| site: siteWithRecentStats, | ||
| period: DashboardPeriod.all | ||
| }) | ||
| ).toBe(CACHE_TTL_SHORT_ONGOING) | ||
| }) | ||
|
|
||
| it('for a historical period when comparison includes today', () => { | ||
| expect( | ||
| getStaleTime({ | ||
| ...base, | ||
| period: DashboardPeriod['28d'], | ||
| date: today, | ||
| comparison: ComparisonMode.custom, | ||
| compare_from: yesterday.subtract(28, 'day'), | ||
| compare_to: today | ||
| }) | ||
| ).toBe(CACHE_TTL_SHORT_ONGOING) | ||
| }) | ||
| }) | ||
|
|
||
| describe('ongoing periods with long TTL (only week or month interval available)', () => { | ||
| it('for custom period over 12 months ending today', () => { | ||
| expect( | ||
| getStaleTime({ | ||
| ...base, | ||
| period: DashboardPeriod.custom, | ||
| from: today.subtract(13, 'month'), | ||
| to: today | ||
| }) | ||
| ).toBe(CACHE_TTL_LONG_ONGOING) | ||
| }) | ||
|
|
||
| it('for all time period when stats begin over 12 months ago', () => { | ||
| const siteWithOldStats = { ...DEFAULT_SITE, statsBegin: '2020-01-01' } | ||
| expect( | ||
| getStaleTime({ | ||
| ...base, | ||
| site: siteWithOldStats, | ||
| period: DashboardPeriod.all | ||
| }) | ||
| ).toBe(CACHE_TTL_LONG_ONGOING) | ||
| }) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,12 +6,23 @@ import { | |
| } from '@tanstack/react-query' | ||
| import * as api from '../api' | ||
| import { DashboardState } from '../dashboard-state' | ||
| import { DashboardPeriod } from '../dashboard-time-periods' | ||
| import { Dayjs } from 'dayjs' | ||
| import { | ||
| DashboardPeriod, | ||
| DashboardTimeSettings, | ||
| isHistoricalPeriod | ||
| } from '../dashboard-time-periods' | ||
| import { REALTIME_UPDATE_TIME_MS } from '../util/realtime-update-timer' | ||
| import { | ||
| GetIntervalProps, | ||
| Interval, | ||
| validIntervals | ||
| } from '../stats/graph/intervals' | ||
|
|
||
| // defines when queries that don't include the current time should be refetched | ||
| const HISTORICAL_RESPONSES_STALE_TIME_MS = 12 * 60 * 60 * 1000 | ||
| // define (in ms) when query API responses should become stale | ||
| export const CACHE_TTL_REALTIME = REALTIME_UPDATE_TIME_MS | ||
| export const CACHE_TTL_SHORT_ONGOING = 5 * 60 * 1000 // 5 minutes | ||
| export const CACHE_TTL_LONG_ONGOING = 60 * 60 * 1000 // 1 hour | ||
| export const CACHE_TTL_HISTORICAL = 12 * 60 * 60 * 1000 // 12 hours | ||
|
|
||
| // how many items per page for breakdown modals | ||
| const PAGINATION_LIMIT = 100 | ||
|
|
@@ -34,12 +45,14 @@ export function usePaginatedGetAPI< | |
| TResponse extends { results: unknown[] }, | ||
| TKey extends PaginatedQueryKeyBase = PaginatedQueryKeyBase | ||
| >({ | ||
| site, | ||
| key, | ||
| getRequestParams, | ||
| afterFetchData, | ||
| afterFetchNextPage, | ||
| initialPageParam = 1 | ||
| }: { | ||
| site: DashboardTimeSettings['site'] | ||
| key: TKey | ||
| getRequestParams: GetRequestParams<TKey> | ||
| afterFetchData?: (response: TResponse) => void | ||
|
|
@@ -89,6 +102,10 @@ export function usePaginatedGetAPI< | |
| ? lastPageIndex + 1 | ||
| : null | ||
| }, | ||
| staleTime: ({ queryKey }) => { | ||
| const [_, opts] = queryKey | ||
| return getStaleTime({ site, ...opts.dashboardState }) | ||
| }, | ||
| initialPageParam, | ||
| placeholderData: (previousData) => previousData | ||
| }) | ||
|
|
@@ -108,59 +125,40 @@ export const cleanToPageOne = < | |
| return data | ||
| } | ||
|
|
||
| export const getStaleTime = ( | ||
| /** the start of the current day */ | ||
| startOfDay: Dayjs, | ||
| { | ||
| period, | ||
| from, | ||
| to, | ||
| date | ||
| }: Pick<DashboardState, 'period' | 'from' | 'to' | 'date'> | ||
| ): number => { | ||
| if (DashboardPeriod.custom && to && from) { | ||
| // historical | ||
| if (from.isBefore(startOfDay) && to.isBefore(startOfDay)) { | ||
| return HISTORICAL_RESPONSES_STALE_TIME_MS | ||
| } | ||
| // period includes now | ||
| if (to.diff(from, 'days') < 7) { | ||
| return 5 * 60 * 1000 | ||
| } | ||
| if (to.diff(from, 'months') < 1) { | ||
| return 15 * 60 * 1000 | ||
| } | ||
| if (to.diff(from, 'months') < 12) { | ||
| return 60 * 60 * 1000 | ||
| } | ||
| return 3 * 60 * 60 * 1000 | ||
| /** | ||
| * Returns the time-to-live for cached query API responses based on the given DashboardTimeSettings. | ||
| * | ||
| * - For a realtime dashboard: {@link CACHE_TTL_REALTIME} | ||
| * - For any historical period (i.e. does not include today): {@link CACHE_TTL_HISTORICAL} | ||
| * - For a period that includes today, supporting 'day' or shorter interval: {@link CACHE_TTL_SHORT_ONGOING} | ||
| * - For a period that includes today, too long to support 'day' interval: {@link CACHE_TTL_LONG_ONGOING} | ||
| */ | ||
| export const getStaleTime = (props: DashboardTimeSettings): number => { | ||
| if ( | ||
| [DashboardPeriod.realtime, DashboardPeriod.realtime_30m].includes( | ||
| props.period | ||
| ) | ||
| ) { | ||
| return CACHE_TTL_REALTIME | ||
| } | ||
|
|
||
| const historical = date?.isBefore(startOfDay) | ||
| if (historical) { | ||
| return HISTORICAL_RESPONSES_STALE_TIME_MS | ||
| if (isHistoricalPeriod(props)) { | ||
| return CACHE_TTL_HISTORICAL | ||
| } | ||
|
|
||
| switch (period) { | ||
| case DashboardPeriod.realtime: | ||
| return REALTIME_UPDATE_TIME_MS | ||
| case DashboardPeriod['24h']: | ||
| case DashboardPeriod.day: | ||
| return 5 * 60 * 1000 | ||
| case DashboardPeriod['7d']: | ||
| return 15 * 60 * 1000 | ||
| case DashboardPeriod['28d']: | ||
| case DashboardPeriod['30d']: | ||
| case DashboardPeriod['91d']: | ||
| case DashboardPeriod['6mo']: | ||
| return 60 * 60 * 1000 | ||
| case DashboardPeriod['12mo']: | ||
| case DashboardPeriod.year: | ||
| return 3 * 60 * 60 * 1000 | ||
| case DashboardPeriod.all: | ||
| default: | ||
| // err on the side of less caching, | ||
| // to avoid the user refresheshing | ||
| return 15 * 60 * 1000 | ||
| const availableIntervals = validIntervals( | ||
| Object.fromEntries( | ||
| Object.entries(props).filter(([k, _v]) => k !== 'date') | ||
| ) as GetIntervalProps | ||
| ) | ||
|
Comment on lines
+149
to
+153
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue, blocking: The cleaner way to exclude |
||
|
|
||
| if ( | ||
| availableIntervals.includes(Interval.day) || | ||
| availableIntervals.includes(Interval.hour) || | ||
| availableIntervals.includes(Interval.minute) | ||
| ) { | ||
| return CACHE_TTL_SHORT_ONGOING | ||
| } else { | ||
| return CACHE_TTL_LONG_ONGOING | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,9 +21,10 @@ export function GraphIntervalProvider({ | |
| }) { | ||
| const site = useSiteContext() | ||
| const { dashboardState } = useDashboardStateContext() | ||
| const intervalStorageKey = `interval__${dashboardState.period}__${site.domain}` | ||
|
|
||
| const { selectedInterval, onIntervalClick, availableIntervals } = | ||
| useStoredInterval({ | ||
| useStoredInterval(intervalStorageKey, { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question, non-blocking: Why can't the hook contain the logic to derive the storage key? Seems totally within its limited responsibilities. |
||
| site, | ||
| to: dashboardState.to, | ||
| from: dashboardState.from, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion, non-blocking: If we refactor
getStaleTimenot to depend on arg namedsitebut directly on the two properties from the site, it makes the test depend on less things. That way, we'd not need to import or fake a full site here, just the propertiesoffsetandstatsBegin.