Compare commits

..

3 Commits

Author SHA1 Message Date
vikrantgupta25
e7443ab1cd fix(tokenizer): persist last_observed_at on postgres
Values in the flush CTE were untyped literals, which postgres resolves
to text and refuses to assign to a timestamptz column. Building the
CTE from the token model lets bun cast per dialect.
2026-09-25 11:57:44 +05:30
Abhi kumar
7ce73f3470 fix(dashboard): don't re-run an errored panel query on scroll back into view (#12958)
#### Description

- Lazy-loaded panels toggle `enabled` on viewport visibility.
react-query treats a key with no data as stale regardless of
`staleTime`, so an errored panel refetched (with retries on 5xx) every
time it scrolled back into view.
- `useGetQueryRangeV5` now keeps an errored key enabled, so only a key
change (time, variables, query) or the Retry button re-runs it. A new
key still stays gated while off-screen.
- Adds a `useGetQueryRangeV5` test suite covering the gating paths.
2026-09-23 07:04:51 +00:00
Nityananda Gohain
5aca8b0d3c chore: remove ai-o11y ff (#12947)
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
Remove ai-o11y FF and enable it by default

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
Part of https://github.com/SigNoz/engineering-pod/issues/6107
2026-09-23 06:39:16 +00:00
13 changed files with 99 additions and 106 deletions

View File

@@ -80,15 +80,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
aiObservability := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableAIObservability, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureEnableAIObservability.String()),
Active: aiObservability,
Usage: 0,
UsageLimit: -1,
Route: "",
})
metricsReduction := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureEnableMetricsReduction.String()),

View File

@@ -8,7 +8,6 @@ import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useIsAIObservabilityEnabled } from 'hooks/useIsAIObservabilityEnabled';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
@@ -44,7 +43,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const isAdmin = user.role === USER_ROLES.ADMIN;
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isAIObservabilityEnabled = useIsAIObservabilityEnabled();
const mapRoutes = useMemo(
() =>
new Map(
@@ -135,14 +133,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
return <Redirect to={ROUTES.HOME} />;
}
if (
(pathname.startsWith(`${ROUTES.AI_OBSERVABILITY_BASE}/`) ||
pathname === ROUTES.AI_OBSERVABILITY_BASE) &&
!isAIObservabilityEnabled
) {
return <Redirect to={ROUTES.HOME} />;
}
// Check for workspace access restriction (cloud only)
const isCloudPlatform = activeLicense?.platform === LicensePlatform.CLOUD;

View File

@@ -8,6 +8,5 @@ export enum FeatureKeys {
PREMIUM_SUPPORT = 'premium_support',
ANOMALY_DETECTION = 'anomaly_detection',
USE_JSON_BODY = 'use_json_body',
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
}

View File

@@ -47,7 +47,6 @@ import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useIsAIObservabilityEnabled } from 'hooks/useIsAIObservabilityEnabled';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { isArray } from 'lodash-es';
@@ -255,7 +254,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const isAdmin = user.role === USER_ROLES.ADMIN;
const isEditor = user.role === USER_ROLES.EDITOR;
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isAIObservabilityEnabled = useIsAIObservabilityEnabled();
const aiAssistantActiveConversationId = useAIAssistantStore(
(s) => s.activeConversationId,
);
@@ -295,9 +293,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
if (item.key === ROUTES.INTEGRATIONS) {
return shouldShowIntegrationsValue;
}
if (item.key === ROUTES.AI_OBSERVABILITY_OVERVIEW) {
return isAIObservabilityEnabled;
}
return item.isEnabled;
};
@@ -314,7 +309,6 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
isEnterpriseSelfHostedUser,
isAdmin,
isEditor,
isAIObservabilityEnabled,
]);
// Track if we've done the initial sync (to avoid overwriting user actions during session)

View File

