diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 629a9e6691..f36fa65a3b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,5 +11,5 @@ /pkg/errors/ @grandwizard28 /pkg/factory/ @grandwizard28 /pkg/types/ @grandwizard28 -/pkg/sqlmigration/ @vikrantgupta25 .golangci.yml @grandwizard28 +**/(zeus|licensing|sqlmigration)/ @vikrantgupta25 \ No newline at end of file diff --git a/README.md b/README.md index 60f8621703..cc0925878a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit signoz.io for the full documentation, tutorials, and guide.

- Downloads + Downloads GitHub issues tweet diff --git a/ee/licensing/httplicensing/provider.go b/ee/licensing/httplicensing/provider.go index 1cfdad4d37..34cf90c46e 100644 --- a/ee/licensing/httplicensing/provider.go +++ b/ee/licensing/httplicensing/provider.go @@ -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) - if err != nil { - return err - } - 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) +func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) { + license, err := provider.GetActive(ctx, organizationID) if err != nil { + if errors.Ast(err, errors.TypeNotFound) { + return licensetypes.BasicPlan, 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 } diff --git a/ee/licensing/licensingstore/sqllicensingstore/store.go b/ee/licensing/licensingstore/sqllicensingstore/store.go index 9e8ea19d71..dfbb257a93 100644 --- a/ee/licensing/licensingstore/sqllicensingstore/store.go +++ b/ee/licensing/licensingstore/sqllicensingstore/store.go @@ -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 -} diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index 15042a4d95..bb24474e23 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -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) diff --git a/ee/query-service/app/api/featureFlags.go b/ee/query-service/app/api/featureFlags.go index 13572c2f5f..459b5d6508 100644 --- a/ee/query-service/app/api/featureFlags.go +++ b/ee/query-service/app/api/featureFlags.go @@ -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") @@ -131,28 +139,28 @@ func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature, } type ZeusFeaturesResponse struct { - Status string `json:"status"` - Data []*featuretypes.GettableFeature `json:"data"` + Status string `json:"status"` + 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) } diff --git a/ee/query-service/app/api/featureFlags_test.go b/ee/query-service/app/api/featureFlags_test.go index 79032a43a5..96194e41fa 100644 --- a/ee/query-service/app/api/featureFlags_test.go +++ b/ee/query-service/app/api/featureFlags_test.go @@ -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}, }, }, } diff --git a/frontend/src/api/features/getFeatureFlags.ts b/frontend/src/api/features/getFeatureFlags.ts deleted file mode 100644 index 2ce37b99e1..0000000000 --- a/frontend/src/api/features/getFeatureFlags.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'api'; -import { ApiResponse } from 'types/api'; -import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags'; - -const getFeaturesFlags = (): Promise => - axios - .get>(`/featureFlags`) - .then((response) => response.data.data); - -export default getFeaturesFlags; diff --git a/frontend/src/api/traceFunnels/index.ts b/frontend/src/api/traceFunnels/index.ts index c93a3c058f..d1cb0a2057 100644 --- a/frontend/src/api/traceFunnels/index.ts +++ b/frontend/src/api/traceFunnels/index.ts @@ -167,8 +167,8 @@ interface UpdateFunnelDescriptionPayload { export const saveFunnelDescription = async ( payload: UpdateFunnelDescriptionPayload, ): Promise | ErrorResponse> => { - const response: AxiosResponse = await axios.post( - `${FUNNELS_BASE_PATH}/save`, + const response: AxiosResponse = await axios.put( + `${FUNNELS_BASE_PATH}/${payload.funnel_id}`, payload, ); diff --git a/frontend/src/api/v1/features/list.ts b/frontend/src/api/v1/features/list.ts new file mode 100644 index 0000000000..43a445ead5 --- /dev/null +++ b/frontend/src/api/v1/features/list.ts @@ -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> => { + try { + const response = await axios.get(`/features`); + + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default list; diff --git a/frontend/src/constants/features.ts b/frontend/src/constants/features.ts index f1e486ee91..2fecd0ea36 100644 --- a/frontend/src/constants/features.ts +++ b/frontend/src/constants/features.ts @@ -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', } diff --git a/frontend/src/container/AllAlertChannels/__tests__/CreateAlertChannelNormalUser.test.tsx b/frontend/src/container/AllAlertChannels/__tests__/CreateAlertChannelNormalUser.test.tsx index e9761e8df0..b613a5a613 100644 --- a/frontend/src/container/AllAlertChannels/__tests__/CreateAlertChannelNormalUser.test.tsx +++ b/frontend/src/container/AllAlertChannels/__tests__/CreateAlertChannelNormalUser.test.tsx @@ -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( diff --git a/frontend/src/container/FormAlertRules/QuerySection.tsx b/frontend/src/container/FormAlertRules/QuerySection.tsx index 6982dfff3d..6a14f50e9e 100644 --- a/frontend/src/container/FormAlertRules/QuerySection.tsx +++ b/frontend/src/container/FormAlertRules/QuerySection.tsx @@ -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); diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx index fe8b1a39c1..84be138d07 100644 --- a/frontend/src/container/GridCardLayout/GridCard/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx @@ -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([ diff --git a/frontend/src/container/ListAlertRules/ListAlert.tsx b/frontend/src/container/ListAlertRules/ListAlert.tsx index 0ad805ede8..fae18d12e9 100644 --- a/frontend/src/container/ListAlertRules/ListAlert.tsx +++ b/frontend/src/container/ListAlertRules/ListAlert.tsx @@ -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, diff --git a/frontend/src/container/LogsExplorerChart/index.tsx b/frontend/src/container/LogsExplorerChart/index.tsx index d0acc3f2fd..ca15909123 100644 --- a/frontend/src/container/LogsExplorerChart/index.tsx +++ b/frontend/src/container/LogsExplorerChart/index.tsx @@ -62,6 +62,8 @@ function LogsExplorerChart({ urlQuery.set(QueryParams.startTime, minTime.toString()); urlQuery.set(QueryParams.endTime, maxTime.toString()); urlQuery.delete(QueryParams.relativeTime); + // Remove Hidden Filters from URL query parameters on time change + urlQuery.delete(QueryParams.activeLogId); const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; safeNavigate(generatedUrl); }, diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 22c321bbce..a6d7c5a1a1 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -188,6 +188,26 @@ function LogsExplorerViews({ }, ], legend: '{{severity_text}}', + ...(activeLogId && { + filters: { + ...listQuery?.filters, + items: [ + ...(listQuery?.filters?.items || []), + { + id: v4(), + key: { + key: 'id', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: OPERATORS['<='], + value: activeLogId, + }, + ], + op: 'AND', + }, + }), }; const modifiedQuery: Query = { @@ -202,7 +222,7 @@ function LogsExplorerViews({ }; return modifiedQuery; - }, [stagedQuery, listQuery]); + }, [stagedQuery, listQuery, activeLogId]); const exportDefaultQuery = useMemo( () => @@ -287,12 +307,12 @@ function LogsExplorerViews({ }); // Add filter for activeLogId if present - let updatedFilters = paginateData.filters; + let updatedFilters = params.filters; if (activeLogId) { updatedFilters = { - ...paginateData.filters, + ...params.filters, items: [ - ...(paginateData.filters?.items || []), + ...(params.filters?.items || []), { id: v4(), key: { diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index da70cd6746..2ddaa028b5 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -1,17 +1,23 @@ import ROUTES from 'constants/routes'; +import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range'; import { server } from 'mocks-server/server'; import { rest } from 'msw'; import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils'; +import { QueryBuilderContext } from 'providers/QueryBuilder'; import { VirtuosoMockContext } from 'react-virtuoso'; import { fireEvent, render, RenderResult } from 'tests/test-utils'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import LogsExplorerViews from '..'; -import { logsQueryRangeSuccessNewFormatResponse } from './mock'; +import { + logsQueryRangeSuccessNewFormatResponse, + mockQueryBuilderContextValue, +} from './mock'; const queryRangeURL = 'http://localhost/api/v3/query_range'; - +const ACTIVE_LOG_ID = 'test-log-id'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: (): { pathname: string } => ({ @@ -81,6 +87,12 @@ jest.mock('hooks/useSafeNavigate', () => ({ }), })); +jest.mock('hooks/logs/useCopyLogLink', () => ({ + useCopyLogLink: jest.fn().mockReturnValue({ + activeLogId: ACTIVE_LOG_ID, + }), +})); + // Set up the specific behavior for useGetExplorerQueryRange in individual test cases beforeEach(() => { (useGetExplorerQueryRange as jest.Mock).mockReturnValue({ @@ -162,4 +174,47 @@ describe('LogsExplorerViews -', () => { queryByText('Something went wrong. Please try again or contact support.'), ).toBeInTheDocument(); }); + + it('should add activeLogId filter when present in URL', () => { + // Mock useCopyLogLink to return an activeLogId + (useCopyLogLink as jest.Mock).mockReturnValue({ + activeLogId: ACTIVE_LOG_ID, + }); + + lodsQueryServerRequest(); + render( + + {}} + listQueryKeyRef={{ current: {} }} + chartQueryKeyRef={{ current: {} }} + /> + , + ); + + // Get the query data from the first call to useGetExplorerQueryRange + const { + queryData, + } = (useGetExplorerQueryRange as jest.Mock).mock.calls[0][0].builder; + const firstQuery = queryData[0]; + + // Get the original number of filters from mock data + const originalFiltersLength = + mockQueryBuilderContextValue.currentQuery.builder.queryData[0].filters?.items + .length || 0; + const expectedFiltersLength = originalFiltersLength + 1; // +1 for activeLogId filter + + // Verify that the activeLogId filter is present + expect( + firstQuery.filters?.items.some( + (item: TagFilterItem) => + item.key?.key === 'id' && item.op === '<=' && item.value === ACTIVE_LOG_ID, + ), + ).toBe(true); + + // Verify the total number of filters (original + 1 new activeLogId filter) + expect(firstQuery.filters?.items.length).toBe(expectedFiltersLength); + }); }); diff --git a/frontend/src/container/LogsExplorerViews/tests/mock.ts b/frontend/src/container/LogsExplorerViews/tests/mock.ts index 6c07004eea..fa612054c8 100644 --- a/frontend/src/container/LogsExplorerViews/tests/mock.ts +++ b/frontend/src/container/LogsExplorerViews/tests/mock.ts @@ -1,3 +1,13 @@ +import { + initialQueriesMap, + initialQueryBuilderFormValues, + OPERATORS, + PANEL_TYPES, +} from 'constants/queryBuilder'; +import { noop } from 'lodash-es'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + export const logsQueryRangeSuccessNewFormatResponse = { data: { result: [], @@ -49,3 +59,148 @@ export const logsQueryRangeSuccessNewFormatResponse = { }, }, }; + +export const mockQueryBuilderContextValue = { + isDefaultQuery: (): boolean => false, + currentQuery: { + ...initialQueriesMap.logs, + builder: { + ...initialQueriesMap.logs.builder, + queryData: [ + { + ...initialQueryBuilderFormValues, + filters: { + items: [ + { + id: '1', + key: { + key: 'service', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: OPERATORS['='], + value: 'frontend', + }, + { + id: '2', + key: { + key: 'log_level', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: OPERATORS['='], + value: 'INFO', + }, + ], + op: 'AND', + }, + }, + initialQueryBuilderFormValues, + ], + }, + }, + setSupersetQuery: jest.fn(), + supersetQuery: { + ...initialQueriesMap.logs, + builder: { + ...initialQueriesMap.logs.builder, + queryData: [ + { + ...initialQueryBuilderFormValues, + filters: { + items: [ + { + id: '1', + key: { + key: 'service', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: OPERATORS['='], + value: 'frontend', + }, + { + id: '2', + key: { + key: 'log_level', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: OPERATORS['='], + value: 'INFO', + }, + ], + op: 'AND', + }, + }, + initialQueryBuilderFormValues, + ], + }, + }, + stagedQuery: { + ...initialQueriesMap.logs, + builder: { + ...initialQueriesMap.logs.builder, + queryData: [ + { + ...initialQueryBuilderFormValues, + filters: { + items: [ + { + id: '1', + key: { + key: 'service', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: OPERATORS['='], + value: 'frontend', + }, + { + id: '2', + key: { + key: 'log_level', + type: '', + dataType: DataTypes.String, + isColumn: true, + }, + op: OPERATORS['='], + value: 'INFO', + }, + ], + op: 'AND', + }, + }, + initialQueryBuilderFormValues, + ], + }, + }, + initialDataSource: null, + panelType: PANEL_TYPES.TIME_SERIES, + isEnabledQuery: false, + lastUsedQuery: 0, + setLastUsedQuery: noop, + handleSetQueryData: noop, + handleSetFormulaData: noop, + handleSetQueryItemData: noop, + handleSetConfig: noop, + removeQueryBuilderEntityByIndex: noop, + removeQueryTypeItemByIndex: noop, + addNewBuilderQuery: noop, + cloneQuery: noop, + addNewFormula: noop, + addNewQueryItem: noop, + redirectWithQueryBuilderData: noop, + handleRunQuery: noop, + resetQuery: noop, + updateAllQueriesOperators: (): Query => initialQueriesMap.logs, + updateQueriesData: (): Query => initialQueriesMap.logs, + initQueryBuilderData: noop, + handleOnUnitsChange: noop, + isStagedQueryUpdated: (): boolean => false, +}; diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx index b390798bab..3ca773569f 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx @@ -377,6 +377,8 @@ function DateTimeSelection({ urlQuery.delete('endTime'); urlQuery.set(QueryParams.relativeTime, value); + // Remove Hidden Filters from URL query parameters on time change + urlQuery.delete(QueryParams.activeLogId); const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; safeNavigate(generatedUrl); @@ -669,9 +671,7 @@ function DateTimeSelection({ urlQuery.set(QueryParams.endTime, endTime); urlQuery.delete(QueryParams.relativeTime); } - const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; - safeNavigate(generatedUrl); // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname, updateTimeInterval, globalTimeLoading]); diff --git a/frontend/src/hooks/TracesFunnels/useFunnels.tsx b/frontend/src/hooks/TracesFunnels/useFunnels.tsx index 7f85619a88..fecbfdb655 100644 --- a/frontend/src/hooks/TracesFunnels/useFunnels.tsx +++ b/frontend/src/hooks/TracesFunnels/useFunnels.tsx @@ -142,6 +142,7 @@ export const useValidateFunnelSteps = ({ interface SaveFunnelDescriptionPayload { funnel_id: string; description: string; + timestamp: number; } export const useSaveFunnelDescription = (): UseMutationResult< @@ -149,7 +150,11 @@ export const useSaveFunnelDescription = (): UseMutationResult< Error, SaveFunnelDescriptionPayload > => - useMutation({ + useMutation< + SuccessResponse | ErrorResponse, + Error, + SaveFunnelDescriptionPayload + >({ mutationFn: saveFunnelDescription, }); diff --git a/frontend/src/hooks/useGetFeatureFlag.tsx b/frontend/src/hooks/useGetFeatureFlag.tsx index 49cdb2379a..9459aca7d2 100644 --- a/frontend/src/hooks/useGetFeatureFlag.tsx +++ b/frontend/src/hooks/useGetFeatureFlag.tsx @@ -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, + APIError +>; + +export const useGetFeatureFlag = ( onSuccessHandler: (routes: FeatureFlagProps[]) => void, isLoggedIn: boolean, -): UseQueryResult => - useQuery({ - queryFn: getFeaturesFlags, +): UseGetFeatureFlag => + useQuery, APIError>({ queryKey: [REACT_QUERY_KEY.GET_FEATURES_FLAGS], - onSuccess: onSuccessHandler, + queryFn: () => list(), + onSuccess: (data) => { + onSuccessHandler(data.data); + }, retryOnMount: false, enabled: !!isLoggedIn, }); - -export default useGetFeatureFlag; diff --git a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx index 1b1e3d8417..778ee993c3 100644 --- a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx +++ b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx @@ -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 () => { diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.tsx index 8f0ab20df1..967e31f9ba 100644 --- a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.tsx +++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.tsx @@ -41,6 +41,7 @@ function AddFunnelDescriptionModal({ { funnel_id: funnelId, description, + timestamp: Date.now(), }, { onSuccess: () => { diff --git a/frontend/src/providers/App/App.tsx b/frontend/src/providers/App/App.tsx index c580815485..e769592a97 100644 --- a/frontend/src/providers/App/App.tsx +++ b/frontend/src/providers/App/App.tsx @@ -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 { diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 3ce37600c0..f95f04a8c2 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -832,6 +832,8 @@ export function QueryBuilderProvider({ ), ); } + // Remove Hidden Filters from URL query parameters on query change + urlQuery.delete(QueryParams.activeLogId); const generatedUrl = redirectingUrl ? `${redirectingUrl}?${urlQuery}` diff --git a/frontend/src/types/api/features/getFeaturesFlags.ts b/frontend/src/types/api/features/getFeaturesFlags.ts index 3eefd1cb4b..f7e986b17e 100644 --- a/frontend/src/types/api/features/getFeaturesFlags.ts +++ b/frontend/src/types/api/features/getFeaturesFlags.ts @@ -8,4 +8,7 @@ export interface FeatureFlagProps { route: string; } -export type PayloadProps = FeatureFlagProps[]; +export interface PayloadProps { + data: FeatureFlagProps[]; + status: string; +} diff --git a/frontend/src/types/api/licensesV3/getActive.ts b/frontend/src/types/api/licensesV3/getActive.ts index be0d488b5e..654a70a857 100644 --- a/frontend/src/types/api/licensesV3/getActive.ts +++ b/frontend/src/types/api/licensesV3/getActive.ts @@ -25,12 +25,6 @@ export enum LicensePlatform { CLOUD = 'CLOUD', } -// Legacy -export const LicensePlanKey = { - ENTERPRISE: 'ENTERPRISE', - BASIC: 'BASIC', -}; - export type LicenseEventQueueResModel = { event: LicenseEvent; status: string; diff --git a/pkg/licensing/licensing.go b/pkg/licensing/licensing.go index 0e0196650f..25385ee9af 100644 --- a/pkg/licensing/licensing.go +++ b/pkg/licensing/licensing.go @@ -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 { diff --git a/pkg/licensing/nooplicensing/provider.go b/pkg/licensing/nooplicensing/provider.go index 0e509615f2..396879fbe7 100644 --- a/pkg/licensing/nooplicensing/provider.go +++ b/pkg/licensing/nooplicensing/provider.go @@ -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") -} diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index c3e6c7fb32..30cd18d274 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -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. diff --git a/pkg/query-service/app/integrations/builtin.go b/pkg/query-service/app/integrations/builtin.go index 603dcfcce5..db865eeb58 100644 --- a/pkg/query-service/app/integrations/builtin.go +++ b/pkg/query-service/app/integrations/builtin.go @@ -186,6 +186,14 @@ func HydrateFileUris(spec interface{}, fs embed.FS, basedir string) (interface{} if strings.HasPrefix(dashboardUri, "file://") { dashboards[i] = strings.Replace(dashboardUri, ".json", "_dot.json", 1) } + } else if dashBoardMap, ok := dashboard.(map[string]interface{}); ok { + if dashboardUri, ok := dashBoardMap["definition"].(string); ok { + if strings.HasPrefix(dashboardUri, "file://") { + dashboardUri = strings.Replace(dashboardUri, ".json", "_dot.json", 1) + } + dashBoardMap["definition"] = dashboardUri + } + dashboards[i] = dashBoardMap } } v = dashboards diff --git a/pkg/query-service/app/integrations/builtin_integrations/clickhouse/assets/dashboards/overview_dot.json b/pkg/query-service/app/integrations/builtin_integrations/clickhouse/assets/dashboards/overview_dot.json index 28945e0081..44a3132e4c 100644 --- a/pkg/query-service/app/integrations/builtin_integrations/clickhouse/assets/dashboards/overview_dot.json +++ b/pkg/query-service/app/integrations/builtin_integrations/clickhouse/assets/dashboards/overview_dot.json @@ -124,7 +124,7 @@ "multiSelect": false, "name": "host.name", "order": 0, - "queryValue": "SELECT JSONExtractString(labels, 'host.name') AS host.name\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'ClickHouseMetrics_VersionInteger' and __normalized=false \nGROUP BY host.name", + "queryValue": "SELECT JSONExtractString(labels, 'host.name') AS `host.name`\nFROM signoz_metrics.distributed_time_series_v4_1day\nWHERE metric_name = 'ClickHouseMetrics_VersionInteger' and __normalized=false \nGROUP BY `host.name`", "selectedValue": "", "showALLOption": false, "sort": "DISABLED", diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index d6430603f0..dffdcf3a12 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -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) diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 4d55471cc4..0d007b0f75 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -90,6 +90,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM sqlmigration.NewAddKeyOrganizationFactory(sqlstore), sqlmigration.NewAddTraceFunnelsFactory(sqlstore), sqlmigration.NewUpdateDashboardFactory(sqlstore), + sqlmigration.NewDropFeatureSetFactory(), ) } diff --git a/pkg/sqlmigration/039_drop_feature_set.go b/pkg/sqlmigration/039_drop_feature_set.go new file mode 100644 index 0000000000..241f08efb3 --- /dev/null +++ b/pkg/sqlmigration/039_drop_feature_set.go @@ -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 +} diff --git a/pkg/types/featuretypes/feature.go b/pkg/types/featuretypes/feature.go deleted file mode 100644 index 68d2e7f538..0000000000 --- a/pkg/types/featuretypes/feature.go +++ /dev/null @@ -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" diff --git a/pkg/types/licensetypes/license.go b/pkg/types/licensetypes/license.go index d78c45585a..55db17a88c 100644 --- a/pkg/types/licensetypes/license.go +++ b/pkg/types/licensetypes/license.go @@ -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 } diff --git a/pkg/types/licensetypes/license_test.go b/pkg/types/licensetypes/license_test.go index b2216accbb..5ddff2c4e4 100644 --- a/pkg/types/licensetypes/license_test.go +++ b/pkg/types/licensetypes/license_test.go @@ -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") } diff --git a/pkg/types/licensetypes/plan.go b/pkg/types/licensetypes/plan.go index 4521018084..f0e697dcc6 100644 --- a/pkg/types/licensetypes/plan.go +++ b/pkg/types/licensetypes/plan.go @@ -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,