diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_panels.go b/pkg/types/dashboardtypes/perses_v1_to_v2_panels.go index c923e9d73e..a7ddf7918a 100644 --- a/pkg/types/dashboardtypes/perses_v1_to_v2_panels.go +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_panels.go @@ -259,6 +259,7 @@ func (d *v1Decoder) mapV1SelectFields(w map[string]any) []telemetrytypes.Telemet if len(raw) == 0 { return nil } + normalizePreV5FieldKeys(raw) fields, err := decodeTelemetryFields(raw) if err != nil { d.note("widget %q has malformed %s: %v", d.readString(w, "id"), field, err) diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_queries.go b/pkg/types/dashboardtypes/perses_v1_to_v2_queries.go index e6eb340e38..6199c6e6e4 100644 --- a/pkg/types/dashboardtypes/perses_v1_to_v2_queries.go +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_queries.go @@ -102,8 +102,8 @@ func (d *v1Decoder) collectV1QueryEnvelopes(widget map[string]any) ([]map[string var out []map[string]any var signal telemetrytypes.Signal for _, q := range d.readObjects(builder, "queryData") { - normalizeV1Having(q) - normalizeV1LogTraceAggregations(q) + normalizePreV5Having(q) + normalizePreV5LogTraceAggregations(q) name := d.readString(q, "queryName") out = append(out, qb.WrapInV5Envelope(name, q, string(qb.QueryTypeBuilder.StringValue()))) if signal.IsZero() { @@ -111,12 +111,12 @@ func (d *v1Decoder) collectV1QueryEnvelopes(widget map[string]any) ([]map[string } } for _, f := range d.readObjects(builder, "queryFormulas") { - normalizeV1Having(f) + normalizePreV5Having(f) name := d.readString(f, "queryName") out = append(out, qb.WrapInV5Envelope(name, f, string(qb.QueryTypeFormula.StringValue()))) } for _, op := range d.readObjects(builder, "queryTraceOperator") { - normalizeV1Having(op) + normalizePreV5Having(op) name := d.readString(op, "queryName") out = append(out, qb.WrapInV5Envelope(name, op, string(qb.QueryTypeTraceOperator.StringValue()))) } diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_malformed.go b/pkg/types/dashboardtypes/perses_v1_to_v2_queries_malformed.go similarity index 68% rename from pkg/types/dashboardtypes/perses_v1_to_v2_malformed.go rename to pkg/types/dashboardtypes/perses_v1_to_v2_queries_malformed.go index 97590c736e..b7611022e2 100644 --- a/pkg/types/dashboardtypes/perses_v1_to_v2_malformed.go +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_queries_malformed.go @@ -11,16 +11,16 @@ import ( // Malformed-field normalization // ══════════════════════════════════════════════ // -// Reshape known-malformed v1 fields into their v5 form before decode. A common -// case: a dashboard stamped version:"v5" whose bodies aren't actually v5-shaped -// bypasses the v4→v5 migrator (pkg/transition) and then fails the strict v5 -// decode. These mirror the frontend, which normalizes by shape regardless of -// the version tag. +// Reshape known-malformed query-builder fields from their pre-v5 shape into the +// v5 form before decode. A common case: a dashboard stamped version:"v5" whose +// bodies aren't actually v5-shaped bypasses the v4→v5 migrator (pkg/transition) +// and then fails the strict v5 decode. These mirror the frontend, which +// normalizes by shape regardless of the version tag. // // Only reshape known field shapes here; leave genuinely corrupt input (e.g. an // empty required field) to fail validation rather than grow per-case fixups. -// normalizeV1Having rewrites a builder query's v4 having (an array of +// normalizePreV5Having rewrites a builder query's v4 having (an array of // {columnName, op, value} clauses) into the v5 {"expression": ...} shape in // place. The v5 decoder wants an object, but a query can still carry the array // form — e.g. a dashboard stamped version:"v5" whose bodies predate v5, which @@ -28,7 +28,7 @@ import ( // convertHavingToExpression (QueryBuilderV2/utils.ts): each clause becomes // "columnName op value", clauses join with " AND ", array values render as // "[v1, v2]". A having that is already an object (or absent) is left untouched. -func normalizeV1Having(query map[string]any) { +func normalizePreV5Having(query map[string]any) { clauses, ok := query["having"].([]any) if !ok { return @@ -49,7 +49,7 @@ func normalizeV1Having(query map[string]any) { query["having"] = map[string]any{"expression": strings.Join(exprs, " AND ")} } -// normalizeV1LogTraceAggregations reshapes a logs/traces builder query's +// normalizePreV5LogTraceAggregations reshapes a logs/traces builder query's // aggregations into the v5 {"expression", "alias"} form in place, dropping the // metric-only fields (metricName/temporality/timeAggregation/spaceAggregation/ // reduceTo) that some dashboards carry on non-metric queries — a logs query @@ -59,7 +59,7 @@ func normalizeV1Having(query map[string]any) { // default an empty one to "count()". Metric queries are left untouched, since a // metric-shaped aggregation is correct for them. Idempotent on aggregations // that are already expression-only. -func normalizeV1LogTraceAggregations(query map[string]any) { +func normalizePreV5LogTraceAggregations(query map[string]any) { switch signalFromDataSource(query["dataSource"]) { case telemetrytypes.SignalLogs, telemetrytypes.SignalTraces: default: @@ -86,6 +86,33 @@ func normalizeV1LogTraceAggregations(query map[string]any) { } } +// normalizePreV5FieldKeys renames telemetry field keys from the pre-v5 +// query-builder shape ({key, dataType, type}) to the v5 one ({name, +// fieldDataType, fieldContext}) in place — the same mapping WrapInV5Envelope +// does for groupBy/orderBy. Without it an old-shape field decodes with an empty +// name, which TelemetryFieldKey rejects. Entries already carrying "name" are +// left as-is. +func normalizePreV5FieldKeys(fields []any) { + for _, f := range fields { + field, ok := f.(map[string]any) + if !ok { + continue + } + if _, hasName := field["name"]; hasName { + continue + } + if key, ok := field["key"]; ok { + field["name"] = key + } + if dataType, ok := field["dataType"]; ok { + field["fieldDataType"] = dataType + } + if typ, ok := field["type"]; ok { + field["fieldContext"] = typ + } + } +} + // formatHavingValue renders a having clause value: an array as "[v1, v2]", any // scalar as its default string form. func formatHavingValue(value any) string {