chore(feature): drop the feature status table (#8124)

* chore(feature): drop the feature set table

* chore(feature): cleanup the types and remove unused flags

* chore(feature): some more cleanup

* chore(feature): add codeowners file

* chore(feature): init to basic plan for failed validations

* chore(feature): cleanup

* chore(feature): pkg handler cleanup

* chore(feature): pkg handler cleanup

* chore(feature): address review comments

* chore(feature): address review comments

* chore(feature): address review comments

---------

Co-authored-by: Vibhu Pandey <vibhupandey28@gmail.com>
This commit is contained in:
Vikrant Gupta 2025-06-03 17:05:42 +05:30 committed by GitHub
parent c58cf67eb0
commit c32dd9f17e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 288 additions and 467 deletions

2
.github/CODEOWNERS vendored
View File

@ -11,5 +11,5 @@
/pkg/errors/ @grandwizard28
/pkg/factory/ @grandwizard28
/pkg/types/ @grandwizard28
/pkg/sqlmigration/ @vikrantgupta25
.golangci.yml @grandwizard28
**/(zeus|licensing|sqlmigration)/ @vikrantgupta25

View File

@ -5,15 +5,12 @@ import (
"encoding/json"
"time"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
@ -89,13 +86,6 @@ func (provider *provider) Validate(ctx context.Context) error {
}
}
if len(organizations) == 0 {
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
}
return nil
}
@ -116,11 +106,6 @@ func (provider *provider) Activate(ctx context.Context, organizationID valuer.UU
return err
}
err = provider.InitFeatures(ctx, license.Features)
if err != nil {
return err
}
return nil
}
@ -140,28 +125,24 @@ func (provider *provider) GetActive(ctx context.Context, organizationID valuer.U
func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUID) error {
activeLicense, err := provider.GetActive(ctx, organizationID)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil
}
provider.settings.Logger().ErrorContext(ctx, "license validation failed", "org_id", organizationID.StringValue())
return err
}
if err != nil && errors.Ast(err, errors.TypeNotFound) {
provider.settings.Logger().DebugContext(ctx, "no active license found, defaulting to basic plan", "org_id", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
return nil
}
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
if err != nil {
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
provider.settings.Logger().ErrorContext(ctx, "license validation failed for consecutive poll intervals, defaulting to basic plan", "failure_threshold", provider.config.FailureThreshold, "license_id", activeLicense.ID.StringValue(), "org_id", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
if err != nil {
return err
}
return nil
}
return err
@ -178,11 +159,6 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
err = provider.InitFeatures(ctx, activeLicense.Features)
if err != nil {
return err
}
return nil
}
@ -224,80 +200,14 @@ func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
}
// feature surrogate
func (provider *provider) CheckFeature(ctx context.Context, key string) error {
feature, err := provider.store.GetFeature(ctx, key)
func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) {
license, err := provider.GetActive(ctx, organizationID)
if err != nil {
return err
if errors.Ast(err, errors.TypeNotFound) {
return licensetypes.BasicPlan, nil
}
if feature.Active {
return nil
}
return errors.Newf(errors.TypeUnsupported, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key)
}
func (provider *provider) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) {
featureStatus, err := provider.store.GetFeature(ctx, key)
if err != nil {
return nil, err
}
return &featuretypes.GettableFeature{
Name: featureStatus.Name,
Active: featureStatus.Active,
Usage: int64(featureStatus.Usage),
UsageLimit: int64(featureStatus.UsageLimit),
Route: featureStatus.Route,
}, nil
}
func (provider *provider) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
storableFeatures, err := provider.store.GetAllFeatures(ctx)
if err != nil {
return nil, err
}
gettableFeatures := make([]*featuretypes.GettableFeature, len(storableFeatures))
for idx, gettableFeature := range storableFeatures {
gettableFeatures[idx] = &featuretypes.GettableFeature{
Name: gettableFeature.Name,
Active: gettableFeature.Active,
Usage: int64(gettableFeature.Usage),
UsageLimit: int64(gettableFeature.UsageLimit),
Route: gettableFeature.Route,
}
}
if constants.IsDotMetricsEnabled {
gettableFeatures = append(gettableFeatures, &featuretypes.GettableFeature{
Name: featuretypes.DotMetricsEnabled,
Active: true,
})
}
return gettableFeatures, nil
}
func (provider *provider) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error {
featureStatus := make([]*featuretypes.StorableFeature, len(features))
for i, f := range features {
featureStatus[i] = &featuretypes.StorableFeature{
Name: f.Name,
Active: f.Active,
Usage: int(f.Usage),
UsageLimit: int(f.UsageLimit),
Route: f.Route,
}
}
return provider.store.InitFeatures(ctx, featureStatus)
}
func (provider *provider) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error {
return provider.store.UpdateFeature(ctx, &featuretypes.StorableFeature{
Name: feature.Name,
Active: feature.Active,
Usage: int(feature.Usage),
UsageLimit: int(feature.UsageLimit),
Route: feature.Route,
})
return license.Features, nil
}