@@ -293,9 +293,7 @@ export const defaultMoreMenuItems: SidebarItem[] = [
label: 'AI Observability',
icon: <Brain size={16} />,
isBeta: true,
// Gated behind the `enable_ai_observability` feature flag in
// SideNav's `computedSecondaryMenuItems`; disabled by default.
isEnabled: false,
isEnabled: true,
itemKey: 'ai-observability',
},
{

View File

@@ -1,11 +0,0 @@
import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
export function useIsAIObservabilityEnabled(): boolean {
const { featureFlags } = useAppContext();
return (
featureFlags?.find(
(flag) => flag.name === FeatureKeys.ENABLE_AI_OBSERVABILITY,
)?.active || false
);
}

View File

@@ -9,7 +9,6 @@ var (
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
FeatureEnableAIObservability = featuretypes.MustNewName("enable_ai_observability")
FeatureEnableMetricsReduction = featuretypes.MustNewName("enable_metrics_reduction")
FeatureResolveSemconvFamilies = featuretypes.MustNewName("resolve_semconv_families")
)
@@ -64,14 +63,6 @@ func MustNewRegistry() featuretypes.Registry {
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
&featuretypes.Feature{
Name: FeatureEnableAIObservability,
Kind: featuretypes.KindBoolean,
Stage: featuretypes.StageExperimental,
Description: "Controls whether ai observability is enabled",
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
},
&featuretypes.Feature{
Name: FeatureEnableMetricsReduction,
Kind: featuretypes.KindBoolean,

View File

@@ -1478,15 +1478,6 @@ func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
aiObservability := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureEnableAIObservability, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureEnableAIObservability.String()),
Active: aiObservability,
Usage: 0,
UsageLimit: -1,
Route: "",
})
aH.Respond(w, featureSet)
}

View File

