Skip to content

Commit 66b630a

Browse files
authored
fix: treat stale experiment refresh requests as settled (#8347)
1 parent 1ad8487 commit 66b630a

4 files changed

Lines changed: 56 additions & 2 deletions

File tree

frontend/web/components/experiments/results/__tests__/exposuresViewState.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ const loaded = exposures({
2424
})
2525

2626
describe('deriveExposuresViewState', () => {
27+
beforeAll(() => {
28+
jest.useFakeTimers().setSystemTime(new Date('2026-06-12T11:05:00Z'))
29+
})
30+
afterAll(() => {
31+
jest.useRealTimers()
32+
})
33+
2734
it('is empty when there is no payload and nothing in flight', () => {
2835
expect(deriveExposuresViewState(exposures()).kind).toBe('empty')
2936
})
@@ -56,6 +63,14 @@ describe('deriveExposuresViewState', () => {
5663
})
5764
expect(state.kind).toBe('refreshing')
5865
})
66+
67+
it('ignores a refresh request older than the cutoff', () => {
68+
const state = deriveExposuresViewState({
69+
...loaded,
70+
refresh_requested_at: '2026-06-12T10:54:00Z',
71+
})
72+
expect(state.kind).toBe('loaded')
73+
})
5974
})
6075

6176
describe('canRefreshExposures', () => {

frontend/web/components/experiments/results/__tests__/resultsViewState.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ const loaded = results({
2525
})
2626

2727
describe('deriveResultsViewState', () => {
28+
beforeAll(() => {
29+
jest.useFakeTimers().setSystemTime(new Date('2026-06-12T11:05:00Z'))
30+
})
31+
afterAll(() => {
32+
jest.useRealTimers()
33+
})
34+
2835
it('is empty when there is no payload and nothing in flight', () => {
2936
expect(deriveResultsViewState(results()).kind).toBe('empty')
3037
})
@@ -57,6 +64,28 @@ describe('deriveResultsViewState', () => {
5764
})
5865
expect(state.kind).toBe('refreshing')
5966
})
67+
68+
const staleCases: [string, Partial<ExperimentBayesianResults>, string][] = [
69+
['loaded', loaded, 'loaded'],
70+
['empty', results(), 'empty'],
71+
]
72+
staleCases.forEach(([name, base, expected]) => {
73+
it(`ignores a refresh request older than the cutoff (${name})`, () => {
74+
const state = deriveResultsViewState({
75+
...results(base),
76+
refresh_requested_at: '2026-06-12T10:54:00Z',
77+
})
78+
expect(state.kind).toBe(expected)
79+
})
80+
})
81+
82+
it('still refreshes just inside the cutoff', () => {
83+
const state = deriveResultsViewState({
84+
...loaded,
85+
refresh_requested_at: '2026-06-12T10:56:00Z',
86+
})
87+
expect(state.kind).toBe('refreshing')
88+
})
6089
})
6190

6291
describe('canRefreshResults', () => {

frontend/web/components/experiments/results/exposuresViewState.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ export type RefreshLabel = { message: string; tone: 'muted' | 'danger' }
1818
export const REFRESH_POLL_INTERVAL_MS = 10000
1919
export const POLL_TIMEOUT_MS = 120000
2020
export const DEFAULT_RETRY_AFTER_S = 300
21+
export const REFRESH_STALE_CUTOFF_MS = 10 * 60 * 1000
2122

2223
const ms = (iso: string | null): number => (iso ? new Date(iso).getTime() : 0)
2324

2425
const isRefreshing = (e: ExperimentExposures): boolean => {
2526
const requested = ms(e.refresh_requested_at)
26-
return requested > 0 && requested > Math.max(ms(e.as_of), ms(e.last_error_at))
27+
return (
28+
requested > 0 &&
29+
requested > Math.max(ms(e.as_of), ms(e.last_error_at)) &&
30+
Date.now() - requested < REFRESH_STALE_CUTOFF_MS
31+
)
2732
}
2833

2934
const hasError = (e: ExperimentExposures): boolean =>

frontend/web/components/experiments/results/resultsViewState.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@ export type RefreshLabel = { message: string; tone: 'muted' | 'danger' }
2121
export const REFRESH_POLL_INTERVAL_MS = 10000
2222
export const POLL_TIMEOUT_MS = 120000
2323
export const DEFAULT_RETRY_AFTER_S = 300
24+
export const REFRESH_STALE_CUTOFF_MS = 10 * 60 * 1000
2425

2526
const ms = (iso: string | null): number => (iso ? new Date(iso).getTime() : 0)
2627

2728
const isRefreshing = (r: ExperimentBayesianResults): boolean => {
2829
const requested = ms(r.refresh_requested_at)
29-
return requested > 0 && requested > Math.max(ms(r.as_of), ms(r.last_error_at))
30+
return (
31+
requested > 0 &&
32+
requested > Math.max(ms(r.as_of), ms(r.last_error_at)) &&
33+
Date.now() - requested < REFRESH_STALE_CUTOFF_MS
34+
)
3035
}
3136

3237
const hasError = (r: ExperimentBayesianResults): boolean =>

0 commit comments

Comments
 (0)