fix: special handling of unmarshalling for invalid json dashboards (#108519)

* implement special dashboard fallback logic when dashboard data has invalid json behind feature flag

---------

Co-authored-by: Will Assis <william@williamassis.com>
This commit is contained in:
Mustafa Sencer Özcan
2025-07-30 10:18:38 -04:00
committed by GitHub
co-authored by Will Assis
parent 4a34a2313c
commit 98e37f2ca9
36 changed files with 218 additions and 35 deletions
@@ -302,6 +302,10 @@ export interface FeatureToggles {
*/
dashboardSchemaValidationLogging?: boolean;
/**
* Enable fallback parsing behavior when scan row encounters invalid dashboard JSON
*/
scanRowInvalidDashboardParseFallbackEnabled?: boolean;
/**
* Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)
*/
datasourceQueryTypes?: boolean;
@@ -63,6 +63,12 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err
},
}
featureManager, err := featuremgmt.ProvideManagerService(cfg)
if err != nil {
return err
}
featureToggles := featuremgmt.ProvideToggles(featureManager)
provisioning, err := newStubProvisioning(cfg.ProvisioningPath)
if err != nil {
return err
@@ -76,9 +82,10 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err
nil, // no librarypanels.Service
sort.ProvideService(),
acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
featureToggles,
)
client, err := newUnifiedClient(cfg, sqlStore)
client, err := newUnifiedClient(cfg, sqlStore, featureToggles)
if err != nil {
return err
}
@@ -215,12 +222,7 @@ func promptYesNo(prompt string) (bool, error) {
}
}
func newUnifiedClient(cfg *setting.Cfg, sqlStore db.DB) (resource.ResourceClient, error) {
featureManager, err := featuremgmt.ProvideManagerService(cfg)
if err != nil {
return nil, err
}
featureToggles := featuremgmt.ProvideToggles(featureManager)
func newUnifiedClient(cfg *setting.Cfg, sqlStore db.DB, featureToggles featuremgmt.FeatureToggles) (resource.ResourceClient, error) {
return unified.ProvideUnifiedStorageClient(&unified.Options{
Cfg: cfg,
Features: featureToggles,
@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/search/sort"
@@ -51,9 +52,10 @@ func ProvideLegacyMigrator(
provisioning provisioning.ProvisioningService, // only needed for dashboard settings
libraryPanelSvc librarypanels.Service,
accessControl accesscontrol.AccessControl,
features featuremgmt.FeatureToggles,
) LegacyMigrator {
dbp := legacysql.NewDatabaseProvider(sql)
return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService(), accessControl)
return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService(), accessControl, features)
}
type BlobStoreInfo struct {
@@ -309,11 +311,12 @@ func (a *dashboardSqlAccess) migrateDashboards(ctx context.Context, orgId int64,
for _, row := range rows.rejected {
id := row.Dash.Labels[utils.LabelKeyDeprecatedInternalID]
a.log.Warn("rejected dashboard",
"namespace", opts.Namespace,
"dashboard", row.Dash.Name,
"uid", row.Dash.UID,
"id", id,
"version", row.Dash.Generation,
"stackId", opts.StackID,
"namespace", opts.Namespace,
)
opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name))
}
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -29,6 +29,7 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/provisioning"
@@ -62,6 +63,8 @@ type dashboardSqlAccess struct {
namespacer request.NamespaceMapper
provisioning provisioning.ProvisioningService
invalidDashboardParseFallbackEnabled bool
// Use for writing (not reading)
dashStore dashboards.Store
dashboardSearchClient legacysearcher.DashboardSearchClient
@@ -82,17 +85,19 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
libraryPanelSvc librarypanels.Service,
sorter sort.Service,
accessControl accesscontrol.AccessControl,
features featuremgmt.FeatureToggles,
) DashboardAccess {
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
return &dashboardSqlAccess{
sql: sql,
namespacer: namespacer,
dashStore: dashStore,
provisioning: provisioning,
dashboardSearchClient: *dashboardSearchClient,
libraryPanelSvc: libraryPanelSvc,
accessControl: accessControl,
log: log.New("dashboard.legacysql"),
sql: sql,
namespacer: namespacer,
dashStore: dashStore,
provisioning: provisioning,
dashboardSearchClient: *dashboardSearchClient,
libraryPanelSvc: libraryPanelSvc,
accessControl: accessControl,
log: log.New("dashboard.legacysql"),
invalidDashboardParseFallbackEnabled: features.IsEnabled(context.Background(), featuremgmt.FlagScanRowInvalidDashboardParseFallbackEnabled),
}
}
@@ -176,7 +181,7 @@ func (r *rowsWrapper) Next() bool {
r.row, err = r.a.scanRow(r.rows, r.history)
if err != nil {
r.a.log.Error("error scanning dashboard", "error", err)
if len(r.rejected) > 0 || r.row == nil {
if len(r.rejected) > 100 || r.row == nil {
r.err = fmt.Errorf("too many rejected rows (%d) %w", len(r.rejected), err)
return false
}
@@ -228,6 +233,51 @@ func (r *rowsWrapper) Value() []byte {
return b
}
func generateFallbackDashboard(data []byte, title, uid string) ([]byte, error) {
generatedDashboard := map[string]interface{}{
"editable": true,
"id": 1,
"panels": []map[string]interface{}{
{
"description": "The JSON is invalid. You can import it again after fixing it.",
"gridPos": map[string]interface{}{"h": 8, "w": 24, "x": 0, "y": 0},
"id": 1,
"options": map[string]interface{}{
"code": map[string]interface{}{"language": "plaintext", "showLineNumbers": false, "showMiniMap": false},
"content": string(data),
"mode": "code",
},
"title": "Invalid dashboard",
"type": "text",
},
},
"schemaVersion": 41,
"title": title,
"uid": uid,
"version": 3,
}
return json.Marshal(generatedDashboard)
}
func (a *dashboardSqlAccess) parseDashboard(dash *dashboardV1.Dashboard, data []byte, id int64, title string) error {
if err := dash.Spec.UnmarshalJSON(data); err != nil {
a.log.Warn("error unmarshalling dashboard spec. Generating fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name)
dash.Spec = *dashboardV0.NewDashboardSpec()
dashboardData, err := generateFallbackDashboard(data, title, string(dash.UID))
if err != nil {
a.log.Warn("error generating fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name)
return err
}
if err = dash.Spec.UnmarshalJSON(dashboardData); err != nil {
a.log.Warn("error unmarshalling fallback dashboard data", "error", err, "uid", dash.UID, "name", dash.Name)
return err
}
}
return nil
}
func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRow, error) {
dash := &dashboardV1.Dashboard{
TypeMeta: dashboardV1.DashboardResourceInfo.TypeMeta(),
@@ -238,6 +288,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
var dashboard_id int64
var orgId int64
var folder_uid sql.NullString
var title string
var updated time.Time
var updatedBy sql.NullString
var updatedByID sql.NullInt64
@@ -257,7 +308,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
var data []byte // the dashboard JSON
var version int64
err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &folder_uid,
err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &title, &folder_uid,
&deleted, &plugin_id,
&origin_name, &origin_path, &origin_hash, &origin_ts,
&created, &createdBy, &createdByID,
@@ -286,6 +337,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
dash.SetCreationTimestamp(metav1.NewTime(created))
meta, err := utils.MetaAccessor(dash)
if err != nil {
a.log.Debug("failed to get meta accessor for dashboard", "error", err, "uid", dash.UID, "name", dash.Name, "version", version)
return nil, err
}
meta.SetUpdatedTimestamp(&updated)
@@ -331,9 +383,14 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
}
if len(data) > 0 {
err = dash.Spec.UnmarshalJSON(data)
if err != nil {
return row, fmt.Errorf("JSON unmarshal error for: %s // %w", dash.Name, err)
if a.invalidDashboardParseFallbackEnabled {
if err := a.parseDashboard(dash, data, dashboard_id, title); err != nil {
return row, err
}
} else {
if err := dash.Spec.UnmarshalJSON(data); err != nil {
return row, fmt.Errorf("JSON unmarshal error for: %s // %w", dash.Name, err)
}
}
}
// Ignore any saved values for id/version/uid
@@ -32,13 +32,15 @@ func TestScanRow(t *testing.T) {
provisioner := provisioning.NewProvisioningServiceMock(context.Background())
provisioner.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "provisioner" }
store := &dashboardSqlAccess{
namespacer: func(_ int64) string { return "default" },
provisioning: provisioner,
log: log.New("test"),
namespacer: func(_ int64) string { return "default" },
provisioning: provisioner,
log: log.New("test"),
invalidDashboardParseFallbackEnabled: false,
}
columns := []string{"orgId", "dashboard_id", "name", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data", "api_version"}
columns := []string{"orgId", "dashboard_id", "name", "title", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data", "api_version"}
id := int64(100)
uid := "someuid"
title := "Test Dashboard"
folderUID := "folder123"
timestamp := time.Now()
@@ -49,7 +51,7 @@ func TestScanRow(t *testing.T) {
updatedUser := "updator"
t.Run("Should scan a valid row correctly", func(t *testing.T) {
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
resultRows, err := mockDB.Query("SELECT *")
require.NoError(t, err)
@@ -59,7 +61,7 @@ func TestScanRow(t *testing.T) {
row, err := store.scanRow(resultRows, false)
require.NoError(t, err)
require.NotNil(t, row)
require.Equal(t, "Test Dashboard", row.Dash.Name)
require.Equal(t, uid, row.Dash.Name)
require.Equal(t, version, row.RV) // rv should be the dashboard version
require.Equal(t, common.Unstructured{
Object: map[string]interface{}{"key": "value"},
@@ -80,7 +82,7 @@ func TestScanRow(t *testing.T) {
})
t.Run("File provisioned dashboard should have annotations", func(t *testing.T) {
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "", "provisioner", pathToFile, "hashing", 100000, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "provisioner", pathToFile, "hashing", 100000, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
resultRows, err := mockDB.Query("SELECT *")
require.NoError(t, err)
@@ -108,7 +110,7 @@ func TestScanRow(t *testing.T) {
})
t.Run("Plugin provisioned dashboard should have annotations", func(t *testing.T) {
rows := sqlmock.NewRows(columns).AddRow(1, id, title, folderUID, nil, "slo", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "slo", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, []byte(`{"key": "value"}`), "vXyz")
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
resultRows, err := mockDB.Query("SELECT *")
require.NoError(t, err)
@@ -144,7 +146,7 @@ func TestScanRow(t *testing.T) {
// In migration scenario, COALESCE functions return dashboard table values
// when dashboard_version values are NULL, ensuring all dashboards are migrated
rows := sqlmock.NewRows(columns).AddRow(
1, id, title, folderUID, nil, "", // basic dashboard fields
1, id, uid, title, folderUID, nil, "", // basic dashboard fields
"", "", "", 0, // origin fields
timestamp, createdUser, 0, // created fields
// These represent COALESCED values from dashboard table (not version table)
@@ -163,7 +165,8 @@ func TestScanRow(t *testing.T) {
require.NotNil(t, row)
// Verify migration scenario works correctly with fallback data
require.Equal(t, title, row.Dash.Name)
require.Equal(t, uid, row.Dash.Name)
require.Equal(t, "Migrated Dashboard", row.Dash.Spec.Object["title"])
require.Equal(t, migrationVersion, row.RV) // Should use COALESCEd dashboard table version
require.Equal(t, common.Unstructured{
Object: map[string]interface{}{
@@ -187,6 +190,72 @@ func TestScanRow(t *testing.T) {
require.Equal(t, folderUID, meta.GetFolder())
require.Equal(t, "dashboard.grafana.app/"+migrationAPIVersion, row.Dash.APIVersion)
})
t.Run("should follow dashboard template when failing to unmarshal dashboard if feature flag X is enabled", func(t *testing.T) {
// row with bad data
badData := []byte(`{"rows":[{"panels":[{"targets":[{"refId":"A","target":"aliasSub(alias, '^(.{27}).+', '\1...')"}]}]}]}`)
rows := sqlmock.NewRows(columns).AddRow(1, id, uid, title, folderUID, nil, "", "", "", "", 0, timestamp, createdUser, 0, timestamp, updatedUser, 0, version, message, badData, "vXyz")
mock.ExpectQuery("SELECT *").WillReturnRows(rows)
resultRows, err := mockDB.Query("SELECT *")
require.NoError(t, err)
defer resultRows.Close() // nolint:errcheck
resultRows.Next()
row, err := store.scanRow(resultRows, false)
require.Error(t, err, "JSON unmarshal error for: Test Dashboard // invalid character '1' in string escape code")
require.NotNil(t, row)
// correctly scans these
require.Equal(t, uid, row.Dash.Name)
require.Equal(t, version, row.RV)
require.Equal(t, "default", row.Dash.Namespace)
require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token)
// failure case: does NOT parse the dashboard itself
require.Equal(t, common.Unstructured{
Object: nil,
}, row.Dash.Spec)
// store with feature flag enabled
store = &dashboardSqlAccess{
namespacer: func(_ int64) string { return "default" },
provisioning: provisioner,
log: log.New("test"),
invalidDashboardParseFallbackEnabled: true,
}
row, err = store.scanRow(resultRows, false)
require.NoError(t, err)
require.NotNil(t, row)
require.Equal(t, uid, row.Dash.Name)
require.Equal(t, version, row.RV)
require.Equal(t, "default", row.Dash.Namespace)
require.Equal(t, &continueToken{orgId: int64(1), id: id}, row.token)
// instead of failing, create dummy dashboard with broken json inlined in text panel
require.Equal(t, title, row.Dash.Spec.Object["title"])
panels, exists := row.Dash.Spec.Object["panels"]
require.True(t, exists, "panels property should exist")
panelsSlice, ok := panels.([]interface{})
require.True(t, ok, "panels should be a slice")
require.Len(t, panelsSlice, 1, "panels should have exactly one element")
panel, ok := panelsSlice[0].(map[string]interface{})
require.True(t, ok, "panel should be a map")
options, exists := panel["options"]
require.True(t, exists, "panel should have options property")
optionsMap, ok := options.(map[string]interface{})
require.True(t, ok, "options should be a map")
content, exists := optionsMap["content"]
require.True(t, exists, "options should have content property")
contentStr, ok := content.(string)
require.True(t, ok, "content should be a string")
require.Equal(t, string(badData), contentStr, "content should match bad json data")
})
}
func TestBuildSaveDashboardCommand(t *testing.T) {
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
@@ -2,6 +2,7 @@ SELECT
dashboard.org_id,
dashboard.id,
dashboard.uid,
dashboard.title,
dashboard.folder_uid,
dashboard.deleted,
plugin_id,
+1 -1
View File
@@ -143,7 +143,7 @@ func RegisterAPIService(
folderClient: folderClient,
legacy: &DashboardStorage{
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, accessControl),
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, accessControl, features),
DashboardService: dashboardService,
},
reg: reg,
+2 -2
View File
@@ -746,7 +746,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser
}
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
factory := github.ProvideFactory()
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl)
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl, featureToggles)
databaseDatabase := database5.ProvideDatabase(sqlStore, tracer)
secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, registerer)
if err != nil {
@@ -1307,7 +1307,7 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface {
}
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
factory := github.ProvideFactory()
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl)
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl, featureToggles)
databaseDatabase := database5.ProvideDatabase(sqlStore, tracer)
secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(databaseDatabase, tracer, registerer)
if err != nil {
+6
View File
@@ -501,6 +501,12 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad,
},
{
Name: "scanRowInvalidDashboardParseFallbackEnabled",
Description: "Enable fallback parsing behavior when scan row encounters invalid dashboard JSON",
Stage: FeatureStageExperimental,
Owner: grafanaSearchAndStorageSquad,
},
{
Name: "datasourceQueryTypes",
Description: "Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)",
+1
View File
@@ -65,6 +65,7 @@ kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,fals
dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false
dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false
dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squad,false,false,false
scanRowInvalidDashboardParseFallbackEnabled,experimental,@grafana/search-and-storage,false,false,false
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
queryService,experimental,@grafana/grafana-datasources-core-services,false,true,false
queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,false,true,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
65 dashboardDisableSchemaValidationV1 experimental @grafana/grafana-app-platform-squad false false false
66 dashboardDisableSchemaValidationV2 experimental @grafana/grafana-app-platform-squad false false false
67 dashboardSchemaValidationLogging experimental @grafana/grafana-app-platform-squad false false false
68 scanRowInvalidDashboardParseFallbackEnabled experimental @grafana/search-and-storage false false false
69 datasourceQueryTypes experimental @grafana/grafana-app-platform-squad false true false
70 queryService experimental @grafana/grafana-datasources-core-services false true false
71 queryServiceRewrite experimental @grafana/grafana-datasources-core-services false true false
+4
View File
@@ -271,6 +271,10 @@ const (
// Log schema validation errors so they can be analyzed later
FlagDashboardSchemaValidationLogging = "dashboardSchemaValidationLogging"
// FlagScanRowInvalidDashboardParseFallbackEnabled
// Enable fallback parsing behavior when scan row encounters invalid dashboard JSON
FlagScanRowInvalidDashboardParseFallbackEnabled = "scanRowInvalidDashboardParseFallbackEnabled"
// FlagDatasourceQueryTypes
// Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)
FlagDatasourceQueryTypes = "datasourceQueryTypes"
+12
View File
@@ -2760,6 +2760,18 @@
"codeowner": "@grafana/identity-access-team"
}
},
{
"metadata": {
"name": "scanRowInvalidDashboardParseFallbackEnabled",
"resourceVersion": "1753730899886",
"creationTimestamp": "2025-07-28T19:28:19Z"
},
"spec": {
"description": "Enable fallback parsing behavior when scan row encounters invalid dashboard JSON",
"stage": "experimental",
"codeowner": "@grafana/search-and-storage"
}
},
{
"metadata": {
"name": "scopeApi",