From 59e31fc9cbcc4811fc8ec8b2540f325dafc1471c Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 2 Jul 2026 17:51:34 +0200 Subject: [PATCH 1/6] Add null-in-targets telemetry Record whether the bundle config has any null value under the targets section, collected before target overrides are merged. Co-authored-by: Isaac --- .../deploy-app-lifecycle-started/output.txt | 4 + .../telemetry/deploy-compute-type/output.txt | 2 + .../telemetry/deploy-experimental/output.txt | 1 + .../deploy-name-prefix/custom/output.txt | 1 + .../mode-development/output.txt | 1 + .../telemetry/deploy-whl-artifacts/output.txt | 2 + .../bundle/telemetry/deploy/out.telemetry.txt | 4 + .../config/mutator/collect_null_telemetry.go | 48 +++++++++ .../mutator/collect_null_telemetry_test.go | 98 +++++++++++++++++++ bundle/config/mutator/mutator.go | 4 + 10 files changed, 165 insertions(+) create mode 100644 bundle/config/mutator/collect_null_telemetry.go create mode 100644 bundle/config/mutator/collect_null_telemetry_test.go diff --git a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt index 3663f140db9..3fb5f331688 100644 --- a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt +++ b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt @@ -9,6 +9,10 @@ Deployment complete! >>> cat out.requests.txt { "bool_values": [ + { + "key": "null-in-targets", + "value": false + }, { "key": "local.cache.attempt", "value": true diff --git a/acceptance/bundle/telemetry/deploy-compute-type/output.txt b/acceptance/bundle/telemetry/deploy-compute-type/output.txt index 754948595ff..73928c8a088 100644 --- a/acceptance/bundle/telemetry/deploy-compute-type/output.txt +++ b/acceptance/bundle/telemetry/deploy-compute-type/output.txt @@ -34,6 +34,8 @@ local.cache.attempt true local.cache.attempt true local.cache.hit true local.cache.miss true +null-in-targets false +null-in-targets false permissions_section_set false permissions_section_set false presets_name_prefix_is_set false diff --git a/acceptance/bundle/telemetry/deploy-experimental/output.txt b/acceptance/bundle/telemetry/deploy-experimental/output.txt index e8c847cd617..5196863ec15 100644 --- a/acceptance/bundle/telemetry/deploy-experimental/output.txt +++ b/acceptance/bundle/telemetry/deploy-experimental/output.txt @@ -21,6 +21,7 @@ has_classic_job_compute false has_serverless_compute false local.cache.attempt true local.cache.miss true +null-in-targets false permissions_section_set false presets_name_prefix_is_set false python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt index 4f5630089f2..08dd225d829 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt @@ -17,6 +17,7 @@ has_classic_job_compute false has_serverless_compute false local.cache.attempt true local.cache.miss true +null-in-targets false permissions_section_set false presets_name_prefix_is_set true python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt index 3122ad1ee9a..bc5efa44507 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt @@ -17,6 +17,7 @@ has_classic_job_compute false has_serverless_compute false local.cache.attempt true local.cache.miss true +null-in-targets false permissions_section_set false presets_name_prefix_is_set true python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt index 95776306d7e..85262c767fa 100644 --- a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt +++ b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt @@ -39,6 +39,8 @@ local.cache.attempt true local.cache.attempt true local.cache.hit true local.cache.miss true +null-in-targets false +null-in-targets false permissions_section_set false permissions_section_set false python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy/out.telemetry.txt b/acceptance/bundle/telemetry/deploy/out.telemetry.txt index 1a4b5cbb143..aab20a4bc45 100644 --- a/acceptance/bundle/telemetry/deploy/out.telemetry.txt +++ b/acceptance/bundle/telemetry/deploy/out.telemetry.txt @@ -42,6 +42,10 @@ "lookup_variable_count": 0, "target_count": 1, "bool_values": [ + { + "key": "null-in-targets", + "value": false + }, { "key": "local.cache.attempt", "value": true diff --git a/bundle/config/mutator/collect_null_telemetry.go b/bundle/config/mutator/collect_null_telemetry.go new file mode 100644 index 00000000000..0abe24d8029 --- /dev/null +++ b/bundle/config/mutator/collect_null_telemetry.go @@ -0,0 +1,48 @@ +package mutator + +import ( + "context" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" +) + +// collectNullTelemetry records whether the config has any null value anywhere +// under the "targets" section. It must run before the target overrides are +// merged, since merging drops the "targets" section from the config tree. +// +// It inspects the normalized config, which is what the merge itself operates on. +// Normalization drops nulls on scalar-typed fields (e.g. workspace.host: with no +// value), so those are not counted; nulls on map, sequence, and struct-typed +// fields (e.g. resources:, variables.foo:) survive and are the ones the merge sees. +type collectNullTelemetry struct{} + +func CollectNullTelemetry() bundle.Mutator { + return &collectNullTelemetry{} +} + +func (*collectNullTelemetry) Name() string { + return "CollectNullTelemetry" +} + +func (*collectNullTelemetry) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + hasNull := false + + targets := b.Config.Value().Get("targets") + if targets.Kind() == dyn.KindMap { + err := dyn.WalkReadOnly(targets, func(p dyn.Path, v dyn.Value) error { + if v.Kind() == dyn.KindNil { + hasNull = true + return dyn.ErrSkip + } + return nil + }) + if err != nil { + return diag.FromErr(err) + } + } + + b.Metrics.SetBoolValue("null-in-targets", hasNull) + return nil +} diff --git a/bundle/config/mutator/collect_null_telemetry_test.go b/bundle/config/mutator/collect_null_telemetry_test.go new file mode 100644 index 00000000000..52bf8990083 --- /dev/null +++ b/bundle/config/mutator/collect_null_telemetry_test.go @@ -0,0 +1,98 @@ +package mutator_test + +import ( + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/mutator" + "github.com/databricks/cli/libs/telemetry/protos" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCollectNullTelemetry(t *testing.T) { + tests := []struct { + name string + yaml string + expected bool + }{ + { + name: "no null under targets", + yaml: ` +bundle: + name: test +targets: + dev: + workspace: + host: https://example.test +`, + expected: false, + }, + { + name: "null map value under targets", + yaml: ` +targets: + dev: + resources: +`, + expected: true, + }, + { + name: "null nested deep under targets", + yaml: ` +targets: + dev: + resources: + jobs: + my_job: +`, + expected: true, + }, + { + name: "scalar null under targets is dropped by normalization", + yaml: ` +targets: + dev: + workspace: + host: +`, + expected: false, + }, + { + name: "null outside targets is ignored", + yaml: ` +targets: + dev: + workspace: + host: https://example.test +variables: + foo: +`, + expected: false, + }, + { + name: "no targets section", + yaml: ` +bundle: + name: test +`, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root, diags := config.LoadFromBytes("databricks.yml", []byte(tt.yaml)) + require.NoError(t, diags.Error()) + + b := &bundle.Bundle{Config: *root} + applyDiags := bundle.Apply(t.Context(), b, mutator.CollectNullTelemetry()) + require.Empty(t, applyDiags) + + assert.Equal(t, []protos.BoolMapEntry{ + {Key: "null-in-targets", Value: tt.expected}, + }, b.Metrics.BoolValues) + }) + } +} diff --git a/bundle/config/mutator/mutator.go b/bundle/config/mutator/mutator.go index a00488a50fa..bf213bc15b7 100644 --- a/bundle/config/mutator/mutator.go +++ b/bundle/config/mutator/mutator.go @@ -32,5 +32,9 @@ func DefaultMutators(ctx context.Context, b *bundle.Bundle) { // See the mutators for more details. validate.NoVariableReferenceInResourceKey(), validate.UniqueResourceKeys(), + + // Must run before the target overrides are merged: it inspects the + // "targets" section, which is dropped from the config tree by the merge. + CollectNullTelemetry(), ) } From 31a65965580da0eb6f1ff9d815c8be70bf555e45 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 6 Jul 2026 13:10:13 +0200 Subject: [PATCH 2/6] Refactor null-in-targets detection to load time Move the null-under-targets check from the CollectNullTelemetry mutator into LoadFromBytes, so it runs on the raw (pre-normalization) YAML and captures scalar-typed nulls (like workspace.host:) that normalization would otherwise drop. The mutator now just surfaces the flag. The Root.Merge method carries the flag forward when included files are merged, so a null in any included file is also counted. Add acceptance tests in acceptance/bundle/null/ that capture the current merge behavior via bundle validate -o json; these will change when the null-removes-value semantics are introduced in a follow-up PR. Co-authored-by: Isaac --- acceptance/bundle/null/map-key/databricks.yml | 17 ++++ acceptance/bundle/null/map-key/out.test.toml | 3 + acceptance/bundle/null/map-key/output.txt | 7 ++ acceptance/bundle/null/map-key/script | 1 + .../null/null-from-include/databricks.yml | 17 ++++ .../null/null-from-include/out.test.toml | 3 + .../bundle/null/null-from-include/output.txt | 11 +++ .../null/null-from-include/override.yml | 6 ++ .../bundle/null/null-from-include/script | 1 + .../bundle/null/scalar-field/databricks.yml | 18 ++++ .../bundle/null/scalar-field/out.test.toml | 3 + .../bundle/null/scalar-field/output.txt | 11 +++ acceptance/bundle/null/scalar-field/script | 1 + acceptance/bundle/null/test.toml | 1 + .../config/mutator/collect_null_telemetry.go | 29 +----- .../mutator/collect_null_telemetry_test.go | 98 ------------------- bundle/config/mutator/mutator.go | 4 - bundle/config/root.go | 44 +++++++++ bundle/config/root_test.go | 88 +++++++++++++++++ bundle/phases/initialize.go | 4 + 20 files changed, 240 insertions(+), 127 deletions(-) create mode 100644 acceptance/bundle/null/map-key/databricks.yml create mode 100644 acceptance/bundle/null/map-key/out.test.toml create mode 100644 acceptance/bundle/null/map-key/output.txt create mode 100644 acceptance/bundle/null/map-key/script create mode 100644 acceptance/bundle/null/null-from-include/databricks.yml create mode 100644 acceptance/bundle/null/null-from-include/out.test.toml create mode 100644 acceptance/bundle/null/null-from-include/output.txt create mode 100644 acceptance/bundle/null/null-from-include/override.yml create mode 100644 acceptance/bundle/null/null-from-include/script create mode 100644 acceptance/bundle/null/scalar-field/databricks.yml create mode 100644 acceptance/bundle/null/scalar-field/out.test.toml create mode 100644 acceptance/bundle/null/scalar-field/output.txt create mode 100644 acceptance/bundle/null/scalar-field/script create mode 100644 acceptance/bundle/null/test.toml delete mode 100644 bundle/config/mutator/collect_null_telemetry_test.go diff --git a/acceptance/bundle/null/map-key/databricks.yml b/acceptance/bundle/null/map-key/databricks.yml new file mode 100644 index 00000000000..24892dd3ca2 --- /dev/null +++ b/acceptance/bundle/null/map-key/databricks.yml @@ -0,0 +1,17 @@ +bundle: + name: test-bundle + +run_as: + user_name: base-user + +targets: + dev: + # Null on a struct-typed field: the target override sets run_as to null, + # which currently leaves the base run_as value in place after merge. + # When null-merge semantics change this field should become absent. + run_as: + +resources: + jobs: + my_job: + name: my job diff --git a/acceptance/bundle/null/map-key/out.test.toml b/acceptance/bundle/null/map-key/out.test.toml new file mode 100644 index 00000000000..d6187dcb046 --- /dev/null +++ b/acceptance/bundle/null/map-key/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = [] diff --git a/acceptance/bundle/null/map-key/output.txt b/acceptance/bundle/null/map-key/output.txt new file mode 100644 index 00000000000..f07a9e50060 --- /dev/null +++ b/acceptance/bundle/null/map-key/output.txt @@ -0,0 +1,7 @@ + +>>> [CLI] bundle validate -o json -t dev +{ + "run_as": { + "user_name": "base-user" + } +} diff --git a/acceptance/bundle/null/map-key/script b/acceptance/bundle/null/map-key/script new file mode 100644 index 00000000000..740eb62abae --- /dev/null +++ b/acceptance/bundle/null/map-key/script @@ -0,0 +1 @@ +trace $CLI bundle validate -o json -t dev | jq '{run_as: .run_as}' diff --git a/acceptance/bundle/null/null-from-include/databricks.yml b/acceptance/bundle/null/null-from-include/databricks.yml new file mode 100644 index 00000000000..e20950f81c7 --- /dev/null +++ b/acceptance/bundle/null/null-from-include/databricks.yml @@ -0,0 +1,17 @@ +bundle: + name: test-bundle + +include: + - override.yml + +workspace: + root_path: /Workspace/Users/base-root + +targets: + dev: + default: true + +resources: + jobs: + my_job: + name: my job diff --git a/acceptance/bundle/null/null-from-include/out.test.toml b/acceptance/bundle/null/null-from-include/out.test.toml new file mode 100644 index 00000000000..d6187dcb046 --- /dev/null +++ b/acceptance/bundle/null/null-from-include/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = [] diff --git a/acceptance/bundle/null/null-from-include/output.txt b/acceptance/bundle/null/null-from-include/output.txt new file mode 100644 index 00000000000..1e3aa4bcdcb --- /dev/null +++ b/acceptance/bundle/null/null-from-include/output.txt @@ -0,0 +1,11 @@ + +>>> [CLI] bundle validate -o json -t dev +Warning: expected a string value, found null + at targets.dev.workspace.root_path + in override.yml:6:17 + +{ + "workspace": { + "root_path": "/Workspace/Users/base-root" + } +} diff --git a/acceptance/bundle/null/null-from-include/override.yml b/acceptance/bundle/null/null-from-include/override.yml new file mode 100644 index 00000000000..0e7ac0a67ac --- /dev/null +++ b/acceptance/bundle/null/null-from-include/override.yml @@ -0,0 +1,6 @@ +targets: + dev: + # Null in an included file under targets: NullInTargets should be true + # even though the null comes from an included file, not the root config. + workspace: + root_path: diff --git a/acceptance/bundle/null/null-from-include/script b/acceptance/bundle/null/null-from-include/script new file mode 100644 index 00000000000..1cceac6bf5b --- /dev/null +++ b/acceptance/bundle/null/null-from-include/script @@ -0,0 +1 @@ +trace $CLI bundle validate -o json -t dev | jq '{workspace: {root_path: .workspace.root_path}}' diff --git a/acceptance/bundle/null/scalar-field/databricks.yml b/acceptance/bundle/null/scalar-field/databricks.yml new file mode 100644 index 00000000000..7ca100645a0 --- /dev/null +++ b/acceptance/bundle/null/scalar-field/databricks.yml @@ -0,0 +1,18 @@ +bundle: + name: test-bundle + +workspace: + root_path: /Workspace/Users/base-root + +targets: + dev: + workspace: + # Null on a scalar field: normalization drops it, so the base root_path + # survives in the merged config. When null-merge semantics change this + # field should become absent (or empty) in the output. + root_path: + +resources: + jobs: + my_job: + name: my job diff --git a/acceptance/bundle/null/scalar-field/out.test.toml b/acceptance/bundle/null/scalar-field/out.test.toml new file mode 100644 index 00000000000..d6187dcb046 --- /dev/null +++ b/acceptance/bundle/null/scalar-field/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = [] diff --git a/acceptance/bundle/null/scalar-field/output.txt b/acceptance/bundle/null/scalar-field/output.txt new file mode 100644 index 00000000000..a24bfa2f603 --- /dev/null +++ b/acceptance/bundle/null/scalar-field/output.txt @@ -0,0 +1,11 @@ + +>>> [CLI] bundle validate -o json -t dev +Warning: expected a string value, found null + at targets.dev.workspace.root_path + in databricks.yml:13:17 + +{ + "workspace": { + "root_path": "/Workspace/Users/base-root" + } +} diff --git a/acceptance/bundle/null/scalar-field/script b/acceptance/bundle/null/scalar-field/script new file mode 100644 index 00000000000..1cceac6bf5b --- /dev/null +++ b/acceptance/bundle/null/scalar-field/script @@ -0,0 +1 @@ +trace $CLI bundle validate -o json -t dev | jq '{workspace: {root_path: .workspace.root_path}}' diff --git a/acceptance/bundle/null/test.toml b/acceptance/bundle/null/test.toml new file mode 100644 index 00000000000..c63fe3fe108 --- /dev/null +++ b/acceptance/bundle/null/test.toml @@ -0,0 +1 @@ +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = [] diff --git a/bundle/config/mutator/collect_null_telemetry.go b/bundle/config/mutator/collect_null_telemetry.go index 0abe24d8029..2f6feb705fd 100644 --- a/bundle/config/mutator/collect_null_telemetry.go +++ b/bundle/config/mutator/collect_null_telemetry.go @@ -5,17 +5,12 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/dyn" ) // collectNullTelemetry records whether the config has any null value anywhere -// under the "targets" section. It must run before the target overrides are -// merged, since merging drops the "targets" section from the config tree. -// -// It inspects the normalized config, which is what the merge itself operates on. -// Normalization drops nulls on scalar-typed fields (e.g. workspace.host: with no -// value), so those are not counted; nulls on map, sequence, and struct-typed -// fields (e.g. resources:, variables.foo:) survive and are the ones the merge sees. +// under the "targets" section, as authored. The signal is computed at load time +// (before normalization drops nulls on scalar-typed fields) and accumulated across +// all included files; this mutator just surfaces it as telemetry. type collectNullTelemetry struct{} func CollectNullTelemetry() bundle.Mutator { @@ -27,22 +22,6 @@ func (*collectNullTelemetry) Name() string { } func (*collectNullTelemetry) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { - hasNull := false - - targets := b.Config.Value().Get("targets") - if targets.Kind() == dyn.KindMap { - err := dyn.WalkReadOnly(targets, func(p dyn.Path, v dyn.Value) error { - if v.Kind() == dyn.KindNil { - hasNull = true - return dyn.ErrSkip - } - return nil - }) - if err != nil { - return diag.FromErr(err) - } - } - - b.Metrics.SetBoolValue("null-in-targets", hasNull) + b.Metrics.SetBoolValue("null-in-targets", b.Config.NullInTargets()) return nil } diff --git a/bundle/config/mutator/collect_null_telemetry_test.go b/bundle/config/mutator/collect_null_telemetry_test.go deleted file mode 100644 index 52bf8990083..00000000000 --- a/bundle/config/mutator/collect_null_telemetry_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package mutator_test - -import ( - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/mutator" - "github.com/databricks/cli/libs/telemetry/protos" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCollectNullTelemetry(t *testing.T) { - tests := []struct { - name string - yaml string - expected bool - }{ - { - name: "no null under targets", - yaml: ` -bundle: - name: test -targets: - dev: - workspace: - host: https://example.test -`, - expected: false, - }, - { - name: "null map value under targets", - yaml: ` -targets: - dev: - resources: -`, - expected: true, - }, - { - name: "null nested deep under targets", - yaml: ` -targets: - dev: - resources: - jobs: - my_job: -`, - expected: true, - }, - { - name: "scalar null under targets is dropped by normalization", - yaml: ` -targets: - dev: - workspace: - host: -`, - expected: false, - }, - { - name: "null outside targets is ignored", - yaml: ` -targets: - dev: - workspace: - host: https://example.test -variables: - foo: -`, - expected: false, - }, - { - name: "no targets section", - yaml: ` -bundle: - name: test -`, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - root, diags := config.LoadFromBytes("databricks.yml", []byte(tt.yaml)) - require.NoError(t, diags.Error()) - - b := &bundle.Bundle{Config: *root} - applyDiags := bundle.Apply(t.Context(), b, mutator.CollectNullTelemetry()) - require.Empty(t, applyDiags) - - assert.Equal(t, []protos.BoolMapEntry{ - {Key: "null-in-targets", Value: tt.expected}, - }, b.Metrics.BoolValues) - }) - } -} diff --git a/bundle/config/mutator/mutator.go b/bundle/config/mutator/mutator.go index bf213bc15b7..a00488a50fa 100644 --- a/bundle/config/mutator/mutator.go +++ b/bundle/config/mutator/mutator.go @@ -32,9 +32,5 @@ func DefaultMutators(ctx context.Context, b *bundle.Bundle) { // See the mutators for more details. validate.NoVariableReferenceInResourceKey(), validate.UniqueResourceKeys(), - - // Must run before the target overrides are merged: it inspects the - // "targets" section, which is dropped from the config tree by the merge. - CollectNullTelemetry(), ) } diff --git a/bundle/config/root.go b/bundle/config/root.go index caca8e1f1ad..641c2f557e0 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -30,6 +30,12 @@ type Root struct { //nolint:recvcheck // value receivers for read-only accessors value dyn.Value depth int + // nullInTargets records whether the raw YAML of this file (or any file merged + // into it) had a null value anywhere under the "targets" section. It is computed + // before normalization, which drops nulls on scalar-typed fields, so it reflects + // the config as authored. Surfaced as telemetry by CollectNullTelemetry. + nullInTargets bool + // Contains user defined variables Variables map[string]*variable.Variable `json:"variables,omitempty"` @@ -123,6 +129,10 @@ func LoadFromBytes(path string, raw []byte) (*Root, diag.Diagnostics) { return nil, diag.Errorf("failed to rewrite %s: %v", path, err) } + // Detect nulls under "targets" before normalization drops nulls on + // scalar-typed fields, so the telemetry reflects the config as authored. + r.nullInTargets = hasNullUnderTargets(v) + // Normalize dynamic configuration tree according to configuration type. v, diags := convert.Normalize(r, v) @@ -135,6 +145,35 @@ func LoadFromBytes(path string, raw []byte) (*Root, diag.Diagnostics) { return &r, diags } +// hasNullUnderTargets reports whether v has a null value anywhere under the +// "targets" section. It operates on the raw (pre-normalization) tree, so it also +// catches nulls on scalar-typed fields that normalization would otherwise drop. +// YAML aliases are expanded inline by the loader, so a null inside an anchor that +// is referenced from within a target is counted as well. +func hasNullUnderTargets(v dyn.Value) bool { + targets := v.Get("targets") + if targets.Kind() != dyn.KindMap { + return false + } + + found := false + _ = dyn.WalkReadOnly(targets, func(_ dyn.Path, v dyn.Value) error { + if v.Kind() == dyn.KindNil { + found = true + return dyn.ErrSkip + } + return nil + }) + return found +} + +// NullInTargets reports whether the config had a null value anywhere under the +// "targets" section, as authored (before normalization and before target overrides +// are merged). +func (r *Root) NullInTargets() bool { + return r.nullInTargets +} + func (r *Root) initializeDynamicValue() error { // Many test cases initialize a config as a Go struct literal. // The value will be invalid and we need to populate it from the typed configuration. @@ -155,9 +194,11 @@ func (r *Root) updateWithDynamicValue(nv dyn.Value) error { // Hack: restore state; it may be cleared by [ToTyped] if // the configuration equals nil (happens in tests). depth := r.depth + nullInTargets := r.nullInTargets defer func() { r.depth = depth + r.nullInTargets = nullInTargets }() // Convert normalized configuration tree to typed configuration. @@ -288,6 +329,9 @@ func (r *Root) InitializeVariables(vars []string) error { } func (r *Root) Merge(other *Root) error { + // Carry over the null-in-targets signal from included files. + r.nullInTargets = r.nullInTargets || other.nullInTargets + // Merge dynamic configuration values. return r.Mutate(func(root dyn.Value) (dyn.Value, error) { return merge.Merge(root, other.value) diff --git a/bundle/config/root_test.go b/bundle/config/root_test.go index 42fae49d98c..04265206b7f 100644 --- a/bundle/config/root_test.go +++ b/bundle/config/root_test.go @@ -170,6 +170,94 @@ func TestRootMergeTargetOverridesWithVariables(t *testing.T) { assert.Equal(t, "complex var", root.Variables["complex"].Description) } +func TestNullInTargets(t *testing.T) { + tests := []struct { + name string + yaml string + expected bool + }{ + { + name: "no null under targets", + yaml: ` +targets: + dev: + workspace: + host: https://example.test +`, + expected: false, + }, + { + name: "null map value under targets", + yaml: ` +targets: + dev: + resources: +`, + expected: true, + }, + { + name: "scalar null under targets", + yaml: ` +targets: + dev: + workspace: + host: +`, + expected: true, + }, + { + name: "null outside targets is ignored", + yaml: ` +targets: + dev: + workspace: + host: https://example.test +variables: + foo: +`, + expected: false, + }, + { + name: "no targets section", + yaml: ` +bundle: + name: test +`, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root, diags := LoadFromBytes("databricks.yml", []byte(tt.yaml)) + require.NoError(t, diags.Error()) + assert.Equal(t, tt.expected, root.NullInTargets()) + }) + } +} + +func TestNullInTargetsAccumulatesOnMerge(t *testing.T) { + main, diags := LoadFromBytes("databricks.yml", []byte(` +targets: + dev: + workspace: + host: https://example.test +`)) + require.NoError(t, diags.Error()) + require.False(t, main.NullInTargets()) + + included, diags := LoadFromBytes("included.yml", []byte(` +targets: + dev: + resources: +`)) + require.NoError(t, diags.Error()) + require.True(t, included.NullInTargets()) + + require.NoError(t, main.Merge(included)) + assert.True(t, main.NullInTargets()) +} + func TestIsFullVariableOverrideDef(t *testing.T) { testCases := []struct { value dyn.Value diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index 8f54c141f80..c7a905c7aa6 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -51,6 +51,10 @@ func Initialize(ctx context.Context, b *bundle.Bundle) { // Updates (typed) b.Config.{Sync,Include,Exclude} they set to be relative to SyncRootPath instead of bundle root mutator.SyncInferRoot(), + // Surface the null-in-targets signal computed at load time as telemetry. + // Must run before InitializeCache so it appears first in bool_values. + mutator.CollectNullTelemetry(), + // Updates (typed): b.Cache (initializes cache for API responses) // Initialize cache before any mutator that might need it mutator.InitializeCache(), From aa973b27d61aeb258a7167c56a8b1520d4668deb Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 6 Jul 2026 13:22:12 +0200 Subject: [PATCH 3/6] Add resources.jobs null target and telemetry to null acceptance tests Add a dev-job-field target that nullifies resources.jobs.my_job.description, alongside dev-run-as which nullifies the run_as struct, to cover both struct-level and scalar field nulls under resources. Add bundle deploy + print_telemetry_bool_values to each test so the null-in-targets signal is asserted end-to-end. Add a dev-empty case to scalar-field that uses an explicit empty string rather than YAML null, documenting that both currently leave the base value in place (via different code paths). Co-authored-by: Isaac --- acceptance/bundle/null/map-key/databricks.yml | 21 ++++++++++---- acceptance/bundle/null/map-key/output.txt | 28 ++++++++++++++++++- acceptance/bundle/null/map-key/script | 8 +++++- .../bundle/null/null-from-include/output.txt | 13 +++++++++ .../bundle/null/null-from-include/script | 5 ++++ .../bundle/null/scalar-field/databricks.yml | 17 ++++++++--- .../bundle/null/scalar-field/output.txt | 28 +++++++++++++++++-- acceptance/bundle/null/scalar-field/script | 8 +++++- acceptance/bundle/null/test.toml | 4 +++ 9 files changed, 118 insertions(+), 14 deletions(-) diff --git a/acceptance/bundle/null/map-key/databricks.yml b/acceptance/bundle/null/map-key/databricks.yml index 24892dd3ca2..18ea91e1fbd 100644 --- a/acceptance/bundle/null/map-key/databricks.yml +++ b/acceptance/bundle/null/map-key/databricks.yml @@ -4,14 +4,25 @@ bundle: run_as: user_name: base-user +resources: + jobs: + my_job: + name: my job + description: base description + targets: - dev: + dev-run-as: # Null on a struct-typed field: the target override sets run_as to null, # which currently leaves the base run_as value in place after merge. # When null-merge semantics change this field should become absent. run_as: -resources: - jobs: - my_job: - name: my job + dev-job-field: + # Null on a field inside resources.jobs.*: the target override sets + # resources.jobs.my_job.description to null, which currently leaves the + # base description in place. When null-merge semantics change this field + # should become absent (empty string). + resources: + jobs: + my_job: + description: diff --git a/acceptance/bundle/null/map-key/output.txt b/acceptance/bundle/null/map-key/output.txt index f07a9e50060..8061e060c01 100644 --- a/acceptance/bundle/null/map-key/output.txt +++ b/acceptance/bundle/null/map-key/output.txt @@ -1,7 +1,33 @@ ->>> [CLI] bundle validate -o json -t dev +>>> [CLI] bundle validate -o json -t dev-run-as +Warning: expected a string value, found null + at targets.dev-job-field.resources.jobs.my_job.description + in databricks.yml:28:23 + { "run_as": { "user_name": "base-user" } } + +>>> [CLI] bundle validate -o json -t dev-job-field +Warning: expected a string value, found null + at targets.dev-job-field.resources.jobs.my_job.description + in databricks.yml:28:23 + +{ + "description": "base description" +} + +>>> [CLI] bundle deploy -t dev-run-as +Warning: expected a string value, found null + at targets.dev-job-field.resources.jobs.my_job.description + in databricks.yml:28:23 + +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/dev-run-as/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> print_telemetry_bool_values +null-in-targets true diff --git a/acceptance/bundle/null/map-key/script b/acceptance/bundle/null/map-key/script index 740eb62abae..f36bad633f6 100644 --- a/acceptance/bundle/null/map-key/script +++ b/acceptance/bundle/null/map-key/script @@ -1 +1,7 @@ -trace $CLI bundle validate -o json -t dev | jq '{run_as: .run_as}' +trace $CLI bundle validate -o json -t dev-run-as | jq '{run_as: .run_as}' +trace $CLI bundle validate -o json -t dev-job-field | jq '{description: .resources.jobs.my_job.description}' + +trace $CLI bundle deploy -t dev-run-as +trace print_telemetry_bool_values | grep null-in-targets + +rm -f out.requests.txt diff --git a/acceptance/bundle/null/null-from-include/output.txt b/acceptance/bundle/null/null-from-include/output.txt index 1e3aa4bcdcb..1b6fe167e75 100644 --- a/acceptance/bundle/null/null-from-include/output.txt +++ b/acceptance/bundle/null/null-from-include/output.txt @@ -9,3 +9,16 @@ Warning: expected a string value, found null "root_path": "/Workspace/Users/base-root" } } + +>>> [CLI] bundle deploy -t dev +Warning: expected a string value, found null + at targets.dev.workspace.root_path + in override.yml:6:17 + +Uploading bundle files to /Workspace/Users/base-root/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> print_telemetry_bool_values +null-in-targets true diff --git a/acceptance/bundle/null/null-from-include/script b/acceptance/bundle/null/null-from-include/script index 1cceac6bf5b..5747d8b32c7 100644 --- a/acceptance/bundle/null/null-from-include/script +++ b/acceptance/bundle/null/null-from-include/script @@ -1 +1,6 @@ trace $CLI bundle validate -o json -t dev | jq '{workspace: {root_path: .workspace.root_path}}' + +trace $CLI bundle deploy -t dev +trace print_telemetry_bool_values | grep null-in-targets + +rm -f out.requests.txt diff --git a/acceptance/bundle/null/scalar-field/databricks.yml b/acceptance/bundle/null/scalar-field/databricks.yml index 7ca100645a0..88a484124b9 100644 --- a/acceptance/bundle/null/scalar-field/databricks.yml +++ b/acceptance/bundle/null/scalar-field/databricks.yml @@ -5,13 +5,22 @@ workspace: root_path: /Workspace/Users/base-root targets: - dev: + dev-null: workspace: - # Null on a scalar field: normalization drops it, so the base root_path - # survives in the merged config. When null-merge semantics change this - # field should become absent (or empty) in the output. + # YAML null: normalization drops the null, so the base root_path survives + # in the merged config. When null-merge semantics change, this field should + # become absent (reverting to the default ~/.bundle/... path). root_path: + dev-empty: + workspace: + # Explicit empty string: not a null, so normalization keeps it. + # The empty string overrides the base, then DefineDefaultWorkspaceRoot + # re-fills it with the default ~/.bundle/... path (since the field is ""). + # Empty string and YAML null currently produce the same final value via + # different paths; this is captured here to document both behaviors. + root_path: "" + resources: jobs: my_job: diff --git a/acceptance/bundle/null/scalar-field/output.txt b/acceptance/bundle/null/scalar-field/output.txt index a24bfa2f603..ef279c8f8db 100644 --- a/acceptance/bundle/null/scalar-field/output.txt +++ b/acceptance/bundle/null/scalar-field/output.txt @@ -1,7 +1,7 @@ ->>> [CLI] bundle validate -o json -t dev +>>> [CLI] bundle validate -o json -t dev-null Warning: expected a string value, found null - at targets.dev.workspace.root_path + at targets.dev-null.workspace.root_path in databricks.yml:13:17 { @@ -9,3 +9,27 @@ Warning: expected a string value, found null "root_path": "/Workspace/Users/base-root" } } + +>>> [CLI] bundle validate -o json -t dev-empty +Warning: expected a string value, found null + at targets.dev-null.workspace.root_path + in databricks.yml:13:17 + +{ + "workspace": { + "root_path": "/Workspace/Users/base-root" + } +} + +>>> [CLI] bundle deploy -t dev-null +Warning: expected a string value, found null + at targets.dev-null.workspace.root_path + in databricks.yml:13:17 + +Uploading bundle files to /Workspace/Users/base-root/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> print_telemetry_bool_values +null-in-targets true diff --git a/acceptance/bundle/null/scalar-field/script b/acceptance/bundle/null/scalar-field/script index 1cceac6bf5b..3bf52d36c68 100644 --- a/acceptance/bundle/null/scalar-field/script +++ b/acceptance/bundle/null/scalar-field/script @@ -1 +1,7 @@ -trace $CLI bundle validate -o json -t dev | jq '{workspace: {root_path: .workspace.root_path}}' +trace $CLI bundle validate -o json -t dev-null | jq '{workspace: {root_path: .workspace.root_path}}' +trace $CLI bundle validate -o json -t dev-empty | jq '{workspace: {root_path: .workspace.root_path}}' + +trace $CLI bundle deploy -t dev-null +trace print_telemetry_bool_values | grep null-in-targets + +rm -f out.requests.txt diff --git a/acceptance/bundle/null/test.toml b/acceptance/bundle/null/test.toml index c63fe3fe108..9c21f98f793 100644 --- a/acceptance/bundle/null/test.toml +++ b/acceptance/bundle/null/test.toml @@ -1 +1,5 @@ EnvMatrix.DATABRICKS_BUNDLE_ENGINE = [] +RecordRequests = true + +[Env] +DATABRICKS_CACHE_ENABLED = "false" From e15b4dd44b8806e5226543619db99dc6feef0766 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 6 Jul 2026 13:49:44 +0200 Subject: [PATCH 4/6] Break null-in-targets telemetry into per-category signals Replace the single null-in-targets bool with five granular signals: - null-in-targets.scalar: null on a scalar field (string/int/bool); normalization drops it and emits a warning - null-in-targets.complex: null on a struct-typed field at the top level of a target override (e.g. run_as:) - null-in-targets.map-key: null as a value in a user-keyed map field (e.g. variables.foo:, or a field inside a resource entry) - null-in-targets.array-index: null at a sequence index - null-in-targets.resource: null on an entire resource entry (targets..resources..) The top-level null-in-targets flag is retained as the OR of all categories. The classification is based on path structure and normalization survival: scalars are dropped by normalization, all others survive. A two-pass walk (raw tree + normalized tree) distinguishes scalar from complex nulls. Add MapKey and Resource acceptance test cases to acceptance/bundle/null/map-key. Co-authored-by: Isaac --- acceptance/bundle/null/map-key/databricks.yml | 29 +++- acceptance/bundle/null/map-key/output.txt | 55 ++++++- acceptance/bundle/null/map-key/script | 2 + .../bundle/null/null-from-include/output.txt | 5 + .../bundle/null/scalar-field/output.txt | 5 + .../deploy-app-lifecycle-started/output.txt | 20 +++ .../telemetry/deploy-compute-type/output.txt | 10 ++ .../telemetry/deploy-experimental/output.txt | 5 + .../deploy-name-prefix/custom/output.txt | 5 + .../mode-development/output.txt | 5 + .../telemetry/deploy-whl-artifacts/output.txt | 10 ++ .../bundle/telemetry/deploy/out.telemetry.txt | 20 +++ .../config/mutator/collect_null_telemetry.go | 10 +- bundle/config/root.go | 155 ++++++++++++++---- bundle/config/root_test.go | 61 +++++-- 15 files changed, 343 insertions(+), 54 deletions(-) diff --git a/acceptance/bundle/null/map-key/databricks.yml b/acceptance/bundle/null/map-key/databricks.yml index 18ea91e1fbd..a937b96f7c0 100644 --- a/acceptance/bundle/null/map-key/databricks.yml +++ b/acceptance/bundle/null/map-key/databricks.yml @@ -4,6 +4,11 @@ bundle: run_as: user_name: base-user +variables: + foo: + default: base-value + description: a variable + resources: jobs: my_job: @@ -12,17 +17,27 @@ resources: targets: dev-run-as: - # Null on a struct-typed field: the target override sets run_as to null, - # which currently leaves the base run_as value in place after merge. - # When null-merge semantics change this field should become absent. + # Complex: null on a struct-typed field at the top level of a target override. + # The base run_as value survives merge. When null-merge semantics change this + # field should become absent. run_as: dev-job-field: - # Null on a field inside resources.jobs.*: the target override sets - # resources.jobs.my_job.description to null, which currently leaves the - # base description in place. When null-merge semantics change this field - # should become absent (empty string). + # Scalar: null on a string field inside a resource. Normalization drops it + # and emits a warning; the base description survives merge. resources: jobs: my_job: description: + + dev-variable: + # MapKey: null on a value in the variables map (map[string]*TargetVariable). + # The null overrides the variable entry; currently survives as null. + variables: + foo: + + dev-resource: + # Resource: null on an entire resource entry. The job is completely nulled. + resources: + jobs: + my_job: diff --git a/acceptance/bundle/null/map-key/output.txt b/acceptance/bundle/null/map-key/output.txt index 8061e060c01..a801929720e 100644 --- a/acceptance/bundle/null/map-key/output.txt +++ b/acceptance/bundle/null/map-key/output.txt @@ -2,7 +2,7 @@ >>> [CLI] bundle validate -o json -t dev-run-as Warning: expected a string value, found null at targets.dev-job-field.resources.jobs.my_job.description - in databricks.yml:28:23 + in databricks.yml:31:23 { "run_as": { @@ -13,16 +13,60 @@ Warning: expected a string value, found null >>> [CLI] bundle validate -o json -t dev-job-field Warning: expected a string value, found null at targets.dev-job-field.resources.jobs.my_job.description - in databricks.yml:28:23 + in databricks.yml:31:23 { "description": "base description" } +>>> [CLI] bundle validate -o json -t dev-variable +Warning: expected a string value, found null + at targets.dev-job-field.resources.jobs.my_job.description + in databricks.yml:31:23 + +{ + "variables": { + "foo": { + "default": "base-value", + "description": "a variable", + "value": "base-value" + } + } +} + +>>> [CLI] bundle validate -o json -t dev-resource +Warning: expected a string value, found null + at targets.dev-job-field.resources.jobs.my_job.description + in databricks.yml:31:23 + +{ + "resources": { + "jobs": { + "my_job": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/dev-resource/state/metadata.json" + }, + "description": "base description", + "edit_mode": "UI_LOCKED", + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "my job", + "queue": { + "enabled": true + }, + "run_as": { + "user_name": "base-user" + } + } + } + } +} + >>> [CLI] bundle deploy -t dev-run-as Warning: expected a string value, found null at targets.dev-job-field.resources.jobs.my_job.description - in databricks.yml:28:23 + in databricks.yml:31:23 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/dev-run-as/files... Deploying resources... @@ -31,3 +75,8 @@ Deployment complete! >>> print_telemetry_bool_values null-in-targets true +null-in-targets.array-index false +null-in-targets.complex true +null-in-targets.map-key true +null-in-targets.resource true +null-in-targets.scalar true diff --git a/acceptance/bundle/null/map-key/script b/acceptance/bundle/null/map-key/script index f36bad633f6..65527669153 100644 --- a/acceptance/bundle/null/map-key/script +++ b/acceptance/bundle/null/map-key/script @@ -1,5 +1,7 @@ trace $CLI bundle validate -o json -t dev-run-as | jq '{run_as: .run_as}' trace $CLI bundle validate -o json -t dev-job-field | jq '{description: .resources.jobs.my_job.description}' +trace $CLI bundle validate -o json -t dev-variable | jq '{variables: {foo: .variables.foo}}' +trace $CLI bundle validate -o json -t dev-resource | jq '{resources: {jobs: .resources.jobs}}' trace $CLI bundle deploy -t dev-run-as trace print_telemetry_bool_values | grep null-in-targets diff --git a/acceptance/bundle/null/null-from-include/output.txt b/acceptance/bundle/null/null-from-include/output.txt index 1b6fe167e75..26941559d5f 100644 --- a/acceptance/bundle/null/null-from-include/output.txt +++ b/acceptance/bundle/null/null-from-include/output.txt @@ -22,3 +22,8 @@ Deployment complete! >>> print_telemetry_bool_values null-in-targets true +null-in-targets.array-index false +null-in-targets.complex false +null-in-targets.map-key false +null-in-targets.resource false +null-in-targets.scalar true diff --git a/acceptance/bundle/null/scalar-field/output.txt b/acceptance/bundle/null/scalar-field/output.txt index ef279c8f8db..cb3820403f2 100644 --- a/acceptance/bundle/null/scalar-field/output.txt +++ b/acceptance/bundle/null/scalar-field/output.txt @@ -33,3 +33,8 @@ Deployment complete! >>> print_telemetry_bool_values null-in-targets true +null-in-targets.array-index false +null-in-targets.complex false +null-in-targets.map-key false +null-in-targets.resource false +null-in-targets.scalar true diff --git a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt index 3fb5f331688..a63de466d90 100644 --- a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt +++ b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt @@ -13,6 +13,26 @@ Deployment complete! "key": "null-in-targets", "value": false }, + { + "key": "null-in-targets.scalar", + "value": false + }, + { + "key": "null-in-targets.complex", + "value": false + }, + { + "key": "null-in-targets.map-key", + "value": false + }, + { + "key": "null-in-targets.array-index", + "value": false + }, + { + "key": "null-in-targets.resource", + "value": false + }, { "key": "local.cache.attempt", "value": true diff --git a/acceptance/bundle/telemetry/deploy-compute-type/output.txt b/acceptance/bundle/telemetry/deploy-compute-type/output.txt index 73928c8a088..03346be8db7 100644 --- a/acceptance/bundle/telemetry/deploy-compute-type/output.txt +++ b/acceptance/bundle/telemetry/deploy-compute-type/output.txt @@ -36,6 +36,16 @@ local.cache.hit true local.cache.miss true null-in-targets false null-in-targets false +null-in-targets.array-index false +null-in-targets.array-index false +null-in-targets.complex false +null-in-targets.complex false +null-in-targets.map-key false +null-in-targets.map-key false +null-in-targets.resource false +null-in-targets.resource false +null-in-targets.scalar false +null-in-targets.scalar false permissions_section_set false permissions_section_set false presets_name_prefix_is_set false diff --git a/acceptance/bundle/telemetry/deploy-experimental/output.txt b/acceptance/bundle/telemetry/deploy-experimental/output.txt index 5196863ec15..fcc2e37427a 100644 --- a/acceptance/bundle/telemetry/deploy-experimental/output.txt +++ b/acceptance/bundle/telemetry/deploy-experimental/output.txt @@ -22,6 +22,11 @@ has_serverless_compute false local.cache.attempt true local.cache.miss true null-in-targets false +null-in-targets.array-index false +null-in-targets.complex false +null-in-targets.map-key false +null-in-targets.resource false +null-in-targets.scalar false permissions_section_set false presets_name_prefix_is_set false python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt index 08dd225d829..1e999666ed6 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt @@ -18,6 +18,11 @@ has_serverless_compute false local.cache.attempt true local.cache.miss true null-in-targets false +null-in-targets.array-index false +null-in-targets.complex false +null-in-targets.map-key false +null-in-targets.resource false +null-in-targets.scalar false permissions_section_set false presets_name_prefix_is_set true python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt index bc5efa44507..41c74127c06 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt @@ -18,6 +18,11 @@ has_serverless_compute false local.cache.attempt true local.cache.miss true null-in-targets false +null-in-targets.array-index false +null-in-targets.complex false +null-in-targets.map-key false +null-in-targets.resource false +null-in-targets.scalar false permissions_section_set false presets_name_prefix_is_set true python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt index 85262c767fa..3ab8d30310e 100644 --- a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt +++ b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt @@ -41,6 +41,16 @@ local.cache.hit true local.cache.miss true null-in-targets false null-in-targets false +null-in-targets.array-index false +null-in-targets.array-index false +null-in-targets.complex false +null-in-targets.complex false +null-in-targets.map-key false +null-in-targets.map-key false +null-in-targets.resource false +null-in-targets.resource false +null-in-targets.scalar false +null-in-targets.scalar false permissions_section_set false permissions_section_set false python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy/out.telemetry.txt b/acceptance/bundle/telemetry/deploy/out.telemetry.txt index aab20a4bc45..32326cf7dd0 100644 --- a/acceptance/bundle/telemetry/deploy/out.telemetry.txt +++ b/acceptance/bundle/telemetry/deploy/out.telemetry.txt @@ -46,6 +46,26 @@ "key": "null-in-targets", "value": false }, + { + "key": "null-in-targets.scalar", + "value": false + }, + { + "key": "null-in-targets.complex", + "value": false + }, + { + "key": "null-in-targets.map-key", + "value": false + }, + { + "key": "null-in-targets.array-index", + "value": false + }, + { + "key": "null-in-targets.resource", + "value": false + }, { "key": "local.cache.attempt", "value": true diff --git a/bundle/config/mutator/collect_null_telemetry.go b/bundle/config/mutator/collect_null_telemetry.go index 2f6feb705fd..c6ef72a71ca 100644 --- a/bundle/config/mutator/collect_null_telemetry.go +++ b/bundle/config/mutator/collect_null_telemetry.go @@ -7,7 +7,7 @@ import ( "github.com/databricks/cli/libs/diag" ) -// collectNullTelemetry records whether the config has any null value anywhere +// collectNullTelemetry records per-category flags for null values found anywhere // under the "targets" section, as authored. The signal is computed at load time // (before normalization drops nulls on scalar-typed fields) and accumulated across // all included files; this mutator just surfaces it as telemetry. @@ -22,6 +22,12 @@ func (*collectNullTelemetry) Name() string { } func (*collectNullTelemetry) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { - b.Metrics.SetBoolValue("null-in-targets", b.Config.NullInTargets()) + info := b.Config.NullInTargets() + b.Metrics.SetBoolValue("null-in-targets", info.Any()) + b.Metrics.SetBoolValue("null-in-targets.scalar", info.Scalar) + b.Metrics.SetBoolValue("null-in-targets.complex", info.Complex) + b.Metrics.SetBoolValue("null-in-targets.map-key", info.MapKey) + b.Metrics.SetBoolValue("null-in-targets.array-index", info.ArrayIndex) + b.Metrics.SetBoolValue("null-in-targets.resource", info.Resource) return nil } diff --git a/bundle/config/root.go b/bundle/config/root.go index 641c2f557e0..478e99672ad 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -30,11 +30,11 @@ type Root struct { //nolint:recvcheck // value receivers for read-only accessors value dyn.Value depth int - // nullInTargets records whether the raw YAML of this file (or any file merged - // into it) had a null value anywhere under the "targets" section. It is computed - // before normalization, which drops nulls on scalar-typed fields, so it reflects - // the config as authored. Surfaced as telemetry by CollectNullTelemetry. - nullInTargets bool + // nullInTargets records the categories of null values found anywhere under + // the "targets" section of this file (or any file merged into it). Computed + // before normalization so scalar-typed nulls (which normalization drops) are + // also detected. Surfaced as telemetry by CollectNullTelemetry. + nullInTargets NullInTargetsInfo // Contains user defined variables Variables map[string]*variable.Variable `json:"variables,omitempty"` @@ -129,13 +129,16 @@ func LoadFromBytes(path string, raw []byte) (*Root, diag.Diagnostics) { return nil, diag.Errorf("failed to rewrite %s: %v", path, err) } - // Detect nulls under "targets" before normalization drops nulls on - // scalar-typed fields, so the telemetry reflects the config as authored. - r.nullInTargets = hasNullUnderTargets(v) + // Collect null categories before normalization (raw tree) and after (norm + // tree) so we can distinguish scalar nulls (dropped by normalization) from + // complex nulls (struct/map/seq typed fields that survive). + rawTargets := v.Get("targets") // Normalize dynamic configuration tree according to configuration type. v, diags := convert.Normalize(r, v) + r.nullInTargets = collectNullsInTargets(rawTargets, v.Get("targets")) + // Convert normalized configuration tree to typed configuration. err = r.updateWithDynamicValue(v) if err != nil { @@ -145,32 +148,128 @@ func LoadFromBytes(path string, raw []byte) (*Root, diag.Diagnostics) { return &r, diags } -// hasNullUnderTargets reports whether v has a null value anywhere under the -// "targets" section. It operates on the raw (pre-normalization) tree, so it also -// catches nulls on scalar-typed fields that normalization would otherwise drop. -// YAML aliases are expanded inline by the loader, so a null inside an anchor that -// is referenced from within a target is counted as well. -func hasNullUnderTargets(v dyn.Value) bool { - targets := v.Get("targets") - if targets.Kind() != dyn.KindMap { - return false +// NullInTargetsInfo categorises null values found under the "targets" section +// of a bundle config as authored (before normalization and target override merge). +// +// Each field is a "has at least one" flag. The categories are: +// - Scalar: null on a scalar-typed field (string/int/bool); normalization drops it +// and emits a warning — it never reaches the merge. +// - Complex: null on a struct-typed field (e.g. run_as:) at the top level of a +// target override (depth ≤ 2 inside the target); survives normalization. +// - MapKey: null as a value in a map[string]X typed field (e.g. variables.foo: +// or resources.jobs.foo.description:) at depth > 2; survives normalization. +// - ArrayIndex: null at a sequence index (e.g. a null item in a list). +// - Resource: null at exactly resources.. inside a target — the +// entire resource entry is nulled out. +type NullInTargetsInfo struct { + Scalar bool + Complex bool + MapKey bool + ArrayIndex bool + Resource bool +} + +// Any reports whether any null category was detected. +func (n NullInTargetsInfo) Any() bool { + return n.Scalar || n.Complex || n.MapKey || n.ArrayIndex || n.Resource +} + +// Merge combines two NullInTargetsInfo values with OR, used when merging +// included files into the root config. +func (n NullInTargetsInfo) Merge(other NullInTargetsInfo) NullInTargetsInfo { + return NullInTargetsInfo{ + Scalar: n.Scalar || other.Scalar, + Complex: n.Complex || other.Complex, + MapKey: n.MapKey || other.MapKey, + ArrayIndex: n.ArrayIndex || other.ArrayIndex, + Resource: n.Resource || other.Resource, } +} + +// collectNullsInTargets walks the raw (pre-normalization) and normalized targets +// sections and classifies each null value into one of the NullInTargetsInfo +// categories. +// +// rawTargets is the targets value from the raw YAML tree (before normalization). +// normTargets is the targets value after normalization (nulls on scalar-typed +// fields have been removed; nulls on complex-typed fields survive). +func collectNullsInTargets(rawTargets, normTargets dyn.Value) NullInTargetsInfo { + if rawTargets.Kind() != dyn.KindMap { + return NullInTargetsInfo{} + } + + // Collect paths of nulls that survived normalization (complex-typed fields). + normNullPaths := make(map[string]bool) + if normTargets.Kind() == dyn.KindMap { + _ = dyn.WalkReadOnly(normTargets, func(p dyn.Path, v dyn.Value) error { + if v.Kind() == dyn.KindNil { + normNullPaths[p.String()] = true + return dyn.ErrSkip + } + return nil + }) + } + + var info NullInTargetsInfo - found := false - _ = dyn.WalkReadOnly(targets, func(_ dyn.Path, v dyn.Value) error { - if v.Kind() == dyn.KindNil { - found = true + // Walk the raw tree and classify each null by path context. + // p is relative to the targets value, so: + // p[0] = target name (e.g. "dev") + // p[1] = first field inside target (e.g. "run_as", "resources", "variables") + // p[2] = second field (e.g. resource type "jobs", or variable name "foo") + // p[3] = third field (e.g. resource name "my_job") + _ = dyn.WalkReadOnly(rawTargets, func(p dyn.Path, v dyn.Value) error { + if v.Kind() != dyn.KindNil { + return nil + } + + // Array index: last path component is a sequence index (Key() is empty for indices). + if len(p) > 0 && p[len(p)-1].Key() == "" { + info.ArrayIndex = true return dyn.ErrSkip } - return nil + + // Paths relative to the target name component (p[0]). + // len(p) == 1 means the entire target is null; handled as Complex below. + field1 := "" + if len(p) >= 2 { + field1 = p[1].Key() + } + + switch { + // Resource null: exactly .resources... + case field1 == "resources" && len(p) == 4: + info.Resource = true + + // Map-key null: a value in a user-keyed map field (variables or inside + // a resource entry). variables. is depth 2; inside-resource fields + // are depth > 3. + case field1 == "variables" && len(p) == 3, + field1 == "resources" && len(p) > 4: + if normNullPaths[p.String()] { + info.MapKey = true + } else { + info.Scalar = true + } + + // Everything else: distinguish scalar vs complex by normalization survival. + default: + if normNullPaths[p.String()] { + info.Complex = true + } else { + info.Scalar = true + } + } + + return dyn.ErrSkip }) - return found + + return info } -// NullInTargets reports whether the config had a null value anywhere under the -// "targets" section, as authored (before normalization and before target overrides -// are merged). -func (r *Root) NullInTargets() bool { +// NullInTargets returns categorised information about null values found under +// the "targets" section of this config as authored. +func (r *Root) NullInTargets() NullInTargetsInfo { return r.nullInTargets } @@ -330,7 +429,7 @@ func (r *Root) InitializeVariables(vars []string) error { func (r *Root) Merge(other *Root) error { // Carry over the null-in-targets signal from included files. - r.nullInTargets = r.nullInTargets || other.nullInTargets + r.nullInTargets = r.nullInTargets.Merge(other.nullInTargets) // Merge dynamic configuration values. return r.Mutate(func(root dyn.Value) (dyn.Value, error) { diff --git a/bundle/config/root_test.go b/bundle/config/root_test.go index 04265206b7f..d50873045cf 100644 --- a/bundle/config/root_test.go +++ b/bundle/config/root_test.go @@ -174,7 +174,7 @@ func TestNullInTargets(t *testing.T) { tests := []struct { name string yaml string - expected bool + expected NullInTargetsInfo }{ { name: "no null under targets", @@ -184,26 +184,59 @@ targets: workspace: host: https://example.test `, - expected: false, + expected: NullInTargetsInfo{}, + }, + { + name: "scalar null: workspace.host", + yaml: ` +targets: + dev: + workspace: + host: +`, + expected: NullInTargetsInfo{Scalar: true}, + }, + { + name: "complex null: run_as at top level of target", + yaml: ` +targets: + dev: + run_as: +`, + expected: NullInTargetsInfo{Complex: true}, }, { - name: "null map value under targets", + name: "resource null: resources.jobs.foo", yaml: ` targets: dev: resources: + jobs: + foo: `, - expected: true, + expected: NullInTargetsInfo{Resource: true}, }, { - name: "scalar null under targets", + name: "scalar null inside resource: resources.jobs.foo.description", yaml: ` targets: dev: - workspace: - host: + resources: + jobs: + foo: + description: `, - expected: true, + expected: NullInTargetsInfo{Scalar: true}, + }, + { + name: "map-key null: variables.foo", + yaml: ` +targets: + dev: + variables: + foo: +`, + expected: NullInTargetsInfo{MapKey: true}, }, { name: "null outside targets is ignored", @@ -215,7 +248,7 @@ targets: variables: foo: `, - expected: false, + expected: NullInTargetsInfo{}, }, { name: "no targets section", @@ -223,7 +256,7 @@ variables: bundle: name: test `, - expected: false, + expected: NullInTargetsInfo{}, }, } @@ -244,18 +277,18 @@ targets: host: https://example.test `)) require.NoError(t, diags.Error()) - require.False(t, main.NullInTargets()) + require.Equal(t, NullInTargetsInfo{}, main.NullInTargets()) included, diags := LoadFromBytes("included.yml", []byte(` targets: dev: - resources: + run_as: `)) require.NoError(t, diags.Error()) - require.True(t, included.NullInTargets()) + require.Equal(t, NullInTargetsInfo{Complex: true}, included.NullInTargets()) require.NoError(t, main.Merge(included)) - assert.True(t, main.NullInTargets()) + assert.Equal(t, NullInTargetsInfo{Complex: true}, main.NullInTargets()) } func TestIsFullVariableOverrideDef(t *testing.T) { From 0379a7177d111fdae6bd7881356fb1c4cd482eed Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 6 Jul 2026 18:33:04 +0200 Subject: [PATCH 5/6] Use dashes in null-in-targets telemetry key names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace dots with dashes in sub-key names to avoid ambiguity with the dyn path separator. Also shorten map-key → key and array-index → index. Co-authored-by: Isaac --- acceptance/bundle/null/map-key/output.txt | 10 +++++----- .../bundle/null/null-from-include/output.txt | 10 +++++----- .../bundle/null/scalar-field/output.txt | 10 +++++----- .../deploy-app-lifecycle-started/output.txt | 10 +++++----- .../telemetry/deploy-compute-type/output.txt | 20 +++++++++---------- .../telemetry/deploy-experimental/output.txt | 10 +++++----- .../deploy-name-prefix/custom/output.txt | 10 +++++----- .../mode-development/output.txt | 10 +++++----- .../telemetry/deploy-whl-artifacts/output.txt | 20 +++++++++---------- .../bundle/telemetry/deploy/out.telemetry.txt | 10 +++++----- .../config/mutator/collect_null_telemetry.go | 10 +++++----- 11 files changed, 65 insertions(+), 65 deletions(-) diff --git a/acceptance/bundle/null/map-key/output.txt b/acceptance/bundle/null/map-key/output.txt index a801929720e..85d09de92ab 100644 --- a/acceptance/bundle/null/map-key/output.txt +++ b/acceptance/bundle/null/map-key/output.txt @@ -75,8 +75,8 @@ Deployment complete! >>> print_telemetry_bool_values null-in-targets true -null-in-targets.array-index false -null-in-targets.complex true -null-in-targets.map-key true -null-in-targets.resource true -null-in-targets.scalar true +null-in-targets-complex true +null-in-targets-index false +null-in-targets-key true +null-in-targets-resource true +null-in-targets-scalar true diff --git a/acceptance/bundle/null/null-from-include/output.txt b/acceptance/bundle/null/null-from-include/output.txt index 26941559d5f..24d7a9cea7a 100644 --- a/acceptance/bundle/null/null-from-include/output.txt +++ b/acceptance/bundle/null/null-from-include/output.txt @@ -22,8 +22,8 @@ Deployment complete! >>> print_telemetry_bool_values null-in-targets true -null-in-targets.array-index false -null-in-targets.complex false -null-in-targets.map-key false -null-in-targets.resource false -null-in-targets.scalar true +null-in-targets-complex false +null-in-targets-index false +null-in-targets-key false +null-in-targets-resource false +null-in-targets-scalar true diff --git a/acceptance/bundle/null/scalar-field/output.txt b/acceptance/bundle/null/scalar-field/output.txt index cb3820403f2..c0d6235ffc5 100644 --- a/acceptance/bundle/null/scalar-field/output.txt +++ b/acceptance/bundle/null/scalar-field/output.txt @@ -33,8 +33,8 @@ Deployment complete! >>> print_telemetry_bool_values null-in-targets true -null-in-targets.array-index false -null-in-targets.complex false -null-in-targets.map-key false -null-in-targets.resource false -null-in-targets.scalar true +null-in-targets-complex false +null-in-targets-index false +null-in-targets-key false +null-in-targets-resource false +null-in-targets-scalar true diff --git a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt index a63de466d90..e7575a70237 100644 --- a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt +++ b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt @@ -14,23 +14,23 @@ Deployment complete! "value": false }, { - "key": "null-in-targets.scalar", + "key": "null-in-targets-scalar", "value": false }, { - "key": "null-in-targets.complex", + "key": "null-in-targets-complex", "value": false }, { - "key": "null-in-targets.map-key", + "key": "null-in-targets-key", "value": false }, { - "key": "null-in-targets.array-index", + "key": "null-in-targets-index", "value": false }, { - "key": "null-in-targets.resource", + "key": "null-in-targets-resource", "value": false }, { diff --git a/acceptance/bundle/telemetry/deploy-compute-type/output.txt b/acceptance/bundle/telemetry/deploy-compute-type/output.txt index 03346be8db7..59697ffdf3e 100644 --- a/acceptance/bundle/telemetry/deploy-compute-type/output.txt +++ b/acceptance/bundle/telemetry/deploy-compute-type/output.txt @@ -36,16 +36,16 @@ local.cache.hit true local.cache.miss true null-in-targets false null-in-targets false -null-in-targets.array-index false -null-in-targets.array-index false -null-in-targets.complex false -null-in-targets.complex false -null-in-targets.map-key false -null-in-targets.map-key false -null-in-targets.resource false -null-in-targets.resource false -null-in-targets.scalar false -null-in-targets.scalar false +null-in-targets-complex false +null-in-targets-complex false +null-in-targets-index false +null-in-targets-index false +null-in-targets-key false +null-in-targets-key false +null-in-targets-resource false +null-in-targets-resource false +null-in-targets-scalar false +null-in-targets-scalar false permissions_section_set false permissions_section_set false presets_name_prefix_is_set false diff --git a/acceptance/bundle/telemetry/deploy-experimental/output.txt b/acceptance/bundle/telemetry/deploy-experimental/output.txt index fcc2e37427a..977bac7fba9 100644 --- a/acceptance/bundle/telemetry/deploy-experimental/output.txt +++ b/acceptance/bundle/telemetry/deploy-experimental/output.txt @@ -22,11 +22,11 @@ has_serverless_compute false local.cache.attempt true local.cache.miss true null-in-targets false -null-in-targets.array-index false -null-in-targets.complex false -null-in-targets.map-key false -null-in-targets.resource false -null-in-targets.scalar false +null-in-targets-complex false +null-in-targets-index false +null-in-targets-key false +null-in-targets-resource false +null-in-targets-scalar false permissions_section_set false presets_name_prefix_is_set false python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt index 1e999666ed6..3f9cba37576 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt @@ -18,11 +18,11 @@ has_serverless_compute false local.cache.attempt true local.cache.miss true null-in-targets false -null-in-targets.array-index false -null-in-targets.complex false -null-in-targets.map-key false -null-in-targets.resource false -null-in-targets.scalar false +null-in-targets-complex false +null-in-targets-index false +null-in-targets-key false +null-in-targets-resource false +null-in-targets-scalar false permissions_section_set false presets_name_prefix_is_set true python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt index 41c74127c06..249e5b1e82b 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt @@ -18,11 +18,11 @@ has_serverless_compute false local.cache.attempt true local.cache.miss true null-in-targets false -null-in-targets.array-index false -null-in-targets.complex false -null-in-targets.map-key false -null-in-targets.resource false -null-in-targets.scalar false +null-in-targets-complex false +null-in-targets-index false +null-in-targets-key false +null-in-targets-resource false +null-in-targets-scalar false permissions_section_set false presets_name_prefix_is_set true python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt index 3ab8d30310e..d03ad421f6b 100644 --- a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt +++ b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt @@ -41,16 +41,16 @@ local.cache.hit true local.cache.miss true null-in-targets false null-in-targets false -null-in-targets.array-index false -null-in-targets.array-index false -null-in-targets.complex false -null-in-targets.complex false -null-in-targets.map-key false -null-in-targets.map-key false -null-in-targets.resource false -null-in-targets.resource false -null-in-targets.scalar false -null-in-targets.scalar false +null-in-targets-complex false +null-in-targets-complex false +null-in-targets-index false +null-in-targets-index false +null-in-targets-key false +null-in-targets-key false +null-in-targets-resource false +null-in-targets-resource false +null-in-targets-scalar false +null-in-targets-scalar false permissions_section_set false permissions_section_set false python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy/out.telemetry.txt b/acceptance/bundle/telemetry/deploy/out.telemetry.txt index 32326cf7dd0..2b92eb92f16 100644 --- a/acceptance/bundle/telemetry/deploy/out.telemetry.txt +++ b/acceptance/bundle/telemetry/deploy/out.telemetry.txt @@ -47,23 +47,23 @@ "value": false }, { - "key": "null-in-targets.scalar", + "key": "null-in-targets-scalar", "value": false }, { - "key": "null-in-targets.complex", + "key": "null-in-targets-complex", "value": false }, { - "key": "null-in-targets.map-key", + "key": "null-in-targets-key", "value": false }, { - "key": "null-in-targets.array-index", + "key": "null-in-targets-index", "value": false }, { - "key": "null-in-targets.resource", + "key": "null-in-targets-resource", "value": false }, { diff --git a/bundle/config/mutator/collect_null_telemetry.go b/bundle/config/mutator/collect_null_telemetry.go index c6ef72a71ca..f72eb076040 100644 --- a/bundle/config/mutator/collect_null_telemetry.go +++ b/bundle/config/mutator/collect_null_telemetry.go @@ -24,10 +24,10 @@ func (*collectNullTelemetry) Name() string { func (*collectNullTelemetry) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { info := b.Config.NullInTargets() b.Metrics.SetBoolValue("null-in-targets", info.Any()) - b.Metrics.SetBoolValue("null-in-targets.scalar", info.Scalar) - b.Metrics.SetBoolValue("null-in-targets.complex", info.Complex) - b.Metrics.SetBoolValue("null-in-targets.map-key", info.MapKey) - b.Metrics.SetBoolValue("null-in-targets.array-index", info.ArrayIndex) - b.Metrics.SetBoolValue("null-in-targets.resource", info.Resource) + b.Metrics.SetBoolValue("null-in-targets-scalar", info.Scalar) + b.Metrics.SetBoolValue("null-in-targets-complex", info.Complex) + b.Metrics.SetBoolValue("null-in-targets-key", info.MapKey) + b.Metrics.SetBoolValue("null-in-targets-index", info.ArrayIndex) + b.Metrics.SetBoolValue("null-in-targets-resource", info.Resource) return nil } From 42842b4566a178eb5b4099fcb931c408be2429aa Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Mon, 6 Jul 2026 18:39:24 +0200 Subject: [PATCH 6/6] Only emit null-in-targets sub-keys when true Co-authored-by: Isaac --- acceptance/bundle/null/map-key/output.txt | 1 - .../bundle/null/null-from-include/output.txt | 4 ---- .../bundle/null/scalar-field/output.txt | 4 ---- .../deploy-app-lifecycle-started/output.txt | 20 ------------------- .../telemetry/deploy-compute-type/output.txt | 10 ---------- .../telemetry/deploy-experimental/output.txt | 5 ----- .../deploy-name-prefix/custom/output.txt | 5 ----- .../mode-development/output.txt | 5 ----- .../telemetry/deploy-whl-artifacts/output.txt | 10 ---------- .../bundle/telemetry/deploy/out.telemetry.txt | 20 ------------------- .../config/mutator/collect_null_telemetry.go | 20 ++++++++++++++----- 11 files changed, 15 insertions(+), 89 deletions(-) diff --git a/acceptance/bundle/null/map-key/output.txt b/acceptance/bundle/null/map-key/output.txt index 85d09de92ab..3997bf6b69c 100644 --- a/acceptance/bundle/null/map-key/output.txt +++ b/acceptance/bundle/null/map-key/output.txt @@ -76,7 +76,6 @@ Deployment complete! >>> print_telemetry_bool_values null-in-targets true null-in-targets-complex true -null-in-targets-index false null-in-targets-key true null-in-targets-resource true null-in-targets-scalar true diff --git a/acceptance/bundle/null/null-from-include/output.txt b/acceptance/bundle/null/null-from-include/output.txt index 24d7a9cea7a..996420de4e2 100644 --- a/acceptance/bundle/null/null-from-include/output.txt +++ b/acceptance/bundle/null/null-from-include/output.txt @@ -22,8 +22,4 @@ Deployment complete! >>> print_telemetry_bool_values null-in-targets true -null-in-targets-complex false -null-in-targets-index false -null-in-targets-key false -null-in-targets-resource false null-in-targets-scalar true diff --git a/acceptance/bundle/null/scalar-field/output.txt b/acceptance/bundle/null/scalar-field/output.txt index c0d6235ffc5..9b56855783e 100644 --- a/acceptance/bundle/null/scalar-field/output.txt +++ b/acceptance/bundle/null/scalar-field/output.txt @@ -33,8 +33,4 @@ Deployment complete! >>> print_telemetry_bool_values null-in-targets true -null-in-targets-complex false -null-in-targets-index false -null-in-targets-key false -null-in-targets-resource false null-in-targets-scalar true diff --git a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt index e7575a70237..3fb5f331688 100644 --- a/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt +++ b/acceptance/bundle/telemetry/deploy-app-lifecycle-started/output.txt @@ -13,26 +13,6 @@ Deployment complete! "key": "null-in-targets", "value": false }, - { - "key": "null-in-targets-scalar", - "value": false - }, - { - "key": "null-in-targets-complex", - "value": false - }, - { - "key": "null-in-targets-key", - "value": false - }, - { - "key": "null-in-targets-index", - "value": false - }, - { - "key": "null-in-targets-resource", - "value": false - }, { "key": "local.cache.attempt", "value": true diff --git a/acceptance/bundle/telemetry/deploy-compute-type/output.txt b/acceptance/bundle/telemetry/deploy-compute-type/output.txt index 59697ffdf3e..73928c8a088 100644 --- a/acceptance/bundle/telemetry/deploy-compute-type/output.txt +++ b/acceptance/bundle/telemetry/deploy-compute-type/output.txt @@ -36,16 +36,6 @@ local.cache.hit true local.cache.miss true null-in-targets false null-in-targets false -null-in-targets-complex false -null-in-targets-complex false -null-in-targets-index false -null-in-targets-index false -null-in-targets-key false -null-in-targets-key false -null-in-targets-resource false -null-in-targets-resource false -null-in-targets-scalar false -null-in-targets-scalar false permissions_section_set false permissions_section_set false presets_name_prefix_is_set false diff --git a/acceptance/bundle/telemetry/deploy-experimental/output.txt b/acceptance/bundle/telemetry/deploy-experimental/output.txt index 977bac7fba9..5196863ec15 100644 --- a/acceptance/bundle/telemetry/deploy-experimental/output.txt +++ b/acceptance/bundle/telemetry/deploy-experimental/output.txt @@ -22,11 +22,6 @@ has_serverless_compute false local.cache.attempt true local.cache.miss true null-in-targets false -null-in-targets-complex false -null-in-targets-index false -null-in-targets-key false -null-in-targets-resource false -null-in-targets-scalar false permissions_section_set false presets_name_prefix_is_set false python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt index 3f9cba37576..08dd225d829 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/custom/output.txt @@ -18,11 +18,6 @@ has_serverless_compute false local.cache.attempt true local.cache.miss true null-in-targets false -null-in-targets-complex false -null-in-targets-index false -null-in-targets-key false -null-in-targets-resource false -null-in-targets-scalar false permissions_section_set false presets_name_prefix_is_set true python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt index 249e5b1e82b..bc5efa44507 100644 --- a/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt +++ b/acceptance/bundle/telemetry/deploy-name-prefix/mode-development/output.txt @@ -18,11 +18,6 @@ has_serverless_compute false local.cache.attempt true local.cache.miss true null-in-targets false -null-in-targets-complex false -null-in-targets-index false -null-in-targets-key false -null-in-targets-resource false -null-in-targets-scalar false permissions_section_set false presets_name_prefix_is_set true python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt index d03ad421f6b..85262c767fa 100644 --- a/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt +++ b/acceptance/bundle/telemetry/deploy-whl-artifacts/output.txt @@ -41,16 +41,6 @@ local.cache.hit true local.cache.miss true null-in-targets false null-in-targets false -null-in-targets-complex false -null-in-targets-complex false -null-in-targets-index false -null-in-targets-index false -null-in-targets-key false -null-in-targets-key false -null-in-targets-resource false -null-in-targets-resource false -null-in-targets-scalar false -null-in-targets-scalar false permissions_section_set false permissions_section_set false python_wheel_wrapper_is_set false diff --git a/acceptance/bundle/telemetry/deploy/out.telemetry.txt b/acceptance/bundle/telemetry/deploy/out.telemetry.txt index 2b92eb92f16..aab20a4bc45 100644 --- a/acceptance/bundle/telemetry/deploy/out.telemetry.txt +++ b/acceptance/bundle/telemetry/deploy/out.telemetry.txt @@ -46,26 +46,6 @@ "key": "null-in-targets", "value": false }, - { - "key": "null-in-targets-scalar", - "value": false - }, - { - "key": "null-in-targets-complex", - "value": false - }, - { - "key": "null-in-targets-key", - "value": false - }, - { - "key": "null-in-targets-index", - "value": false - }, - { - "key": "null-in-targets-resource", - "value": false - }, { "key": "local.cache.attempt", "value": true diff --git a/bundle/config/mutator/collect_null_telemetry.go b/bundle/config/mutator/collect_null_telemetry.go index f72eb076040..0ecb39cbfa8 100644 --- a/bundle/config/mutator/collect_null_telemetry.go +++ b/bundle/config/mutator/collect_null_telemetry.go @@ -24,10 +24,20 @@ func (*collectNullTelemetry) Name() string { func (*collectNullTelemetry) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { info := b.Config.NullInTargets() b.Metrics.SetBoolValue("null-in-targets", info.Any()) - b.Metrics.SetBoolValue("null-in-targets-scalar", info.Scalar) - b.Metrics.SetBoolValue("null-in-targets-complex", info.Complex) - b.Metrics.SetBoolValue("null-in-targets-key", info.MapKey) - b.Metrics.SetBoolValue("null-in-targets-index", info.ArrayIndex) - b.Metrics.SetBoolValue("null-in-targets-resource", info.Resource) + if info.Scalar { + b.Metrics.SetBoolValue("null-in-targets-scalar", true) + } + if info.Complex { + b.Metrics.SetBoolValue("null-in-targets-complex", true) + } + if info.MapKey { + b.Metrics.SetBoolValue("null-in-targets-key", true) + } + if info.ArrayIndex { + b.Metrics.SetBoolValue("null-in-targets-index", true) + } + if info.Resource { + b.Metrics.SetBoolValue("null-in-targets-resource", true) + } return nil }