diff --git a/static/app/utils/analytics/logsAnalyticsEvent.tsx b/static/app/utils/analytics/logsAnalyticsEvent.tsx
index 4b6ddce0dd8d36..41f387231f70aa 100644
--- a/static/app/utils/analytics/logsAnalyticsEvent.tsx
+++ b/static/app/utils/analytics/logsAnalyticsEvent.tsx
@@ -12,9 +12,10 @@ export type LogsAnalyticsEventParameters = {
page_source: LogsAnalyticsPageSource;
};
'logs.auto_refresh.toggled': {
- enabled: boolean;
+ fromPaused: boolean;
organization: Organization;
page_source: LogsAnalyticsPageSource;
+ toggleState: 'enabled' | 'disabled';
};
'logs.doc_link.clicked': {
organization: Organization;
diff --git a/static/app/views/explore/contexts/logs/logsAutoRefreshContext.tsx b/static/app/views/explore/contexts/logs/logsAutoRefreshContext.tsx
index f548e5f4bac424..2a7f450af9cbd8 100644
--- a/static/app/views/explore/contexts/logs/logsAutoRefreshContext.tsx
+++ b/static/app/views/explore/contexts/logs/logsAutoRefreshContext.tsx
@@ -11,7 +11,7 @@ import {useLogsQueryKeyWithInfinite} from 'sentry/views/explore/logs/useLogsQuer
export const LOGS_AUTO_REFRESH_KEY = 'live';
export const LOGS_REFRESH_INTERVAL_KEY = 'refreshEvery';
const LOGS_REFRESH_INTERVAL_DEFAULT = 5000;
-const MAX_AUTO_REFRESH_PAUSED_TIME_MS = 60 * 1000; // 60 seconds
+const MAX_AUTO_REFRESH_PAUSED_TIME_MS = 60 * 1000; // 10 seconds
export const ABSOLUTE_MAX_AUTO_REFRESH_TIME_MS = 10 * 60 * 1000; // 10 minutes
export const CONSECUTIVE_PAGES_WITH_MORE_DATA = 5;
@@ -102,6 +102,11 @@ export function useLogsAutoRefreshEnabled() {
return isTableFrozen ? false : autoRefresh === 'enabled';
}
+export function useLogsAutoRefreshContinued() {
+ const {autoRefresh, pausedAt} = useLogsAutoRefresh();
+ return autoRefresh === 'enabled' && pausedAtAllowedToContinue(pausedAt);
+}
+
export function useAutorefreshEnabledOrWithinPauseWindow() {
const {autoRefresh, pausedAt} = useLogsAutoRefresh();
return (
@@ -113,11 +118,14 @@ export function useAutorefreshEnabledOrWithinPauseWindow() {
function withinPauseWindow(autoRefresh: AutoRefreshState, pausedAt: number | undefined) {
return (
(autoRefresh === 'paused' || autoRefresh === 'enabled') &&
- pausedAt &&
- Date.now() - pausedAt < MAX_AUTO_REFRESH_PAUSED_TIME_MS
+ pausedAtAllowedToContinue(pausedAt)
);
}
+function pausedAtAllowedToContinue(pausedAt: number | undefined) {
+ return pausedAt && Date.now() - pausedAt < MAX_AUTO_REFRESH_PAUSED_TIME_MS;
+}
+
export function useSetLogsAutoRefresh() {
const location = useLocation();
const navigate = useNavigate();
@@ -139,6 +147,7 @@ export function useSetLogsAutoRefresh() {
if (autoRefresh === 'paused') {
setPausedAt(newPausedAt);
} else if (autoRefresh !== 'enabled') {
+ // Any error state, or disabled state, should reset the pause state.
setPausedAt(undefined);
}
diff --git a/static/app/views/explore/logs/content.spec.tsx b/static/app/views/explore/logs/content.spec.tsx
index 3de832c7fb4301..0006f51ba2cf1a 100644
--- a/static/app/views/explore/logs/content.spec.tsx
+++ b/static/app/views/explore/logs/content.spec.tsx
@@ -1,83 +1,27 @@
-import {LogFixture, LogFixtureMeta} from 'sentry-fixture/log';
+import {createLogFixtures, initializeLogsTest} from 'sentry-fixture/log';
-import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
-import PageFiltersStore from 'sentry/stores/pageFiltersStore';
-import ProjectsStore from 'sentry/stores/projectsStore';
import {LOGS_AUTO_REFRESH_KEY} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext';
-import type {TraceItemResponseAttribute} from 'sentry/views/explore/hooks/useTraceItemDetails';
-import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types';
import LogsPage from './content';
-const BASE_FEATURES = ['ourlogs-enabled', 'ourlogs-visualize-sidebar'];
-
describe('LogsPage', function () {
- const {organization, project} = initializeOrg({
- organization: {
- features: BASE_FEATURES,
- },
- });
+ const {organization, project, setupPageFilters, setupEventsMock} = initializeLogsTest();
- ProjectsStore.loadInitialData([project]);
- PageFiltersStore.init();
- PageFiltersStore.onInitializeUrlState(
- {
- projects: [project].map(p => parseInt(p.id, 10)),
- environments: [],
- datetime: {period: '12h', start: null, end: null, utc: null},
- },
- new Set()
- );
+ setupPageFilters();
let eventTableMock: jest.Mock;
let eventStatsMock: jest.Mock;
+
+ // Standard log fixtures for consistent testing
+ const testDate = new Date('2024-01-15T10:00:00.000Z');
+ const {baseFixtures} = createLogFixtures(organization, project, testDate);
+
beforeEach(function () {
- organization.features = BASE_FEATURES;
MockApiClient.clearMockResponses();
- eventTableMock = MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/events/`,
- method: 'GET',
- body: {
- data: [
- {
- 'sentry.item_id': '019621262d117e03bce898cb8f4f6ff7',
- 'project.id': 1,
- trace: '17cc0bae407042eaa4bf6d798c37d026',
- severity_number: 9,
- severity_text: 'info',
- timestamp: '2025-04-10T19:21:12+00:00',
- message: 'some log message1',
- 'tags[sentry.timestamp_precise,number]': 1.7443128722090732e18,
- },
- {
- 'sentry.item_id': '0196212624a17144aa392d01420256a2',
- 'project.id': 1,
- trace: 'c331c2df93d846f5a2134203416d40bb',
- severity_number: 9,
- severity_text: 'info',
- timestamp: '2025-04-10T19:21:10+00:00',
- message: 'some log message2',
- 'tags[sentry.timestamp_precise,number]': 1.744312870049196e18,
- },
- ],
- meta: {
- fields: {
- 'sentry.item_id': 'string',
- 'project.id': 'string',
- trace: 'string',
- severity_number: 'integer',
- severity_text: 'string',
- timestamp: 'string',
- message: 'string',
- 'tags[sentry.timestamp_precise,number]': 'number',
- },
- units: {},
- },
- },
- });
+ eventTableMock = setupEventsMock(baseFixtures.slice(0, 2));
eventStatsMock = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events-stats/`,
@@ -127,10 +71,12 @@ describe('LogsPage', function () {
});
const table = screen.getByTestId('logs-table');
- expect(await screen.findByText('some log message1')).toBeInTheDocument();
+ expect(
+ await screen.findByText('Error occurred in authentication service')
+ ).toBeInTheDocument();
expect(table).not.toHaveTextContent(/auto refresh/i);
- expect(table).toHaveTextContent(/some log message1/);
- expect(table).toHaveTextContent(/some log message2/);
+ expect(table).toHaveTextContent(/Error occurred in authentication service/);
+ expect(table).toHaveTextContent(/User login successful/);
});
it('should call aggregates APIs as expected', async function () {
@@ -190,193 +136,124 @@ describe('LogsPage', function () {
});
});
- it('enables autorefresh when Switch is clicked', async function () {
- const {organization: newOrganization} = initializeOrg({
- organization: {
- features: [...BASE_FEATURES, 'ourlogs-infinite-scroll', 'ourlogs-live-refresh'],
- },
+ describe('autorefresh flag enabled', () => {
+ const {
+ organization: autorefreshOrganization,
+ setupEventsMock: _setupEventsMock,
+ setupTraceItemsMock: _setupTraceItemsMock,
+ generateRouterConfig,
+ routerConfig: initialRouterConfig,
+ } = initializeLogsTest({
+ refreshInterval: '10000', // 10 seconds, should not first events multiple times.
+ liveRefresh: true,
});
- MockApiClient.addMockResponse({
- url: `/organizations/${newOrganization.slug}/events/`,
- method: 'GET',
- body: {
- data: [
- {
- 'sentry.item_id': '1',
- 'project.id': 1,
- trace: 'trace1',
- severity_number: 9,
- severity_text: 'info',
- timestamp: '2025-04-10T19:21:12+00:00',
- message: 'some log message',
- 'tags[sentry.timestamp_precise,number]': 100,
- },
- ],
- meta: {fields: {}, units: {}},
- },
- });
+ const {baseFixtures: autorefreshBaseFixtures} = createLogFixtures(
+ autorefreshOrganization,
+ project,
+ testDate,
+ {
+ intervalMs: 24 * 60 * 60 * 1000, // 24 hours
+ }
+ );
- render(, {
- organization: newOrganization,
- initialRouterConfig: {
- location: `/organizations/${newOrganization.slug}/explore/logs/`,
- },
+ beforeEach(() => {
+ eventTableMock.mockClear();
+ eventTableMock = _setupEventsMock(autorefreshBaseFixtures.slice(0, 5));
});
- await waitFor(() => {
- expect(screen.getByTestId('logs-table')).toBeInTheDocument();
- });
+ it('enables autorefresh when Switch is clicked', async function () {
+ render(, {
+ organization: autorefreshOrganization,
+ initialRouterConfig,
+ });
- const switchInput = screen.getByRole('checkbox', {name: /auto-refresh/i});
- expect(switchInput).not.toBeChecked();
- expect(switchInput).toBeEnabled();
+ await waitFor(() => {
+ expect(screen.getByTestId('logs-table')).toBeInTheDocument();
+ });
- await userEvent.click(switchInput);
+ const switchInput = screen.getByRole('checkbox', {name: /auto-refresh/i});
+ expect(switchInput).not.toBeChecked();
+ expect(switchInput).toBeEnabled();
- await waitFor(
- () => {
- expect(switchInput).toBeChecked();
- },
- {timeout: 5000}
- );
- });
+ await userEvent.click(switchInput);
- it('pauses auto-refresh when enabled switch is clicked', async function () {
- const {organization: newOrganization} = initializeOrg({
- organization: {
- features: [...BASE_FEATURES, 'ourlogs-infinite-scroll', 'ourlogs-live-refresh'],
- },
+ await waitFor(() => {
+ expect(switchInput).toBeChecked();
+ });
});
- MockApiClient.addMockResponse({
- url: `/organizations/${newOrganization.slug}/events/`,
- method: 'GET',
- body: {
- data: [
- {
- 'sentry.item_id': '1',
- 'project.id': 1,
- trace: 'trace1',
- severity_number: 9,
- severity_text: 'info',
- timestamp: '2025-04-10T19:21:12+00:00',
- message: 'some log message',
- 'tags[sentry.timestamp_precise,number]': 100,
- },
- ],
- meta: {fields: {}, units: {}},
- },
- });
+ it('pauses auto-refresh when enabled switch is clicked', async function () {
+ const {router} = render(, {
+ organization: autorefreshOrganization,
+ initialRouterConfig: generateRouterConfig({
+ [LOGS_AUTO_REFRESH_KEY]: 'enabled',
+ }),
+ });
+ expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('enabled');
- const {router} = render(, {
- organization: newOrganization,
- initialRouterConfig: {
- location: {
- pathname: `/organizations/${newOrganization.slug}/explore/logs/`,
- query: {
- [LOGS_AUTO_REFRESH_KEY]: 'enabled',
- },
- },
- },
- });
- expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('enabled');
+ await waitFor(() => {
+ expect(screen.getByTestId('logs-table')).toBeInTheDocument();
+ });
- await waitFor(() => {
- expect(screen.getByTestId('logs-table')).toBeInTheDocument();
- });
+ const switchInput = screen.getByRole('checkbox', {name: /auto-refresh/i});
+ expect(switchInput).toBeChecked();
- const switchInput = screen.getByRole('checkbox', {name: /auto-refresh/i});
- expect(switchInput).toBeChecked();
+ await userEvent.click(switchInput);
- await userEvent.click(switchInput);
-
- await waitFor(() => {
- expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('paused');
+ await waitFor(() => {
+ expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('paused');
+ });
+ expect(switchInput).not.toBeChecked();
});
- expect(switchInput).not.toBeChecked();
- });
- it('pauses auto-refresh when row is clicked', async function () {
- const {organization: newOrganization, project: newProject} = initializeOrg({
- organization: {
- features: [...BASE_FEATURES, 'ourlogs-infinite-scroll', 'ourlogs-live-refresh'],
- },
- });
+ it('pauses auto-refresh when row is clicked', async function () {
+ const rowDetailsMock = _setupTraceItemsMock(autorefreshBaseFixtures.slice(0, 1))[0];
+ const {router} = render(, {
+ organization: autorefreshOrganization,
+ initialRouterConfig: generateRouterConfig({
+ [LOGS_AUTO_REFRESH_KEY]: 'enabled',
+ }),
+ });
- ProjectsStore.loadInitialData([newProject]);
-
- const rowData = LogFixture({
- [OurLogKnownFieldKey.ID]: '1',
- [OurLogKnownFieldKey.PROJECT_ID]: newProject.id,
- [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(newOrganization.id),
- [OurLogKnownFieldKey.TRACE_ID]: '7b91699f',
- [OurLogKnownFieldKey.MESSAGE]: 'some log message',
- [OurLogKnownFieldKey.TIMESTAMP]: '2025-04-10T19:21:12+00:00',
- [OurLogKnownFieldKey.TIMESTAMP_PRECISE]: 100,
- [OurLogKnownFieldKey.SEVERITY_NUMBER]: 9,
- [OurLogKnownFieldKey.SEVERITY]: 'info',
- });
+ expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('enabled');
- const rowDetails = Object.entries(rowData).map(
- ([k, v]) =>
- ({
- name: k,
- value: v,
- type: typeof v === 'string' ? 'str' : 'float',
- }) as TraceItemResponseAttribute
- );
+ await waitFor(() => {
+ expect(screen.getByTestId('logs-table')).toBeInTheDocument();
+ });
- MockApiClient.addMockResponse({
- url: `/organizations/${newOrganization.slug}/events/`,
- method: 'GET',
- body: {
- data: [rowData],
- meta: {fields: {}, units: {}},
- },
- });
+ expect(screen.getAllByTestId('log-table-row')).toHaveLength(5);
- const rowDetailsMock = MockApiClient.addMockResponse({
- url: `/projects/${newOrganization.slug}/${newProject.slug}/trace-items/${rowData[OurLogKnownFieldKey.ID]}/`,
- method: 'GET',
- body: {
- itemId: rowData[OurLogKnownFieldKey.ID],
- links: null,
- meta: LogFixtureMeta(rowData),
- timestamp: rowData[OurLogKnownFieldKey.TIMESTAMP],
- attributes: rowDetails,
- },
- });
+ expect(rowDetailsMock).not.toHaveBeenCalled();
- const {router} = render(, {
- organization: newOrganization,
- initialRouterConfig: {
- location: {
- pathname: `/organizations/${newOrganization.slug}/explore/logs/`,
- query: {
- [LOGS_AUTO_REFRESH_KEY]: 'enabled',
- },
- },
- },
- });
+ const row = screen.getByText('Error occurred in authentication service');
+ await userEvent.click(row.parentElement!.parentElement!); // Avoid clicking the cells which can have their own click handlers.
- expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('enabled');
+ await waitFor(() => {
+ expect(rowDetailsMock).toHaveBeenCalled();
+ });
- await waitFor(() => {
- expect(screen.getByTestId('logs-table')).toBeInTheDocument();
- });
+ await waitFor(() => {
+ expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('paused');
+ });
- expect(rowDetailsMock).not.toHaveBeenCalled();
+ const switchInput = screen.getByRole('checkbox', {name: /auto-refresh/i});
+ expect(switchInput).not.toBeChecked();
+ expect(switchInput).toBeEnabled();
- const row = screen.getByText('some log message');
- await userEvent.click(row);
+ expect(eventTableMock).toHaveBeenCalledTimes(1);
+ eventTableMock.mockClear();
+ eventTableMock = _setupEventsMock(autorefreshBaseFixtures.slice(0, 5));
- await waitFor(() => {
- expect(rowDetailsMock).toHaveBeenCalled();
- });
+ await userEvent.click(switchInput);
- await waitFor(() => {
- expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('paused');
+ await waitFor(() => {
+ expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('enabled');
+ });
+
+ expect(eventTableMock).toHaveBeenCalledTimes(1);
+
+ expect(switchInput).toBeChecked();
});
});
});
diff --git a/static/app/views/explore/logs/logsAutoRefresh.spec.tsx b/static/app/views/explore/logs/logsAutoRefresh.spec.tsx
index 3733959741ca3c..b5782437c2cd23 100644
--- a/static/app/views/explore/logs/logsAutoRefresh.spec.tsx
+++ b/static/app/views/explore/logs/logsAutoRefresh.spec.tsx
@@ -1,17 +1,11 @@
import React from 'react';
-import type {RouterConfig} from '@react-types/shared';
-import {LogFixture} from 'sentry-fixture/log';
-import {OrganizationFixture} from 'sentry-fixture/organization';
-import {ProjectFixture} from 'sentry-fixture/project';
+import {createLogFixtures, initializeLogsTest} from 'sentry-fixture/log';
import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import PageFiltersStore from 'sentry/stores/pageFiltersStore';
import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
-import {
- LOGS_AUTO_REFRESH_KEY,
- LOGS_REFRESH_INTERVAL_KEY,
-} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext';
+import {LOGS_AUTO_REFRESH_KEY} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext';
import {LogsPageDataProvider} from 'sentry/views/explore/contexts/logs/logsPageData';
import {
LOGS_FIELDS_KEY,
@@ -19,68 +13,39 @@ import {
} from 'sentry/views/explore/contexts/logs/logsPageParams';
import {LOGS_SORT_BYS_KEY} from 'sentry/views/explore/contexts/logs/sortBys';
import {AutorefreshToggle} from 'sentry/views/explore/logs/logsAutoRefresh';
-import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types';
-
-const REFRESH_INTERVAL_MS = 100;
describe('LogsAutoRefresh Integration Tests', () => {
- const organization = OrganizationFixture({
- features: ['ourlogs-enabled', 'ourlogs-live-refresh', 'ourlogs-infinite-scroll'],
- });
+ const {organization, project, routerConfig, setupPageFilters, setupEventsMock} =
+ initializeLogsTest({
+ liveRefresh: true, // Enable ourlogs-live-refresh for these tests
+ });
- const projects = [ProjectFixture()];
-
- const initialRouterConfig = {
- location: {
- pathname: `/organizations/${organization.slug}/explore/logs/`,
- query: {
- // Toggle is disabled if sort is not a timestamp
- [LOGS_SORT_BYS_KEY]: '-timestamp',
- [LOGS_REFRESH_INTERVAL_KEY]: REFRESH_INTERVAL_MS, // Fast refresh for testing
- },
- },
- route: '/organizations/:orgId/explore/logs/',
- };
+ const testDate = new Date('2024-01-15T10:00:00.000Z');
+ const {baseFixtures} = createLogFixtures(organization, project, testDate);
const enabledRouterConfig = {
- ...initialRouterConfig,
+ ...routerConfig,
location: {
- ...initialRouterConfig.location,
- query: {...initialRouterConfig.location.query, [LOGS_AUTO_REFRESH_KEY]: 'enabled'},
+ ...routerConfig.location,
+ query: {...routerConfig.location.query, [LOGS_AUTO_REFRESH_KEY]: 'enabled'},
},
};
- const mockLogsData = [
- LogFixture({
- [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
- [OurLogKnownFieldKey.PROJECT_ID]: String(projects[0]!.id),
- }),
- ];
+ setupPageFilters();
+
+ let mockApiCall: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
MockApiClient.clearMockResponses();
- // Init PageFiltersStore to make pageFiltersReady true (otherwise logs query won't fire)
- PageFiltersStore.init();
- PageFiltersStore.onInitializeUrlState(
- {
- projects: [parseInt(projects[0]!.id, 10)],
- environments: [],
- datetime: {
- period: '7d',
- start: null,
- end: null,
- utc: null,
- },
- },
- new Set()
- );
+ // Default API mock
+ mockApiCall = setupEventsMock(baseFixtures.slice(0, 1));
});
const renderWithProviders = (
children: React.ReactNode,
- options: Parameters[1] & {initialRouterConfig: RouterConfig}
+ options: Parameters[1]
) => {
const result = render(
@@ -94,17 +59,9 @@ describe('LogsAutoRefresh Integration Tests', () => {
return result;
};
- const mockApiCall = () =>
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/events/`,
- method: 'GET',
- body: {data: mockLogsData},
- });
-
it('renders correctly with time-based sort', async () => {
- const mockApi = mockApiCall();
const {router} = renderWithProviders(, {
- initialRouterConfig,
+ initialRouterConfig: routerConfig,
organization,
});
@@ -115,16 +72,15 @@ describe('LogsAutoRefresh Integration Tests', () => {
expect(toggleSwitch).not.toBeChecked();
await waitFor(() => {
- expect(mockApi).toHaveBeenCalledTimes(1);
+ expect(mockApiCall).toHaveBeenCalledTimes(1);
});
expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBeUndefined();
});
it('enables auto-refresh when toggled and updates URL', async () => {
- const mockApi = mockApiCall();
const {router} = renderWithProviders(, {
- initialRouterConfig,
+ initialRouterConfig: routerConfig,
organization,
});
@@ -140,12 +96,10 @@ describe('LogsAutoRefresh Integration Tests', () => {
expect(toggleSwitch).toBeChecked();
});
- expect(mockApi).toHaveBeenCalled();
+ expect(mockApiCall).toHaveBeenCalled();
});
it('disables auto-refresh when toggled off and sets paused state', async () => {
- mockApiCall();
-
const {router} = renderWithProviders(, {
initialRouterConfig: enabledRouterConfig,
organization,
@@ -168,12 +122,10 @@ describe('LogsAutoRefresh Integration Tests', () => {
});
it('disables toggle when using non-timestamp sort', async () => {
- mockApiCall();
-
const nonTimestampRouterConfig = {
- ...initialRouterConfig,
+ ...routerConfig,
location: {
- ...initialRouterConfig.location,
+ ...routerConfig.location,
query: {
[LOGS_SORT_BYS_KEY]: 'level', // Non-timestamp sort
[LOGS_FIELDS_KEY]: ['level', 'timestamp'], // Fields have to be set for sort bys to be applied
@@ -199,8 +151,6 @@ describe('LogsAutoRefresh Integration Tests', () => {
});
it('disables toggle when using absolute date range', async () => {
- mockApiCall();
-
// Update PageFiltersStore with absolute dates
act(() => {
PageFiltersStore.updateDateTime({
@@ -212,7 +162,7 @@ describe('LogsAutoRefresh Integration Tests', () => {
});
renderWithProviders(, {
- initialRouterConfig,
+ initialRouterConfig: routerConfig,
organization,
});
@@ -231,16 +181,15 @@ describe('LogsAutoRefresh Integration Tests', () => {
});
it('shows error state in URL when query fails', async () => {
- const mockCall = mockApiCall();
-
const {router} = renderWithProviders(, {
initialRouterConfig: enabledRouterConfig,
organization,
});
await screen.findByRole('checkbox', {name: 'Auto-refresh'});
- expect(mockCall).toHaveBeenCalledTimes(1);
+ expect(mockApiCall).toHaveBeenCalledTimes(1);
+ // Clear default mocks and add custom error mock
MockApiClient.clearMockResponses();
const mockErrorCall = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events/`,
@@ -259,8 +208,6 @@ describe('LogsAutoRefresh Integration Tests', () => {
});
it('shows as checked and calls api repeatedly when auto-refresh is enabled', async () => {
- const mockApi = mockApiCall();
-
renderWithProviders(, {
initialRouterConfig: enabledRouterConfig,
organization,
@@ -270,16 +217,19 @@ describe('LogsAutoRefresh Integration Tests', () => {
expect(toggleSwitch).toBeChecked();
await waitFor(() => {
- expect(mockApi).toHaveBeenCalledTimes(5);
+ expect(mockApiCall).toHaveBeenCalledTimes(5);
});
});
it('disables auto-refresh after 5 consecutive requests with more data', async () => {
jest.useFakeTimers();
+
+ // Clear default mocks and add custom mock
+ MockApiClient.clearMockResponses();
const mockApi = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events/`,
method: 'GET',
- body: {data: mockLogsData},
+ body: {data: baseFixtures.slice(0, 1)},
headers: {
Link: '; rel="next"; results="true"',
},
@@ -301,10 +251,12 @@ describe('LogsAutoRefresh Integration Tests', () => {
});
it('continues auto-refresh when there is no more data', async () => {
+ // Clear default mocks and add custom mock
+ MockApiClient.clearMockResponses();
const mockApi = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events/`,
method: 'GET',
- body: {data: mockLogsData},
+ body: {data: baseFixtures.slice(0, 1)},
headers: {
Link: '', // No Link header means no more data
},
@@ -327,7 +279,8 @@ describe('LogsAutoRefresh Integration Tests', () => {
});
it('does not rate limit on initial load even with more data', async () => {
- // Mock API response with Link header indicating more data
+ // Clear default mocks and add custom mock with Link header indicating more data
+ MockApiClient.clearMockResponses();
const mockCall = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events/`,
method: 'GET',
@@ -335,7 +288,7 @@ describe('LogsAutoRefresh Integration Tests', () => {
Link: '; rel="previous"; results="false"; cursor="0:0:1", ; rel="next"; results="true"; cursor="0:100:0"',
},
body: {
- data: mockLogsData,
+ data: baseFixtures.slice(0, 1),
meta: {fields: {}},
},
});
@@ -353,7 +306,6 @@ describe('LogsAutoRefresh Integration Tests', () => {
// Verify auto-refresh is still enabled (not rate limited)
expect(router.location.query[LOGS_AUTO_REFRESH_KEY]).toBe('enabled');
- // Wait a bit to ensure no rate limiting occurs
await waitFor(() => {
expect(mockCall).toHaveBeenCalledTimes(5);
});
diff --git a/static/app/views/explore/logs/logsAutoRefresh.tsx b/static/app/views/explore/logs/logsAutoRefresh.tsx
index 60f2b792931c8a..55af63c42019b4 100644
--- a/static/app/views/explore/logs/logsAutoRefresh.tsx
+++ b/static/app/views/explore/logs/logsAutoRefresh.tsx
@@ -106,7 +106,8 @@ export function AutorefreshToggle({
const newChecked = !autorefreshEnabled;
trackAnalytics('logs.auto_refresh.toggled', {
- enabled: newChecked,
+ toggleState: newChecked ? 'enabled' : 'disabled',
+ fromPaused: autoRefresh === 'paused',
organization,
page_source: analyticsPageSource,
});
diff --git a/static/app/views/explore/logs/logsTab.spec.tsx b/static/app/views/explore/logs/logsTab.spec.tsx
index 05e33497780736..a8e4725eb95d08 100644
--- a/static/app/views/explore/logs/logsTab.spec.tsx
+++ b/static/app/views/explore/logs/logsTab.spec.tsx
@@ -1,8 +1,7 @@
-import {initializeOrg} from 'sentry-test/initializeOrg';
+import {initializeLogsTest} from 'sentry-fixture/log';
+
import {render, screen} from 'sentry-test/reactTestingLibrary';
-import PageFiltersStore from 'sentry/stores/pageFiltersStore';
-import ProjectsStore from 'sentry/stores/projectsStore';
import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
import {LogsPageDataProvider} from 'sentry/views/explore/contexts/logs/logsPageData';
import {
@@ -29,11 +28,7 @@ const datePageFilterProps: PickableDays = {
};
describe('LogsTabContent', function () {
- const {organization, project} = initializeOrg({
- organization: {
- features: ['ourlogs-enabled'],
- },
- });
+ const {organization, project, setupPageFilters} = initializeLogsTest();
let eventTableMock: jest.Mock;
let eventStatsMock: jest.Mock;
@@ -63,26 +58,12 @@ describe('LogsTabContent', function () {
route: '/organizations/:orgId/explore/logs/',
};
+ setupPageFilters();
+
beforeEach(function () {
MockApiClient.clearMockResponses();
- ProjectsStore.loadInitialData([project]);
-
- PageFiltersStore.init();
- PageFiltersStore.onInitializeUrlState(
- {
- projects: [parseInt(project.id, 10)],
- environments: [],
- datetime: {
- period: '14d',
- start: null,
- end: null,
- utc: null,
- },
- },
- new Set()
- );
-
+ // Default API mocks
eventTableMock = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events/`,
method: 'GET',
diff --git a/static/app/views/explore/logs/tables/logsTableRow.spec.tsx b/static/app/views/explore/logs/tables/logsTableRow.spec.tsx
index 9658aab00d5735..56fd1c5b871984 100644
--- a/static/app/views/explore/logs/tables/logsTableRow.spec.tsx
+++ b/static/app/views/explore/logs/tables/logsTableRow.spec.tsx
@@ -43,6 +43,7 @@ describe('logsTableRow', () => {
// These are the values in the actual row - e.g., the ones loaded before you click the row
const rowData = LogFixture({
+ [OurLogKnownFieldKey.ID]: '1',
[OurLogKnownFieldKey.PROJECT_ID]: project.id,
[OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
[OurLogKnownFieldKey.TRACE_ID]: '7b91699f',
@@ -70,6 +71,7 @@ describe('logsTableRow', () => {
);
const rowDataWithCodeFilePath = LogFixture({
+ [OurLogKnownFieldKey.ID]: '2',
[OurLogKnownFieldKey.PROJECT_ID]: project.id,
[OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
// Code file path fields
@@ -147,6 +149,26 @@ describe('logsTableRow', () => {
attributes: rowDetails,
},
});
+
+ // Mock for rowDataWithCodeFilePath
+ MockApiClient.addMockResponse({
+ url: `/projects/${organization.slug}/${project.slug}/trace-items/${rowDataWithCodeFilePath[OurLogKnownFieldKey.ID]}/`,
+ method: 'GET',
+ body: {
+ itemId: rowDataWithCodeFilePath[OurLogKnownFieldKey.ID],
+ links: null,
+ meta: {},
+ timestamp: rowDataWithCodeFilePath[OurLogKnownFieldKey.TIMESTAMP],
+ attributes: Object.entries(rowDataWithCodeFilePath).map(
+ ([k, v]) =>
+ ({
+ name: k,
+ value: v,
+ type: typeof v === 'string' ? 'str' : 'float',
+ }) as TraceItemResponseAttribute
+ ),
+ },
+ });
});
it('hovering the row causes prefetching of the row details', async () => {
diff --git a/static/app/views/explore/logs/useStreamingTimeseriesResult.spec.tsx b/static/app/views/explore/logs/useStreamingTimeseriesResult.spec.tsx
index 84c246e719df0c..357530ce5bd89e 100644
--- a/static/app/views/explore/logs/useStreamingTimeseriesResult.spec.tsx
+++ b/static/app/views/explore/logs/useStreamingTimeseriesResult.spec.tsx
@@ -1,11 +1,8 @@
import {LocationFixture} from 'sentry-fixture/locationFixture';
-import {LogFixture} from 'sentry-fixture/log';
-import {OrganizationFixture} from 'sentry-fixture/organization';
-import {ProjectFixture} from 'sentry-fixture/project';
+import {initializeLogsTest, LogFixture} from 'sentry-fixture/log';
import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary';
-import PageFiltersStore from 'sentry/stores/pageFiltersStore';
import type {Organization} from 'sentry/types/organization';
import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
import {useLocation} from 'sentry/utils/useLocation';
@@ -28,29 +25,19 @@ function preciseTimestampFromMillis(timestamp: number) {
}
describe('useStreamingTimeseriesResult', () => {
- const logsOrganization = OrganizationFixture({
- features: ['ourlogs-enabled', 'ourlogs-live-refresh'],
+ const {
+ organization: logsOrganization,
+ project,
+ setupPageFilters,
+ } = initializeLogsTest({
+ liveRefresh: true,
});
- const project = ProjectFixture();
+
+ setupPageFilters();
beforeEach(() => {
jest.resetAllMocks();
mockUseLocation.mockReturnValue(LocationFixture());
-
- PageFiltersStore.init();
- PageFiltersStore.onInitializeUrlState(
- {
- projects: [parseInt(project.id, 10)],
- environments: [],
- datetime: {
- period: '14d',
- start: null,
- end: null,
- utc: null,
- },
- },
- new Set()
- );
});
const createWrapper = ({
@@ -212,17 +199,13 @@ describe('useStreamingTimeseriesResult', () => {
}) as any;
it('should return original timeseries when feature flag is enabled', () => {
- const orgWithFeature = OrganizationFixture({
- features: ['ourlogs-enabled', 'ourlogs-live-refresh'],
- });
-
const mockTableData = createMockTableData([]);
const mockTimeseriesData = getMockSingleAxisTimeseries();
const {result} = renderHook(
() => useStreamingTimeseriesResult(mockTableData, mockTimeseriesData, 0n),
{
- wrapper: createWrapper({autoRefresh: 'enabled', organization: orgWithFeature}),
+ wrapper: createWrapper({autoRefresh: 'enabled', organization: logsOrganization}),
}
);
@@ -270,15 +253,27 @@ describe('useStreamingTimeseriesResult', () => {
const mockTableData = createMockTableData([
LogFixture({
+ [OurLogKnownFieldKey.ID]: '1',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(9000),
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '2',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(8200),
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '3',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(8100),
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '4',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(8000),
}),
]);
@@ -361,22 +356,37 @@ describe('useStreamingTimeseriesResult', () => {
const initialTableFixtures = [
LogFixture({
+ [OurLogKnownFieldKey.ID]: '5',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(9000),
[OurLogKnownFieldKey.SEVERITY]: 'brand_new_severity',
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '6',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(8001),
[OurLogKnownFieldKey.SEVERITY]: 'error',
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '7',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(8001),
[OurLogKnownFieldKey.SEVERITY]: 'warn',
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '8',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(8000),
[OurLogKnownFieldKey.SEVERITY]: 'warn',
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '9',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(7000),
[OurLogKnownFieldKey.SEVERITY]: 'warn',
}),
@@ -445,6 +455,9 @@ describe('useStreamingTimeseriesResult', () => {
rerender(
createMockTableData([
LogFixture({
+ [OurLogKnownFieldKey.ID]: '10',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(12000),
[OurLogKnownFieldKey.SEVERITY]: 'yet_another_severity',
}),
@@ -546,22 +559,37 @@ describe('useStreamingTimeseriesResult', () => {
const mockTableData = createMockTableData([
LogFixture({
+ [OurLogKnownFieldKey.ID]: '10',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(9000),
[OurLogKnownFieldKey.SEVERITY]: 'error',
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '11',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(8600),
[OurLogKnownFieldKey.SEVERITY]: 'warn',
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '12',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(8000),
[OurLogKnownFieldKey.SEVERITY]: 'warn',
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '13',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(6500),
[OurLogKnownFieldKey.SEVERITY]: 'info',
}),
LogFixture({
+ [OurLogKnownFieldKey.ID]: '14',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(logsOrganization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: preciseTimestampFromMillis(8500),
[OurLogKnownFieldKey.SEVERITY]: 'error',
}),
diff --git a/static/app/views/explore/logs/useVirtualStreaming.spec.tsx b/static/app/views/explore/logs/useVirtualStreaming.spec.tsx
index 4709d32764efad..0feb6630d11d9b 100644
--- a/static/app/views/explore/logs/useVirtualStreaming.spec.tsx
+++ b/static/app/views/explore/logs/useVirtualStreaming.spec.tsx
@@ -1,12 +1,9 @@
import {LocationFixture} from 'sentry-fixture/locationFixture';
-import {LogFixture} from 'sentry-fixture/log';
-import {OrganizationFixture} from 'sentry-fixture/organization';
-import {ProjectFixture} from 'sentry-fixture/project';
+import {initializeLogsTest, LogFixture} from 'sentry-fixture/log';
import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary';
import type {ApiResult} from 'sentry/api';
-import PageFiltersStore from 'sentry/stores/pageFiltersStore';
import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
import type {InfiniteData} from 'sentry/utils/queryClient';
import {useLocation} from 'sentry/utils/useLocation';
@@ -30,28 +27,15 @@ const mockUseLocation = jest.mocked(useLocation);
describe('useVirtualStreaming', () => {
let requestAnimationFrameSpy: jest.SpyInstance;
let cancelAnimationFrameSpy: jest.SpyInstance;
- const organization = OrganizationFixture();
- const project = ProjectFixture();
+
+ const {organization, project, setupPageFilters} = initializeLogsTest();
+
+ setupPageFilters();
beforeEach(() => {
jest.resetAllMocks();
mockUseLocation.mockReturnValue(LocationFixture());
- PageFiltersStore.init();
- PageFiltersStore.onInitializeUrlState(
- {
- projects: [parseInt(project.id, 10)],
- environments: [],
- datetime: {
- period: '14d',
- start: null,
- end: null,
- utc: null,
- },
- },
- new Set()
- );
-
requestAnimationFrameSpy = jest
.spyOn(window, 'requestAnimationFrame')
.mockImplementation((_callback: FrameRequestCallback): number => {
@@ -87,21 +71,29 @@ describe('useVirtualStreaming', () => {
// Data should be sorted by timestamp, so the first one should be the latest
LogFixture({
[OurLogKnownFieldKey.ID]: '4',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
// 42 seconds ago in nanoseconds
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: String(BigInt(now - 42000) * 1_000_000n),
}),
LogFixture({
[OurLogKnownFieldKey.ID]: '3',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
// 45 seconds ago in nanoseconds
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: String(BigInt(now - 44000) * 1_000_000n),
}),
LogFixture({
[OurLogKnownFieldKey.ID]: '2',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
// 50 seconds ago in nanoseconds
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: String(BigInt(now - 46000) * 1_000_000n),
}),
LogFixture({
[OurLogKnownFieldKey.ID]: '1',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
// 55 seconds ago in nanoseconds
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: String(BigInt(now - 55000) * 1_000_000n),
}),
@@ -125,6 +117,8 @@ describe('useVirtualStreaming', () => {
const mockData = createMockData([
LogFixture({
[OurLogKnownFieldKey.ID]: '1',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: '1000000000000000000',
}),
]);
@@ -140,6 +134,8 @@ describe('useVirtualStreaming', () => {
const mockData = createMockData([
LogFixture({
[OurLogKnownFieldKey.ID]: '1',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: '1000000000000000000',
}),
]);
@@ -157,6 +153,8 @@ describe('useVirtualStreaming', () => {
const mockData = createMockData([
LogFixture({
[OurLogKnownFieldKey.ID]: '1',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: '1000000000000000000',
}),
]);
@@ -183,9 +181,13 @@ describe('useVirtualStreaming', () => {
});
describe('isRowVisibleInVirtualStream', () => {
+ const {organization: testOrg, project: testProject} = initializeLogsTest();
+
it('should filter based on the milliseconds of the passed timestamp', () => {
const row = LogFixture({
[OurLogKnownFieldKey.ID]: '1',
+ [OurLogKnownFieldKey.PROJECT_ID]: testProject.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(testOrg.id),
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: '2000000000',
});
diff --git a/static/app/views/explore/logs/useVirtualStreaming.tsx b/static/app/views/explore/logs/useVirtualStreaming.tsx
index 78953ff491252e..5a4606654bf381 100644
--- a/static/app/views/explore/logs/useVirtualStreaming.tsx
+++ b/static/app/views/explore/logs/useVirtualStreaming.tsx
@@ -1,10 +1,11 @@
import {useCallback, useEffect, useRef, useState} from 'react';
+import isEqual from 'lodash/isEqual';
import type {ApiResult} from 'sentry/api';
import type {InfiniteData} from 'sentry/utils/queryClient';
import usePrevious from 'sentry/utils/usePrevious';
import {
- useAutorefreshEnabledOrWithinPauseWindow,
+ useLogsAutoRefreshContinued,
useLogsAutoRefreshEnabled,
useLogsRefreshInterval,
} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext';
@@ -53,8 +54,8 @@ export function useVirtualStreaming(
data: InfiniteData> | undefined
) {
const autoRefresh = useLogsAutoRefreshEnabled();
- const isAutoRefreshEnabledOrWithinPauseWindow =
- useAutorefreshEnabledOrWithinPauseWindow();
+ const previousAutoRefresh = usePrevious(autoRefresh);
+ const isAutoRefreshContinued = useLogsAutoRefreshContinued();
const refreshInterval = useLogsRefreshInterval();
const rafOn = useRef(false);
const [virtualTimestamp, setVirtualTimestamp] = useState(undefined);
@@ -74,7 +75,7 @@ export function useVirtualStreaming(
// If we've received data, initialize the virtual timestamp to be refreshEvery seconds before the max ingest delay timestamp
const initializeVirtualTimestamp = useCallback(() => {
- if (!data?.pages?.length || virtualTimestamp !== undefined) {
+ if (!data?.pages?.length) {
return;
}
@@ -107,20 +108,33 @@ export function useVirtualStreaming(
);
setVirtualTimestamp(initialTimestamp);
- }, [data, virtualTimestamp, refreshInterval]);
+ }, [data, refreshInterval]);
- // Initialize when auto refresh is enabled and we have data
useEffect(() => {
- if (!isAutoRefreshEnabledOrWithinPauseWindow || virtualTimestamp !== undefined) {
+ if (!autoRefresh) {
+ return;
+ }
+
+ if (virtualTimestamp === undefined) {
+ // First time enabling autorefresh, initialize virtual timestamp.
+ initializeVirtualTimestamp();
return;
}
- initializeVirtualTimestamp();
+ if (isAutoRefreshContinued) {
+ // Re-enabling autorefresh with existing virtual timestamp, and within continue window, do nothing.
+ return;
+ }
+ if (!isEqual(autoRefresh, previousAutoRefresh)) {
+ // Re-enabling autorefresh with existing virtual timestamp, but outside continue window, reset virtual timestamp.
+ initializeVirtualTimestamp();
+ }
}, [
- isAutoRefreshEnabledOrWithinPauseWindow,
+ autoRefresh,
initializeVirtualTimestamp,
virtualTimestamp,
- data?.pages?.length,
+ isAutoRefreshContinued,
+ previousAutoRefresh,
]);
// Get the newest timestamp from the latest page to calculate how far behind we are
diff --git a/tests/js/fixtures/log.ts b/tests/js/fixtures/log.ts
index 41f651f6c9fe88..305a57f7f77588 100644
--- a/tests/js/fixtures/log.ts
+++ b/tests/js/fixtures/log.ts
@@ -1,5 +1,20 @@
+import pick from 'lodash/pick';
+
+import {initializeOrg} from 'sentry-test/initializeOrg';
+
+import PageFiltersStore from 'sentry/stores/pageFiltersStore';
+import ProjectsStore from 'sentry/stores/projectsStore';
+import type {PageFilters} from 'sentry/types/core';
+import type {Organization} from 'sentry/types/organization';
+import type {Project} from 'sentry/types/project';
import type {EventsMetaType} from 'sentry/utils/discover/eventView';
-import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types';
+import {LOGS_REFRESH_INTERVAL_KEY} from 'sentry/views/explore/contexts/logs/logsAutoRefreshContext';
+import {LOGS_SORT_BYS_KEY} from 'sentry/views/explore/contexts/logs/sortBys';
+import type {TraceItemResponseAttribute} from 'sentry/views/explore/hooks/useTraceItemDetails';
+import type {
+ EventsLogsResult,
+ OurLogsResponseItem,
+} from 'sentry/views/explore/logs/types';
import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types';
export function LogFixture({
@@ -13,7 +28,15 @@ export function LogFixture({
[OurLogKnownFieldKey.TRACE_ID]: traceId = '7b91699fd385d9fd52e0c4bc',
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: timestampPrecise = 1.744312870049196e18,
...rest
-}: Partial): OurLogsResponseItem {
+}: Partial &
+ Required<
+ Pick<
+ OurLogsResponseItem,
+ | OurLogKnownFieldKey.ID
+ | OurLogKnownFieldKey.PROJECT_ID
+ | OurLogKnownFieldKey.ORGANIZATION_ID
+ >
+ >): OurLogsResponseItem {
return {
[OurLogKnownFieldKey.ID]: String(id),
[OurLogKnownFieldKey.PROJECT_ID]: String(projectId),
@@ -29,18 +52,321 @@ export function LogFixture({
}
// Incomplete, only provides type of field if it's a string or number.
-export function LogFixtureMeta(fixture: Partial): EventsMetaType {
+export function LogFixtureMeta(
+ fixture: OurLogsResponseItem | OurLogsResponseItem[]
+): EventsMetaType {
+ const logFixtures = Array.isArray(fixture) ? fixture : [fixture];
+ const fields = logFixtures.flatMap(logFixture =>
+ Object.entries(logFixture).map(([key, value]) => {
+ const valueType = typeof value;
+ if (!['string', 'number'].includes(valueType)) {
+ throw new Error(`Invalid value type: ${valueType}`);
+ }
+ return [key, valueType];
+ })
+ );
return {
- fields: Object.fromEntries(
- Object.entries(fixture).map(([key, value]) => {
- const valueType = typeof value;
- if (!['string', 'number'].includes(valueType)) {
- throw new Error(`Invalid value type: ${valueType}`);
- }
- const type = valueType === 'string' ? 'string' : 'number';
- return [key, type];
- })
- ),
+ fields: Object.fromEntries(fields),
units: {},
};
}
+
+interface LogsTestInitOptions {
+ liveRefresh?: boolean;
+ organization?: Partial;
+ ourlogs?: boolean;
+ pageFiltersPeriod?: string;
+ project?: Partial;
+ refreshInterval?: string;
+ routerQuery?: Record;
+}
+
+// Narrowed type from sentry-test/reactTestingLibrary.tsx
+type LocationConfig = {
+ pathname: string;
+ query?: Record;
+};
+
+/**
+ * Standardized initialization for logs tests
+ */
+export function initializeLogsTest({
+ organization: orgOverrides = {},
+ project: projectOverrides = {},
+ ourlogs = true,
+ liveRefresh: hasLiveRefreshFlag = false,
+ routerQuery = {},
+ refreshInterval = '60', // Fast refresh for testing, should always exceed waitFor internal timer interval (50ms default)
+}: LogsTestInitOptions = {}): {
+ generateRouterConfig: (routerQueryOverrides: Record) => {
+ location: LocationConfig;
+ route?: string;
+ };
+ initialLocation: LocationConfig;
+ initialPageFilters: PageFilters;
+ organization: Organization;
+ project: Project;
+ routerConfig: {
+ location: LocationConfig;
+ route?: string;
+ };
+ setupEventsMock: (logFixtures: OurLogsResponseItem[]) => jest.Mock;
+ setupPageFilters: () => void;
+ setupTraceItemsMock: (logFixtures: OurLogsResponseItem[]) => jest.Mock[];
+} {
+ const baseFeatures = ourlogs
+ ? [
+ 'ourlogs-enabled',
+ 'ourlogs-visualize-sidebar',
+ 'ourlogs-dashboard',
+ 'ourlogs-alerts',
+ 'ourlogs-infinite-scroll',
+ ]
+ : [];
+
+ if (hasLiveRefreshFlag && ourlogs) {
+ baseFeatures.push('ourlogs-live-refresh');
+ }
+
+ const {organization, project} = initializeOrg({
+ organization: {
+ features: baseFeatures,
+ ...orgOverrides,
+ },
+ project: projectOverrides,
+ });
+
+ const initialLocation: LocationConfig = {
+ pathname: `/organizations/${organization.slug}/explore/logs/`,
+ query: {
+ [LOGS_SORT_BYS_KEY]: '-timestamp',
+ [LOGS_REFRESH_INTERVAL_KEY]: refreshInterval, // Fast refresh for testing, should always exceed waitFor internal timer interval (50ms default)
+ ...routerQuery,
+ },
+ };
+
+ const routerConfig: {
+ location: LocationConfig;
+ route?: string;
+ } = {
+ location: initialLocation,
+ route: '/organizations/:orgId/explore/logs/',
+ };
+
+ const initialPageFilters: PageFilters = {
+ projects: [parseInt(project.id, 10)],
+ environments: [],
+ datetime: {
+ period: '14d',
+ start: null,
+ end: null,
+ utc: null,
+ },
+ };
+
+ const generateRouterConfig = (routerQueryOverrides: Record) => {
+ return {
+ location: {
+ ...initialLocation,
+ query: {
+ ...initialLocation.query,
+ ...routerQueryOverrides,
+ },
+ },
+ };
+ };
+
+ const setupPageFilters = () => {
+ ProjectsStore.loadInitialData([project]);
+ PageFiltersStore.init();
+ PageFiltersStore.onInitializeUrlState(initialPageFilters, new Set());
+ };
+
+ const setupEventsMock = (logFixtures: OurLogsResponseItem[]) => {
+ const eventsData: EventsLogsResult = {
+ data: logFixtures,
+ meta: LogFixtureMeta(logFixtures),
+ };
+
+ return MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/events/`,
+ method: 'GET',
+ body: eventsData,
+ });
+ };
+
+ const setupTraceItemsMock = (logFixtures: OurLogsResponseItem[]) => {
+ return logFixtures.map(logFixture => {
+ const attributes: TraceItemResponseAttribute[] = Object.entries(logFixture).map(
+ ([k, v]) => {
+ if (typeof v === 'string') {
+ return {name: k, value: v, type: 'str' as const};
+ }
+ return {name: k, value: Number(v), type: 'float' as const};
+ }
+ );
+
+ return MockApiClient.addMockResponse({
+ url: `/projects/${organization.slug}/${project.slug}/trace-items/${logFixture[OurLogKnownFieldKey.ID]}/`,
+ method: 'GET',
+ body: {
+ itemId: logFixture[OurLogKnownFieldKey.ID],
+ links: null,
+ meta: LogFixtureMeta(logFixture),
+ timestamp: logFixture[OurLogKnownFieldKey.TIMESTAMP],
+ attributes,
+ },
+ });
+ });
+ };
+
+ return {
+ organization,
+ project,
+ routerConfig,
+ initialPageFilters,
+ initialLocation,
+ generateRouterConfig,
+ setupPageFilters,
+ setupEventsMock,
+ setupTraceItemsMock,
+ };
+}
+
+/**
+ * Standard set of log fixtures for testing - can be sliced as needed
+ * Creates logs in descending timestamp order (newest first)
+ * Returns both base fixtures (minimal fields) and detailed fixtures (all fields)
+ */
+export function createLogFixtures(
+ organization: Organization,
+ project: Project,
+ nowDate: Date,
+ options: {
+ baseFields?: string[];
+ intervalMs?: number;
+ } = {}
+): {
+ baseFixtures: OurLogsResponseItem[];
+ detailedFixtures: OurLogsResponseItem[];
+} {
+ const {intervalMs = 1000} = options;
+ const nowTimestamp = nowDate.getTime();
+
+ // Default base fields - minimal fields for basic testing
+ const defaultBaseFields = [
+ OurLogKnownFieldKey.ID,
+ OurLogKnownFieldKey.PROJECT_ID,
+ OurLogKnownFieldKey.ORGANIZATION_ID,
+ OurLogKnownFieldKey.MESSAGE,
+ OurLogKnownFieldKey.SEVERITY,
+ OurLogKnownFieldKey.TIMESTAMP,
+ OurLogKnownFieldKey.TRACE_ID,
+ ];
+
+ const baseFieldKeys = options.baseFields || defaultBaseFields;
+
+ const logData: Array>> = [
+ {
+ [OurLogKnownFieldKey.ID]: '1',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
+ [OurLogKnownFieldKey.MESSAGE]: 'Error occurred in authentication service',
+ [OurLogKnownFieldKey.SEVERITY]: 'error',
+ [OurLogKnownFieldKey.SEVERITY_NUMBER]: 17,
+ [OurLogKnownFieldKey.TRACE_ID]: '7b91699fd385d9fd52e0c4bc',
+ [OurLogKnownFieldKey.RELEASE]: '1.0.0',
+ [OurLogKnownFieldKey.CODE_FILE_PATH]:
+ '/usr/local/lib/python3.11/dist-packages/gunicorn/glogging.py',
+ [OurLogKnownFieldKey.CODE_LINE_NUMBER]: 123,
+ [OurLogKnownFieldKey.SDK_NAME]: 'sentry.python',
+ [OurLogKnownFieldKey.SDK_VERSION]: '1.0.0',
+ },
+ {
+ [OurLogKnownFieldKey.ID]: '2',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
+ [OurLogKnownFieldKey.MESSAGE]: 'User login successful',
+ [OurLogKnownFieldKey.SEVERITY]: 'info',
+ [OurLogKnownFieldKey.SEVERITY_NUMBER]: 9,
+ [OurLogKnownFieldKey.TRACE_ID]: '8c92799fe496e0ee63f1d5cd',
+ [OurLogKnownFieldKey.RELEASE]: '1.0.0',
+ [OurLogKnownFieldKey.CODE_FILE_PATH]:
+ '/usr/local/lib/python3.11/dist-packages/gunicorn/glogging.py',
+ [OurLogKnownFieldKey.CODE_LINE_NUMBER]: 456,
+ [OurLogKnownFieldKey.SDK_NAME]: 'sentry.python',
+ [OurLogKnownFieldKey.SDK_VERSION]: '1.0.0',
+ },
+ {
+ [OurLogKnownFieldKey.ID]: '3',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
+ [OurLogKnownFieldKey.MESSAGE]: 'Database connection warning',
+ [OurLogKnownFieldKey.SEVERITY]: 'warn',
+ [OurLogKnownFieldKey.SEVERITY_NUMBER]: 13,
+ [OurLogKnownFieldKey.TRACE_ID]: '9d03800gf5a7f1ff74g2e6de',
+ [OurLogKnownFieldKey.RELEASE]: '1.0.1',
+ [OurLogKnownFieldKey.CODE_FILE_PATH]:
+ '/usr/local/lib/python3.11/dist-packages/gunicorn/glogging.py',
+ [OurLogKnownFieldKey.CODE_LINE_NUMBER]: 789,
+ [OurLogKnownFieldKey.SDK_NAME]: 'sentry.python',
+ [OurLogKnownFieldKey.SDK_VERSION]: '1.0.0',
+ },
+ {
+ [OurLogKnownFieldKey.ID]: '4',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
+ [OurLogKnownFieldKey.MESSAGE]: 'Request processed successfully',
+ [OurLogKnownFieldKey.SEVERITY]: 'info',
+ [OurLogKnownFieldKey.SEVERITY_NUMBER]: 9,
+ [OurLogKnownFieldKey.TRACE_ID]: 'ae14911hg6b8g2gg85h3f7ef',
+ [OurLogKnownFieldKey.RELEASE]: '1.0.1',
+ [OurLogKnownFieldKey.CODE_FILE_PATH]:
+ '/usr/local/lib/python3.11/dist-packages/gunicorn/glogging.py',
+ [OurLogKnownFieldKey.CODE_LINE_NUMBER]: 321,
+ [OurLogKnownFieldKey.SDK_NAME]: 'sentry.python',
+ [OurLogKnownFieldKey.SDK_VERSION]: '1.0.0',
+ },
+ {
+ [OurLogKnownFieldKey.ID]: '5',
+ [OurLogKnownFieldKey.PROJECT_ID]: project.id,
+ [OurLogKnownFieldKey.ORGANIZATION_ID]: Number(organization.id),
+ [OurLogKnownFieldKey.MESSAGE]: 'Debug trace information',
+ [OurLogKnownFieldKey.SEVERITY]: 'debug',
+ [OurLogKnownFieldKey.SEVERITY_NUMBER]: 5,
+ [OurLogKnownFieldKey.TRACE_ID]: 'bf25022ih7c9h3hh96i4g8fg',
+ [OurLogKnownFieldKey.RELEASE]: '1.0.1',
+ [OurLogKnownFieldKey.CODE_FILE_PATH]:
+ '/usr/local/lib/python3.11/dist-packages/gunicorn/glogging.py',
+ [OurLogKnownFieldKey.CODE_LINE_NUMBER]: 654,
+ [OurLogKnownFieldKey.SDK_NAME]: 'sentry.python',
+ [OurLogKnownFieldKey.SDK_VERSION]: '1.0.0',
+ },
+ ];
+
+ const fixtures = logData.map((data, index) => {
+ const logTimestamp = nowTimestamp - index * intervalMs;
+ const completeLogData = {
+ ...data,
+ [OurLogKnownFieldKey.TIMESTAMP]: new Date(logTimestamp).toISOString(),
+ [OurLogKnownFieldKey.TIMESTAMP_PRECISE]: String(BigInt(logTimestamp) * 1_000_000n),
+ };
+ return completeLogData;
+ });
+
+ // Used for /events mock
+ const baseFixtures = fixtures.map(completeLogData => {
+ const baseOnlyFields = pick(completeLogData, baseFieldKeys);
+ return LogFixture(baseOnlyFields as any);
+ });
+
+ // Used for /trace-items mock
+ const detailedFixtures = fixtures.map(completeLogData => {
+ return LogFixture(completeLogData as any);
+ });
+
+ return {
+ baseFixtures,
+ detailedFixtures,
+ };
+}