View File

@ -5,7 +5,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@ -80,81 +79,3 @@ func (store *store) Update(ctx context.Context, organizationID valuer.UUID, stor
return nil
}
func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(storableFeature).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "feature with name:%s already exists", storableFeature.Name)
}
return nil
}
func (store *store) GetFeature(ctx context.Context, key string) (*featuretypes.StorableFeature, error) {
storableFeature := new(featuretypes.StorableFeature)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storableFeature).
Where("name = ?", key).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "feature with name:%s does not exist", key)
}
return storableFeature, nil
}
func (store *store) GetAllFeatures(ctx context.Context) ([]*featuretypes.StorableFeature, error) {
storableFeatures := make([]*featuretypes.StorableFeature, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&storableFeatures).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "features do not exist")
}
return storableFeatures, nil
}
func (store *store) InitFeatures(ctx context.Context, storableFeatures []*featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(&storableFeatures).
On("CONFLICT (name) DO UPDATE").
Set("active = EXCLUDED.active").
Set("usage = EXCLUDED.usage").
Set("usage_limit = EXCLUDED.usage_limit").
Set("route = EXCLUDED.route").
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to initialise features")
}
return nil
}
func (store *store) UpdateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewUpdate().
Model(storableFeature).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update feature with key: %s", storableFeature.Name)
}
return nil
}

View File

@ -1,7 +1,6 @@
package api
import (
"context"
"net/http"
"net/http/httputil"
"time"
@ -86,17 +85,12 @@ func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
return ah.opts.Gateway
}
func (ah *APIHandler) CheckFeature(ctx context.Context, key string) bool {
err := ah.Signoz.Licensing.CheckFeature(ctx, key)
return err == nil
}
// RegisterRoutes registers routes for this handler on the given router
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// note: add ee override methods first
// routes available only in ee version
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
// paid plans specific routes
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)

View File

