mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-12 19:31:13 +00:00
Compare commits
4 Commits
main
...
feat/new-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c84245d381 | ||
|
|
e7d10078de | ||
|
|
6493e2a503 | ||
|
|
ea05b1ee0e |
@@ -3,6 +3,8 @@ import { QueryClient } from 'react-query';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import post from 'api/v2/sessions/rotate/post';
|
||||
import afterLogin from 'AppRoutes/utils';
|
||||
import { SERVER_VERSION_HEADER } from 'components/NewVersionPrompt/constants';
|
||||
import { reportServerVersion } from 'components/NewVersionPrompt/useAppVersionStore';
|
||||
import axios, {
|
||||
AxiosError,
|
||||
AxiosResponse,
|
||||
@@ -48,6 +50,11 @@ export const interceptorsResponse = (
|
||||
}
|
||||
}
|
||||
|
||||
const serverVersion = value.headers?.[SERVER_VERSION_HEADER];
|
||||
if (typeof serverVersion === 'string') {
|
||||
reportServerVersion(serverVersion);
|
||||
}
|
||||
|
||||
return Promise.resolve(value);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { useUnsavedChangesStore } from 'hooks/useUnsavedChangesStore';
|
||||
import { reloadApp } from 'utils/appReload';
|
||||
|
||||
import { NEW_VERSION_TOAST_ID } from './constants';
|
||||
import { useAppReloadNotice } from './useAppReloadNotice';
|
||||
import { useAppVersionStore } from './useAppVersionStore';
|
||||
import { useCheckVersionOnTabReturn } from './useCheckVersionOnTabReturn';
|
||||
import { useLoadNewVersionOnNavigation } from './useLoadNewVersionOnNavigation';
|
||||
|
||||
function NewVersionPrompt(): null {
|
||||
const isNewVersionAvailable = useAppVersionStore(
|
||||
(state) => state.isNewVersionAvailable,
|
||||
);
|
||||
const hasUnsavedChanges = useUnsavedChangesStore((state) => state.count > 0);
|
||||
const hasPrompted = useRef(false);
|
||||
|
||||
useAppReloadNotice();
|
||||
useCheckVersionOnTabReturn();
|
||||
useLoadNewVersionOnNavigation(isNewVersionAvailable && !hasUnsavedChanges);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewVersionAvailable || hasUnsavedChanges || hasPrompted.current) {
|
||||
return;
|
||||
}
|
||||
hasPrompted.current = true;
|
||||
toast.info('A new version of SigNoz is available', {
|
||||
id: NEW_VERSION_TOAST_ID,
|
||||
description: 'It loads when you open another page.',
|
||||
duration: Infinity,
|
||||
action: {
|
||||
label: 'Refresh',
|
||||
onClick: (): void => reloadApp({ reason: 'refresh-clicked', detail: '' }),
|
||||
},
|
||||
});
|
||||
}, [isNewVersionAvailable, hasUnsavedChanges]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default NewVersionPrompt;
|
||||
@@ -0,0 +1,153 @@
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
holdUnsavedChanges,
|
||||
useUnsavedChangesStore,
|
||||
} from 'hooks/useUnsavedChangesStore';
|
||||
import { getCurrentLocation, navigate } from 'lib/router/navigation';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { act, fireEvent, render, waitFor } from 'tests/test-utils';
|
||||
import { Info } from 'types/api/v1/version/get';
|
||||
import { consumeAppReload, markAppReload, reloadApp } from 'utils/appReload';
|
||||
|
||||
import { NEW_VERSION_TOAST_ID, SERVER_VERSION_HEADER } from '../constants';
|
||||
import NewVersionPrompt from '../NewVersionPrompt';
|
||||
import { reportServerVersion, useAppVersionStore } from '../useAppVersionStore';
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: { info: jest.fn(), loading: jest.fn() },
|
||||
}));
|
||||
|
||||
jest.mock('utils/appReload', () => ({
|
||||
...jest.requireActual('utils/appReload'),
|
||||
reloadApp: jest.fn(),
|
||||
}));
|
||||
|
||||
const VERSION_ENDPOINT = '*/api/v1/version';
|
||||
const BUNDLE_VERSION = 'v0.100.0';
|
||||
const NEW_VERSION = 'v0.101.0';
|
||||
|
||||
const expectRefreshPrompt = (): void => {
|
||||
expect(toast.info).toHaveBeenCalledTimes(1);
|
||||
expect(toast.info).toHaveBeenCalledWith(
|
||||
'A new version of SigNoz is available',
|
||||
expect.objectContaining({
|
||||
id: NEW_VERSION_TOAST_ID,
|
||||
description: 'It loads when you open another page.',
|
||||
duration: Infinity,
|
||||
action: expect.objectContaining({ label: 'Refresh' }),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
describe('NewVersionPrompt', () => {
|
||||
const originalVersion = process.env.VERSION;
|
||||
let servedVersion: string;
|
||||
let versionRequests: number;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env.VERSION = BUNDLE_VERSION;
|
||||
useAppVersionStore.setState({ isNewVersionAvailable: false });
|
||||
useUnsavedChangesStore.setState({ count: 0 });
|
||||
consumeAppReload();
|
||||
|
||||
servedVersion = BUNDLE_VERSION;
|
||||
versionRequests = 0;
|
||||
server.use(
|
||||
rest.get(VERSION_ENDPOINT, (_req, res, ctx) => {
|
||||
versionRequests += 1;
|
||||
const body: Info = { version: '', ee: 'Y', setupCompleted: true };
|
||||
return res(ctx.set(SERVER_VERSION_HEADER, servedVersion), ctx.json(body));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.VERSION = originalVersion;
|
||||
});
|
||||
|
||||
it('prompts to refresh once the server runs a different build than the tab', async () => {
|
||||
render(<NewVersionPrompt />);
|
||||
await waitFor(() => expect(versionRequests).toBe(1));
|
||||
expect(toast.info).not.toHaveBeenCalled();
|
||||
|
||||
servedVersion = NEW_VERSION;
|
||||
fireEvent.focus(window);
|
||||
|
||||
await waitFor(expectRefreshPrompt);
|
||||
});
|
||||
|
||||
it('stays quiet while the server runs the same build or either version is unknown', () => {
|
||||
reportServerVersion(BUNDLE_VERSION);
|
||||
reportServerVersion('');
|
||||
process.env.VERSION = '';
|
||||
reportServerVersion(NEW_VERSION);
|
||||
|
||||
expect(useAppVersionStore.getState().isNewVersionAvailable).toBe(false);
|
||||
});
|
||||
|
||||
it('waits until unsaved changes are saved or discarded', () => {
|
||||
render(<NewVersionPrompt />);
|
||||
|
||||
let releaseUnsavedChanges = (): void => undefined;
|
||||
act(() => {
|
||||
releaseUnsavedChanges = holdUnsavedChanges();
|
||||
reportServerVersion(NEW_VERSION);
|
||||
});
|
||||
expect(toast.info).not.toHaveBeenCalled();
|
||||
|
||||
act(() => releaseUnsavedChanges());
|
||||
expectRefreshPrompt();
|
||||
});
|
||||
|
||||
it('opens the next page with a full page load once a new version is out', () => {
|
||||
render(<NewVersionPrompt />, undefined, { initialRoute: '/home' });
|
||||
act(() => reportServerVersion(NEW_VERSION));
|
||||
|
||||
act(() => navigate('/home?search=cpu'));
|
||||
expect(reloadApp).not.toHaveBeenCalled();
|
||||
expect(getCurrentLocation().search).toBe('?search=cpu');
|
||||
|
||||
act(() => navigate('/dashboard'));
|
||||
expect(toast.loading).toHaveBeenCalledWith(
|
||||
'Loading the latest version of SigNoz',
|
||||
expect.objectContaining({ id: NEW_VERSION_TOAST_ID }),
|
||||
);
|
||||
expect(reloadApp).toHaveBeenCalledWith(
|
||||
{ reason: 'navigation', detail: '/dashboard' },
|
||||
'/dashboard',
|
||||
);
|
||||
expect(getCurrentLocation().pathname).toBe('/home');
|
||||
});
|
||||
|
||||
it('says SigNoz was updated when the reload landed on a new build', () => {
|
||||
process.env.VERSION = 'v0.099.0';
|
||||
markAppReload({ reason: 'navigation', detail: '/dashboard' });
|
||||
process.env.VERSION = BUNDLE_VERSION;
|
||||
|
||||
render(<NewVersionPrompt />);
|
||||
|
||||
expect(toast.info).toHaveBeenCalledWith('SigNoz was updated', {
|
||||
description: 'The page reloaded to load the latest version.',
|
||||
});
|
||||
expect(consumeAppReload()).toBeNull();
|
||||
});
|
||||
|
||||
it('says part of the page failed to load when the reload kept the same build', () => {
|
||||
markAppReload({
|
||||
reason: 'chunk-load-failed',
|
||||
detail: 'Failed to fetch dynamically imported module: /assets/Home.js',
|
||||
});
|
||||
|
||||
render(<NewVersionPrompt />);
|
||||
|
||||
expect(toast.info).toHaveBeenCalledWith('The page reloaded', {
|
||||
description: 'Part of the page failed to load.',
|
||||
});
|
||||
});
|
||||
});
|
||||
3
frontend/src/components/NewVersionPrompt/constants.ts
Normal file
3
frontend/src/components/NewVersionPrompt/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const SERVER_VERSION_HEADER = 'x-signoz-version';
|
||||
|
||||
export const NEW_VERSION_TOAST_ID = 'new-version';
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useEffect } from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { consumeAppReload } from 'utils/appReload';
|
||||
|
||||
export function useAppReloadNotice(): void {
|
||||
useEffect(() => {
|
||||
const reload = consumeAppReload();
|
||||
if (!reload) {
|
||||
return;
|
||||
}
|
||||
const toVersion = process.env.VERSION;
|
||||
const isUpdated = reload.fromVersion !== toVersion;
|
||||
const data = { ...reload, toVersion };
|
||||
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'app.reload',
|
||||
message: isUpdated
|
||||
? 'Page reloaded to load the latest version'
|
||||
: 'Page reloaded',
|
||||
data,
|
||||
});
|
||||
void logEvent('App: Reloaded', data);
|
||||
|
||||
if (isUpdated) {
|
||||
toast.info('SigNoz was updated', {
|
||||
description: 'The page reloaded to load the latest version.',
|
||||
});
|
||||
} else {
|
||||
toast.info('The page reloaded', {
|
||||
description: 'Part of the page failed to load.',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface AppVersionState {
|
||||
isNewVersionAvailable: boolean;
|
||||
}
|
||||
|
||||
export const useAppVersionStore = create<AppVersionState>()(() => ({
|
||||
isNewVersionAvailable: false,
|
||||
}));
|
||||
|
||||
export const reportServerVersion = (serverVersion: string): void => {
|
||||
const bundleVersion = process.env.VERSION;
|
||||
if (bundleVersion && serverVersion && serverVersion !== bundleVersion) {
|
||||
useAppVersionStore.setState({ isNewVersionAvailable: true });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useQuery } from 'react-query';
|
||||
import getUserVersion from 'api/v1/version/get';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
export function useCheckVersionOnTabReturn(): void {
|
||||
const { isLoggedIn, user } = useAppContext();
|
||||
useQuery({
|
||||
queryFn: getUserVersion,
|
||||
queryKey: ['getUserVersion', user?.accessJwt],
|
||||
enabled: isLoggedIn,
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
blockNavigation,
|
||||
getCurrentLocation,
|
||||
getHref,
|
||||
} from 'lib/router/navigation';
|
||||
import { reloadApp } from 'utils/appReload';
|
||||
|
||||
import { NEW_VERSION_TOAST_ID } from './constants';
|
||||
|
||||
export function useLoadNewVersionOnNavigation(enabled: boolean): void {
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
return undefined;
|
||||
}
|
||||
return blockNavigation(({ location, retry }) => {
|
||||
if (location.pathname === getCurrentLocation().pathname) {
|
||||
retry();
|
||||
return;
|
||||
}
|
||||
// sonner merges an update into the toast with the same id, so clear the prompt's description and action.
|
||||
toast.loading('Loading the latest version of SigNoz', {
|
||||
id: NEW_VERSION_TOAST_ID,
|
||||
description: undefined,
|
||||
action: undefined,
|
||||
});
|
||||
reloadApp(
|
||||
{ reason: 'navigation', detail: location.pathname },
|
||||
getHref(location),
|
||||
);
|
||||
});
|
||||
}, [enabled]);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum SESSIONSTORAGE {
|
||||
RETRY_LAZY_REFRESHED = 'retry-lazy-refreshed',
|
||||
VIEW_PANEL_HANDOFF = 'view-panel-handoff',
|
||||
APP_RELOAD = 'app-reload',
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
|
||||
import NewVersionPrompt from 'components/NewVersionPrompt/NewVersionPrompt';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
@@ -846,6 +847,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
<Toaster
|
||||
offset={{ bottom: 'calc(var(--bottom-strip-height, 0px) + 24px)' }}
|
||||
/>
|
||||
<NewVersionPrompt />
|
||||
</Layout>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { renderHook } from 'tests/test-utils';
|
||||
|
||||
import { useUnloadPrompt } from '../useUnloadPrompt';
|
||||
import { useUnsavedChangesStore } from '../useUnsavedChangesStore';
|
||||
|
||||
function fireBeforeUnload(): Event {
|
||||
const event = new Event('beforeunload', { cancelable: true });
|
||||
@@ -29,4 +30,14 @@ describe('useUnloadPrompt', () => {
|
||||
|
||||
expect(fireBeforeUnload().defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('marks the page as having unsaved changes until they are saved or discarded', () => {
|
||||
const { rerender } = renderHook(({ enabled }) => useUnloadPrompt(enabled), {
|
||||
initialProps: { enabled: true },
|
||||
});
|
||||
expect(useUnsavedChangesStore.getState().count).toBe(1);
|
||||
|
||||
rerender({ enabled: false });
|
||||
expect(useUnsavedChangesStore.getState().count).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useUnloadPrompt } from 'hooks/useUnloadPrompt';
|
||||
import { blockNavigation } from 'lib/router/navigation';
|
||||
import type {
|
||||
AppLocation,
|
||||
@@ -88,21 +89,7 @@ export function useNavigationBlocker(
|
||||
};
|
||||
}, [shouldBlock]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleBeforeUnload = (event: BeforeUnloadEvent): void => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
return (): void => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [shouldBlock]);
|
||||
useUnloadPrompt(shouldBlock);
|
||||
|
||||
const confirmNavigation = useCallback((): void => {
|
||||
if (!blockedNavigation) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { holdUnsavedChanges } from 'hooks/useUnsavedChangesStore';
|
||||
|
||||
/** While `enabled`, a refresh or tab close asks the browser to confirm leaving. */
|
||||
/**
|
||||
* While `enabled`, a refresh or tab close asks the browser to confirm leaving,
|
||||
* and the new-version prompt waits.
|
||||
*/
|
||||
export function useUnloadPrompt(enabled: boolean): void {
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
@@ -10,8 +14,10 @@ export function useUnloadPrompt(enabled: boolean): void {
|
||||
event.preventDefault();
|
||||
};
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
const releaseUnsavedChanges = holdUnsavedChanges();
|
||||
return (): void => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
releaseUnsavedChanges();
|
||||
};
|
||||
}, [enabled]);
|
||||
}
|
||||
15
frontend/src/hooks/useUnsavedChangesStore.ts
Normal file
15
frontend/src/hooks/useUnsavedChangesStore.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface UnsavedChangesState {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const useUnsavedChangesStore = create<UnsavedChangesState>()(() => ({
|
||||
count: 0,
|
||||
}));
|
||||
|
||||
export const holdUnsavedChanges = (): (() => void) => {
|
||||
useUnsavedChangesStore.setState((state) => ({ count: state.count + 1 }));
|
||||
return (): void =>
|
||||
useUnsavedChangesStore.setState((state) => ({ count: state.count - 1 }));
|
||||
};
|
||||
@@ -51,6 +51,10 @@ export function getCurrentLocation(): AppLocation {
|
||||
return toAppLocation(history.location);
|
||||
}
|
||||
|
||||
export function getHref(location: AppLocation): string {
|
||||
return history.createHref(location);
|
||||
}
|
||||
|
||||
export function blockNavigation(blocker: NavigationBlocker): () => void {
|
||||
return history.block((location, action) => {
|
||||
const appLocation = toAppLocation(location);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useConfirmableAction } from 'hooks/useConfirmableAction';
|
||||
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
|
||||
|
||||
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
|
||||
import { useUnloadPrompt } from '../hooks/useUnloadPrompt';
|
||||
import { useUnloadPrompt } from 'hooks/useUnloadPrompt';
|
||||
import styles from './Header.module.scss';
|
||||
import type { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
|
||||
|
||||
|
||||
47
frontend/src/utils/__tests__/lazyWithRetries.test.ts
Normal file
47
frontend/src/utils/__tests__/lazyWithRetries.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import setSessionStorageApi from 'api/browser/sessionstorage/set';
|
||||
import { SESSIONSTORAGE } from 'constants/sessionStorage';
|
||||
import { consumeAppReload } from 'utils/appReload';
|
||||
import { lazyRetry } from 'utils/lazyWithRetries';
|
||||
|
||||
const chunkError = new Error(
|
||||
'Failed to fetch dynamically imported module: /assets/DashboardPage-DdQ20L2t.js',
|
||||
);
|
||||
|
||||
const settleWithinATick = (promise: Promise<unknown>): Promise<string> =>
|
||||
Promise.race([
|
||||
promise.then(
|
||||
() => 'resolved',
|
||||
() => 'rejected',
|
||||
),
|
||||
new Promise<string>((resolve) => {
|
||||
setTimeout(() => resolve('pending'), 0);
|
||||
}),
|
||||
]);
|
||||
|
||||
describe('lazyRetry', () => {
|
||||
beforeEach(() => {
|
||||
setSessionStorageApi(SESSIONSTORAGE.RETRY_LAZY_REFRESHED, 'false');
|
||||
consumeAppReload();
|
||||
});
|
||||
|
||||
it('keeps loading and records why while it reloads after a chunk fails', async () => {
|
||||
const outcome = await settleWithinATick(
|
||||
lazyRetry(() => Promise.reject(chunkError)),
|
||||
);
|
||||
|
||||
expect(outcome).toBe('pending');
|
||||
expect(consumeAppReload()).toStrictEqual({
|
||||
reason: 'chunk-load-failed',
|
||||
detail: chunkError.message,
|
||||
});
|
||||
});
|
||||
|
||||
it('fails without reloading when the chunk is still missing after the reload', async () => {
|
||||
setSessionStorageApi(SESSIONSTORAGE.RETRY_LAZY_REFRESHED, 'true');
|
||||
|
||||
await expect(lazyRetry(() => Promise.reject(chunkError))).rejects.toBe(
|
||||
chunkError,
|
||||
);
|
||||
expect(consumeAppReload()).toBeNull();
|
||||
});
|
||||
});
|
||||
46
frontend/src/utils/appReload.ts
Normal file
46
frontend/src/utils/appReload.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import getSessionStorageApi from 'api/browser/sessionstorage/get';
|
||||
import removeSessionStorageApi from 'api/browser/sessionstorage/remove';
|
||||
import setSessionStorageApi from 'api/browser/sessionstorage/set';
|
||||
import { SESSIONSTORAGE } from 'constants/sessionStorage';
|
||||
|
||||
export interface AppReload {
|
||||
reason: 'refresh-clicked' | 'navigation' | 'chunk-load-failed';
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface RecordedAppReload extends AppReload {
|
||||
fromVersion?: string;
|
||||
}
|
||||
|
||||
export function markAppReload(reload: AppReload): boolean {
|
||||
const recorded: RecordedAppReload = {
|
||||
...reload,
|
||||
fromVersion: process.env.VERSION,
|
||||
};
|
||||
return setSessionStorageApi(
|
||||
SESSIONSTORAGE.APP_RELOAD,
|
||||
JSON.stringify(recorded),
|
||||
);
|
||||
}
|
||||
|
||||
export function consumeAppReload(): RecordedAppReload | null {
|
||||
const stored = getSessionStorageApi(SESSIONSTORAGE.APP_RELOAD);
|
||||
if (!stored) {
|
||||
return null;
|
||||
}
|
||||
removeSessionStorageApi(SESSIONSTORAGE.APP_RELOAD);
|
||||
try {
|
||||
return JSON.parse(stored) as RecordedAppReload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function reloadApp(reload: AppReload, href?: string): void {
|
||||
markAppReload(reload);
|
||||
if (href) {
|
||||
window.location.assign(href);
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import getSessionStorageApi from 'api/browser/sessionstorage/get';
|
||||
import setSessionStorageApi from 'api/browser/sessionstorage/set';
|
||||
import { SESSIONSTORAGE } from 'constants/sessionStorage';
|
||||
import { markAppReload } from 'utils/appReload';
|
||||
|
||||
type ComponentImport = () => Promise<any>;
|
||||
|
||||
@@ -24,7 +25,11 @@ export const lazyRetry = (componentImport: ComponentImport): Promise<any> =>
|
||||
!hasRefreshed &&
|
||||
setSessionStorageApi(SESSIONSTORAGE.RETRY_LAZY_REFRESHED, 'true')
|
||||
) {
|
||||
markAppReload({ reason: 'chunk-load-failed', detail: error.message });
|
||||
window.location.reload();
|
||||
// Left pending, so Suspense keeps its loader up until the reload lands
|
||||
// instead of the error boundary flashing its fallback.
|
||||
return;
|
||||
}
|
||||
|
||||
reject(error);
|
||||
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/subscription"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -322,6 +323,7 @@ func newProvider(
|
||||
provider.authzMiddleware = middleware.NewAuthZ(settings.Logger(), orgGetter, authzService)
|
||||
|
||||
router.Use(middleware.NewRecovery(settings.Logger()).Wrap)
|
||||
router.Use(middleware.NewVersion(version.Info.Version()).Wrap)
|
||||
router.Use(middleware.NewOtel("apiserver", providerSettings.MeterProvider, providerSettings.TracerProvider).Wrap)
|
||||
router.Use(middleware.NewIdentN(identNResolver, sharder, settings.Logger()).Wrap)
|
||||
router.Use(middleware.NewTimeout(settings.Logger(),
|
||||
|
||||
@@ -16,6 +16,7 @@ func NewCors() *Cors {
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control"},
|
||||
ExposedHeaders: []string{versionHeader},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
22
pkg/http/middleware/version.go
Normal file
22
pkg/http/middleware/version.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const versionHeader = "X-Signoz-Version"
|
||||
|
||||
type Version struct {
|
||||
version string
|
||||
}
|
||||
|
||||
func NewVersion(version string) *Version {
|
||||
return &Version{version: version}
|
||||
}
|
||||
|
||||
func (middleware *Version) Wrap(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set(versionHeader, middleware.version)
|
||||
next.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
@@ -16,6 +17,9 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Vite's build.assetsDir.
|
||||
const assetsPathPrefix = "/assets/"
|
||||
|
||||
type provider struct {
|
||||
config web.Config
|
||||
indexContents []byte
|
||||
@@ -83,6 +87,11 @@ func (provider *provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// check whether a file exists or is a directory at the given path
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) && strings.HasPrefix(req.URL.Path, assetsPathPrefix) {
|
||||
http.NotFound(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
// if the file doesn't exist, serve index.html
|
||||
if os.IsNotExist(err) {
|
||||
provider.serveIndex(rw)
|
||||
|
||||
@@ -241,6 +241,39 @@ func TestServeInvalidTemplateIndex(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeMissingPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "Asset_NotFound",
|
||||
path: "/assets/DashboardPage-DdQ20L2t.js",
|
||||
statusCode: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "AppRoute_ServesIndex",
|
||||
path: "/dashboard/0198a2c4",
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
base := startServer(t, web.Config{Index: "valid_template.html", Directory: "testdata"}, global.Config{})
|
||||
|
||||
res, err := http.DefaultClient.Get(base + testCase.path)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
assert.Equal(t, testCase.statusCode, res.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeStaticFilesUnchanged(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user