From def682ff25930010944aa1c70f9d335e886f91fc Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Fri, 31 Jul 2026 14:51:06 +0000 Subject: [PATCH 1/7] config-remote-sync: record which state the run read in telemetry Co-authored-by: Isaac --- bundle/configsync/telemetry.go | 38 +++++++++ bundle/configsync/telemetry_test.go | 84 +++++++++++++++++++ cmd/bundle/config_remote_sync.go | 5 ++ .../protos/bundle_config_remote_sync.go | 22 +++++ 4 files changed, 149 insertions(+) diff --git a/bundle/configsync/telemetry.go b/bundle/configsync/telemetry.go index 3de6a34ed17..7f409994342 100644 --- a/bundle/configsync/telemetry.go +++ b/bundle/configsync/telemetry.go @@ -8,6 +8,7 @@ import ( "github.com/databricks/cli/bundle/config/engine" "github.com/databricks/cli/bundle/direct/dresources" + "github.com/databricks/cli/bundle/statemgmt" "github.com/databricks/cli/libs/log" "github.com/databricks/cli/libs/structs/structpath" "github.com/databricks/cli/libs/telemetry" @@ -51,10 +52,41 @@ type Stats struct { Restore RestoreStats + // Identity of the resource state the run actually read. Without these, a + // "no deployed resource with id" failure cannot be told apart from + // reading a stale local cache, another target's state, or no state at all. + StateSerial int64 + StateLineage string + StateSource string + StatesAvailableCount int64 + + // Number of --select-ids selectors passed in, and how many resolved to a + // deployed resource. matched=0 with selectors>0 is the hard-fail case. + SelectorCount int64 + SelectorMatchedCount int64 + ErrorMessage string ErrorCategory protos.BundleConfigRemoteSyncErrorCategory } +// CollectStateStats records which resource state the run read. Only the +// state's identity is recorded (serial, system-generated lineage UUID, +// local/remote origin, candidate count) — never a path or a target name. +func (s *Stats) CollectStateStats(desc *statemgmt.StateDesc) { + if desc == nil { + return + } + s.StateSerial = int64(desc.Serial) + s.StateLineage = desc.Lineage + s.StateSource = "remote" + if desc.IsLocal { + s.StateSource = "local" + } + // AllStates is only populated when at least one state was found; a + // synthesized empty state descriptor leaves it nil. + s.StatesAvailableCount = int64(len(desc.AllStates)) +} + // RestoreStats counts the variable-reference restorations that can leak a // current-target-scoped reference into a shared file. The safe paths (kept / // compound-realigned) are not counted. @@ -180,6 +212,12 @@ func (s *Stats) LogTelemetry(ctx context.Context) { FilesWrittenCount: s.FilesWrittenCount, RefsRetargeted: s.Restore.Retargeted, RefsFromSiblings: s.Restore.FromSiblings, + StateSerial: s.StateSerial, + StateLineage: s.StateLineage, + StateSource: s.StateSource, + StatesAvailableCount: s.StatesAvailableCount, + SelectorCount: s.SelectorCount, + SelectorMatchedCount: s.SelectorMatchedCount, ErrorMessage: s.ErrorMessage, ErrorCategory: s.ErrorCategory, }, diff --git a/bundle/configsync/telemetry_test.go b/bundle/configsync/telemetry_test.go index d2a3da537f8..79ca42d3dbc 100644 --- a/bundle/configsync/telemetry_test.go +++ b/bundle/configsync/telemetry_test.go @@ -1,10 +1,15 @@ package configsync import ( + "encoding/json" "testing" + "github.com/databricks/cli/bundle/config/engine" + "github.com/databricks/cli/bundle/statemgmt" "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/telemetry/protos" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCollectChangeStats(t *testing.T) { @@ -106,3 +111,82 @@ func TestRestoreStatsCountersNilSafe(t *testing.T) { s.incFromSiblings() }) } + +func TestCollectStateStats(t *testing.T) { + t.Run("remote state with candidates", func(t *testing.T) { + s := &Stats{} + s.CollectStateStats(&statemgmt.StateDesc{ + Serial: 7, + Lineage: "f1621b9c-6ccd-481b-854d-40fa4176e68c", + IsLocal: false, + AllStates: []*statemgmt.StateDesc{{}, {}}, + }) + assert.Equal(t, int64(7), s.StateSerial) + assert.Equal(t, "f1621b9c-6ccd-481b-854d-40fa4176e68c", s.StateLineage) + assert.Equal(t, "remote", s.StateSource) + assert.Equal(t, int64(2), s.StatesAvailableCount) + }) + + t.Run("local state wins", func(t *testing.T) { + s := &Stats{} + s.CollectStateStats(&statemgmt.StateDesc{Serial: 99, IsLocal: true}) + assert.Equal(t, int64(99), s.StateSerial) + assert.Equal(t, "local", s.StateSource) + }) + + t.Run("synthesized empty state reports no candidates", func(t *testing.T) { + // PullResourcesState synthesizes a descriptor with no AllStates when no + // state file was found; that must be visible as 0, not omitted. + s := &Stats{} + s.CollectStateStats(&statemgmt.StateDesc{IsLocal: true}) + assert.Equal(t, int64(0), s.StatesAvailableCount) + assert.Equal(t, int64(0), s.StateSerial) + assert.Empty(t, s.StateLineage) + }) + + t.Run("nil descriptor is a no-op", func(t *testing.T) { + s := &Stats{} + s.CollectStateStats(nil) + assert.Empty(t, s.StateSource) + }) +} + +// The new state/selector fields must reach the emitted payload, and must be +// omitted (not zero-valued noise) when the run recorded nothing. +func TestLogTelemetryPayloadIncludesStateAndSelectorFields(t *testing.T) { + s := &Stats{ + Engine: engine.EngineDirect, + StateSerial: 99, + StateLineage: "f1621b9c-6ccd-481b-854d-40fa4176e68c", + StateSource: "local", + StatesAvailableCount: 2, + SelectorCount: 3, + SelectorMatchedCount: 0, + } + payload, err := json.Marshal(protos.BundleConfigRemoteSyncEvent{ + Engine: string(s.Engine), + StateSerial: s.StateSerial, + StateLineage: s.StateLineage, + StateSource: s.StateSource, + StatesAvailableCount: s.StatesAvailableCount, + SelectorCount: s.SelectorCount, + SelectorMatchedCount: s.SelectorMatchedCount, + }) + require.NoError(t, err) + + got := string(payload) + assert.Contains(t, got, `"state_serial":99`) + assert.Contains(t, got, `"state_lineage":"f1621b9c-6ccd-481b-854d-40fa4176e68c"`) + assert.Contains(t, got, `"state_source":"local"`) + assert.Contains(t, got, `"states_available_count":2`) + assert.Contains(t, got, `"selector_count":3`) + // omitempty drops a zero matched-count; the failure is still identifiable + // from selector_count being present with no matched field. + assert.NotContains(t, got, "selector_matched_count") + + empty, err := json.Marshal(protos.BundleConfigRemoteSyncEvent{}) + require.NoError(t, err) + for _, f := range []string{"state_serial", "state_lineage", "state_source", "states_available_count", "selector_count"} { + assert.NotContains(t, string(empty), f) + } +} diff --git a/cmd/bundle/config_remote_sync.go b/cmd/bundle/config_remote_sync.go index e5231fef58a..8ec11eab382 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -75,6 +75,7 @@ Examples: }, PostStateFunc: func(ctx context.Context, b *bundle.Bundle, stateDesc *statemgmt.StateDesc) error { stats.Engine = stateDesc.Engine + stats.CollectStateStats(stateDesc) // Open the deployment state once and reuse it for both planning and // selector resolution (avoids reading the terraform snapshot twice). @@ -101,6 +102,7 @@ Examples: stats.CollectChangeStats(ctx, changes) if len(selectIDs) > 0 { + stats.SelectorCount = int64(len(selectIDs)) // Filter after planning, never before: the plan must cover every // resource so ${resources.*} references resolve; only the emitted // changes are restricted to the selected resources. @@ -108,6 +110,9 @@ Examples: if err != nil { return err } + // Selectors resolve to plan keys and are deduplicated, so this is + // the number of distinct resources that matched, not of selectors. + stats.SelectorMatchedCount = int64(len(selected)) changes = configsync.FilterChanges(changes, selected) } diff --git a/libs/telemetry/protos/bundle_config_remote_sync.go b/libs/telemetry/protos/bundle_config_remote_sync.go index 466ef3af0f9..49c4700ea2e 100644 --- a/libs/telemetry/protos/bundle_config_remote_sync.go +++ b/libs/telemetry/protos/bundle_config_remote_sync.go @@ -60,6 +60,28 @@ type BundleConfigRemoteSyncEvent struct { RefsRetargeted int64 `json:"refs_retargeted,omitempty"` RefsFromSiblings int64 `json:"refs_from_siblings,omitempty"` + // Identity of the resource state this run read, so a selector that matched + // nothing can be attributed: a stale local cache, a different target's or + // bundle's state, and no state at all are otherwise indistinguishable. + // + // StateSerial and StateLineage are the state file's own monotonic counter + // and its system-generated lineage UUID (uuid.New(), never user input). + // StateSource is "local" or "remote". StatesAvailableCount is how many of + // the four candidate state files (direct/terraform x local/remote) were + // found; 0 means the run proceeded against a synthesized empty state. + // No state paths and no target names are recorded. + StateSerial int64 `json:"state_serial,omitempty"` + StateLineage string `json:"state_lineage,omitempty"` + StateSource string `json:"state_source,omitempty"` + StatesAvailableCount int64 `json:"states_available_count,omitempty"` + + // Number of --select-ids selectors passed in, and how many resolved to a + // deployed resource in the state above. Zero matched with a non-zero count + // is the hard-failure case; a shortfall between them means some selectors + // were skipped as stale. + SelectorCount int64 `json:"selector_count,omitempty"` + SelectorMatchedCount int64 `json:"selector_matched_count,omitempty"` + // Scrubbed, truncated summary of the failure when the command exits with an // error. Privileged free-text (DATA_LABEL_USER_COMMANDS_RESPONSE, LPP-5543); // stays in-region and is stripped from centralized logfood. Unset on success. From 292c4b1772d672a36df97c4507db75177af68eaa Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Fri, 31 Jul 2026 15:44:46 +0000 Subject: [PATCH 2/7] emit informative zeros for state/selector counts; update acceptance snapshots --- .../config-remote-sync/job_fields/output.txt | 6 ++++- .../resolve_variables/output.txt | 6 ++++- .../resolve_variables/test.toml | 7 +++++ .../config-remote-sync-error/output.txt | 1 + .../config-remote-sync-recreate/output.txt | 6 ++++- .../config-remote-sync-save/output.txt | 6 ++++- .../telemetry/config-remote-sync/output.txt | 6 ++++- bundle/configsync/telemetry.go | 18 ++++++++----- bundle/configsync/telemetry_test.go | 19 +++++++------ cmd/bundle/config_remote_sync.go | 6 +++-- .../protos/bundle_config_remote_sync.go | 27 ++++++++++--------- 11 files changed, 74 insertions(+), 34 deletions(-) diff --git a/acceptance/bundle/config-remote-sync/job_fields/output.txt b/acceptance/bundle/config-remote-sync/job_fields/output.txt index 7e0b7fe07d4..bb59040af28 100644 --- a/acceptance/bundle/config-remote-sync/job_fields/output.txt +++ b/acceptance/bundle/config-remote-sync/job_fields/output.txt @@ -105,7 +105,11 @@ Resource: resources.jobs.my_job } ], "files_changed_count": 1, - "files_written_count": 1 + "files_written_count": 1, + "state_serial": 1, + "state_lineage": "[UUID]", + "state_source": "local", + "states_available_count": 2 } >>> [CLI] bundle destroy --auto-approve diff --git a/acceptance/bundle/config-remote-sync/resolve_variables/output.txt b/acceptance/bundle/config-remote-sync/resolve_variables/output.txt index 388f7ab3bbf..be026ad5807 100644 --- a/acceptance/bundle/config-remote-sync/resolve_variables/output.txt +++ b/acceptance/bundle/config-remote-sync/resolve_variables/output.txt @@ -134,7 +134,11 @@ Resource: resources.pipelines.my_pipeline "files_changed_count": 1, "files_written_count": 1, "refs_retargeted": 1, - "refs_from_siblings": 7 + "refs_from_siblings": 7, + "state_serial": [SERIAL], + "state_lineage": "[UUID]", + "state_source": "local", + "states_available_count": 2 } >>> [CLI] bundle destroy --auto-approve diff --git a/acceptance/bundle/config-remote-sync/resolve_variables/test.toml b/acceptance/bundle/config-remote-sync/resolve_variables/test.toml index fff2325bf22..c9156b947f2 100644 --- a/acceptance/bundle/config-remote-sync/resolve_variables/test.toml +++ b/acceptance/bundle/config-remote-sync/resolve_variables/test.toml @@ -13,3 +13,10 @@ DATABRICKS_BUNDLE_ENABLE_EXPERIMENTAL_YAML_SYNC = "true" [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["direct", "terraform"] + +# The state serial counts state writes, and the terraform path writes more times +# than the direct path for the same script, so the value differs per engine. +# The serial is not what this test asserts — normalize it. +[[Repls]] +Old = '"state_serial": [0-9]+' +New = '"state_serial": [SERIAL]' diff --git a/acceptance/bundle/telemetry/config-remote-sync-error/output.txt b/acceptance/bundle/telemetry/config-remote-sync-error/output.txt index 93457dc95e1..8c6ecc0b445 100644 --- a/acceptance/bundle/telemetry/config-remote-sync-error/output.txt +++ b/acceptance/bundle/telemetry/config-remote-sync-error/output.txt @@ -7,6 +7,7 @@ Exit code: 1 >>> cat out.requests.txt { "engine": "terraform", + "state_source": "local", "error_message": "state snapshot not available: resources state snapshot not found remotely at resources-config-sync-snapshot.json: state snapshot not found", "error_category": "STATE_NOT_FOUND" } diff --git a/acceptance/bundle/telemetry/config-remote-sync-recreate/output.txt b/acceptance/bundle/telemetry/config-remote-sync-recreate/output.txt index c002f652ed3..b1ba2f2d878 100644 --- a/acceptance/bundle/telemetry/config-remote-sync-recreate/output.txt +++ b/acceptance/bundle/telemetry/config-remote-sync-recreate/output.txt @@ -26,5 +26,9 @@ Resource: resources.pipelines.foo "replace_count": 1 } ], - "files_changed_count": 1 + "files_changed_count": 1, + "state_serial": 1, + "state_lineage": "[UUID]", + "state_source": "local", + "states_available_count": 2 } diff --git a/acceptance/bundle/telemetry/config-remote-sync-save/output.txt b/acceptance/bundle/telemetry/config-remote-sync-save/output.txt index ccefb19ce1b..2bd01880644 100644 --- a/acceptance/bundle/telemetry/config-remote-sync-save/output.txt +++ b/acceptance/bundle/telemetry/config-remote-sync-save/output.txt @@ -28,5 +28,9 @@ Resource: resources.jobs.foo } ], "files_changed_count": 1, - "files_written_count": 1 + "files_written_count": 1, + "state_serial": 1, + "state_lineage": "[UUID]", + "state_source": "local", + "states_available_count": 2 } diff --git a/acceptance/bundle/telemetry/config-remote-sync/output.txt b/acceptance/bundle/telemetry/config-remote-sync/output.txt index 8886bae291a..1034d9d525c 100644 --- a/acceptance/bundle/telemetry/config-remote-sync/output.txt +++ b/acceptance/bundle/telemetry/config-remote-sync/output.txt @@ -25,5 +25,9 @@ Resource: resources.jobs.foo "replace_count": 1 } ], - "files_changed_count": 1 + "files_changed_count": 1, + "state_serial": 1, + "state_lineage": "[UUID]", + "state_source": "local", + "states_available_count": 2 } diff --git a/bundle/configsync/telemetry.go b/bundle/configsync/telemetry.go index 7f409994342..8ee27f132e3 100644 --- a/bundle/configsync/telemetry.go +++ b/bundle/configsync/telemetry.go @@ -55,15 +55,18 @@ type Stats struct { // Identity of the resource state the run actually read. Without these, a // "no deployed resource with id" failure cannot be told apart from // reading a stale local cache, another target's state, or no state at all. - StateSerial int64 - StateLineage string - StateSource string - StatesAvailableCount int64 + StateSerial int64 + StateLineage string + StateSource string + // Pointers so an informative zero survives omitempty on serialization: no + // states found, and no selectors matched, are the two most diagnostic + // values this event can carry. + StatesAvailableCount *int64 // Number of --select-ids selectors passed in, and how many resolved to a // deployed resource. matched=0 with selectors>0 is the hard-fail case. - SelectorCount int64 - SelectorMatchedCount int64 + SelectorCount *int64 + SelectorMatchedCount *int64 ErrorMessage string ErrorCategory protos.BundleConfigRemoteSyncErrorCategory @@ -84,7 +87,8 @@ func (s *Stats) CollectStateStats(desc *statemgmt.StateDesc) { } // AllStates is only populated when at least one state was found; a // synthesized empty state descriptor leaves it nil. - s.StatesAvailableCount = int64(len(desc.AllStates)) + n := int64(len(desc.AllStates)) + s.StatesAvailableCount = &n } // RestoreStats counts the variable-reference restorations that can leak a diff --git a/bundle/configsync/telemetry_test.go b/bundle/configsync/telemetry_test.go index 79ca42d3dbc..21ad345be20 100644 --- a/bundle/configsync/telemetry_test.go +++ b/bundle/configsync/telemetry_test.go @@ -124,7 +124,8 @@ func TestCollectStateStats(t *testing.T) { assert.Equal(t, int64(7), s.StateSerial) assert.Equal(t, "f1621b9c-6ccd-481b-854d-40fa4176e68c", s.StateLineage) assert.Equal(t, "remote", s.StateSource) - assert.Equal(t, int64(2), s.StatesAvailableCount) + require.NotNil(t, s.StatesAvailableCount) + assert.Equal(t, int64(2), *s.StatesAvailableCount) }) t.Run("local state wins", func(t *testing.T) { @@ -139,7 +140,8 @@ func TestCollectStateStats(t *testing.T) { // state file was found; that must be visible as 0, not omitted. s := &Stats{} s.CollectStateStats(&statemgmt.StateDesc{IsLocal: true}) - assert.Equal(t, int64(0), s.StatesAvailableCount) + require.NotNil(t, s.StatesAvailableCount) + assert.Equal(t, int64(0), *s.StatesAvailableCount) assert.Equal(t, int64(0), s.StateSerial) assert.Empty(t, s.StateLineage) }) @@ -159,9 +161,9 @@ func TestLogTelemetryPayloadIncludesStateAndSelectorFields(t *testing.T) { StateSerial: 99, StateLineage: "f1621b9c-6ccd-481b-854d-40fa4176e68c", StateSource: "local", - StatesAvailableCount: 2, - SelectorCount: 3, - SelectorMatchedCount: 0, + StatesAvailableCount: ptr(int64(2)), + SelectorCount: ptr(int64(3)), + SelectorMatchedCount: ptr(int64(0)), } payload, err := json.Marshal(protos.BundleConfigRemoteSyncEvent{ Engine: string(s.Engine), @@ -180,9 +182,8 @@ func TestLogTelemetryPayloadIncludesStateAndSelectorFields(t *testing.T) { assert.Contains(t, got, `"state_source":"local"`) assert.Contains(t, got, `"states_available_count":2`) assert.Contains(t, got, `"selector_count":3`) - // omitempty drops a zero matched-count; the failure is still identifiable - // from selector_count being present with no matched field. - assert.NotContains(t, got, "selector_matched_count") + // The zero must survive: "no selector matched" is the hard-failure signal. + assert.Contains(t, got, `"selector_matched_count":0`) empty, err := json.Marshal(protos.BundleConfigRemoteSyncEvent{}) require.NoError(t, err) @@ -190,3 +191,5 @@ func TestLogTelemetryPayloadIncludesStateAndSelectorFields(t *testing.T) { assert.NotContains(t, string(empty), f) } } + +func ptr[T any](v T) *T { return &v } diff --git a/cmd/bundle/config_remote_sync.go b/cmd/bundle/config_remote_sync.go index 8ec11eab382..0b6ec426cea 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -102,7 +102,8 @@ Examples: stats.CollectChangeStats(ctx, changes) if len(selectIDs) > 0 { - stats.SelectorCount = int64(len(selectIDs)) + selectorCount := int64(len(selectIDs)) + stats.SelectorCount = &selectorCount // Filter after planning, never before: the plan must cover every // resource so ${resources.*} references resolve; only the emitted // changes are restricted to the selected resources. @@ -112,7 +113,8 @@ Examples: } // Selectors resolve to plan keys and are deduplicated, so this is // the number of distinct resources that matched, not of selectors. - stats.SelectorMatchedCount = int64(len(selected)) + matched := int64(len(selected)) + stats.SelectorMatchedCount = &matched changes = configsync.FilterChanges(changes, selected) } diff --git a/libs/telemetry/protos/bundle_config_remote_sync.go b/libs/telemetry/protos/bundle_config_remote_sync.go index 49c4700ea2e..a9186fa1603 100644 --- a/libs/telemetry/protos/bundle_config_remote_sync.go +++ b/libs/telemetry/protos/bundle_config_remote_sync.go @@ -66,21 +66,24 @@ type BundleConfigRemoteSyncEvent struct { // // StateSerial and StateLineage are the state file's own monotonic counter // and its system-generated lineage UUID (uuid.New(), never user input). - // StateSource is "local" or "remote". StatesAvailableCount is how many of - // the four candidate state files (direct/terraform x local/remote) were - // found; 0 means the run proceeded against a synthesized empty state. + // StateSource is "local" or "remote". // No state paths and no target names are recorded. - StateSerial int64 `json:"state_serial,omitempty"` - StateLineage string `json:"state_lineage,omitempty"` - StateSource string `json:"state_source,omitempty"` - StatesAvailableCount int64 `json:"states_available_count,omitempty"` + StateSerial int64 `json:"state_serial,omitempty"` + StateLineage string `json:"state_lineage,omitempty"` + StateSource string `json:"state_source,omitempty"` + + // How many of the four candidate state files (direct/terraform x + // local/remote) were found. A pointer so that zero — the run proceeded + // against a synthesized empty state, which is the most diagnostic value + // here — is emitted rather than dropped by omitempty. + StatesAvailableCount *int64 `json:"states_available_count,omitempty"` // Number of --select-ids selectors passed in, and how many resolved to a - // deployed resource in the state above. Zero matched with a non-zero count - // is the hard-failure case; a shortfall between them means some selectors - // were skipped as stale. - SelectorCount int64 `json:"selector_count,omitempty"` - SelectorMatchedCount int64 `json:"selector_matched_count,omitempty"` + // deployed resource in the state above. Pointers for the same reason: zero + // matched with a non-zero count is the hard-failure case, so the zero must + // survive serialization. Both nil when the command ran without --select-ids. + SelectorCount *int64 `json:"selector_count,omitempty"` + SelectorMatchedCount *int64 `json:"selector_matched_count,omitempty"` // Scrubbed, truncated summary of the failure when the command exits with an // error. Privileged free-text (DATA_LABEL_USER_COMMANDS_RESPONSE, LPP-5543); From ef214dca1aaca2ff41a1544a4acf37bb478ebfba Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Fri, 31 Jul 2026 16:08:36 +0000 Subject: [PATCH 3/7] report state and selected resource ids; drop derivable counters --- .../config-remote-sync/job_fields/output.txt | 7 +- .../resolve_variables/output.txt | 10 ++- bundle/configsync/select.go | 45 +++++----- bundle/configsync/telemetry.go | 81 +++++++++++++++-- bundle/configsync/telemetry_test.go | 87 ------------------- cmd/bundle/config_remote_sync.go | 13 +-- .../protos/bundle_config_remote_sync.go | 32 +++++-- 7 files changed, 143 insertions(+), 132 deletions(-) diff --git a/acceptance/bundle/config-remote-sync/job_fields/output.txt b/acceptance/bundle/config-remote-sync/job_fields/output.txt index bb59040af28..71e6a37921c 100644 --- a/acceptance/bundle/config-remote-sync/job_fields/output.txt +++ b/acceptance/bundle/config-remote-sync/job_fields/output.txt @@ -109,7 +109,12 @@ Resource: resources.jobs.my_job "state_serial": 1, "state_lineage": "[UUID]", "state_source": "local", - "states_available_count": 2 + "states_available_count": 2, + "state_resource_ids": { + "resource_job_ids": [ + "[MY_JOB_ID]" + ] + } } >>> [CLI] bundle destroy --auto-approve diff --git a/acceptance/bundle/config-remote-sync/resolve_variables/output.txt b/acceptance/bundle/config-remote-sync/resolve_variables/output.txt index be026ad5807..4dad29e392b 100644 --- a/acceptance/bundle/config-remote-sync/resolve_variables/output.txt +++ b/acceptance/bundle/config-remote-sync/resolve_variables/output.txt @@ -138,7 +138,15 @@ Resource: resources.pipelines.my_pipeline "state_serial": [SERIAL], "state_lineage": "[UUID]", "state_source": "local", - "states_available_count": 2 + "states_available_count": 2, + "state_resource_ids": { + "resource_job_ids": [ + "[MY_JOB_ID]" + ], + "resource_pipeline_ids": [ + "[MY_PIPELINE_ID]" + ] + } } >>> [CLI] bundle destroy --auto-approve diff --git a/bundle/configsync/select.go b/bundle/configsync/select.go index 443c94423ae..3c561fe9141 100644 --- a/bundle/configsync/select.go +++ b/bundle/configsync/select.go @@ -10,6 +10,30 @@ import ( "github.com/databricks/cli/libs/log" ) +// IndexDeployedResources indexes the deployed resources by ":", +// mapping to the plan key ("resources.."). Indexing by the +// component means a selector can only ever match a resource of that exact type, +// never an id that happens to collide across types. +func IndexDeployedResources(state *dstate.DeploymentState) map[string]string { + byTypeID := make(map[string]string) + for key := range state.Data.State { + id := state.GetResourceID(key) + if id == "" { + continue + } + typeAndName, ok := strings.CutPrefix(key, "resources.") + if !ok { + continue + } + resourceType, _, ok := strings.Cut(typeAndName, ".") + if !ok { + continue + } + byTypeID[resourceType+":"+id] = key + } + return byTypeID +} + // ResolveResourceSelectors maps ":" selectors to their plan keys // ("resources..") using the open deployment state (see // OpenDeploymentState). @@ -35,26 +59,7 @@ import ( // Duplicate selectors are deduplicated; the returned keys preserve the order in // which their selectors first appear. func ResolveResourceSelectors(ctx context.Context, state *dstate.DeploymentState, selectors []string) ([]string, error) { - // Index deployed resources by ":". State keys have the form - // "resources.."; indexing by the component means a - // selector can only ever match a resource of that exact type, never an id - // that happens to collide across types. - byTypeID := make(map[string]string) - for key := range state.Data.State { - id := state.GetResourceID(key) - if id == "" { - continue - } - typeAndName, ok := strings.CutPrefix(key, "resources.") - if !ok { - continue - } - resourceType, _, ok := strings.Cut(typeAndName, ".") - if !ok { - continue - } - byTypeID[resourceType+":"+id] = key - } + byTypeID := IndexDeployedResources(state) keys := make([]string, 0, len(selectors)) var missing []string diff --git a/bundle/configsync/telemetry.go b/bundle/configsync/telemetry.go index 8ee27f132e3..544535d4dd1 100644 --- a/bundle/configsync/telemetry.go +++ b/bundle/configsync/telemetry.go @@ -58,15 +58,14 @@ type Stats struct { StateSerial int64 StateLineage string StateSource string - // Pointers so an informative zero survives omitempty on serialization: no - // states found, and no selectors matched, are the two most diagnostic - // values this event can carry. + // Pointer so the informative zero (no state file found at all) survives + // omitempty on serialization. StatesAvailableCount *int64 - // Number of --select-ids selectors passed in, and how many resolved to a - // deployed resource. matched=0 with selectors>0 is the hard-fail case. - SelectorCount *int64 - SelectorMatchedCount *int64 + // Resource ids present in the state that was read, and the ids the run was + // asked to sync. Comparing the two classifies a selector miss. + StateResourceIDs *protos.BundleConfigRemoteSyncResourceIDs + SelectedResourceIDs *protos.BundleConfigRemoteSyncResourceIDs ErrorMessage string ErrorCategory protos.BundleConfigRemoteSyncErrorCategory @@ -190,6 +189,70 @@ func resourceTypeFromKey(resourceKey string) string { return parts[1] } +// resourceIDLimit caps how many ids of one type are reported. Mirrors +// phases.ResourceIdLimit: the telemetry upload has a short timeout, so a bundle +// with very many resources must not produce an unbounded payload. +const resourceIDLimit = 1000 + +// collectResourceIDs groups ":" pairs into the per-type id lists. +// Returns nil when nothing was collected so the field is omitted entirely. +func collectResourceIDs(typeAndIDs []string) *protos.BundleConfigRemoteSyncResourceIDs { + out := &protos.BundleConfigRemoteSyncResourceIDs{} + any := false + for _, typeAndID := range typeAndIDs { + resourceType, id, ok := strings.Cut(typeAndID, ":") + if !ok || id == "" { + continue + } + // Sub-resources (permissions/grants) are indexed under their parent's + // type with a path-shaped object id; those are not selectable and the + // path would be redacted, so skip them. + if strings.Contains(id, "/") { + continue + } + switch resourceType { + case "jobs": + if len(out.ResourceJobIDs) < resourceIDLimit { + out.ResourceJobIDs = append(out.ResourceJobIDs, id) + any = true + } + case "pipelines": + if len(out.ResourcePipelineIDs) < resourceIDLimit { + out.ResourcePipelineIDs = append(out.ResourcePipelineIDs, id) + any = true + } + case "clusters": + if len(out.ResourceClusterIDs) < resourceIDLimit { + out.ResourceClusterIDs = append(out.ResourceClusterIDs, id) + any = true + } + case "dashboards": + if len(out.ResourceDashboardIDs) < resourceIDLimit { + out.ResourceDashboardIDs = append(out.ResourceDashboardIDs, id) + any = true + } + } + } + if !any { + return nil + } + slices.Sort(out.ResourceJobIDs) + slices.Sort(out.ResourcePipelineIDs) + slices.Sort(out.ResourceClusterIDs) + slices.Sort(out.ResourceDashboardIDs) + return out +} + +// CollectSelectedIDs records the ids the run was asked to sync. +func (s *Stats) CollectSelectedIDs(selectors []string) { + s.SelectedResourceIDs = collectResourceIDs(selectors) +} + +// CollectStateIDs records the ids present in the state that was read. +func (s *Stats) CollectStateIDs(typeAndIDs []string) { + s.StateResourceIDs = collectResourceIDs(typeAndIDs) +} + // LogTelemetry emits the BundleConfigRemoteSyncEvent for this run. func (s *Stats) LogTelemetry(ctx context.Context) { defer recoverTelemetry(ctx) @@ -220,8 +283,8 @@ func (s *Stats) LogTelemetry(ctx context.Context) { StateLineage: s.StateLineage, StateSource: s.StateSource, StatesAvailableCount: s.StatesAvailableCount, - SelectorCount: s.SelectorCount, - SelectorMatchedCount: s.SelectorMatchedCount, + StateResourceIDs: s.StateResourceIDs, + SelectedResourceIDs: s.SelectedResourceIDs, ErrorMessage: s.ErrorMessage, ErrorCategory: s.ErrorCategory, }, diff --git a/bundle/configsync/telemetry_test.go b/bundle/configsync/telemetry_test.go index 21ad345be20..d2a3da537f8 100644 --- a/bundle/configsync/telemetry_test.go +++ b/bundle/configsync/telemetry_test.go @@ -1,15 +1,10 @@ package configsync import ( - "encoding/json" "testing" - "github.com/databricks/cli/bundle/config/engine" - "github.com/databricks/cli/bundle/statemgmt" "github.com/databricks/cli/libs/dyn" - "github.com/databricks/cli/libs/telemetry/protos" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestCollectChangeStats(t *testing.T) { @@ -111,85 +106,3 @@ func TestRestoreStatsCountersNilSafe(t *testing.T) { s.incFromSiblings() }) } - -func TestCollectStateStats(t *testing.T) { - t.Run("remote state with candidates", func(t *testing.T) { - s := &Stats{} - s.CollectStateStats(&statemgmt.StateDesc{ - Serial: 7, - Lineage: "f1621b9c-6ccd-481b-854d-40fa4176e68c", - IsLocal: false, - AllStates: []*statemgmt.StateDesc{{}, {}}, - }) - assert.Equal(t, int64(7), s.StateSerial) - assert.Equal(t, "f1621b9c-6ccd-481b-854d-40fa4176e68c", s.StateLineage) - assert.Equal(t, "remote", s.StateSource) - require.NotNil(t, s.StatesAvailableCount) - assert.Equal(t, int64(2), *s.StatesAvailableCount) - }) - - t.Run("local state wins", func(t *testing.T) { - s := &Stats{} - s.CollectStateStats(&statemgmt.StateDesc{Serial: 99, IsLocal: true}) - assert.Equal(t, int64(99), s.StateSerial) - assert.Equal(t, "local", s.StateSource) - }) - - t.Run("synthesized empty state reports no candidates", func(t *testing.T) { - // PullResourcesState synthesizes a descriptor with no AllStates when no - // state file was found; that must be visible as 0, not omitted. - s := &Stats{} - s.CollectStateStats(&statemgmt.StateDesc{IsLocal: true}) - require.NotNil(t, s.StatesAvailableCount) - assert.Equal(t, int64(0), *s.StatesAvailableCount) - assert.Equal(t, int64(0), s.StateSerial) - assert.Empty(t, s.StateLineage) - }) - - t.Run("nil descriptor is a no-op", func(t *testing.T) { - s := &Stats{} - s.CollectStateStats(nil) - assert.Empty(t, s.StateSource) - }) -} - -// The new state/selector fields must reach the emitted payload, and must be -// omitted (not zero-valued noise) when the run recorded nothing. -func TestLogTelemetryPayloadIncludesStateAndSelectorFields(t *testing.T) { - s := &Stats{ - Engine: engine.EngineDirect, - StateSerial: 99, - StateLineage: "f1621b9c-6ccd-481b-854d-40fa4176e68c", - StateSource: "local", - StatesAvailableCount: ptr(int64(2)), - SelectorCount: ptr(int64(3)), - SelectorMatchedCount: ptr(int64(0)), - } - payload, err := json.Marshal(protos.BundleConfigRemoteSyncEvent{ - Engine: string(s.Engine), - StateSerial: s.StateSerial, - StateLineage: s.StateLineage, - StateSource: s.StateSource, - StatesAvailableCount: s.StatesAvailableCount, - SelectorCount: s.SelectorCount, - SelectorMatchedCount: s.SelectorMatchedCount, - }) - require.NoError(t, err) - - got := string(payload) - assert.Contains(t, got, `"state_serial":99`) - assert.Contains(t, got, `"state_lineage":"f1621b9c-6ccd-481b-854d-40fa4176e68c"`) - assert.Contains(t, got, `"state_source":"local"`) - assert.Contains(t, got, `"states_available_count":2`) - assert.Contains(t, got, `"selector_count":3`) - // The zero must survive: "no selector matched" is the hard-failure signal. - assert.Contains(t, got, `"selector_matched_count":0`) - - empty, err := json.Marshal(protos.BundleConfigRemoteSyncEvent{}) - require.NoError(t, err) - for _, f := range []string{"state_serial", "state_lineage", "state_source", "states_available_count", "selector_count"} { - assert.NotContains(t, string(empty), f) - } -} - -func ptr[T any](v T) *T { return &v } diff --git a/cmd/bundle/config_remote_sync.go b/cmd/bundle/config_remote_sync.go index 0b6ec426cea..b3d1d82c4d5 100644 --- a/cmd/bundle/config_remote_sync.go +++ b/cmd/bundle/config_remote_sync.go @@ -5,7 +5,9 @@ import ( "encoding/json" "errors" "fmt" + "maps" "runtime" + "slices" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/configsync" @@ -101,9 +103,12 @@ Examples: } stats.CollectChangeStats(ctx, changes) + // Record the ids present in state and the ids requested: on failure + // they are what classifies the miss. + stats.CollectStateIDs(slices.Collect(maps.Keys(configsync.IndexDeployedResources(&deployBundle.StateDB)))) + if len(selectIDs) > 0 { - selectorCount := int64(len(selectIDs)) - stats.SelectorCount = &selectorCount + stats.CollectSelectedIDs(selectIDs) // Filter after planning, never before: the plan must cover every // resource so ${resources.*} references resolve; only the emitted // changes are restricted to the selected resources. @@ -111,10 +116,6 @@ Examples: if err != nil { return err } - // Selectors resolve to plan keys and are deduplicated, so this is - // the number of distinct resources that matched, not of selectors. - matched := int64(len(selected)) - stats.SelectorMatchedCount = &matched changes = configsync.FilterChanges(changes, selected) } diff --git a/libs/telemetry/protos/bundle_config_remote_sync.go b/libs/telemetry/protos/bundle_config_remote_sync.go index a9186fa1603..63d28f47a6b 100644 --- a/libs/telemetry/protos/bundle_config_remote_sync.go +++ b/libs/telemetry/protos/bundle_config_remote_sync.go @@ -74,16 +74,18 @@ type BundleConfigRemoteSyncEvent struct { // How many of the four candidate state files (direct/terraform x // local/remote) were found. A pointer so that zero — the run proceeded - // against a synthesized empty state, which is the most diagnostic value - // here — is emitted rather than dropped by omitempty. + // against a synthesized empty state — is emitted rather than dropped by + // omitempty. Not derivable from StateResourceIDs: a state file holding no + // resources and no state file at all both yield an empty id set. StatesAvailableCount *int64 `json:"states_available_count,omitempty"` - // Number of --select-ids selectors passed in, and how many resolved to a - // deployed resource in the state above. Pointers for the same reason: zero - // matched with a non-zero count is the hard-failure case, so the zero must - // survive serialization. Both nil when the command ran without --select-ids. - SelectorCount *int64 `json:"selector_count,omitempty"` - SelectorMatchedCount *int64 `json:"selector_matched_count,omitempty"` + // IDs of the resources found in the deployment state this run read, and the + // IDs the run was asked to sync via --select-ids. Comparing the two is what + // classifies a selector miss; the counts are intentionally not sent + // separately since they are len() of these lists, and the matched set is + // their intersection. + StateResourceIDs *BundleConfigRemoteSyncResourceIDs `json:"state_resource_ids,omitempty"` + SelectedResourceIDs *BundleConfigRemoteSyncResourceIDs `json:"selected_resource_ids,omitempty"` // Scrubbed, truncated summary of the failure when the command exits with an // error. Privileged free-text (DATA_LABEL_USER_COMMANDS_RESPONSE, LPP-5543); @@ -95,6 +97,20 @@ type BundleConfigRemoteSyncEvent struct { ErrorCategory BundleConfigRemoteSyncErrorCategory `json:"error_category,omitempty"` } +// BundleConfigRemoteSyncResourceIDs holds resource IDs grouped by type. The +// same shape is used for the state's IDs and for the selected IDs so the two +// can be compared directly. +// +// IDs of resources managed by the bundle. Some resources like volumes or schemas +// do not expose a numerical or UUID identifier and are tracked by name. Those +// resources are not tracked here since the names are PII. +type BundleConfigRemoteSyncResourceIDs struct { + ResourceJobIDs []string `json:"resource_job_ids,omitempty"` + ResourcePipelineIDs []string `json:"resource_pipeline_ids,omitempty"` + ResourceClusterIDs []string `json:"resource_cluster_ids,omitempty"` + ResourceDashboardIDs []string `json:"resource_dashboard_ids,omitempty"` +} + // BundleConfigRemoteSyncResourceChanges holds field-level change counts for a // single resource type within one config-remote-sync run. type BundleConfigRemoteSyncResourceChanges struct { From 4a7bab7afbd98890d8d9e1dae9bb1ac1c2773c28 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Fri, 31 Jul 2026 16:25:44 +0000 Subject: [PATCH 4/7] rename resource id message to match proto linter casing --- bundle/configsync/telemetry.go | 8 ++++---- libs/telemetry/protos/bundle_config_remote_sync.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bundle/configsync/telemetry.go b/bundle/configsync/telemetry.go index 544535d4dd1..c1783fb92d0 100644 --- a/bundle/configsync/telemetry.go +++ b/bundle/configsync/telemetry.go @@ -64,8 +64,8 @@ type Stats struct { // Resource ids present in the state that was read, and the ids the run was // asked to sync. Comparing the two classifies a selector miss. - StateResourceIDs *protos.BundleConfigRemoteSyncResourceIDs - SelectedResourceIDs *protos.BundleConfigRemoteSyncResourceIDs + StateResourceIDs *protos.BundleConfigRemoteSyncResourceIds + SelectedResourceIDs *protos.BundleConfigRemoteSyncResourceIds ErrorMessage string ErrorCategory protos.BundleConfigRemoteSyncErrorCategory @@ -196,8 +196,8 @@ const resourceIDLimit = 1000 // collectResourceIDs groups ":" pairs into the per-type id lists. // Returns nil when nothing was collected so the field is omitted entirely. -func collectResourceIDs(typeAndIDs []string) *protos.BundleConfigRemoteSyncResourceIDs { - out := &protos.BundleConfigRemoteSyncResourceIDs{} +func collectResourceIDs(typeAndIDs []string) *protos.BundleConfigRemoteSyncResourceIds { + out := &protos.BundleConfigRemoteSyncResourceIds{} any := false for _, typeAndID := range typeAndIDs { resourceType, id, ok := strings.Cut(typeAndID, ":") diff --git a/libs/telemetry/protos/bundle_config_remote_sync.go b/libs/telemetry/protos/bundle_config_remote_sync.go index 63d28f47a6b..3846539af2c 100644 --- a/libs/telemetry/protos/bundle_config_remote_sync.go +++ b/libs/telemetry/protos/bundle_config_remote_sync.go @@ -84,8 +84,8 @@ type BundleConfigRemoteSyncEvent struct { // classifies a selector miss; the counts are intentionally not sent // separately since they are len() of these lists, and the matched set is // their intersection. - StateResourceIDs *BundleConfigRemoteSyncResourceIDs `json:"state_resource_ids,omitempty"` - SelectedResourceIDs *BundleConfigRemoteSyncResourceIDs `json:"selected_resource_ids,omitempty"` + StateResourceIDs *BundleConfigRemoteSyncResourceIds `json:"state_resource_ids,omitempty"` + SelectedResourceIDs *BundleConfigRemoteSyncResourceIds `json:"selected_resource_ids,omitempty"` // Scrubbed, truncated summary of the failure when the command exits with an // error. Privileged free-text (DATA_LABEL_USER_COMMANDS_RESPONSE, LPP-5543); @@ -97,14 +97,14 @@ type BundleConfigRemoteSyncEvent struct { ErrorCategory BundleConfigRemoteSyncErrorCategory `json:"error_category,omitempty"` } -// BundleConfigRemoteSyncResourceIDs holds resource IDs grouped by type. The +// BundleConfigRemoteSyncResourceIds holds resource IDs grouped by type. The // same shape is used for the state's IDs and for the selected IDs so the two // can be compared directly. // // IDs of resources managed by the bundle. Some resources like volumes or schemas // do not expose a numerical or UUID identifier and are tracked by name. Those // resources are not tracked here since the names are PII. -type BundleConfigRemoteSyncResourceIDs struct { +type BundleConfigRemoteSyncResourceIds struct { ResourceJobIDs []string `json:"resource_job_ids,omitempty"` ResourcePipelineIDs []string `json:"resource_pipeline_ids,omitempty"` ResourceClusterIDs []string `json:"resource_cluster_ids,omitempty"` From 56d256b12e5c43b051b35efe56e519f50c88a566 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Fri, 31 Jul 2026 16:38:41 +0000 Subject: [PATCH 5/7] update terraform-engine telemetry snapshots --- .../bundle/telemetry/config-remote-sync-error/output.txt | 1 + .../telemetry/config-remote-sync-recreate/output.txt | 7 ++++++- .../bundle/telemetry/config-remote-sync-save/output.txt | 7 ++++++- acceptance/bundle/telemetry/config-remote-sync/output.txt | 7 ++++++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/acceptance/bundle/telemetry/config-remote-sync-error/output.txt b/acceptance/bundle/telemetry/config-remote-sync-error/output.txt index 8c6ecc0b445..2edf3bb39ca 100644 --- a/acceptance/bundle/telemetry/config-remote-sync-error/output.txt +++ b/acceptance/bundle/telemetry/config-remote-sync-error/output.txt @@ -8,6 +8,7 @@ Exit code: 1 { "engine": "terraform", "state_source": "local", + "states_available_count": 0, "error_message": "state snapshot not available: resources state snapshot not found remotely at resources-config-sync-snapshot.json: state snapshot not found", "error_category": "STATE_NOT_FOUND" } diff --git a/acceptance/bundle/telemetry/config-remote-sync-recreate/output.txt b/acceptance/bundle/telemetry/config-remote-sync-recreate/output.txt index b1ba2f2d878..cd4b55cd992 100644 --- a/acceptance/bundle/telemetry/config-remote-sync-recreate/output.txt +++ b/acceptance/bundle/telemetry/config-remote-sync-recreate/output.txt @@ -30,5 +30,10 @@ Resource: resources.pipelines.foo "state_serial": 1, "state_lineage": "[UUID]", "state_source": "local", - "states_available_count": 2 + "states_available_count": 2, + "state_resource_ids": { + "resource_pipeline_ids": [ + "[FOO_ID]" + ] + } } diff --git a/acceptance/bundle/telemetry/config-remote-sync-save/output.txt b/acceptance/bundle/telemetry/config-remote-sync-save/output.txt index 2bd01880644..6b15aa03063 100644 --- a/acceptance/bundle/telemetry/config-remote-sync-save/output.txt +++ b/acceptance/bundle/telemetry/config-remote-sync-save/output.txt @@ -32,5 +32,10 @@ Resource: resources.jobs.foo "state_serial": 1, "state_lineage": "[UUID]", "state_source": "local", - "states_available_count": 2 + "states_available_count": 2, + "state_resource_ids": { + "resource_job_ids": [ + "[NUMID]" + ] + } } diff --git a/acceptance/bundle/telemetry/config-remote-sync/output.txt b/acceptance/bundle/telemetry/config-remote-sync/output.txt index 1034d9d525c..ea1949a9058 100644 --- a/acceptance/bundle/telemetry/config-remote-sync/output.txt +++ b/acceptance/bundle/telemetry/config-remote-sync/output.txt @@ -29,5 +29,10 @@ Resource: resources.jobs.foo "state_serial": 1, "state_lineage": "[UUID]", "state_source": "local", - "states_available_count": 2 + "states_available_count": 2, + "state_resource_ids": { + "resource_job_ids": [ + "[FOO_ID]" + ] + } } From 004722958d0715ffb5c4a20f2f3b230d92efd33b Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Fri, 31 Jul 2026 18:33:23 +0000 Subject: [PATCH 6/7] document PII rationale for state identity fields --- bundle/configsync/telemetry.go | 3 +++ .../protos/bundle_config_remote_sync.go | 23 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/bundle/configsync/telemetry.go b/bundle/configsync/telemetry.go index c1783fb92d0..45a620685b7 100644 --- a/bundle/configsync/telemetry.go +++ b/bundle/configsync/telemetry.go @@ -74,6 +74,9 @@ type Stats struct { // CollectStateStats records which resource state the run read. Only the // state's identity is recorded (serial, system-generated lineage UUID, // local/remote origin, candidate count) — never a path or a target name. +// +// None of these can contain PII: the lineage is an opaque random UUID minted by +// the state layer, and the source is one of two fixed literals assigned here. func (s *Stats) CollectStateStats(desc *statemgmt.StateDesc) { if desc == nil { return diff --git a/libs/telemetry/protos/bundle_config_remote_sync.go b/libs/telemetry/protos/bundle_config_remote_sync.go index 3846539af2c..56c368714ca 100644 --- a/libs/telemetry/protos/bundle_config_remote_sync.go +++ b/libs/telemetry/protos/bundle_config_remote_sync.go @@ -64,13 +64,24 @@ type BundleConfigRemoteSyncEvent struct { // nothing can be attributed: a stale local cache, a different target's or // bundle's state, and no state at all are otherwise indistinguishable. // - // StateSerial and StateLineage are the state file's own monotonic counter - // and its system-generated lineage UUID (uuid.New(), never user input). - // StateSource is "local" or "remote". - // No state paths and no target names are recorded. - StateSerial int64 `json:"state_serial,omitempty"` + // StateSerial is the state file's own monotonic counter. + StateSerial int64 `json:"state_serial,omitempty"` + + // StateLineage is the state's lineage identifier. It cannot contain PII: it + // is an opaque random UUIDv4 minted by dstate.GetOrInitLineage (uuid.New(), + // so no MAC address or timestamp) and is never derived from a user, + // workspace, path, or resource name. For terraform-engine bundles the value + // is generated by Terraform itself and only parsed through. StateLineage string `json:"state_lineage,omitempty"` - StateSource string `json:"state_source,omitempty"` + + // StateSource is "local" or "remote" — a closed, system-defined set of + // exactly two values assigned in CollectStateStats, never user input, so it + // cannot contain PII. + StateSource string `json:"state_source,omitempty"` + + // No state paths and no target names are recorded: the target is + // user-authored configuration, and BundleDeployEvent likewise records only + // the target count. // How many of the four candidate state files (direct/terraform x // local/remote) were found. A pointer so that zero — the run proceeded From 5f537fb71fbbd0e7ca2a4617df0b4c5a9a85f70c Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Fri, 31 Jul 2026 20:02:43 +0000 Subject: [PATCH 7/7] trim non-factual detail from state field comments --- bundle/configsync/telemetry.go | 2 +- libs/telemetry/protos/bundle_config_remote_sync.go | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bundle/configsync/telemetry.go b/bundle/configsync/telemetry.go index 45a620685b7..44117608c69 100644 --- a/bundle/configsync/telemetry.go +++ b/bundle/configsync/telemetry.go @@ -76,7 +76,7 @@ type Stats struct { // local/remote origin, candidate count) — never a path or a target name. // // None of these can contain PII: the lineage is an opaque random UUID minted by -// the state layer, and the source is one of two fixed literals assigned here. +// the state layer, and the source is one of two fixed literals. func (s *Stats) CollectStateStats(desc *statemgmt.StateDesc) { if desc == nil { return diff --git a/libs/telemetry/protos/bundle_config_remote_sync.go b/libs/telemetry/protos/bundle_config_remote_sync.go index 56c368714ca..56405b11869 100644 --- a/libs/telemetry/protos/bundle_config_remote_sync.go +++ b/libs/telemetry/protos/bundle_config_remote_sync.go @@ -68,10 +68,8 @@ type BundleConfigRemoteSyncEvent struct { StateSerial int64 `json:"state_serial,omitempty"` // StateLineage is the state's lineage identifier. It cannot contain PII: it - // is an opaque random UUIDv4 minted by dstate.GetOrInitLineage (uuid.New(), - // so no MAC address or timestamp) and is never derived from a user, - // workspace, path, or resource name. For terraform-engine bundles the value - // is generated by Terraform itself and only parsed through. + // is an opaque random UUID minted by dstate.GetOrInitLineage and is never + // derived from a user, workspace, path, or resource name. StateLineage string `json:"state_lineage,omitempty"` // StateSource is "local" or "remote" — a closed, system-defined set of