@ -12,7 +12,7 @@ import (
pkgError "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
)
@ -31,7 +31,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
return
}
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context())
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context(), orgID)
if err != nil {
ah.HandleError(w, err, http.StatusInternalServerError)
return
@ -61,7 +61,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
if ah.opts.PreferSpanMetrics {
for idx, feature := range featureSet {
if feature.Name == featuretypes.UseSpanMetrics {
if feature.Name == licensetypes.UseSpanMetrics {
featureSet[idx].Active = true
}
}
}
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {
featureSet[idx].Active = true
}
}
@ -72,7 +80,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
// and returns the FeatureSet.
func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature, error) {
func fetchZeusFeatures(url, licenseKey string) ([]*licensetypes.Feature, error) {
// Check if the URL is empty
if url == "" {
return nil, fmt.Errorf("url is empty")
@ -132,27 +140,27 @@ func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature,
type ZeusFeaturesResponse struct {
Status string `json:"status"`
Data []*featuretypes.GettableFeature `json:"data"`
Data []*licensetypes.Feature `json:"data"`
}
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
func MergeFeatureSets(zeusFeatures, internalFeatures []*featuretypes.GettableFeature) []*featuretypes.GettableFeature {
func MergeFeatureSets(zeusFeatures, internalFeatures []*licensetypes.Feature) []*licensetypes.Feature {
// Create a map to store the merged features
featureMap := make(map[string]*featuretypes.GettableFeature)
featureMap := make(map[string]*licensetypes.Feature)
// Add all features from the otherFeatures set to the map
for _, feature := range internalFeatures {
featureMap[feature.Name] = feature
featureMap[feature.Name.StringValue()] = feature
}
// Add all features from the zeusFeatures set to the map
// If a feature already exists (i.e., same name), the zeusFeature will overwrite it
for _, feature := range zeusFeatures {
featureMap[feature.Name] = feature
featureMap[feature.Name.StringValue()] = feature
}
// Convert the map back to a FeatureSet slice
var mergedFeatures []*featuretypes.GettableFeature
var mergedFeatures []*licensetypes.Feature
for _, feature := range featureMap {
mergedFeatures = append(mergedFeatures, feature)
}

View File

@ -3,78 +3,79 @@ package api
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
)
func TestMergeFeatureSets(t *testing.T) {
tests := []struct {
name string
zeusFeatures []*featuretypes.GettableFeature
internalFeatures []*featuretypes.GettableFeature
expected []*featuretypes.GettableFeature
zeusFeatures []*licensetypes.Feature
internalFeatures []*licensetypes.Feature
expected []*licensetypes.Feature
}{
{
name: "empty zeusFeatures and internalFeatures",
zeusFeatures: []*featuretypes.GettableFeature{},
internalFeatures: []*featuretypes.GettableFeature{},
expected: []*featuretypes.GettableFeature{},
zeusFeatures: []*licensetypes.Feature{},
internalFeatures: []*licensetypes.Feature{},
expected: []*licensetypes.Feature{},
},
{
name: "non-empty zeusFeatures and empty internalFeatures",
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
zeusFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
},
internalFeatures: []*featuretypes.GettableFeature{},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
internalFeatures: []*licensetypes.Feature{},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
},
},
{
name: "empty zeusFeatures and non-empty internalFeatures",
zeusFeatures: []*featuretypes.GettableFeature{},
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
zeusFeatures: []*licensetypes.Feature{},
internalFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature3", Active: false},
zeusFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature3"), Active: false},
},
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature2", Active: true},
{Name: "Feature4", Active: false},
internalFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature2"), Active: true},
{Name: valuer.NewString("Feature4"), Active: false},
},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: true},
{Name: "Feature3", Active: false},
{Name: "Feature4", Active: false},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: true},
{Name: valuer.NewString("Feature3"), Active: false},
{Name: valuer.NewString("Feature4"), Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
zeusFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
},
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: false},
{Name: "Feature3", Active: true},
internalFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: false},
{Name: valuer.NewString("Feature3"), Active: true},
},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
{Name: "Feature3", Active: true},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
{Name: valuer.NewString("Feature3"), Active: true},
},
},
}

View File

@ -1,10 +0,0 @@
import axios from 'api';
import { ApiResponse } from 'types/api';
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
const getFeaturesFlags = (): Promise<FeatureFlagProps[]> =>
axios
.get<ApiResponse<FeatureFlagProps[]>>(`/featureFlags`)
.then((response) => response.data.data);
export default getFeaturesFlags;

View File

