Skip to content

feat(ourlogs): Add auto-refresh to logs #94887

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

Merged
merged 3 commits into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions static/app/components/searchQueryBuilder/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,29 @@ export function SearchQueryBuilderProvider({
disabled,
});

const stableFieldDefinitionGetter = useMemo(
() => fieldDefinitionGetter,
[fieldDefinitionGetter]
);

const stableFilterKeys = useMemo(() => filterKeys, [filterKeys]);

const stableGetSuggestedFilterKey = useCallback(
(key: string) => {
return getSuggestedFilterKey ? getSuggestedFilterKey(key) : key;
},
[getSuggestedFilterKey]
);

const parseQuery = useCallback(
(query: string) =>
parseQueryBuilderValue(query, fieldDefinitionGetter, {
parseQueryBuilderValue(query, stableFieldDefinitionGetter, {
getFilterTokenWarning,
disallowFreeText,
disallowLogicalOperators,
disallowUnsupportedFilters,
disallowWildcard,
filterKeys,
filterKeys: stableFilterKeys,
invalidMessages,
filterKeyAliases,
}),
Expand All @@ -121,8 +135,8 @@ export function SearchQueryBuilderProvider({
disallowLogicalOperators,
disallowUnsupportedFilters,
disallowWildcard,
fieldDefinitionGetter,
filterKeys,
stableFieldDefinitionGetter,
stableFilterKeys,
getFilterTokenWarning,
invalidMessages,
filterKeyAliases,
Expand Down Expand Up @@ -150,10 +164,10 @@ export function SearchQueryBuilderProvider({
parsedQuery,
filterKeySections: filterKeySections ?? [],
filterKeyMenuWidth,
filterKeys,
getSuggestedFilterKey: getSuggestedFilterKey ?? ((key: string) => key),
filterKeys: stableFilterKeys,
getSuggestedFilterKey: stableGetSuggestedFilterKey,
getTagValues,
getFieldDefinition: fieldDefinitionGetter,
getFieldDefinition: stableFieldDefinitionGetter,
dispatch,
wrapperRef,
actionBarRef,
Expand All @@ -177,10 +191,10 @@ export function SearchQueryBuilderProvider({
parsedQuery,
filterKeySections,
filterKeyMenuWidth,
filterKeys,
getSuggestedFilterKey,
stableFilterKeys,
stableGetSuggestedFilterKey,
getTagValues,
fieldDefinitionGetter,
stableFieldDefinitionGetter,
dispatch,
handleSearch,
placeholder,
Expand Down
5 changes: 5 additions & 0 deletions static/app/utils/timeSeries/markDelayedData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Given a timeseries and a delay in seconds, goes through the timeseries data, and marks each point as either delayed (data bucket ended before the delay threshold) or not
*/

import {defined} from 'sentry/utils';
import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types';

export function markDelayedData(timeSeries: TimeSeries, delay: number): TimeSeries {
Expand All @@ -22,6 +23,10 @@ export function markDelayedData(timeSeries: TimeSeries, delay: number): TimeSeri
const bucketEndTimestamp = new Date(datum.timestamp).getTime() + bucketSize;
const delayed = bucketEndTimestamp >= ingestionDelayTimestamp;

if (defined(datum.incomplete)) {
return datum;
}

if (!delayed) {
return datum;
}
Expand Down
20 changes: 17 additions & 3 deletions static/app/views/explore/contexts/logs/logsPageParams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const [_LogsPageParamsProvider, _useLogsPageParams, LogsPageParamsContext] =
interface LogsPageParamsProviderProps {
analyticsPageSource: LogsAnalyticsPageSource;
children: React.ReactNode;
_testContext?: Partial<LogsPageParams>;
blockRowExpanding?: boolean;
isTableFrozen?: boolean;
limitToProjectIds?: number[];
Expand All @@ -128,6 +129,7 @@ export function LogsPageParamsProvider({
blockRowExpanding,
isTableFrozen,
analyticsPageSource,
_testContext,
}: LogsPageParamsProviderProps) {
const location = useLocation();
const logsQuery = decodeLogsQuery(location);
Expand Down Expand Up @@ -207,6 +209,7 @@ export function LogsPageParamsProvider({
groupBy,
aggregateFn,
aggregateParam,
..._testContext,
}}
>
{children}
Expand Down Expand Up @@ -237,7 +240,15 @@ function setLogsPageParams(location: Location, pageParams: LogPageParamsUpdate)
updateNullableLocation(target, LOGS_AGGREGATE_PARAM_KEY, pageParams.aggregateParam);
if (!pageParams.isTableFrozen) {
updateLocationWithLogSortBys(target, pageParams.sortBys);
updateLocationWithAggregateSortBys(target, pageParams.aggregateSortBys);
if (
pageParams.sortBys ||
pageParams.search ||
pageParams.aggregateFn ||
pageParams.aggregateParam ||
pageParams.groupBy
) {
updateLocationWithAggregateSortBys(target, pageParams.aggregateSortBys);
}
if (pageParams.sortBys || pageParams.aggregateSortBys || pageParams.search) {
// make sure to clear the cursor every time the query is updated
delete target.query[LOGS_CURSOR_KEY];
Expand Down Expand Up @@ -511,14 +522,17 @@ export function useLogsAutoRefresh() {

export function useSetLogsAutoRefresh() {
const setPageParams = useSetLogsPageParams();
const {queryKey} = useLogsQueryKeyWithInfinite({referrer: 'api.explore.logs-table'});
const {queryKey} = useLogsQueryKeyWithInfinite({
referrer: 'api.explore.logs-table',
autoRefresh: false,
});
const queryClient = useQueryClient();
return useCallback(
(autoRefresh: boolean) => {
setPageParams({autoRefresh});
if (autoRefresh) {
queryClient.removeQueries({queryKey});
}
setPageParams({autoRefresh});
},
[setPageParams, queryClient, queryKey]
);
Expand Down
7 changes: 5 additions & 2 deletions static/app/views/explore/logs/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export const LogAttributesHumanLabel: Partial<Record<OurLogFieldKey, string>> =
[OurLogKnownFieldKey.TRACE_ID]: t('Trace'),
};

export const LOG_INGEST_DELAY = 10_000;
export const MAX_LOG_INGEST_DELAY = 40_000;
export const MAX_LOG_INGEST_QUERY_DELAY = 15_000;
export const QUERY_PAGE_LIMIT = 5000; // If this does not equal the limit with auto-refresh, the query keys will diverge and they will have separate caches. We may want to make this change in the future.
export const QUERY_PAGE_LIMIT_WITH_AUTO_REFRESH = 5000;

/**
* These are required fields are always added to the query when fetching the log table.
Expand Down Expand Up @@ -66,6 +69,6 @@ export const LOGS_INSTRUCTIONS_URL =

export const LOGS_FILTER_KEY_SECTIONS: FilterKeySection[] = [LOGS_FILTERS];

export const VIRTUAL_STREAMED_INTERVAL_MS = 333;
export const VIRTUAL_STREAMED_INTERVAL_MS = 250;

export const LOGS_GRID_SCROLL_MIN_ITEM_THRESHOLD = 100; // Items from bottom of table to trigger table fetch.
102 changes: 102 additions & 0 deletions static/app/views/explore/logs/logsAutoRefresh.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,106 @@ describe('LogsAutoRefresh', () => {
expect(mockApi).toHaveBeenCalledTimes(5);
});
});

it('disables auto-refresh after 3 consecutive requests with more data', async () => {
const mockApi = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events/`,
method: 'GET',
body: {data: mockLogsData},
headers: {
Link: '<http://localhost/api/0/organizations/org-slug/events/?cursor=0:5000:0>; rel="next"; results="true"',
},
});

jest.spyOn(logsPageParams, 'useLogsAutoRefresh').mockReturnValue(true);
jest.spyOn(logsPageParams, 'useLogsRefreshInterval').mockReturnValue(1); // Faster interval for testing

renderWithProviders(<AutorefreshToggle />);

const toggleSwitch = screen.getByRole('checkbox', {name: 'Auto-refresh'});
expect(toggleSwitch).toBeChecked();

await waitFor(() => {
expect(mockApi).toHaveBeenCalledTimes(3);
});

// After 3 consecutive requests with more data, auto-refresh should be disabled
await waitFor(() => {
expect(setAutoRefresh).toHaveBeenCalledWith(false);
});
});

it('continues auto-refresh when there is no more data', async () => {
const mockApi = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events/`,
method: 'GET',
body: {data: mockLogsData},
headers: {
Link: '', // No Link header means no more data
},
});

jest.spyOn(logsPageParams, 'useLogsAutoRefresh').mockReturnValue(true);
jest.spyOn(logsPageParams, 'useLogsRefreshInterval').mockReturnValue(1); // Faster interval for testing

renderWithProviders(<AutorefreshToggle />);

const toggleSwitch = screen.getByRole('checkbox', {name: 'Auto-refresh'});
expect(toggleSwitch).toBeChecked();

await waitFor(() => {
expect(mockApi).toHaveBeenCalledTimes(5);
});

// Auto-refresh should NOT be disabled
expect(setAutoRefresh).not.toHaveBeenCalledWith(false);
});

it('disables auto-refresh when API request fails', async () => {
const initialMock = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events/`,
method: 'GET',
body: {data: mockLogsData},
});

jest.spyOn(logsPageParams, 'useLogsAutoRefresh').mockReturnValue(true);
jest.spyOn(logsPageParams, 'useLogsRefreshInterval').mockReturnValue(1); // Faster interval for testing

renderWithProviders(<AutorefreshToggle />);

const toggleSwitch = screen.getByRole('checkbox', {name: 'Auto-refresh'});
expect(toggleSwitch).toBeChecked();

await waitFor(() => {
expect(initialMock).toHaveBeenCalled();
});

expect(setAutoRefresh).not.toHaveBeenCalled();

initialMock.mockClear();
const errorMock = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events/`,
method: 'GET',
statusCode: 500,
body: {
detail: 'Internal Server Error',
},
});

await waitFor(() => {
expect(errorMock).toHaveBeenCalled();
});

await waitFor(() => {
expect(setAutoRefresh).toHaveBeenCalledWith(false);
});

await userEvent.hover(toggleSwitch);

expect(
await screen.findByText(
'Auto-refresh was disabled due to an error fetching logs. If the issue persists, please contact support.'
)
).toBeInTheDocument();
});
});
Loading
Loading