mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 12:18:58 +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 afterLogin from 'AppRoutes/utils';
|
||||||
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
import { ENVIRONMENT } from 'constants/env';
|
import { ENVIRONMENT } from 'constants/env';
|
||||||
|
import { Events } from 'constants/events';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { eventEmitter } from 'utils/getEventEmitter';
|
||||||
|
|
||||||
import apiV1, {
|
import apiV1, {
|
||||||
apiAlertManager,
|
apiAlertManager,
|
||||||
@ -18,13 +20,40 @@ import apiV1, {
|
|||||||
} from './apiV1';
|
} from './apiV1';
|
||||||
import { Logout } from './utils';
|
import { Logout } from './utils';
|
||||||
|
|
||||||
|
const RESPONSE_TIMEOUT_THRESHOLD = 5000; // 5 seconds
|
||||||
|
|
||||||
const interceptorsResponse = (
|
const interceptorsResponse = (
|
||||||
value: AxiosResponse<any>,
|
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 = (
|
const interceptorsRequestResponse = (
|
||||||
value: InternalAxiosRequestConfig,
|
value: InternalAxiosRequestConfig,
|
||||||
): 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) || '';
|
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || '';
|
||||||
|
|
||||||
if (value && value.headers) {
|
if (value && value.headers) {
|
||||||
|
@ -2,4 +2,5 @@ export enum Events {
|
|||||||
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
|
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
|
||||||
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
||||||
TABLE_COLUMNS_DATA = 'TABLE_COLUMNS_DATA',
|
TABLE_COLUMNS_DATA = 'TABLE_COLUMNS_DATA',
|
||||||
|
SLOW_API_WARNING = 'SLOW_API_WARNING',
|
||||||
}
|
}
|
||||||
|
@ -25,4 +25,5 @@ export enum LOCALSTORAGE {
|
|||||||
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
|
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
|
||||||
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
|
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
|
||||||
CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS',
|
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;
|
padding: 8px;
|
||||||
background-color: #f25733;
|
background-color: #f25733;
|
||||||
color: white;
|
color: white;
|
||||||
text-align: center;
|
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 {
|
.payment-failed-banner {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: var(--bg-sakura-500);
|
background-color: var(--bg-sakura-500);
|
||||||
|
@ -6,13 +6,18 @@ import './AppLayout.styles.scss';
|
|||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
import manageCreditCardApi from 'api/billing/manage';
|
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 getUserLatestVersion from 'api/user/getLatestVersion';
|
||||||
import getUserVersion from 'api/user/getVersion';
|
import getUserVersion from 'api/user/getVersion';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
|
import { Events } from 'constants/events';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import SideNav from 'container/SideNav';
|
import SideNav from 'container/SideNav';
|
||||||
import TopNav from 'container/TopNav';
|
import TopNav from 'container/TopNav';
|
||||||
@ -24,7 +29,14 @@ import { isNull } from 'lodash-es';
|
|||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
|
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
|
||||||
import { useAppContext } from 'providers/App/App';
|
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 { Helmet } from 'react-helmet-async';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMutation, useQueries } from 'react-query';
|
import { useMutation, useQueries } from 'react-query';
|
||||||
@ -41,7 +53,9 @@ import {
|
|||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||||
|
import { USER_ROLES } from 'types/roles';
|
||||||
import { isCloudUser } from 'utils/app';
|
import { isCloudUser } from 'utils/app';
|
||||||
|
import { eventEmitter } from 'utils/getEventEmitter';
|
||||||
import {
|
import {
|
||||||
getFormattedDate,
|
getFormattedDate,
|
||||||
getFormattedDateWithMinutes,
|
getFormattedDateWithMinutes,
|
||||||
@ -72,6 +86,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
setShowPaymentFailedWarning,
|
setShowPaymentFailedWarning,
|
||||||
] = useState<boolean>(false);
|
] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
||||||
|
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
||||||
|
|
||||||
const handleBillingOnSuccess = (
|
const handleBillingOnSuccess = (
|
||||||
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
||||||
): void => {
|
): void => {
|
||||||
@ -261,22 +278,25 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
setShowTrialExpiryBanner(false);
|
setShowTrialExpiryBanner(false);
|
||||||
setShowPaymentFailedWarning(false);
|
setShowPaymentFailedWarning(false);
|
||||||
|
setShowSlowApiWarning(false);
|
||||||
}
|
}
|
||||||
}, [isLoggedIn]);
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
const handleUpgrade = (): void => {
|
const handleUpgrade = useCallback((): void => {
|
||||||
if (user.role === 'ADMIN') {
|
if (user.role === USER_ROLES.ADMIN) {
|
||||||
history.push(ROUTES.BILLING);
|
history.push(ROUTES.BILLING);
|
||||||
}
|
}
|
||||||
};
|
}, [user.role]);
|
||||||
|
|
||||||
const handleFailedPayment = (): void => {
|
const handleFailedPayment = useCallback((): void => {
|
||||||
manageCreditCard({
|
if (activeLicenseV3?.key) {
|
||||||
licenseKey: activeLicenseV3?.key || '',
|
manageCreditCard({
|
||||||
successURL: window.location.origin,
|
licenseKey: activeLicenseV3?.key || '',
|
||||||
cancelURL: window.location.origin,
|
successURL: window.location.origin,
|
||||||
});
|
cancelURL: window.location.origin,
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
}, [activeLicenseV3?.key, manageCreditCard]);
|
||||||
|
|
||||||
const isLogsView = (): boolean =>
|
const isLogsView = (): boolean =>
|
||||||
routeKey === 'LOGS' ||
|
routeKey === 'LOGS' ||
|
||||||
@ -360,6 +380,107 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
licenses,
|
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 (
|
return (
|
||||||
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
|
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -370,7 +491,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
<div className="trial-expiry-banner">
|
<div className="trial-expiry-banner">
|
||||||
You are in free trial period. Your free trial will end on{' '}
|
You are in free trial period. Your free trial will end on{' '}
|
||||||
<span>{getFormattedDate(licenses?.trialEnd || Date.now())}.</span>
|
<span>{getFormattedDate(licenses?.trialEnd || Date.now())}.</span>
|
||||||
{user.role === 'ADMIN' ? (
|
{user.role === USER_ROLES.ADMIN ? (
|
||||||
<span>
|
<span>
|
||||||
{' '}
|
{' '}
|
||||||
Please{' '}
|
Please{' '}
|
||||||
@ -384,6 +505,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!showTrialExpiryBanner && showPaymentFailedWarning && (
|
{!showTrialExpiryBanner && showPaymentFailedWarning && (
|
||||||
<div className="payment-failed-banner">
|
<div className="payment-failed-banner">
|
||||||
Your bill payment has failed. Your workspace will get suspended on{' '}
|
Your bill payment has failed. Your workspace will get suspended on{' '}
|
||||||
@ -393,18 +515,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
.
|
.
|
||||||
</span>
|
</span>
|
||||||
{user.role === 'ADMIN' ? (
|
{user.role === USER_ROLES.ADMIN ? (
|
||||||
<span>
|
<span>
|
||||||
{' '}
|
{' '}
|
||||||
Please{' '}
|
Please{' '}
|
||||||
<a
|
<a className="upgrade-link" onClick={handleFailedPayment}>
|
||||||
className="upgrade-link"
|
|
||||||
onClick={(): void => {
|
|
||||||
if (!isLoadingManageBilling) {
|
|
||||||
handleFailedPayment();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
pay the bill
|
pay the bill
|
||||||
</a>
|
</a>
|
||||||
to continue using SigNoz features.
|
to continue using SigNoz features.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user