@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
FeatureFlagProps,
PayloadProps,
} from 'types/api/features/getFeaturesFlags';
const list = async (): Promise<SuccessResponseV2<FeatureFlagProps[]>> => {
try {
const response = await axios.get<PayloadProps>(`/features`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default list;

View File

@ -1,14 +1,12 @@
// keep this consistent with backend constants.go
// keep this consistent with backend plan.go
export enum FeatureKeys {
SSO = 'SSO',
USE_SPAN_METRICS = 'USE_SPAN_METRICS',
ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
ONBOARDING_V3 = 'ONBOARDING_V3',
THIRD_PARTY_API = 'THIRD_PARTY_API',
TRACE_FUNNELS = 'TRACE_FUNNELS',
DOT_METRICS_ENABLED = 'DOT_METRICS_ENABLED',
SSO = 'sso',
USE_SPAN_METRICS = 'use_span_metrics',
ONBOARDING = 'onboarding',
CHAT_SUPPORT = 'chat_support',
GATEWAY = 'gateway',
PREMIUM_SUPPORT = 'premium_support',
ANOMALY_DETECTION = 'anomaly_detection',
ONBOARDING_V3 = 'onboarding_v3',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
}

View File

@ -316,7 +316,6 @@ describe('Create Alert Channel (Normal User)', () => {
expect(screen.getByText('Microsoft Teams')).toBeInTheDocument();
});
// TODO[vikrantgupta25]: check with Shaheer
it.skip('Should check if the upgrade plan message is shown', () => {
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
expect(

View File

@ -36,7 +36,6 @@ function QuerySection({
const { t } = useTranslation('alerts');
const [currentTab, setCurrentTab] = useState(queryCategory);
// TODO[vikrantgupta25] : check if this is still required ??
const handleQueryCategoryChange = (queryType: string): void => {
setQueryCategory(queryType as EQueryType);
setCurrentTab(queryType as EQueryType);

View File

@ -160,8 +160,6 @@ function GridCardGraph({
};
});
// TODO [vikrantgupta25] remove this useEffect with refactor as this is prone to race condition
// this is added to tackle the case of async communication between VariableItem.tsx and GridCard.tsx
useEffect(() => {
if (variablesToGetUpdated.length > 0) {
queryClient.cancelQueries([

View File

@ -41,7 +41,6 @@ const { Search } = Input;
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const { t } = useTranslation('common');
const { user } = useAppContext();
// TODO[vikrantgupta25]: check with sagar on cleanup
const [addNewAlert, action] = useComponentPermission(
['add_new_alert', 'action'],
user.role,

View File

@ -1,18 +1,29 @@
import getFeaturesFlags from 'api/features/getFeatureFlags';
import list from 'api/v1/features/list';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
const useGetFeatureFlag = (
export interface Props {
onSuccessHandler: (routes: FeatureFlagProps[]) => void;
isLoggedIn: boolean;
}
type UseGetFeatureFlag = UseQueryResult<
SuccessResponseV2<FeatureFlagProps[]>,
APIError
>;
export const useGetFeatureFlag = (
onSuccessHandler: (routes: FeatureFlagProps[]) => void,
isLoggedIn: boolean,
): UseQueryResult<FeatureFlagProps[], unknown> =>
useQuery<FeatureFlagProps[]>({
queryFn: getFeaturesFlags,
): UseGetFeatureFlag =>
useQuery<SuccessResponseV2<FeatureFlagProps[]>, APIError>({
queryKey: [REACT_QUERY_KEY.GET_FEATURES_FLAGS],
onSuccess: onSuccessHandler,
queryFn: () => list(),
onSuccess: (data) => {
onSuccessHandler(data.data);
},
retryOnMount: false,
enabled: !!isLoggedIn,
});
export default useGetFeatureFlag;

View File

@ -145,14 +145,6 @@ describe('Logs Explorer Tests', () => {
await waitFor(() =>
expect(queryByTestId('logs-list-virtuoso')).toBeInTheDocument(),
);
// check for data being present in the UI
// todo[@vikrantgupta25]: skipping this for now as the formatting matching is not picking up in the CI will debug later.
// expect(
// queryByText(
// `2024-02-16 02:50:22.000 | 2024-02-15T21:20:22.035Z INFO frontend Dispatch successful {"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}`,
// ),
// ).toBeInTheDocument();
});
test('Multiple Current Queries', async () => {

View File

@ -5,7 +5,7 @@ import getUserVersion from 'api/v1/version/getVersion';
import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs from 'dayjs';
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
import { useGetFeatureFlag } from 'hooks/useGetFeatureFlag';
import { useGlobalEventListener } from 'hooks/useGlobalEventListener';
import useGetUser from 'hooks/user/useGetUser';
import {

View File

@ -8,4 +8,7 @@ export interface FeatureFlagProps {
route: string;
}
export type PayloadProps = FeatureFlagProps[];
export interface PayloadProps {
data: FeatureFlagProps[];
status: string;
}

View File

@ -25,12 +25,6 @@ export enum LicensePlatform {
CLOUD = 'CLOUD',
}
// Legacy
export const LicensePlanKey = {
ENTERPRISE: 'ENTERPRISE',
BASIC: 'BASIC',
};
export type LicenseEventQueueResModel = {
event: LicenseEvent;
status: string;

View File

@ -6,7 +6,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@ -31,18 +30,8 @@ type Licensing interface {
Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error)
// Portal creates a portal session via upstream server and return the redirection link
Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error)
// feature surrogate
// CheckFeature checks if the feature is active or not
CheckFeature(ctx context.Context, key string) error
// GetFeatureFlags fetches all the defined feature flags
GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error)
// GetFeatureFlags fetches all the defined feature flags
GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error)
// InitFeatures initialises the feature flags
InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error
// UpdateFeatureFlag updates the feature flag
UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error
GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error)
}
type API interface {

View File

@ -6,7 +6,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@ -60,40 +59,6 @@ func (provider *noopLicensing) GetActive(ctx context.Context, organizationID val
return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "fetching active license is not supported")
}
func (provider *noopLicensing) CheckFeature(ctx context.Context, key string) error {
feature, err := provider.GetFeatureFlag(ctx, key)
if err != nil {
return err
}
if feature.Active {
return nil
}
return errors.Newf(errors.TypeNotFound, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key)
}
func (provider *noopLicensing) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) {
features, err := provider.GetFeatureFlags(ctx)
if err != nil {
return nil, err
}
for _, feature := range features {
if feature.Name == key {
return feature, nil
}
}
return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "no feature available with given key: %s", key)
}
func (provider *noopLicensing) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
func (provider *noopLicensing) GetFeatureFlags(_ context.Context, _ valuer.UUID) ([]*licensetypes.Feature, error) {
return licensetypes.DefaultFeatureSet, nil
}
func (provider *noopLicensing) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "init features is not supported")
}
func (provider *noopLicensing) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error {
return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "updating feature flag is not supported")
}

View File

@ -62,7 +62,7 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
@ -550,7 +550,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v2/traces/waterfall/{traceId}", am.ViewAccess(aH.GetWaterfallSpansForTraceWithMetadata)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/version", am.OpenAccess(aH.getVersion)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/features", am.ViewAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/health", am.OpenAccess(aH.getHealth)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/listErrors", am.ViewAccess(aH.listErrors)).Methods(http.MethodPost)
@ -1931,32 +1931,29 @@ func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
}
func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
featureSet, err := aH.Signoz.Licensing.GetFeatureFlags(r.Context())
featureSet, err := aH.Signoz.Licensing.GetFeatureFlags(r.Context(), valuer.GenerateUUID())
if err != nil {
aH.HandleError(w, err, http.StatusInternalServerError)
return
}
if aH.preferSpanMetrics {
for idx, feature := range featureSet {
if feature.Name == featuretypes.UseSpanMetrics {
if feature.Name == licensetypes.UseSpanMetrics {
featureSet[idx].Active = true
}
}
}
if constants.IsDotMetricsEnabled {
featureSet = append(featureSet, &featuretypes.GettableFeature{
Name: featuretypes.DotMetricsEnabled,
Active: true,
})
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {
featureSet[idx].Active = true
}
}
}
aH.Respond(w, featureSet)
}
func (aH *APIHandler) CheckFeature(ctx context.Context, key string) bool {
err := aH.Signoz.Licensing.CheckFeature(ctx, key)
return err == nil
}
// getHealth is used to check the health of the service.
// 'live' query param can be used to check liveliness of
// the service by checking the database connection.

View File

@ -9,7 +9,6 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
)
const (
@ -66,16 +65,6 @@ func UseMetricsPreAggregation() bool {
var KafkaSpanEval = GetOrDefaultEnv("KAFKA_SPAN_EVAL", "false")
var DEFAULT_FEATURE_SET = []*featuretypes.GettableFeature{
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
}
func GetEvalDelay() time.Duration {
evalDelayStr := GetOrDefaultEnv("RULES_EVAL_DELAY", "2m")
evalDelayDuration, err := time.ParseDuration(evalDelayStr)

View File

@ -90,6 +90,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
sqlmigration.NewAddKeyOrganizationFactory(sqlstore),
sqlmigration.NewAddTraceFunnelsFactory(sqlstore),
sqlmigration.NewUpdateDashboardFactory(sqlstore),
sqlmigration.NewDropFeatureSetFactory(),
)
}

View File

@ -0,0 +1,58 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type dropFeatureSet struct{}
func NewDropFeatureSetFactory() factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("drop_feature_set"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return newDropFeatureSet(ctx, ps, c)
})
}
func newDropFeatureSet(_ context.Context, _ factory.ProviderSettings, _ Config) (SQLMigration, error) {
return &dropFeatureSet{}, nil
}
func (migration *dropFeatureSet) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *dropFeatureSet) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
if _, err := tx.
NewDropTable().
IfExists().
Table("feature_status").
Exec(ctx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *dropFeatureSet) Down(context.Context, *bun.DB) error {
return nil
}

View File

@ -1,29 +0,0 @@
package featuretypes
import "github.com/uptrace/bun"
type FeatureSet []*GettableFeature
type GettableFeature struct {
Name string `db:"name" json:"name"`
Active bool `db:"active" json:"active"`
Usage int64 `db:"usage" json:"usage"`
UsageLimit int64 `db:"usage_limit" json:"usage_limit"`
Route string `db:"route" json:"route"`
}
type StorableFeature struct {
bun.BaseModel `bun:"table:feature_status"`
Name string `bun:"name,pk,type:text" json:"name"`
Active bool `bun:"active" json:"active"`
Usage int `bun:"usage,default:0" json:"usage"`
UsageLimit int `bun:"usage_limit,default:0" json:"usage_limit"`
Route string `bun:"route,type:text" json:"route"`
}
func NewStorableFeature() {}
const UseSpanMetrics = "USE_SPAN_METRICS"
const AnomalyDetection = "ANOMALY_DETECTION"
const TraceFunnels = "TRACE_FUNNELS"
const DotMetricsEnabled = "DOT_METRICS_ENABLED"

View File

@ -9,7 +9,6 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
@ -30,9 +29,9 @@ type License struct {
ID valuer.UUID
Key string
Data map[string]interface{}
PlanName string
Features []*featuretypes.GettableFeature
Status string
PlanName valuer.String
Features []*Feature
Status valuer.String
ValidFrom int64
ValidUntil int64
CreatedAt time.Time
@ -124,7 +123,7 @@ func NewLicense(data []byte, organizationID valuer.UUID) (*License, error) {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal license data")
}
var features []*featuretypes.GettableFeature
var features []*Feature
// extract id from data
licenseIDStr, err := extractKeyFromMapStringInterface[string](licenseData, "id")
@ -145,26 +144,28 @@ func NewLicense(data []byte, organizationID valuer.UUID) (*License, error) {
delete(licenseData, "key")
// extract status from data
status, err := extractKeyFromMapStringInterface[string](licenseData, "status")
statusStr, err := extractKeyFromMapStringInterface[string](licenseData, "status")
if err != nil {
return nil, err
}
status := valuer.NewString(statusStr)
planMap, err := extractKeyFromMapStringInterface[map[string]any](licenseData, "plan")
if err != nil {
return nil, err
}
planName, err := extractKeyFromMapStringInterface[string](planMap, "name")
planNameStr, err := extractKeyFromMapStringInterface[string](planMap, "name")
if err != nil {
return nil, err
}
planName := valuer.NewString(planNameStr)
// if license status is invalid then default it to basic
if status == LicenseStatusInvalid {
planName = PlanNameBasic
}
featuresFromZeus := make([]*featuretypes.GettableFeature, 0)
featuresFromZeus := make([]*Feature, 0)
if _features, ok := licenseData["features"]; ok {
featuresData, err := json.Marshal(_features)
if err != nil {
@ -232,28 +233,30 @@ func NewLicense(data []byte, organizationID valuer.UUID) (*License, error) {
}
func NewLicenseFromStorableLicense(storableLicense *StorableLicense) (*License, error) {
var features []*featuretypes.GettableFeature
var features []*Feature
// extract status from data
status, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "status")
statusStr, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "status")
if err != nil {
return nil, err
}
status := valuer.NewString(statusStr)
planMap, err := extractKeyFromMapStringInterface[map[string]any](storableLicense.Data, "plan")
if err != nil {
return nil, err
}
planName, err := extractKeyFromMapStringInterface[string](planMap, "name")
planNameStr, err := extractKeyFromMapStringInterface[string](planMap, "name")
if err != nil {
return nil, err
}
planName := valuer.NewString(planNameStr)
// if license status is invalid then default it to basic
if status == LicenseStatusInvalid {
planName = PlanNameBasic
}
featuresFromZeus := make([]*featuretypes.GettableFeature, 0)
featuresFromZeus := make([]*Feature, 0)
if _features, ok := storableLicense.Data["features"]; ok {
featuresData, err := json.Marshal(_features)
if err != nil {
@ -320,6 +323,10 @@ func NewLicenseFromStorableLicense(storableLicense *StorableLicense) (*License,
}
func (license *License) UpdateFeatures(features []*Feature) {
license.Features = features
}
func (license *License) Update(data []byte) error {
updatedLicense, err := NewLicense(data, license.OrganizationID)
if err != nil {
@ -373,11 +380,4 @@ type Store interface {
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableLicense, error)
GetAll(context.Context, valuer.UUID) ([]*StorableLicense, error)
Update(context.Context, valuer.UUID, *StorableLicense) error
// feature surrogate
InitFeatures(context.Context, []*featuretypes.StorableFeature) error
CreateFeature(context.Context, *featuretypes.StorableFeature) error
GetFeature(context.Context, string) (*featuretypes.StorableFeature, error)
GetAllFeatures(context.Context) ([]*featuretypes.StorableFeature, error)
UpdateFeature(context.Context, *featuretypes.StorableFeature) error
}

View File

@ -4,7 +4,6 @@ import (
"testing"
"time"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
@ -92,8 +91,8 @@ func TestNewLicenseV3(t *testing.T) {
PlanName: PlanNameEnterprise,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "ACTIVE",
Features: make([]*featuretypes.GettableFeature, 0),
Status: valuer.NewString("ACTIVE"),
Features: make([]*Feature, 0),
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
},
},
@ -116,8 +115,8 @@ func TestNewLicenseV3(t *testing.T) {
PlanName: PlanNameBasic,
ValidFrom: 1730899309,
ValidUntil: -1,
Status: "INVALID",
Features: make([]*featuretypes.GettableFeature, 0),
Status: valuer.NewString("INVALID"),
Features: make([]*Feature, 0),
OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"),
},
},
@ -140,8 +139,8 @@ func TestNewLicenseV3(t *testing.T) {
PlanName: PlanNameEnterprise,
ValidFrom: 1234,
ValidUntil: 5678,
Status: "ACTIVE",
Features: make([]*featuretypes.GettableFeature, 0),
Status: valuer.NewString("ACTIVE"),
Features: make([]*Feature, 0),
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
LastValidatedAt: time.Time{},
@ -153,7 +152,7 @@ func TestNewLicenseV3(t *testing.T) {
for _, tc := range testCases {
license, err := NewLicense(tc.data, valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"))
if license != nil {
license.Features = make([]*featuretypes.GettableFeature, 0)
license.Features = make([]*Feature, 0)
delete(license.Data, "features")
}

View File

@ -1,67 +1,72 @@
package licensetypes
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
const SSO = "SSO"
const Basic = "BASIC_PLAN"
const Enterprise = "ENTERPRISE_PLAN"
import "github.com/SigNoz/signoz/pkg/valuer"
var (
PlanNameEnterprise = "ENTERPRISE"
PlanNameBasic = "BASIC"
// Feature Key
SSO = valuer.NewString("sso")
Onboarding = valuer.NewString("onboarding")
ChatSupport = valuer.NewString("chat_support")
Gateway = valuer.NewString("gateway")
PremiumSupport = valuer.NewString("premium_support")
UseSpanMetrics = valuer.NewString("use_span_metrics")
AnomalyDetection = valuer.NewString("anomaly_detection")
DotMetricsEnabled = valuer.NewString("dot_metrics_enabled")
// License State
LicenseStatusInvalid = valuer.NewString("invalid")
// Plan
PlanNameEnterprise = valuer.NewString("enterprise")
PlanNameBasic = valuer.NewString("basic")
)
var (
MapOldPlanKeyToNewPlanName map[string]string = map[string]string{PlanNameBasic: Basic, PlanNameEnterprise: Enterprise}
)
type Feature struct {
Name valuer.String `json:"name"`
Active bool `json:"active"`
Usage int64 `json:"usage"`
UsageLimit int64 `json:"usage_limit"`
Route string `json:"route"`
}
var (
LicenseStatusInvalid = "INVALID"
)
const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT"
var BasicPlan = featuretypes.FeatureSet{
&featuretypes.GettableFeature{
var BasicPlan = []*Feature{
{
Name: SSO,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
{
Name: UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: Gateway,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: PremiumSupport,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.AnomalyDetection,
{
Name: AnomalyDetection,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.TraceFunnels,
{
Name: DotMetricsEnabled,
Active: false,
Usage: 0,
UsageLimit: -1,
@ -69,58 +74,58 @@ var BasicPlan = featuretypes.FeatureSet{
},
}
var EnterprisePlan = featuretypes.FeatureSet{
&featuretypes.GettableFeature{
var EnterprisePlan = []*Feature{
{
Name: SSO,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
{
Name: UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: Onboarding,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: ChatSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: Gateway,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
{
Name: PremiumSupport,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.AnomalyDetection,
{
Name: AnomalyDetection,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
&featuretypes.GettableFeature{
Name: featuretypes.TraceFunnels,
{
Name: DotMetricsEnabled,
Active: false,
Usage: 0,
UsageLimit: -1,
@ -128,9 +133,16 @@ var EnterprisePlan = featuretypes.FeatureSet{
},
}
var DefaultFeatureSet = featuretypes.FeatureSet{
&featuretypes.GettableFeature{
Name: featuretypes.UseSpanMetrics,
var DefaultFeatureSet = []*Feature{
{
Name: UseSpanMetrics,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
{
Name: DotMetricsEnabled,
Active: false,
Usage: 0,
UsageLimit: -1,