fix: licenses in community edition & improve messaging (#7456)

Enhance platform to handle cloud, self-hosted, community, and enterprise user types with tailored routing, error handling, and feature access.
This commit is contained in:
Yunus M 2025-04-02 01:12:42 +05:30 committed by GitHub
parent 07a244f569
commit 597752a4bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 226 additions and 158 deletions

View File

@ -1,3 +1,4 @@
import * as Sentry from '@sentry/react';
import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
@ -15,6 +16,7 @@ import { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import { NotificationProvider } from 'hooks/useNotifications';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import history from 'lib/history';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import posthog from 'posthog-js';
import AlertRuleProvider from 'providers/Alert';
import { useAppContext } from 'providers/App/App';
@ -46,7 +48,6 @@ function App(): JSX.Element {
activeLicenseV3,
isFetchingActiveLicenseV3,
userFetchError,
licensesFetchError,
featureFlagsFetchError,
isLoggedIn: isLoggedInState,
featureFlags,
@ -56,10 +57,7 @@ function App(): JSX.Element {
const { hostname, pathname } = window.location;
const {
isCloudUser: isCloudUserVal,
isEECloudUser: isEECloudUserVal,
} = useGetTenantLicense();
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
const enableAnalytics = useCallback(
(user: IUser): void => {
@ -169,7 +167,7 @@ function App(): JSX.Element {
let updatedRoutes = defaultRoutes;
// if the user is a cloud user
if (isCloudUserVal || isEECloudUserVal) {
if (isCloudUser || isEnterpriseSelfHostedUser) {
// if the user is on basic plan then remove billing
if (isOnBasicPlan) {
updatedRoutes = updatedRoutes.filter(
@ -191,10 +189,10 @@ function App(): JSX.Element {
isLoggedInState,
user,
licenses,
isCloudUserVal,
isCloudUser,
isEnterpriseSelfHostedUser,
isFetchingLicenses,
isFetchingUser,
isEECloudUserVal,
]);
useEffect(() => {
@ -209,6 +207,7 @@ function App(): JSX.Element {
}
}, [pathname]);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
// feature flag shouldn't be loading and featureFlags or fetchError any one of this should be true indicating that req is complete
// licenses should also be present. there is no check for licenses for loading and error as that is mandatory if not present then routing
@ -234,7 +233,12 @@ function App(): JSX.Element {
const showAddCreditCardModal =
!isPremiumSupportEnabled && !trialInfo?.trialConvertedToSubscription;
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
if (
isLoggedInState &&
isChatSupportEnabled &&
!showAddCreditCardModal &&
(isCloudUser || isEnterpriseSelfHostedUser)
) {
window.Intercom('boot', {
app_id: process.env.INTERCOM_APP_ID,
email: user?.email || '',
@ -253,13 +257,53 @@ function App(): JSX.Element {
licenses,
activeLicenseV3,
trialInfo,
isCloudUser,
isEnterpriseSelfHostedUser,
]);
useEffect(() => {
if (!isFetchingUser && isCloudUserVal && user && user.email) {
if (!isFetchingUser && isCloudUser && user && user.email) {
enableAnalytics(user);
}
}, [user, isFetchingUser, isCloudUserVal, enableAnalytics]);
}, [user, isFetchingUser, isCloudUser, enableAnalytics]);
useEffect(() => {
if (isCloudUser || isEnterpriseSelfHostedUser) {
if (process.env.POSTHOG_KEY) {
posthog.init(process.env.POSTHOG_KEY, {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
});
}
Sentry.init({
dsn: process.env.SENTRY_DSN,
tunnel: process.env.TUNNEL_URL,
environment: 'production',
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: [],
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
});
} else {
posthog.reset();
Sentry.close();
if (window.cioanalytics && typeof window.cioanalytics.reset === 'function') {
window.cioanalytics.reset();
}
}
}, [isCloudUser, isEnterpriseSelfHostedUser]);
// if the user is in logged in state
if (isLoggedInState) {
@ -271,61 +315,55 @@ function App(): JSX.Element {
// if the required calls fails then return a something went wrong error
// this needs to be on top of data missing error because if there is an error, data will never be loaded and it will
// move to indefinitive loading
if (
(userFetchError || licensesFetchError) &&
pathname !== ROUTES.SOMETHING_WENT_WRONG
) {
if (userFetchError && pathname !== ROUTES.SOMETHING_WENT_WRONG) {
history.replace(ROUTES.SOMETHING_WENT_WRONG);
}
// if all of the data is not set then return a spinner, this is required because there is some gap between loading states and data setting
if (
(!licenses || !user.email || !featureFlags) &&
!userFetchError &&
!licensesFetchError
) {
if ((!licenses || !user.email || !featureFlags) && !userFetchError) {
return <Spinner tip="Loading..." />;
}
}
return (
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<NotificationProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</NotificationProvider>
</CompatRouter>
</Router>
</ConfigProvider>
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<NotificationProvider>
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</NotificationProvider>
</CompatRouter>
</Router>
</ConfigProvider>
</Sentry.ErrorBoundary>
);
}

View File

@ -61,7 +61,7 @@ export default function CustomDomainSettings(): JSX.Element {
isLoading: isLoadingDeploymentsData,
isFetching: isFetchingDeploymentsData,
refetch: refetchDeploymentsData,
} = useGetDeploymentsData();
} = useGetDeploymentsData(true);
const {
mutate: updateSubDomain,

View File

@ -23,10 +23,13 @@ function DataSourceInfo({
const notSendingData = !dataSentToSigNoz;
const isEnabled =
activeLicenseV3 && activeLicenseV3.platform === LicensePlatform.CLOUD;
const {
data: deploymentsData,
isError: isErrorDeploymentsData,
} = useGetDeploymentsData();
} = useGetDeploymentsData(isEnabled || false);
const [region, setRegion] = useState<string>('');
const [url, setUrl] = useState<string>('');

View File

@ -293,7 +293,7 @@ function MultiIngestionSettings(): JSX.Element {
isLoading: isLoadingDeploymentsData,
isFetching: isFetchingDeploymentsData,
isError: isErrorDeploymentsData,
} = useGetDeploymentsData();
} = useGetDeploymentsData(true);
const {
mutate: createIngestionKey,

View File

@ -63,7 +63,7 @@ export default function OnboardingIngestionDetails(): JSX.Element {
isLoading: isLoadingDeploymentsData,
isFetching: isFetchingDeploymentsData,
isError: isDeploymentsDataError,
} = useGetDeploymentsData();
} = useGetDeploymentsData(true);
const handleCopyKey = (text: string): void => {
handleCopyToClipboard(text);

View File

@ -69,6 +69,12 @@
opacity: 1;
transition: all 0.2s;
}
&.community-enterprise-user {
color: var(--bg-sakura-500);
background: rgba(255, 113, 113, 0.1);
border: 1px solid var(--bg-sakura-500);
}
}
.dockBtn {

View File

@ -3,7 +3,7 @@
import './SideNav.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
@ -75,7 +75,7 @@ function SideNav(): JSX.Element {
const [userManagementMenuItems, setUserManagementMenuItems] = useState<
UserManagementMenuItems[]
>([manageLicenseMenuItem]);
>([]);
const onClickSlackHandler = (): void => {
window.open('https://signoz.io/slack', '_blank');
@ -88,8 +88,10 @@ function SideNav(): JSX.Element {
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const {
isCloudUser: isCloudUserVal,
isEECloudUser: isEECloudUserVal,
isCloudUser,
isEnterpriseSelfHostedUser,
isCommunityUser,
isCommunityEnterpriseUser,
} = useGetTenantLicense();
const { t } = useTranslation('');
@ -103,11 +105,6 @@ function SideNav(): JSX.Element {
licenseStatus?.toLocaleLowerCase() ===
LICENSE_PLAN_STATUS.VALID.toLocaleLowerCase();
const isEnterprise = licenses?.licenses?.some(
(license: License) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.ENTERPRISE_PLAN,
);
const onClickSignozCloud = (): void => {
window.open(
'https://signoz.io/oss-to-cloud/?utm_source=product_navbar&utm_medium=frontend&utm_campaign=oss_users',
@ -201,14 +198,21 @@ function SideNav(): JSX.Element {
};
useEffect(() => {
if (isCloudUserVal) {
if (isCloudUser) {
setLicenseTag('Cloud');
} else if (isEnterprise) {
} else if (isEnterpriseSelfHostedUser) {
setLicenseTag('Enterprise');
} else {
setLicenseTag('Free');
} else if (isCommunityEnterpriseUser) {
setLicenseTag('Enterprise');
} else if (isCommunityUser) {
setLicenseTag('Community');
}
}, [isCloudUserVal, isEnterprise]);
}, [
isCloudUser,
isEnterpriseSelfHostedUser,
isCommunityEnterpriseUser,
isCommunityUser,
]);
const [isCurrentOrgSettings] = useComponentPermission(
['current_org_settings'],
@ -290,7 +294,7 @@ function SideNav(): JSX.Element {
);
}
if (isCloudUserVal || isEECloudUserVal) {
if (isCloudUser || isEnterpriseSelfHostedUser) {
const isOnboardingEnabled =
featureFlags?.find((feature) => feature.name === FeatureKeys.ONBOARDING)
?.active || false;
@ -317,6 +321,11 @@ function SideNav(): JSX.Element {
}
updatedUserManagementItems = [helpSupportMenuItem];
// Show manage license menu item for EE cloud users with a active license
if (isEnterpriseSelfHostedUser) {
updatedUserManagementItems.push(manageLicenseMenuItem);
}
} else {
updatedMenuItems = updatedMenuItems.filter(
(item) => item.key !== ROUTES.INTEGRATIONS && item.key !== ROUTES.BILLING,
@ -332,20 +341,21 @@ function SideNav(): JSX.Element {
onClick: onClickVersionHandler,
};
updatedUserManagementItems = [
versionMenuItem,
slackSupportMenuItem,
manageLicenseMenuItem,
];
updatedUserManagementItems = [versionMenuItem, slackSupportMenuItem];
if (isCommunityEnterpriseUser) {
updatedUserManagementItems.push(manageLicenseMenuItem);
}
}
setMenuItems(updatedMenuItems);
setUserManagementMenuItems(updatedUserManagementItems);
}, [
isCommunityEnterpriseUser,
currentVersion,
featureFlags,
isCloudUserVal,
isCloudUser,
isEnterpriseSelfHostedUser,
isCurrentVersionError,
isEECloudUserVal,
isLatestVersion,
licenses?.licenses,
onClickVersionHandler,
@ -372,12 +382,31 @@ function SideNav(): JSX.Element {
</div>
{licenseTag && (
<div className="license tag nav-item-label">{licenseTag}</div>
<Tooltip
title={
// eslint-disable-next-line no-nested-ternary
isCommunityUser
? 'You are running the community version of SigNoz. You have to install the Enterprise edition in order enable Enterprise features.'
: isCommunityEnterpriseUser
? 'You do not have an active license present. Add an active license to enable Enterprise features.'
: ''
}
placement="bottomRight"
>
<div
className={cx(
'license tag nav-item-label',
isCommunityEnterpriseUser && 'community-enterprise-user',
)}
>
{licenseTag}
</div>
</Tooltip>
)}
</div>
</div>
{isCloudUserVal && user?.role !== USER_ROLES.VIEWER && (
{isCloudUser && user?.role !== USER_ROLES.VIEWER && (
<div className="get-started-nav-items">
<Button
className="get-started-btn"
@ -396,7 +425,7 @@ function SideNav(): JSX.Element {
</div>
)}
<div className={cx(`nav-wrapper`, isCloudUserVal && 'nav-wrapper-cloud')}>
<div className={cx(`nav-wrapper`, isCloudUser && 'nav-wrapper-cloud')}>
<div className="primary-nav-items">
{menuItems.map((item, index) => (
<NavItem

View File

@ -3,11 +3,11 @@ import { AxiosError, AxiosResponse } from 'axios';
import { useQuery, UseQueryResult } from 'react-query';
import { DeploymentsDataProps } from 'types/api/customDomain/types';
export const useGetDeploymentsData = (): UseQueryResult<
AxiosResponse<DeploymentsDataProps>,
AxiosError
> =>
export const useGetDeploymentsData = (
isEnabled: boolean,
): UseQueryResult<AxiosResponse<DeploymentsDataProps>, AxiosError> =>
useQuery<AxiosResponse<DeploymentsDataProps>, AxiosError>({
queryKey: ['getDeploymentsData'],
queryFn: () => getDeploymentsData(),
enabled: isEnabled,
});

View File

@ -1,15 +1,36 @@
import { AxiosError } from 'axios';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
export const useGetTenantLicense = (): {
isCloudUser: boolean;
isEECloudUser: boolean;
isEnterpriseSelfHostedUser: boolean;
isCommunityUser: boolean;
isCommunityEnterpriseUser: boolean;
} => {
const { activeLicenseV3 } = useAppContext();
const { activeLicenseV3, activeLicenseV3FetchError } = useAppContext();
return {
const responsePayload = {
isCloudUser: activeLicenseV3?.platform === LicensePlatform.CLOUD || false,
isEECloudUser:
isEnterpriseSelfHostedUser:
activeLicenseV3?.platform === LicensePlatform.SELF_HOSTED || false,
isCommunityUser: false,
isCommunityEnterpriseUser: false,
};
if (
activeLicenseV3FetchError &&
(activeLicenseV3FetchError as AxiosError)?.response?.status === 404
) {
responsePayload.isCommunityEnterpriseUser = true;
}
if (
activeLicenseV3FetchError &&
(activeLicenseV3FetchError as AxiosError)?.response?.status === 501
) {
responsePayload.isCommunityUser = true;
}
return responsePayload;
};

View File

@ -1,12 +1,9 @@
import './ReactI18';
import 'styles.scss';
import * as Sentry from '@sentry/react';
import AppRoutes from 'AppRoutes';
import { AxiosError } from 'axios';
import { ThemeProvider } from 'hooks/useDarkMode';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import posthog from 'posthog-js';
import { AppProvider } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import { createRoot } from 'react-dom/client';
@ -37,54 +34,25 @@ const queryClient = new QueryClient({
const container = document.getElementById('root');
if (process.env.POSTHOG_KEY) {
posthog.init(process.env.POSTHOG_KEY, {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
});
}
Sentry.init({
dsn: process.env.SENTRY_DSN,
tunnel: process.env.TUNNEL_URL,
environment: 'production',
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: [],
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
});
if (container) {
const root = createRoot(container);
root.render(
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<HelmetProvider>
<ThemeProvider>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AppProvider>
<AppRoutes />
</AppProvider>
</Provider>
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
</TimezoneProvider>
</ThemeProvider>
</HelmetProvider>
</Sentry.ErrorBoundary>,
<HelmetProvider>
<ThemeProvider>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<AppProvider>
<AppRoutes />
</AppProvider>
</Provider>
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
</TimezoneProvider>
</ThemeProvider>
</HelmetProvider>,
);
}

View File

@ -13,10 +13,7 @@ import { getRoutes } from './utils';
function SettingsPage(): JSX.Element {
const { pathname } = useLocation();
const { user, featureFlags, trialInfo } = useAppContext();
const {
isCloudUser: isCloudAccount,
isEECloudUser: isEECloudAccount,
} = useGetTenantLicense();
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
@ -37,8 +34,8 @@ function SettingsPage(): JSX.Element {
isCurrentOrgSettings,
isGatewayEnabled,
isWorkspaceBlocked,
isCloudAccount,
isEECloudAccount,
isCloudUser,
isEnterpriseSelfHostedUser,
t,
),
[
@ -46,8 +43,8 @@ function SettingsPage(): JSX.Element {
isCurrentOrgSettings,
isGatewayEnabled,
isWorkspaceBlocked,
isCloudAccount,
isEECloudAccount,
isCloudUser,
isEnterpriseSelfHostedUser,
t,
],
);

View File

@ -17,8 +17,8 @@ export const getRoutes = (
isCurrentOrgSettings: boolean,
isGatewayEnabled: boolean,
isWorkspaceBlocked: boolean,
isCloudAccount: boolean,
isEECloudAccount: boolean,
isCloudUser: boolean,
isEnterpriseSelfHostedUser: boolean,
t: TFunction,
): RouteTabProps['routes'] => {
const settings = [];
@ -42,17 +42,17 @@ export const getRoutes = (
settings.push(...multiIngestionSettings(t));
}
if (isCloudAccount && !isGatewayEnabled) {
if (isCloudUser && !isGatewayEnabled) {
settings.push(...ingestionSettings(t));
}
settings.push(...alertChannels(t));
if ((isCloudAccount || isEECloudAccount) && isAdmin) {
if ((isCloudUser || isEnterpriseSelfHostedUser) && isAdmin) {
settings.push(...apiKeys(t));
}
if (isCloudAccount && isAdmin) {
if (isCloudUser && isAdmin) {
settings.push(...customDomainSettings(t));
}

View File

@ -56,6 +56,8 @@ func Error(rw http.ResponseWriter, cause error) {
httpCode = http.StatusConflict
case errors.TypeUnauthenticated:
httpCode = http.StatusUnauthorized
case errors.TypeUnsupported:
httpCode = http.StatusNotImplemented
}
rea := make([]responseerroradditional, len(a))

View File

@ -614,6 +614,13 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
router.HandleFunc("/api/v1/getResetPasswordToken/{id}", am.AdminAccess(aH.getResetPasswordToken)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/resetPassword", am.OpenAccess(aH.resetPassword)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/changePassword/{id}", am.SelfAccess(aH.changePassword)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
render.Success(rw, http.StatusOK, []any{})
})).Methods(http.MethodGet)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
render.Error(rw, errorsV2.New(errorsV2.TypeUnsupported, errorsV2.CodeUnsupported, "not implemented"))
})).Methods(http.MethodGet)
}
func (ah *APIHandler) MetricExplorerRoutes(router *mux.Router, am *AuthMiddleware) {

View File

@ -113,8 +113,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
// initiate feature manager
fm := featureManager.StartManager()
readerReady := make(chan bool)
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
if err != nil {
return nil, err
@ -150,7 +148,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
c = cache.NewCache(cacheOpts)
}
<-readerReady
rm, err := makeRulesManager(
serverOptions.RuleRepoURL,
serverOptions.SigNoz.SQLStore.SQLxDB(),