mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 18:45:56 +08:00
feat: user pilot reload (#7905)
* feat: user pilot reload * feat: added test cases --------- Co-authored-by: SagarRajput-7 <sagar@signoz.io>
This commit is contained in:
parent
175b059268
commit
9c0134da54
@ -5,6 +5,7 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import NotFound from 'components/NotFound';
|
import NotFound from 'components/NotFound';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
|
import UserpilotRouteTracker from 'components/UserpilotRouteTracker/UserpilotRouteTracker';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@ -354,6 +355,7 @@ function App(): JSX.Element {
|
|||||||
<ConfigProvider theme={themeConfig}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<CompatRouter>
|
<CompatRouter>
|
||||||
|
<UserpilotRouteTracker />
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
<ResourceProvider>
|
<ResourceProvider>
|
||||||
|
@ -0,0 +1,223 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { Userpilot } from 'userpilot';
|
||||||
|
|
||||||
|
import UserpilotRouteTracker from './UserpilotRouteTracker';
|
||||||
|
|
||||||
|
// Mock constants
|
||||||
|
const INITIAL_PATH = '/initial';
|
||||||
|
const TIMER_DELAY = 100;
|
||||||
|
|
||||||
|
// Mock the userpilot module
|
||||||
|
jest.mock('userpilot', () => ({
|
||||||
|
Userpilot: {
|
||||||
|
reload: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock location state
|
||||||
|
let mockLocation = {
|
||||||
|
pathname: INITIAL_PATH,
|
||||||
|
search: '',
|
||||||
|
hash: '',
|
||||||
|
state: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock react-router-dom
|
||||||
|
jest.mock('react-router-dom', () => {
|
||||||
|
const originalModule = jest.requireActual('react-router-dom');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...originalModule,
|
||||||
|
useLocation: jest.fn(() => mockLocation),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('UserpilotRouteTracker', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
// Reset timers
|
||||||
|
jest.useFakeTimers();
|
||||||
|
// Reset error mock implementation
|
||||||
|
(Userpilot.reload as jest.Mock).mockImplementation(() => {});
|
||||||
|
// Reset location to initial state
|
||||||
|
mockLocation = {
|
||||||
|
pathname: INITIAL_PATH,
|
||||||
|
search: '',
|
||||||
|
hash: '',
|
||||||
|
state: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls Userpilot.reload on initial render', () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<UserpilotRouteTracker />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fast-forward timer to trigger the setTimeout in reloadUserpilot
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(TIMER_DELAY);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Userpilot.reload).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls Userpilot.reload when pathname changes', () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<UserpilotRouteTracker />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fast-forward initial render timer
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(TIMER_DELAY);
|
||||||
|
});
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Create a new location object with different pathname
|
||||||
|
const newLocation = {
|
||||||
|
...mockLocation,
|
||||||
|
pathname: '/new-path',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the mock location with new path and trigger re-render
|
||||||
|
act(() => {
|
||||||
|
mockLocation = newLocation;
|
||||||
|
// Force a component update with the new location
|
||||||
|
rerender(
|
||||||
|
<MemoryRouter>
|
||||||
|
<UserpilotRouteTracker />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fast-forward timer to allow the setTimeout to execute
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(TIMER_DELAY);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Userpilot.reload).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls Userpilot.reload when search parameters change', () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<UserpilotRouteTracker />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fast-forward initial render timer
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(TIMER_DELAY);
|
||||||
|
});
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Create a new location object with different search params
|
||||||
|
const newLocation = {
|
||||||
|
...mockLocation,
|
||||||
|
search: '?param=value',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the mock location with new search and trigger re-render
|
||||||
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
|
act(() => {
|
||||||
|
mockLocation = newLocation;
|
||||||
|
// Force a component update with the new location
|
||||||
|
rerender(
|
||||||
|
<MemoryRouter>
|
||||||
|
<UserpilotRouteTracker />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fast-forward timer to allow the setTimeout to execute
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(TIMER_DELAY);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Userpilot.reload).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles errors in Userpilot.reload gracefully', () => {
|
||||||
|
// Mock console.error to prevent test output noise and capture calls
|
||||||
|
const consoleErrorSpy = jest
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Instead of using the component, we test the error handling behavior directly
|
||||||
|
const errorMsg = 'Error message';
|
||||||
|
|
||||||
|
// Set up a function that has the same error handling behavior as in component
|
||||||
|
const testErrorHandler = (): void => {
|
||||||
|
try {
|
||||||
|
if (typeof Userpilot !== 'undefined' && Userpilot.reload) {
|
||||||
|
Userpilot.reload();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Userpilot] Error reloading on route change:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make Userpilot.reload throw an error
|
||||||
|
(Userpilot.reload as jest.Mock).mockImplementation(() => {
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute the function that should handle errors
|
||||||
|
testErrorHandler();
|
||||||
|
|
||||||
|
// Verify error was logged
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
|
'[Userpilot] Error reloading on route change:',
|
||||||
|
expect.any(Error),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore console mock
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call Userpilot.reload when same route is rendered again', () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<UserpilotRouteTracker />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fast-forward initial render timer
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(TIMER_DELAY);
|
||||||
|
});
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
mockLocation = {
|
||||||
|
pathname: mockLocation.pathname,
|
||||||
|
search: mockLocation.search,
|
||||||
|
hash: mockLocation.hash,
|
||||||
|
state: mockLocation.state,
|
||||||
|
};
|
||||||
|
// Force a component update with the same location
|
||||||
|
rerender(
|
||||||
|
<MemoryRouter>
|
||||||
|
<UserpilotRouteTracker />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fast-forward timer
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(TIMER_DELAY);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not call reload since path and search are the same
|
||||||
|
expect(Userpilot.reload).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,60 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { Userpilot } from 'userpilot';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserpilotRouteTracker - A component that tracks route changes and calls Userpilot.reload
|
||||||
|
* on actual page changes (pathname changes or significant query parameter changes).
|
||||||
|
*
|
||||||
|
* This component renders nothing and is designed to be placed once high in the component tree.
|
||||||
|
*/
|
||||||
|
function UserpilotRouteTracker(): null {
|
||||||
|
const location = useLocation();
|
||||||
|
const prevPathRef = useRef<string>(location.pathname);
|
||||||
|
const prevSearchRef = useRef<string>(location.search);
|
||||||
|
const isFirstRenderRef = useRef<boolean>(true);
|
||||||
|
|
||||||
|
// Function to reload Userpilot safely - using useCallback to avoid dependency issues
|
||||||
|
const reloadUserpilot = useCallback((): void => {
|
||||||
|
try {
|
||||||
|
if (typeof Userpilot !== 'undefined' && Userpilot.reload) {
|
||||||
|
setTimeout(() => {
|
||||||
|
Userpilot.reload();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Userpilot] Error reloading on route change:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle first render
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFirstRenderRef.current) {
|
||||||
|
isFirstRenderRef.current = false;
|
||||||
|
reloadUserpilot();
|
||||||
|
}
|
||||||
|
}, [reloadUserpilot]);
|
||||||
|
|
||||||
|
// Handle route/query changes
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip first render as it's handled by the effect above
|
||||||
|
if (isFirstRenderRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the path has changed or if significant query params have changed
|
||||||
|
const pathChanged = location.pathname !== prevPathRef.current;
|
||||||
|
const searchChanged = location.search !== prevSearchRef.current;
|
||||||
|
|
||||||
|
if (pathChanged || searchChanged) {
|
||||||
|
// Update refs
|
||||||
|
prevPathRef.current = location.pathname;
|
||||||
|
prevSearchRef.current = location.search;
|
||||||
|
reloadUserpilot();
|
||||||
|
}
|
||||||
|
}, [location.pathname, location.search, reloadUserpilot]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserpilotRouteTracker;
|
Loading…
x
Reference in New Issue
Block a user