@@ -364,12 +364,26 @@ func (provider *provider) gc(ctx context.Context, org *types.Organization) error
}
func (provider *provider) flushLastObservedAt(ctx context.Context, org *types.Organization) error {
accessTokenToLastObservedAt, err := provider.listLastObservedAtDesc(ctx, org.ID)
tokens, err := provider.tokenStore.ListByOrgID(ctx, org.ID)
if err != nil {
return err
}
if err := provider.tokenStore.UpdateLastObservedAtByAccessToken(ctx, accessTokenToLastObservedAt); err != nil {
observedTokens := make([]*authtypes.StorableToken, 0, len(tokens))
for _, token := range tokens {
cachedLastObservedAt, ok := provider.lastObservedAtCache.Get(lastObservedAtCacheKey(token.AccessToken, token.UserID))
if !ok {
continue
}
if err := token.UpdateLastObservedAt(cachedLastObservedAt); err != nil {
continue
}
observedTokens = append(observedTokens, token)
}
if err := provider.tokenStore.UpdateLastObservedAt(ctx, observedTokens); err != nil {
return err
}

View File

@@ -232,15 +232,16 @@ func (store *store) ListByUserID(ctx context.Context, userID valuer.UUID) ([]*au
return tokens, nil
}
func (store *store) UpdateLastObservedAtByAccessToken(ctx context.Context, accessTokenToLastObservedAt []map[string]any) error {
if len(accessTokenToLastObservedAt) == 0 {
func (store *store) UpdateLastObservedAt(ctx context.Context, tokens []*authtypes.StorableToken) error {
if len(tokens) == 0 {
return nil
}
values := store.
sqlstore.
BunDBCtx(ctx).
NewValues(&accessTokenToLastObservedAt)
NewValues(&tokens).
Column("id", "last_observed_at", "updated_at")
_, err := store.
sqlstore.
@@ -250,8 +251,8 @@ func (store *store) UpdateLastObservedAtByAccessToken(ctx context.Context, acces
Model((*authtypes.StorableToken)(nil)).
TableExpr("update_cte").
Set("last_observed_at = update_cte.last_observed_at").
Where("auth_token.access_token = update_cte.access_token").
Where("auth_token.user_id = update_cte.user_id").
Set("updated_at = update_cte.updated_at").
Where("auth_token.id = update_cte.id").
Exec(ctx)
if err != nil {
return err

View File

@@ -0,0 +1,74 @@
package sqltokenizerstore
import (
"context"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUpdateLastObservedAt(t *testing.T) {
testCases := []struct {
name string
provider string
tokens []*authtypes.StorableToken
expectedQuery string
}{
{
name: "Sqlite_Empty",
provider: "sqlite",
tokens: nil,
expectedQuery: "",
},
{
name: "Postgres_Empty",
provider: "postgres",
tokens: []*authtypes.StorableToken{},
expectedQuery: "",
},
{
name: "Sqlite_OneToken",
provider: "sqlite",
tokens: []*authtypes.StorableToken{
{ID: valuer.MustNewUUID("019984d1-0000-7000-8000-000000000001"), AccessToken: "access-one", RefreshToken: "refresh-one", LastObservedAt: time.Date(2026, 9, 22, 10, 0, 0, 0, time.UTC), UpdatedAt: time.Date(2026, 9, 22, 10, 0, 1, 0, time.UTC)},
},
expectedQuery: `WITH "update_cte" ("id", "last_observed_at", "updated_at") AS (VALUES ('019984d1-0000-7000-8000-000000000001', '2026-09-22 10:00:00+00:00', '2026-09-22 10:00:01+00:00')) UPDATE "auth_token" AS "auth_token" SET last_observed_at = update_cte.last_observed_at, updated_at = update_cte.updated_at FROM update_cte WHERE (auth_token.id = update_cte.id)`,
},
{
name: "Postgres_TwoTokens",
provider: "postgres",
tokens: []*authtypes.StorableToken{
{ID: valuer.MustNewUUID("019984d1-0000-7000-8000-000000000002"), AccessToken: "access-two", RefreshToken: "refresh-two", LastObservedAt: time.Date(2026, 9, 22, 11, 0, 0, 0, time.UTC), UpdatedAt: time.Date(2026, 9, 22, 11, 0, 1, 0, time.UTC)},
{ID: valuer.MustNewUUID("019984d1-0000-7000-8000-000000000003"), AccessToken: "access-three", RefreshToken: "refresh-three", LastObservedAt: time.Date(2026, 9, 22, 12, 0, 0, 0, time.UTC), UpdatedAt: time.Date(2026, 9, 22, 12, 0, 1, 0, time.UTC)},
},
expectedQuery: `WITH "update_cte" ("id", "last_observed_at", "updated_at") AS (VALUES ('019984d1-0000-7000-8000-000000000002'::text, '2026-09-22 11:00:00+00:00'::TIMESTAMPTZ, '2026-09-22 11:00:01+00:00'::TIMESTAMPTZ), ('019984d1-0000-7000-8000-000000000003'::text, '2026-09-22 12:00:00+00:00'::TIMESTAMPTZ, '2026-09-22 12:00:01+00:00'::TIMESTAMPTZ)) UPDATE "auth_token" AS "auth_token" SET last_observed_at = update_cte.last_observed_at, updated_at = update_cte.updated_at FROM update_cte WHERE (auth_token.id = update_cte.id)`,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
var executedQuery string
matcher := sqlmock.QueryMatcherFunc(func(_, actual string) error {
executedQuery = actual
return nil
})
sqlStore := sqlstoretest.New(sqlstore.Config{Provider: testCase.provider}, matcher)
if testCase.expectedQuery != "" {
sqlStore.Mock().ExpectExec("").WillReturnResult(sqlmock.NewResult(0, int64(len(testCase.tokens))))
}
err := NewStore(sqlStore).UpdateLastObservedAt(context.Background(), testCase.tokens)
require.NoError(t, err)
require.NoError(t, sqlStore.Mock().ExpectationsWereMet())
assert.Equal(t, testCase.expectedQuery, executedQuery)
})
}
}

View File

@@ -258,6 +258,6 @@ type TokenStore interface {
// Delete a token by userID.
DeleteByUserID(context.Context, valuer.UUID) error
// Update last observed at by access token.
UpdateLastObservedAtByAccessToken(context.Context, []map[string]any) error
// Update last observed at of the given tokens.
UpdateLastObservedAt(context.Context, []*StorableToken) error
}

View File

@@ -1,39 +0,0 @@
# TODO: remove this file once enable_ai_observability flag is removed or defaulted to True
import pytest
from testcontainers.core.container import Network
from fixtures import types
from fixtures.signoz import create_signoz
@pytest.fixture(name="signoz", scope="package")
def signoz_e2e( # pylint: disable=too-many-arguments,too-many-positional-arguments
network: Network,
zeus: types.TestContainerDocker,
gateway: types.TestContainerDocker,
sqlstore: types.TestContainerSQL,
clickhouse: types.TestContainerClickhouse,
request: pytest.FixtureRequest,
pytestconfig: pytest.Config,
) -> types.SigNoz:
"""
E2E-scoped SigNoz override. Enables the experimental AI/LLM Observability
module (disabled by default in pkg/flagger/registry.go) so its routes render
instead of redirecting to /home — required by the llm-o11y e2e specs. Scoped
to the e2e package via this conftest so normal integration tests keep the
stock feature set. Follows the same pattern as
tests/integration/tests/metricreduction/conftest.py.
"""
return create_signoz(
network=network,
zeus=zeus,
gateway=gateway,
sqlstore=sqlstore,
clickhouse=clickhouse,
request=request,
pytestconfig=pytestconfig,
cache_key="signoz_e2e",
env_overrides={
"SIGNOZ_FLAGGER_CONFIG_BOOLEAN_ENABLE__AI__OBSERVABILITY": True,
},
)