mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 20:29:04 +08:00
feat: show a alert if api takes more than 5 secs (#7112)
* feat: show a banner if api takes more than 5 secs * Update frontend/src/container/AppLayout/index.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * feat: show a banner if api takes more than 5 secs * feat: show toast message with upgrade option * feat: log api delays * feat: igmore /events calls --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
parent
bc907b9e61
commit
e18bda8480
@ -6,7 +6,9 @@ import loginApi from 'api/user/login';
|
||||
import afterLogin from 'AppRoutes/utils';
|
||||
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { Events } from 'constants/events';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
|
||||
import apiV1, {
|
||||
apiAlertManager,
|
||||
@ -18,13 +20,40 @@ import apiV1, {
|
||||
} from './apiV1';
|
||||
import { Logout } from './utils';
|
||||
|
||||
const RESPONSE_TIMEOUT_THRESHOLD = 5000; // 5 seconds
|
||||
|
||||
const interceptorsResponse = (
|
||||
value: AxiosResponse<any>,
|
||||
): Promise<AxiosResponse<any>> => Promise.resolve(value);
|
||||
): Promise<AxiosResponse<any>> => {
|
||||
if ((value.config as any)?.metadata) {
|
||||
const duration =
|
||||
new Date().getTime() - (value.config as any).metadata.startTime;
|
||||
|
||||
if (duration > RESPONSE_TIMEOUT_THRESHOLD && value.config.url !== '/event') {
|
||||
eventEmitter.emit(Events.SLOW_API_WARNING, true, {
|
||||
duration,
|
||||
url: value.config.url,
|
||||
threshold: RESPONSE_TIMEOUT_THRESHOLD,
|
||||
});
|
||||
|
||||
console.warn(
|
||||
`[API Warning] Request to ${value.config.url} took ${duration}ms`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(value);
|
||||
};
|
||||
|
||||
const interceptorsRequestResponse = (
|
||||
value: InternalAxiosRequestConfig,
|
||||
): InternalAxiosRequestConfig => {
|
||||
// Attach metadata safely (not sent with the request)
|
||||
Object.defineProperty(value, 'metadata', {
|
||||
value: { startTime: new Date().getTime() },
|
||||
enumerable: false, // Prevents it from being included in the request
|
||||
});
|
||||
|
||||
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || '';
|
||||
|
||||
if (value && value.headers) {
|
||||
|
@ -2,4 +2,5 @@ export enum Events {
|
||||
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
|
||||
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
||||
TABLE_COLUMNS_DATA = 'TABLE_COLUMNS_DATA',
|
||||
SLOW_API_WARNING = 'SLOW_API_WARNING',
|
||||
}
|
||||
|
@ -25,4 +25,5 @@ export enum LOCALSTORAGE {
|
||||
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
|
||||
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
|
||||
CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS',
|
||||
DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING',
|
||||
}
|
||||
|
@ -101,13 +101,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
.trial-expiry-banner {
|
||||
.trial-expiry-banner,
|
||||
.slow-api-warning-banner {
|
||||
padding: 8px;
|
||||
background-color: #f25733;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.slow-api-warning-banner {
|
||||
background-color: var(--bg-robin-500);
|
||||
|
||||
.dismiss-banner {
|
||||
color: white;
|
||||
float: right;
|
||||
padding: 0 16px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.payment-failed-banner {
|
||||
padding: 8px;
|
||||
background-color: var(--bg-sakura-500);
|
||||
|
@ -6,13 +6,18 @@ import './AppLayout.styles.scss';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Flex } from 'antd';
|
||||
import manageCreditCardApi from 'api/billing/manage';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||
import getUserVersion from 'api/user/getVersion';
|
||||
import cx from 'classnames';
|
||||
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { Events } from 'constants/events';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import SideNav from 'container/SideNav';
|
||||
import TopNav from 'container/TopNav';
|
||||
@ -24,7 +29,14 @@ import { isNull } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQueries } from 'react-query';
|
||||
@ -41,7 +53,9 @@ import {
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import {
|
||||
getFormattedDate,
|
||||
getFormattedDateWithMinutes,
|
||||
@ -72,6 +86,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
setShowPaymentFailedWarning,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
||||
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
||||
|
||||
const handleBillingOnSuccess = (
|
||||
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
||||
): void => {
|
||||
@ -261,22 +278,25 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
if (!isLoggedIn) {
|
||||
setShowTrialExpiryBanner(false);
|
||||
setShowPaymentFailedWarning(false);
|
||||
setShowSlowApiWarning(false);
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
const handleUpgrade = (): void => {
|
||||
if (user.role === 'ADMIN') {
|
||||
const handleUpgrade = useCallback((): void => {
|
||||
if (user.role === USER_ROLES.ADMIN) {
|
||||
history.push(ROUTES.BILLING);
|
||||
}
|
||||
};
|
||||
}, [user.role]);
|
||||
|
||||
const handleFailedPayment = (): void => {
|
||||
manageCreditCard({
|
||||
licenseKey: activeLicenseV3?.key || '',
|
||||
successURL: window.location.origin,
|
||||
cancelURL: window.location.origin,
|
||||
});
|
||||
};
|
||||
const handleFailedPayment = useCallback((): void => {
|
||||
if (activeLicenseV3?.key) {
|
||||
manageCreditCard({
|
||||
licenseKey: activeLicenseV3?.key || '',
|
||||
successURL: window.location.origin,
|
||||
cancelURL: window.location.origin,
|
||||
});
|
||||
}
|
||||
}, [activeLicenseV3?.key, manageCreditCard]);
|
||||
|
||||
const isLogsView = (): boolean =>
|
||||
routeKey === 'LOGS' ||
|
||||
@ -360,6 +380,107 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
licenses,
|
||||
]);
|
||||
|
||||
// Listen for API warnings
|
||||
const handleWarning = (
|
||||
isSlow: boolean,
|
||||
data: { duration: number; url: string; threshold: number },
|
||||
): void => {
|
||||
const dontShowSlowApiWarning = getLocalStorageApi(
|
||||
LOCALSTORAGE.DONT_SHOW_SLOW_API_WARNING,
|
||||
);
|
||||
|
||||
logEvent(`Slow API Warning`, {
|
||||
duration: `${data.duration}ms`,
|
||||
url: data.url,
|
||||
threshold: data.threshold,
|
||||
});
|
||||
|
||||
const isDontShowSlowApiWarning = dontShowSlowApiWarning === 'true';
|
||||
|
||||
if (isDontShowSlowApiWarning) {
|
||||
setShowSlowApiWarning(false);
|
||||
} else {
|
||||
setShowSlowApiWarning(isSlow);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
eventEmitter.on(Events.SLOW_API_WARNING, handleWarning);
|
||||
|
||||
return (): void => {
|
||||
eventEmitter.off(Events.SLOW_API_WARNING, handleWarning);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isTrialUser = useMemo(
|
||||
(): boolean =>
|
||||
(!isFetchingLicenses &&
|
||||
licenses &&
|
||||
licenses.onTrial &&
|
||||
!licenses.trialConvertedToSubscription) ||
|
||||
false,
|
||||
[licenses, isFetchingLicenses],
|
||||
);
|
||||
|
||||
const handleDismissSlowApiWarning = (): void => {
|
||||
setShowSlowApiWarning(false);
|
||||
|
||||
setLocalStorageApi(LOCALSTORAGE.DONT_SHOW_SLOW_API_WARNING, 'true');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
showSlowApiWarning &&
|
||||
isTrialUser &&
|
||||
!licenses?.trialConvertedToSubscription &&
|
||||
!slowApiWarningShown
|
||||
) {
|
||||
setSlowApiWarningShown(true);
|
||||
|
||||
notifications.info({
|
||||
message: (
|
||||
<div>
|
||||
Our systems are taking longer than expected for your trial workspace.
|
||||
Please{' '}
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
<a
|
||||
className="upgrade-link"
|
||||
onClick={(): void => {
|
||||
notifications.destroy('slow-api-warning');
|
||||
|
||||
logEvent(`Slow API Banner: Upgrade clicked`, {});
|
||||
|
||||
handleUpgrade();
|
||||
}}
|
||||
>
|
||||
upgrade
|
||||
</a>
|
||||
your workspace for a smoother experience.
|
||||
</span>
|
||||
) : (
|
||||
'contact your administrator for upgrading to a paid plan for a smoother experience.'
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
duration: 60000,
|
||||
placement: 'topRight',
|
||||
onClose: handleDismissSlowApiWarning,
|
||||
key: 'slow-api-warning',
|
||||
});
|
||||
}
|
||||
}, [
|
||||
showSlowApiWarning,
|
||||
notifications,
|
||||
isTrialUser,
|
||||
licenses?.trialConvertedToSubscription,
|
||||
user.role,
|
||||
isLoadingManageBilling,
|
||||
handleFailedPayment,
|
||||
slowApiWarningShown,
|
||||
handleUpgrade,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||
<Helmet>
|
||||
@ -370,7 +491,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
<div className="trial-expiry-banner">
|
||||
You are in free trial period. Your free trial will end on{' '}
|
||||
<span>{getFormattedDate(licenses?.trialEnd || Date.now())}.</span>
|
||||
{user.role === 'ADMIN' ? (
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
@ -384,6 +505,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showTrialExpiryBanner && showPaymentFailedWarning && (
|
||||
<div className="payment-failed-banner">
|
||||
Your bill payment has failed. Your workspace will get suspended on{' '}
|
||||
@ -393,18 +515,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
)}
|
||||
.
|
||||
</span>
|
||||
{user.role === 'ADMIN' ? (
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a
|
||||
className="upgrade-link"
|
||||
onClick={(): void => {
|
||||
if (!isLoadingManageBilling) {
|
||||
handleFailedPayment();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<a className="upgrade-link" onClick={handleFailedPayment}>
|
||||
pay the bill
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
|
Loading…
x
Reference in New Issue
Block a user