From e18bda848034d54eec57d12826e213e6a2dd7d4b Mon Sep 17 00:00:00 2001 From: Yunus M Date: Fri, 21 Feb 2025 14:50:29 +0530 Subject: [PATCH] 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> --- frontend/src/api/index.ts | 31 +++- frontend/src/constants/events.ts | 1 + frontend/src/constants/localStorage.ts | 1 + .../container/AppLayout/AppLayout.styles.scss | 21 ++- frontend/src/container/AppLayout/index.tsx | 157 +++++++++++++++--- 5 files changed, 188 insertions(+), 23 deletions(-) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 5b1d89f496..8435bb37ff 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -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, -): Promise> => Promise.resolve(value); +): Promise> => { + 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) { diff --git a/frontend/src/constants/events.ts b/frontend/src/constants/events.ts index 1b6a0e6feb..fe16e83d92 100644 --- a/frontend/src/constants/events.ts +++ b/frontend/src/constants/events.ts @@ -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', } diff --git a/frontend/src/constants/localStorage.ts b/frontend/src/constants/localStorage.ts index 659093f5ec..806aa87379 100644 --- a/frontend/src/constants/localStorage.ts +++ b/frontend/src/constants/localStorage.ts @@ -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', } diff --git a/frontend/src/container/AppLayout/AppLayout.styles.scss b/frontend/src/container/AppLayout/AppLayout.styles.scss index 6bd06caded..569e96f388 100644 --- a/frontend/src/container/AppLayout/AppLayout.styles.scss +++ b/frontend/src/container/AppLayout/AppLayout.styles.scss @@ -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); diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index ab73a3fbb7..4e0fe0a1c0 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -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(false); + const [showSlowApiWarning, setShowSlowApiWarning] = useState(false); + const [slowApiWarningShown, setSlowApiWarningShown] = useState(false); + const handleBillingOnSuccess = ( data: ErrorResponse | SuccessResponse, ): 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: ( +
+ Our systems are taking longer than expected for your trial workspace. + Please{' '} + {user.role === USER_ROLES.ADMIN ? ( + + { + notifications.destroy('slow-api-warning'); + + logEvent(`Slow API Banner: Upgrade clicked`, {}); + + handleUpgrade(); + }} + > + upgrade + + your workspace for a smoother experience. + + ) : ( + 'contact your administrator for upgrading to a paid plan for a smoother experience.' + )} +
+ ), + duration: 60000, + placement: 'topRight', + onClose: handleDismissSlowApiWarning, + key: 'slow-api-warning', + }); + } + }, [ + showSlowApiWarning, + notifications, + isTrialUser, + licenses?.trialConvertedToSubscription, + user.role, + isLoadingManageBilling, + handleFailedPayment, + slowApiWarningShown, + handleUpgrade, + ]); + return ( @@ -370,7 +491,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
You are in free trial period. Your free trial will end on{' '} {getFormattedDate(licenses?.trialEnd || Date.now())}. - {user.role === 'ADMIN' ? ( + {user.role === USER_ROLES.ADMIN ? ( {' '} Please{' '} @@ -384,6 +505,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { )}
)} + {!showTrialExpiryBanner && showPaymentFailedWarning && (
Your bill payment has failed. Your workspace will get suspended on{' '} @@ -393,18 +515,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element { )} . - {user.role === 'ADMIN' ? ( + {user.role === USER_ROLES.ADMIN ? ( {' '} Please{' '} - { - if (!isLoadingManageBilling) { - handleFailedPayment(); - } - }} - > + pay the bill to continue using SigNoz features.