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:
Yunus M 2025-02-21 14:50:29 +05:30 committed by GitHub
parent bc907b9e61
commit e18bda8480
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 188 additions and 23 deletions

View File

@ -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) {

View File

@ -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',
}

View File

@ -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',
}

View File

@ -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);

View File

@ -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.