Skip to content

Commit 21c9af1

Browse files
k-fishandrewshie-sentry
authored andcommitted
ref(ourlogs): Refactor autorefresh state into separate context (#96056)
### Summary This refactor autorefresh state into it's own provider, simplifying into a string union state, and moves the setInterval logic into a hook. #### Other - This also fixes a bug with the groupBy on single axis charts cause updates into an 'empty' timeseries. - This also removes a double delay condition on the initial page being queried.
1 parent 0c7359c commit 21c9af1

14 files changed

+918
-541
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {useCallback} from 'react';
2+
import type {Location} from 'history';
3+
4+
import {createDefinedContext} from 'sentry/utils/performance/contexts/utils';
5+
import {useQueryClient} from 'sentry/utils/queryClient';
6+
import {decodeInteger, decodeScalar} from 'sentry/utils/queryString';
7+
import {useLocation} from 'sentry/utils/useLocation';
8+
import {useNavigate} from 'sentry/utils/useNavigate';
9+
import {useLogsQueryKeyWithInfinite} from 'sentry/views/explore/logs/useLogsQuery';
10+
11+
export const LOGS_AUTO_REFRESH_KEY = 'live';
12+
export const LOGS_REFRESH_INTERVAL_KEY = 'refreshEvery';
13+
const LOGS_REFRESH_INTERVAL_DEFAULT = 5000;
14+
15+
export const ABSOLUTE_MAX_AUTO_REFRESH_TIME_MS = 10 * 60 * 1000; // 10 minutes
16+
export const CONSECUTIVE_PAGES_WITH_MORE_DATA = 5;
17+
18+
// These don't include the pre-flight conditions (wrong sort, absolute time, aggregates view, or initial rate limit) which are handled inside the toggle.
19+
export type AutoRefreshState =
20+
| 'enabled'
21+
| 'timeout' // Hit 10 minute limit
22+
| 'rate_limit' // Too much data during refresh
23+
| 'error' // Fetch error
24+
| 'idle'; // Default (inactive ) state. Should never appear in query params.
25+
26+
interface LogsAutoRefreshContextValue {
27+
autoRefresh: AutoRefreshState;
28+
isTableFrozen: boolean | undefined;
29+
refreshInterval: number;
30+
}
31+
32+
const [_LogsAutoRefreshProvider, useLogsAutoRefresh, LogsAutoRefreshContext] =
33+
createDefinedContext<LogsAutoRefreshContextValue>({
34+
name: 'LogsAutoRefreshContext',
35+
});
36+
37+
export {useLogsAutoRefresh};
38+
39+
interface LogsAutoRefreshProviderProps {
40+
children: React.ReactNode;
41+
_testContext?: Partial<LogsAutoRefreshContextValue>;
42+
isTableFrozen?: boolean;
43+
}
44+
45+
export function LogsAutoRefreshProvider({
46+
children,
47+
isTableFrozen,
48+
_testContext,
49+
}: LogsAutoRefreshProviderProps) {
50+
const location = useLocation();
51+
52+
const autoRefreshRaw = decodeScalar(location.query[LOGS_AUTO_REFRESH_KEY]);
53+
const autoRefresh: AutoRefreshState = (
54+
autoRefreshRaw &&
55+
['enabled', 'timeout', 'rate_limit', 'error'].includes(autoRefreshRaw)
56+
? autoRefreshRaw
57+
: 'idle'
58+
) as AutoRefreshState;
59+
60+
const refreshInterval = decodeInteger(
61+
location.query[LOGS_REFRESH_INTERVAL_KEY],
62+
LOGS_REFRESH_INTERVAL_DEFAULT
63+
);
64+
65+
return (
66+
<LogsAutoRefreshContext
67+
value={{
68+
autoRefresh,
69+
refreshInterval,
70+
isTableFrozen,
71+
..._testContext,
72+
}}
73+
>
74+
{children}
75+
</LogsAutoRefreshContext>
76+
);
77+
}
78+
79+
/**
80+
* This only checks if the autoRefresh state is 'enabled'. It does NOT check
81+
* pre-flight conditions (wrong sort, absolute time, aggregates view, or initial rate limit).
82+
* Those conditions are handled separately in the UI to prevent enabling autorefresh.
83+
*/
84+
export function useLogsAutoRefreshEnabled() {
85+
const {autoRefresh, isTableFrozen} = useLogsAutoRefresh();
86+
return isTableFrozen ? false : autoRefresh === 'enabled';
87+
}
88+
89+
export function useSetLogsAutoRefresh() {
90+
const location = useLocation();
91+
const navigate = useNavigate();
92+
const {queryKey} = useLogsQueryKeyWithInfinite({
93+
referrer: 'api.explore.logs-table',
94+
autoRefresh: true,
95+
});
96+
const queryClient = useQueryClient();
97+
98+
return useCallback(
99+
(autoRefresh: AutoRefreshState) => {
100+
if (autoRefresh === 'enabled') {
101+
queryClient.removeQueries({queryKey});
102+
}
103+
104+
const target: Location = {...location, query: {...location.query}};
105+
if (autoRefresh === 'idle') {
106+
delete target.query[LOGS_AUTO_REFRESH_KEY];
107+
} else {
108+
target.query[LOGS_AUTO_REFRESH_KEY] = autoRefresh;
109+
}
110+
navigate(target);
111+
},
112+
[navigate, location, queryClient, queryKey]
113+
);
114+
}
115+
116+
export function useLogsRefreshInterval() {
117+
const {refreshInterval} = useLogsAutoRefresh();
118+
return refreshInterval;
119+
}

static/app/views/explore/contexts/logs/logsPageParams.tsx

Lines changed: 28 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import type {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalytics
77
import type {Sort} from 'sentry/utils/discover/fields';
88
import localStorage from 'sentry/utils/localStorage';
99
import {createDefinedContext} from 'sentry/utils/performance/contexts/utils';
10-
import {useQueryClient} from 'sentry/utils/queryClient';
11-
import {decodeBoolean, decodeInteger, decodeScalar} from 'sentry/utils/queryString';
10+
import {decodeScalar} from 'sentry/utils/queryString';
1211
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
1312
import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
1413
import {useLocation} from 'sentry/utils/useLocation';
@@ -18,6 +17,11 @@ import {
1817
defaultLogFields,
1918
getLogFieldsFromLocation,
2019
} from 'sentry/views/explore/contexts/logs/fields';
20+
import {
21+
type AutoRefreshState,
22+
LOGS_AUTO_REFRESH_KEY,
23+
LogsAutoRefreshProvider,
24+
} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext';
2125
import {
2226
getLogAggregateSortBysFromLocation,
2327
getLogSortBysFromLocation,
@@ -31,7 +35,6 @@ import {
3135
updateLocationWithMode,
3236
} from 'sentry/views/explore/contexts/pageParamsContext/mode';
3337
import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types';
34-
import {useLogsQueryKeyWithInfinite} from 'sentry/views/explore/logs/useLogsQuery';
3538

3639
const LOGS_PARAMS_VERSION = 2;
3740
export const LOGS_QUERY_KEY = 'logsQuery'; // Logs may exist on other pages.
@@ -42,23 +45,17 @@ export const LOGS_AGGREGATE_FN_KEY = 'logsAggregate'; // e.g., p99
4245
export const LOGS_AGGREGATE_PARAM_KEY = 'logsAggregateParam'; // e.g., message.parameters.0
4346
export const LOGS_GROUP_BY_KEY = 'logsGroupBy'; // e.g., message.template
4447

45-
const LOGS_AUTO_REFRESH_KEY = 'live';
46-
const LOGS_REFRESH_INTERVAL_KEY = 'refreshEvery';
47-
const LOGS_REFRESH_INTERVAL_DEFAULT = 5000;
48-
4948
interface LogsPageParams {
5049
/** In the 'aggregates' table, if you GROUP BY, there can be many rows. This is the cursor for pagination there. */
5150
readonly aggregateCursor: string;
5251
/** In the 'aggregates' table, if you GROUP BY, there can be many rows. This is the 'sort by' for that table. */
5352
readonly aggregateSortBys: Sort[];
5453
readonly analyticsPageSource: LogsAnalyticsPageSource;
55-
readonly autoRefresh: boolean;
5654
readonly blockRowExpanding: boolean | undefined;
5755
readonly cursor: string;
5856
readonly fields: string[];
5957
readonly isTableFrozen: boolean | undefined;
6058
readonly mode: Mode;
61-
readonly refreshInterval: number;
6259
readonly search: MutableSearch;
6360
/**
6461
* See setSearchForFrozenPages
@@ -119,7 +116,10 @@ const [_LogsPageParamsProvider, _useLogsPageParams, LogsPageParamsContext] =
119116
interface LogsPageParamsProviderProps {
120117
analyticsPageSource: LogsAnalyticsPageSource;
121118
children: React.ReactNode;
122-
_testContext?: Partial<LogsPageParams>;
119+
_testContext?: Partial<LogsPageParams> & {
120+
autoRefresh?: AutoRefreshState;
121+
refreshInterval?: number;
122+
};
123123
blockRowExpanding?: boolean;
124124
isTableFrozen?: boolean;
125125
limitToProjectIds?: number[];
@@ -140,12 +140,6 @@ export function LogsPageParamsProvider({
140140
const location = useLocation();
141141
const logsQuery = decodeLogsQuery(location);
142142

143-
const autoRefresh = decodeBoolean(location.query[LOGS_AUTO_REFRESH_KEY]) ?? false;
144-
const refreshInterval = decodeInteger(
145-
location.query[LOGS_REFRESH_INTERVAL_KEY],
146-
LOGS_REFRESH_INTERVAL_DEFAULT
147-
);
148-
149143
// on embedded pages with search bars, use a useState instead of a URL parameter
150144
const [searchForFrozenPages, setSearchForFrozenPages] = useState(new MutableSearch(''));
151145
const [cursorForFrozenPages, setCursorForFrozenPages] = useState('');
@@ -206,8 +200,6 @@ export function LogsPageParamsProvider({
206200
cursor,
207201
setCursorForFrozenPages,
208202
isTableFrozen,
209-
autoRefresh,
210-
refreshInterval,
211203
blockRowExpanding,
212204
baseSearch,
213205
projectIds,
@@ -220,7 +212,9 @@ export function LogsPageParamsProvider({
220212
..._testContext,
221213
}}
222214
>
223-
{children}
215+
<LogsAutoRefreshProvider isTableFrozen={isTableFrozen} _testContext={_testContext}>
216+
{children}
217+
</LogsAutoRefreshProvider>
224218
</LogsPageParamsContext>
225219
);
226220
}
@@ -250,16 +244,29 @@ function setLogsPageParams(location: Location, pageParams: LogPageParamsUpdate)
250244
if (!pageParams.isTableFrozen) {
251245
updateLocationWithLogSortBys(target, pageParams.sortBys);
252246
updateLocationWithAggregateSortBys(target, pageParams.aggregateSortBys);
253-
if (pageParams.sortBys || pageParams.aggregateSortBys || pageParams.search) {
247+
248+
// Only update cursors if table isn't frozen, frozen is for embedded views where cursor is managed by state instead of url.
249+
if (shouldResetCursor(pageParams)) {
254250
// make sure to clear the cursor every time the query is updated
255251
delete target.query[LOGS_CURSOR_KEY];
256252
delete target.query[LOGS_AUTO_REFRESH_KEY];
257253
}
258254
}
259-
updateNullableLocation(target, LOGS_AUTO_REFRESH_KEY, pageParams.autoRefresh);
260255
return target;
261256
}
262257

258+
function shouldResetCursor(pageParams: LogPageParamsUpdate) {
259+
return (
260+
pageParams.hasOwnProperty('sortBys') ||
261+
pageParams.hasOwnProperty('aggregateSortBys') ||
262+
pageParams.hasOwnProperty('search') ||
263+
pageParams.hasOwnProperty('groupBy') ||
264+
pageParams.hasOwnProperty('fields') ||
265+
pageParams.hasOwnProperty('aggregateFn') ||
266+
pageParams.hasOwnProperty('aggregateParam')
267+
);
268+
}
269+
263270
/**
264271
* Allows updating a location field, removing it if the value is null.
265272
*
@@ -530,33 +537,3 @@ function getLogsParamsStorageKey(version: number) {
530537
function getPastLogsParamsStorageKey(version: number) {
531538
return `logs-params-v${version - 1}`;
532539
}
533-
534-
export function useLogsAutoRefresh() {
535-
const {autoRefresh, isTableFrozen} = useLogsPageParams();
536-
return isTableFrozen ? false : autoRefresh;
537-
}
538-
539-
export function useSetLogsAutoRefresh() {
540-
const setPageParams = useSetLogsPageParams();
541-
const {queryKey} = useLogsQueryKeyWithInfinite({
542-
referrer: 'api.explore.logs-table',
543-
autoRefresh: false,
544-
});
545-
const queryClient = useQueryClient();
546-
return useCallback(
547-
(autoRefresh: boolean) => {
548-
if (autoRefresh) {
549-
queryClient.removeQueries({queryKey});
550-
// Until we change our timeseries hooks to be build their query keys separately, we need to remove the query via the route.
551-
queryClient.removeQueries({queryKey: ['events-stats']});
552-
}
553-
setPageParams({autoRefresh});
554-
},
555-
[setPageParams, queryClient, queryKey]
556-
);
557-
}
558-
559-
export function useLogsRefreshInterval() {
560-
const {refreshInterval} = useLogsPageParams();
561-
return refreshInterval;
562-
}

0 commit comments

Comments
 (0)